Claude API 多轮对话怎么实现?上下文管理详解
Claude API 实现多轮对话的关键只有一句话:Messages API 是无状态的,服务端不保存任何历史,每次请求你都要把完整的对话记录通过 messages 数组重新发回去。所谓"上下文管理",本质就是在客户端把每一轮的用户输入和模型回复按顺序追加进一个数组,并控制它的长度与成本。下面从最小可运行示例讲到生产级的 Token 控制方案。
多轮对话的本质:自己维护 messages 数组
调用 POST /v1/messages 时,请求体里的 messages 就是整段对话历史。每个元素是一个对象,包含 role(user 或 assistant)和 content。模型返回回复后,你需要把模型的回复也作为一条 assistant 消息追加进数组,下一轮再带上用户的新问题,如此循环。
有两条硬性规则必须遵守,否则会收到 400 报错:
- 第一条消息的
role必须是user,不能以assistant开头; user和assistant必须严格交替出现,不能连续两条相同 role。
系统提示词(system prompt)不放在 messages 里,而是用顶层的 system 参数单独传,它在整段对话中保持稳定。关于它的写法可以参考 Claude 系统提示词怎么设置?完整配置步骤图解。如果你还没跑通第一次请求,建议先看 Claude API 怎么调用?从注册到第一次请求完整教程。
Python 完整示例:一个能记住上文的命令行对话
用官方 anthropic SDK,先安装:pip install anthropic。鉴权用环境变量 ANTHROPIC_API_KEY(SDK 会自动读取,底层对应 x-api-key 与 anthropic-version 两个请求头)。
import anthropic
client = anthropic.Anthropic() # 自动读取 ANTHROPIC_API_KEY
SYSTEM = "你是一个简洁、专业的中文技术助手。"
messages = [] # 这就是不断增长的对话历史
while True:
user_input = input("你: ")
if user_input.strip() in ("exit", "quit"):
break
# 1. 追加用户这一轮的输入
messages.append({"role": "user", "content": user_input})
# 2. 带上完整历史发起请求
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=2048,
system=SYSTEM,
messages=messages,
)
# 3. 取出文本回复
reply = resp.content[0].text
print("Claude:", reply)
# 4. 关键:把模型回复也追加回历史,下一轮才有记忆
messages.append({"role": "assistant", "content": reply})
第 4 步是新手最容易漏掉的:如果不把 assistant 回复追加回去,模型每一轮看到的都只有当前问题,对话就"失忆"了。Claude 当前主力模型有 Claude Opus 4.8、Claude Sonnet 4.6、Claude Haiku 4.5 三个档位,闲聊类多轮对话用 Haiku 更省钱,选型可看 Claude 模型怎么选?Opus / Sonnet / Haiku 选型指南。
Node.js 示例:追加 assistant 回复的正确姿势
Node 用 @anthropic-ai/sdk,安装:npm install @anthropic-ai/sdk。注意把 resp.content 里的整个内容块数组追加回去,而不仅仅是文本字符串——这样能正确保留工具调用等结构化块。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const system = "你是一个简洁、专业的中文技术助手。";
const messages = [];
async function ask(userInput) {
messages.push({ role: "user", content: userInput });
const resp = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 2048,
system,
messages,
});
// 把完整 content 数组作为 assistant 消息追加回去
messages.push({ role: "assistant", content: resp.content });
const text = resp.content.find((b) => b.type === "text")?.text ?? "";
console.log("Claude:", text);
return text;
}
await ask("帮我用一句话解释什么是闭包");
await ask("再举一个 JavaScript 的例子"); // 这一轮能记住上文
第二轮问"再举一个例子"时模型知道"例子"指的是闭包,正是因为前一轮的 user/assistant 都在数组里。完整的 Node 用法和流式输出见 Claude API Node.js 调用示例:从安装到流式输出。
Token 会随轮数线性增长,必须主动控制
因为每次都要回传全部历史,对话越长,输入 Token 越多,费用和延迟都会上升。三种实用的控制策略:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 滑动窗口 | 只保留最近 N 轮,丢弃更早的历史 | 客服、闲聊,旧上下文不重要 |
| 摘要压缩 | 把早期对话用一次 API 调用总结成一段话,替换原始消息 | 需要保留要点的长对话 |
| 固定前缀 + Prompt 缓存 | system 和早期稳定内容打缓存断点,命中后只按约 0.1 倍计价 | system 长、历史复用率高 |
滑动窗口最简单:每轮发送前裁剪数组(注意裁剪后第一条仍必须是 user,否则会 400):
MAX_TURNS = 10 # 保留最近 10 轮(20 条消息)
if len(messages) > MAX_TURNS * 2:
messages = messages[-MAX_TURNS * 2:]
# 确保裁剪后第一条是 user
while messages and messages[0]["role"] != "user":
messages.pop(0)
想知道当前历史占多少 Token,不要用 OpenAI 的 tiktoken(会算不准),用官方的 client.messages.count_tokens(...),方法见 Claude API Token 怎么计算?附在线估算方法。更多降本技巧见 Claude API 怎么省钱?5 个降低 Token 成本的方法。
用 Prompt 缓存降低多轮对话成本
多轮对话天然适合 Prompt 缓存:稳定的 system 和较早的历史构成可复用前缀。在最后一个稳定内容块上加 cache_control 断点,后续请求命中缓存后,这部分按约 0.1 倍输入价计费。
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=2048,
system=[{
"type": "text",
"text": SYSTEM,
"cache_control": {"type": "ephemeral"}, # 缓存 system
}],
messages=messages,
)
# 检查命中:resp.usage.cache_read_input_tokens 应大于 0
缓存是前缀匹配:前缀里任何一个字节变化都会让后面全部失效。所以不要把当前时间、随机 ID 拼进 system,缓存命中率才稳。注意最小可缓存前缀有门槛(Opus 4.8 约 4096 Token),太短不会缓存。
多轮对话里的流式输出与工具调用
对话场景下回复较长时建议用流式输出(SSE),避免请求超时、提升交互体验。SDK 用 client.messages.stream(...),结束后用 get_final_message() 拿到完整消息再追加回历史。Python 的 SSE 细节见 Claude API 流式输出 Python 实现教程(SSE 详解)。
如果对话中用到了 Tool Use(工具调用),多轮的拼装会多一步:模型返回 tool_use 块后,你要把模型回复(含 tool_use)整体追加为 assistant 消息,再把工具执行结果作为一条 user 消息(内含 tool_result 块,且 tool_use_id 要对应)追加回去,role 交替规则同样适用。完整流程见 Claude Tool Use 工具调用怎么用?完整代码实战。
超长对话:上下文窗口与压缩
Claude Opus 4.8 提供 1M Token 的上下文窗口,单纯靠窗口大也能撑很多轮,但成本会上去。当对话可能逼近窗口上限时,可以采用"摘要压缩"——在历史过长时调用一次模型把前面内容浓缩成一段摘要,替换掉原始消息,再继续对话。关于窗口和长文档处理技巧见 Claude 上下文窗口多大?长文档处理实用技巧。具体配额与价格以 Anthropic 官网为准。
常见问题
为什么模型每次都"忘记"上一轮说的话?
几乎都是因为没有把模型的 assistant 回复追加回 messages 数组。Messages API 无状态,服务端不记历史,你只发了当前问题,模型自然只能看到当前问题。确保每轮"追加用户消息 → 请求 → 追加模型回复"三步齐全。
报错 "messages: roles must alternate" 是怎么回事?
这是 role 没有严格交替导致的 400。常见原因:连续追加了两条 user(比如重试时重复追加)、裁剪历史后第一条变成了 assistant、或把 system 内容误放进了 messages。检查数组确保 user/assistant 一一交替且首条为 user。
对话太长导致 Token 超限或变贵,怎么办?
用滑动窗口只保留最近若干轮,或对早期内容做摘要压缩;同时给稳定的 system 加 Prompt 缓存断点降低重复计费。发送前用 count_tokens 估算长度,逼近上限前主动裁剪或压缩,避免触发 max_tokens 或上下文窗口超限。