ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI客服上下文管理实战:滑动窗口、摘要压缩与语义检索模式

AI客服上下文管理实战:滑动窗口、摘要压缩与语义检索模式 做客服场景的AI问答助手时我踩过一个特别打脸的坑系统刚上线那两天所有人都夸它聪明问什么答什么等用了一周开始有人反馈这个机器人是不是变笨了聊到第八轮之后经常重复问已经给过的信息甚至把用户前面确认过的收货地址给忘了。我一开始以为是模型抽风反复调 prompt、换模型都没解决。后来把一个会话的完整请求体打出来一看才意识到问题出在我对上下文context的处理方式上——我把所有历史消息一股脑全塞进请求里既不筛选也不压缩。这个教训让我把上下文怎么管理当成一个正经的架构问题来做也就有了今天这篇关于 context-mode 的文章。这篇文章不是什么理论推演而是我在真实项目里把整个上下文处理链路重做一遍后的记录。核心就三件事为什么要给上下文分模式、每种模式适合什么场景、以及模式之间怎么切换才能既省钱又不丢信息。适合正在做 AI 客服、知识库问答、智能体应用的开发者和技术负责人参考。1. 把历史全塞给模型为什么会翻车成本、窗口与注意力三重夹击1.1 先算一笔 token 账讲到上下文大家第一个想到的词肯定是 token。模型按 token 计费上下文越长单次请求成本越高。但很多人对高没有体感我算一笔实际账给你看。假设一个客服会话平均聊 20 轮每轮用户加助手大约 300 token那么完整历史就是 6000 token。你部署的模型输入价格如果是 0.003 美元 / 1K token单次请求光是输入部分就要 0.018 美元。听起来不贵但客服系统是高频场景一个坐席机器人一天处理 300 个会话、每个会话平均请求 15 次那就是 4500 次请求光输入成本就 81 美元。如果业务量再翻十倍每月光 token 费用就能吃掉团队小半的预算。更坑的是这 6000 token 里真正对当前回答有决定作用的可能只有最近 3 到 5 轮以及用户在第一轮描述的场景背景。那些中间态的确认信息、寒暄、重复提问全是实打实的钱。1.2 窗口再大也不是无限大有人说现在模型上下文窗口都 128K、200K 了我全塞进去不行吗行但有个前提——你得接受延迟和成本的线性上涨。而且窗口大不代表质量高实际用下来超过一定长度之后模型对中段信息的回忆准确率会明显下滑。我做过一个极端测试把一个 15 万字的技术文档全文放进上下文然后问模型第三章第二节提到的参数默认值是多少。模型给出的答案有三次是错的其中一次是把第四章的参数搬过来了。这不是模型能力问题是长上下文场景下注意力天然会被稀释。后面我会详细讲这个现象。1.3 注意力稀释为什么越长越健忘Transformer 的注意力机制本质上是在一堆 token 里做谁最重要的加权求和。当上下文里只有 2000 个 token 时关键信息很容易获得高权重当上下文塞到 20000 个 token关键信息周围全是噪声它的相对权重就被摊薄了。这就好比你在嘈杂的食堂里找人聊天人少的时候对面说什么你都听得清人一多哪怕对方提高音量你还是容易漏掉关键句。模型也一样——不是它忘了是它的注意力被无关内容干扰了。所以我的第一个结论是上下文管理的本质不是能塞多少而是该塞多少。该塞的必须塞进来不该塞的坚决不塞。这就引出 context-mode 的核心——把上下文拆成几种明确的处理模式按需使用。2. context-mode 的三条主流路线滑动窗口、摘要压缩、语义检索2.1 滑动窗口模式只保留最近 N 轮滑动窗口是最好理解也最容易实现的方案维护一个固定长度的队列新消息进来最早的消息被挤出去。比如窗口设为 10 轮那每次请求只带最近 10 轮对话。它的优点是成本可控、实现简单、延迟稳定缺点是早期信息会被无情丢弃。如果用户在第 3 轮告诉你我的订单号是 ABC123到第 12 轮他问帮我查下 ABC123 现在到哪了模型已经看不到那个订单号了。所以滑动窗口适合那些当前意图只依赖最近几轮的场景比如闲聊机器人、需要快速响应的工具调用。不适合业务流程长、早期关键信息容易被反复引用的场景。2.2 摘要压缩模式把旧历史蒸馏成一句话摘要压缩模式是我项目中后期加入的。思路是当历史超过一定阈值就调用一次模型把前面的对话总结成一段结构化的摘要然后后续请求只带摘要 最近几轮完整对话。比如用户聊了 30 轮前 25 轮被压缩成用户咨询退货流程订单号 ABC123已确认收货地址为上海市浦东新区XX路XX号用户同意选择上门取件退款将原路返回。这一段摘要可能只有 80 token但保留了所有关键业务要素。后续模型回答时摘要 最近 5 轮完整对话总 token 数反而比全量 30 轮还要少而且关键信息一个不丢。摘要压缩适合中长会话、信息密度高、时间跨度大的场景。代价是需要额外一次摘要模型调用有轻微延迟和成本另外摘要质量直接影响后续回答摘要写得烂后面全崩。2.3 语义检索模式不靠窗口靠相关性语义检索模式是给知识密集型对话准备的。它不依赖对话顺序而是把历史消息按内容切块做向量化存入内存索引每次请求前用当前用户问题做相似度检索只把最相关的几块历史拼进上下文。它的优点非常明显突破了时间和顺序的限制。用户在第 2 轮问过一个问题第 20 轮又回到这个话题即使中间隔了 18 轮无关内容检索模式依然能精准捞回那段历史。缺点是需要引入向量数据库或者内存向量索引架构复杂度上升另外切块大小、检索条数、相似度阈值这三个参数都得调调不好反而引入不相关内容造成上下文污染。我用的是一个轻量方案先用滑动窗口和摘要兜底再叠加检索增强。切块按每轮对话为单位而不是按固定字数切这样能保证每块语义完整检索命中率明显更高。2.4 三种模式怎么选一张表说清楚模式适用场景优点缺点实现成本滑动窗口短会话、闲聊、工具调用实现简单、成本低、延迟稳定早期信息丢失低摘要压缩中长会话、业务背景复杂保留关键要素、token 省额外摘要调用、摘要质量依赖模型中语义检索知识问答、长周期反复引用历史突破顺序限制、精准找回架构复杂、参数调优成本高高这里给个实操建议不要一开始就追求语义检索。我见过很多团队上来就搞向量库结果检索质量没调好反而把不相关的历史块拼进 prompt导致模型回答被带偏。正确顺序是先滑动窗口跑通业务再叠加摘要压缩解决中期信息丢失确实验证检索的价值后再把语义检索加进来。3. 模式切换的落地实现状态机、触发条件与代码骨架3.1 为什么需要动态切换有人会问我选一种模式固定用不就行了吗为什么要切换原因是会话长度和信息需求是动态变化的。一个会话刚开始时历史很短全量携带毫无压力此时用全量模式效果最好聊了 10 轮以上全量开始变贵变慢就该切到滑动窗口再过几轮关键旧信息快被挤掉了就该切到摘要压缩如果用户反复追问某个具体细节比如订单号、型号参数就要临时启用检索。所以 context-mode 不只是一个模式选择更是一个模式状态机。每个会话独立维护自己的状态按规则在模式之间迁移。3.2 状态定义我为每个会话定义了四个状态FULL历史总 token 低于阈值时使用全量携带。WINDOW历史超过阈值进入滑动窗口。SUMMARIZED窗口内早期信息被摘要替代。RETRIEVAL检测到用户对特定历史信息的强引用时从索引中检索补充。3.3 核心代码骨架下面这段代码是我项目里的简化版本去掉了业务细节保留核心逻辑from dataclasses import dataclass, field from typing import List, Optional dataclass class SessionState: session_id: str mode: str FULL turns: List[dict] field(default_factorylist) summary: Optional[str] None token_count: int 0 full_window_turns: int 10 summary_after_turns: int 25 class ContextModeManager: def __init__(self, config: dict): self.full_window_turns config.get(full_window_turns, 10) self.summary_after_turns config.get(summary_after_turns, 25) self.token_threshold config.get(token_threshold, 8000) self.summarizer config.get(summarizer, None) def add_turn(self, state: SessionState, user_msg: str, ai_msg: str): state.turns.append({user: user_msg, assistant: ai_msg}) state.token_count self._estimate_tokens(state.turns) def decide_mode(self, state: SessionState, current_query: str) - str: # 超过 token 阈值先降级到滑动窗口 if state.token_count self.token_threshold: state.mode WINDOW # 轮数超过摘要阈值且已经生成过摘要则维持在摘要模式 if len(state.turns) self.summary_after_turns: if state.summary is None and self.summarizer: state.summary self._make_summary(state.turns) state.mode SUMMARIZED # 如果当前 query 明确引用了历史里的实体订单号/地址/型号切到检索模式 if self._has_entity_reference(current_query): state.mode RETRIEVAL return state.mode def build_prompt(self, state: SessionState, current_query: str) - str: mode self.decide_mode(state, current_query) if mode FULL: return self._concat_turns(state.turns) current_query if mode WINDOW: recent state.turns[-self.full_window_turns:] return self._concat_turns(recent) current_query if mode SUMMARIZED: recent state.turns[-self.full_window_turns:] return f[摘要]\n{state.summary}\n\n[最近对话]\n \ self._concat_turns(recent) current_query if mode RETRIEVAL: recent state.turns[-self.full_window_turns:] retrieved self._semantic_search(state.turns, current_query, top_k3) return f[检索到的相关历史]\n{retrieved}\n\n[最近对话]\n \ self._concat_turns(recent) current_query return self._concat_turns(state.turns) current_query这里有几个我实际调试中得到的经验直接说_estimate_tokens不要用字符数估算要用tiktoken或模型对应的 tokenizer 做准确计数否则阈值会失真。我一开始偷懒用len(text) / 2估算结果实际 token 数高了一倍导致窗口过早切换老出问题。摘要生成前要判断历史里是否真的有值得保留的信息。如果前 25 轮全是在吗谢谢好的这类寒暄摘要就一句话用户未提出具体需求完全没必要调用摘要模型浪费钱。检索模式不要覆盖摘要模式的上下文。我现在的做法是检索出来的内容放在最前面但摘要和最近窗口依然保留形成检索 摘要 最近 N 轮三层结构。只靠检索结果来撑上下文遇到检索质量波动时回答稳定性会大幅下降。3.4 切换的触发条件状态机的触发条件设计是这套系统最微妙的部分。我踩过几个坑总结成三条硬规则切换要带 hysteresis迟滞比如从 FULL 切到 WINDOW 的阈值是 8000 token那么从 WINDOW 切回 FULL 要等 token 降到 6000 以下。否则对话在阈值附近波动时系统会来回切换行为不稳定。这就是我后面要讲的模式抖动问题这里先在设计上埋个预防机制。不要在用户消息处理中途切模式模式切换只发生在下一次请求构造时而不是当前请求处理中。原因很简单如果正在生成回答时切换了上下文会导致回答和上下文不一致生成到一半的信息可能是基于旧上下文推理的。检索模式的触发要同时满足两个条件当前 query 包含实体引用订单号、日期、型号等 该实体确实在旧历史里出现过。我见过不少系统只判断前者结果用户随口说那个订单检索模块就把一堆不相干的历史块捞进来。4. 实测数据与参数调优窗口多大、压到多狠、检索取几条4.1 三组对照实验我在内部数据集上做了三组对照实验数据集是 200 个真实客服会话每个会话 15 到 40 轮。评测指标有三个回答准确率、单次请求平均输入 token、端到端 P95 延迟。第一组是全量基线全部历史直接拼接。第二组是滑动窗口 摘要压缩窗口 10 轮超过 25 轮后启用摘要。第三组是滑动窗口 摘要 语义检索在前者基础上叠加 top-3 检索。结果如下方案准确率平均输入 tokenP95 延迟全量基线82.5%68001.2s滑动窗口 摘要87.0%24000.8s滑动窗口 摘要 检索91.5%31000.9s注意一个反直觉的结果滑动窗口 摘要的准确率比全量基线还高了 4.5 个百分点。原因就是前面说的注意力稀释——去掉大量无关历史后模型反而更容易聚焦关键信息。这也验证了我最初的判断上下文管理的价值不只是省钱更是提升质量。第三组准确率最高但输入 token 比第二组多了 700主要来自检索出的 top-3 历史块。延迟增加不多因为检索走的是内存向量索引不是远程调用。4.2 关键参数的起始建议值基于这组实验和后续多次调参我给出一组可直接抄作业的起始参数滑动窗口轮数10 到 15 轮。窗口太小小于 5会让模型失去短期记忆太大超过 20会让 token 成本回升注意力又开始稀释。摘要触发轮数窗口轮数的 2 到 2.5 倍。窗口 10 轮那么 20 到 25 轮时启动摘要比较合适。太早启动会把还在活跃期的信息也压缩掉太晚启动则前期已经产生大量无效成本。token 阈值8000 到 12000。这个值主要取决于模型性能和你的预算。如果用的模型是 128K 窗口的高端模型阈值可以放宽如果用的是小模型建议压到 6000 以下。检索 top_k3 到 5 条。每条按一轮对话切块平均 250 token。top_k 取 5 以上时检索内容开始夹杂噪声准确率不升反降。相似度阈值0.35 到 0.45取决于 embedding 模型。低于 0.35 的结果基本是噪声捞进来只会污染上下文。4.3 成本对比省下来的都是利润还是按前面的收费模型算全量基线每请求平均 6800 token滑动窗口 摘要方案只有 2400 token。同样 4500 次请求前者一天输入成本约 91 美元后者约 32 美元一天省 59 美元。按一个月 22 个工作日算能省 1300 美元。这还是在没有考虑摘要模型额外调用成本的前提下——实际摘要调用每会话 1 到 2 次每次 300 到 500 token成本占比很小。我的体会是context-mode 最大的价值不是技术上的优雅而是直接把成本砍到了原来的三分之一同时准确率还涨了。这种又省钱又变好的优化在工程里不多见值得认真做。5. 实战中的坑与解法上下文污染、摘要失真、模式抖动5.1 上下文污染检索捞回了不该捞的内容上下文污染是我在叠加语义检索之后遇到的最头疼的问题。典型案例用户问上次说的那个优惠券还能用吗检索模块按照相似度捞回了 3 块历史其中一块来自另一个用户的问题因为我的切块索引曾不小心混入了测试数据。模型看到优惠券相关内容就顺着答了结果答的是别人那个订单的优惠券规则。这个问题的根源不在检索算法而在数据隔离。我当时的索引是全局的没有按会话隔离导致跨会话污染。解法很简单索引必须按 session_id 分桶检索时只查当前会话的桶。另外切块时不要把系统提示词、内置指令切进检索索引这是另一个容易踩的坑——有次我把系统 prompt 的一句话当成历史消息切进去了模型误以为用户说过那句话。5.2 摘要失真压缩丢了关键细节摘要压缩模式的坑在摘要丢了关键细节。有次用户在第 20 轮问我改一下发票抬头行不行当时的摘要把发票抬头改为北京XX科技有限公司压缩成了用户咨询发票修改事项。后续模型就只知道用户要改发票但不知道改成什么只能再问一遍。用户很不满说我不是刚说过吗。这个问题的解法有两个层面。第一层摘要 prompt 里强制要求保留五个要素实体、数字、地址、时间、动作。我现在的摘要模板是请总结以下对话必须保留订单号/编号、金额、地址、时间节点、 用户明确表达过的意图。格式要求分条列出每条不超过50字。第二层对高价值实体订单号、金额、身份证号等做正则匹配如果命中就直接从原始文本里抽出来打进摘要后面的关键实体字段不依赖模型抽取。这两层叠加之后摘要失真率从 12% 降到了 3% 以下。5.3 模式抖动状态在边界来回横跳模式抖动是我在三套模式全上线后才发现的。症状是会话 A 在 WINDOW 和 RETRIEVAL 之间反复切换导致用户看到的前后回答风格不一致而且系统日志里请求构造每次都不一样问题排查极其困难。原因是检索模式的触发条件当前 query 包含实体引用太敏感。用户连续几轮都在问那个地址那个订单每轮都触发检索而每轮检索结果又因为相似度计算微小的数值波动而不同导致上下文内容不稳定。解法就是我前面提到的 hysteresis 机制。我在状态机里加了冷却时间从 WINDOW 切到 RETRIEVAL 后至少 5 轮之内不允许切回从 RETRIEVAL 切回 WINDOW 时要求连续 3 轮 query 都不含实体引用。这样把抖动压在了可控范围。5.4 一套可复用的排查方法如果你也在做 context-mode遇到问题后建议按这个顺序排查而不是凭感觉乱调先查请求体把实际发给模型的 prompt 打出来保存成一个 log。八成的问题在 prompt 里一眼就能看出来比如检索内容混进了别的会话。再查模式切换记录给每个会话的状态机打点日志记录每次切换的时间和触发条件。模式抖动类问题靠这个查最快。然后查摘要内容摘要是否保留了关键实体。拿出原始对话和摘要做 diff看丢了什么。我写了个小脚本自动比对摘要里的实体集合和原始文本里的实体集合覆盖率低于 80% 就告警。最后才调参数前三个都没问题才建议动窗口大小、阈值这些参数。参数调整一次只动一个别同时改三个否则你根本不知道是哪个变量导致了变化。最后再分享一个我现在的实践习惯context-mode 的每个模式切换行为都要可视化。我在内部管理后台画了一个会话时间轴上面标着模式切换点、token 数变化、摘要生成时间。运营同事反馈机器人变笨了的时候我第一件事就是看这个时间轴十有八九能在五分钟内定位到问题。这个习惯帮我省了大量排查时间也让我更敢放心地给 context-mode 叠加更多智能逻辑。
返回列表