一、引言
在使用大模型API(如OpenAI GPT系列、DeepSeek、Anthropic Claude等)时,我们经常需要实现多轮对话------让模型记住前几句聊天内容,而不是每次都"失忆"。很多新手会疑惑:明明只是调了一个接口,模型怎么知道我上一句问了啥?
答案是:大语言模型本身是无状态的,它并不会自动保存任何历史记录。要实现上下文关联,我们必须自己在调用API时,把完整的对话历史"喂"给它。
二、核心原理:把对话历史拼成数组发给模型
目前主流的大模型Chat接口(Chat Completions API)都接收一个叫 messages 的数组参数。这个数组里存放的就是从对话开始到现在的所有消息。
比如你第一次问"我叫小明",第二次问"我叫什么名字",如果不传历史,模型根本不知道你是谁。正确的做法是:
-
第一次请求时,
messages里只有:{"role": "user", "content": "我叫小明"}
-
模型回答后,我们把模型的回答 和这个新问题 都追加到
messages中。 -
第二次请求时,
messages变成了:-
{"role": "user", "content": "我叫小明"} -
{"role": "assistant", "content": "好的小明,记住啦!"} -
{"role": "user", "content": "我叫什么名字?"}
-
模型看到完整的列表,自然就能"回忆"起之前说过的话,从而给出正确回答:"你叫小明"。
一句话总结:上下文就是你自己维护的一个消息列表,每次请求都全量发给模型。
三、三个重要角色
在 messages 数组中,每条消息都有 role 属性,主要包含以下三种角色:
| role | 含义 | 示例 |
|---|---|---|
system |
系统提示词,用于设定AI的人设、规则,通常放在数组最前面。 | "你是一个专业的菜谱顾问,回答要简洁。" |
user |
用户说的每一句话。 | "西红柿炒鸡蛋怎么做?" |
assistant |
模型之前的回答,需要手动追加回数组。 | "先准备西红柿、鸡蛋、葱......" |
保持这些角色的正确顺序,就能模拟真实、连续的对话。
四、动手实现:Python简易多轮对话
下面是一个完整的、可运行的控制台多轮对话程序(使用 OpenAI 风格接口,DeepSeek 等也可通用)。
import openai
# 初始化客户端(请替换为你的API Key和Base URL)
client = openai.OpenAI(
api_key="your-api-key",
base_url="https://api.deepseek.com" # 如果使用DeepSeek,这样配置
)
# 初始化上下文,system prompt 作为基础设定
messages = [
{"role": "system", "content": "你是一个幽默风趣的菜谱顾问。"}
]
print("开始聊天!输入 quit 退出。\n")
while True:
user_input = input("你: ")
if user_input.lower() == "quit":
break
# 1. 把用户新问题加入上下文
messages.append({"role": "user", "content": user_input})
# 2. 带着完整的 messages 数组请求模型
response = client.chat.completions.create(
model="deepseek-chat", # 改成你用的模型名
messages=messages # 关键:每次都传入全部历史
)
# 3. 获取模型回复
assistant_reply = response.choices[0].message.content
print(f"AI: {assistant_reply}\n")
# 4. 把模型的回答也加入上下文,下一秒它会记住
messages.append({"role": "assistant", "content": assistant_reply})
运行效果:
你: 我叫小明,今天想吃鱼。
AI: 小明你好!吃鱼好啊,聪明人爱吃鱼。你想怎么吃?清蒸、红烧还是水煮?
你: 你刚才叫我什么?
AI: 我叫你小明呀,你自己说的,我可记着呢!
五、实际工程中的4个关键优化
上述方案简单直接,但对话一长,messages 会越来越庞大。每次请求都把几千字的聊天记录发过去,成本高、速度慢,还可能超过模型的最大上下文限制。所以真正用在产品里时,我们一定会做下面的优化。
1. 限制轮次(保留最近 N 轮)
只保留最近的几十条消息,旧消息直接丢弃。
MAX_HISTORY = 20 # 保留最近20条消息(10轮对话)
if len(messages) > MAX_HISTORY:
# 保留 system prompt,裁剪掉最早的用户和助手消息
messages = [messages[0]] + messages[-(MAX_HISTORY-1):]
2. 滑动窗口 + Token 计数
更精确的做法是按 token 数量裁剪。确保总 token 数不超过模型上限(如 4K、8K、128K)和你设定的阈值。
import tiktoken # OpenAI 的 token 计算库
def count_tokens(messages):
encoding = tiktoken.encoding_for_model("gpt-4")
text = "".join([m["content"] for m in messages])
return len(encoding.encode(text))
MAX_TOKENS = 3000
while count_tokens(messages) > MAX_TOKENS:
# 保留 system prompt,从第二早的消息开始删除
messages.pop(1) # 删除第一条非system消息
3. 对话摘要压缩
当历史太长时,用模型把前面的对话总结成一段摘要,然后只保留摘要 + 最近的原始消息。
# 伪逻辑:
if len(messages) > 20:
# 将前15轮对话拿出来,让模型生成摘要
old_messages = messages[:15]
summary = summarize(old_messages) # 调用模型总结
# 新上下文 = system + 摘要 + 最近5轮原始对话
messages = [system_msg, {"role": "system", "content": f"对话历史摘要:{summary}"}] + messages[-10:]
4. 关键信息外部持久化
不要依赖上下文记住用户的永久信息(如姓名、会员等级、地址)。第一次获取后就存入数据库,每次请求时动态注入到 system prompt 中。
user_name = get_name_from_db(user_id) # 从数据库取
messages = [
{"role": "system", "content": f"当前用户叫{user_name},是金牌会员。"},
{"role": "user", "content": "我的订单到哪了?"}
]
这样即使对话历史被清空,关键信息也不会丢失。
六、注意事项
-
模型的最大上下文长度:每个模型都有最大窗口(如 4k、128k),在裁剪历史时一定要留出模型输出的空间。
-
system prompt 要保护:裁剪时永远保留第一条系统设定,否则人设会崩。
-
成本意识:每次全量发送历史会让 token 消耗翻倍增长,上生产一定要做裁剪或摘要。
-
同一请求可传多轮:有些时候为了省事,一次请求就塞入完整的几轮历史,这也是允许的,关键是你要自己维护。
七、总结
-
直接API调用实现上下文关联,本质就是你在客户端维护一个 messages 数组,每次请求时全量(或截断后)发送。
-
模型本身不存储任何会话信息,所有的"记忆"都靠你手动传递。
-
实际应用时必须配合裁剪、摘要、持久化等策略,才能在保证对话连贯性的同时控制成本与性能。