跳到主要内容

多轮对话

多轮对话是大模型最常见的使用场景之一。本文介绍如何构建多轮上下文、消息格式规范以及生产环境中的历史管理策略。

工作原理

大模型 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(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年)..."
// 模型能理解"这首诗"指的是上一轮提到的《静夜思》

流式多轮对话

在流式场景中,需要等流式输出完成后,再将完整的助手回复追加到历史中:

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 数组会越来越长,带来两个问题:

  1. Token 消耗增长:每轮对话的输入 Token = 所有历史消息的 Token + 当前新消息的 Token
  2. 超出上下文窗口:当历史总 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,利用提示词缓存降低重复计费