上下文装配:每一轮请求怎么拼出来
系统提示、工具定义、项目说明、记忆、历史与工具结果的组装顺序和预算。
一句话理解:模型不记得上一轮,所以 Harness 每一轮都要把完整请求重新拼一遍:越稳定的内容越靠前,新内容只往末尾追加,再在窗口预算内决定每一块放多少;模型看到什么、花多少钱、会不会“忘事”,都在这一步决定。
在 最小 Agent 实操 里,你已经见过装配的雏形:messages 列表每轮追加一条模型回复和一条工具结果,然后整个列表连同系统提示、工具定义一起再发一次。模型 API 是无状态的,每一轮请求都必须自带全部上下文。
这件事在 Demo 里不起眼,上了真实任务却会变成三类问题:第 30 步的时候模型“忘了”开头的规则;账单里重复发送的固定内容占了大头;上下文窗口被一次巨大的工具结果撑爆。它们看起来是模型问题,根子都在装配器。这一课讲清楚一次请求由哪几块组成、按什么顺序拼、每一轮哪些部分在变,以及预算怎么分。
一次请求由什么组成
不管用哪家模型、哪个框架,一次 Agent 请求大体都由下面几块拼成。按从前到后的顺序画出来,并标上每一块的稳定程度:
| 部分 | 典型内容 | 谁产生 | 最常见的问题 |
|---|---|---|---|
| 工具定义 | 每个工具的名称、描述、参数 Schema | 研发、MCP Server | 数量一多成了隐形大户 |
| 系统提示 | 产品身份、安全底线、工具使用规范、运行环境 | 产品团队 | 被塞进时间戳等易变信息 |
| 项目说明与规则 | 构建命令、代码规范、业务约定 | 项目团队 | 越写越长、互相矛盾 |
| 记忆与 Skill 目录 | 记忆索引、Skill 的名称和一句话描述 | 用户、Agent、领域专家 | 过期或描述写不清 |
| 对话历史 | 用户消息、模型回复、工具调用与结果 | 运行时累积 | 线性增长,旧结果占着窗口 |
| 按需加载的内容 | Skill 正文、子目录说明、检索到的文档 | 触发时装入 | 该加载的没加载 |
| 本轮新增 | 最新的用户输入或工具结果 | 用户、工具 | 单条结果过大 |
几点说明:
- 顺序是有讲究的。 以 Claude API 为例,提示缓存按“工具定义 → 系统提示 → 消息”的顺序构成前缀,改动任何一个工具定义,整个缓存都会失效。所以工具定义和系统提示合起来是“固定前缀”,会话中途不要碰(缓存机制详见 提示缓存)。
- 位置也影响权重。 系统提示代表运营方,消息区里的内容代表用户或外部数据。Claude Code 的文档说明,项目说明文件
CLAUDE.md是作为系统提示之后的一条用户消息送进去的,而不是系统提示的一部分;所以它是“很重要的上下文”,不是强制规则。哪些内容该放在哪一层,见 指令的四层结构。 - 来源决定可信度。 工具结果、网页、用户上传的文件永远只是数据,不因为它排在哪里就获得指令的权威,参见 Prompt 注入。
以 Claude Code 为例,官方文档的示意会话里,用户还没输入任何字,系统提示、自动记忆索引、环境信息、MCP 工具名称、Skill 描述、用户级和项目级 CLAUDE.md 就已经装好,合起来有数千 Token;用户的第一句话只有几十 Token。真正让上下文膨胀的,是之后一次次的读文件和搜索结果。
每一轮循环里哪些部分在变
装配不是一次性的动作,而是每一轮都要重做。好的装配器遵守一条纪律:只在末尾追加,不回头改前面。 这样上一轮的完整请求就是这一轮请求的前缀,缓存能持续命中,模型看到的开头也始终一致。
用 Coding Agent 修一个登录报错的过程来看,各部分在每一轮的变化如下:
图里有三个值得注意的细节,都来自 Claude Code 的官方文档:
- 按需内容是“追加”进来的。 子目录里的说明文件、声明了适用路径的规则,都是在 Agent 读到匹配的文件时,作为消息追加到历史里,而不是插回开头。这样既省预算,又不破坏前缀。
- 压缩是一次“重装”。 历史被替换成结构化摘要后,项目根目录的说明文件和自动记忆会从磁盘重新注入,最近修改过的几个文件会被重新读取;但那些按路径加载的规则和子目录说明,会随历史一起被摘要掉,要等再次读到相关文件才重新出现。如果某条规则必须在压缩后依然有效,就不能只做成按需加载。
- 系统提示里的“动态信息”会拖累缓存。 Claude Agent SDK 的文档举过一个例子:
claude_code预设会把工作目录、平台、Shell 等会话信息写进系统提示,于是两个在不同目录运行的会话即使配置相同也无法共享缓存。SDK 为此提供了一个选项,把这些信息移到第一条用户消息里;代价是放在用户消息里的内容权重略低。这正是装配中典型的取舍:位置越靠前越权威、越利于复用,但也越不能变。
反过来,会破坏前缀的操作主要有三类:会话中途增删工具、在系统提示里放每次都变的内容(时间、请求 ID)、清理或压缩历史。前两类应当避免,第三类无法避免,就要“攒一批再做”,具体策略见 长任务的上下文管理。
预算:窗口是一块需要分配的地
上下文窗口有上限,而且就算没到上限,塞得越满模型也越容易分心(参见 上下文腐化)。Anthropic 在谈上下文工程时给过一个很好的标准:找到能让期望结果最可能发生的、最小的一组高信号 Token。落到装配器上,就是把窗口当成一块要分配的地:
- 固定开销设上限。 系统提示、工具定义、项目说明、记忆索引每一轮都在,它们多一千 Token,整场会话的每一轮都多一千 Token。Claude Code 的做法可以参考:建议每个
CLAUDE.md控制在 200 行以内;自动记忆只在启动时加载索引文件的前 200 行或前 25KB;MCP 工具默认只列名称,完整定义通过工具搜索按需加载(参见 渐进式披露)。 - 增长部分要治理。 历史和工具结果是真正的大头:单条工具结果要在工具层截断(见本模块的 工具运行时),旧的工具结果可以清理,总量到阈值时压缩或交给 子 Agent 在独立窗口里处理。
- 给输出留余量。 请求里的
max_tokens和思考预算也要算进窗口,输入塞得太满,回答就会被截断。
把装配写成一个函数
装配器最好是一个独立、可测试、可记录的函数,而不是散落在循环各处的字符串拼接。以订单售后诊断 Agent 为例,伪代码如下:
def build_request(session, new_items):
# ① 固定前缀:整场会话保持逐字不变
tools = session.tool_definitions # 会话开始时确定,中途不增删
system = session.system_prompt # 不放当前时间、请求 ID
# ② 会话开头的上下文(项目说明、用户画像、记忆索引)在第一轮已写入 history
# ③ 本轮新增只追加到末尾,从不回头修改前面的消息
session.history.extend(new_items) # 用户输入,或上一轮的工具结果
# ④ 预算检查:超过阈值才压缩,并且一次压缩一大段
if count_tokens(tools, system, session.history) > session.budget.compact_at:
session.history = compact(session.history) # 会触发一次缓存重写
session.history[:0] = reload_pinned_context() # 重新注入必须常驻的说明
log_composition(tools, system, session.history) # 记录每块的 Token 数,便于排查
return {"tools": tools, "system": system, "messages": session.history,
"max_tokens": session.budget.output_reserve}
注意最后的 log_composition:出了问题时,第一件事就是回看“那一轮模型到底看到了什么”。没有这份记录,你只能猜。
常见误区
- “模型记得之前说过的话。” 模型本身什么都不记得,它“记得”只是因为 Harness 把历史又发了一遍。被压缩或清理掉的内容,对模型来说就是从未发生过。
- “重要的东西越多越好,全放进系统提示。” 固定开销会在每一轮重复计费,还会稀释注意力。系统提示只放每一步都适用的规则,其余按需加载。
- “动态切换工具集更精准。” 效果上也许如此,但中途改工具定义会让整个缓存失效。工具很多时,优先用工具搜索这类“追加式”的方案。
设计要点
- 画出你的 Agent 的请求组成图,标出每一块的顺序、来源、稳定程度和预估 Token 数。
- 固定前缀里不出现时间戳、随机 ID、用户姓名等每次都变的内容。
- 新内容只往末尾追加;按需加载的说明、Skill 正文也以追加的方式进入。
- 为固定开销和单条工具结果分别设上限,并确定压缩阈值和压缩后必须重新注入的内容。
- 每一轮记录请求的组成和缓存命中情况,作为排查和回归的依据。
练习
在 最小 Agent 实操 的 agent.py 基础上完成下面的改造,约 45 分钟:
- 在每次调用模型后打印
response.usage里的input_tokens、cache_creation_input_tokens和cache_read_input_tokens,观察它们随轮次如何变化(需要先按 提示缓存 一课开启缓存)。 - 故意在系统提示开头加一行当前时间(精确到秒),再跑一次,对比缓存读取量的变化。
- 画出这个 Agent 第 1、2、3 轮请求的组成表,标出哪些部分不变、哪些是追加的。
如果你在用 Claude Code,可以在会话里运行 /context,对照图 1 看看你的会话里每一块各占多少。
延伸阅读
- Claude Code 文档:Explore the context window,一次会话的上下文如何被填满,以及压缩后哪些内容会重新加载。
- Claude 文档:Prompt caching,缓存前缀的顺序与哪些改动会让缓存失效。
- Claude Agent SDK 文档:Modifying system prompts,系统提示的几种写法,以及动态信息对缓存的影响。
- Anthropic Engineering:Effective context engineering for AI agents,把上下文当作有限资源来管理。
下一步
装配顺序里有一块内容完全由你的团队来写:项目说明文件。下一课「项目说明文件与配置分层」讲 CLAUDE.md、AGENTS.md 怎么写,以及用户、项目、本地三层配置如何叠加。