很多人第一次接入大模型 API 时,都会产生一个错觉:
模型好像“记得”刚才我们说过的话。
等真正做成一个多轮聊天产品,这个错觉很快就会被现实击碎。
大多数大模型的对话接口,本质上都是无状态 API。服务端不会保存你的历史对话,也不会理解“刚才”和“前面”的含义。每一次调用,模型只认你这一次传过去的内容。
多轮对话能成立,完全依赖应用端如何构造提示词上下文。
这篇文章从最基础的上下文拼接讲起,一步步过渡到真实产品中必须面对的工程问题,最后给出一套在生产环境中可落地的完整方案。
如果你是工程师,会知道该怎么写代码。
如果你是产品经理,会明白哪些能力必须在产品层面提前设计。
1
大模型为什么“不记得你说过的话”
在对话类 API 中,模型并没有会话状态这个概念。
模型接收到的是一个 messages 数组,其中每一项都只是文本和角色标识。
模型返回的结果,也只是基于这次输入的一次性生成。
多轮对话之所以成立,是因为我们在下一轮请求中,把上一轮的问答再次发给了模型。
从模型视角看,并不存在“第几轮对话”,只有一段越来越长的文本。
2
最基础的多轮对话实现方式
在最简单的场景下,多轮对话的实现逻辑非常直接。
每一轮请求时:
-
把用户的提问追加到 messages
-
把模型的回答也追加到 messages
-
下一轮请求时,把整个 messages 再传给模型
示例代码如下,用 Python 展示基本结构。
from openai import OpenAIclient = 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 话题继续追问
如果只是顺序拼接历史,模型很容易给出前后不一致的回答。
4
dify 中的真实多轮对话是如何构造的
我们以 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
产品层面一定要提前设计的能力
这件事不只是技术问题。
产品层面至少需要考虑:
-
会话是否支持话题分段
-
是否允许用户显式开启新话题
-
是否支持对话历史回溯
-
是否提供长对话的记忆说明
这些设计,会直接决定多轮对话在真实用户面前是否可信。
写在最后
多轮对话看起来像模型能力,实质上是应用工程能力。
真正稳定的智能体系统,一定在以下几个地方下过功夫:
-
上下文选择
-
话题判断
-
记忆压缩
-
成本控制
当你把这些能力补齐后,大模型才会看起来“真的在和用户对话”。


