在上一篇文章中,我们发出了第一次 API 请求并得到了模型的回复。但你很快会发现一个问题:当你问第二个问题时,模型似乎完全”失忆”了——它不记得上一轮你们聊过什么。这不是 bug,而是大模型 API 的默认行为:每轮请求之间互不关联。想让模型”记住”之前的对话,就必须自己管理历史记录,而这背后依赖一个核心概念:消息角色

三种消息角色

大模型 API(以 OpenAI Chat Completions 为例)的消息数组由一条条消息对象构成,每条消息有两个关键字段:role(角色)和 content(内容)。API 定义了三种角色:

角色 含义 用途
system 系统指令 设定助手的行为、语气、专业领域和约束条件
user 用户消息 代表终端用户的问题或指令
assistant 助手回复 模型上一次给出的回答

一个完整的请求示例:

1
2
3
4
messages = [
{"role": "system", "content": "你是一个 Python 编程助手,用中文回答。"},
{"role": "user", "content": "如何读取 CSV 文件?"},
]

system:给模型定规矩

system 消息是对话的”宪法”。它不会被终端用户看到,但会影响模型的所有行为。可以在 system 消息中指定:

  • 角色定位:”你是一个资深后端工程师”
  • 回答风格:”用简洁的要点回答,不要长篇大论”
  • 输出格式:”始终用 JSON 格式返回”
  • 安全边界:”不回答任何涉及政治或违法的内容”

注意:部分开源模型(如 Llama 3、Qwen 等)对 system 消息的遵循程度不如 GPT-4,在实际使用中需要测试验证。

user 与 assistant:对话的”乒乓球”

userassistant 交替出现,模拟真实的对话。模型只会根据消息数组中所有消息来生成回复——它没有自己的”记忆”。

1
2
3
4
5
messages = [
{"role": "user", "content": "1 + 1 等于几?"},
{"role": "assistant", "content": "等于 2。"},
{"role": "user", "content": "再加 3 呢?"},
]

在这个例子中,模型看到完整历史,才知道”再加 3”是指 2 + 3 = 5,而不是凭空猜一个数。

实现多轮对话

核心思路很简单:把所有对话记录存在一个列表里,每次请求都带上

最小可运行示例

以下是一个命令行聊天程序的完整实现,保存为 chat.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import os
from openai import OpenAI

client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY"), # 从环境变量读取
base_url=os.environ.get("OPENAI_BASE_URL"),
)

# 初始化消息列表,system 在最前面
messages = [
{
"role": "system",
"content": "你是一个友好的助手,用中文回答,每次回复不超过三句话。",
}
]

print("多轮对话程序已启动,输入 'quit' 退出。\n")

while True:
user_input = input("你: ")
if user_input.strip().lower() == "quit":
print("再见!")
break

# 1. 把用户消息追加到历史
messages.append({"role": "user", "content": user_input})

# 2. 带上完整历史发送请求
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)

assistant_reply = response.choices[0].message.content

# 3. 把助手回复也追加到历史
messages.append({"role": "assistant", "content": assistant_reply})

print(f"助手: {assistant_reply}\n")

代码解析

步骤 操作 说明
初始化 创建 messages 列表,放入 system 消息 system 始终排在最前面
收到用户输入 messages.append({"role": "user", ...}) 把用户问题追加到列表末尾
发送请求 传入完整的 messages 模型根据全部历史理解上下文
收到回复 messages.append({"role": "assistant", ...}) 把模型回答也追加进去,供下一轮使用

运行效果演示:

1
2
3
4
5
6
7
多轮对话程序已启动,输入 'quit' 退出。

你: 我叫小明
助手: 你好,小明!有什么我可以帮助你的吗?

你: 我叫什么名字?
助手: 你叫小明!还需要我帮你做些什么吗?

第二轮的回复证明模型已经通过历史消息知道用户的名字——这就是多轮对话的核心价值。

关于 system 消息的位置

spec 建议将 system 消息放在消息数组的最前面,然后交替排列 user 和 assistant。部分模型对此有严格要求,错误顺序可能导致行为异常。以下是一个推荐的消息排列:

1
2
3
4
5
6
7
8
9
10
11
12
13
# ✅ 正确
[
{"role": "system", "content": "..."},
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."},
{"role": "user", "content": "..."},
]

# ❌ 错误:assistant 在 user 前面
[
{"role": "system", "content": "..."},
{"role": "assistant", "content": "..."}, # 没有前置 user
]

另外,不要在对话中间插入新的 system 消息——虽然某些模型允许用 system 消息来注入新的指令(如”从现在开始改用英文回答”),但这不是规范用法,兼容性没有保障。如果需要改变行为,可以改用 user 角色携带指令。

历史记录的三大陷阱

1. 上下文窗口溢出

每次请求都带着完整历史,历史越长,消息越多。当总 token 数超过模型的上下文窗口时,API 会返回错误。应对方法(后面文章会详细展开):

  • 统计 token 数,在发送前检查
  • 丢弃最早的消息(只保留 system + 最近 N 轮)
  • 对历史做摘要压缩

2. 成本线性增长

因为每轮都要重新发送所有历史消息,所以对话越长,单次请求成本越高。第 10 轮请求的费用远高于第 1 轮。实际项目中需要做历史裁剪。

3. 思路会被历史”带偏”

如果早期对话出现了错误信息,后续所有轮次都会受其影响。可以在 system 消息中给出纠错指令,或在发现错误后手动编辑历史记录中的错误条消息。

多轮对话 vs. 单轮对话:选择指南

场景 推荐方式 原因
客服机器人、辅导老师 多轮 需要上下文理解用户意图
批量文本分类、翻译 单轮 每条独立,无需上下文
代码审查、文档问答 多轮 需要追问和细化
内容生成(标题、摘要) 单轮 一次完成任务,历史无意义

单轮模式下每次请求只用一条 user 消息,不需要管理历史,成本和延迟最低。

小结

  • 消息角色有三种:system(设定行为)、user(用户输入)和 assistant(模型回复)
  • 多轮对话的本质是把历史消息累积在列表中,每次请求都完整发送
  • system 消息应放在数组最前面,user 和 assistant 交替排列
  • 随对话增长需要注意上下文窗口、成本和历史污染三个问题
  • 不是所有场景都需要多轮对话——单轮在批量任务中更高效

掌握了消息角色和历史管理,你就有了构建真正”对话式” AI 应用的基础。下一篇文章我们将深入 token 与上下文窗口,学会估算和控制对话的长度。