1. 从"上下文爆炸"说起:AI 编码代理为什么需要一次范式重构
如果你最近半年深度用过任何一款 AI 编码代理,大概率都经历过这样的场景:一开始它表现得像个靠谱的结对程序员,能准确理解你的意图、给出符合项目风格的代码;但随着对话轮次增加,它开始"失忆"——忘记你三小时前定下的命名规范,忘记某个模块不能引入新依赖,甚至开始重复修改已经修好的 bug。你不得不反复把同样的背景信息粘贴进去,token 消耗像流水一样涨,而它的表现却越来越飘。
这不是模型能力的问题,而是上下文管理范式的问题。绝大多数 AI 编码代理采用的是"全量对话历史 + 检索片段"的朴素策略:把整个会话记录塞进上下文窗口,再按相似度捞几段相关代码。这套做法在短会话里勉强能用,一旦项目规模上去、会话拉长,就会同时踩中三个坑——上下文窗口被无关信息挤爆、关键约束被稀释、跨会话状态无法延续。
Context Mode 这个项目,本质上就是冲着这三个坑去的。它把"上下文"从一段被动的文本流,重新定义为一个可查询、可分层、可持久化的结构化状态,底层用 SQLite 做存储,通过 MCP(Model Context Protocol)把这份状态暴露给编码代理调用。换句话说,它不再让代理"读一大段历史",而是让代理"按需查询它真正需要的那部分记忆"。
这篇文章适合三类人看:一是正在自建 AI 编码工作流、被上下文问题折磨的工程师;二是想搞清楚 MCP 到底能干什么、怎么落地的开发者;三是对"代理记忆系统"这个方向感兴趣、想动手复现一套的人。我会从范式差异讲起,拆到 SQLite 表结构设计、MCP 工具接口、检索策略,再补上我在实际搭建中踩过的坑和调优经验。全程给可抄的配置和代码,不玩虚的。
2. Context Mode 与传统上下文注入的本质差异
2.1 传统方案:把历史当"文本"塞进去
先明确传统方案到底怎么做的,才能理解 Context Mode 改了什么。主流编码代理的上下文组装逻辑大致是这样:维护一个消息数组,每轮把用户输入、模型输出、工具调用结果依次 append 进去;当总长度接近模型窗口上限时,触发截断或摘要——要么丢掉最早的消息,要么用一个小模型把旧对话压缩成一段摘要。
这套逻辑的问题在于,它把所有信息都当成同质的文本。你三小时前随口说的一句"这个项目用 pnpm 不用 npm",和刚刚贴进来的一段报错日志,在上下文里是平等的。模型没有任何机制知道哪条是长期约束、哪条是一次性噪音。结果就是:真正重要的约束被淹没在大量工具调用日志里,而截断策略又往往优先砍掉最早的消息——恰恰是最早那批消息里藏着项目级的约定。
我实测过一个中等规模的 TypeScript 项目,会话进行到 40 轮左右时,代理对"禁止使用 any 类型"这条约束的遵守率从最初的 95% 掉到了 60% 上下。不是它不想遵守,是这条约束在上下文里的"信号强度"被稀释了。
2.2 Context Mode:把上下文当"数据库"来查
Context Mode 的核心思路完全不同。它不再维护一条线性的消息流,而是把上下文拆成若干结构化的实体,存进 SQLite,然后通过 MCP 暴露一组查询接口。代理在需要的时候,主动去查"当前项目的编码规范是什么""这个文件最近改过哪些地方""上次会话我们卡在哪个问题上"。
这个转变的意义在于三点:
- 持久化:上下文不再随会话结束而消失,跨会话、跨天、跨工具都能复用同一份记忆。
- 可检索:代理按需拉取,而不是被动接收全量历史,token 花在刀刃上。
- 可分层:项目级约束、会话级状态、任务级细节可以分表存储,检索时按优先级组合。
打个比方,传统方案像是把整本会议纪要每次开会都重读一遍;Context Mode 像是建了一个带索引的档案库,需要哪份调哪份。前者受限于"能读多少",后者受限于"会不会查"——而查询能力,恰恰是 MCP 要解决的问题。
2.3 为什么是 SQLite 而不是向量库
很多人第一反应是:上下文检索不该用向量数据库吗?Context Mode 偏偏选了 SQLite,这个选择值得说道说道。
向量库擅长的是语义模糊匹配——你问"怎么处理用户登录",它能召回语义相近的代码片段。但编码代理真正高频需要的,往往是精确的结构化查询:"这个文件的所有导出函数""最近 24 小时修改过的文件""标记为 todo 的任务"。这类查询用 SQL 一句话搞定,用向量库反而别扭,还得额外维护 embedding 的时效性。
SQLite 的优势很实在:单文件、零运维、事务可靠、查询快。十万条记录量级的上下文数据,加好索引后单次查询基本在毫秒级,完全够用。而且它天然支持全文检索(FTS5 扩展),语义和关键词两条路都能走。对于个人开发者和小团队来说,引入一个向量库的运维成本,远高于它带来的收益。
提示:如果你的上下文规模真的到了千万级、且强依赖语义召回,可以在 SQLite 之上再叠一层向量索引做混合检索。但对 90% 的编码代理场景,SQLite 单打独斗就够了。
3. 拆开 Context Mode 的存储层:SQLite 表结构怎么设计
3.1 四张核心表撑起整个记忆系统
Context Mode 的存储层设计得相当克制,核心就四张表,但每张都对应一类明确的上下文语义。我把它整理成下面这张对照表,方便你理解每张表"管什么":
| 表名 | 存储内容 | 典型查询场景 | 生命周期 |
|---|---|---|---|
projects | 项目元信息、根路径、技术栈 | 会话启动时定位当前项目 | 长期 |
constraints | 编码规范、依赖约束、禁用项 | 生成代码前校验合规性 | 长期 |
sessions | 会话摘要、进行中的任务状态 | 跨会话恢复工作现场 | 中期 |
artifacts | 文件片段、代码块、报错日志 | 按需召回具体内容 | 短期可归档 |
这个分层的逻辑是按"信息衰减速度"来切。项目约束几乎不变,放最稳的表;会话状态几天内有效;具体代码片段和日志时效性最强,用完可以归档。检索时按这个优先级组合,就能在有限的上下文预算里塞进最有价值的信息。
3.2 constraints 表:把"口头约定"变成"可查询规则"
这是我认为整个设计里最妙的一张表。传统方案里,"用 pnpm 不用 npm"这种约定只存在于对话历史里,随时可能被截断。Context Mode 把它抽成一条结构化记录:
CREATE TABLE constraints ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, category TEXT NOT NULL, -- 'dependency' | 'style' | 'architecture' rule TEXT NOT NULL, -- '使用 pnpm 作为包管理器' severity TEXT DEFAULT 'warn', -- 'block' | 'warn' | 'info' created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id) ); CREATE INDEX idx_constraints_project ON constraints(project_id, category);关键在于severity字段。block级别的约束(比如"禁止引入 GPL 协议依赖")在代理生成代码前必须强制校验,warn级别只做提醒。这样代理在动手前可以先查一遍约束表,而不是等生成完了再靠人肉 review 发现违规。
我自己的实践是,把团队 code review 里反复出现的意见,逐条沉淀成 constraints 记录。跑了两周之后,代理在"命名规范"和"错误处理风格"这两类问题上的返工率明显下降——因为它每次动手前都会查一遍规则,而不是靠记忆。
3.3 artifacts 表与 FTS5 全文索引
artifacts 表存的是最"重"的内容——代码片段、报错日志、文档摘录。这类内容的特点是量大、时效性强、需要模糊召回。所以 Context Mode 给它配了 FTS5 全文索引:
CREATE VIRTUAL TABLE artifacts_fts USING fts5( content, file_path, content='artifacts', content_rowid='id' );FTS5 的好处是查询快且支持分词。代理想找"上次那个关于 token 过期的报错",直接SELECT ... WHERE artifacts_fts MATCH 'token 过期'就能召回,不需要 embedding 那一套。实测十万条 artifact 记录,带 FTS5 索引的关键词查询稳定在 10ms 以内,比走向量检索快一个数量级。
注意:FTS5 默认的分词器对中文支持一般,如果你的项目注释和日志以中文为主,建议编译 SQLite 时启用
unicode61分词器,或者用trigram分词器做子串匹配。这个坑我踩过,默认配置下中文查询召回率惨不忍睹。
3.4 会话状态的"快照 + 增量"策略
sessions 表的设计有个细节值得单独说。如果每轮对话都往表里写一条完整快照,数据量会爆炸;如果只写增量,恢复现场时又得重放整个会话。Context Mode 采用的是周期性快照 + 增量日志的折中:
- 每 N 轮(默认 10 轮)写一条完整会话摘要快照;
- 快照之间只记录增量事件(新增约束、修改文件、标记任务);
- 恢复时先加载最近快照,再重放其后的增量。
这个策略和数据库的 WAL(预写日志)思路如出一辙,兼顾了恢复速度和存储成本。我在一个持续三天的长会话里验证过,恢复现场耗时稳定在 200ms 以内,而如果全量重放,得几秒钟。
4. MCP 接口层:代理到底怎么"查"上下文
4.1 MCP 是什么,为什么它适合干这件事
MCP(Model Context Protocol)是一套让 AI 应用与外部数据源、工具对接的开放协议。你可以把它理解成"给模型用的 USB 接口"——只要按协议实现一个 server,任何支持 MCP 的客户端(编码代理、IDE 插件、桌面工具)都能调用它暴露的能力。
它适合 Context Mode 的原因很直接:上下文查询本质上是"代理主动发起的工具调用",而 MCP 就是为工具调用而生的。代理不需要知道底层是 SQLite 还是别的什么,它只需要知道"有一个叫query_constraints的工具,传 project_id 就能拿到约束列表"。这种解耦让存储层可以随便换,接口层保持稳定。
4.2 Context Mode 暴露的核心工具集
一个设计良好的 MCP server,工具数量要克制——工具太多,模型反而选不明白。Context Mode 暴露的核心工具我梳理成下面这张表:
| 工具名 | 入参 | 返回 | 使用时机 |
|---|---|---|---|
get_project_context | project_path | 项目元信息 + 约束摘要 | 会话启动 |
query_constraints | project_id, category? | 约束列表 | 生成代码前 |
search_artifacts | query, limit? | 相关代码/日志片段 | 需要召回具体内容 |
save_session_state | session_id, summary | 确认写入 | 会话关键节点 |
recall_session | project_id, days? | 历史会话摘要 | 跨会话恢复 |
注意get_project_context这个入口工具的设计——它一次性返回项目元信息和约束摘要,而不是让代理分两次查。这是为了减少往返次数,因为每次工具调用都有延迟和 token 成本。把"会话启动必查"的信息打包成一个工具,是 MCP server 设计里很实用的一个模式。
4.3 一个最小可用的 MCP server 实现
下面这段是基于 Python 的 MCP server 骨架,展示query_constraints工具怎么接 SQLite。省略了协议层的样板代码,只保留核心逻辑:
import sqlite3 from mcp.server import Server from mcp.types import Tool, TextContent app = Server("context-mode") DB_PATH = "./context.db" def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn @app.list_tools() async def list_tools(): return [ Tool( name="query_constraints", description="查询指定项目的编码约束规则", inputSchema={ "type": "object", "properties": { "project_id": {"type": "integer"}, "category": {"type": "string"} }, "required": ["project_id"] } ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "query_constraints": conn = get_db() sql = "SELECT rule, severity FROM constraints WHERE project_id = ?" params = [arguments["project_id"]] if arguments.get("category"): sql += " AND category = ?" params.append(arguments["category"]) rows = conn.execute(sql, params).fetchall() conn.close() result = "\n".join(f"[{r['severity']}] {r['rule']}" for r in rows) return [TextContent(type="text", text=result or "无约束记录")]这段代码的关键点在于:工具描述要写清楚"什么时候用"。模型选工具靠的是 description,如果你只写"查询约束",它可能不知道该在生成代码前调用。写成"查询指定项目的编码约束规则,生成代码前应调用",命中率会高很多。这是我在调优 MCP 工具时最深的体会——工具描述的质量,直接决定代理会不会用、用得对不对。
4.4 流式输出与工具结果的配合
热词里出现了"使用 mcp 工具流式输出内容到文件",这其实是个很实际的场景。当代理通过 MCP 工具拿到一大段上下文(比如几百行代码片段),如果一次性返回,会瞬间占满上下文窗口。更好的做法是让工具支持分页或流式返回,代理按需拉取。
Context Mode 在search_artifacts里就做了分页:默认返回前 5 条,每条截断到 200 行,代理觉得不够可以带offset再查。这个设计避免了"一次查询把上下文撑爆"的尴尬。我在处理一个上万行的老项目时,正是靠这个分页机制,让代理能精准定位到某个函数,而不是把整个文件吞进去。
5. 检索策略:让代理"查得准"比"查得多"更重要
5.1 分层检索:先约束,再状态,最后细节
上下文预算永远是稀缺的。Context Mode 的检索策略遵循一个明确的优先级:约束 > 会话状态 > 具体细节。代理在动手前,先拉约束(几十条,token 少但价值高),再拉会话状态(知道当前任务进行到哪),最后才按需召回具体代码片段。
这个顺序不能反。我见过有人一上来就让代理召回一堆相关代码,结果约束信息被挤到后面,代理写出了违反项目规范的代码。正确的做法是让约束"常驻"在上下文里,细节"按需"加载。
5.2 关键词检索与语义检索的取舍
前面提到 SQLite 的 FTS5 做关键词检索又快又准。但关键词检索有个天然短板:它匹配的是字面,不是意图。代理想找"处理超时的逻辑",但代码里写的是handleTimeout,纯关键词可能漏掉。
我的实践是关键词为主、语义为辅:先用 FTS5 做一轮快速召回,如果结果数量低于阈值(比如少于 3 条),再触发一次语义检索兜底。这样大部分查询走快路径,只有少数模糊查询才付出语义检索的成本。这个混合策略在保证召回率的同时,把平均查询延迟压得很低。
5.3 上下文预算的动态分配
一个容易被忽略的点是:上下文预算不是固定的。会话早期,约束和项目背景占大头;会话中期,任务状态和最近改动占大头;会话后期,具体细节和待办事项占大头。Context Mode 允许按会话阶段动态调整各表的检索配额,比如早期给 constraints 分配 40% 预算,后期降到 15%。
这个动态分配的逻辑,本质上是模拟人类工作时注意力的转移——刚开始先搞清楚规则,干到一半关注进度,快结束时盯细节。把它显式地编码进检索策略,代理的表现会稳定很多。
6. 落地实录:我在真实项目里踩过的坑
6.1 坑一:约束表写成了"许愿池"
刚开始我把所有能想到的规范都往 constraints 表里塞,结果攒了两百多条。代理每次查询返回一大堆,反而不知道该重点遵守哪条,token 也浪费。后来我做了减法:只保留会被 code review 反复打回的硬性规则,控制在 30 条以内,其余降级成文档。约束表不是许愿池,是"红线清单"。
6.2 坑二:SQLite 并发写入的锁问题
编码代理可能同时触发多个工具调用,如果都往 SQLite 写,容易撞上database is locked。我一开始没在意,直到某次会话里代理连续报错。解决办法有两个:一是开启 WAL 模式(PRAGMA journal_mode=WAL),让读写不互相阻塞;二是给写操作加一个简单的队列,串行化写入。WAL 模式基本能解决 90% 的并发问题,强烈建议默认开启。
PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;busy_timeout这个参数也很关键,它让 SQLite 在遇到锁时等待而不是立刻报错,5 秒的等待对编码场景足够。
6.3 坑三:会话摘要写得太"文学"
sessions 表里的摘要,我一开始让模型自由发挥,结果写出来一堆"我们深入探讨了……"这种废话,恢复现场时根本提取不出有效信息。后来我改成结构化摘要模板:当前任务、已完成项、待办项、阻塞点、关键决策。强制按这个格式写,恢复效率立刻上来了。摘要不是给人读的散文,是给代理读的状态机。
6.4 坑四:忘了给 artifacts 做归档
artifacts 表只增不减,跑了一个月就攒了几十万条,查询开始变慢。后来加了个定时任务,把 30 天前的、且没被引用过的 artifact 归档到冷表。热表保持在十万条以内,查询速度就稳了。这个"热冷分离"的思路,和很多日志系统的做法是一样的。
7. 从 Context Mode 看代理记忆系统的演进方向
Context Mode 这套设计,其实指向了一个更大的趋势:AI 编码代理的竞争,正在从"模型多强"转向"记忆多好"。模型能力趋同之后,谁能把项目上下文管理得更精准、更持久、更省 token,谁就能在实际工程里跑得更远。
我个人的判断是,接下来这个方向会往三个地方走。一是多代理共享记忆——一个团队里多个代理共用一份上下文库,A 代理踩过的坑,B 代理不用再踩。二是记忆的自动衰减与强化——高频引用的约束自动提升优先级,长期没用的自动降级归档,让记忆系统自己"新陈代谢"。三是跨工具的记忆可移植——今天在编码代理里积累的上下文,明天能无缝迁移到另一个工具,不被单一平台绑架。
Context Mode 现在还是个相对早期的实现,但它的范式是对的:把上下文从"一段文本"变成"一个可查询的状态"。这个转变一旦被更多工具接受,AI 编码代理的体验会有一次质的提升。如果你正在自建编码工作流,我建议尽早把上下文管理这块独立出来,别让它和对话逻辑耦合在一起——耦合得越深,后面越难改。这是我踩了几个月坑之后,最想分享的一句话。