上下文工程实践指南:让 Agent 在多轮任务里看见该看见的

创建于2025.07.14预计阅读20 分钟

设想一个退款 Agent:读工单、查订单状态、核对政策,生成处理建议,提交前需要用户确认。这条提示词把任务、授权动作、确认边界都写清楚了,单看这一步没有问题。

问题出在第二轮往后。Agent 查完订单状态,又去查了政策,工具返回的原始 JSON、上一轮的错误重试记录、订单里和这次退款无关的历史字段,都会跟着留在对话里。跑到第五轮,窗口里已经有一半内容和当前这次判断没关系,模型开始把旧的工具返回当成本轮该参考的信息。提示词本身没变,但它已经不是模型唯一看到的东西了。

这就是上下文工程(Context Engineering)要处理的问题:模型下一次采样时,窗口里装的不只是那条提示,还有系统约定、检索到的文档、工具返回、到这一步为止的记录,全都挤在同一串 token 里。2025 年夏天,这类工作有了一个比较固定的名字。核心是给任务备齐上下文,让模型有可能做对。工业级应用里,是把窗口刚好填对:太少做不了,太多或形态不对,费用上去,表现也会掉。

它和提示词工程差在哪

提示词工程管怎么写、怎么组织指令,尤其是系统提示:任务、判断依据、约束、示例、输出格式,这些字段解决的是”这一次调用该怎么问”。上下文工程管的是推理过程中整份状态:指令之外,还有工具、模型上下文协议(MCP,会把一批工具说明一次性塞进窗口)、外部数据、消息历史,以及每一轮要留下什么、丢掉什么。

退款 Agent 的提示词只需要写一次;但它跑到第几轮、窗口里还留着哪些工具返回、要不要把三轮前的错误也带上,这些判断每一步都要重新做一遍。早期很多任务是一次性分类或生成,写好系统提示就够了。Agent 会连着跑很多轮,每轮都在往窗口里堆新东西,就必须每一步重新挑。提示还在,只是变成窗口里的一块。窗口变脏了,先看是指令不清楚,还是这一轮不该进窗口的东西进来了。

它不是 LLM 应用的全部:问题怎么拆、调用哪类模型、校验和界面、安全、评测,都不在这个词里面,那些仍然是提示词工程和产品设计要管的事。

一次推理里会装进什么

一次调用里,窗口通常由这些拼起来:

  • 指令:系统提示、规则、少样本、工具说明。
  • 用户当前这句话。
  • 短时状态:到这一步为止的对话和工具轨迹。
  • 长时记忆:跨会话存下来的偏好、项目摘要、事实。
  • 检索到的外部材料。
  • 工具定义,以及上一轮工具返回的结果。
  • 输出格式:JSON Schema、枚举、必填字段。

退款 Agent 跑到第三轮时,窗口里已经有:系统提示(任务、授权动作、确认边界)、原始工单、订单查询的完整返回、政策文档片段、第一次工具调用失败的错误信息。可以把模型比成 CPU,窗口比成内存:磁盘不会整盘装进内存,这一步该进窗口的才进。

窗口变大解决不了什么

2023 年有过一个很干净的实验:多文档问答里,答案一定在某一篇里,只改这篇的位置。模型在开头和结尾表现最好,放在中间明显变差,曲线像 U。有的设置下,答案放中间,还不如不给文档(闭卷)准。更简单的键值检索里,一部分模型连”从中间取出配对的值”都会掉。这篇就是《迷失在中间》(Lost in the Middle)。

这件事和预测下一个词对得上。Transformer 里每个 token 都要和其它 token 两两算相关度,长度是 nn 时关系是 n2n^2。序列变长,这些关系会变稀;训练数据里短序列也更常见。所以不是到了某个长度突然不能用,而是越长越不准。

2025 年 7 月的上下文腐烂(Context Rot)报告把这件事又测了一遍,评了 18 个当时的模型。任务故意保持简单,只改输入长度。结论是:模型并不均匀地使用上下文,输入越长,越不可靠。大海捞针(Needle in a Haystack)那种”在长文里找回一句原话”很多模型接近满分,所以会给人”长上下文已经解决了”的错觉。把针改成语义相关、再加干扰项,掉点就出来了。

放回退款 Agent 的例子:把订单查询的完整返回、政策文档全文都塞进窗口,看起来是”给够信息”,实际是在往里加噪声。等更大窗口,解决不了污染和相关性。长任务还是得管窗口里留下什么。

原则:少而有用

尽量用一小撮高信号 token,让目标结果更可能出现。少,不等于短。该交代的行为、约束、例子还是要给够。可以先用最小提示加最强模型试任务,再按失败模式往上加,而不是一上来写满。

系统提示的高度要合适。一端是把 if-else 写进提示,又脆又难维护;另一端是太空,假装模型和你有共同背景。可用的区间是:具体到能指导行为,又留出启发式,而不是写死每条路径。

工具是模型和外部世界的契约,返回值要省 token,功能不要互相重叠。退款 Agent 如果同时有”查订单”和”查订单详情”两个工具,说明重叠,模型选错的概率会上升;人说不清这一步该用哪个工具,模型更说不清。

少样本仍然有用,但不要把边角案例列成清单。挑一组多样、能代表预期行为的例子。例子太多、太像,会反过来把模型带进重复动作。

写:把东西存到窗外

写,是先存到窗口外面,需要时再读回来。

一轮任务里可以用草稿:计划写进 Memory 或 NOTES.md,窗口超限被截断时计划还在。Claude Code 的待办、不断改写的 todo.md,都是同一类。退款 Agent 可以把”已确认工单信息、待核对政策、待生成建议”这类进度写成一份状态文件,而不是全靠对话历史推断。把总目标反复写到上下文末尾,等于把计划推到最近被看到的位置,减轻迷失在中间的问题。

跨会话的记忆是另一层。产品会从交互里抽出长期记忆。选错会很烦:画一张不该带位置的图,却把记忆里的住址塞进去。记忆一旦乱选,用户会觉得窗口不再属于自己。

选:用的时候再装进来

选,是决定这一步读什么进来。

一种是推理前检索:嵌入搜索、规则文件、总是装进窗口的 CLAUDE.md。代码 Agent 里这仍然常见。建索引不等于检索。代码库一大,单靠嵌入不可靠,得叠 grep、知识图谱、重排。

另一种是即时取(just-in-time):只拿轻量标识(路径、查询、链接),用工具在运行时再装。退款 Agent 不必一开始就把整份订单历史塞进窗口,先拿订单号,需要核对退款次数时再查一次。对大数据库可以写查询,用 head / tail 看,不必把整表塞进窗口。代价是比预取慢,工具和启发式没设计好时,Agent 会空转、走死路。

常见的混合是:CLAUDE.md 一开始就进窗口,具体文件靠 glob、grep 现查。

智能体技能(Agent Skills)把”先露目录、再翻章节”做成了产品机制。启动时只把每个 skill 的 name 和 description 放进系统提示;相关了再读整个 SKILL.md;更细的说明和脚本,用到再打开。脚本可以在窗外跑,只把输出送回来。这样 skill 里能捆多少材料,几乎不受窗口限制。

工具太多也是选择问题。说明重叠,模型会选错。运行中动态装卸工具通常不划算:工具定义在窗口前部,一改就作废后面的键值缓存(KV-cache);历史里还会提着已经卸掉的工具。更稳的是定义不动,只在解码时屏蔽某些工具名。

压:压缩但不丢到找不回来

压,是窗口快满或已经太吵时,留下还能继续干活的部分。

压缩(compaction)就是对话接近上限时做摘要,用摘要开一个新窗口。留下架构决定、未修的 bug、实现细节,丢掉重复的工具输出,再带上最近打开的几个文件。先保证关键信息不丢,再去掉多余。最轻的一档是清掉很早以前的工具结果。退款 Agent 跑到第十轮时,可以把前面几轮反复核对政策的过程压成一句”已核对退款政策第 3.2 条,符合条件”,但订单号和工单 ID 这类还要用的字段不能压掉。

再严一点:压缩必须可还原。网页正文可以从窗口里拿掉,但 URL 要留着;文档可以不在窗口里,但沙箱路径要在。文件系统几乎是无限的上下文。不可逆压缩有风险,因为你不知道十步之后哪条观察会变成关键。

和压缩绑在一起的是键值缓存。Agent 的输入往往远长于输出,有的产品平均大约 100

。前缀不变时,缓存能把延迟和费用打下来,缓存输入可以到未缓存的十分之一。要做到高命中:

  • 前缀稳定。系统提示开头放精确到秒的时间戳,从那个 token 起缓存全废。
  • 只追加,不改已经发生的动作和观察。JSON 序列化键序不稳,也会悄悄打穿缓存。
  • 需要时显式打缓存断点(cache breakpoint),至少覆盖系统提示末尾。

隔:不要所有事挤在同一扇窗

隔,是把上下文拆开,避免一件事把整扇窗填满。

子 Agent 是最常见的拆法。子 Agent 自己跑很长的检索,只把 1000 到 2000 token 的摘要交回主 Agent。复杂研究任务上,这样往往比单线程把所有搜索轨迹都留在主窗口里更干净。退款 Agent 如果要同时核对三份不同版本的政策文档,可以让子 Agent 分别去读,只把”是否符合条件 + 依据条款”带回来。

反过来,并行写很容易出事。不是”多个模型”本身不行,是上下文传不干净。要共享的是完整轨迹,不是只转发一句任务。动作还带着没写明的决定,决定冲突,结果就坏。

把”做个 Flappy Bird 克隆”拆成两个子任务:一个去做背景,做成了马里奥管道;一个去做鸟,做成了不像游戏资源的东西。把原任务再复制一份给子 Agent 也不够,因为真实系统是多轮的,中间还做过工具调用。就算都看到原任务,两边仍可能做成两套画风,因为彼此看不见对方已经做了哪些隐含选择。

比较稳的折中是:能并行读,不要并行写互相看不见的决定。调查可以丢给子任务,改代码留在能看见既有决定的那条线程。Claude Code 早期即使生子任务,也多半只让它回答问题,不并行写。

重对象还可以留在环境里。沙箱里跑代码,图片、音频先赋给变量,需要时再把返回值送回模型。运行时状态对象也可以:schema 里只有 messages 每轮给模型看,别的字段按需暴露。

动作会带上没写明的决定

不是只有”任务描述”算上下文。一次改文件、一次选库、一次定接口,都在做没有写进提示的决定。后面的步骤如果看不见这些决定,就会在另一套假设上继续干。

2024 年不少编码 Agent 用”大模型写修改说明、小模型按说明重写整个文件”。说明稍有含糊,小模型就会改错。决定和执行拆开、上下文又传不完整,就会这样。后来编辑决定和落盘更常由同一个模型一次做完。

设计拆分时可以先问:这一步做完,有哪些决定被写进了世界,下一步看不看得到。

错误和少样本

出错当时最好让模型看见失败动作和对应的观察。抹掉失败等于抹掉证据,它没法改先验。错误恢复是 Agent 能不能接着干的标志,公开基准很少测这个。

修好以后,原始堆栈不一定还要占着位置,但”试过什么、为什么不行”最好以更密的形式留下。

少样本也有两面。例子要多样、典型。审 20 份简历这种重复任务,窗口里如果已经是同一套动作-观察,模型会跟着重复,甚至幻觉。序列化和措辞上加一点变化,可以打断模式。别把自己用少样本套进沟里。

三个例子

给退款 Agent 准备该看见的窗口

第一版实现里,退款 Agent 每一轮都带着完整对话历史:原始工单、订单查询的整段 JSON、政策文档全文、上一轮失败的堆栈。跑到第五轮,模型开始把”上一次查询失败”误判成”这次也该走失败流程”。

改法按写、选、压、隔一条条对上:

  • 写:把”工单结论、已核对的政策条款、待确认动作”写成一份结构化状态,而不是让模型从对话历史里反复推断。
  • 选:政策文档不整份进窗口,先用检索找到对应条款,只把条款文本和链接带进来。
  • 压:订单查询的原始返回只在当轮使用,下一轮压成一句”订单状态:已发货,物流单号 SF123”。
  • 隔:需要同时核对多份政策文件时,交给子 Agent 分别处理,只把”结论 + 依据”带回主线程。
refund-agent-state.md
## 当前状态
- 工单:#4821,主诉求为退款到账延迟
- 订单:已发货,物流单号 SF123({{QUERY_TIME}})
- 政策核对:符合《退款政策》第 3.2 条({{POLICY_LINK}})
- 待确认:是否向用户发起退款,需人工确认

## 上一轮工具调用
- 调用:查询订单状态 -> 成功
- 调用:查询退款政策 -> 成功

窗口里留下的是当前该看见的结论,不是让模型每一轮都重新读一遍原始返回。

回一封约会议的邮件

邮件只有一句:“明天方便快速同步一下吗?”

窗口里只有这一句时,模型只能回”明天可以,你想几点”,听起来像客服。

调用前先把这些装进去:日历(明天已经排满)、和对方的历史邮件(语气)、联系人(这是重要伙伴)、以及发邀请 / 回邮件的工具。模型才写得出:“明天一天会,周四上午可以的话我发了邀请。”

差别不在模型突然变聪明,在这一步该看见的东西有没有进窗口。

一次部署

窗口格式可以自己定,不必死守标准的 role: tool 消息数组。按事件拼起来,密度更高,也更好控制留下什么:

xml
<slack_message>
  From: @alex
  Channel: #deployments
  Text: Can you deploy the latest backend to production?
</slack_message>

<list_git_tags>
  intent: "list_git_tags"
</list_git_tags>

<list_git_tags_result>
  tags:
    - name: "v1.2.3"
      commit: "abc123"
</list_git_tags_result>

<deploy_backend>
  intent: "deploy_backend"
  tag: "v1.2.3"
  environment: "production"
</deploy_backend>

<error>
  error running deploy_backend: Failed to connect to deployment service
</error>

下一步仍然是同一句:到目前为止发生了这些,下一步做什么。工具失败留在窗口里,模型才能改去查工作流状态,而不是再部署一次。

编码 Agent 也可以按同一套清单想:系统约定(CLAUDE.md / 规则)先在;相关文件用搜索现取,不要把整个仓库嵌入进去;工具输出太大就写到文件,窗口里留路径;窗口到阈值就摘要,但保留未完成的决定和最近打开的文件;调查类工作可以丢给子 Agent,改代码尽量留在能看见既有决定的那条线程。

常见错误

  • 把长窗口当免费午餐。大海捞针满分不等于中间的内容用得好。先假设越长越吵,再决定装什么。
  • 系统提示里放会变的前缀,尤其是精确时间。缓存从第一个不同的 token 作废。
  • 运行中增删工具定义。前缀变了,历史里还会提到已经不存在的工具。
  • 把所有边角写成规则清单,而不是少数典型例子。
  • 多 Agent 并行写,又不共享完整轨迹和已经做过的决定。
  • 不可逆压缩。十步之后要用的观察,只留”大概摘要”会找不回来。能还原则留指针。
  • 工具又多又重叠。人分不清该用哪个,就先砍到能分清。
  • 重复任务里上下文太整齐,模型开始复读自己。
  • 失败一出现就从轨迹里抹掉,模型没有改行为的证据。
  • 长期记忆无差别注入。选错一次,用户会觉得窗口被抢走了。

实践检查清单

  • 每一步都能说清”这一步该看见什么”,而不是默认带上全部历史。
  • 工具返回、检索片段等大块内容用完即压缩或转成指针,不长期占着窗口。
  • 系统提示前缀稳定,可变内容(时间戳等)不放在缓存前缀里。
  • 压缩可还原:URL、路径、ID 这类指针留着,不做不可逆摘要。
  • 失败动作和观察保留在轨迹里,不在修复后立即抹掉。
  • 并行子任务只用于能独立读的工作,写操作留在能看见既有决定的主线程。
  • 工具定义之间没有明显重叠,运行中不动态增删。
  • 长期记忆的写入和读取都有明确触发条件,不做无差别注入。

参考资料

Yi Liu

© 2026 Yi Liu

GitHubRSS