资讯详情 Context Mode 重构 AI 编码代理:MCP 与 SQLite 实现上下文精准管理
📅 2026/10/26 14:32:58
1. 为什么上下文成了 AI 编码代理最贵的资源先抛一个我自己的实测数据。前阵子我在一个中型 TypeScript 项目里跑一个编码代理让它连续完成读需求 → 定位文件 → 改代码 → 跑测试 → 修报错这一整条链路。单次任务平均要消耗 6 到 9 万 token 的上下文其中真正被写进最终代码的有效信息不到 8%。剩下的 92% 是什么是重复读取的文件内容、被截断的日志、来回确认的对话历史、以及代理自己忘了又重读的循环。这就是 Context Mode 想解决的核心问题AI 编码代理的上下文不是不够用而是被浪费得太厉害。传统做法是把所有东西一股脑塞进对话窗口——文件全文、终端输出、历史消息。窗口一满要么截断丢信息要么压缩丢精度要么重开一轮丢状态。Context Mode 换了个思路把上下文从对话流里抽出来变成一个可查询、可持久化、可增量更新的外部状态。代理不再记住一切而是按需检索。关键词里的 MCP、SQLite 就是这套思路的两个支点。MCPModel Context Protocol负责让代理用标准化接口去访问外部上下文源SQLite 负责把上下文落盘成一个轻量、可查询、可版本化的本地库。两者一结合代理的记忆就从易失的对话窗口变成了一个可以反复读写的结构化存储。这篇文章我会拆四件事Context Mode 到底重构了什么、MCP 在其中的角色、SQLite 作为上下文存储的工程细节、以及我在实际落地时踩过的坑和验证过的配置。适合正在做编码代理、或者被上下文窗口折磨过的开发者看。2. Context Mode 重构的到底是什么从对话记忆到外部状态2.1 传统上下文管理的三个死结要理解 Context Mode 的价值得先看清老路子为什么走不通。我把常见问题归成三类第一线性累积导致的上下文通胀。对话式代理的上下文是一条只增不减的线。每读一个文件、每跑一次命令内容就追加到历史里。任务做到一半窗口里塞满了已经没用的中间产物。我见过一个极端案例代理为了改一个 20 行的函数前面累积了 4 万 token 的无关文件内容最后真正相关的上下文被挤到了窗口边缘模型注意力直接涣散。第二截断与压缩的精度损失。窗口满了之后常见策略是滑动窗口截断或者摘要压缩。截断会把早期关键约束比如这个字段不能为 null丢掉摘要压缩则会把精确的代码片段变成模糊描述代理据此改代码错误率飙升。这两种都是用信息换空间本质上是亏本买卖。第三状态无法跨会话延续。代理跑完一轮进程一关所有上下文归零。下次接着做得从头再读一遍项目。对于长周期任务这个重复成本高得离谱。Context Mode 的破局点在于它不把上下文当成对话的属性而是当成项目的属性。上下文独立于任何一次对话存在代理只是它的一个消费者。2.2 Context Mode 的核心抽象上下文即外部状态用一句话概括 Context Mode 的范式转变代理不再携带上下文而是查询上下文。具体来说它把上下文拆成几个可独立管理的维度维度传统对话模式Context Mode存储位置对话窗口易失外部存储持久访问方式全量塞入按需检索更新粒度追加整段增量写入生命周期单次会话跨会话容量上限受窗口限制受存储限制精度截断/压缩后失真原始内容保留这个表里最关键的一行是访问方式。传统模式是我有什么就全给你看Context Mode 是你要什么我就给你查什么。前者是被动灌输后者是主动检索。打个比方。传统对话代理像一个记性很好但不会做笔记的人所有事都记在脑子里脑子一满就开始忘。Context Mode 像一个随身带笔记本的人脑子里只放当前正在处理的那一页需要查历史就翻笔记本。笔记本可以无限厚脑子始终清爽。2.3 为什么这个转变对编码场景特别重要编码任务有个鲜明特点信息密度极不均匀且高度依赖精确文本。一个项目里真正需要代理反复查看的可能就是几个核心文件但代理为了理解全局往往会读一大堆边缘文件。这些边缘文件在传统模式下会永久占据上下文而在 Context Mode 下它们被存进外部库只在真正被引用时才被拉进来。更关键的是代码的精确性要求。摘要压缩对自然语言还行对代码是灾难——压成let压成var一个字符的差异就是 bug。Context Mode 保留原始文本检索出来的是什么代理看到的就是什么没有中间商赚差价。我在一个重构任务里对比过两种模式传统对话模式下代理改完 5 个文件后开始记混变量名需要人工纠正 3 次切到 Context Mode 后代理每次改文件前都从外部库拉取该文件的最新版本全程零混淆。这个差异不是模型能力问题纯粹是上下文管理方式的问题。3. MCP 在 Context Mode 里扮演的角色标准化的上下文出入口3.1 MCP 是什么为什么编码代理需要它MCPModel Context Protocol是一套让模型与外部工具、数据源交互的标准化协议。你可以把它理解成AI 世界的 USB 接口——不管外接的是数据库、文件系统还是某个 API只要遵循 MCP模型就能用统一的方式去调用。在 Context Mode 的架构里MCP 解决的是一个很实际的问题上下文存储和代理之间需要一个稳定的、与具体模型无关的接口。如果没有 MCP每个代理框架都得自己定义一套怎么读上下文、怎么写上下文的私有协议。换个模型、换个框架整套对接逻辑就得重写。MCP 把这层抽象出来存储端只需要实现一次 MCP Server任何支持 MCP 的代理都能接进来。这对编码代理尤其重要因为编码场景要接的外部源特别杂文件系统、Git 历史、终端输出、测试报告、甚至 IDE 的符号索引。MCP 让这些源都能以统一的资源和工具形式暴露给代理。3.2 Context Mode 中 MCP 的两类核心能力在 Context Mode 的语境下MCP 主要提供两类能力我分开说。第一类是 Resources资源读取。代理通过 MCP 读取外部上下文比如读取当前项目的文件树读取某个文件的指定行范围读取最近一次测试的失败用例。这类操作是只读的目的是把外部状态拉进代理的推理过程。第二类是 Tools工具调用。代理通过 MCP 执行有副作用的操作比如把这段上下文写入存储更新某个文件的状态标记触发一次增量索引。这类操作会改变外部状态。Context Mode 的巧妙之处在于它把上下文管理本身也做成了一组 MCP 工具。代理不只是消费上下文还能主动管理上下文——决定什么该存、什么该丢、什么该标记为已处理。3.3 一个典型的 MCP 交互流程我拿一个真实场景走一遍方便你建立直觉。假设代理要修改user-service.ts里的一个校验逻辑代理先调用 MCP 工具search_context查询user-service 校验相关的上下文条目。MCP Server 在 SQLite 里做一次全文检索返回匹配的文件片段和行号。代理拿到片段判断需要看完整文件调用read_resource读取该文件。代理修改代码调用write_context把已修改 user-service.ts 的校验逻辑这条状态写回存储。代理跑测试测试输出通过 MCP 工具append_log写入存储标记为关联任务 X。下一轮对话时代理不需要重新读文件直接search_context就能拿到最新状态。整个流程里代理的对话窗口始终只装着当前这一步需要的信息历史全部沉淀在 SQLite 里。这就是 Context Mode 的运转方式。提示MCP 的 Resources 和 Tools 边界要划清楚。只读的走 Resources有副作用的走 Tools。混用会导致代理行为不可预测比如它以为在读、实际在写。3.4 MCP 带来的可组合性MCP 还有个容易被忽略的好处可组合。因为接口标准化了你可以把多个 MCP Server 串起来用。比如一个 Server 管代码上下文SQLite一个 Server 管浏览器自动化Playwright MCP一个 Server 管数据库查询DB Browser for SQLite 那类工具。代理面对的是统一的 MCP 接口背后挂多少个数据源它不关心。这让 Context Mode 可以从小规模起步——先接一个 SQLite 上下文库——再逐步扩展而不用推倒重来。4. SQLite 作为上下文存储为什么是它怎么用4.1 选 SQLite 而不是向量库或内存的理由一提到上下文存储很多人第一反应是向量数据库。但 Context Mode 这类场景SQLite 往往是更务实的选择。我把理由列清楚理由一编码上下文需要精确检索不是语义检索。向量检索擅长意思相近但编码场景经常需要精确匹配——找某个函数名、某个变量、某个文件路径。SQLite 的全文检索FTS5加上精确的 WHERE 条件比向量相似度靠谱得多。理由二零运维、单文件、可版本化。SQLite 就是一个.db文件不需要起服务、不需要配连接池。你可以把它放进项目目录跟代码一起版本管理。这对本地优先的编码代理太友好了。理由三事务保证一致性。代理并发写入上下文时SQLite 的事务机制能保证不写坏。向量库在这块往往要额外处理。理由四查询能力完整。上下文管理需要的不只是存和取还有按时间排序按任务分组按状态过滤统计某类上下文的大小。这些用 SQL 一句话搞定用向量库得写一堆胶水代码。当然SQLite 不是万能的。如果上下文需要跨机器共享、或者写入量极大那得上正经的服务端数据库。但对单机编码代理SQLite 的性价比几乎无出其右。4.2 上下文表结构设计我实际用的表结构大概是这样简化后给你参考-- 上下文条目主表 CREATE TABLE context_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, -- 关联的任务标识 entry_type TEXT NOT NULL, -- file / log / note / diff source_path TEXT, -- 文件类条目的路径 content TEXT NOT NULL, -- 原始内容不压缩 content_hash TEXT NOT NULL, -- 内容哈希用于去重 token_estimate INTEGER, -- 预估 token 数用于预算控制 status TEXT DEFAULT active, -- active / archived / consumed created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); -- 全文检索索引 CREATE VIRTUAL TABLE context_fts USING fts5( content, source_path, contentcontext_entries, content_rowidid ); -- 任务表管理上下文归属 CREATE TABLE tasks ( id TEXT PRIMARY KEY, description TEXT, created_at INTEGER NOT NULL, status TEXT DEFAULT running );几个设计要点值得展开说。content字段存原始内容不做任何压缩。这是 Context Mode 的底线——精度不能丢。空间不够就归档不能压缩。content_hash用于去重。代理经常重复读取同一个文件哈希一样就直接复用不重复写入。我实测这一项能省掉 30% 以上的冗余存储。token_estimate是预算控制的关键。代理在决定要不要把这条上下文拉进窗口时先看它的 token 估算值避免一次性拉太多撑爆窗口。status字段实现上下文的生命周期管理。active是当前任务在用的archived是历史任务留下的consumed是已经处理完、可以安全丢弃的。4.3 增量更新与去重逻辑Context Mode 相比传统模式最大的工程优势是增量更新。代理不需要每次重写整个上下文只写变化的部分。去重逻辑我用的是一套哈希比对 版本标记的组合import hashlib import sqlite3 import time def upsert_context(conn, task_id, entry_type, source_path, content): content_hash hashlib.sha256(content.encode()).hexdigest() cur conn.cursor() # 先查是否已存在相同内容 cur.execute( SELECT id FROM context_entries WHERE content_hash ? AND source_path ?, (content_hash, source_path) ) row cur.fetchone() if row: # 内容没变只更新访问时间 cur.execute( UPDATE context_entries SET updated_at ? WHERE id ?, (int(time.time()), row[0]) ) return row[0] # 内容变了插入新版本 cur.execute( INSERT INTO context_entries (task_id, entry_type, source_path, content, content_hash, token_estimate, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, active, ?, ?), (task_id, entry_type, source_path, content, content_hash, len(content) // 4, int(time.time()), int(time.time())) ) return cur.lastrowid这段代码的核心是内容没变就不写内容变了就新增版本。这样代理每次读文件后写回不会产生重复条目但历史版本也保留着需要回溯时能查到。len(content) // 4是个粗糙的 token 估算英文代码大致 4 字符 1 token。要精确的话可以接 tokenizer但估算值用于预算控制已经够用。4.4 检索策略怎么让代理查得准存储只是基础检索才是 Context Mode 的价值兑现点。我总结了几种检索模式按使用频率排序模式一按任务 类型过滤。最常见代理说给我当前任务下所有 file 类型的上下文直接走索引。模式二全文检索。代理说找所有提到 validateUser 的上下文走 FTS5。模式三按路径前缀。代理说给我 src/services 下的所有上下文走source_path LIKE src/services/%。模式四按时间窗口。代理说给我最近 10 分钟写入的上下文用于追踪当前操作序列。这几种模式可以组合。比如当前任务下、最近 5 分钟、提到 validateUser 的文件上下文一条 SQL 就能表达。这种组合查询能力是向量库很难优雅提供的。注意FTS5 默认对中文分词不友好。如果你的上下文里有中文注释要么配tokenizeunicode61要么在写入前做预处理。我踩过这个坑中文注释搜不出来排查了半天。5. 落地 Context Mode 时我踩过的坑5.1 坑一把上下文库当成什么都往里塞的垃圾桶我一开始的想法很朴素既然存储便宜那就把所有东西都存进去。结果两周后库膨胀到 2GB检索变慢代理反而因为候选太多而选错上下文。问题出在没有做上下文的价值分级。不是所有信息都值得持久化。我的修正方案是引入写入白名单文件内容只存被实际修改或引用的文件不存全项目扫描结果。日志只存错误和警告正常输出丢弃。对话只存决策结论不存来回确认过程。白名单一上库体积降了 70%检索准确率反而升了。上下文管理的核心不是存得多而是存得准。5.2 坑二MCP 工具调用没有幂等性导致重复写入MCP 工具调用可能因为网络、超时等原因重试。如果write_context不是幂等的重试就会写入重复条目。我最初的实现没考虑这点结果代理一次任务里同一个文件被写了 5 遍。修复方法是给写入操作加上基于content_hash的幂等判断——就是 4.3 节那段代码的逻辑。任何会被代理反复调用的 MCP 工具都必须设计成幂等的这是硬性要求。5.3 坑三上下文检索结果没有排序代理拿到一堆无关内容早期我的检索直接返回所有匹配项没排序。代理拿到 50 条结果其中 45 条是无关的它得自己筛。这既浪费 token 又容易出错。后来我加了多级排序先按任务相关性当前任务优先再按时间新鲜度最后按内容匹配度。排序后代理通常只需要看前 5 条。这个改动让单次检索的平均 token 消耗降了 60%。5.4 坑四忽略了 SQLite 的并发写入限制SQLite 默认是单写者模型。代理如果并发跑多个子任务同时写上下文会撞上database is locked。解决方案有两个一是开启 WAL 模式PRAGMA journal_modeWAL允许读写并发二是把写入串行化用一个队列缓冲。我两个都用了WAL 打底队列兜底。开启 WAL 后并发写入的报错基本消失。PRAGMA journal_modeWAL; PRAGMA synchronousNORMAL; PRAGMA busy_timeout5000;busy_timeout设成 5000 毫秒遇到锁时自动等待重试而不是立刻报错。这三行配置是我每个 Context Mode 项目的标配。5.5 坑五上下文没有过期清理库无限增长即使做了白名单库还是会慢慢长大。我加了一个定期清理任务把statusconsumed且超过 7 天的条目归档到冷存储主库只留活跃上下文。清理策略要谨慎不能一刀切删。我的做法是先归档再删除归档文件保留 30 天确认没问题再彻底清。这样万一代理需要回溯还有救。6. 一套可复现的 Context Mode 最小实现6.1 环境准备与依赖这套最小实现只需要 Python 标准库加一个 SQLitePython 自带sqlite3零外部依赖。如果你想接 MCP再装一个 MCP 的 Python SDK。# 确认 SQLite 版本FTS5 需要 3.9 以上 sqlite3 --version # 如果系统没装 sqlite3 命令行工具Linux 下常见 sudo apt-get install sqlite3Python 侧不需要额外安装sqlite3是标准库。MCP SDK 按需装pip install mcp6.2 初始化上下文库import sqlite3 def init_context_db(db_pathcontext.db): conn sqlite3.connect(db_path) conn.execute(PRAGMA journal_modeWAL) conn.execute(PRAGMA synchronousNORMAL) conn.execute(PRAGMA busy_timeout5000) conn.executescript( CREATE TABLE IF NOT EXISTS context_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, entry_type TEXT NOT NULL, source_path TEXT, content TEXT NOT NULL, content_hash TEXT NOT NULL, token_estimate INTEGER, status TEXT DEFAULT active, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_task ON context_entries(task_id); CREATE INDEX IF NOT EXISTS idx_path ON context_entries(source_path); CREATE INDEX IF NOT EXISTS idx_status ON context_entries(status); CREATE VIRTUAL TABLE IF NOT EXISTS context_fts USING fts5( content, source_path, contentcontext_entries, content_rowidid, tokenizeunicode61 ); ) conn.commit() return conntokenizeunicode61是为了让中文注释也能被检索到这是我踩坑后加的。6.3 写入与检索的完整闭环写入用 4.3 节的upsert_context。检索我封装成一个函数def search_context(conn, task_idNone, keywordNone, path_prefixNone, entry_typeNone, limit10): sql SELECT id, entry_type, source_path, content, token_estimate \ FROM context_entries WHERE statusactive params [] if task_id: sql AND task_id ? params.append(task_id) if entry_type: sql AND entry_type ? params.append(entry_type) if path_prefix: sql AND source_path LIKE ? params.append(path_prefix %) if keyword: sql AND id IN (SELECT rowid FROM context_fts WHERE context_fts MATCH ?) params.append(keyword) sql ORDER BY updated_at DESC LIMIT ? params.append(limit) return conn.execute(sql, params).fetchall()这个函数把前面说的几种检索模式都覆盖了。代理调用时按需传参不用关心底层 SQL。6.4 接 MCP 的封装思路把上面的函数包成 MCP 工具核心是定义好工具的输入 schema。以search_context为例from mcp.server import Server from mcp.types import Tool, TextContent server Server(context-mode) server.list_tools() async def list_tools(): return [ Tool( namesearch_context, description按任务、关键词、路径检索上下文, inputSchema{ type: object, properties: { task_id: {type: string}, keyword: {type: string}, path_prefix: {type: string}, limit: {type: integer, default: 10} } } ) ] server.call_tool() async def call_tool(name, arguments): if name search_context: rows search_context(conn, **arguments) return [TextContent(typetext, textformat_rows(rows))]关键点是inputSchema要写清楚代理靠它理解怎么调用。schema 越明确代理调用越准。6.5 验证闭环是否跑通跑通的标准很简单代理能在不重读文件的情况下靠检索拿到它需要的上下文。我的验证方法是让代理完成一个改两个文件 跑测试的任务然后检查对话窗口的 token 消耗。传统模式下这个任务要 5 万 token 以上Context Mode 下应该能压到 1.5 万以内。如果没压下来说明检索没生效代理还在全量读文件。7. 上下文预算控制让代理学会省着用7.1 为什么需要预算控制即使有了外部存储代理还是会把检索结果拉进对话窗口。如果一次拉太多窗口照样爆。所以 Context Mode 必须配一套上下文预算控制。核心思路给每次检索设定 token 上限超了就分批拉或者只拉摘要索引。7.2 基于 token_estimate 的预算分配我在检索函数里加了一层预算过滤def search_with_budget(conn, budget4000, **kwargs): rows search_context(conn, **kwargs) selected [] used 0 for row in rows: est row[4] or 0 if used est budget: break selected.append(row) used est return selected, used代理先声明我这次最多用 4000 token 的上下文函数按顺序装填装不下就停。这样代理永远知道自己拉了多少不会失控。7.3 分级加载先索引后内容更精细的做法是分级加载。第一级只拉上下文索引——每条只有路径、类型、token 估算不拉内容。代理看完索引决定要哪几条再发起第二级请求拉具体内容。这个模式能把首次检索的 token 消耗压到极低。我实测一个 200 条上下文的项目索引级检索只花 800 token代理据此精准选中 3 条第二级拉内容花 2000 token总共 2800 token 搞定。传统模式全量读要 6 万 token。7.4 预算超限时的降级策略预算不够时怎么办我设了三档降级第一档只返回匹配度最高的前 N 条N 随预算动态调整。第二档返回内容截断版保留头尾中间省略并标记已截断。第三档只返回索引让代理自己决定是否追加预算。降级策略要显式告诉代理我降级了否则代理会以为拿到的是完整内容据此推理就出错。8. 从单机到协作Context Mode 的扩展边界8.1 多代理共享上下文库Context Mode 的一个自然延伸是多代理共享同一个上下文库。比如一个代理负责写代码一个负责跑测试一个负责审查。它们通过同一个 SQLite 库交换上下文而不是通过对话传递。这带来一个新问题并发写入的冲突。我的处理方式是给每条上下文加owner字段写入时带上代理标识读取时按需过滤。冲突严重的场景可以给每个代理分配独立的表定期合并。8.2 上下文库的版本化把context.db纳入 Git 管理是个双刃剑。好处是上下文可追溯坏处是二进制文件 diff 不友好。我的折中方案是主库不进 Git但定期导出为 JSON 快照进 Git。快照只包含active状态的条目体积可控且 diff 可读。sqlite3 context.db .mode json \ SELECT * FROM context_entries WHERE statusactive \ context_snapshot.json这样既能追溯上下文演变又不污染仓库。8.3 什么时候该放弃 SQLiteSQLite 不是终点。以下几种情况我会考虑换掉它上下文需要跨机器实时共享SQLite 的文件锁扛不住。写入量达到每秒上千条单文件写入成为瓶颈。需要复杂的权限控制SQLite 的权限模型太粗。这些场景下把存储层换成服务端数据库MCP 接口保持不变代理侧零改动。这正是 MCP 抽象层的价值——存储可替换接口不变化。9. 我在实际项目里验证出的几条经验第一条也是最反直觉的一条上下文库的价值不在于存了多少而在于检索命中率。我早期追求大而全结果代理检索时噪声太多。后来把库做小、做精命中率上去了代理表现反而更好。衡量指标很简单——代理每次检索后实际用到的条目占返回条目的比例。这个比例低于 30%说明检索该优化了。第二条MCP 工具的 description 写得越具体代理调用越准。我一开始 description 写得很泛代理经常传错参数。后来把每个参数的用途、取值范围、默认值都写清楚调用错误率降了一大截。别小看这段文字它是代理理解工具的唯一入口。第三条SQLite 的 WAL 模式和 busy_timeout 是必配不是可选。我见过太多人因为没配这两个在并发场景下被database is locked折磨。三行 PRAGMA省下无数排查时间。第四条上下文清理要有冷静期。别一处理完就删留几天缓冲。代理有时候会在后续任务里回溯之前的上下文删早了就得重读得不偿失。第五条token 估算宁大勿小。估算偏小会导致预算超支代理拉进来的内容撑爆窗口估算偏大只是浪费一点预算空间。两害相权宁可估大。这套 Context Mode 的落地方式我从最初的什么都塞迭代到现在的精准检索 预算控制前后改了五六版。每次改动都对应一个具体的踩坑。如果你也在做编码代理建议从最小实现起步先跑通写入 - 检索 - 预算这个闭环再逐步加 MCP 封装和多代理共享。别一上来就追求大而全上下文管理这件事做减法比做加法难也更有价值。