Skip to content

引言

一家电商公司的客服机器人上线了。用户开始聊天:

第 1 轮:用户问退货政策 → AI 准确回答 ✅
第 10 轮:用户换了个话题问物流 → AI 仍然准确 ✅
第 30 轮:用户回到退货话题 → AI 还能关联上下文 ✅
第 51 轮:用户说「刚才说的那个订单号多少来着?」→ AI:「抱歉,我没有相关记录。」❌

客服机器人「失忆」了。原因是:50 轮对话积累了约 15000 个 token,接近了模型的上下文窗口。系统自动截断了早期消息,所以第 3 轮提到的订单号被丢弃了。

多轮对话的上下文管理,是 AI 应用中最容易被忽视、也最容易出问题的环节。


问题:对话只会越来越长

上下文增长

每轮对话的 token 增长:

  第 1 轮:system(100) + user(50) + assistant(100) = 250 tokens
  第 2 轮:250 + user(50) + assistant(100) = 400 tokens
  第 3 轮:400 + user(50) + assistant(100) = 550 tokens
  ...
  第 N 轮:250 + (N-1) × 150 tokens

  第 50 轮 ≈ 7,650 tokens(还好)
  第 100 轮 ≈ 15,150 tokens(开始紧张)
  第 500 轮 ≈ 75,150 tokens(可能超出 128K 窗口?不太会)

  但如果每轮对话很长(比如 RAG 检索了 3000 token 的文档):
  第 50 轮 ≈ 250 + 49 × 3200 = 157,050 tokens → 超了!

超出上下文时的表现

上下文溢出的症状:

  1. API 报错:context_length_exceeded
     → 直接崩溃

  2. 模型「失忆」
     → 忘记了之前的重要信息

  3. 模型产生幻觉
     → 因为缺少上下文,开始编造

  4. 回答质量下降
     → 因为信息太多,模型难以聚焦

四种上下文管理策略

1. 截断策略(Truncation)

最简单的策略:只保留最近 N 轮对话

  完整对话:
  [轮次1] [轮次2] ... [轮次45] [轮次46] [轮次47] [轮次48] [轮次49] [轮次50]

  截断后(保留最近 10 轮):
  [截断] [截断] ... [截断] [截断] [轮次41] ... [轮次50]

  优点:实现简单,token 消耗可控
  缺点:丢失早期重要信息

  适用:闲聊、简单问答
python
def truncate_messages(messages: list, max_rounds: int = 10) -> list:
    """只保留最近 N 轮(system 消息始终保留)"""
    system_msg = [m for m in messages if m["role"] == "system"]
    conversation = [m for m in messages if m["role"] != "system"]

    # 每轮包含 user + assistant(可能还有 tool)
    # 简化处理:保留最后 2*max_rounds 条消息
    if len(conversation) > max_rounds * 2:
        conversation = conversation[-(max_rounds * 2):]

    return system_msg + conversation

2. 滑动窗口(Sliding Window)

更精细的截断:基于 token 数而非轮数

  设定 max_context_tokens = 10000
  从最新消息往前加,直到 token 数超过限制

  [早期消息] ... [消息N-5] [消息N-4] [消息N-3] [消息N-2] [消息N-1] [消息N]
                           ↑ 从这里开始保留
  总 token 数刚好 ≤ max_context_tokens
python
def sliding_window(messages: list, max_tokens: int = 10000) -> list:
    """基于 token 数的滑动窗口"""
    import tiktoken
    enc = tiktoken.encoding_for_model("gpt-4o")

    system_msgs = [m for m in messages if m["role"] == "system"]
    other_msgs = [m for m in messages if m["role"] != "system"]

    total_tokens = sum(len(enc.encode(m["content"])) for m in system_msgs)

    selected = []
    for msg in reversed(other_msgs):
        msg_tokens = len(enc.encode(msg["content"]))
        if total_tokens + msg_tokens > max_tokens:
            break
        selected.insert(0, msg)
        total_tokens += msg_tokens

    return system_msgs + selected

3. 摘要压缩(Summarization)

用 LLM 总结旧对话,用摘要替代原文

  原始对话(10000 tokens):
  [轮次1] [轮次2] ... [轮次50]

  处理后(3000 tokens):
  [摘要:用户是张三,订单号 #12345,之前问过退货政策...] [轮次41] ... [轮次50]

  优点:保留了关键信息
  缺点:摘要可能不准确,额外 API 调用成本
python
def summarize_old_messages(messages: list, keep_recent: int = 6) -> list:
    """总结旧消息,保留最近几轮"""
    system_msgs = [m for m in messages if m["role"] == "system"]
    other_msgs = [m for m in messages if m["role"] != "system"]

    if len(other_msgs) <= keep_recent * 2:
        return messages

    # 旧消息
    old_msgs = other_msgs[:-keep_recent * 2]
    recent_msgs = other_msgs[-keep_recent * 2:]

    # 生成摘要
    old_text = "\n".join(
        f"{m['role']}: {m['content']}" for m in old_msgs
    )

    summary_response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": f"总结以下对话中的关键信息(名字、订单号、重要决策等):\n{old_text}"
        }]
    )
    summary = summary_response.choices[0].message.content

    # 构造新消息列表
    summary_msg = {"role": "system", "content": f"之前的对话摘要:{summary}"}

    return system_msgs + [summary_msg] + recent_msgs

4. 混合策略(推荐)

生产级方案:System Prompt + 摘要 + 滑动窗口 + 重要信息提取

  ┌─────────────────────────────────┐
  │ System Prompt(始终保留)         │
  ├─────────────────────────────────┤
  │ 重要信息摘要(用户信息、关键事实)  │
  ├─────────────────────────────────┤
  │ 最近 N 轮完整对话                 │
  └─────────────────────────────────┘

  重要信息持续提取和更新:
  每轮对话后,提取新出现的关键信息追加到摘要中

四种策略对比

策略信息保留实现复杂度Token 成本适用场景
截断闲聊
滑动窗口一般通用
摘要压缩中(额外调用)长对话
混合策略最好生产环境

本节小结

概念要点
核心问题对话只会越来越长,最终超出上下文窗口
截断最简单,丢弃早期消息——适合闲聊
滑动窗口基于 token 数控制,比截断更精确
摘要压缩用 LLM 总结旧对话——保留关键信息
混合策略System Prompt + 摘要 + 最近对话——生产级方案

思考题

  1. 摘要压缩引入了额外的 API 调用成本。在什么情况下,这个额外成本是值得的?
  2. 如果用户在对话中给出了一个重要的密码或 API key,摘要时不小心把它总结进去了,会有什么安全问题?
  3. 你能设计一个自动判断「哪些信息值得保留」的机制吗?需要考虑什么因素?