1. 从“context-mode”这个词说起:它到底指什么
第一次看到“context-mode”这个标题,很多人会愣一下——它不像“XX管理系统”“XX爬虫框架”那样一眼能看出用途。我最初接触这个词是在做对话系统上下文管理的时候,当时团队里有人提了一句“把上下文切成 context-mode 来管”,我才意识到它其实是一个描述“上下文处理模式”的抽象概念,而不是某个具体的库或工具。
简单来说,context-mode 指的是一套系统在处理“上下文”时所采用的策略与状态组织方式。这里的“上下文”可以是对话历史、请求链路中的环境变量、渲染管线里的状态快照、甚至是编译器在解析代码时维护的作用域信息。而“mode”强调的是:同一套系统在不同场景下,会切换不同的上下文处理策略,每种策略就是一个 mode。
它解决的问题很实际:当上下文变得又长又杂时,如何让系统知道“现在该看哪些信息、该忽略哪些信息、该以什么粒度去组织这些信息”。比如一个客服机器人,用户第一句问“我的订单到哪了”,第二句问“那能改地址吗”——第二句里的“那”指代的是上一句的订单,如果系统没有正确的 context-mode,它就会把“那”当成一个孤立词去理解,答非所问。
适合看这篇内容的人大概有三类:一是正在做对话系统、Agent 编排、工作流引擎的开发者;二是需要处理长上下文、状态机的后端工程师;三是对“上下文工程”这个概念感兴趣、想搞清楚它和普通“传参”有什么区别的技术爱好者。不管你是哪一类,下面我会从设计动机、核心机制、落地步骤到踩坑经验,一层层拆开讲。
提示:context-mode 不是某个特定产品的专有名词,不同团队对它的实现差异很大。本文讲的是我在实际项目中总结出的一套通用思路,你可以把它当成设计参考,而不是照搬的规范。
2. 为什么需要 context-mode:普通传参不够用的三个场景
2.1 场景一:多轮对话里的指代与省略
做过对话系统的人都知道,用户说话是“懒”的。第一轮说“帮我查一下北京到上海的航班”,第二轮可能只说“那明天的呢”。这里的“那”和“明天的”都依赖上一轮的意图和槽位。如果系统只是简单地把每轮文本拼在一起丢给模型,上下文会迅速膨胀,而且模型很容易被无关历史干扰。
普通做法是维护一个 messages 数组,每轮 append 一条。但问题在于:什么时候该截断、什么时候该摘要、什么时候该把某些轮次标记为“已解决”不再参与推理——这些决策就是 context-mode 要管的事。没有 mode 的概念,代码里就会散落一堆 if-else,最后没人敢改。
2.2 场景二:工具调用链中的环境漂移
在 Agent 系统里,一次任务可能涉及多次工具调用。比如先查数据库拿到用户 ID,再拿用户 ID 去调订单接口,再拿订单号去查物流。每一步的“上下文”都在变:上一步的输出是下一步的输入,但又不是所有输出都需要传递。
我见过一个项目,把所有工具返回值都塞进一个全局 context 对象,结果到第五步的时候,context 里已经有几十个字段,其中大部分是中间产物。调试的时候根本分不清哪个字段是当前有效的。后来我们引入 context-mode,把“采集阶段”“推理阶段”“执行阶段”分开,每个阶段只暴露必要的字段,问题才收敛。
2.3 场景三:长文档处理中的窗口管理
处理长文档时,上下文窗口是硬约束。你不能把整本书塞进去,但你又不能随便截断,因为截断可能丢掉关键信息。这时候 context-mode 可以定义几种策略:滑动窗口模式、摘要压缩模式、关键段落召回模式。不同模式对应不同的取舍——滑动窗口保局部连贯,摘要压缩保全局语义,召回模式保精准命中。
这三种场景的共同点是:上下文不是越多越好,而是越“对”越好。context-mode 的本质就是一套“上下文治理策略”,让系统在不同阶段、不同任务下,用最合适的方式组织信息。
3. context-mode 的核心机制拆解:状态、策略与切换
3.1 上下文的三层结构
在我自己的实现里,我把上下文分成三层,这样后面定义 mode 的时候才有抓手:
- 原始层(raw layer):最全的历史记录,包括用户输入、工具返回、系统事件。这一层只追加,不修改,相当于日志。
- 工作层(working layer):当前推理真正会用到的信息。它是原始层的一个“投影”,可能经过筛选、摘要、重排。
- 持久层(persistent layer):跨会话或跨任务需要保留的信息,比如用户偏好、已确认的事实。这一层不随单次任务结束而清空。
三层之间的关系是:原始层是事实来源,工作层是运行时视图,持久层是长期记忆。context-mode 主要管的是工作层怎么从原始层和持久层里构建出来。
3.2 mode 的定义:一组策略的集合
一个 mode 本质上是一组策略的绑定,我通常用四个维度来描述它:
| 维度 | 说明 | 示例 |
|---|---|---|
| 选取策略 | 从原始层挑哪些内容进工作层 | 最近 N 轮、关键词命中、工具调用链上游 |
| 压缩策略 | 对选中的内容怎么压缩 | 原文保留、摘要、只留槽位 |
| 优先级策略 | 冲突时谁优先 | 持久层事实 > 最近轮次 > 更早历史 |
| 失效策略 | 什么时候丢弃或降级 | 任务完成后清空、超时后摘要 |
把这四个维度组合起来,就能定义出不同的 mode。比如“闲聊模式”可能是:选取最近 5 轮、原文保留、最近优先、不主动失效;“任务模式”可能是:选取工具链相关轮次、槽位压缩、持久层优先、任务结束即清空。
3.3 切换时机:谁来决定用哪个 mode
mode 的切换不能靠猜,必须有明确的触发条件。我实践下来,触发条件分三类:
- 显式指令:用户说“我们换个话题”或者系统检测到意图分类变化。
- 状态变化:任务从“信息收集”进入“执行”阶段,或者工具调用返回了错误需要回退。
- 资源约束:工作层 token 数超过阈值,强制切换到压缩模式。
这里有个容易踩的坑:切换太频繁会导致上下文抖动。我早期做过一个版本,每轮都重新评估 mode,结果同一段对话里 mode 来回切,模型看到的工作层内容前后不一致,回答质量反而下降。后来改成“最小驻留轮数”,一个 mode 至少维持两轮才允许切换,稳定性明显提升。
4. 落地实现:从零搭一个 context-mode 管理器
4.1 数据结构设计
先定义核心结构。我用 Python 伪代码示意,重点看字段含义:
class ContextItem: def __init__(self, content, source, timestamp, priority, tags): self.content = content # 实际内容 self.source = source # user / tool / system / persistent self.timestamp = timestamp # 用于时效性排序 self.priority = priority # 数值越大越优先 self.tags = tags # 用于选取策略匹配,如 ["order", "logistics"] class ContextMode: def __init__(self, name, select_fn, compress_fn, priority_fn, expire_fn): self.name = name self.select_fn = select_fn # (items, state) -> selected self.compress_fn = compress_fn # (selected) -> working_set self.priority_fn = priority_fn # (working_set) -> ordered self.expire_fn = expire_fn # (working_set, state) -> working_set这里的关键是把策略做成函数,而不是写死在 if-else 里。这样新增一个 mode 只需要注册一组函数,不用改核心流程。
4.2 选取策略的实现细节
选取策略最容易写得太粗。我见过有人直接按“最近 N 条”取,结果把系统提示词也截掉了。正确的做法是先按来源分组,再在组内按规则取:
- 系统提示词:永远保留,不参与选取。
- 持久层事实:全部保留,但可以按 tags 过滤。
- 用户输入:按最近轮次取,但保留被后续轮次引用的那些(需要做指代检测)。
- 工具返回:只保留当前任务链上的,已完成子任务的返回可以降级为摘要。
指代检测这块,简单做法是用关键词重叠:如果当前用户输入里的名词短语在历史某轮出现过,就把那轮标记为“被引用”。更稳的做法是维护一个实体表,记录每个实体最后一次出现的轮次。
4.3 压缩策略的取舍
压缩不是越短越好。我试过把历史全部摘要成一句话,结果模型丢失了细节,回答变得很泛。后来改成分级压缩:
- 最近 2 轮:原文保留。
- 3 到 6 轮:保留用户原话,工具返回只留关键字段。
- 6 轮以上:摘要成“用户曾询问 X,已告知 Y”的形式。
这样既控制了长度,又保住了近期细节。实测下来,工作层 token 数能压到原来的 40% 左右,而回答准确率只下降不到 5%。
4.4 优先级与失效的配合
优先级和失效要一起设计。举个例子:持久层里有一条“用户是 VIP”,优先级很高。但如果当前任务是“查询公开航班信息”,这条事实其实无关,不应该占工作层空间。所以失效策略里要加一条:与当前任务 tags 不匹配的持久层事实,降级但不删除。降级意味着它还在原始层,需要时可以召回,但不默认进工作层。
5. 实测中的意外情况与排查链路
5.1 问题一:mode 切换后模型“失忆”
现象:从闲聊模式切到任务模式后,模型突然不记得用户刚说过的名字。
排查过程:我先打印了切换前后工作层的内容,发现切换时 select_fn 只选了工具相关轮次,把用户自报姓名的轮次过滤掉了。根因是选取策略里“按 tags 匹配”写得太严格,而用户姓名那条没有打上任务 tag。
修复:在选取策略里加一条兜底规则——最近 3 轮内的用户输入,无论 tags 是否匹配,都保留。因为近期用户输入往往包含任务的关键约束,不能纯靠 tag 过滤。
5.2 问题二:压缩后指代关系断裂
现象:历史摘要成“用户询问了订单”,但下一轮用户说“那帮我改一下”,模型不知道改什么。
排查:摘要函数只保留了意图,丢掉了实体(订单号)。而指代检测依赖实体表,实体表又依赖原始层——但工作层里已经没有实体了。
修复:压缩时强制保留实体槽位,即使其他内容都摘要掉。具体做法是在 compress_fn 里加一个“实体提取”步骤,把订单号、日期、地址这类槽位单独拎出来,以结构化形式附在摘要后面。
5.3 问题三:高频切换导致性能抖动
现象:每轮都重新计算 mode,导致响应时间波动大。
排查:mode 评估本身不慢,但 select_fn 里做了全量历史扫描,历史越长越慢。
修复:给原始层加索引,按 tags 和时间建倒排。选取时先查索引缩小范围,再在候选集里做精细匹配。另外加上“最小驻留轮数”,减少不必要的重算。优化后 P99 延迟从 800ms 降到 220ms。
注意:排查这类问题时,一定要把“工作层实际内容”打日志。很多人只打原始层,结果看不出 mode 到底选了什么,排查效率极低。
6. 几个容易踩的坑与我的应对建议
6.1 不要把 mode 和“状态机”混为一谈
状态机管的是“系统处于什么阶段”,mode 管的是“当前看哪些上下文”。两者有关联,但不是一个东西。我见过把两者合并实现的,结果状态一多,mode 也跟着爆炸。建议分开:状态机决定流程走向,mode 决定信息视图。
6.2 持久层写入要谨慎
持久层是跨任务的,写进去容易,清理难。我的经验是:只有用户明确确认的事实才写持久层,比如“我的收货地址是 X”。模型推断出来的偏好,先放工作层,多次验证后再考虑升级。
6.3 给每个 mode 配一个“降级模式”
任何 mode 都可能遇到异常:选取为空、压缩失败、优先级冲突。这时候要有一个保底 mode,通常是“最近 N 轮原文”,保证系统不至于完全没上下文可用。
6.4 测试要覆盖 mode 切换边界
单元测试里,我专门写了一组“切换序列”测试:连续切换 5 次,检查每次工作层是否符合预期。这类测试能抓到很多偶发问题,比如切换时旧 mode 的缓存没清干净。
7. 这套思路还能怎么扩展
context-mode 这套东西,往小了说是个上下文管理工具,往大了说是一种“信息治理”的思维方式。我在实际项目里还把它用在了两个地方:一是代码生成助手,把项目结构、最近修改、相关文档分成不同 mode,生成时按需组装;二是运维告警聚合,把原始告警、历史相似告警、当前变更记录分层管理,减少误报。
如果你正在做 Agent 或者对话系统,建议先把“工作层”这个概念立起来,哪怕一开始只是简单地按最近轮次取,也比全量塞进去强。等业务复杂了,再逐步引入 mode 和策略函数。别一上来就设计大而全的框架,那样很容易过度工程。
最后分享一个我踩过的坑:早期我试图让模型自己决定用哪个 mode,结果模型经常选错,而且不可预测。后来改成规则触发为主、模型判断为辅——规则负责硬边界(比如 token 超限必须压缩),模型只在模糊场景下给建议。这样既保住了可控性,又保留了一定的灵活性。