news 2026/9/25 4:17:01

Context Mode 实战:用 SQLite FTS5 与 MCP 构建上下文窗口管理骨架

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Context Mode 实战:用 SQLite FTS5 与 MCP 构建上下文窗口管理骨架

1. 当 CLI 工具把上下文窗口当成垃圾桶

用 Claude Code 或者类似的 CLI 编程助手写代码,半小时后模型开始重复、跑偏、甚至输出乱码,这个场景我猜你大概率遇到过。问题往往不在模型本身,而在上下文窗口被工具调用的原始输出塞满了。一次 Playwright 快照 56 KB,20 个 GitHub issue 59 KB,500 行访问日志 45 KB,跑 30 分钟,四成窗口就被这些「噪音」占掉,模型自然开始健忘。

Context Mode 这个思路的核心,就是不让原始数据直接进上下文。它在工具调用时启动一个隔离子进程,只捕获 stdout,把完整原始数据落到本地 SQLite FTS5 数据库里,然后只把一小段摘要或者检索句柄返回给模型。模型需要细节时,再通过检索把相关片段召回。这样上下文窗口里放的是「索引卡片」,而不是「整本会议纪要」。

这篇要落地的是 CLI 场景下的上下文窗口管理骨架:以 SQLite FTS5 做检索底座,MCP 做工具接入层,给出可复制的 config.toml 与 settings.json,并演示一次检索验证动作。目标很明确——让长上下文按需召回,而不是全量塞入。适合正在用 CLI 编程助手、被上下文烧光困扰、想自己搭一套骨架的开发者。

2. 前置准备:TaoToken 接入与本地环境

在动手写配置之前,先把模型接入层准备好。我这边用的是 TaoToken 的 API 作为模型调用入口,它兼容常见的 OpenAI 风格接口,CLI 工具和 MCP 服务都能直接对接。你需要先去控制台拿一个 API Key,然后确认本地已经有 Python 3.10+ 和 sqlite3 命令行工具。

拿 Key 的路径很简单:打开 https://taotoken.net/api-keys ,创建一个新 Key,复制保存。注意这个 Key 只在创建时完整显示一次,丢了就得重建。如果你还没注册,可以从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 进去,注册后在控制台里能看到用量和余额。

环境侧确认三件事:python3 --version输出 3.10 以上;sqlite3 --version能正常输出;pip install mcp能装上 MCP 的 Python SDK。SQLite 的 FTS5 模块在大多数发行版里是默认编译进去的,验证方式是进 sqlite3 交互界面执行PRAGMA compile_options;,输出里能看到ENABLE_FTS5就说明可用。如果没看到,需要换一个带 FTS5 的 SQLite 构建,或者用 Python 的sqlite3模块连接时检查sqlite3.sqlite_version。

模型侧建议先用对话接口验证 Key 是否可用,打开 https://taotoken.net/models 可以看当前支持的模型列表。CLI 场景下我一般选响应快、上下文大的模型做主力,具体型号按你实际任务挑。这一步不用纠结太久,Key 能通、模型能回话,就可以进入配置环节。

3. 可复制配置:config.toml 与 settings.json 骨架

整个骨架分两层:config.toml 管 CLI 工具和检索库的参数,settings.json 管 MCP 服务的注册和工具暴露。先建目录结构,我习惯放在~/.context-mode/下:

mkdir -p ~/.context-mode/{db,scripts,logs} cd ~/.context-mode

然后是 config.toml,这是检索底座和沙盒行为的核心配置:

# ~/.context-mode/config.toml [storage] db_path = "~/.context-mode/db/context.db" fts_table = "context_fts" max_snippet_bytes = 512 retention_days = 7 [sandbox] enabled = true capture = "stdout" max_raw_bytes = 5242880 timeout_seconds = 30 [retrieval] default_limit = 5 bm25_weights = { title = 3.0, body = 1.0 } snippet_tokens = 64 [model] api_base = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" default_model = "claude-sonnet"

几个参数说明一下。max_snippet_bytes控制返回给模型的摘要上限,512 字节大约 100 多个汉字,够模型判断相关性。max_raw_bytes是单次工具输出落库的上限,超过就截断并标记,防止一个巨大日志把库撑爆。bm25_weights里 title 权重给到 3.0,是因为工具名和命令名往往比正文更能定位内容。

接着是 settings.json,这是 MCP 服务的注册文件,Claude Code 和多数 CLI 助手都认这个格式:

{ "mcpServers": { "context-mode": { "command": "python3", "args": ["~/.context-mode/scripts/server.py"], "env": { "CONTEXT_MODE_CONFIG": "~/.context-mode/config.toml", "TAOTOKEN_API_KEY": "sk-your-key-here" } } } }

注意TAOTOKEN_API_KEY这里直接写明文只适合本地开发,生产环境建议用环境变量注入或者系统钥匙串。command和args指向我们接下来要写的 MCP server 脚本。这个 server 的职责就三件事:接收工具调用、把原始输出写进 FTS5、返回摘要和检索句柄。

初始化数据库的 SQL 也一并给你,跑一次就行:

-- ~/.context-mode/scripts/init.sql CREATE VIRTUAL TABLE IF NOT EXISTS context_fts USING fts5( tool_name, command, body, created_at UNINDEXED, raw_path UNINDEXED, tokenize = 'porter unicode61' );

tokenize = 'porter unicode61'是关键,porter 词干化让 running、runs、ran 归到同一词根,unicode61 处理中文和符号。created_at和raw_path不参与索引,只做元数据。执行sqlite3 ~/.context-mode/db/context.db < ~/.context-mode/scripts/init.sql就建好了。

4. MCP Server 实现与一次检索验证

MCP server 用 Python SDK 写,核心逻辑是拦截工具输出、落库、返回摘要。下面是一个最小可跑的骨架:

# ~/.context-mode/scripts/server.py import os, sqlite3, hashlib, subprocess, tomllib from pathlib import Path from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent cfg = tomllib.loads(Path(os.path.expanduser( os.environ["CONTEXT_MODE_CONFIG"])).read_text()) DB = os.path.expanduser(cfg["storage"]["db_path"]) def store(tool, cmd, body): raw_dir = Path(DB).parent / "raw" raw_dir.mkdir(exist_ok=True) h = hashlib.sha1(body.encode()).hexdigest()[:16] raw_path = raw_dir / f"{h}.txt" raw_path.write_text(body) conn = sqlite3.connect(DB) conn.execute( "INSERT INTO context_fts(tool_name,command,body,created_at,raw_path)" " VALUES(?,?,?,datetime('now'),?)", (tool, cmd, body[:cfg["sandbox"]["max_raw_bytes"]], str(raw_path))) conn.commit(); conn.close() return h def search(query, limit=5): conn = sqlite3.connect(DB) rows = conn.execute( "SELECT tool_name, command, snippet(context_fts,2,'[',']', '...',64)," " bm25(context_fts,3.0,1.0) FROM context_fts" " WHERE context_fts MATCH ? ORDER BY bm25(context_fts,3.0,1.0) LIMIT ?", (query, limit)).fetchall() conn.close() return rows app = Server("context-mode") @app.list_tools() async def tools(): return [ Tool(name="run_and_index", description="执行命令并索引输出", inputSchema={"type":"object","properties":{ "command":{"type":"string"}},"required":["command"]}), Tool(name="recall", description="按关键词召回历史输出", inputSchema={"type":"object","properties":{ "query":{"type":"string"}},"required":["query"]}), ] @app.call_tool() async def call(name, args): if name == "run_and_index": out = subprocess.run(args["command"], shell=True, capture_output=True, text=True, timeout=cfg["sandbox"]["timeout_seconds"]).stdout h = store("cli", args["command"], out) return [TextContent(type="text", text=f"已索引 {len(out)} 字节,句柄 {h},用 recall 检索细节")] if name == "recall": rows = search(args["query"]) text = "\n".join(f"[{r[0]}] {r[1]} :: {r[2]}" for r in rows) return [TextContent(type="text", text=text or "无匹配")] raise ValueError(name) async def main(): async with stdio_server() as (r, w): await app.run(r, w, app.create_initialization_options()) if __name__ == "__main__": import asyncio; asyncio.run(main())

这段代码里run_and_index执行命令、把 stdout 落库、只返回字节数和句柄;recall用 FTS5 的MATCH加bm25排序召回,snippet()函数直接生成带高亮的片段。模型看到的是「已索引 56KB,句柄 abc123」,需要细节时再调 recall。

验证动作分两步。先手动灌一条数据,确认 FTS5 检索通:

sqlite3 ~/.context-mode/db/context.db \ "INSERT INTO context_fts(tool_name,command,body,created_at,raw_path) VALUES('cli','gh issue list','running tests failed on auth module',datetime('now'),'/tmp/x');" sqlite3 ~/.context-mode/db/context.db \ "SELECT tool_name, snippet(context_fts,2,'[',']','...',32) FROM context_fts WHERE context_fts MATCH 'run';"

第二条命令应该返回cli|running tests failed on [auth] module这样的结果,注意run匹配到了running,说明 porter 词干化生效。然后启动 MCP server,在 CLI 助手里调用recall工具,query 传auth,应该能召回同一条。这一步通了,整条链路就活了。

5. 本篇常见错排查

FTS5 报no such module: fts5。这是 SQLite 构建没带 FTS5。先PRAGMA compile_options;确认,没有就换构建。Python 用户可以用pysqlite3-binary替代标准库,它自带 FTS5。macOS 自带的 sqlite3 通常没问题,Linux 上某些精简发行版需要装libsqlite3-dev后重编。

MCP server 启动后 CLI 里看不到工具。九成是 settings.json 路径没展开。~在 JSON 里不会自动展开,要么写绝对路径,要么在 server.py 里用os.path.expanduser处理。另外确认command指向的 python3 就是装了 mcp SDK 的那个,虚拟环境里要写全路径。

recall 返回空但数据明明在库里。检查 MATCH 的查询语法。FTS5 默认把空格当 AND,auth module会要求两个词都出现。想模糊匹配用auth OR module,想前缀匹配用auth*。中文检索要注意 unicode61 分词对连续中文的处理,必要时在写入前做分词预处理。

落库的 body 被截断导致检索不到尾部内容。这是max_raw_bytes在起作用,原始文件其实完整存在raw_path里。召回时如果 snippet 不够,可以加一个fetch_raw工具按句柄读原文,但要注意别把原文又整个塞回上下文,只读需要的行区间。

API Key 报 401。确认TAOTOKEN_API_KEY环境变量在 MCP server 进程里可见。settings.json 的 env 块只对该 server 生效,如果你在 shell 里 export 了但 server 没读到,检查是不是用了不同的 shell 会话。Key 本身可以在 https://taotoken.net/api-keys 重新生成一个对比测试。

6. 把骨架跑起来之后

这套骨架跑通后,你会发现上下文窗口的占用曲线明显平缓了。原来跑 30 分钟就告急的会话,现在能撑到两三个小时,因为进窗口的只有摘要和检索结果,原始数据都沉在 SQLite 里。需要回溯细节时,模型自己调 recall 就行,不用你手动贴日志。

如果你主要做长期编码或者 Agent 任务,建议把 Coding Plan 也配上,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,它针对长会话场景做了额度优化。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有 MCP 对接的完整参数说明。想先验证模型对话是否正常,直接开 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 试一轮就行。

最后留一个我踩过的坑:FTS5 的snippet()函数参数顺序容易记混,它是snippet(表名, 列索引, 前缀, 后缀, 省略符, token 数),列索引从 0 开始。我一开始把列索引写成 1,结果高亮打在了 command 列上,排查了半天。你写的时候直接照抄上面的 SQL 就不会错。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 4:15:37

醋理大师糟粕醋口碑好吗,规模怎么样

一碗酸辣鲜香的糟粕醋火锅&#xff0c;正在从海南的街头巷尾走向全国餐桌。社交平台上&#xff0c;关于这道风味的话题热度持续攀升&#xff0c;越来越多的餐饮门店把它写进菜单&#xff0c;越来越多的外地食客开始好奇这口令人念念不忘的酸。然而热潮之下&#xff0c;真实的困…

作者头像 李华
网站建设 2026/9/25 4:13:05

应用类加载器全解析:从双亲委派到依赖冲突排查

先从一个很常见的现象说起。不知道你有没有遇到过这种情况&#xff1a;一个依赖明明已经放进去了&#xff0c;ClassNotFoundException却还是无情地砸下来&#xff1b;或者两个同名的类在项目里都存在&#xff0c;程序却“诡异地”加载了其中某一个&#xff0c;你翻遍代码也找不…

作者头像 李华
网站建设 2026/9/25 4:12:56

IntelliJ IDEA插件开发实战:菜单、弹窗与右键交互源码解析

简介&#xff1a;这是一份面向IntelliJ IDEA插件开发初学者与进阶者的详细源码示例&#xff0c;围绕插件结构、事件监听、Action系统、Dialog与Popup交互以及Swing组件应用等核心知识点展开&#xff0c;帮助开发者在较短时间内理解IDE扩展机制并上手实践。压缩包共16个文件&…

作者头像 李华
网站建设 2026/9/25 4:12:54

ETH多链密钥碰撞工具V2.01解析:私钥推导、地址生成与安全验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 4:11:45

日志信息==

从完整堆栈可以看出&#xff0c;问题出在 Desugar&#xff08;脱糖&#xff09;过程中&#xff1a; 核心问题&#xff1a;DesugaringGraphs.forVariant() 在处理 Java 8 特性降级时&#xff0c;需要 ASM7 支持 触发位置&#xff1a;DexArchiveBuilderTask 执行 DEX 构建时&…

作者头像 李华