多轮对话
多轮对话是大模型最常见的使用场景之一。本文介绍如何构建多轮上下文、消息格式规范以及生产环境中的历史管理策略。
工作原理
大模型 API 是无状态的——服务端不保存任何对话历史。每次请求都是独立的,模型不会"记住"之前的对话内容。
要实现多轮对话,需要在每次请求时将完整的历史消息作为输入传给模型:
第 1 轮:messages = [用户消息1]
第 2 轮:messages = [用户消息1, 助手回复1, 用户消息2]
第 3 轮:messages = [用户消息1, 助手回复1, 用户消息2, 助手回复2, 用户消息3]
每一轮的 messages 数组都包含了之前所有的对话记录,模型根据完整上下文生成回复。
这意味着随着对话轮次增加,输入的 Token 数量会持续增长,直接影响调用成本和响应速度。后文的历史管理策略会介绍如何解决这个问题。
消息格式
消息列表 messages 是一个数组,每条消息包含 role(角色)和 content(内容)两个核心字段:
[
{ "role": "system", "content": "你是一个专业的翻译助手" },
{ "role": "user", "content": "把「你好」翻译成英文" },
{ "role": "assistant", "content": "Hello" },
{ "role": "user", "content": "再翻译成日文" }
]
角色说明
| 角色 | 说明 | 数量 |
|---|---|---|
system | 系统提示,设定模型的行为和背景知识 | 0 或 1 条,放在最前面 |
user | 用户输入的消息 | 至少 1 条 |
assistant | 模型的回复 | 由模型生成,多轮时需回传 |
tool | 工具调用的执行结果 | 仅工具调用场景使用 |
内容格式
content 字段支持两种格式:
纯文本(最常用):
{ "role": "user", "content": "介绍一下李白" }
多模态内容(图片理解等场景):
{
"role": "user",
"content": [
{ "type": "text", "text": "这张图片里有什么?" },
{ "type": "image_url", "image_url": { "url": "https://example.com/photo.png" } }
]
}
快速开始
以下示例基于 Chat Completions 协议进行演示。多轮对话的核心机制(维护完整 messages 数组)适用于所有协议,包括 CloudBase SDK、OpenAI SDK 和 Anthropic SDK 兼容协议。各协议的接入方式请参考接入方式文档。
- CloudBase SDK
- OpenAI SDK
- cURL
- 小程序
使用 CloudBase SDK(Web / Node.js)实现多轮对话:
const model = ai.createModel("cloudbase");
// 维护消息历史
const messages = [
{ role: "system", content: "你是一个诗词专家" }
];
async function chat(userInput) {
// 1. 追加用户消息
messages.push({ role: "user", content: userInput });
// 2. 调用模型
const result = await model.generateText({
model: "deepseek-v4-flash",
messages
});
// 3. 追加助手回复到历史
messages.push({ role: "assistant", content: result.text });
return result.text;
}
// 多轮对话
await chat("李白最著名的诗是什么?");
// → "李白最著名的诗之一是《静夜思》..."
await chat("这首诗的创作背景是什么?");
// → "《静夜思》创作于唐玄宗开元十四年(726年)..."
// 模型能理解"这首诗"指的是上一轮提到的《静夜思》
使用 OpenAI SDK 实现多轮对话:
const OpenAI = require("openai");
const client = new OpenAI({
apiKey: "<YOUR_API_KEY>",
baseURL: "https://<ENV_ID>.api.tcloudbasegateway.com/v1/ai/cloudbase"
});
const messages = [
{ role: "system", content: "你是一个诗词专家" }
];
async function chat(userMessage) {
messages.push({ role: "user", content: userMessage });
const completion = await client.chat.completions.create({
model: "deepseek-v4-flash",
messages
});
const assistantMessage = completion.choices[0].message;
messages.push(assistantMessage);
return assistantMessage.content;
}
await chat("李白最著名的诗是什么?");
await chat("这首诗的创作背景是什么?");
使用 HTTP API 实现多轮对话,每次请求需携带完整历史:
curl -X POST 'https://<ENV_ID>.api.tcloudbasegateway.com/v1/ai/cloudbase/chat/completions' \
-H 'Authorization: Bearer <YOUR_API_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"model": "deepseek-v4-flash",
"messages": [
{"role": "system", "content": "你是一个诗词专家"},
{"role": "user", "content": "李白最著名的诗是什么?"},
{"role": "assistant", "content": "李白最著名的诗之一是《静夜思》..."},
{"role": "user", "content": "这首诗的创作背景是什么?"}
]
}'
微信小程序中实现多轮对话:
Page({
data: {
chatHistory: [], // 消息历史
inputValue: ""
},
async sendMessage() {
const { inputValue, chatHistory } = this.data;
if (!inputValue.trim()) return;
// 构建消息列表
const messages = [
{ role: "system", content: "你是一个诗词专家" },
...chatHistory,
{ role: "user", content: inputValue }
];
const model = wx.cloud.extend.AI.createModel("cloudbase");
let assistantContent = "";
const res = await model.streamText({
data: { model: "deepseek-v4-flash", messages }
});
for await (const text of res.textStream) {
assistantContent += text;
this.setData({ currentReply: assistantContent });
}
// 更新历史
this.setData({
chatHistory: [
...chatHistory,
{ role: "user", content: inputValue },
{ role: "assistant", content: assistantContent }
],
inputValue: ""
});
}
});
流式多轮对话
在流式场景中,需要等流式输出完成后,再将完整的助手回复追加到历史中:
const model = ai.createModel("cloudbase");
const messages = [
{ role: "system", content: "你是一个有帮助的助手" }
];
async function chatStream(userInput) {
messages.push({ role: "user", content: userInput });
const res = await model.streamText({
model: "deepseek-v4-flash",
messages
});
// 逐步输出文本
let fullText = "";
for await (const text of res.textStream) {
fullText += text;
process.stdout.write(text); // 实时输出
}
// 流结束后,将完整回复追加到历史
messages.push({ role: "assistant", content: fullText });
return fullText;
}
历史管理策略
随着对话轮次增加,messages 数组会越来越长,带来两个问题:
- Token 消耗增长:每轮对话的输入 Token = 所有历史消息的 Token + 当前新消息的 Token
- 超出上下文窗口:当历史总 Token 超过模型的最大上下文长度时,请求会失败
以下是常见的管理策略:
策略一:上下文截断
保留最近 N 轮对话,丢弃更早的历史。实现简单,适合大多数场景。
const MAX_ROUNDS = 10; // 保留最近 10 轮
function trimMessages(messages) {
// 始终保留 system 消息
const systemMsg = messages.find(m => m.role === "system");
const history = messages.filter(m => m.role !== "system");
// 每轮 = 1条user + 1条assistant = 2条消息
const trimmed = history.slice(-MAX_ROUNDS * 2);
return systemMsg ? [systemMsg, ...trimmed] : trimmed;
}
// 每次调用前截断
const trimmedMessages = trimMessages(messages);
const result = await model.generateText({
model: "deepseek-v4-flash",
messages: trimmedMessages
});
策略二:Token 预算控制
根据 Token 数量而非轮次来控制历史长度,更精确。
const MAX_INPUT_TOKENS = 8000; // 预留 8000 token 给输入
function trimByTokens(messages, maxTokens) {
const systemMsg = messages.find(m => m.role === "system");
const history = messages.filter(m => m.role !== "system");
let totalTokens = estimateTokens(systemMsg?.content || "");
const result = systemMsg ? [systemMsg] : [];
// 从最新的消息开始,往前累加
for (let i = history.length - 1; i >= 0; i--) {
const msgTokens = estimateTokens(history[i].content);
if (totalTokens + msgTokens > maxTokens) break;
totalTokens += msgTokens;
result.splice(systemMsg ? 1 : 0, 0, history[i]);
}
return result;
}
// 粗略估算 Token 数(中文约 1.5 token/字,英文约 0.75 token/word)
function estimateTokens(text) {
if (!text) return 0;
return Math.ceil(text.length * 1.5);
}
策略三:滚动摘要
当历史过长时,用模型对早期对话生成摘要,压缩历 史但保留核心信息。
async function summarizeHistory(messages) {
const model = ai.createModel("cloudbase");
const result = await model.generateText({
model: "deepseek-v4-flash",
messages: [
{
role: "user",
content: `请用一段话简要概括以下对话的关键信息:\n\n${
messages.map(m => `${m.role}: ${m.content}`).join("\n")
}`
}
]
});
return result.text;
}
// 当历史超过阈值时,压缩早期对话为摘要
async function manageHistory(messages, maxRounds = 10) {
const systemMsg = messages.find(m => m.role === "system");
const history = messages.filter(m => m.role !== "system");
if (history.length <= maxRounds * 2) return messages;
// 将早期对话压缩为摘要
const earlyHistory = history.slice(0, -maxRounds * 2);
const recentHistory = history.slice(-maxRounds * 2);
const summary = await summarizeHistory(earlyHistory);
return [
...(systemMsg ? [systemMsg] : []),
{ role: "system", content: `[历史对话摘要] ${summary}` },
...recentHistory
];
}
策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 上下文截断 | 实现简单,零额外开销 | 丢失早期信息 | 简单问答、客服 |
| Token 预算控制 | 精确控制成本 | 实现稍复杂 | 对成本敏感的场景 |
| 滚动摘要 | 保留核心信息 | 需额外 API 调用 | 长期对话、复杂任务 |
思考模型的多轮对话
使用深度思考模型(如 deepseek-r1)时,模型会返回 reasoning_content(思考过程)和 content(最终回答)两个字段。
关键规则:更新 messages 时只保留 content,忽略 reasoning_content。
const OpenAI = require("openai");
const client = new OpenAI({
apiKey: "<YOUR_API_KEY>",
baseURL: "https://<ENV_ID>.api.tcloudbasegateway.com/v1/ai/cloudbase"
});
const messages = [];
async function chatWithThinking(userMessage) {
messages.push({ role: "user", content: userMessage });
const completion = await client.chat.completions.create({
model: "deepseek-r1",
messages
});
const choice = completion.choices[0];
// ⚠️ reasoning_content 是思考过程,不要追加到 messages 中
console.log("思考过程:", choice.message.reasoning_content);
console.log("最终回答:", choice.message.content);
// ✅ 只将 content 追加到历史
messages.push({
role: "assistant",
content: choice.message.content
});
return choice.message.content;
}
将 reasoning_content 追加到 messages 会导致后续请求格式异常或回复质量下降。思考过程仅用于展示,不参与上下文传递。
成本优化建议
| 优化方向 | 做法 |
|---|---|
| 精简 system prompt | 用简短精确的指令替代冗长描述 |
| 控制历史长度 | 使用上述截断或摘要策略 |
| 选择合适的模型 | 简单任务用 deepseek-v4-flash,复杂推理用 hy3-preview |
| 使用缓存 | 对于固定的长 system prompt,利用提示词缓存降低重复计费 |