机器人答得不好,一半原因是"没记住之前说过啥"。多轮对话的核心是三件事:滑窗保留最近 N 轮原文、更早的压成摘要、会话过期自动清空。

一、滑动窗口:保留最近 6-8 轮原文

全部历史塞给模型肯定超上限。做法是只保留最近 6-8 轮的完整原文,更早的压成摘要。滑窗比固定取最后 N 条好——保留的是一段连续对话,不会打断上下文连贯性。

窗口大小要调——对话短(FAQ 类)用 6 轮足够,对话长(咨询方案类)用 8 轮甚至 10 轮。超过窗口长度才触发摘要。

二、历史摘要:聚焦用户诉求而非对话流水

摘要不是把前 30 轮对话缩成一段流水账——"用户先说 A,机器人说 B,用户又说 C"。而是聚焦:用户的核心诉求是什么、已经达成了什么、还没解决什么。

好的摘要示例:"用户想退去年双 11 买的耳机,已告知保修 1 年,用户对物流费 15 元有疑问,提到之前客服说过可以免。" 模型拿到这样的摘要,不会忘了物流费这件事。

三、会话过期:超时自动清空上下文

同一个客户隔三天再来,他可能换了新问题,但机器人还记着上周聊的订单号和耳机,答得就不对。做法是会话有 TTL(比如 30 分钟),过期自动清空滑窗和摘要,当成新会话。

TTL 到期前如果用户又发消息,重置 TTL 继续同一会话。边界场景(比如用户半夜留消息)宁可重置别混淆。

多轮对话设计对照

组件

做法

关键点

常见坑

滑动窗口

保留最近 6-8 轮原文

连续不截断

全量塞超上限

历史摘要

聚焦诉求+已达成+未解决

200 字内

流水账式摘要

会话 TTL

30 分钟过期清空

到期重置

隔三天混对话

多轮上下文实现

import time, collections

class ContextManager:
    WINDOW_SIZE = 8                     # 最近 N 轮原文
    TTL_SECONDS = 1800                  # 30 分钟
    SUMMARY_MAX = 200                   # 摘要 200 字

    def __init__(self, model):
        self.model = model
        self.histories = {}             # wxid -> deque
        self.summaries = {}             # wxid -> 摘要文本
        self.expires = {}               # wxid -> 过期时间

    def _evict_expired(self):
        # 每次调用前扫一次过期会话
        now = time.time()
        for wxid in list(self.expires.keys()):
            if self.expires[wxid] < now:
                del self.histories[wxid]
                del self.summaries[wxid]
                del self.expires[wxid]

    def get_messages(self, wxid, new_content):
        self._evict_expired()
        self.expires[wxid] = time.time() + self.TTL_SECONDS

        # 构建给模型的消息列表
        msgs = []
        if wxid in self.summaries:
            msgs.append({
                "role": "system",
                "content": f"之前对话摘要:{self.summaries[wxid]}"})
        hist = self.histories.setdefault(wxid, collections.deque())
        msgs.extend(list(hist))
        msgs.append({"role": "user", "content": new_content})
        return msgs

    def add_exchange(self, wxid, user_text, bot_text):
        hist = self.histories.setdefault(wxid, collections.deque())
        hist.append({"role": "user", "content": user_text})
        hist.append({"role": "assistant", "content": bot_text})

        # 滑窗溢出时做摘要
        if len(hist) > self.WINDOW_SIZE * 2:
            old = list(hist)[:-self.WINDOW_SIZE * 2]
            new_summary = self._summarize(old, self.summaries.get(wxid))
            self.summaries[wxid] = new_summary[:self.SUMMARY_MAX]
            # 只保留最近窗口
            kept = list(hist)[-self.WINDOW_SIZE * 2:]
            hist.clear()
            hist.extend(kept)

    def _summarize(self, old_pairs, prev_summary):
        # 摘要:聚焦诉求、已达成、未解决
        transcript = " ".join(
            f"{p['role']}: {p['content']}" for p in old_pairs)
        prompt = (
            f"把这段对话压缩成200字以内的摘要,聚焦:"
            f"1. 用户核心诉求 2. 已达成的结论 3. 未解决的问题。\n"
        )
        if prev_summary:
            prompt += f"之前的摘要:{prev_summary}\n"
        prompt += f"本次新增对话:{transcript}\n"
        return self.model.chat([
            {"role": "system",
             "content": "你是客服对话摘要助手"},
            {"role": "user", "content": prompt},
        ]).get("content", "")

落地建议

多轮对话用滑动窗口保留最近 6-8 轮完整原文,更早的压成摘要。摘要别写成流水账,聚焦"用户诉求、已达成、未解决"三要素,200 字以内。会话有 TTL(比如 30 分钟),到期自动清空滑窗和摘要,隔两三天再来别混淆上下文。微信侧的消息接口由Eyun这类个人微信API平台 提供,滑窗与摘要逻辑在自建服务实现,接口字段以平台开发文档为准。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐