Appearance
Context 如何组装:Assembly Pipeline 与 Token Budget
前置:为什么只写 Prompt 不够:从 Prompt Engineering 到 Context Engineering
Context 不是把“所有可能有用的信息”拼起来,而是一条受预算约束的装配流水线。ContextManager 先放 protected instructions/constraints,再从 history、retrieval、memory、tool catalog 中选 working set,最后为模型输出保留余量。本文只定义 Assembly 的处理阶段和 Token Budget;具体“哪些候选信息应该被选中”放到下一篇 Selection。
Mental Model
Context 不是存储层。它是一次 inference 的 working set:可持久事实先存在各自 Store,ContextManager 只在当前 turn 做组合、裁剪和来源标注。
Context Assembly Pipeline
Context assembly 应成为显式 pipeline,而不是各模块随意 messages.append(...):
text
protected instructions / constraints
↓
candidate history / memory / retrieval / tool catalog
↓ attach provenance + priority + estimated tokens
selection
↓
budget allocation / compaction
↓
normalized ModelRequest + ContextSnapshot这样一次模型调用为什么看见某段信息、为什么丢掉另一段信息,都能从 ContextSnapshot 追出来,也能在测试里固定输入重放。
🔍 源码对照:Context Assembly 不是拼字符串,而是 Runtime Projection
精确定位
- 文件:
source/host/runner/system-prompt-assembly.ts - Symbol:
createSystemPromptAssembly()→getSystemPrompt()→getMemorySection() - 深入索引:Code Atlas:02-context-assembly
ts
function getSystemPrompt(snapshot) {
const sections = [resolveBasePrompt()]
add(sections, getProfileSection(snapshot))
add(sections, getUserIdentitySection())
add(sections, getTimeZoneSection())
add(sections, getMemorySection())
add(sections, getWorkflowsSection())
add(sections, getChannelsSection())
add(sections, mcpDiscoveryStatusSection())
return sections.join("\n\n")
}对照理解
Runtime持有的 profile、memory、workflow、MCP 状态并不会自动进入模型;getSystemPrompt()是一次明确的 projection/assembly。- 因此 Runtime State ≠ Model Context。Context Engineering 的核心工作就是决定哪些 section 在这一 Turn 对模型可见。
getMemorySection()还带有 snapshot/freeze 逻辑,说明 Context Assembly 与长期状态存储是两层职责。
源码说明
system-prompt-assembly.ts 的 dependency surface 同时包含 memory、profile、workflow、connector、MCP discovery/custom instructions、request timezone、remote box/computer 等信息源。这个结构很适合验证本文的核心观点:Context 不是 conversation history 的别名,而是 Runtime 在当前 Turn 对多种候选信息做的一次投影。源码还维护 prompt snapshot/compaction epoch 一类状态,说明“本轮到底组装了什么”本身也需要可追踪。
工程边界
这里值得学的是 assembly boundary 和 snapshot 思维;不应把它具体有哪些 prompt section、如何拼字符串当成通用 Context 架构。产品拥有的信息集合与 Shadow Harness 完全不同。
Shadow Harness 对照
- 让
ContextBuilder接受显式 source inputs,而不是在 ModelClient 里临时到处读取状态。 - 输出
ContextSnapshot,至少记录 selected items、tool catalog version、budget/compaction decision。
验证
同一 RunState 固定不变,只改变可用 Tool 或 Memory snapshot;断言 ModelRequest 的变化是可解释、可重放的,并且 protected instructions 不会被 budget eviction。
Token Budget
Token Budget 把有限窗口当成可分配资源:protected instructions/state 有最低保留量,Tools/History/Retrieval 在剩余预算中竞争,同时为 output 和不可预测增长预留 headroom。达到 soft limit 时先淘汰可重建信息,不能等 provider 报 overflow 后才粗暴 truncate。
一次真实的 Context Assembly 怎么做
假设一个 Coding Agent 要“定位并修复登录 token 刷新失败”。模型有 32K context window,但 Runtime 不能把 32K 全部塞满。一个更实用的预算可能是:
text
system / developer instructions 2K
当前用户任务 1K
tool definitions 4K
最近几轮关键 observation 5K
检索到的 auth 相关代码 9K
已有 plan / protected state 3K
---------------------------------------
已用 24K
为模型输出、tool result、意外膨胀预留 8K这里最重要的不是这些数字本身,而是 budget 是 Context Manager 的决策输入。如果检索突然返回 20K 日志,Context Manager 应该先压缩或裁剪低价值内容,而不是把 output reserve 挤到接近 0。
可以把 assembly 看成一个确定性的流水线:
text
collect candidates
→ mark protected items
→ score/select optional items
→ apply per-category budget
→ order into provider request
→ verify total budget + output reserveprotected state 与普通历史消息的地位不同。例如用户已经批准“只允许修改 auth/ 目录”,这条约束不应因为最近日志太长被裁掉。反过来,一段 8K 的重复 shell 输出即使很新,也可能只保留 error summary + artifact reference。
关联源码
自测
- 预算只剩 8k token 时,system constraints、pending state、retrieval evidence、旧 history 应按什么原则取舍?为什么不能只按时间顺序截断?
- 如果输入把窗口全部吃满,没有为输出预留空间,最可能在哪个阶段失败?Context budget 应怎样提前避免?