人人都会AI编程

3.1.3 多轮对话上下文管理

更新时间:2026-06-30

多轮对话不是单轮问答的简单累加。用户说"再便宜一点的",模型必须知道"再"指的是哪件商品、之前筛过什么条件。上下文管理要做的,就是在有限的窗口长度里,让模型"记得住"且"不混淆"。

1. 核心矛盾

  • 窗口有限:哪怕是 128K 的模型,也不可能无限堆积聊天记录。
  • 成本与延迟:传得越多,Token 越贵、响应越慢。
  • 噪音干扰:太久远的闲聊会淹没当前真正重要的信息。

2. 常用策略

| 策略 | 适用场景 | 做法 |
|---|---|---|
| 滑动窗口 | 一般客服/助手 | 只保留最近 N 轮(如最近 10 轮),超出的丢弃。 |
| Token 硬截断 | 精确控制长度 | 按 Token 数从后往前截,确保总长度不超过阈值;系统提示始终保留。 |
| 摘要压缩 | 长文档/深度访谈 | 每过 M 轮,把早期对话调用模型总结成 1~2 句话,替换原始记录。 |
| 槽位缓存 | 任务型对话(订酒店、查订单) | 提取关键实体(时间、地点、价格)存入结构化缓存,后续直接读变量,不用翻历史。 |

3. 最小可用示例

下面是一个兼顾滑动窗口与 Token 截断的简化实现:

class ContextManager:
    def __init__(self, max_token=3000, max_rounds=10):
        self.history = []          # [(role, content), ...]
        self.max_token = max_token
        self.max_rounds = max_rounds
        self.slots = {}            # 关键信息槽位

    def add(self, role, content):
        self.history.append({"role": role, "content": content})
        # 1. 按轮数滑动
        if len(self.history) > self.max_rounds * 2:  # *2 因为含问答
            self.history = self.history[-self.max_rounds * 2:]
        # 2. 按 Token 截断(伪代码,实际需调用 tokenizer)
        while estimate_tokens(self.history) > self.max_token:
            self.history.pop(0)    # 丢弃最早的一轮

    def get_messages(self, system_prompt):
        # 系统提示必须始终置顶
        messages = [{"role": "system", "content": system_prompt}]
        # 注入槽位摘要,避免模型遗忘关键事实
        if self.slots:
            slot_text = "已知信息:" + str(self.slots)
            messages.append({"role": "system", "content": slot_text})
        messages.extend(self.history)
        return messages

4. 工程注意事项

  • 系统提示不可丢:截断时永远保留 system 消息,否则模型会突然"失忆"自己的角色。
  • 主动检测话题切换:当用户输入"换个问题"或意图分类显示新主题时,及时清空或归档旧上下文,避免残留干扰。
  • 敏感信息定期清理:用户名、手机号等 PII 数据,要么不进上下文,要么对话结束后立即清除,不要长期放在内存或日志里。
  • 摘要别做太勤:生成摘要本身也要调模型,建议每 5~8 轮做一次,而不是每轮都做。

上下文管理没有银弹。短对话直接滑动窗口最省事;长流程任务优先把关键信息抽到槽位里;只有持续很长时间的深度对话,才值得上摘要压缩。先跑起来,再按实际 Token 消耗和错误率微调阈值。