🎯 课程主题
讲解多轮对话中历史消息的正确维护方式、常见错误做法,以及如何通过"保留最近 N 轮对话"来优化对话历史的策略与代码实现。
📝 核心知识点
1. 为什么需要维护对话历史
- 概念说明:大模型本身是无状态的,不记忆之前的对话内容。要让模型"记住"上下文,必须每次调用时将历史消息列表一并传入。
- 关键细节:每轮对话需按"上一轮的 AI 回复 + 本轮用户问题"成对追加到消息列表中。
2. 常见的错误做法
- 概念说明:三种典型的错误演示:
- 完全不做任何历史维护,每次只传当前问题。
- 每次都新建一个消息列表,与之前的对话无关联。
- 使用
append添加新问题,但忘记将上一轮 AI 的回复(AIMessage)也加进去。
- 关键细节:正确做法是每轮都需添加 (上一轮的 AIMessage + 本轮 HumanMessage) 这对消息。
3. 对话历史的优化策略
- 概念说明:随着对话轮次增多,历史消息列表越来越长,带来三个问题:
- Token 消耗增加 → 成本上升
- 信息越多模型处理越慢
- 过早的无关信息可能干扰模型对当前问题的响应
- 关键细节:
- 保留 SystemMessage(不能丢弃,否则模型失去角色定位)
- 仅保留最近 N 轮对话(每轮 = 1 条 HumanMessage + 1 条 AIMessage,即 N*2 条消息)
- 更早的对话直接丢弃
🏗️ 架构与工作流
消息列表 = [SystemMessage, ...历史对话消息..., 最新HumanMessage]
↓
分离 SystemMessage 与 对话消息
↓
对话消息取切片 [-N*2 :]
↓
返回 = SystemMessage + 最近N轮对话消息
↓
供后续 model.invoke() 使用
💻 代码实战
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
model = ChatOpenAI(
model="gpt-4o",
base_url="https://api.claude.ai/v1",
api_key="your-api-key"
)
# 初始消息列表
conversation = [
SystemMessage(content="你是一个友好的AI助手。")
]
# 第一轮对话
conversation.append(HumanMessage(content="我叫张三"))
response1 = model.invoke(conversation)
conversation.append(AIMessage(content=response1.content))
# 第二轮对话
conversation.append(HumanMessage(content="你叫什么名字?"))
response2 = model.invoke(conversation)
conversation.append(AIMessage(content=response2.content))
# 第三轮对话
conversation.append(HumanMessage(content="我刚才说我叫什么?"))
response3 = model.invoke(conversation)
# 此时 conversation 有 7 条消息(1条system + 6条对话)
print(f"原始消息数: {len(conversation)}")
# ========== 对话历史优化函数 ==========
def optimize_memory(messages, max_pairs=3):
"""
保留最近的 N 轮对话
Args:
messages: 完整的消息列表
max_pairs: 保留的对话轮数(每轮 = user消息 + assistant消息)
Returns:
优化后的消息列表(system消息 + 最近N轮对话)
"""
system_messages = [m for m in messages if m.__class__.__name__ == "SystemMessage"]
# 或使用: [m for m in messages if getattr(m, "type", None) == "system"]
conversation_messages = [m for m in messages if m.__class__.__name__ != "SystemMessage"]
recent_messages = conversation_messages[-(max_pairs * 2):]
return system_messages + recent_messages
# 测试:仅保留最近2轮对话
optimized = optimize_memory(conversation, max_pairs=2)
print(f"优化后消息数: {len(optimized)}") # 1(system) + 4(2*2) = 5
# 追问第一个问题
optimized.append(HumanMessage(content="我第一个问题问的是什么?"))
response4 = model.invoke(optimized)
print(response4.content)
# 输出将基于当前列表中仅存的消息来回答,而非最早的历史
⚠️ 常见问题与避坑指南
- 不要遗漏 SystemMessage:优化后如果去掉了 system 消息,模型会失去角色定位,回复质量会大幅下降。
- 不要只加 user 消息不加 AI 回复:如果上一轮的 AIMessage 未被加入列表,模型就"忘记"自己刚刚说过什么,对话连续性断裂。
- 每轮消息必须成对处理:取切片时的
max_pairs * 2是核心里程碑,注意不能只取 user 方或只取 AI 方。 - 使用
content(字符串)构造 AIMessage 即可:不需要完整复制 response 对象,用字典{"role": "assistant", "content": response.content}或AIMessage(content=response.content)构造均可。
💡 个人总结与延伸
对话历史管理是多轮对话系统的核心机制。本节给出的优化函数是一个简洁实用的模板,实际生产中可进一步扩展为:支持滚动窗口 + 摘要(保留开头 N 轮的摘要 + 最近 M 轮完整对话)的方式。LangChain 后续章节中的 Memory 模块正是对此类需求的封装抽象,本节课的手动实现有助于理解其底层原理。