|
很多人第一次接入大模型 API 时,都会产生一个错觉: 模型好像“记得”刚才我们说过的话。 等真正做成一个多轮聊天产品,这个错觉很快就会被现实击碎。 大多数大模型的对话接口,本质上都是无状态 API。服务端不会保存你的历史对话,也不会理解“刚才”和“前面”的含义。每一次调用,模型只认你这一次传过去的内容。 多轮对话能成立,完全依赖应用端如何构造提示词上下文。 这篇文章从最基础的上下文拼接讲起,一步步过渡到真实产品中必须面对的工程问题,最后给出一套在生产环境中可落地的完整方案。 如果你是工程师,会知道该怎么写代码。 如果你是产品经理,会明白哪些能力必须在产品层面提前设计。 1大模型为什么“不记得你说过的话”在对话类 API 中,模型并没有会话状态这个概念。 模型接收到的是一个 messages 数组,其中每一项都只是文本和角色标识。 模型返回的结果,也只是基于这次输入的一次性生成。 多轮对话之所以成立,是因为我们在下一轮请求中,把上一轮的问答再次发给了模型。 从模型视角看,并不存在“第几轮对话”,只有一段越来越长的文本。 2最基础的多轮对话实现方式在最简单的场景下,多轮对话的实现逻辑非常直接。 每一轮请求时: 示例代码如下,用 Python 展示基本结构。 from openaiimportOpenAI
client = OpenAI( api_key="<DeepSeekAPIKey>", base_url="https://api.deepseek.com")
messages = [ {"role":"system","content":"You are a helpful assistant."}]
# 第一轮messages.append({"role":"user","content":"What's the highest mountain in the world?"})resp = client.chat.completions.create( model="deepseek-chat", messages=messages)messages.append({"role":"assistant","content": resp.choices[0].message["content"]})
# 第二轮messages.append({"role":"user","content":"What is the second?"})resp = client.chat.completions.create( model="deepseek-chat", messages=messages)messages.append({"role":"assistant","content": resp.choices[0].message["content"]})
在第二轮请求时,模型真正看到的内容是: [{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"What's the highest mountain in the world?"},{"role":"assistant","content":"The highest mountain in the world is Mount Everest."},{"role":"user","content":"What is the second?"}]
正是因为第一轮的问答被完整保留,模型才能理解 What is the second 指的是什么。 这个方式在 Demo 阶段完全够用,但在真实产品中问题会迅速暴露。 3真实产品中,简单拼接为什么会失效一旦进入真实使用场景,多轮对话会很快遇到几个不可回避的问题。 1. 上下文窗口有限模型对一次请求可接收的 token 数是有限的。 对话一旦变长,早期内容就会被截断,模型会突然表现得“失忆”。 2. 无关上下文干扰模型判断用户在一个会话中,很可能会频繁切换话题。 当无关历史被全部拼进提示词,模型的注意力会被严重稀释,回答质量明显下降。 3. 成本与延迟快速上升每多传一段历史,调用成本和响应时间都会同步增长。 在高并发场景下,这会直接影响产品可用性。 4. 用户跳题再回头,模型最容易出错一个非常典型的场景是: 用户聊 A 话题 中途突然问了一个完全无关的问题 再回到 A 话题继续追问
如果只是顺序拼接历史,模型很容易给出前后不一致的回答。 4dify 中的真实多轮对话是如何构造的我们以 Dify 的真实请求为例,看一下多轮对话在工程层面是怎么实现的。 第一轮请求参数中没有 conversation_id,表示新会话。 {"query":"你是一个人吗?","conversation_id":""}
构造出的提示词只有 system prompt 和用户问题。 System:你是一个可以进行多轮对话的客服小姐姐。User:你是一个人吗? 第二轮请求请求中带上 conversation_id 和 parent_message_id。 {"query":"那你是男的还是女的?","conversation_id":"8f8f9ee2-2a7d-4118-8aad-bc5711cabe32"}此时系统会从数据库中取回历史消息,重新构造完整提示词。 System:你是一个可以进行多轮对话的客服小姐姐。User:你是一个人吗?Assistant:哈哈,我不是真人哦……User:那你是男的还是女的? 第三轮请求逻辑完全一致,只是历史更长。 可以看到,多轮对话的核心并不在模型,而在应用端如何重建上下文。 5多轮对话中最棘手的问题是什么在真实系统中,最难处理的情况并不在于对话变长,而在于话题跳转。 当用户突然问一个和当前上下文无关的问题,再回到原话题继续问时,模型极容易出现以下问题: 这并不是模型能力不足,而是上下文构造方式不合理。 6大模型在这种场景下是否真的会掉质量答案是明确的,会。 原因主要集中在三点: 无关上下文占据了有限的注意力空间 关键历史被截断或被噪声淹没 模型无法判断哪些历史仍然重要
当上下文长度逼近上限时,这种问题会成倍放大。 7应用端应该如何系统性解决这个问题真正可行的方案,一定发生在应用层,而不是寄希望于模型“自己理解”。 核心思路应用端需要完成三件事: 判断当前问题和历史是否相关 只把真正相关的内容交给模型 用更短的形式保留长期记忆
8一套可落地的完整实现方案下面是一套在真实产品中已经被大量验证的实现方式。 第一步,所有对话入库并生成向量每一条 user 和 assistant 消息: 存文本 存时间 存 embedding 向量 关联 conversation_id
第二步,检测话题相关性新问题到来时: 与最近几轮消息计算向量相似度 相似度过低,判定为新话题
第三步,构造上下文候选集优先级顺序建议为: system prompt 当前话题摘要 与新问题最相关的历史片段 最近几轮完整对话 当前用户问题
第四步,摘要而不是丢弃当历史过长时: 对早期对话做结构化摘要 用摘要替代原始长文本 摘要本身也参与向量检索
第五步,控制 token 总量在真正调用模型前: 预估 token 数 超限时优先压缩摘要或移除低相关片段
9产品层面一定要提前设计的能力这件事不只是技术问题。 产品层面至少需要考虑: 会话是否支持话题分段 是否允许用户显式开启新话题 是否支持对话历史回溯 是否提供长对话的记忆说明
这些设计,会直接决定多轮对话在真实用户面前是否可信。 写在最后 多轮对话看起来像模型能力,实质上是应用工程能力。 真正稳定的智能体系统,一定在以下几个地方下过功夫: 当你把这些能力补齐后,大模型才会看起来“真的在和用户对话”。 |