
客服机器人上线 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 步走:
- 立即接入
tiktoken统计 token 数,不知道用量就没法管 - 上线关键信息池 + 滑动窗口,这是最小可行方案
- 接 Prometheus 告警,70% 和 90% 两档,这是投入产出比最高的改进
- 如果接了知识库,上 HyDE,实测能提 36% 召回率
如果你的机器人已经上线超过 1 个月,去查一下 token 最高的那个会话是多少。数字会吓你一跳。
© 版权声明
本站部分内容为网络收集,若侵犯到您的权益,请提供相关证明联系,即删。
更多交流点击入群
更多交流点击入群
THE END






