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历史对话维持多轮连贯性可摘要、可丢弃L6few-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_tokens8000): # 保留最近 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 是“能用”还是“好用”。上下文工程没有银弹靠的是一轮轮实测、一点点抠。我自己的体会是把上下文管理做扎实比换一个更强的模型带来的提升更明显而且成本更低。