资讯动态

AI编码代理上下文管理:MCP与SQLite实战

发布时间:2026/10/8 4:47:28 来源:尧图企业网站定制
1. 为什么“上下文管理”成了 AI 编码代理的生死线过去一年我一直在折腾各种 AI 编码代理从最早的补全插件到后来的自主 Agent踩过的坑比写过的代码还多。最让我抓狂的不是模型不够聪明而是它记性太差、记的东西又太乱。你让它改一个函数它把整个仓库塞进提示词你让它连续做三步重构第三步它已经忘了第一步改了什么。这就是典型的上下文管理失控。Context Mode 这个项目本质上就是在解决这件事给 AI 编码代理换一套上下文管理范式。它不再把“上下文”简单理解成一段塞进窗口的文本而是把上下文当成一个有结构、可检索、可持久化的数据层来管理。核心关键词里出现的 MCP、SQLite正是这套范式的两大支柱——MCP 负责让代理和外部工具、数据源标准化对话SQLite 负责把上下文落盘成可查询的本地状态。这篇文章适合三类人看一是正在自建 AI 编码代理、被上下文窗口折磨的开发者二是想搞懂 MCP 到底是什么、怎么落地的人三是好奇 SQLite 这种“老古董”为什么又回到 AI 基础设施舞台中央的工程师。我会从设计思路讲到实操细节把参数、表结构、排查技巧都摊开说尽量让你看完能直接抄作业。先说结论Context Mode 的价值不在于它用了多新的技术而在于它把“上下文”从易失的提示词变成了可管理的状态。这个转变才是 AI 编码代理从玩具走向工具的分水岭。2. Context Mode 的整体设计与思路拆解2.1 传统上下文管理的三个死结在讲 Context Mode 怎么做之前得先搞清楚老办法为什么不行。我总结下来是三个死结。第一个是窗口焦虑。主流模型的上下文窗口看着挺大动辄几十万 token但真正用起来你会发现塞进去的东西越多模型注意力越涣散。我实测过一个中等规模的 TypeScript 项目把整个 src 目录塞进去大概 8 万 token问它“这个函数在哪里被调用”它经常答错因为关键信息被淹没在噪音里。窗口大不等于有效。第二个是状态易失。传统做法是把对话历史一股脑塞进 messages 数组每轮都重发。问题是代理执行多步任务时中间产生的文件变更、命令输出、临时决策全都没有结构化的存储。一旦超出窗口被截断代理就“失忆”了。我见过最离谱的一次代理改完文件后自己又把它改回去因为它忘了自己刚做过什么。第三个是检索靠猜。想让代理找到相关代码传统做法要么全量塞要么靠关键词 grep。前者浪费窗口后者召回率低。语义检索虽然好但把 embedding 和向量库接进代理流程工程复杂度一下就上来了。Context Mode 的思路很直接把上下文从“对话流”里抽出来变成一个独立的、可增删改查的数据层。代理不再靠“记住”来工作而是靠“查询”来工作。2.2 为什么是 MCP 加 SQLite 这个组合选型这件事我想多说两句因为很多人会问为什么不用向量数据库为什么不用 Redis为什么偏偏是 SQLite先说 MCP。MCPModel Context Protocol的核心价值是标准化。在没有 MCP 之前每个代理框架接工具的方式都不一样你给 A 框架写的文件读取工具换到 B 框架就得重写。MCP 把“代理如何发现工具、调用工具、拿回结果”这件事定义成了一套协议。Context Mode 用 MCP 作为对外接口意味着它的上下文数据层可以被任何支持 MCP 的客户端访问——不管你是用某个桌面 AI 工具还是自己写的 Agent 循环。再说 SQLite。这里有个反直觉的点上下文管理最需要的不是高并发而是低延迟、零运维、可嵌入。SQLite 恰好全中。它是个单文件数据库不需要起服务进程内直接读写查询延迟在微秒级。对于代理这种“本地跑、频繁读写、数据量中等”的场景SQLite 比任何 C/S 架构的数据库都合适。而且它支持全文检索FTS5、JSON 字段、甚至向量扩展能力完全够用。我做过一个对比测试同样十万条上下文记录SQLite 做一次带索引的模糊查询大概 3 到 8 毫秒而走网络请求的向量库光网络往返就得 20 毫秒起步。代理每轮要查好几次上下文这个差距会被放大。提示选 SQLite 不代表不能用向量检索。SQLite 有 sqlite-vec 这类扩展可以在本地做向量相似度搜索把“结构化查询”和“语义检索”合到一个文件里这是它比纯向量库更灵活的地方。2.3 分层上下文模型热、温、冷三层Context Mode 最核心的设计是把上下文分成三层来管理我把它叫做热、温、冷。热层是当前任务直接相关的上下文比如正在编辑的文件片段、最近几条工具调用结果。这部分会直接进模型窗口要求高保真、低延迟。温层是任务相关但不必常驻窗口的内容比如同模块的其他文件、相关的历史决策记录。这部分存在 SQLite 里代理需要时通过 MCP 工具按需拉取。冷层是长期归档比如几个月前的提交记录、已经完成任务的完整轨迹。这部分主要用来做检索和回溯平时不参与推理。这个分层的好处是窗口利用率大幅提升。代理不再无脑塞全量而是先查温层、按需加载。我实测下来同样的任务分层管理后进窗口的 token 量能降 60% 到 70%而任务成功率反而上升因为关键信息不再被噪音稀释。3. 核心细节解析与实操要点3.1 MCP 服务端到底暴露了哪些工具Context Mode 通过 MCP 暴露的工具集是整个范式的操作入口。根据我的实践一套完整的上下文管理至少需要这几类工具。第一类是写入类context_write用来把新的上下文片段落盘。参数通常包括内容、类型代码/决策/输出、来源、时间戳、关联任务 ID。这里的关键是类型标注因为后续检索要靠它过滤。第二类是查询类context_query支持按任务 ID、类型、时间范围、关键词组合查询。这是代理最常用的工具性能直接决定体验。第三类是更新类context_update用来标记某条上下文已失效或被取代。代理改完代码后旧版本上下文应该被标记而不是删除保留可追溯性。第四类是摘要类context_summarize把一批冷层记录压缩成摘要。这个工具很关键因为冷层数据会无限增长定期摘要能控制体积。下面是一个 MCP 工具定义的简化示例用 JSON Schema 描述参数{ name: context_query, description: 按条件查询上下文记录, inputSchema: { type: object, properties: { task_id: { type: string }, type: { type: string, enum: [code, decision, output, summary] }, keyword: { type: string }, limit: { type: integer, default: 20 } }, required: [task_id] } }注意工具描述description写得好不好直接决定模型会不会正确调用。我踩过的坑是描述太笼统模型经常把 query 和 write 搞混。后来我把每个工具的适用场景写进描述里调用准确率明显提升。3.2 SQLite 表结构设计别小看这几张表表结构设计是 Context Mode 的地基设计不好后面全是坑。我推荐的最小可用结构是四张表。contexts主表存上下文记录字段包括 id、task_id、type、content、metadataJSON、created_at、updated_at、status。status用来标记 active、superseded、archived这是实现“更新而非删除”的关键。tasks表存任务元信息包括 id、title、status、created_at。任务和上下文是一对多关系查询时先定位任务再拉上下文效率更高。context_fts是 FTS5 全文索引表对 content 建索引支持关键词快速检索。这张表是查询性能的核心。summaries表存摘要包括 id、task_id、content、covers_from、covers_to记录这段摘要覆盖了哪个时间范围的原始记录。建表语句大概长这样CREATE TABLE contexts ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, type TEXT NOT NULL, content TEXT NOT NULL, metadata TEXT, status TEXT DEFAULT active, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE INDEX idx_contexts_task ON contexts(task_id, status); CREATE INDEX idx_contexts_type ON contexts(type); CREATE VIRTUAL TABLE context_fts USING fts5( content, contentcontexts, content_rowidid );这里有个细节created_at我用的是整数时间戳而不是文本日期因为整数比较和索引效率更高十万条数据下差距很明显。3.3 上下文写入的时机与粒度控制什么时候写、写多细是实操中最容易翻车的地方。写太勤数据库膨胀快、查询变慢写太懒代理失忆。我的经验是按“决策点”写入而不是按“每一步”写入。具体来说这几种情况必须写代理做出一个影响后续的决策时、工具调用返回了关键结果时、文件被修改后、任务状态变更时。而像“代理思考了一下”这种中间过程不必每条都落盘。粒度上单条上下文控制在 200 到 2000 字之间比较合适。太短信息不足太长检索时噪音大。如果一段内容超过 2000 字我会先切分再写入切分点选在语义边界比如函数之间、段落之间。提示写入时一定要带 task_id。我早期图省事没带结果多个任务的上下文混在一起查询时根本分不清哪条属于哪个任务后来只能写脚本回填血的教训。3.4 检索策略结构化过滤加全文检索的组合拳单纯靠全文检索不够单纯靠结构化过滤也不够Context Mode 的检索是两者组合。流程是这样的先用结构化条件task_id、type、status、时间范围把候选集缩小再用 FTS5 做关键词匹配排序最后按相关度和时间新鲜度加权返回 top N。这个组合能把查询从“全表扫描”变成“索引命中”十万条数据下也能稳定在毫秒级。相关度加权我用的公式是score fts_rank * 0.7 freshness * 0.3其中 freshness 是时间衰减因子越新的记录权重越高。这个权重不是拍脑袋定的我调过几轮0.7/0.3 在我的场景下召回质量最好。你可以根据自己的数据特点调整。4. 实操过程与核心环节实现4.1 环境准备从零搭起本地上下文层先把环境搭起来。我以 Linux 环境为例Windows 和 macOS 思路一样命令略有差异。第一步装 SQLite。大多数 Linux 发行版自带但版本可能偏老建议装新版以支持 FTS5 和 JSON 函数# Debian/Ubuntu 系 sudo apt update sudo apt install sqlite3 libsqlite3-dev # RHEL/Rocky 系 sudo dnf install sqlite sqlite-devel # 验证版本和 FTS5 支持 sqlite3 --version sqlite3 :memory: CREATE VIRTUAL TABLE t USING fts5(x);最后那条命令如果没报错说明 FTS5 可用。这一步很关键很多老版本 SQLite 默认不带 FTS5会导致后面全文检索建表失败。第二步初始化数据库文件。我习惯把数据库放在项目根目录的.context/下方便随项目走mkdir -p .context sqlite3 .context/context.db schema.sqlschema.sql就是前面那套建表语句。初始化完可以用.tables命令确认表都建好了。第三步配置 MCP 服务端。Context Mode 的 MCP 服务端本质是个进程它持有 SQLite 连接对外暴露工具。配置时要注意两点一是数据库路径要用绝对路径避免工作目录变化导致找不到文件二是设置合理的连接超时和 busy_timeout防止并发写入时锁冲突。# 设置 busy_timeout避免写锁冲突直接报错 sqlite3 .context/context.db PRAGMA busy_timeout 5000; sqlite3 .context/context.db PRAGMA journal_mode WAL;WAL 模式一定要开它让读写可以并发代理一边查一边写不会互相阻塞。这是本地上下文层能流畅运行的关键配置。4.2 实现一个最小的上下文写入与查询闭环环境好了来跑通一个最小闭环。我用 Python 演示因为它的 sqlite3 标准库开箱即用。先写一个写入函数import sqlite3 import time import json def write_context(conn, task_id, ctx_type, content, metadataNone): now int(time.time()) cur conn.execute( INSERT INTO contexts (task_id, type, content, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?), (task_id, ctx_type, content, json.dumps(metadata or {}), now, now) ) # 同步写入 FTS 索引 conn.execute( INSERT INTO context_fts (rowid, content) VALUES (?, ?), (cur.lastrowid, content) ) conn.commit() return cur.lastrowid注意 FTS 索引要手动同步因为外部内容表模式contentcontexts不会自动更新。这是很多人第一次用 FTS5 会踩的坑写完主表忘了写索引表结果查不到。再写查询函数def query_context(conn, task_id, keywordNone, ctx_typeNone, limit20): sql SELECT c.id, c.type, c.content, c.created_at, bm25(context_fts) AS rank FROM contexts c JOIN context_fts ON context_fts.rowid c.id WHERE c.task_id ? AND c.status active params [task_id] if ctx_type: sql AND c.type ? params.append(ctx_type) if keyword: sql AND context_fts MATCH ? params.append(keyword) sql ORDER BY rank LIMIT ? params.append(limit) return conn.execute(sql, params).fetchall()bm25()是 FTS5 内置的相关度排序函数值越小越相关。用它排序比按时间排更符合“找相关内容”的需求。跑一遍验证conn sqlite3.connect(.context/context.db) write_context(conn, task-001, decision, 决定用 WAL 模式提升并发读写性能) write_context(conn, task-001, code, def parse_config(path): ...) rows query_context(conn, task-001, keywordWAL) print(rows)如果能看到那条 WAL 相关的记录闭环就通了。4.3 把上下文层接进代理循环光有数据层不够得让代理真正用起来。代理循环里我通常在两个位置插入上下文操作。一是在任务开始时先查一次温层把相关上下文加载进窗口。查询条件用当前任务描述里的关键词加上最近活跃的 task_id。二是在每轮工具调用后判断这次调用是否产生了需要落盘的上下文。如果是文件修改或关键决策就调context_write。这里有个实操技巧不要每轮都全量重查。我一开始每轮都查一遍结果延迟累积很明显。后来改成“增量加载”——只在窗口里缺少某类信息时才查比如代理要改一个文件但窗口里没有它的内容这时才去温层拉。这样查询次数能降一个数量级。代理循环的伪代码大概是这样def agent_loop(task): ctx query_context(conn, task.id, keywordtask.keywords, limit10) window build_window(task, ctx) while not task.done: action model.decide(window) result execute(action) if is_significant(result): write_context(conn, task.id, classify(result), result.content) if needs_more_context(action, window): extra query_context(conn, task.id, keywordaction.target) window append(window, extra) window append(window, result)is_significant这个判断函数是核心它决定什么值得落盘。我的实现是看结果类型文件写入、命令执行、显式决策返回 True普通查询返回 False。4.4 冷层摘要的触发与实现冷层数据会一直涨必须定期摘要。我的做法是设一个阈值比如某个 task_id 下的 active 记录超过 500 条就触发摘要。摘要流程是取最老的一批记录比如 200 条调模型生成一段压缩摘要写入summaries表同时把这 200 条记录 status 改成 archived。这样它们不再参与常规查询但保留可追溯性。def summarize_old_contexts(conn, task_id, batch200): rows conn.execute( SELECT id, content FROM contexts WHERE task_id ? AND status active ORDER BY created_at LIMIT ?, (task_id, batch) ).fetchall() if len(rows) batch: return text \n.join(r[1] for r in rows) summary model.summarize(text) ids [r[0] for r in rows] conn.execute( INSERT INTO summaries (task_id, content, covers_from, covers_to) VALUES (?, ?, ?, ?), (task_id, summary, ids[0], ids[-1]) ) conn.executemany( UPDATE contexts SET status archived WHERE id ?, [(i,) for i in ids] ) conn.commit()摘要的粒度我建议控制在原文的 10% 到 20%。太短丢信息太长没起到压缩作用。我实测 15% 左右比较平衡。5. 常见问题与排查技巧实录5.1 查询变慢先看索引再看数据量十万条数据下查询变慢八成是索引没建对。我遇到过几种典型情况。一是 FTS 索引没同步导致 MATCH 查询走全表扫描。排查方法是EXPLAIN QUERY PLAN看执行计划如果出现SCAN而不是SEARCH就是索引没命中。二是结构化过滤字段没建索引。task_id和status是高频过滤字段必须建复合索引。我早期只建了单列索引组合查询时优化器选错索引性能差好几倍。三是数据量真的太大了。这时候要么摘要归档要么分库。我的经验是单库控制在 50 万条以内体验最好超过就按 task 或时间分片。下面这张表是我整理的常见性能问题速查现象可能原因排查方法解决查询超过 100ms索引未命中EXPLAIN QUERY PLAN补建索引写入报 database is locked未开 WAL 或 busy_timeout 太短查 journal_mode开 WAL设 busy_timeoutFTS 查不到刚写的内容索引表未同步对比主表和 FTS 行数补写 FTS 索引数据库文件暴涨冷层未归档统计各 status 行数触发摘要归档查询结果不相关权重不合理抽样看 rank 分布调 bm25 权重5.2 MCP 工具调用失败从描述和参数两头查MCP 工具调用失败最常见的原因是工具描述不清导致模型选错工具或者参数格式不对。我踩过的一个坑是context_query的 keyword 参数我定义成可选但模型经常传空字符串而不是不传结果 MATCH 空字符串报错。后来我在服务端加了空值过滤问题解决。另一个坑是工具返回内容太长超出模型能处理的范围。MCP 工具返回建议控制在 4000 字以内超长的内容应该分页返回让模型按需拉取下一页。注意MCP 服务端的错误信息要写清楚。模型看到“error: invalid params”这种模糊报错往往不知道怎么修正。我后来把错误信息写成“keyword 不能为空请提供至少一个关键词”模型自我修正的成功率明显提高。5.3 上下文污染代理被过期信息带偏这是最隐蔽也最致命的问题。代理查到了已经被取代的旧上下文基于它做出错误决策。根因是 status 管理没做好。我的解决方案是强制版本化每次更新上下文不是原地改而是插入新记录并把旧记录 status 改成 superseded。查询时默认只查 active需要历史时才显式查 superseded。另外摘要归档后原始记录虽然 archived但如果查询没过滤 status还是会被捞出来。所以查询函数里status active这个条件不能省。5.4 并发写入冲突WAL 也不是万能的开了 WAL 之后读写能并发但多写并发仍然会冲突。代理如果并行执行多个子任务同时写数据库就会遇到 SQLITE_BUSY。我的处理是加一个写入队列所有写操作串行化。SQLite 的写入本来就快串行化带来的延迟增加可以忽略但能彻底避免锁冲突。如果确实需要高并发写那就得考虑分库每个子任务一个数据库文件最后再合并。5.5 数据迁移与备份别等丢了才后悔SQLite 是单文件备份很简单直接复制文件就行。但复制前要先 checkpoint否则 WAL 里的数据可能没落盘。sqlite3 .context/context.db PRAGMA wal_checkpoint(TRUNCATE); cp .context/context.db .context/context.db.bak我习惯在每次摘要归档后做一次备份因为归档是不可逆操作万一摘要质量差还能回滚。数据迁移方面如果表结构要改SQLite 的 ALTER TABLE 能力有限改字段类型基本得重建表。流程是建新表、导数据、删旧表、改名。这个操作一定要在事务里做中途失败能回滚。6. 我在这套范式里踩出来的几条经验Context Mode 这套东西我前后迭代了三四版有些体会是文档里不会写的。第一上下文管理的本质是信息架构不是存储技术。很多人一上来就纠结用 SQLite 还是向量库其实真正决定效果的是你怎么给上下文分类、怎么定粒度、怎么设计检索权重。存储只是载体架构才是灵魂。第二写入策略比查询策略更重要。查询慢可以优化但写错了、写漏了后面怎么查都救不回来。我现在的原则是“宁可多写一条决策记录也不要漏掉一个关键状态”。第三摘要要趁早。我早期想着数据不多不用摘要结果攒到几万条才处理摘要质量明显下降因为跨度太大模型抓不住重点。现在改成定期小批量摘要效果稳定得多。第四给代理留“遗忘”的能力。不是所有上下文都值得长期保留。我后来加了个机制任务完成后一段时间把该任务的冷层记录进一步压缩成一句话结论原始记录彻底归档。这样数据库不会无限膨胀检索也不会被历史噪音干扰。这套范式还在演进MCP 生态也在快速变化但“把上下文当数据管”这个方向我认为是对的。如果你也在做 AI 编码代理建议尽早把上下文层独立出来别让它和对话流耦合在一起后面会省很多事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑