LLM 对话机器人上线3个月后答非所问:上下文窗口溢出的4个真实踩坑与工程解法

封面图

客服机器人上线 3 个月后,用户开始反馈"答非所问"。查日志发现:单会话 token 数突破 18 万,关键信息被截断,模型拿到的上下文全是碎片。这就是上下文窗口溢出的威力——它不是慢慢来的,是突然崩的。

本文记录我们踩过的 4 个坑,以及每个坑对应的工程解法,全部是生产环境验证过的代码和配置。

坑 1:对话历史无限累积

最常见的错误:对话历史只增不减。3 个月下来,单会话 token 轻松破万,直到模型开始"失忆"。

错误示范:

# 错误:每轮都追加,不清理
messages.append({"role": "user", "content": user_input})
messages.append({"role": "assistant", "content": response})

正确做法:滑动窗口 + 关键信息池分离

import tiktoken

class ConversationManager:
    def __init__(self, model="gpt-4o", max_tokens=6000):
        self.model = model
        self.max_tokens = max_tokens
        self.encoder = tiktoken.encoding_for_model(model)
        # 关键信息池:永远不清理
        self.priority_pool = []
        # 对话历史:超过预算时从最旧处清理
        self.history = []

    def add_message(self, role, content, priority=False):
        if priority:
            self.priority_pool.append({"role": role, "content": content})
        else:
            self.history.append({"role": role, "content": content})
        self._trim_by_budget()

    def _trim_by_budget(self):
        while self._total_tokens() > self.max_tokens and self.history:
            self.history.pop(0)

    def _total_tokens(self):
        all_text = "\n".join([m["content"] for m in self.priority_pool + self.history])
        return len(self.encoder.encode(all_text))

    def build_context(self):
        return self.priority_pool + self.history

设计原则:工单号、订单号、用户ID等关键字段放进 priority_pool,永远保留,滑动窗口只清理普通对话历史。

坑 2:滑动窗口把关键字段切掉了

即使用了滑动窗口,如果只按顺序 pop(0),关键字段可能在第 3 轮就被挤出去。以下是我们的实测数据:

# 单会话 token 随对话轮次增长实测
Round 1  (新会话,用户问订单状态):       ~800 tokens    ✓
Round 15 (半个月用户):                   ~9800 tokens   ✓
Round 25 (三个月用户):                   ~46000 tokens   ⚠️  开始告警
Round 50 (重度使用用户):                 ~180000 tokens  ← 崩溃临界点

解决方案:关键词自动进入 priority_pool + 双重 token 控制。

    def add_message(self, role, content, priority=False):
        # 内容中包含关键字段,自动提升优先级
        keywords = ["订单号", "工单号", "ORDER", "工单", "case #"]
        is_priority = priority or any(kw in content for kw in keywords)
        target = self.priority_pool if is_priority else self.history
        target.append({"role": role, "content": content})
        self._trim_by_budget()

    def _trim_by_budget(self):
        while self._total_tokens() > self.max_tokens:
            if len(self.history) > 3:
                # 至少保留最近 3 轮
                self.history.pop(0)
            else:
                # 已经没有普通历史可删,触发告警
                logger.warning(f"Token budget exhausted: {self._total_tokens()}")
                break

坑 3:RAG 检索"工单"和"订单"傻傻分不清

接入了知识库,检索"工单号 20240518001 的处理进度",top_1 结果返回的是"订单号 20240518001 的物流信息",相似度只有 0.05。

# 检索日志(生产环境,脱敏)
query: "工单号 20240518001 的处理进度"
top_1: "订单号 20240518001 的物流信息:已发货"
similarity: 0.05  ← 这个值极低,正常应 > 0.7

# 原因:"工单"和"订单"在 embedding 空间中距离很近
# 短文本歧义更严重

解法:HyDE(Hypothetical Document Embeddings),先让模型生成假设答案,再用假设答案检索。

def hyde_retrieve(query, top_k=5):
    # Step 1: LLM 生成假设答案
    hypothetical = llm.generate(
        f"针对这个问题,给出你认为最可能的回答:{query}"
    )
    # Step 2: 用假设答案检索,召回率大幅提升
    results = vector_db.search(query=hypothetical, top_k=top_k)
    return results

# 对比测试(100条query,平均长度15字):
# 原始检索 recall@5:  0.42
# HyDE 检索 recall@5: 0.78  ← 提升明显

坑 4:没有 token 预算告警,出了事才知道

上面几个坑的共同问题:直到用户投诉才暴露。必须在扩散之前就告警。

from prometheus_client import Counter, Gauge

token_alerts = Counter(
    "token_budget_alert_total",
    "Token budget告警次数",
    ["session_id", "reason"]
)
session_tokens = Gauge(
    "session_current_tokens",
    "当前会话token数",
    ["session_id"]
)

    def _trim_by_budget(self):
        while self._total_tokens() > self.max_tokens:
            if len(self.history) > 3:
                self.history.pop(0)
            else:
                token_alerts.labels(
                    session_id=self.session_id,
                    reason="budget_exhausted"
                ).inc()
                session_tokens.labels(
                    session_id=self.session_id
                ).set(self._total_tokens())
                logger.warning(f"ALERT: session {self.session_id} tokens={self._total_tokens()}")
                break

配合 Grafana 设置 2 档告警:

  • Warning:token > 70% 上限 → 自动触发语义摘要
  • Critical:token > 90% 上限 → 禁止新消息入队,强制用户等待

现在你可以做什么

4 个坑对应 4 步走:

  1. 立即接入 tiktoken 统计 token 数,不知道用量就没法管
  2. 上线关键信息池 + 滑动窗口,这是最小可行方案
  3. 接 Prometheus 告警,70% 和 90% 两档,这是投入产出比最高的改进
  4. 如果接了知识库,上 HyDE,实测能提 36% 召回率

如果你的机器人已经上线超过 1 个月,去查一下 token 最高的那个会话是多少。数字会吓你一跳。

© 版权声明
THE END
喜欢就支持一下吧
点赞10分享