1. 上下文工程到底在解决什么问题
1.1 从一个真实翻车现场说起
去年帮一个团队调他们的客服 Agent,场景不复杂:用户问退货政策,Agent 查知识库,组织语言回复。Demo 阶段一切正常,上线第三天开始出问题。用户问“我上周买的鞋子能退吗”,Agent 回复了一段退货政策,但政策是三个月前下架的旧版。排查了半天,发现问题出在上下文组装环节——知识库检索返回了三条结果,两条新版一条旧版,拼接时旧版排在了最前面,模型优先采信了它。
这就是上下文工程的典型战场。模型本身没变,Prompt 模板没变,变的是喂给模型的那堆 token 里,什么内容排在什么位置、以什么形式呈现、带了多少噪声。
很多人把上下文工程等同于“写 Prompt”,这是最大的误解。Prompt 是你对模型说的话,上下文是模型在回答你之前看到的全部信息——系统指令、历史对话、检索文档、工具返回结果、few-shot 示例、格式约束,甚至包括那些你没注意到的分隔符和空行。上下文工程管的是这一整坨东西怎么组织、怎么裁剪、怎么排序、怎么在有限的窗口里塞进最有价值的信息。
1.2 为什么现在必须单独把它拎出来讲
三个变化凑到了一起。
第一,Agent 的上下文从“单轮”变成了“多轮 + 工具调用 + 检索”的复合结构。以前聊天机器人的上下文就是对话历史,现在一个 Agent 跑一轮,上下文里可能同时有系统提示、用户输入、检索到的五篇文档、两次工具调用的返回、上一次的反思记录。信息量翻了好几倍,但窗口没翻好几倍。
第二,模型对上下文的敏感度被反复验证。同一个问题,关键信息放在开头、中间、结尾,回答质量能差出一大截。业界常说的“lost in the middle”现象——模型对上下文中间部分的信息召回率明显低于首尾——这不是玄学,是大量实测得出的结论。
第三,成本。上下文越长,token 消耗越大,延迟越高,费用越贵。一个设计糟糕的 Agent,光是把无关的历史对话全量塞进去,一天就能烧掉设计良好版本的几倍成本。上下文工程不只是效果问题,也是钱的问题。
1.3 它和 Prompt 工程、RAG 的边界在哪
我用一个类比说清楚。把模型当成一个刚入职的聪明员工:
- Prompt 工程是你教他怎么理解任务、用什么语气说话,相当于岗位培训。
- RAG是给他配了一个资料库,让他遇到不懂的能去查,相当于给他开了数据库权限。
- 上下文工程是决定每次他做决策前,你往他桌上放哪些材料、按什么顺序放、放多少页、哪些是重点标黄的。
三者有重叠,但上下文工程更偏“信息调度”这一层。RAG 负责“找得到”,上下文工程负责“放得对”。很多团队 RAG 做得不错,检索准确率挺高,但 Agent 效果还是差,问题往往就出在上下文组装这一步——检索回来的东西没被好好利用。
2. 上下文的组成结构与优先级设计
2.1 一个 Agent 的上下文里到底有什么
拆开看,一个典型 Agent 单轮推理的上下文通常包含这几块,我按重要性排个序:
| 层级 | 内容类型 | 作用 | 是否可裁剪 |
|---|---|---|---|
| L1 | 系统指令 / 角色设定 | 定义 Agent 身份、能力边界、输出格式 | 不可裁剪,但可精简 |
| L2 | 当前任务与用户输入 | 本轮要解决的核心问题 | 不可裁剪 |
| L3 | 工具定义与调用规范 | 告诉模型有哪些工具、怎么调 | 可动态加载 |
| L4 | 检索/工具返回结果 | 支撑回答的事实依据 | 可裁剪、可重排 |
| L5 | 历史对话 | 维持多轮连贯性 | 可摘要、可丢弃 |
| L6 | few-shot 示例 | 引导输出风格与格式 | 可裁剪 |
| L7 | 反思/中间推理记录 | 多步任务的中间状态 | 可压缩 |
这个排序不是随便定的。越靠上的层级,对模型行为的约束越强,越不能动;越靠下的层级,越是可以根据窗口余量灵活取舍。我见过不少项目把 few-shot 示例放在系统指令前面,结果模型把示例当成了任务本身,输出格式全乱。
2.2 优先级冲突时怎么取舍
实际跑起来,冲突几乎必然发生。用户输入很长、检索结果很多、历史对话又舍不得丢,窗口就那么大,怎么办?
我的处理原则是保 L1、L2,压缩 L5,精选 L4,动态加载 L3 和 L6。
具体操作上,历史对话不要全量保留,用滑动窗口 + 摘要的方式。比如保留最近 3 轮完整对话,更早的用一段 100 字以内的摘要代替。摘要不是简单截断,而是让模型自己总结“到目前为止用户的核心诉求和已达成的共识”。这一步多花一点 token,能省下后面大量的窗口空间。
检索结果这块,不要检索到几条就塞几条。我通常的做法是:检索返回 top-k(比如 8 条),然后用一个轻量的重排模型或规则打分,只把 top-3 塞进上下文,其余作为“备选”在需要时二次检索。上下文里塞 8 条半相关文档,效果往往不如塞 3 条高相关文档。
2.3 位置编排:首尾优先原则
前面提到“lost in the middle”,这个现象对上下文编排有直接的指导意义。
模型对上下文开头和结尾的信息注意力最集中,中间部分容易被忽略。所以关键信息要往两头放:
- 开头放系统指令、角色设定、本轮任务的核心约束。
- 结尾放当前用户的具体问题,或者最关键的检索结论。
- 中间放支撑性材料、历史对话、示例。
一个常见的错误是把检索到的最重要文档放在中间,然后用户问题放在最前面。这样模型读完问题,翻过一堆材料,到结尾时已经“忘了”问题是什么。正确的做法是把用户问题在开头提一次(作为任务定义),在结尾再提一次(作为待回答项),中间夹材料。
提示:这个“首尾各放一次问题”的技巧,在长上下文场景下实测能明显提升回答的相关性,代价只是多几十个 token。
3. 上下文压缩与裁剪的实操方法
3.1 什么时候该压缩,什么时候不该
压缩不是无脑做。判断标准很简单:如果这段内容删掉后,模型回答的质量没有可感知的下降,那就该压缩或删除。
我一般分三种情况处理:
- 冗余信息:直接删。比如工具返回的 JSON 里一堆模型用不上的字段,只保留关键字段。
- 可摘要信息:用模型或规则压缩。比如长历史对话、长文档。
- 不可压缩信息:保留原文。比如精确的数值、代码、法律条款,摘要会丢精度。
很多人一上来就搞“全量摘要”,把所有历史对话都摘要一遍,结果关键细节丢了,Agent 开始胡编。摘要适合处理“叙事性”内容,不适合处理“事实性”内容。
3.2 滑动窗口 + 摘要的具体实现
这是我用得最多的一套组合拳,代码逻辑大致如下:
def build_context(history, current_query, max_tokens=8000): # 保留最近 N 轮完整对话 recent = history[-3:] # 更早的对话做摘要 older = history[:-3] if older: summary = summarize(older) # 调用模型生成摘要 else: summary = "" # 组装 context = [] if summary: context.append({"role": "system", "content": f"历史对话摘要:{summary}"}) context.extend(recent) context.append({"role": "user", "content": current_query}) # 检查 token 数,超了就继续裁 while count_tokens(context) > max_tokens: if len(recent) > 1: recent.pop(0) # 从最旧的完整对话开始丢 else: break return context这里有个细节:摘要的生成时机。不要每轮都重新摘要全部历史,那样成本高且不稳定。我的做法是维护一个“摘要缓冲区”,每积累 5 轮对话触发一次增量摘要,把新内容合并进已有摘要。这样既控制了成本,又保证了摘要的连贯性。
3.3 检索结果的裁剪策略
检索结果进上下文前,我通常过三道关:
第一道,相关性阈值过滤。检索返回的相似度分数低于某个阈值的直接丢。阈值设多少要看具体检索模型,我一般从 0.7 开始试,根据实际效果调。
第二道,去重与合并。同一个文档被切成多个 chunk 检索回来,内容高度重叠的合并成一条,避免重复占用窗口。
第三道,按需截断。单条文档太长时,不是简单截前 N 个字符,而是保留包含查询关键词的段落及其上下文。这个可以用简单的关键词定位实现,也可以用模型做段落级摘要。
注意:裁剪检索结果时,一定要保留来源标识(文档名、段落号)。Agent 在回答时如果能引用来源,可信度会高很多,也方便后续排查。
4. 工具调用场景下的上下文管理
4.1 工具返回结果怎么进上下文
工具调用是 Agent 区别于普通聊天机器人的核心,也是上下文管理最容易出问题的地方。
工具返回的结果,格式五花八门。有的返回一大坨 JSON,有的返回自然语言,有的返回表格。直接原样塞进上下文,轻则浪费 token,重则干扰模型判断。
我的处理原则是结构化提取 + 自然语言包装。举个例子,一个查询天气的工具返回:
{"code": 200, "data": {"city": "北京", "temp": 25, "humidity": 60, "wind": "3级", "forecast": [...]}}不要把这个 JSON 直接塞进去,而是转成:
工具调用结果(天气查询): - 城市:北京 - 当前温度:25摄氏度 - 湿度:60% - 风力:3级这样模型读起来更顺,也更容易在后续推理中引用。转换这一步可以用规则做,也可以用一个小模型做,成本很低但收益明显。
4.2 多轮工具调用的上下文累积问题
一个复杂任务,Agent 可能连续调用五六个工具。每轮调用的结果都往上下文里塞,很快就爆了。
我的做法是工具结果分级保留:
- 最近一次工具调用的完整结果:保留。
- 中间工具调用的结果:只保留关键结论,比如“查询到用户ID为12345”。
- 早期工具调用的结果:如果后续不再需要,直接丢弃;如果需要,压缩成一行摘要。
判断“后续是否还需要”,可以在系统指令里让模型自己标注哪些结果是“已消费”的。比如让模型在每次工具调用后输出一个consumed: true/false标记,标记为 true 的结果在下一轮就可以压缩。
4.3 工具定义本身的上下文开销
很多人忽略了一点:工具定义本身也占上下文。一个 Agent 挂 20 个工具,每个工具的定义(名称、描述、参数 schema)加起来可能就两三千 token。这些 token 每轮都要传,成本不低。
优化手段有两个:
一是动态加载工具。根据当前任务类型,只加载相关的工具定义。比如用户问的是订单问题,就只加载订单相关的 5 个工具,其余 15 个不加载。这个可以用简单的意图分类实现。
二是精简工具描述。工具描述不是越详细越好,把模型真正需要知道的写清楚就行。参数说明能省则省,默认值不用写,模型不关心的字段不用列。
5. 上下文工程的评估与迭代
5.1 怎么判断上下文设计得好不好
不能只看最终回答对不对,要拆开看。我通常从四个维度评估:
| 维度 | 评估方法 | 合格标准 |
|---|---|---|
| 信息完整性 | 检查关键信息是否都在上下文里 | 回答所需事实无遗漏 |
| 信息密度 | 计算有效 token 占比 | 有效信息占比 > 60% |
| 位置合理性 | 关键信息是否在首尾 | 核心约束在开头,问题在结尾 |
| 噪声水平 | 统计无关内容占比 | 无关内容占比 < 20% |
“有效 token”怎么定义?我的土办法是:把上下文里的内容逐段删掉,看回答质量是否下降。下降的就是有效信息,不下降的就是噪声。这个方法费时但准,适合在调优阶段做几轮。
5.2 一个可复用的评估流程
我一般按这个流程走:
- 构造测试集:收集 20-50 个真实场景的输入,覆盖简单问答、多轮对话、工具调用、长文档检索等类型。
- 跑基线:用当前上下文策略跑一遍,记录每个 case 的回答质量和 token 消耗。
- 逐项改动:每次只改一个变量(比如调整检索结果数量、改变历史对话保留轮数),重跑测试集。
- 对比分析:看改动后哪些 case 变好、哪些变差,找出规律。
- 固化策略:把验证有效的改动合并进主流程。
这个流程的关键是一次只改一个变量。我见过团队同时改五六个参数,结果效果变好了也不知道是哪个起的作用,变差了也找不到原因。
5.3 常见的效果退化模式
上下文工程做久了,会发现一些反复出现的退化模式:
模式一:上下文膨胀导致注意力稀释。表现是回答越来越泛、越来越不聚焦。原因是上下文里塞了太多东西,模型抓不住重点。解法是狠心裁剪,宁可少而精。
模式二:历史对话污染。表现是 Agent 把之前轮次的错误结论当成事实继续用。原因是历史对话里包含了未修正的错误信息。解法是在历史摘要里显式标注“以下结论已被修正”。
模式三:工具结果格式不一致。表现是模型对工具返回的理解时好时坏。原因是不同工具返回格式差异大,模型需要额外精力去解析。解法是统一工具返回的包装格式。
模式四:系统指令被淹没。表现是 Agent 不遵守输出格式要求。原因是系统指令太长或位置太靠前,被后续内容冲淡。解法是精简系统指令,并在结尾处重复关键约束。
6. 我踩过的坑和几条硬经验
6.1 不要迷信“越长越好”
刚做 Agent 那会儿,我的直觉是上下文给得越全,模型回答越准。实测下来完全不是。有一次做合同审查 Agent,把整份合同全文塞进去,模型反而抓不住关键条款,回答泛泛而谈。后来改成先检索相关条款,只把 3-5 条最相关的塞进去,准确率反而上去了。
上下文的价值不在于“全”,而在于“准”。给模型一堆它不需要的信息,等于给它增加了解析负担。
6.2 分隔符和格式标记比想象中重要
上下文里不同来源的内容,一定要用清晰的分隔符隔开。我习惯用这种格式:
=== 系统指令 === ... === 检索文档 1(来源:xxx)=== ... === 用户问题 === ...别小看这些标记,它们帮模型快速定位“哪段是哪段”。我做过对比测试,加了明确分隔符的版本,在长上下文场景下回答准确率能高出 10 个百分点以上。
6.3 动态调整比固定配置更靠谱
没有一套上下文配置能适配所有场景。简单问答不需要检索,复杂任务需要多轮工具调用,长文档分析需要大窗口。我的做法是根据任务类型动态切换上下文策略:
- 简单问答:系统指令 + 用户问题,不检索,不加载历史。
- 多轮对话:系统指令 + 历史摘要 + 最近对话 + 用户问题。
- 工具调用任务:系统指令 + 工具定义 + 工具结果 + 用户问题。
- 文档分析:系统指令 + 检索结果 + 用户问题 + 格式约束。
这个切换逻辑用一个轻量的意图分类器就能实现,成本很低,但效果提升明显。
6.4 留出“思考空间”
上下文不要塞满。我一般会预留 20% 的窗口空间给模型的推理过程。如果上下文占满了窗口,模型没有空间做中间推理,回答质量会明显下降。这个预留比例可以根据任务复杂度调整,复杂任务留 30%,简单任务留 10% 就够。
6.5 监控上下文长度分布
上线后一定要监控上下文长度的分布。如果发现 P95 长度接近窗口上限,说明上下文管理有问题,迟早会出截断事故。我一般设两个告警:P95 超过窗口 80% 告警,出现截断告警。
这套东西说起来都是细节,但正是这些细节决定了 Agent 是“能用”还是“好用”。上下文工程没有银弹,靠的是一轮轮实测、一点点抠。我自己的体会是,把上下文管理做扎实,比换一个更强的模型带来的提升更明显,而且成本更低。