Stage 07:别把整个仓库搬上办公桌——Context Engineering
Language: English | 简体中文
上一章我们终于让 Agent 学会了“存档”。
Checkpoint 能让执行在进程重启后继续,Long-term Memory 能保存经过允许的用户偏好,RAG 能提供外部证据,MCP 又能从外部系统带回 Tool Result、Resource 和 Prompt。
现在麻烦来了。
我们手里的信息突然多得像搬家前一天的客厅:
System Instructions
Conversation History
Checkpoint State
Long-term Memory
RAG Evidence
Tool Observations
MCP Resources
Current User Message
...
于是一个非常自然、也非常危险的想法出现了:
“既然这些东西都可能有用,那每次调用模型时全部塞进去吧。”
这就像为了开一个十五分钟的退款会议,把公司档案室、财务仓库和员工食堂菜单全部搬进会议室。资料确实没有遗漏,会议也基本开不下去了。
Stage 07 解决的就是这个问题:
在当前这一轮决策里,模型究竟应该看到什么?
这就是 Context Engineering。
它不是“把 Prompt 写得更文艺”,也不只是“快超过 token limit 了就删前几条消息”。更准确地说,Context Engineering 是一套选择、组织、压缩和标记模型输入信息的方法。
1. 先把 Context 和上一章的 Memory 分开
这是最容易混的地方。
Memory 回答的是:
哪些信息值得被保存?
Context 回答的是:
这一轮模型需要看到哪些信息?
同一条 Long-term Memory 可以存在数据库里很久,却并不需要每一轮都出现在 Context 中。
比如:
用户偏好:回答使用中文
在写中文邮件时可能有用。
如果 Agent 正在执行一个完全不需要自然语言回复的后台数据校验任务,它可能一点用都没有。
所以:
stored != selected
同样,Checkpoint 中的数据也不等于 Context。
Checkpoint 可能包含:
retry_count
workflow_phase
idempotency_key
last_node
internal_error_code
这些对 Runtime 很重要,但模型可能根本不需要看到。
Stage 03 的 State、Stage 06 的 Memory 和本章 Context,分别属于三层:
State
-> 程序继续执行需要什么
Memory / Persistence
-> 什么值得跨时间保留
Context
-> 当前模型调用应该看什么
把它们分开以后,很多设计问题会突然简单很多。
2. Context Window 不是仓库,是一张有限的办公桌
模型的 Context Window 有容量上限。
即使某个模型的窗口已经很大,也不代表“越多越好”。
原因有两个。
第一个最容易理解:输入越多,通常意味着更多成本和延迟。
第二个更关键:无关信息也会参与模型的注意过程。
你问:
“ORDER-42 能不能原路退款?”
真正相关的也许只有:
退款政策
订单日期
当前用户问题
如果同时塞进去:
用户去年的旅行偏好
过去 80 轮对话
三个无关 Tool 的输出
团队周报
旧版退款政策
模型不只是多读了几页纸。
你还主动增加了冲突、误引用和被过时信息带偏的机会。
所以 Context Engineering 的目标不是:
“把窗口填满。”
而是:
让有限的输入预算尽可能被当前决策真正需要的信息占据。
3. Context Budget:先承认预算存在
我们先写一个很朴素的预算:
@dataclass(frozen=True, slots=True)
class ContextBudget:
max_input_tokens: int
reserved_output_tokens: int = 0
为什么还要 reserved_output_tokens?
因为有些模型接口给的是整体上下文限制。输入塞得太满,输出就没有空间。
于是:
usable = max_input_tokens - reserved_output_tokens
本章教学代码使用一个粗略 token estimate:
return max(1, (len(self.content) + 3) // 4)
请注意:这不是精确 tokenizer。
它只是为了让选择算法离线可观察。
真实系统如果需要精确计费或硬限制,应该使用对应模型 tokenizer 或 provider 返回的 usage 信息。
不要把“平均四个字符约一个 token”这种教学近似写进财务结算系统,然后怪语言模型数学不好。
4. Context Item:不要让所有文本都变成匿名字符串
如果 Context Builder 接收的是:
[
"some text",
"more text",
"another text",
]
很快就会回答不了几个重要问题:
- 这是谁提供的?
- 它是什么类型?
- 是必须保留还是可以丢?
- 和当前任务相比有多重要?
- 它来自用户、Memory、Retriever,还是 Tool?
因此我们给 Context Item 加上结构:
@dataclass(frozen=True, slots=True)
class ContextItem:
key: str
content: str
kind: str
priority: int
required: bool = False
provenance: str = "application"
这一步看起来有点像给文件贴标签。
实际上很重要。
因为 Context Engineering 的核心不是字符串拼接,而是信息选择。
没有 metadata,你连“选择依据”都没有。
5. 有些 Context 是 Required,不应该靠打分竞争
假设系统要求:
只能根据提供的政策证据回答,证据不足时明确说明。
同时当前问题是:
ORDER-42 可以原路退款吗?
这两项不是“如果预算够最好留下”。
它们是当前调用成立的前提。
于是:
ContextItem(
key="instructions",
...,
required=True,
)
Builder 先计算 Required Context 的成本。
如果它已经超过预算,本章实现直接抛出:
ContextOverflowError
为什么不偷偷删一点 Required 内容?
因为如果“不能删除的东西”在超限时还是被悄悄删除,那 required=True 就只是一个安慰奖。
有些失败应该 Fail Closed。
这和 Stage 09 以后会不断出现的可靠性原则是一脉相承的。
6. Optional Context 才需要 Priority
Required 放进去以后,剩余空间才交给 Optional Items 竞争。
本章使用一个非常简单的规则:
sorted(
optional,
key=lambda item: (-item.priority, item.key),
)
高 Priority 先选。
例如:
retrieved policy evidence priority 90
recent tool observation priority 80
history summary priority 40
user style memory priority 30
这不是在声称“所有 Context 都应该用一个整数排序”。
生产系统可能使用 recency、semantic relevance、task phase、trust class、cost、source type 等更多因素。
但这个小算法让一件事情变得可见:
Context 是一个 selection problem。
不是一个 "\n".join(all_the_things) problem。
7. Priority 不是 Trust
这里要防止另一个常见误解。
一段 Retrieval Evidence 很相关,所以 Priority 很高。
这不代表它可信。
一条 Tool Result 可能非常重要,也可能来自外部系统。
用户刚刚发来的内容永远很重要,但用户内容当然不能自动变成 System Instruction。
所以至少要区分两个维度:
relevance / priority
vs
authority / trust
本章的 provenance 字段用来保留来源:
ContextItem(
key="retrieved-policy",
kind="evidence",
priority=90,
provenance="policy-handbook",
)
渲染时也不把所有内容糊成一段:
<context kind='evidence'
source='policy-handbook'
key='retrieved-policy'>
...
</context>
这不等于完整 Prompt Injection 防御。
但至少系统没有主动把“来自哪”丢掉。
后面的安全章节会继续处理外部输入的信任边界。
8. Recent History 不是越长越好
聊天系统最容易写成:
messages.append(new_message)
send_everything(messages)
前十轮很舒服。
第一百轮以后,Context 里开始出现:
- 早已过期的讨论;
- 已经纠正过的错误;
- 重复 Tool Result;
- 很久以前的临时计划;
- 大量与当前任务无关的寒暄。
这时一种常见策略是:
保留最近消息
+
压缩更老历史
注意这里说的是“压缩”,不是“把最老消息删掉然后假装它们没存在过”。
9. Compaction 是有损操作
本章的 Teaching Compactor 会把旧消息压成:
user: ...
assistant: ...
并记录:
source_message_ids=("m1", "m2")
为什么要保留这些 ID?
因为 Summary 不是原始事实本身。
它是对原始消息的有损表示。
如果系统以后发现摘要出了问题,至少还知道摘要来自哪些消息。
这就是 Provenance。
我们可以把 Compaction 想成把一箱文件整理成一张会议纪要:
raw history
↓ compact
summary
会议纪要很好用。
但如果合同纠纷需要逐字证据,你不能拿一句:
“大概就是这个意思。”
当原件。
所以:
compacted context != checkpoint
compacted context != source of truth
10. Compaction 解决长度,不自动解决质量
一个很长的旧对话压成 200 token,并不意味着这 200 token 一定值得放进当前 Context。
压缩之后仍然要 Selection。
正确关系更像:
retained history
↓
optional compaction
↓
candidate context item
↓
selection
↓
model context
而不是:
summarized
↓
therefore mandatory forever
“我都费劲总结了,不放进去多亏啊”不是信息架构原则。
11. RAG 和 Memory 都只是 Context 来源
Stage 04 教 RAG 时,我们强调 Retriever 返回的是 Evidence Candidate。
Stage 06 教 Memory 时,我们强调存储的是被允许长期保留的信息。
到了 Stage 07,这两者终于站到同一张桌子前:
RAG Result
\
\
Memory ----> Context Builder ---> Model
/
Tool Result
Context Builder 决定当前到底需要哪些。
例如退款问题里:
退款政策 evidence
-> 高优先级
用户喜欢中文
-> 有用,但优先级较低
用户去年喜欢住靠窗酒店
-> 这轮直接不选
Memory 和 RAG 的价值都不在于“每次自动全量注入”。
它们的价值是提供可被按需选择的信息源。
12. JIT Context:能晚一点拿,就别什么都提前塞
有些数据只有当任务走到某个阶段才需要。
比如:
用户问普通概念
-> 不需要订单详情
用户明确问 ORDER-42
-> 才加载订单状态
模型决定需要退款政策
-> 才检索对应政策
这种 Just-in-Time Context 的思想和 Stage 02 的 Routing、Stage 04 的 Agentic Retrieval 是连起来的。
先判断需求,再加载相关信息。
而不是开场就说:
“欢迎来到系统,这是用户过去五年的所有数据,请慢用。”
延迟加载不仅节省 token,也减少不必要的数据暴露。
13. Tool Schema 也吃 Context
Tool 很容易被忽略。
当模型拥有几十个 Tool 时,每个 Tool 的名称、描述、参数 schema 也会占输入空间。
所以:
100 个 Tool
≠
免费获得 100 项能力
它们会增加:
- Context 成本;
- Tool 选择难度;
- 名称混淆;
- 错误调用概率。
这也是为什么前面一直强调 Router、Capability Boundary 和 Namespace。
Context Engineering 不只管理“聊天消息”。
凡是模型这一轮必须读到的东西,都在争夺注意预算。
14. 一个完整的 Context Assembly
我们的 Demo 有五类候选:
items = [
instructions,
current_question,
retrieved_policy,
memory,
old_history_summary,
]
其中 Instructions 和 Current Question 是 Required。
Policy Evidence Priority 高。
History Summary 和 User Memory 根据剩余预算决定是否进入。
最终:
selection = ContextBuilder().build(
items,
ContextBudget(
max_input_tokens=120,
reserved_output_tokens=30,
),
)
这段代码很短。
但它把“给模型什么”从隐式字符串拼接变成了一个可以观察、测试和调整的系统决策。
15. 为什么要记录 Omitted Items?
Builder 返回:
ContextSelection(
items=...,
used_tokens=...,
omitted_keys=...,
)
omitted_keys 很有用。
想象模型答错了。
你开始排查:
“为什么它不知道退款条款?”
如果系统只能给你最终 Prompt,你可能只知道条款没进去。
如果 Context Builder 能告诉你:
retrieved-policy omitted: insufficient budget
诊断就容易多了。
到了 Stage 10,我们会把这种决策进一步变成 Trace 和 Evaluation 信号。
所以 Context Engineering 本身也应该是可观察的。
16. Context 不是越少越高级
讲了这么多压缩和选择,也不要走向另一个极端:
“最好的 Context 就是最短的 Context。”
不是。
如果删掉真正需要的 Evidence,答案当然会变差。
Context Engineering 追求的是:
足够
+
相关
+
边界清楚
+
预算可控
而不是参加 token 节约比赛。
一个只剩 "answer carefully" 的 Prompt 确实很便宜。
它也确实什么都没告诉模型。
17. 这一章和 Prompt Engineering 是什么关系?
Prompt Engineering 通常关心:
怎么表达 Instructions
怎么组织示例
怎么让输出更稳定
Context Engineering 的范围更大:
哪些信息进入模型
何时进入
从哪里来
以什么形式进入
什么被省略
什么被压缩
Prompt 是 Context 的一部分。
但 Context 还包括:
- Conversation;
- Memory;
- RAG Evidence;
- Tool Schemas;
- Tool Results;
- Runtime 提供的任务数据。
因此在 Agent 系统里,只会调一句 System Prompt 远远不够。
18. 运行完整代码
运行:
python stages/07-context-engineering/code/demo.py
它会先压缩旧 History,再把 Instructions、当前问题、RAG Evidence、Memory 与 History Summary 作为候选交给 Context Builder。
然后观察:
used_tokens
omitted
最终渲染 Context
边界检查:
python stages/07-context-engineering/code/checks.py
它验证 Required Item 不会被 Optional 挤掉;Required 超预算时直接失败;Priority 与输入顺序无关;Output Reservation 会减少可用输入预算;重复 key 被拒绝;Compaction 保留来源;Summary 明确是有损表示。
19. 下一章为什么是 Agent Skills?
现在我们的 Agent 已经会选择 Context。
但还有一种内容很尴尬。
假设 Agent 经常要做:
代码审查
发布检查
数据库迁移
事故复盘
每种任务都有一大段固定的操作说明。
把所有流程说明永远塞进 System Prompt,会再次把 Context 撑胖。
把它们全写死进程序,又失去可移植性和可发现性。
于是下一个自然问题是:
能不能先只知道“有哪些可用流程”,真正需要某个流程时,再加载它的详细说明?
这就是 Progressive Disclosure。
也是 Stage 08 的主角:Agent Skills。