ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

上下文工程实战:解决多轮对话中模型失忆与格式错乱

上下文工程实战:解决多轮对话中模型失忆与格式错乱

1. 从一个真实困境说起:为什么模型“变笨”了

很多人应该都遇到过这种场景:明明用的是一个能力很强的模型,单轮对话里问它一个独立的问题,回答得又快又好。可一旦把它放进一个多轮交互的系统里,让它连续处理十几个回合,它就开始“失忆”——前面说过的约束忘了,中间确认过的格式不遵守了,甚至开始胡编乱造一些根本没出现过的信息。

我最早踩到这个坑,是在做一个文档问答类的小工具时。当时我的思路很朴素:把用户的问题和检索到的资料拼在一起,丢给模型就完事了。单次测试效果不错,可一旦用户追问、补充条件、修改要求,整个对话链条就开始崩。我一度以为是模型不行,换了好几个版本,结果都差不多。后来才意识到,问题根本不在模型本身,而在于我喂给它的“上下文”是一团乱麻。

这就是上下文工程要解决的核心问题。所谓上下文工程,说白了就是:在模型每一次生成之前,精心设计它能看到的所有信息——包括系统指令、历史对话、检索到的知识、工具返回结果、格式约束等等,并且管理好这些信息在有限窗口内的取舍、排序和压缩。它不是一个新概念,但在实际项目里,它往往比选哪个模型、调什么参数更能决定最终效果。

这篇文章适合所有正在做对话系统、智能助手、Agent 应用、RAG 问答的开发者,也适合那些“模型明明很强但我的产品就是不好用”的团队。我会从设计思路、核心细节、实操落地到问题排查,把上下文工程这件事掰开揉碎讲清楚,尽量让你看完就能对照自己的项目改起来。

2. 上下文工程到底在工程什么

2.1 上下文不是“聊天记录”,而是一份精心编排的工作简报

很多人对上下文的直觉理解就是“把之前的对话都带上”。这个理解在早期确实够用,但一旦系统复杂起来就会出问题。我更愿意把上下文理解成一份给模型看的工作简报:它要在有限的篇幅里,告诉模型“你是谁、现在要干什么、有哪些约束、可以参考哪些材料、之前已经确认了什么”。

这个视角的转变很关键。因为一旦你把上下文当成简报,你就会开始思考:哪些信息是必须的?哪些是冗余的?哪些应该放在最前面?哪些应该放在最后?哪些可以压缩成一句话?这些问题,恰恰就是上下文工程的全部内容。

举个生活化的类比。你让一个刚接手项目的同事帮你处理一件事,你有两种做法:一种是把过去三个月的所有聊天记录、邮件、文档一股脑转发给他,让他自己找重点;另一种是写一份清晰的简报,说明背景、目标、约束和参考材料。前者信息量更大,但后者成功率更高。模型面对上下文时,和这个同事的处境一模一样。

2.2 为什么窗口越大,反而越容易出问题

现在很多模型的上下文窗口动辄几十万甚至上百万 token,于是有一种流行的说法:“窗口够大,就不用做上下文工程了,全塞进去就行。”我实测下来的结论是:窗口大不等于效果好,甚至可能更差。

原因有几个。第一,模型对上下文的注意力分布是不均匀的,通常对开头和结尾更敏感,中间部分容易被忽略,这就是常说的“中间迷失”现象。第二,无关信息越多,噪声越大,模型越容易被带偏。第三,长上下文意味着更高的成本和更长的响应时间,工程上并不划算。

所以窗口变大,只是给了你更大的操作空间,但怎么用这个空间,仍然需要上下文工程来决策。把一百万 token 的窗口塞满垃圾,效果不会比精心组织的两千 token 更好。

2.3 上下文工程的四个核心动作

我把日常做的上下文工程工作归纳成四个动作,基本覆盖了绝大多数场景:

  • 选择:从所有可用信息里,挑出这一次生成真正需要的部分。
  • 排序:决定这些信息以什么顺序排列,谁在前谁在后。
  • 压缩:把冗长的历史或材料,提炼成更短但信息密度更高的形式。
  • 约束:用清晰的指令和格式要求,框定模型的输出边界。

这四个动作听起来简单,但每一个都有大量细节。下面我会逐个展开,结合实操讲清楚怎么做、为什么这么做。

3. 核心细节拆解:选择、排序、压缩、约束

3.1 选择:什么该进上下文,什么该被挡在门外

选择是上下文工程的第一步,也是最容易被忽视的一步。很多系统的做法是“能塞就塞”,结果上下文里一半是无关内容。我的原则是:每一条进入上下文的信息,都要能回答“它为什么在这里”。如果答不上来,就删掉。

具体怎么选?我通常按优先级分三层:

第一层是硬约束,包括系统角色设定、必须遵守的规则、输出格式要求。这些是每次生成都必须带的,不能省。

第二层是任务相关,包括当前用户的问题、检索到的相关材料、工具调用的返回结果。这一层需要根据当前任务动态筛选,比如检索结果只保留最相关的几条,而不是把整个知识库都塞进去。

第三层是历史上下文,包括之前的对话轮次。这一层最需要克制,因为历史会不断累积。我的做法是只保留最近若干轮,更早的内容要么丢弃,要么压缩成摘要。

这里有个实操心得:检索类内容一定要做相关性过滤和数量上限。我见过不少 RAG 系统把检索到的十几条文档全部塞进上下文,结果模型被无关内容干扰,回答质量反而下降。通常保留 top 3 到 top 5 就够了,具体数量要看单条内容的长度和任务复杂度。

3.2 排序:位置决定注意力

选好了内容,接下来是排序。前面提到模型对上下文不同位置的注意力不一样,所以排序不是随便排的。我的经验是遵循“重要信息放两端,次要信息放中间”的原则。

具体来说,系统指令和核心约束放在最前面,因为这是模型必须始终记住的。当前任务和最关键的材料放在最后,因为这是模型生成时“眼前”的信息,注意力最集中。历史对话和次要参考材料放在中间。

还有一个细节:格式约束最好在开头和结尾各强调一次。比如你要求模型输出 JSON,那就在系统指令里说一遍,在用户消息末尾再提醒一遍。实测下来,这样能显著降低格式错误的概率。原因也不难理解,模型在生成时对最近的内容更敏感,结尾的提醒相当于临门一脚。

3.3 压缩:让历史对话不占地方又不丢信息

多轮对话最大的敌人就是历史累积。十轮对话下来,光历史就可能占掉几千 token,而且越往后越臃肿。压缩是必须的,但压缩不能简单截断,否则会丢关键信息。

我常用的压缩策略有三种。第一种是滑动窗口,只保留最近 N 轮,简单粗暴但有效,适合历史信息价值衰减快的场景。第二种是摘要压缩,把较早的对话用模型总结成一段简短摘要,保留关键结论和已确认事项。第三种是结构化提取,把对话中出现的实体、约束、决策点提取成结构化字段,比如“用户已确认预算上限为 X”“已排除方案 Y”。

这三种策略可以组合使用。我的常见做法是:最近三轮保留原文,三轮之前的对话压缩成摘要,同时把关键约束提取成结构化字段放在系统指令区。这样既控制了长度,又保证了关键信息不丢。

注意:摘要压缩本身也要消耗一次模型调用,所以要权衡成本和收益。如果对话轮次不多,直接滑动窗口可能更划算。

3.4 约束:把“希望它这样做”变成“必须这样做”

约束是上下文工程里最直接见效的部分。很多人写系统指令时喜欢用“尽量”“最好”“如果可以”这类模糊词,结果模型的行为就很不稳定。我的建议是:能用硬性规则表达的,就不要用软性期望。

比如,与其写“回答尽量简洁”,不如写“回答不超过三句话”。与其写“尽量用中文”,不如写“必须用中文回答,不得出现其他语言”。与其写“格式尽量规范”,不如直接给出一个 JSON schema 示例。

约束还包括负面约束,也就是明确告诉模型不要做什么。比如“不要编造材料中没有的信息”“不要重复用户的问题”“不要在回答里出现内部字段名”。负面约束在防止模型跑偏时特别有用,因为模型天然倾向于“多说话”,明确的禁止能有效收敛它的行为。

4. 实操落地:一套可复用的上下文组装流程

4.1 整体流程设计

讲了这么多原则,接下来给一套可以直接抄作业的流程。我把它分成五步:

  1. 收集所有候选信息:系统指令、用户当前输入、历史对话、检索结果、工具返回、格式要求。
  2. 按优先级筛选:硬约束全保留,任务相关按相关性排序取 top N,历史按轮次和摘要处理。
  3. 组装成结构化上下文:按“系统区—历史区—材料区—任务区”的顺序拼接。
  4. 做长度检查:估算 token 数,超限则触发压缩或裁剪。
  5. 生成前做一次自检:确认关键约束在开头和结尾都出现了。

这套流程看起来步骤不少,但实际实现起来并不复杂,核心是把每个环节的决策显式化,而不是随手拼接。

4.2 一个具体的上下文模板

下面是我常用的一个上下文模板结构,用伪代码表示:

[系统区] 你是XX助手。必须遵守以下规则: 1. 只基于提供的材料回答,材料中没有的信息不要编造。 2. 输出格式必须是JSON,字段包括answer和confidence。 3. 回答使用中文。 [历史区] (最近三轮对话原文) (更早对话的摘要:用户已确认...,已排除...) [材料区] 材料1:... 材料2:... 材料3:... [任务区] 用户当前问题:... 再次提醒:只基于材料回答,输出JSON格式。

这个模板的关键点在于:系统区放硬约束,历史区做压缩,材料区控制数量,任务区放当前问题并重复关键约束。实测下来,这个结构在大多数问答和助手场景里都很稳。

4.3 参数选择与长度预算

长度预算是很多人忽略的一环。我的做法是给每个区分配一个 token 预算,比如系统区不超过 500 token,历史区不超过 1500 token,材料区不超过 3000 token,任务区不超过 500 token。总预算根据模型窗口留出足够余量,一般不超过窗口的 60% 到 70%。

为什么要留余量?因为模型的输出也要占 token,而且留出余量能让注意力更集中。把窗口塞到 95% 满,效果往往不如塞到 60% 好。这个比例不是绝对的,需要根据具体任务调,但“不要塞满”这个原则是通用的。

4.4 实操现场记录:一次格式错乱的修复过程

说一个我实际遇到的案例。当时做一个信息抽取工具,要求模型输出固定字段的 JSON。单轮测试没问题,但多轮之后模型开始输出多余的解释文字,导致解析失败。

排查过程是这样的:先看上下文,发现历史对话里模型之前的回答包含了自然语言解释,这些解释被当作历史带进了新一轮,模型就“有样学样”继续输出解释。修复方法是在历史区做过滤,只保留结构化的 JSON 结果,把自然语言解释剔除掉。同时在任务区末尾加了一句“只输出JSON,不要任何额外文字”。改完之后,格式错误率从大概三成降到了几乎为零。

这个案例说明一个道理:历史里的坏例子会被模型模仿。所以历史区不仅要压缩,还要清洗,把不符合期望的输出剔除掉。

5. 常见问题与排查技巧实录

5.1 模型“失忆”,忘了前面的约束

这是最常见的问题。排查思路是:先确认约束是否真的在上下文里,再看它出现的位置。如果约束只在开头出现,而对话已经很长,模型很可能就淡忘了。解决办法是在任务区末尾重复关键约束,或者把约束提取成结构化字段放在显眼位置。

5.2 模型被无关材料带偏

如果检索结果里混入了不相关内容,模型很容易被带跑。排查时先看材料区的相关性,做一次人工抽查。解决办法是加相关性阈值过滤,并限制材料数量。另外,可以在系统指令里明确“只使用与问题直接相关的材料”。

5.3 输出格式不稳定

格式问题几乎都和约束的强度有关。排查时看约束是否用了模糊词,是否在结尾重复。解决办法是给出明确的格式示例,并在开头结尾各强调一次。如果还是不稳定,可以考虑用结构化输出功能或后处理校验。

5.4 响应越来越慢、成本越来越高

这通常是历史累积导致的。排查时统计每轮的 token 消耗,看历史区占比。解决办法是启用摘要压缩和滑动窗口,给历史区设硬上限。

下面这张表是我整理的常见问题速查表,方便对照排查:

问题现象可能原因排查动作解决方向
忘记约束约束位置太靠前或被淹没检查约束出现位置结尾重复约束
被带偏材料噪声大抽查材料相关性加过滤和数量上限
格式错乱约束模糊或历史有坏例子检查约束措辞和历史输出明确格式并清洗历史
越来越慢历史累积统计 token 占比摘要压缩加滑动窗口
编造信息缺少负面约束检查是否有禁止编造的指令加明确的负面约束

5.5 几个独家避坑技巧

第一个技巧:给上下文加“锚点”。在长上下文里,用明显的分隔标记(比如### 材料开始、### 材料结束)把不同区域隔开,能帮助模型区分信息边界,减少混淆。

第二个技巧:约束要具体到可验证。“回答要准确”这种约束没法验证,“材料中没有的信息不要出现”就可以验证。约束越具体,模型越容易遵守。

第三个技巧:定期做上下文回归测试。准备一组固定的多轮测试用例,每次改动上下文结构后跑一遍,看效果有没有退化。上下文工程很容易“改一处坏一处”,回归测试能帮你及时发现。

6. 上下文工程与提示词工程的区别

很多人把上下文工程和提示词工程混为一谈,其实两者关注点不同。提示词工程主要关注“怎么把一句话说好”,比如措辞、语气、示例。上下文工程关注的是“模型能看到什么”,包括信息的选取、组织和管理。

打个比方,提示词工程像是你写邮件时的措辞技巧,上下文工程像是你决定这封邮件里附哪些附件、按什么顺序排列、要不要加摘要。两者都重要,但在复杂系统里,上下文工程的影响往往更大,因为它决定了模型的信息基础。

我个人的体会是:单轮简单任务,提示词工程占主导;多轮复杂任务,上下文工程占主导。随着系统越来越复杂,上下文工程的权重会越来越高。这也是为什么这个主题值得单独拿出来讲。

7. 不同场景下的上下文策略差异

上下文工程没有万能方案,不同场景策略差别很大。问答类场景,重点是材料筛选和相关性排序。助手类场景,重点是历史压缩和约束管理。Agent 类场景,重点是工具返回结果的整理和多步任务的中间状态管理。

以 Agent 场景为例,工具返回的结果往往很长很杂,直接塞进上下文会迅速占满窗口。我的做法是对工具返回做一次结构化提取,只保留关键字段和结论,把原始返回存档备查。这样既保留了信息,又控制了长度。

再比如多步任务,中间状态的管理很关键。我会把每一步的结论提取成简短的状态记录,而不是把每一步的完整过程都带上。这样模型在后续步骤里能看到“已经完成了什么”,但不会被过程细节淹没。

8. 我踩过的几个典型坑

第一个坑是过度压缩。有一次为了省 token,把历史压得太狠,结果关键约束被压没了,模型开始乱答。后来我调整策略,硬约束永远不压缩,只压缩过程性内容。

第二个坑是材料排序随意。早期我没在意材料顺序,后来发现把最相关的材料放在最后,效果明显更好。原因是模型对结尾的注意力更集中。

第三个坑是忽视输出长度。有段时间响应特别慢,排查后发现是模型输出太长,而输出长度也受上下文影响。当上下文里有很多长文本时,模型倾向于也输出长文本。后来我在约束里加了输出长度限制,问题就缓解了。

这些坑的共同点是:它们都不是模型能力问题,而是上下文设计问题。这也是我想强调的核心观点——很多时候,效果不好不是模型不行,而是上下文没组织好。

9. 一套可以立刻上手的检查清单

最后分享一份我日常用的上下文自检清单,每次上线前过一遍:

  • 硬约束是否在开头和结尾都出现?
  • 历史是否做了压缩和清洗?
  • 材料是否做了相关性过滤和数量限制?
  • 关键信息是否放在了注意力高的位置?
  • 输出格式是否有明确示例?
  • 是否有防止编造的负面约束?
  • 总 token 是否留足了余量?
  • 是否跑过回归测试?

这份清单不复杂,但能挡住大部分低级问题。上下文工程说到底是个细致活,没有什么一招制敌的秘诀,靠的就是把这些细节一个个抠到位。我在实际项目里的体会是,把上下文工程做扎实之后,同样的模型能发挥出明显更好的效果,而且系统稳定性会大幅提升。这个投入,绝对值得。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进