ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

context-mode:多轮对话与Agent场景下的上下文管理模式设计与落地

context-mode:多轮对话与Agent场景下的上下文管理模式设计与落地

1. 从“context-mode”这个词说起:它到底指什么

第一次看到“context-mode”这个标题,很多人会愣一下——它不像“XX管理系统”“XX爬虫”那样一眼能看出用途,反而像某个框架里的一个配置项,或者某个编辑器的一个状态开关。我最初接触这个词,是在做对话式应用的时候,当时团队里有人提出“要不要给会话加一个 context-mode”,我才意识到,这其实是一个被严重低估的设计概念。

先把话说直白:context-mode 本质上是一种“上下文管理模式”。它描述的是一个系统在处理信息时,如何决定“当前这一步该看多少历史、该忽略多少历史、该把哪些信息提升为长期记忆”。你可以把它理解成人的注意力机制——你在跟朋友聊天时,不会把对方三年前说过的每一句话都翻出来重新过一遍,而是根据当前话题,自动调取相关的近期记忆和少量长期记忆。context-mode 要解决的,就是让机器也具备这种“按需取用上下文”的能力。

为什么这个概念最近被频繁提起?因为现在越来越多的应用不再是“一问一答”的无状态交互,而是多轮对话、长文档处理、多步骤任务编排。一旦交互变长,上下文就会膨胀:token 消耗飙升、响应变慢、关键信息被淹没在噪声里。context-mode 就是用来给上下文“做减法”和“做分层”的。它适合谁参考?如果你在做聊天机器人、智能客服、文档问答、Agent 工作流,或者任何需要维护“会话状态”的系统,这个概念你绕不开。

我见过太多项目,前期跑得挺顺,一到真实场景就崩——用户聊了二十轮之后,模型开始答非所问,或者成本高到无法承受。根因往往不是模型不行,而是上下文管理没设计好。所以这篇内容,我想把 context-mode 这件事从概念到落地讲透,包括它背后的取舍逻辑、几种常见模式的适用边界,以及我在实际项目里踩过的坑。

2. 为什么“无脑塞上下文”一定会出问题

2.1 上下文窗口变大,不等于问题被解决

很多人有个误区:现在模型的上下文窗口动辄几十万 token,那我直接把所有历史都塞进去不就行了?理论上可行,实践上非常糟糕。原因有三层。

第一层是成本。上下文窗口是按 token 计费的,你每轮都把全部历史重新发一遍,token 消耗是随轮次线性甚至平方增长的。假设每轮对话新增 200 token,聊到第 50 轮,单次请求就要带上近万 token 的历史。如果一天有几千次会话,账单会非常难看。我做过一个粗略测算:一个中等规模的客服场景,不做上下文裁剪的话,月度 token 成本能比做裁剪高出 4 到 6 倍。

第二层是效果。这不是玄学,而是有实证的。当上下文里塞了大量无关历史时,模型对当前问题的注意力会被稀释,专业说法叫“lost in the middle”——关键信息如果埋在长上下文的中间位置,模型很容易忽略。你会发现模型开始“翻旧账”,把很早之前的话题重新扯进来,或者对当前指令反应迟钝。

第三层是延迟。上下文越长,推理时间越长。对于实时交互场景,用户等三秒和等八秒,体验是天壤之别。所以 context-mode 的第一个核心任务,就是在保留必要信息的前提下,尽可能压缩上下文体积。

2.2 一个真实的反面案例

我之前接手过一个文档问答项目,初版逻辑极其简单:用户每问一个问题,就把整篇文档加上之前所有问答记录一起发给模型。文档本身两万字,聊到第十轮,单次请求就超过三万 token。表现是什么?前几轮还行,到后面模型开始混淆不同问题的答案,甚至把用户上一轮问的内容当成文档内容来回答。

排查过程很有意思。我先把历史记录截断到最近三轮,问题立刻缓解了一大半。这说明什么?说明大部分历史对当前问题根本没贡献,反而在捣乱。后来我引入了分层上下文管理,把“文档内容”和“对话历史”分开处理,文档用检索方式按需取用,历史只保留摘要和最近几轮原文,token 直接降到原来的五分之一,准确率反而上升了。

这个案例让我彻底明白:上下文不是越多越好,而是要“刚刚好”。context-mode 的设计目标,就是找到这个“刚刚好”的边界。

3. 四种常见的 context-mode 及其适用边界

3.1 滑动窗口模式:最简单也最容易被误用

滑动窗口是最直观的模式:只保留最近 N 轮对话,超出的直接丢弃。实现简单,成本可控,适合短会话场景,比如简单的问答机器人、一次性咨询。

但它的问题也很明显:它会“失忆”。如果用户在第五轮说了一个关键约束(比如“我对花生过敏”),到第十五轮再问“推荐个零食”,这个约束早就被滑出窗口了,模型给出的推荐可能包含花生。这在医疗、金融、客服等场景是致命的。

我的经验是,滑动窗口可以作为兜底策略,但不能作为唯一策略。它适合那些“每轮问题相对独立”的场景。判断标准很简单:如果你把历史全部删掉,只留当前问题,答案会不会变?如果不会,那滑动窗口够用;如果会,你就需要更复杂的模式。

3.2 摘要压缩模式:用“记忆摘要”换空间

摘要压缩的思路是:不保留全部原文,而是定期把历史对话压缩成一段摘要,随请求一起发送。这样既保留了长期信息,又控制了体积。

具体怎么做?常见做法是每积累 K 轮对话,就调用一次模型,把这几轮压缩成两三句话的摘要,追加到“长期记忆”里,然后清空这几轮的原文。请求时,上下文 = 长期记忆摘要 + 最近几轮原文。

这个模式的关键在于摘要的质量。我踩过的坑是:早期让模型自由发挥做摘要,结果它把一些看似不重要但实际关键的细节丢了,比如用户提到的具体数字、专有名词。后来我改成结构化摘要,强制模型按“用户身份信息 / 已确认事实 / 待办事项 / 当前话题”几个字段来填,信息保留率明显提升。

提示:摘要压缩适合长会话、需要记住用户偏好的场景,比如个性化助手、长期陪伴类应用。但摘要本身也是 token,别指望压缩到零,一般能压到原文的 10% 到 20% 就很不错了。

3.3 检索增强模式:把上下文变成“按需查询”

检索增强(RAG 思路在上下文管理上的应用)是另一种思路:不把所有历史都放进上下文,而是把它们存进一个向量库或关键词索引,当前问题来了,先去检索最相关的若干条历史,再拼进上下文。

这个模式特别适合历史信息量大、但每次只用一小部分的场景。比如一个知识库问答系统,用户问“退款流程”,你只需要检索出和退款相关的历史对话和文档片段,其他无关的聊天记录根本不用带。

实现上有几个细节要注意。第一是检索粒度,按轮次检索还是按段落检索?我一般建议按“语义完整的片段”检索,太细会丢上下文,太粗会引入噪声。第二是检索数量,一般取 top 3 到 top 5 就够,取太多等于没检索。第三是时效性加权,近期历史应该比远期历史权重更高,可以在检索打分时加一个时间衰减因子。

3.4 分层记忆模式:最接近人类的设计

分层记忆是我目前最推荐的模式,尤其适合复杂的 Agent 场景。它把上下文分成几层:

层级内容保留策略典型体积
工作记忆当前轮次和最近 2-3 轮原文始终保留小
短期记忆本次会话的摘要随会话滚动更新中
长期记忆跨会话的用户画像、偏好、事实持久化存储,按需检索大
外部知识文档、数据库等检索取用不定

请求时,系统根据当前任务动态决定从哪几层取信息。比如用户问“帮我订上次那家餐厅”,系统需要从长期记忆里取出“上次那家餐厅”是什么,从短期记忆里确认当前对话意图,工作记忆提供当前这句话。这种分层让每一层各司其职,既不会信息过载,也不会关键信息丢失。

4. 落地 context-mode 时最容易踩的五个坑

4.1 坑一:把“裁剪”做成了“截断”

很多人实现上下文管理时,直接按字符数或 token 数截断,从尾部砍掉超出部分。这是最粗暴也最危险的做法,因为你可能正好砍掉了关键信息。

正确做法是按语义单元裁剪。对话的最小语义单元是“一轮问答”,文档的最小单元是“一个段落或一个小节”。裁剪时以这些单元为粒度,而不是按字符数硬切。如果必须按 token 控制,也要先按语义单元分组,再从最不重要的组开始丢弃。

4.2 坑二:摘要更新频率没调好

摘要压缩模式里,多久做一次摘要是个关键参数。太频繁,摘要调用本身消耗大量 token 和延迟;太稀疏,原文积累太多,压缩效果打折。

我的经验值是:每 5 到 8 轮对话做一次摘要比较平衡。但这个值要看场景,如果是信息密度很高的对话(比如技术排查),可以缩短到 3 轮;如果是闲聊类,可以放宽到 10 轮。另外,摘要不要覆盖式更新,而是“增量追加 + 定期合并”,避免每次重写导致信息漂移。

4.3 坑三:忽略了“上下文顺序”的影响

同样一组信息,放在上下文的不同位置,模型的使用效果是不一样的。实测下来,把最关键的信息放在上下文的开头和结尾,模型利用率最高;放在中间最容易被忽略。

所以拼接上下文时,顺序应该是:系统指令 → 关键约束/长期记忆 → 检索到的相关历史 → 最近几轮原文 → 当前问题。当前问题放最后,是因为模型对末尾内容的注意力最强。

4.4 坑四:没有做上下文去重

多轮对话里经常出现重复信息。比如用户反复确认同一个需求,或者检索出来的历史片段之间有重叠。如果不做去重,这些重复内容会白白占用 token。

简单的做法是在拼接前做一次相似度检测,把高度相似的片段合并。更彻底的做法是在存储层就做好去重,同一事实只存一份,用引用计数管理。

4.5 坑五:忘了给上下文加“元信息”

光有内容不够,还要告诉模型这些内容是什么。比如“以下是三天前的对话摘要”“以下是用户的历史偏好”“以下是检索到的相关文档片段”。加上这些标签,模型才知道该怎么用这些信息,而不是把它们当成当前对话的一部分。

我见过一个案例,因为没加标签,模型把检索到的历史文档内容当成了用户当前说的话,回答完全跑偏。加上“以下内容来自知识库,不是用户发言”之后,问题立刻消失。

5. 一套可复用的 context-mode 实现骨架

5.1 数据结构设计

先定义清楚你要管理哪些上下文。我一般用这样的结构:

class ContextItem: def __init__(self, content, item_type, timestamp, importance=1.0): self.content = content # 内容文本 self.item_type = item_type # 类型:dialogue/summary/fact/document self.timestamp = timestamp # 时间戳,用于时效性加权 self.importance = importance # 重要性权重,0-1 self.token_count = count_tokens(content)

这个结构的关键是item_type和importance。类型决定了它在拼接时的位置和标签,重要性决定了它在裁剪时的优先级。

5.2 上下文组装流程

组装一次请求的上下文,我通常按这个流程走:

  1. 收集候选:从工作记忆、短期记忆、长期记忆、外部知识四个来源收集候选片段。
  2. 打分排序:按相关性(与当前问题的语义相似度)、时效性(越近越高)、重要性三个维度打分。
  3. 去重合并:合并相似片段,去除冗余。
  4. 预算分配:设定总 token 预算,按分数从高到低填充,直到接近预算上限。
  5. 排序拼接:按“关键信息靠前靠后”的原则排列,加上类型标签。
  6. 最终校验:检查总 token 是否超限,超了就再裁一轮。

这个流程里,预算分配是最需要调参的地方。我一般把总预算的 20% 留给系统指令和当前问题,30% 留给最近原文,30% 留给摘要和长期记忆,20% 留给检索内容。当然这个比例要按场景调。

5.3 一个最小可运行示例

下面是一个简化版的上下文管理器核心逻辑,用 Python 示意:

def build_context(current_query, memory, token_budget=4000): # 1. 收集候选 candidates = [] candidates += memory.get_recent_dialogues(n=3) candidates += memory.get_summaries() candidates += memory.retrieve_facts(current_query, top_k=5) # 2. 打分 for item in candidates: item.score = ( 0.5 * semantic_similarity(item.content, current_query) + 0.3 * time_decay(item.timestamp) + 0.2 * item.importance ) # 3. 排序并填充预算 candidates.sort(key=lambda x: x.score, reverse=True) selected = [] used = 0 for item in candidates: if used + item.token_count > token_budget * 0.8: continue selected.append(item) used += item.token_count # 4. 拼接 context = format_system_prompt() context += format_with_tags(selected) context += f"\n当前问题:{current_query}" return context

这段代码不复杂,但涵盖了核心思想:候选收集、打分、预算控制、标签化拼接。你可以根据自己的场景替换打分函数和预算比例。

6. 不同场景下 context-mode 的选型建议

6.1 客服机器人:摘要 + 检索混合

客服场景的特点是会话轮次多、用户会重复描述问题、需要记住用户身份和订单信息。我的建议是:短期用摘要压缩记住会话主线,长期用检索取出用户历史工单和订单信息,工作记忆保留最近三轮。关键约束(比如用户明确说“不要再给我推荐这个”)要提升为高重要性事实,长期保留。

6.2 文档问答:检索为主,对话历史为辅

文档问答里,文档内容才是主体,对话历史只是辅助理解用户意图。所以上下文预算应该大部分给检索到的文档片段,对话历史只保留最近一两轮。检索时要注意按段落粒度,并且给每个片段加上来源标注,方便模型引用。

6.3 多步骤 Agent:分层记忆 + 任务状态

Agent 场景最复杂,因为它有任务状态。除了对话历史,还要维护“当前任务进行到哪一步”“已经完成了哪些子任务”“下一步要做什么”。我一般把任务状态单独作为一层,每步更新,始终放在上下文靠前的位置。历史对话则按摘要 + 检索的方式管理。

6.4 个性化助手:长期记忆是核心

个性化助手的价值在于“懂你”,所以长期记忆的权重最高。用户偏好、习惯、重要日期这些信息要持久化,并且在每次请求时都带上相关部分。短期对话历史可以压缩得狠一点,因为用户更在意“你记得我”,而不是“你记得我们刚才聊了什么”。

7. 实测中的几个经验数字

调 context-mode 参数时,下面这些数字是我在多个项目里反复验证过的起点,你可以直接拿去用,再根据实际情况微调。

  • 最近原文保留轮数:3 轮。少于 3 轮,模型容易丢失当前话题的上下文;多于 3 轮,收益递减明显。
  • 摘要触发间隔:6 轮。这个值在信息密度中等的对话里表现最稳。
  • 检索 top_k:4。取 4 条相关片段,既能覆盖大部分需求,又不会引入太多噪声。
  • 总 token 预算:按模型窗口的 30% 到 50% 设置。留出余量给模型输出,也避免上下文过长导致注意力稀释。
  • 时间衰减半衰期:24 小时。也就是说,一天前的信息权重降为一半,这个衰减速度在多数场景下比较合理。

这些数字不是金科玉律,但作为起点能帮你省下大量试错时间。我最初就是靠这几个值快速跑通第一版,然后再针对性优化的。

8. 关于 context-mode 的一点个人体会

做了这么多项目,我越来越觉得 context-mode 的本质不是技术问题,而是信息优先级判断问题。你得不断问自己:如果只能保留三样东西,哪三样对当前这一步最重要?这个判断做对了,技术实现反而是简单的。

另外,别指望一次设计就完美。上下文管理是个需要持续观察和调整的活儿。我一般会在系统里加一个“上下文使用日志”,记录每次请求实际用了哪些片段、各占多少 token、模型回答质量如何。跑一段时间后,你就能看出哪些信息是真正被用到的,哪些是白占地方的。根据日志做优化,比拍脑袋调参靠谱得多。

最后分享一个小技巧:在开发阶段,可以做一个“上下文可视化”的调试面板,把每次请求的上下文按层级和来源用不同颜色标出来。我第一次做这个面板时,才发现有大量重复的摘要片段在悄悄消耗预算,肉眼根本看不出来。这个面板后来成了团队里最常用的调试工具之一。

RELATED READING

延伸阅读

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