ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

上下文压缩的可靠性设计:控制字段与状态版本校验机制

上下文压缩的可靠性设计:控制字段与状态版本校验机制

1. 上下文压缩之后,为什么“看起来没丢”反而最危险

做过 Agent 系统的人大概都有过这种体验:对话轮次一多,上下文窗口顶不住,于是上压缩策略——摘要、滑窗、关键信息抽取、向量召回,一套组合拳打下来,token 数确实降下来了,模型也不报超长了。但真正让人心里发毛的时刻,是某天用户问了一句“我上次说的那个截止日期是几号来着”,Agent 一脸自信地回了一个完全错误的日期,语气还特别笃定。

这就是上下文压缩最阴险的地方:它不会报错,它只会悄悄丢信息,然后用幻觉把坑填上。

我在做 Agent 系统工程这个系列的时候,把上下文压缩单独拎出来讲,就是因为绝大多数团队在压缩环节只做了“减法”,没做“校验”。压缩本身不难,难的是压缩完之后,你怎么知道哪些关键信息还在、哪些已经没了。这篇文章就围绕这个问题展开,把上下文压缩的校验机制、状态版本管理、控制字段设计这几件事讲透。适合正在搭 Agent 框架、做多轮对话系统、或者被上下文窗口折磨过的开发者参考,不管你是刚入门 agent 开发,还是已经在调多 agent 协作的老手,这里面的坑大概率你都踩过或者即将踩到。

先说结论:上下文压缩的可靠性,不取决于压缩算法多聪明,而取决于你有没有一套独立于压缩过程之外的“关键信息账本”。压缩可以丢东西,但账本不能丢。下面我从设计思路开始,一层层拆。

2. 上下文压缩的整体设计与校验思路拆解

2.1 压缩的本质是信息有损,别指望它无损

很多人对上下文压缩有个误解,觉得只要摘要写得好、抽取做得准,就能做到“无损压缩”。这个想法从根上就是错的。上下文压缩和 zip 压缩是两码事,zip 能无损是因为它利用了数据的统计冗余,解压后能一比一还原。而上下文压缩面对的是自然语言,摘要本身就是一种有损变换,你不可能从一段 200 字的摘要里还原出原始 2000 字的对话。

所以正确的心理预期是:压缩一定丢信息,问题只是丢的是不是关键信息。既然一定丢,那我们的目标就从“不丢”变成了“丢了能发现、发现能补救”。这个思路的转变很关键,它直接决定了你的系统架构——你需要一个独立的校验层,而不是把希望寄托在压缩算法本身。

我见过一些 agent 框架,压缩逻辑和业务逻辑耦合在一起,摘要生成完直接替换原始上下文,中间没有任何校验。这种设计在 demo 阶段没问题,一上生产就出事。因为一旦关键信息在压缩时被丢掉,后面所有依赖这条信息的决策都会连锁出错,而且你根本不知道错在哪一步。

2.2 校验层的三个核心问题

要设计校验层,先得回答三个问题:

  • 哪些信息算“关键信息”?不是所有信息都值得校验,全量校验成本太高。关键信息通常是那些一旦丢失就会导致任务失败或用户明显感知错误的字段,比如用户明确给出的约束条件、任务目标、已确认的事实、时间节点、金额数字等。
  • 怎么判断关键信息还在不在?这就需要一个独立于压缩结果的“账本”,记录压缩前关键信息的状态,压缩后拿账本去比对。
  • 发现丢了怎么办?是回滚、重新压缩、还是从原始上下文里补回来?这涉及到状态版本管理。

这三个问题对应了三个技术点:关键信息识别、状态版本、控制字段。下面逐个拆。

2.3 为什么用“控制字段”而不是“全文比对”

有人可能会想,校验还不简单,压缩前后做一次全文比对不就行了。问题是全文比对根本没法做,因为压缩后的文本和原文措辞完全不同,你没法用字符串匹配。就算用语义相似度,也只能判断“大意还在不在”,判断不了“那个具体的日期数字还在不在”。

所以更靠谱的做法是:在压缩之前,先把关键信息抽取成结构化的控制字段,比如{"deadline": "2024-06-30", "budget": 50000, "target_user": "企业客户"}。压缩之后,再从压缩结果里重新抽取一遍控制字段,两个版本做结构化比对。这样比对的是字段值,不是自然语言,准确率高得多,而且能精确定位到哪个字段丢了。

这个思路其实就是把“校验”从自然语言层面下沉到了结构化层面。自然语言层面模糊、不可靠,结构化层面精确、可断言。这也是为什么我在 agent 系统里一直强调:凡是需要可靠校验的东西,都要尽量结构化。

3. 核心细节解析:关键信息识别与控制字段设计

3.1 关键信息识别的三条实用规则

关键信息识别没有万能公式,但有几条实战中总结出来的规则,命中率比较高:

规则一:用户显式强调的内容优先。用户说“记住”“重点是”“一定要”“别搞错”这类词后面的内容,几乎都是关键信息。这类信号在对话里很明确,用一个轻量的分类模型或者关键词规则就能抓。

规则二:数字、日期、专有名词优先。这类信息一旦丢失,错误最明显,用户最容易察觉。而且这类信息在压缩时最容易被摘要“模糊化”——摘要模型倾向于把“6月30日”写成“近期”,把“50000元”写成“一笔预算”,这种模糊化对人是可读的,对 Agent 执行任务是致命的。

规则三:任务约束和已确认事实优先。比如“只能用免费方案”“已经确认用 A 供应商”“排除 B 方案”,这些是决策的前提,丢了会导致 Agent 走错方向。

把这三条规则组合起来,基本能覆盖 80% 以上的关键信息场景。剩下的长尾可以靠人工配置补充,比如在特定业务里把某些字段强制标记为关键。

3.2 控制字段的结构设计

控制字段不是随便存几个 key-value 就完事,它的结构设计直接影响校验的可靠性。我一般会设计成三层:

{ "field_id": "deadline_001", "field_type": "date", "value": "2024-06-30", "source_turn": 12, "confidence": 0.95, "critical": true, "version": 3 }

逐层解释一下:

  • field_id 和 field_type:给每个字段一个唯一标识和类型。类型很重要,因为不同类型比对方式不同,日期要比对具体值,文本要比对语义,数字要比对数值。
  • value:字段的实际值,这是校验的核心。
  • source_turn:这个字段来自第几轮对话。一旦校验发现丢了,可以顺着 source_turn 回到原始上下文里找,这是补救的关键线索。
  • confidence:抽取时的置信度。低置信度的字段在校验时可以放宽标准,避免误报。
  • critical:是否关键字段。只有 critical 为 true 的字段才参与强校验。
  • version:字段版本号,配合状态版本管理用。

这个结构看起来有点重,但实际跑起来开销很小,因为字段数量通常就几十个,比对是纯内存操作。

3.3 抽取时机:压缩前抽,压缩后再抽

控制字段的抽取要抽两次:压缩前抽一次,作为基准账本;压缩后再抽一次,作为校验对象。两次抽取用同一套抽取逻辑,保证可比性。

这里有个细节:压缩后的抽取,输入是压缩结果,可能是一段摘要,也可能是抽取出的若干条关键句。如果压缩结果本身已经是结构化的(比如你压缩时就抽成了关键句列表),那第二次抽取可以直接复用压缩过程的中间产物,省一次调用。

注意:两次抽取一定要用同一个模型或同一套规则。我踩过的坑是压缩前用规则抽、压缩后用模型抽,结果两边对同一个日期的表述不一致,校验疯狂误报,排查了半天才发现是抽取器不统一。

4. 状态版本管理:让每次压缩都可追溯、可回滚

4.1 为什么需要状态版本

上下文压缩不是一次性的,多轮对话里可能压缩好几次。每次压缩都是一次状态变更,如果没有版本管理,出了问题你根本不知道是哪次压缩丢的。

状态版本管理的核心思想是:每次压缩生成一个新版本,旧版本保留,版本之间可以 diff、可以回滚。这跟代码的版本控制是一个道理,只不过管理的是上下文状态。

具体来说,每次压缩时记录:

  • 压缩前的上下文快照(或至少是关键字段账本)
  • 压缩后的上下文
  • 压缩策略(摘要/滑窗/抽取)
  • 压缩前后的控制字段 diff
  • 时间戳和触发原因(比如 token 超限触发)

这样一旦发现关键信息丢了,可以精确定位到是哪次压缩、用了什么策略、丢了哪个字段。

4.2 版本 diff 的实用做法

版本 diff 不用做得太复杂,核心就是比对控制字段。我一般用一个简单的 diff 函数:

def diff_fields(old_fields, new_fields): lost = [] changed = [] for fid, old in old_fields.items(): if fid not in new_fields: lost.append(old) elif new_fields[fid]["value"] != old["value"]: changed.append((old, new_fields[fid])) return lost, changed

lost是丢失的字段,changed是值发生变化的字段。这两个列表就是校验的核心输出。lost 里的 critical 字段要告警,changed 里的字段要人工或自动确认是不是合理变更。

实测下来,这个 diff 逻辑能抓住绝大多数压缩导致的信息丢失。剩下抓不住的,通常是那些没被识别为关键信息的字段,这类只能靠抽样人工检查兜底。

4.3 回滚策略:不是所有丢失都要回滚

发现字段丢了,不代表一定要回滚。回滚成本高,而且可能引发新的问题。我的经验是分三档处理:

丢失类型处理策略理由
critical 字段丢失立即回滚到上一版本,重新压缩关键信息不能丢,宁可多花 token
非 critical 字段丢失记录告警,继续运行不影响任务,避免过度反应
字段值变化人工确认或二次抽取确认可能是合理更新,也可能是压缩错误

这个分档策略能避免系统过于敏感,一有风吹草动就回滚,那样反而影响可用性。

5. 实操过程:从压缩到校验的完整链路

5.1 完整链路拆解

把前面讲的东西串起来,一次完整的压缩校验链路是这样的:

  1. 压缩触发:监控 token 数,超过阈值(比如窗口的 70%)触发压缩。
  2. 基准抽取:从当前上下文抽取控制字段,生成基准账本,记录版本号。
  3. 执行压缩:用选定的压缩策略生成压缩后上下文。
  4. 二次抽取:从压缩结果抽取控制字段。
  5. diff 比对:比对基准账本和二次抽取结果,输出 lost 和 changed。
  6. 决策处理:根据丢失类型决定回滚、告警还是继续。
  7. 版本记录:把本次压缩的完整信息存入版本历史。

这个链路里,第 2 步和第 4 步是新增的校验开销,其他步骤是压缩本身本来就有的。校验开销主要是两次字段抽取的模型调用,如果字段抽取用轻量模型或规则,开销可以控制在可接受范围。

5.2 参数选择:阈值怎么定

几个关键参数的经验值:

  • 压缩触发阈值:窗口的 70% 到 80%。留 20% 到 30% 的余量给压缩后的对话和校验开销。定太低会频繁压缩,定太高可能压缩时已经超限。
  • critical 字段数量上限:建议控制在 20 个以内。太多会导致校验过于严格,频繁回滚。如果业务确实需要更多关键字段,考虑分层,核心 20 个强校验,其余弱校验。
  • 置信度阈值:字段抽取置信度低于 0.7 的,标记为弱校验,不参与强告警。

这些值不是拍脑袋定的,是我在几个项目里调出来的。你可以根据自己的业务特点微调,但大方向是:宁可校验宽松一点,也不要因为误报导致系统频繁回滚。

5.3 一个具体的校验现场记录

举个我实际遇到的例子。一个客服 Agent,用户在第 8 轮说“我的订单号是 A12345,请帮我查物流”。第 15 轮时上下文压缩,摘要生成后,控制字段校验发现order_id字段丢了。diff 输出:

lost: [{"field_id": "order_id_001", "value": "A12345", "critical": true, "source_turn": 8}] changed: []

因为order_id是 critical 字段,系统触发回滚,重新压缩时把订单号强制保留在摘要里。如果没有这套校验,Agent 在第 16 轮回答物流问题时就会因为没有订单号而胡编一个,用户直接炸锅。

这个例子说明:校验的价值不在于压缩本身,而在于压缩之后系统还能不能可靠地继续工作。

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

6.1 校验误报太多怎么办

误报是校验系统最常见的抱怨。表现是:明明关键信息还在,diff 却报 lost。原因通常有三个:

  • 抽取器不一致:压缩前后用了不同的抽取逻辑,导致同一信息表述不同。解决办法是统一抽取器。
  • 字段值格式不统一:比如日期一个抽成“2024-06-30”,一个抽成“6月30日”。解决办法是在比对前做归一化。
  • 语义相同但字面不同:比如“企业客户”和“B 端客户”。这种情况要么用语义比对,要么在抽取时就统一术语。

排查顺序建议:先看抽取器是否一致,再看格式是否归一化,最后才考虑语义比对。大部分误报在前两步就能解决。

6.2 关键信息识别漏了怎么办

漏报比误报更危险,因为漏报意味着关键信息丢了但系统没发现。漏报的常见原因是识别规则覆盖不全。我的做法是定期做“压缩后人工抽检”:随机抽一批压缩记录,人工看压缩结果,找出那些丢了但没被识别为关键的信息,反哺到识别规则里。

这个抽检不用太频繁,每周一次,每次抽 20 条左右,坚持几周就能把识别规则补得比较全。

6.3 压缩后上下文太短导致模型能力下降

有时候压缩太狠,关键信息是保住了,但上下文太短,模型缺乏足够的背景,回答质量下降。这是压缩率和可用性的权衡问题。我的经验是:压缩率不要超过 70%,也就是压缩后至少保留原上下文的 30%。低于这个比例,模型的表现会明显下滑。

如果确实需要更高压缩率,考虑分层压缩:近期对话保留原文,远期对话才压缩。这样既省 token,又保证近期上下文完整。

6.4 常见问题速查表

问题现象可能原因排查方向解决建议
校验频繁报 lost抽取器不一致/格式不统一检查两次抽取逻辑统一抽取器,比对前归一化
关键信息丢了没告警识别规则覆盖不全人工抽检压缩记录补充识别规则
压缩后回答质量下降压缩率过高检查压缩比例压缩率不超过 70%
回滚后仍然丢信息回滚版本本身有问题检查版本历史回滚到更早的稳定版本
字段值频繁变化抽取不稳定检查抽取置信度低置信度字段弱校验

6.5 几个踩过的坑

坑一:把校验做成同步阻塞。一开始我把字段抽取和 diff 都放在压缩主流程里同步执行,结果压缩耗时翻倍,用户明显感知到卡顿。后来改成异步校验,压缩先完成,校验在后台跑,发现问题再异步回滚或告警,体验好很多。

坑二:版本历史无限增长。每次压缩都存完整快照,跑几天存储就爆了。后来改成只存控制字段账本和 diff,原始上下文只保留最近几个版本,老版本归档。存储压力小了很多,校验能力没受影响。

坑三:忽略 source_turn 的价值。早期设计里没记 source_turn,发现字段丢了只能重新压缩,没法精准补回。加上 source_turn 后,可以直接从原始对话里把丢失字段对应的那句话捞出来,补进压缩结果,比重压缩高效得多。

7. 控制字段与状态版本的进阶玩法

7.1 用控制字段做跨会话记忆

控制字段这套机制,其实不只用于单次压缩校验,还能扩展到跨会话记忆。把每次会话的关键字段存到一个持久化的字段库里,下次会话开始时加载,Agent 就能记住用户上次说的偏好、约束、历史决策。这比把整段历史对话塞进上下文高效得多,也更可靠。

这也是 agent 记忆体系里短期记忆和长期记忆衔接的一个实用做法:短期记忆用完整上下文,长期记忆用控制字段。两者通过字段版本号关联,保证一致性。

7.2 多 Agent 协作时的字段同步

多 agent 协作场景下,控制字段还能当 agent 之间的“共享状态”。每个 agent 处理完自己的部分,把关键字段更新到共享字段库,其他 agent 读取。这样避免了 agent 之间传递大段上下文,也避免了信息在传递中丢失。

这里要注意字段的并发更新问题。多个 agent 同时更新同一个字段,需要加版本号做乐观锁,版本冲突时以高置信度或后写入的为准,具体策略看业务。

7.3 字段演化的追踪

控制字段不是一成不变的,随着对话推进,字段会新增、修改、废弃。追踪字段演化历史,能帮你分析 Agent 的决策路径。比如某个约束字段是什么时候被加入的、什么时候被修改的,这些信息对调试和优化 Agent 行为很有价值。

实现上就是在字段结构里加一个history数组,记录每次变更的值、时间和原因。查询时可以看到字段的完整演化链。

8. 我个人的一些实操体会

上下文压缩这件事,我做了几个项目之后最大的体会是:压缩算法本身的重要性,远低于校验机制的重要性。一个普通的摘要模型配上靠谱的校验,效果比一个顶级摘要模型配上没有校验要好得多。因为前者出了问题能发现、能补救,后者出了问题只能靠运气。

另一个体会是,控制字段这套东西,越早引入越好。很多团队是等到线上出了事故才想起来加校验,这时候上下文压缩逻辑已经和业务深度耦合,改造起来很痛苦。如果一开始就把字段抽取和校验作为压缩流程的标准环节,后面扩展和维护都轻松很多。

最后分享一个小技巧:控制字段的抽取,能用规则就别用模型。日期、数字、订单号这类结构化信息,正则加规则抽取又快又准,还省 token。模型抽取留给那些真正需要语义理解的字段,比如任务目标、约束条件。规则和模型混用,是性价比最高的方案。

这套机制我在几个 Agent 项目里跑下来,关键信息丢失导致的事故基本清零。当然它不是银弹,遇到特别复杂的语义信息,校验还是会有盲区,但至少把最常见、最致命的丢失场景兜住了。对于大多数 Agent 系统来说,这就够了。

RELATED READING

延伸阅读

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