同一个模型、同一条 Prompt,只因为”每轮看见的信息”不同,Agent 表现可以判若两人。这篇教程讲清上下文工程的对象与失效机制、压缩截断与按需加载策略、长上下文的注意力稀释问题,以及它与 RAG、Prompt 工程的协同边界。

它解决什么问题

上下文窗口是模型唯一的”工作记忆”,也是一块严格受限的稀缺资源:系统指令、历史消息、工具结果、检索片段全部挤在同一个窗口里按 Token 计费。窗口不够时,任务信息进不来;窗口塞满时,关键约束被噪声淹没。这两种失败都表现为 Agent”突然变笨”。

一个直观的售后例子:用户说”上周买的耳机右耳没声音了”。上下文贫乏的 Agent 只会反问订单号和故障现象;而在调用模型前先把订单记录、保修状态、历史工单查好、挂好退换工具的 Agent,可以直接给出”3 月 25 日购买、仍在退换期、仓库有货,是否帮您发起换货”的结论。模型没变,变的是上下文供给。上下文工程要回答的问题就是:每一轮调用,窗口里该放什么、怎么组织、什么时候换出。

核心原理

一次 LLM 调用实际携带的上下文,通常由几类内容组成,各自的角色与易错点不同:

  • 系统提示:角色、约束、输出格式,优先级最高,必须防止被截断或被用户输入覆盖。
  • 历史消息:会话的短期记忆,随轮次单调膨胀,是窗口溢出的主要来源。
  • 工具结果(Observation):外部世界的反馈,原文往往冗长,需要摘要并保留可回查引用(traceId、错误码、位置)。
  • 检索片段:RAG 捞回的证据,质量取决于召回与排序,噪声片段会直接误导判断。
  • 工具描述与 Schema:也占用窗口,挂载几十个相似工具本身就是一种污染。

与 Prompt 工程的分界可以这样记:Prompt 工程管”指令怎么写”,一次写成、相对静态;上下文工程管”每轮调用实际带入哪些信息”,随任务动态变化、逐轮组装。只优化 Prompt 不管上下文,Agent 跑几轮就可能丢失目标——指令写得再漂亮,证据没进窗口也是白搭。

深入机制

注意力稀释:能放进去,不等于能用好

Transformer 通过注意力机制在上下文中筛选与当前生成最相关的内容。上下文越长,候选内容越多,干扰项也越多,模型定位关键证据的难度随之上升。两个已被反复观察到的现象:Lost in the Middle——模型对窗口开头与结尾的内容更敏感,夹在中间的关键信息容易被”看漏”;Context Rot——随着输入变长、噪声增多,模型对关键证据的利用不再稳定,标称 100K 的窗口不代表能稳定用好 100K。因此重要约束应靠近首尾放置,关键证据要显眼、去重、带编号,而不是淹没在日志洪流里。

压缩与截断:窗口快满时的取舍

长任务消息持续追加,接近上限时必须腾挪空间,常见三件套:

  • Compaction(压缩):把早期历史摘要成一段,保留架构决策、未解决问题、关键结论,丢弃冗余工具结果,再以摘要衔接后续轮次。难点在取舍粒度——留太多失去压缩意义,留太少丢失约束与版本号。
  • 结构化笔记(Note-taking):让 Agent 把进度、已知问题、下一步计划写到外部文件(如 NOTES.md),上下文重置后再读回恢复状态,相当于把记忆外挂到文件系统。
  • Sub-agent 隔离:检索、代码阅读等探索性任务交给独立上下文的子 Agent,主 Agent 只接收一两千 Token 的证据摘要,探索过程不占用主窗口。

按需加载:Just-in-Time 取代一次性塞入

预检索在调用前一次性把片段塞进窗口,适合 FAQ 这类信息需求稳定的任务;但 Agent 任务的信息需求随执行变化,更合理的方式是只保留轻量引用(文件路径、URL、查询句柄),需要时再通过工具取回内容——Anthropic 称之为渐进式披露(Progressive Disclosure)。代码 Agent 是典型场景:先看目录结构和文件名收窄范围,再用 grep、head 按需读取,而非把整个仓库倒进窗口。

与 RAG 的协同

RAG 可以看作上下文工程的一个子系统,负责”检索什么、结果怎么进窗口”。协同要点有三:检索片段进入窗口前去重、重排并标注来源,避免模型混淆用户输入与外部资料;追问型对话中按轮次增量加载新证据,而不是每轮重复全量检索;低分无结果时降级到关键词检索或让 Agent 向用户澄清缺口,而非硬答。

工程实践

把组装逻辑收敛到一个 Context Assembler,在每次调用前统一完成装配:

def assemble(task, session, budget):
parts = []
parts.append((P0, load_system_rules())) # 固定区:约束与目标,防截断
parts.append((P1, current_goal(task, session))) # 当前任务目标
parts.append((P2, retrieve_evidence(task))) # 检索证据:重排、去重、带来源
parts.append((P2, summarize_tool_results(session)))# 工具结果摘要 + 可回查引用
parts.append((P3, compact_history(session))) # 历史:近期原文 + 早期摘要
return fit_token_budget(rank(parts), budget) # 排序后按预算裁剪

两个关键决策点:rank 决定谁先谁后(重要约束靠近首尾),fit_token_budget 决定谁保留原文、谁压成摘要、谁只留引用。配套的做法是:按模块划分 Token 预算并设优先级;记录每轮实际进入窗口的完整消息,出问题时对比前后两轮的 context diff——“突然偏题”类故障,这是最高效的定位手段。评估策略优劣时,除任务成功率外,还要看平均输入 Token 与 P95 延迟,避免准确率上升但成本失控。

对比与权衡

策略 上下文干净度 延迟与调用次数 适合任务
预检索全量注入 差,易混入噪声 低,一次到位 FAQ、固定知识库问答
Just-in-Time 按需加载 好,证据按需进入 高,工具调用增多 代码库分析、故障排查、开放研究
混合策略 较好,静态预载 + 动态拉取 中 复杂业务 Agent、长任务
长任务手段 适用场景
Compaction 需保持连贯对话的长流程
结构化笔记 有清晰里程碑的迭代式任务
Sub-agent 可并行探索、最终汇总的研究型任务

面试视角

上下文工程和 Prompt 工程的区别? Prompt 工程管指令的写法,相对静态;上下文工程管每轮调用实际带入窗口的信息选择与组织,是逐轮动态决策。后者还覆盖工具结果、记忆、检索片段与 Token 预算。

为什么窗口越大效果不一定越好? 注意力稀释:候选内容增多使关键证据更难被定位,Lost in the Middle 与 Context Rot 让中间与噪声区域的信息利用率下降。准确说法是退化程度取决于模型、上下文结构与任务类型,但”放进去”与”用好”是两回事。

上下文快满时有哪些手段,怎么选? 保持对话连贯用 Compaction 压缩历史;有里程碑的长任务用结构化笔记外挂状态;可拆分的探索任务用 Sub-agent 隔离上下文。三者常组合,短任务没出现膨胀时不必提前引入。

如何评估上下文策略的改动是否有效? 固定任务集,只改一个变量(如检索 top-k、摘要方式),对比成功率、平均输入 Token 和 P95 延迟;用 Trace 回放每轮窗口内容,对比 context diff 定位偏题来源。

上下文工程与 RAG 是什么关系? RAG 是上下文工程的子系统,解决证据的检索与注入;上下文工程还要管检索结果进窗口后的去重、排序、预算分配,以及历史、工具结果、记忆的整体装配。

小结

  1. 上下文窗口是稀缺资源,上下文工程的对象是”每轮调用带入什么、何时换出”,与 Prompt 工程动静互补。
  2. 窗口由系统提示、历史、工具结果、检索片段、工具描述共同占用,需按优先级分配 Token 预算。
  3. 长上下文存在注意力稀释(Lost in the Middle、Context Rot),重要约束放首尾,关键证据去重显眼。
  4. 压缩三件套——Compaction、结构化笔记、Sub-agent——分别对应连贯对话、里程碑任务、并行探索。
  5. 落地从 Context Assembler 与逐轮 Trace 开始:先能回放,再谈优化;高信噪比永远比信息量重要。