How AI Coding Works III: Context Compaction & MCP
LLM 一次调用能处理的 token 总量有硬上限,叫上下文窗口。它同时约束输入 + 输出:你发的 messages 越长,留给模型回复的空间就越少,超过上限 API 直接报错。
以 Claude 为例,标准档窗口约 200K tokens(最新型号已扩展到 1M)。这个”约 200K”是当前主流 Agent 设计时实际假设的可用容量。
不要凭直觉估,token 数和字数不是一回事。token 是分词器(tokenizer)切出的最小单元,和具体语言、具体分词器强相关:
| 语言 | 大致比例(经验值) |
|---|---|
| 英文 | 1 token ≈ 0.75 词(约 4 字符) |
| 中文 | 1 个汉字常常 ≈ 1~2 token(看分词器) |
按中文的实际情况估算,200K tokens 大约对应 十几万汉字——比英文少很多,因为 CJK 文本在多数分词器下 token 效率低。
旁白里”约 50 万字”是高估了。中文场景下更接近十几万字,读几十个源文件就可能撑满。这正是后文”大项目装不下”的根源。
在 Agent 循环里(见 Agent 循环),messages 每一轮都增长:
tool 结果都可能很长(一段日志、一个源文件全文)。assistant 消息也留在数组里。分析一个真实项目,读几十个源文件,几十轮工具调用下来,messages 累计 token 轻松逼近甚至超过 200K。messages 就是这个容器,它不能无限增长。
不同模型/供应商行为略有差异,常见两种:
成熟的 Agent(如 Claude Code)不会等到超限,而是在接近上限时主动启动上下文压缩(见上下文压缩),按策略裁剪/摘要旧消息。
真实项目动辄几十万行代码,远超 200K token 的窗口(见上下文窗口)。Claude Code 生成架构文档时,策略和人类工程师一样——先全局后细节,按需检索:
| 阶段 | 工具 | 目的 |
|---|---|---|
| 看全局 | Glob | 列出文件清单、目录结构 |
| 定位关键 | Grep | 找入口函数、核心类、关键调用 |
| 精读片段 | Read | 只读入口和核心实现,不读无关文件 |
最后进入主上下文的,是经过过滤的精华(相关文件的关键片段),而不是项目原文。这是把”几十万行”压进有限窗口的唯一办法。
光靠主 Agent 自己 Glob/Grep/Read,遇到大项目仍然可能撑爆上下文。于是有第二招——派子 Agent(见多 Agent 与后台 Agent):
主 Agent
├─ 派子 Agent A → 读 模块A 全部文件 → 回传一段摘要
├─ 派子 Agent B → 读 模块B 全部文件 → 回传一段摘要
└─ 派子 Agent C → 读 模块C 全部文件 → 回传一段摘要
主 Agent 只收集到 A/B/C 三段摘要
关键点:子 Agent 有自己独立的消息数组(独立的上下文窗口)。它在自己的窗口里读完整模块、做摘要,但只把摘要这一小段回传给主 Agent。模块的原始代码从不进入主上下文——主 Agent 的窗口因此保持精简。
这本质上是Map-Reduce:把大任务拆给多个 worker(map),各自消化后只汇总结论(reduce)。
长会话里 messages 不断增长(见消息数组全貌),迟早逼近上下文窗口上限(见上下文窗口)。与其等 API 报错或被供应商静默截断,Claude Code 选择主动压缩——一个分层的”智能遗忘系统”。
旁白描述的”五层压缩”,按从轻到重、从可逆到有损排列:
| 层 | 做什么 | 损失 |
|---|---|---|
| 1 | 裁剪工具输出冗余:超长日志只保留关键几行、超长文件只保留相关片段 | 低,原始意图可还原 |
| 2 | 微压缩不重要轮次:把早期不太关键的问答做轻度改写 | 低~中 |
| 3 | 折叠最早期的上下文:把多条早期消息合并成一条摘要 | 中 |
| 4 | 整段对话做摘要:用模型把一大段历史浓缩成几段总结 | 高,细节大量丢失 |
| 5 | 跨会话记忆:把关键事实存为可持久化、跨会话复用的记忆条目 | 取决于存什么 |
核心思想:优先保留最近和最重要的信息,越早越次要的越激进地丢。
要清醒认识一点:从第 3 层往后,压缩都是不可逆的。一段被摘要掉的报错原文,模型之后再想看具体某行,已经拿不回来——摘要里没写就是没了。
这解释了一个高频现象:AI 突然”忘了”几分钟前说过的话。往往不是模型失忆,而是那条消息在压缩时被并进了摘要,具体细节丢失。要恢复,只能让用户或工具重新提供。
第 5 层”跨会话记忆”听起来像 LLM 终于能记事了,但本质仍是外部存储 + 按需注入:
所以”记忆”是工程实现的产物,不是模型权重的改变。
MCP 是把”工具/数据源”接入 AI 应用的开放标准协议。官方的比喻是”AI 工具世界的 USB-C”:任何支持 MCP 的 AI 应用,都能接入任何实现 MCP 的服务——数据库、文件系统、GitHub、Slack、浏览器……不用每个工具各自重写一套适配层。
回看工具调用(见工具调用):工具是 LLM 的双手。MCP 就是这双手的接口标准——它规定了”工具长什么样、怎么列出来、怎么调用、结果怎么回传”。
MCP 是客户端-服务端架构,三个角色:
| 角色 | 是谁 | 职责 |
|---|---|---|
| Host(宿主) | AI 应用本身(Claude Code、Cursor、VS Code Copilot) | 管理多个 client、对接 LLM |
| Client(客户端) | Host 内部为每个 server 建的连接对象 | 维持一条到 server 的专用连接 |
| Server(服务端) | 提供工具/数据的程序(filesystem server、GitHub server……) | 暴露能力给 client |
一个 host 可以同时连多个 server(本地 stdio 进程,或远程 HTTP 服务),每个连接由一个独立的 client 对象维护。
MCP 的数据层建立在 JSON-RPC 2.0 上——和 LLM 的 function calling 是两层不同的东西。function calling 是”模型 ↔ 宿主”的约定,MCP 是”宿主 ↔ 工具服务”的约定。MCP 把分散在各处的工具统一成同一个 RPC 接口,宿主就能用同一套代码对接任意 server。
消息层分两层:
server/discover)、三大原语。server 通过三类原语向 AI 应用提供能力:
| 原语 | 含义 | 对应方法 |
|---|---|---|
| Tools | 可执行的函数(查数据库、调 API、文件操作) | tools/list、tools/call |
| Resources | 提供上下文数据(文件内容、数据库 schema、API 响应) | resources/list、resources/read |
| Prompts | 可复用的交互模板(system prompt 片段、few-shot 示例) | prompts/list、prompts/get |
宿主连上 server 后,先用 tools/list 发现可用工具,再把它们统一注册进 LLM 的 tools 参数。模型决定调用某个工具时,宿主把调用路由到对应 server 的 tools/call,拿到结果再写回 messages(见消息数组)——整个过程对模型是透明的,它只看到熟悉的 function calling 接口。
一个简化的工具发现请求(JSON-RPC):
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
server 返回带 name / description / inputSchema(JSON Schema)的工具清单,结构和 LLM 的 tools 参数几乎一一对应——这正是 MCP 能无缝对接 function calling 的原因。
在没有 MCP 之前,每接一个新工具(比如接 GitHub),Agent 都得专门写一遍适配:怎么认证、怎么列功能、怎么调。N 个应用 × M 个工具 = N×M 套适配代码。
MCP 把这变成 N+M:每个应用实现一次 MCP client,每个工具实现一次 MCP server,二者用统一协议对接。这就是协议标准化的价值——和 USB、HTTP、SQL 一样,统一接口催生生态。
tools 参数 → 模型用 function calling 决定调哪个。sampling(让 server 反向请求宿主跑 LLM)等客户端原语在新版(2026-07-28)已标记弃用,新实现应直接对接 LLM 提供商 API。读资料时注意版本。面对大任务,主 Agent 把它拆成子任务,派给多个子 Agent 并行执行。和单 Agent 顺序读文件的关键区别是:
每个子 Agent 有自己独立的消息数组(独立的上下文窗口)。
这是处理大项目的杀手锏(见大项目装不下):
主 Agent(主上下文)
├─ 子 Agent A 独立 messages → 读完整模块A → 回传一段摘要
├─ 子 Agent B 独立 messages → 读完整模块B → 回传一段摘要
└─ 子 Agent C 独立 messages → 读完整模块C → 回传一段摘要
主上下文只收到 3 段摘要,原始代码从不进入主上下文
两个收益:
这本质是 Map-Reduce(见大项目装不下):map 阶段子 Agent 各自消化一个分块,reduce 阶段只把摘要汇回主 Agent。
这是多 Agent 设计最关键的一点。子 Agent 完成任务后,只向主 Agent 汇报一段结构化摘要(结论、关键发现、必要的数据点),不把读过的原始内容回传。否则主上下文照样爆,隔离就没意义了。
这要求子 Agent 有”总结”能力——本质也是一次 LLM 调用,把长上下文压成短摘要,和上下文压缩(见上下文压缩)是同一种思路。
再进一步是后台 Agent(background agent)——运行在云端,不依赖你开着的终端。典型场景:
你在 GitHub 提一个 Issue → 云端 Agent 自动接管 → 读代码、写改动、跑测试 → 提一个 Pull Request → 你只需做 Code Review。
这不是科幻,GitHub、Anthropic、Cursor 等都已上线类似能力。它的技术含义:
从架构上看,后台 Agent 仍是同一个 Agent 循环(见 Agent 循环)——一个 while 加 API 加工具。变的是运行位置和生命周期管理,不是核心原理。
前面拆完了所有概念——无状态 LLM、消息数组、工具调用、System Prompt、Agent 循环。把它们捏到一起,一个能用的 Agent 核心其实极短。下面是最小骨架(Python,对接 OpenAI 风格 API,~20 行):
tools = { # 1. 工具表:名字 → 真正的 Python 函数
"write_file": write_file,
"bash": run_bash,
"read_file": read_file,
}
messages = [ # 2. 初始化消息数组:system + 用户请求
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_request},
]
while True: # 3. Agent 循环
resp = client.chat.completions.create(
model=MODEL, messages=messages, tools=TOOL_SCHEMAS
) # 4. 调一次 LLM
msg = resp.choices[0].message
messages.append(msg) # 5. 把回复记进数组
if not msg.tool_calls: # 6. 没有工具调用 → 任务完成,退出循环
print(msg.content)
break
for call in msg.tool_calls: # 7. 否则逐个执行工具
fn = tools[call.function.name]
args = json.loads(call.function.arguments) # arguments 是 JSON 字符串
result = fn(**args) # 8. 真正的副作用在这里发生
messages.append({ # 9. 结果作为 tool 消息追加(带上 id 配对)
"role": "tool",
"tool_call_id": call.id,
"content": str(result),
})
# 回到 while 顶部,带上包含工具结果的 messages 再调一次 LLM
| 代码 | 对应概念 |
|---|---|
tools 表 + TOOL_SCHEMAS | 工具调用:函数注册给模型 |
messages 初始含 system + user | System Prompt、消息数组 |
client.chat.completions.create(...) | LLM 是无状态函数 |
messages.append(msg) | 每轮回复都记进数组(“记忆”的真相) |
if not msg.tool_calls: break | Agent 循环的终止条件 |
for call in msg.tool_calls | 执行工具并把结果写回 |
json.loads(call.function.arguments) | arguments 是字符串,必须解析 |
整套流程就是 Agent 循环(见 Agent 循环)的字面翻译——没有任何额外魔法。
tool_call_id 必须对上。结果消息要带发起调用时的 call.id,模型才能把结果和调用配对,否则下次调用它会困惑(见工具调用)。arguments 是 JSON 字符串,不是对象。直接 fn(call.function.arguments) 会失败,必须先 json.loads。json.dumps 或 str()。tool 消息。工具抛错时别让循环崩——把错误信息作为 content 回传,模型会读到错误并尝试修复(这正是自动修 Bug 的来源,见错误修复重试)。while True 是隐患,模型可能空转烧 token。加个 for _ in range(MAX_STEPS): 兜底。这段 20 行能演示原理,但缺少真实 Agent 必备的:权限校验、流式输出、上下文压缩、并发子 Agent、工具路由、错误恢复、可观测性……这些工程细节才是 Claude Code 的主体(见从玩具到产品)。理解 20 行,是拿到了正确的起点,不是终点。