10.03概念更新于 2026年9月28日

上下文装配:每一轮请求怎么拼出来

系统提示、工具定义、项目说明、记忆、历史与工具结果的组装顺序和预算。

10.03 · 课时讲解

上下文装配:每一轮请求怎么拼出来

越稳定越靠前,新内容只往末尾追加,再在窗口预算内决定每一块放多少。

1 / 12

一句话理解:模型不记得上一轮,所以 Harness 每一轮都要把完整请求重新拼一遍:越稳定的内容越靠前,新内容只往末尾追加,再在窗口预算内决定每一块放多少;模型看到什么、花多少钱、会不会“忘事”,都在这一步决定。

在 最小 Agent 实操 里,你已经见过装配的雏形:messages 列表每轮追加一条模型回复和一条工具结果,然后整个列表连同系统提示、工具定义一起再发一次。模型 API 是无状态的,每一轮请求都必须自带全部上下文。

这件事在 Demo 里不起眼,上了真实任务却会变成三类问题:第 30 步的时候模型“忘了”开头的规则;账单里重复发送的固定内容占了大头;上下文窗口被一次巨大的工具结果撑爆。它们看起来是模型问题,根子都在装配器。这一课讲清楚一次请求由哪几块组成、按什么顺序拼、每一轮哪些部分在变,以及预算怎么分。

一次请求由什么组成

不管用哪家模型、哪个框架,一次 Agent 请求大体都由下面几块拼成。按从前到后的顺序画出来,并标上每一块的稳定程度:

一次请求的组成:从前到后依次是工具定义、系统提示、项目说明、记忆与 Skill 目录、对话历史、按需内容和本轮新增,越往下越易变 请求内容(从前到后拼接) 稳定程度 固定前缀 会话开头 消息区 工具定义 名称、描述、参数 Schema 会话内不变 系统提示 身份、安全规则、环境信息 按版本更新 项目说明与规则 CLAUDE.md、AGENTS.md 按项目稳定 记忆与 Skill 目录 记忆索引、Skill 名称和描述 按用户稳定 对话历史 用户消息、模型回复、工具调用 逐轮追加 按需加载的内容 Skill 正文、子目录说明、检索结果 触发时追加 本轮新增 最新工具结果或用户输入 每轮都变 稳定 易变 上一轮的完整请求就是这一轮的前缀:只要不回头改前面,缓存就能命中
图 1 · 一次请求的组成:越稳定越靠前,越易变越靠后
部分 典型内容 谁产生 最常见的问题
工具定义 每个工具的名称、描述、参数 Schema 研发、MCP Server 数量一多成了隐形大户
系统提示 产品身份、安全底线、工具使用规范、运行环境 产品团队 被塞进时间戳等易变信息
项目说明与规则 构建命令、代码规范、业务约定 项目团队 越写越长、互相矛盾
记忆与 Skill 目录 记忆索引、Skill 的名称和一句话描述 用户、Agent、领域专家 过期或描述写不清
对话历史 用户消息、模型回复、工具调用与结果 运行时累积 线性增长,旧结果占着窗口
按需加载的内容 Skill 正文、子目录说明、检索到的文档 触发时装入 该加载的没加载
本轮新增 最新的用户输入或工具结果 用户、工具 单条结果过大

几点说明:

  • 顺序是有讲究的。 以 Claude API 为例,提示缓存按“工具定义 → 系统提示 → 消息”的顺序构成前缀,改动任何一个工具定义,整个缓存都会失效。所以工具定义和系统提示合起来是“固定前缀”,会话中途不要碰(缓存机制详见 提示缓存)。
  • 位置也影响权重。 系统提示代表运营方,消息区里的内容代表用户或外部数据。Claude Code 的文档说明,项目说明文件 CLAUDE.md 是作为系统提示之后的一条用户消息送进去的,而不是系统提示的一部分;所以它是“很重要的上下文”,不是强制规则。哪些内容该放在哪一层,见 指令的四层结构。
  • 来源决定可信度。 工具结果、网页、用户上传的文件永远只是数据,不因为它排在哪里就获得指令的权威,参见 Prompt 注入。

以 Claude Code 为例,官方文档的示意会话里,用户还没输入任何字,系统提示、自动记忆索引、环境信息、MCP 工具名称、Skill 描述、用户级和项目级 CLAUDE.md 就已经装好,合起来有数千 Token;用户的第一句话只有几十 Token。真正让上下文膨胀的,是之后一次次的读文件和搜索结果。

每一轮循环里哪些部分在变

装配不是一次性的动作,而是每一轮都要重做。好的装配器遵守一条纪律:只在末尾追加,不回头改前面。 这样上一轮的完整请求就是这一轮请求的前缀,缓存能持续命中,模型看到的开头也始终一致。

用 Coding Agent 修一个登录报错的过程来看,各部分在每一轮的变化如下:

每一轮循环中请求各部分的变化:固定前缀和项目说明保持不变,历史逐轮追加,压缩时历史被改写为摘要 第 1 轮 第 2 轮 第 3 轮 压缩之后 工具定义与系统提示 写入缓存 首次发送 不变 缓存命中 不变 缓存命中 不变 缓存命中 项目说明与记忆 写入缓存 会话开头加载 不变 缓存命中 不变 缓存命中 重新注入 从磁盘再读一遍 对话历史 空 还没有历史 追加 第 1 轮的调用 追加 第 2 轮的调用 变成摘要 缓存需重新写入 本轮新增 用户输入 修复登录报错 工具结果 读文件、跑测试 工具结果 附带路径规则 续接任务 重读最近文件 不变 新增或追加 被改写
图 2 · 以 Coding Agent 为例:正常轮次只在末尾追加,压缩会改写历史

图里有三个值得注意的细节,都来自 Claude Code 的官方文档:

  1. 按需内容是“追加”进来的。 子目录里的说明文件、声明了适用路径的规则,都是在 Agent 读到匹配的文件时,作为消息追加到历史里,而不是插回开头。这样既省预算,又不破坏前缀。
  2. 压缩是一次“重装”。 历史被替换成结构化摘要后,项目根目录的说明文件和自动记忆会从磁盘重新注入,最近修改过的几个文件会被重新读取;但那些按路径加载的规则和子目录说明,会随历史一起被摘要掉,要等再次读到相关文件才重新出现。如果某条规则必须在压缩后依然有效,就不能只做成按需加载。
  3. 系统提示里的“动态信息”会拖累缓存。 Claude Agent SDK 的文档举过一个例子:claude_code 预设会把工作目录、平台、Shell 等会话信息写进系统提示,于是两个在不同目录运行的会话即使配置相同也无法共享缓存。SDK 为此提供了一个选项,把这些信息移到第一条用户消息里;代价是放在用户消息里的内容权重略低。这正是装配中典型的取舍:位置越靠前越权威、越利于复用,但也越不能变。

反过来,会破坏前缀的操作主要有三类:会话中途增删工具、在系统提示里放每次都变的内容(时间、请求 ID)、清理或压缩历史。前两类应当避免,第三类无法避免,就要“攒一批再做”,具体策略见 长任务的上下文管理。

预算:窗口是一块需要分配的地

上下文窗口有上限,而且就算没到上限,塞得越满模型也越容易分心(参见 上下文腐化)。Anthropic 在谈上下文工程时给过一个很好的标准:找到能让期望结果最可能发生的、最小的一组高信号 Token。落到装配器上,就是把窗口当成一块要分配的地:

上下文窗口的预算分配示意:固定开销设上限,增长部分要治理,输出要留余量 上下文窗口(示意,非真实比例) 固定前缀 系统提示 + 工具 会话开头 说明 + 记忆 历史与工具结果 随轮次增长 输出预留 回答 + 思考 压缩阈值 固定开销设上限 说明精简、工具按需加载 增长部分要治理 单条截断、旧结果清理、到阈值压缩 留足余量 防止输出被截断
图 3 · 预算分配:固定开销设上限,增长部分要治理,输出要留余量
  • 固定开销设上限。 系统提示、工具定义、项目说明、记忆索引每一轮都在,它们多一千 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 分钟:

  1. 在每次调用模型后打印 response.usage 里的 input_tokens、cache_creation_input_tokens 和 cache_read_input_tokens,观察它们随轮次如何变化(需要先按 提示缓存 一课开启缓存)。
  2. 故意在系统提示开头加一行当前时间(精确到秒),再跑一次,对比缓存读取量的变化。
  3. 画出这个 Agent 第 1、2、3 轮请求的组成表,标出哪些部分不变、哪些是追加的。

如果你在用 Claude Code,可以在会话里运行 /context,对照图 1 看看你的会话里每一块各占多少。

延伸阅读

下一步

装配顺序里有一块内容完全由你的团队来写:项目说明文件。下一课「项目说明文件与配置分层」讲 CLAUDE.md、AGENTS.md 怎么写,以及用户、项目、本地三层配置如何叠加。

试试:上下文、MCP、沙箱、Prompt 注入、外链