1. 上下文工程到底在解决什么问题
1.1 从一次翻车现场说起
去年冬天我帮一个做跨境电商的朋友调他的客服 Agent。模型用的是当时第一梯队的开源权重,工具链也搭得规规矩矩,ReAct 循环跑得挺欢。结果上线第三天,客诉炸了——用户问“我上周买的那个蓝色杯子什么时候到”,Agent 转头去查了订单系统,拿回来一串 JSON,然后开始一本正经地胡说八道:把别人的收货地址念了出来。
排查了两天,问题不在模型,也不在工具调用,而在上下文。检索模块把三条相似订单全塞进了 prompt,没有做用户身份过滤,没有做时间衰减,没有做字段裁剪。模型看到什么就说什么,它没错,是我们喂错了。
这件事之后我彻底想明白一个道理:Agent 的能力上限,很多时候不是被模型智商卡住的,是被上下文质量卡住的。这两年大家把大量精力花在选模型、调 prompt、接工具上,但真正决定一个 Agent 能不能从 demo 走到生产的,是上下文工程这一层。
1.2 上下文工程和提示词工程不是一回事
很多人把这两个概念混着用,我觉得有必要先掰扯清楚。
提示词工程(Prompt Engineering)关注的是怎么把话说清楚——措辞、格式、few-shot 示例、思维链引导。它的作用域是单次交互,目标是让模型在这一次调用里输出你想要的东西。
上下文工程(Context Engineering)关注的是在正确的时刻,把正确的信息,以正确的形式,放进模型的视野里。它管的是整个信息供应链:什么该进、什么该出、什么时候进、以什么结构进、进多少、进不下怎么办。
打个比方。提示词工程像是你给下属写一封邮件,措辞要精准;上下文工程像是你管理整个公司的信息系统——谁在什么时候能看到哪些数据、数据怎么流转、过期数据怎么清理、敏感信息怎么脱敏。前者是写作技巧,后者是系统工程。
一个 ReAct 风格的 Agent,每一轮循环都要重新组装一次上下文:系统指令、历史对话、工具返回结果、检索到的知识、当前任务状态。这个组装过程的质量,直接决定了 Agent 是“聪明地干活”还是“自信地闯祸”。
1.3 为什么现在必须重视这一层
三个现实压力逼着我们必须把上下文工程做扎实。
第一,上下文窗口再大也不够用。现在动辄 128K、200K 甚至 1M 的窗口,听起来很宽裕,但真实业务里,一个客服 Agent 跑上十几轮对话,加上工具返回的 JSON、检索的文档片段、历史摘要,很容易就顶到上限。而且窗口越大,注意力越稀释,中间部分的信息容易被忽略——这就是业内常说的“lost in the middle”。塞得多不等于用得好。
第二,token 成本是真金白银。我算过一笔账,一个中等复杂度的 Agent 任务,如果每轮都把完整历史塞进去,跑完一次任务的 token 消耗可能是精简上下文的 5 到 8 倍。日活上千之后,这个差距就是每个月几万块的成本差。上下文工程做得好,等于直接省钱。
第三,可靠性要求。demo 阶段 Agent 答错一句大家哈哈一笑,生产环境答错一句可能就是客诉、合规问题、资金损失。上下文里混进过期信息、错误检索结果、越权数据,模型再强也救不回来。上下文工程本质上是在给 Agent 的输出质量做“上游质检”。
所以我的判断是:2026 年做 AI Agent,模型选型会越来越同质化,工具生态会越来越标准化,真正拉开差距的,就是上下文工程这一层的功力。
2. 上下文的核心构成与分层设计
2.1 一个 Agent 的上下文里到底装了什么
我习惯把 Agent 的上下文拆成六个层次,从稳定到易变排列:
| 层级 | 内容 | 变化频率 | 典型 token 占比 |
|---|---|---|---|
| 系统层 | 角色设定、行为准则、输出格式约束 | 极低 | 5%-10% |
| 工具层 | 可用工具描述、参数 schema、调用示例 | 低 | 10%-20% |
| 记忆层 | 长期记忆、用户画像、历史摘要 | 中 | 10%-15% |
| 检索层 | 当前任务相关的知识片段 | 高 | 20%-40% |
| 对话层 | 近期多轮交互原文 | 高 | 15%-30% |
| 状态层 | 当前任务进度、中间结果、待办 | 极高 | 5%-15% |
这个划分不是学术分类,是我在实际调优时用来定位问题的工具。当 Agent 表现异常,我会先看是哪一层出了问题:是系统指令被稀释了?还是检索层塞了噪声?还是状态层丢了关键中间结果?
2.2 系统层:稳定但最容易被忽视
系统层是 Agent 的“宪法”,一旦定下来就不该频繁改动。但很多人写系统提示词时太随意,导致后面所有层都在给这个随意买单。
我的经验是,系统层要包含四样东西:身份定义、能力边界、行为准则、输出契约。身份定义告诉模型它是谁;能力边界明确它不能做什么(比如不能承诺退款、不能透露内部字段);行为准则规定遇到不确定情况怎么办;输出契约规定格式。
这里有个坑:行为准则要写成可执行的判断规则,而不是抽象的美德。“要诚实”是废话,“当检索结果与用户陈述冲突时,以检索结果为准并说明来源”才是可执行的。我见过太多 Agent 因为系统层写了“尽量帮助用户”这种模糊指令,在边界情况下乱来。
2.3 工具层:描述质量决定调用准确率
工具层是 ReAct 类 Agent 的命脉。工具描述写得好不好,直接决定模型能不能在正确的时机调用正确的工具。
我踩过的坑是:工具描述写得太技术化。比如一个查询订单的工具,描述写成“调用订单服务 API 获取订单详情”,模型经常在用户只是闲聊时也去调它。后来改成“当用户明确询问某笔订单的状态、物流、金额时使用;用户只是打招呼或询问政策时不要调用”,误调用率立刻降下来。
工具描述要回答三个问题:什么时候用、什么时候不用、参数怎么填。尤其是“什么时候不用”,很多人不写,结果模型过度调用。参数 schema 里每个字段都要有 description,枚举值要列全,必填项要标清楚。这些细节看着琐碎,但每一条都在减少模型的猜测空间。
2.4 记忆层与检索层:最容易互相打架
记忆层存的是跨会话的长期信息,检索层取的是当前任务相关的知识。这两层最容易出问题,因为它们都在往上下文里塞“外部信息”,边界模糊。
我的处理原则是:记忆层管“关于这个用户/这个任务的稳定事实”,检索层管“这次任务需要的一次性知识”。用户偏好、历史工单结论、账户等级,这些进记忆层;产品文档、政策条款、FAQ,这些进检索层。
两者都要做相关性过滤和数量控制。我一般给检索层设硬上限:最多 5 条,每条不超过 300 token,超出就按相关性分数截断。记忆层更严格,只保留与当前意图强相关的 2 到 3 条。宁可少喂,不要喂杂。
2.5 对话层与状态层:动态管理的重点
对话层是原始多轮记录,状态层是任务进度的结构化表示。这两层是动态变化最剧烈的,也是上下文膨胀的主要来源。
对话层不能无限增长。我的做法是滑动窗口 + 摘要压缩:保留最近 N 轮原文(N 根据任务复杂度定,一般 5 到 8 轮),更早的对话压缩成一段摘要。摘要不是简单截断,而是提取“用户诉求、已达成的结论、未解决的问题”三要素。
状态层是很多人忽略的宝藏。把任务进度显式写进上下文,比如“当前步骤:已确认订单号,待查询物流”,能极大减少模型重复劳动和跑偏。这相当于给 Agent 一个随身记事本,它不用靠记忆去推断自己走到哪了。
3. 上下文组装与压缩的实操方法
3.1 组装顺序有讲究
上下文不是随便拼起来的,顺序会影响模型的注意力分配。我实测下来比较稳的顺序是:
- 系统层(最前,稳定锚点)
- 工具层(紧跟系统层,让模型先知道有什么武器)
- 状态层(当前任务进度,让模型知道走到哪了)
- 记忆层(相关长期事实)
- 检索层(当前知识片段)
- 对话层(最近交互,放最后,紧贴模型生成位置)
把对话层放最后是有道理的:模型对靠近生成位置的内容注意力更强,最近的用户输入应该离生成点最近。而系统层放最前,是因为它需要作为全局约束贯穿始终。
注意:这个顺序不是铁律,不同模型对位置的敏感度不一样。换模型后建议做一次 A/B 测试,用同一批任务对比不同顺序的成功率。
3.2 压缩的三种武器
上下文压缩是上下文工程的核心技能。我常用三种手段,按激进程度递增:
第一种,字段裁剪。工具返回的 JSON 往往字段一大堆,但 Agent 真正需要的可能就三五个。在工具层和上下文之间加一个裁剪器,只保留必要字段。这一步能砍掉 50% 以上的工具返回体积,而且几乎不损失信息。
第二种,语义摘要。对长文档、长对话做摘要。摘要要用模型生成,但要有明确的摘要指令:保留什么、丢弃什么、字数上限。我一般要求摘要保留“结论、数字、专有名词、未决事项”,丢弃“寒暄、重复、过程性描述”。
第三种,结构化替换。把大段自然语言替换成结构化表示。比如把十轮对话压缩成一个 JSON:{"intent": "查物流", "order_id": "xxx", "status": "已确认", "pending": "等待物流接口"}。这种表示 token 效率极高,但要求下游能正确解析。
三种手段可以叠加使用。我的默认策略是:工具返回先裁剪,长对话先摘要,状态信息结构化。
3.3 一个可复用的组装伪代码
下面这段是我常用的组装逻辑骨架,用 Python 风格写,实际落地时按你的框架调整:
def build_context(task, user, history, tools, retriever): ctx = [] # 1. 系统层,固定不变 ctx.append(system_prompt) # 2. 工具层,按当前任务动态筛选相关工具 relevant_tools = filter_tools(tools, task.intent) ctx.append(render_tools(relevant_tools)) # 3. 状态层,结构化任务进度 ctx.append(render_state(task.state)) # 4. 记忆层,只取强相关 memories = fetch_memories(user.id, task.intent, top_k=3) ctx.append(render_memories(memories)) # 5. 检索层,带数量与长度上限 docs = retriever.search(task.query, top_k=5, max_tokens=1500) ctx.append(render_docs(docs)) # 6. 对话层,滑动窗口 + 摘要 recent, summary = split_history(history, keep_last=6) if summary: ctx.append(render_summary(summary)) ctx.append(render_dialogue(recent)) # 7. 总长度兜底,超限则按优先级裁剪 return enforce_budget(ctx, max_tokens=8000)关键在最后一步enforce_budget。它要按优先级从低到高裁剪:先砍检索层的低分文档,再砍记忆层的弱相关项,再压缩对话摘要,最后才动系统层和状态层。系统层和状态层是底线,不能动。
3.4 参数怎么定:一个实际计算过程
很多人问 top_k 设多少、max_tokens 设多少。我给一个实际推算过程。
假设模型窗口 32K,我要留 4K 给输出,实际可用 28K。按经验,系统层 1K,工具层 2K,状态层 0.5K,记忆层 1K,对话层 6K,那么检索层可用约 17K。如果每条文档平均 400 token,理论上能放 40 条。但放 40 条必然稀释注意力,所以我主动压到 5 到 8 条,把剩余预算留作缓冲。
核心原则是:不要用满窗口。我一般只用窗口的 60% 到 70%,留出余量应对突发长输入。用满窗口的 Agent,遇到一个长文档就崩,稳定性极差。
4. ReAct 循环中的上下文动态管理
4.1 ReAct 每一轮都在重写上下文
ReAct 的本质是“思考—行动—观察”的循环。每一轮循环,Agent 都要基于当前上下文决定下一步。这意味着上下文不是静态的,而是每轮都在变。
我见过很多实现,把 ReAct 写成“把历史全部追加,然后重新调用”。这样跑几轮上下文就爆了。正确的做法是:每一轮都重新组装上下文,而不是简单追加。
具体来说,每一轮循环要做四件事:把上一轮的工具返回裁剪后并入状态层;把上一轮的思考压缩成一行进度;把已经完成的子任务从待办里移除;重新评估检索层是否需要更新。
4.2 工具返回结果的处理是重灾区
工具返回是上下文膨胀的头号来源。一个查询接口返回 2000 token 的 JSON,跑五轮就是 10000 token。必须处理。
我的处理流程是:先解析,再裁剪,再摘要,最后才入上下文。解析是把 JSON 转成结构化对象;裁剪是只留必要字段;摘要是把结构化对象转成一句人话;入上下文是只放那句人话,原始数据存在外部,需要时再取。
举个例子,物流查询返回一大坨轨迹数据,我最终入上下文的可能就一句:“订单 xxx 当前状态:运输中,最新节点:已到达分拨中心,预计 2 天后送达。” 这一句话 30 token,替代了原本 2000 token 的原始数据,信息密度反而更高。
4.3 循环终止条件的上下文信号
ReAct 什么时候停?很多人靠最大轮数硬截断,这很粗暴。更好的做法是在上下文里放终止信号。
我会在状态层维护一个task_status字段:in_progress、need_clarification、done、failed。每轮循环后更新。当模型看到done或failed,就知道该收尾了。同时系统层要写清楚:当任务完成或无法完成时,输出最终答复并停止调用工具。
这样比硬截断优雅得多,也避免了“明明做完了还在瞎调工具”的浪费。
4.4 并发场景下的上下文隔离
热词里有人问“ai agent 怎么扛并发”,这问题问到点子上了。并发场景下,上下文管理最大的风险是串号——A 用户的上下文混进了 B 用户的信息。
我的做法是:上下文对象严格绑定会话 ID,任何跨会话的共享只走只读的知识库,绝不共享可变状态。记忆层读取时带上用户 ID 过滤,检索层如果涉及用户私有数据,也必须带权限过滤。工具层调用时,用户身份从会话上下文注入,不依赖模型传参。
这一点在电商、金融类 Agent 里是红线。我那个朋友的事故,根子就是检索层没做用户过滤。并发越高,串号概率越大,必须从架构上杜绝,而不是靠模型自觉。
5. 常见问题与排查技巧实录
5.1 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| Agent 答非所问 | 检索层噪声大 / 系统层被稀释 | 检查检索相关性分数,检查系统层位置 |
| 工具过度调用 | 工具描述缺“何时不用” | 补充负面触发条件 |
| 工具该调不调 | 工具描述太技术化 / 参数 schema 不清 | 改写成业务语言,补全参数说明 |
| 上下文超限报错 | 无预算控制 / 工具返回未裁剪 | 加 enforce_budget,加返回裁剪 |
| 多轮后跑偏 | 对话层无摘要 / 状态层缺失 | 加滑动窗口与摘要,加状态字段 |
| 并发下串号 | 上下文未绑定会话 / 检索无权限过滤 | 检查会话隔离与权限过滤 |
| 重复劳动 | 状态层未记录已完成步骤 | 显式写入任务进度 |
| 输出格式乱 | 输出契约不明确 | 系统层加格式约束与示例 |
5.2 三个我踩过的坑
坑一:摘要越摘越丢。早期我用模型做摘要,没给约束,结果它把关键订单号摘没了。后来改成结构化摘要指令,明确要求保留数字、专有名词、结论,问题解决。教训是:摘要也要有 schema。
坑二:检索 top_k 拍脑袋。一开始设 10,觉得多喂点总没错。结果 Agent 经常被不相关文档带偏。后来做了对比实验,发现 5 条时准确率最高,10 条反而下降。教训是:检索数量要实验确定,不是越多越好。
坑三:系统层频繁改。有段时间我遇到问题就改系统提示词,改到最后系统层又长又乱,模型反而抓不住重点。后来定规矩:系统层一个月最多改一次,其他问题在对应层解决。教训是:系统层要稳定,别让它变成补丁堆。
5.3 一个实用的调试技巧
我调试上下文问题时,会做一个“上下文快照”工具:每次调用模型前,把完整上下文 dump 成文件,标注每部分的 token 数和来源。出问题时回看快照,一眼就能看出是哪层塞多了、哪层丢了。
这个工具不复杂,但极其有用。它把“模型为什么这么答”这个黑盒问题,变成了“上下文里有什么”这个白盒问题。我强烈建议每个做 Agent 的团队都搞一个。
6. 上下文工程的评估与迭代
6.1 怎么衡量上下文质量
上下文质量不能只看最终答案对不对,要拆开看。我一般看四个指标:
相关性:检索层和记忆层的内容,与当前任务的相关度。可以用人工标注一小批样本,算命中率。
完整性:完成任务所需的关键信息,是否都在上下文里。这个靠失败案例分析,看哪些失败是因为“信息没喂进去”。
简洁性:上下文 token 数与任务复杂度的比值。同样的任务,token 越少越好,说明信息密度高。
稳定性:同一任务多次运行,上下文组装结果的一致性。波动大说明检索或摘要不稳定。
6.2 迭代的正确姿势
上下文工程的迭代,最忌讳“凭感觉改”。我的做法是:建一个回归测试集,每次改动都跑一遍。
测试集不用大,20 到 50 个真实任务就够,但要覆盖典型场景和边界情况。每个任务记录:期望行为、实际行为、上下文快照、token 消耗。改动后对比,看成功率、token 消耗、延迟三个维度的变化。
这样迭代,每一步都有据可依,不会出现“改好了 A 场景,改坏了 B 场景”的情况。
6.3 后续可以扩展的方向
上下文工程往下走,有几个方向值得投入。一是自适应上下文,根据任务难度动态调整各层预算,简单任务少喂,复杂任务多喂。二是上下文缓存,把稳定的系统层和工具层做前缀缓存,降低重复计算成本。三是多 Agent 协作下的上下文传递,主 Agent 和子 Agent 之间怎么高效传递上下文,而不是全量复制。
这些方向我自己也在摸索,等有成熟经验了再单独写一篇。就目前而言,把上面这套分层、组装、压缩、评估的框架落地扎实,已经能让你的 Agent 在生产环境里稳一大截。我个人的体会是,上下文工程这活儿,七分靠设计,三分靠调优,而设计阶段想清楚“什么信息在什么时刻该出现”,比后面反复调 prompt 有效得多。