第十八章:Context Engineering 与上下文装配
18.1 本章边界:装配管线,不是压缩策略
第二章 2.8 节已经把 Context Engineering 定义为"管理有限上下文"的四类操作(Write / Select / Compress / Isolate),第十章完整展开了 Compress 这一类操作的具体方法(Sliding Window、Summarization、Structured Extraction 等)。这两处讲的是"该保留什么、该压缩什么"的决策问题。本章讲的是更下游的工程问题:harness 在每一次模型调用之前,具体把哪些来源的内容、按什么顺序、拼装成最终发给模型的那一份 payload,以及这个装配过程如何与 Prompt Cache 配合而不互相拆台。这是 17.3 节状态机图中 "装配上下文" 这一步的展开。
18.2 一次请求的上下文由哪些部分拼成
一次发给模型的请求,通常由五类互相独立维护的来源拼接而成:
flowchart TB
SP["System Prompt
身份、总则、输出格式约束"]
INST["Instructions / Memory
项目级配置(AGENTS.md、Skill、长期记忆)"]
TOOLS["Tool Definitions
本轮可用工具的 Schema"]
HIST["Conversation History
Working Memory 中的历史消息"]
USER["Current Turn Input
本轮新增的用户输入/工具结果"]
ASSEMBLE["装配管线"]
REQ["最终 Request Payload"]
SP --> ASSEMBLE
INST --> ASSEMBLE
TOOLS --> ASSEMBLE
HIST --> ASSEMBLE
USER --> ASSEMBLE
ASSEMBLE --> REQ
这五类来源的生命周期完全不同:System Prompt 通常整个会话不变;Instructions/Memory 按任务或项目变化(第七、八章讨论的长期记忆检索结果就注入在这里);Tool Definitions 可能随本轮上下文动态收缩(第 19 章 19.8 节的渐进式披露);Conversation History 每轮追加;Current Turn Input 每轮全新。装配管线的职责就是把这五路不同节奏变化的内容,稳定地拼成一份连贯的请求。
18.3 装配顺序为什么重要
装配顺序不是审美问题,而是同时影响三件事:
- Prompt Cache 命中率。 主流模型的 Prompt Caching 按"从头开始的最长公共前缀"匹配缓存(第十章 10.14–10.18 节已详细讨论 Prompt Caching 的机制与限制)。如果易变的内容(比如当前时间戳、本轮的工具结果)被放在前面,会导致后面几乎不变的大段内容(工具定义、系统提示词)也无法命中缓存。装配顺序的第一条原则是:把最稳定的内容放在最前面,把最易变的内容放在最后面。
- 模型的注意力分配。 位置越靠近末尾,通常获得越强的近因效应;关键指令放在系统提示词开头和当前输入结尾,往往比塞在历史消息中间更容易被遵守。
- 工具调用的确定性。 MCP 规范明确要求"服务器应当以确定的顺序返回工具列表(相同的工具集合下,多次请求返回顺序一致),这样确定性顺序能让客户端可靠地缓存工具列表,并提升工具在模型上下文中出现时的 Prompt Cache 命中率"(Model Context Protocol: Tools)——这条协议层面的要求,本质上是 18.3 条第一点在工具定义这个具体来源上的落地。
18.4 System Prompt 的分层来源
生产级 harness 的系统提示词很少是一整块写死的字符串,而是分层拼接:
- 产品层默认指令(harness 内置,不可被用户配置覆盖,例如安全边界);
- 组织/项目级配置(第三章 3.6 节讨论的 AGENTS.md 属于这一层,通常在会话开始时读取一次);
- 技能与命令(第三章 3.5 节的 Skill,按需渐进式披露,往往只注入摘要而非全文,直到被显式调用);
- 运行时修改(部分 harness 允许在会话中途追加系统级指令,例如 Claude Agent SDK 提供修改系统提示词的机制,见 Claude Agent SDK: Modifying system prompts)。
这几层的加载时机不同:会话启动时一次性加载 vs 按需触发加载,是 18.2 节 "Instructions / Memory" 这一路输入内部还需要再拆分的子结构。
18.5 工具定义注入的代价
工具 Schema 本身要消耗上下文预算,而且是每一轮都要重复发送的固定开销(模型 API 通常是无状态的,每次调用都要带上完整的工具定义)。这意味着工具数量和 Schema 复杂度直接线性影响每一轮的固定成本。第 19 章 19.8 节会讨论几种降低这项开销的机制(渐进式披露、Code Execution with MCP),本章只强调装配层的结论:工具定义应该被当作"每轮都要付的税"来预算,而不是一次性成本。
18.6 历史消息装配:从 Working Memory 到 Prompt
第七、八章定义的 Working Memory 是 harness 在进程内维护的结构化状态(近期消息、任务状态、草稿、工件引用);装配管线要把这些结构化状态序列化成模型能理解的消息序列。这一步不是简单的字符串拼接,至少要处理三个问题:
- 工具调用与工具结果必须严格配对,且 ID 匹配(第 19 章 19.3 节的调用契约)——断开配对会导致模型 API 直接拒绝请求。
- 压缩后的历史(第十章的 Summarization/Structured Extraction 结果)要以模型能识别的形式插入,通常作为一条系统级或工具级的"历史摘要"消息,而不是伪装成用户或助手说过的话。
- 多模态或超大工具结果(如整份文件、长日志)不应该原样内嵌,而应替换为引用 + 按需检索的占位符——这正是第二章 2.8.4 节 "Just-in-Time 检索" 在装配层的具体动作。
18.7 Just-in-Time 装配与 Prompt Cache 断点对齐
Just-in-Time 检索的思路(不预先把所有可能用到的内容塞进上下文,而是让 Agent 自己在需要时用工具去取)和 Prompt Cache 之间存在一个容易被忽略的张力:如果 JIT 检索的结果被插入到历史消息中间,会破坏这条消息之后所有内容的缓存前缀。因此装配管线通常约定一个显式的"缓存断点"位置——系统提示词和工具定义结束之处——只要 JIT 检索结果被追加在断点之后而不是插入断点之前,就不会使前面已经稳定的部分失效。这是 18.3 条第一条原则在实现层面的具体落地方式。
18.8 装配时的预算控制
装配管线在真正发起模型调用之前,需要对 18.2 节五类来源做一次预算校验。用 $B$ 表示模型上下文窗口的总预算,$b_i$ 表示第 $i$ 类来源占用的 token 数,装配必须保证:
$$ \sum_{i=1}^{5} b_i \le B - r $$
其中 $r$ 是为本轮模型输出预留的最小 token 余量。当左侧超过阈值时,装配管线需要按优先级降级——通常的降级顺序是:先触发第十章讨论的压缩策略缩减 Conversation History,再考虑收缩 Tool Definitions(渐进式披露只保留最相关的一批工具),System Prompt 和 Current Turn Input 通常是预算控制中最后才会被裁剪的部分,因为裁剪它们直接影响任务正确性。
18.9 一个装配管线的参考实现结构
def assemble_context(session, turn_input):
parts = []
parts.append(load_system_prompt(session)) # 最稳定,放最前
parts.append(load_project_instructions(session)) # AGENTS.md / Skill 摘要
parts.append(load_tool_definitions(session)) # 稳定,紧随其后
# --- Prompt Cache 断点 ---
parts.append(session.working_memory.history()) # 易变,断点之后
parts.append(turn_input) # 最易变,放最后
budget_check_and_compress(parts, session.token_budget)
return render(parts)
这个结构只是示意:真实实现还要处理工具调用/结果配对校验、多模态占位符展开、以及与第 21 章 checkpoint 的交互(装配前需要先从 checkpoint 恢复 Working Memory)。
18.10 常见错误
- 把易变内容放在提示词最前面。 直接摧毁 Prompt Cache 命中率,是最常见也最容易被忽视的性能问题。
- 工具结果直接原样拼进历史消息,不做大小限制。 一次调用返回的整份日志或文件会瞬间吃掉大半上下文预算,应参考第十章的压缩与摘要策略处理。
- 预算超限时统一整体截断,不分优先级。 应该按 18.8 节的降级顺序处理,而不是简单粗暴地砍掉最早的 N 条消息(可能砍掉仍在生效的任务约束)。
- 把装配逻辑和压缩策略耦合在一起实现。 装配管线应该只负责"按顺序拼接、校验预算、触发降级",具体怎么压缩应该委托给独立的压缩模块(第十章),保持职责分离便于替换压缩算法。
18.11 本章总结
装配管线在每次模型调用前,把 System Prompt、Instructions/Memory、Tool Definitions、Conversation History、Current Turn Input 五类互相独立变化的来源,按"最稳定的放最前、最易变的放最后"的顺序拼接成一份请求,这个顺序同时决定了 Prompt Cache 命中率、模型注意力分配和工具调用的确定性。System Prompt 本身是分层来源的叠加;工具定义是每轮重复的固定开销;历史消息装配需要保证工具调用配对、处理压缩结果和大结果的引用化;JIT 检索结果应插入在显式的缓存断点之后;预算超限时按可预测的优先级降级,而不是简单粗暴截断。