【大模型】8.2基于LangChain实现带会话历史的多语言聊天机器人
·
1. 代码功能简要说明
该代码基于LangChain框架整合OpenAI的gpt-4-turbo模型,实现带会话历史的多语言聊天机器人:
- 核心能力1:通过
ChatMessageHistory和RunnableWithMessageHistory管理会话历史,按session_id区分不同用户,模型能记住上下文(如记住用户名字并回答相关问题); - 核心能力2:支持参数化多语言响应(通过
language变量指定回答语言); - 核心能力3:支持两种调用方式——一次性调用(invoke)返回完整响应、流式调用(stream)逐token返回响应(模拟实时对话效果);
- 基础设计:通过字典
store存储所有用户的会话历史,实现简单的会话持久化(内存级,重启后丢失)。
2. 带逐行详细注释的完整代码
# 导入os库:1.配置网络代理(解决国内访问OpenAI API的网络限制) 2.配置LangChain环境变量
import os
# 导入ChatMessageHistory:LangChain的会话历史管理类,用于存储单个用户的聊天记录(消息列表)
from langchain_community.chat_message_histories import ChatMessageHistory
# 导入HumanMessage:LangChain的用户消息类型,标识用户发送的内容
from langchain_core.messages import HumanMessage
# 导入提示模板相关:ChatPromptTemplate(创建提示模板)、MessagesPlaceholder(会话历史占位符)
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 导入RunnableWithMessageHistory:将普通链包装为“带会话历史的链”,核心实现上下文关联
from langchain_core.runnables import RunnableWithMessageHistory
# 导入LangChain封装的OpenAI聊天模型:适配LangChain生态的gpt-4-turbo调用入口
from langchain_openai import ChatOpenAI
# 导入LangServe路由函数:(本代码未实际部署,仅导入,预留扩展为Web服务的能力)
from langserve import add_routes
# 配置HTTP代理:127.0.0.1:7890是代理工具的本地端口,确保API请求访问OpenAI服务器
os.environ['http_proxy'] = '127.0.0.1:7890'
# 配置HTTPS代理:OpenAI/LangChain API基于HTTPS,需配置该代理确保请求正常
os.environ['https_proxy'] = '127.0.0.1:7890'
# 开启LangChain Tracing V2:追踪链的执行过程(包括会话历史、模型调用、响应时间),便于调试
os.environ["LANGCHAIN_TRACING_V2"] = "true"
# 配置LangChain项目名称:追踪数据归类到该项目,便于管理不同应用
os.environ["LANGCHAIN_PROJECT"] = "LangchainDemo"
# 配置LangChain API Key:认证LangChain Smith服务(追踪功能必需),替换为自己的密钥
os.environ["LANGCHAIN_API_KEY"] = ''
# ===================== 核心步骤1:初始化OpenAI聊天模型 =====================
# 聊天机器人案例
# 创建ChatOpenAI模型实例:指定gpt-4-turbo模型,适配LangChain调用规范
model = ChatOpenAI(model='gpt-4-turbo')
# ===================== 核心步骤2:定义带会话历史占位符的提示模板 =====================
# 定义提示模板:包含系统指令+会话历史占位符(关键:支持上下文关联)
prompt_template = ChatPromptTemplate.from_messages([
# system角色:系统指令,指定助手行为+多语言参数({language}是动态变量)
('system', '你是一个乐于助人的助手。用{language}尽你所能回答所有问题。'),
# 会话历史占位符:变量名my_msg,执行时会自动填充该session_id的历史聊天记录
# 作用:让模型能看到之前的对话,实现上下文关联(如记住用户名字)
MessagesPlaceholder(variable_name='my_msg')
])
# ===================== 核心步骤3:构建基础链(提示模板→模型) =====================
# 得到链:管道符|串联提示模板和模型,执行逻辑:填充模板变量→调用模型
chain = prompt_template | model
# ===================== 核心步骤4:初始化会话历史存储 =====================
# 保存聊天的历史记录:字典store用于存储所有用户的会话历史
# key:session_id(会话唯一标识,区分不同用户);value:ChatMessageHistory对象(存储该用户的历史消息)
store = {}
# ===================== 核心步骤5:定义会话历史获取函数 =====================
# 此函数预期将接收一个session_id并返回一个消息历史记录对象(LangChain要求的固定格式)
def get_session_history(session_id: str):
# 如果session_id不在store中(新用户),创建新的ChatMessageHistory对象并存入store
if session_id not in store:
store[session_id] = ChatMessageHistory()
# 返回该session_id对应的会话历史对象(无论新老用户)
return store[session_id]
# ===================== 核心步骤6:包装链为“带会话历史的链” =====================
do_message = RunnableWithMessageHistory(
chain, # 基础链(提示模板+模型)
get_session_history, # 会话历史获取函数(告诉链如何获取/创建会话历史)
input_messages_key='my_msg' # 输入消息的key:与提示模板中的MessagesPlaceholder变量名一致,指定新消息存入哪个位置
)
# ===================== 核心步骤7:测试带会话历史的聊天(invoke方式,完整响应) =====================
# 配置会话ID:config字典指定当前会话的唯一标识(zs1234),区分不同用户的聊天记录
config = {'configurable': {'session_id': 'zs1234'}}
# 第一轮对话:用户打招呼并告知名字
resp1 = do_message.invoke(
# 输入参数:my_msg是新发送的用户消息,language指定回答语言为中文
{
'my_msg': [HumanMessage(content='你好啊! 我是LaoXiao')],
'language': '中文'
},
config=config # 传入会话配置,关联到session_id=zs1234的历史
)
# 优化print:添加轮次说明,清晰展示第一轮对话的输入和输出
print('=== 第一轮对话(session_id=zs1234) ===')
print(f'用户输入:你好啊! 我是LaoXiao')
print(f'助手回复:{resp1.content}')
# 第二轮对话:用户询问自己的名字(测试上下文关联)
resp2 = do_message.invoke(
{
'my_msg': [HumanMessage(content='请问:我的名字是什么?')],
'language': '中文'
},
config=config # 复用zs1234的会话配置,链会读取该session的历史记录
)
# 优化print:展示第二轮对话,验证模型记住了用户名字
print('\n=== 第二轮对话(session_id=zs1234) ===')
print(f'用户输入:请问:我的名字是什么?')
print(f'助手回复:{resp2.content}')
# ===================== 核心步骤8:测试流式响应(stream方式,逐token返回) =====================
# 新的会话配置:session_id=lis2323(新用户,与zs1234的历史隔离)
config = {'configurable': {'session_id': 'lis2323'}}
print('\n=== 第三轮对话(session_id=lis2323,流式响应) ===')
print(f'用户输入:请给我讲一个笑话?')
print(f'助手回复(流式):', end='')
# 流式调用:stream方法逐token返回响应,模拟实时聊天效果
for resp in do_message.stream(
# 输入参数:用户请求讲笑话,回答语言为英文
{'my_msg': [HumanMessage(content='请给我讲一个笑话?')], 'language': 'English'},
config=config # 关联到session_id=lis2323的历史
):
# 每一次resp都是一个token,end='-'表示不换行,用-分隔token(演示流式效果)
# 实际场景可改为end='',直接拼接成完整句子
print(resp.content, end='-')
# 补充换行,优化排版
print('\n')
3. 无任何注释的代码版本
import os
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langserve import add_routes
os.environ['http_proxy'] = '127.0.0.1:7890'
os.environ['https_proxy'] = '127.0.0.1:7890'
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = "LangchainDemo"
os.environ["LANGCHAIN_API_KEY"] = ''
model = ChatOpenAI(model='gpt-4-turbo')
prompt_template = ChatPromptTemplate.from_messages([
('system', '你是一个乐于助人的助手。用{language}尽你所能回答所有问题。'),
MessagesPlaceholder(variable_name='my_msg')
])
chain = prompt_template | model
store = {}
def get_session_history(session_id: str):
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
do_message = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key='my_msg'
)
config = {'configurable': {'session_id': 'zs1234'}}
resp1 = do_message.invoke(
{
'my_msg': [HumanMessage(content='你好啊! 我是LaoXiao')],
'language': '中文'
},
config=config
)
print('=== 第一轮对话(session_id=zs1234) ===')
print(f'用户输入:你好啊! 我是LaoXiao')
print(f'助手回复:{resp1.content}')
resp2 = do_message.invoke(
{
'my_msg': [HumanMessage(content='请问:我的名字是什么?')],
'language': '中文'
},
config=config
)
print('\n=== 第二轮对话(session_id=zs1234) ===')
print(f'用户输入:请问:我的名字是什么?')
print(f'助手回复:{resp2.content}')
config = {'configurable': {'session_id': 'lis2323'}}
print('\n=== 第三轮对话(session_id=lis2323,流式响应) ===')
print(f'用户输入:请给我讲一个笑话?')
print(f'助手回复(流式):', end='')
for resp in do_message.stream({'my_msg': [HumanMessage(content='请给我讲一个笑话?')], 'language': 'English'},
config=config):
print(resp.content, end='-')
print('\n')
4. 核心知识点详解(系统梳理+表格)
4.1 核心概念梳理(小白友好版)
| 概念 | 通俗解释 | 关键说明 |
|---|---|---|
| 会话历史(Chat History) | 保存用户与助手的过往对话记录,让模型“记住上下文” | 1. 核心痛点:原生大模型无记忆,每次调用都是独立的,需手动传递历史记录 2. LangChain解决方案:通过 ChatMessageHistory管理单用户历史,RunnableWithMessageHistory自动关联历史3. 隔离性:通过 session_id区分不同用户,避免历史记录混淆 |
| ChatMessageHistory | LangChain的会话历史存储类 | 1. 作用:封装单个用户的聊天记录(包含HumanMessage/AIMessage等),提供add_message()/clear()等方法2. 存储位置:本案例用内存字典 store存储,重启后丢失;生产环境可替换为Redis/数据库3. 核心方法:无需手动调用,由 RunnableWithMessageHistory自动管理 |
| MessagesPlaceholder | 提示模板中的会话历史占位符 | 1. 作用:在提示模板中预留位置,执行时自动填充该session_id的历史聊天记录2. 变量名:需与 input_messages_key和输入参数的key一致(本案例均为my_msg)3. 必要性:没有占位符,模型无法看到历史记录,只能单次对话 |
| RunnableWithMessageHistory | 带会话历史的链包装器 | 1. 核心功能:将普通链(prompt+model)包装为“自动管理历史”的链,无需手动拼接历史记录 2. 必传参数: - chain:基础链 - get_session_history:获取/创建会话历史的函数 - input_messages_key:新消息的key(与占位符变量名一致) 3. 工作流程:调用时自动读取 session_id的历史→拼接新消息→调用模型→保存新消息到历史 |
| session_id | 会话唯一标识 | 1. 作用:区分不同用户/不同会话的历史记录(如zs1234和lis2323是两个独立会话) 2. 配置方式:通过 config={'configurable': {'session_id': 'xxx'}}传入3. 生产场景:可使用用户ID、UUID等作为session_id,确保唯一性 |
| invoke vs stream | 链的两种调用方式 | 1. invoke:一次性调用,返回完整响应(适合短文本、快速获取结果) 2. stream:流式调用,逐token返回响应(适合长文本、实时对话,提升用户体验) 3. 输出差异:invoke返回完整的AIMessage对象;stream返回生成器,每次yield一个token的AIMessage |
| 会话存储(store字典) | 内存级的会话历史存储 | 1. 本案例实现:key=session_id,value=ChatMessageHistory对象 2. 局限性:内存存储,重启程序后历史记录丢失 3. 生产替代方案: - 短期存储:Redis(高性能,适合会话级数据) - 长期存储:MySQL/MongoDB(持久化,适合需要留存的聊天记录) |
4.2 核心组件用法对照表
| 组件 | 导入路径 | 核心用法 | 作用 | 本案例应用 |
|---|---|---|---|---|
| ChatMessageHistory | langchain_community.chat_message_histories.ChatMessageHistory | ChatMessageHistory()(创建空历史)history.add_message(HumanMessage/AIMessage)(添加消息) |
存储单个用户的聊天历史 | 作为store字典的值,存储每个session_id的历史 |
| MessagesPlaceholder | langchain_core.prompts.MessagesPlaceholder | MessagesPlaceholder(variable_name='my_msg')(创建占位符) |
在提示模板中预留历史记录位置 | 让模型调用时自动拼接历史消息 |
| RunnableWithMessageHistory | langchain_core.runnables.RunnableWithMessageHistory | RunnableWithMessageHistory(chain, get_session_history, input_messages_key)(包装链) |
让普通链具备会话历史能力 | 包装prompt+model链,实现上下文关联聊天 |
| invoke(链调用) | 链对象的方法 | chain.invoke(输入参数, config=配置) |
一次性获取完整响应 | 前两轮对话,测试上下文关联(记住用户名) |
| stream(链调用) | 链对象的方法 | for resp in chain.stream(输入参数, config=配置) |
逐token获取流式响应 | 第三轮对话,模拟实时讲笑话的效果 |
| session_id配置 | 字典参数 | config={'configurable': {'session_id': 'xxx'}} |
指定当前会话的唯一标识 | 区分zs1234和lis2323的会话历史 |
4.3 会话历史工作流程(可视化)
5. Print函数优化与说明
5.1 优化对比与核心改进
| 优化前代码 | 优化后代码 | 核心改进 |
|---|---|---|
print(resp1.content)print(resp2.content)print(resp.content, end='-') |
print('=== 第一轮对话(session_id=zs1234) ===')print(f'用户输入:xxx')print(f'助手回复:xxx')流式输出添加“助手回复(流式):”前缀 |
1. 增加轮次/会话ID说明,小白能清晰区分不同会话的对话 2. 明确标注“用户输入/助手回复”,提升可读性 3. 流式输出添加前缀,说明输出类型,end='-'演示token级返回效果 |
5.2 输出示例(参考)
=== 第一轮对话(session_id=zs1234) ===
用户输入:你好啊! 我是LaoXiao
助手回复:你好呀LaoXiao!很高兴认识你,有什么我能帮忙的吗?
=== 第二轮对话(session_id=zs1234) ===
用户输入:请问:我的名字是什么?
助手回复:你的名字是LaoXiao呀😊
=== 第三轮对话(session_id=lis2323,流式响应) ===
用户输入:请给我讲一个笑话?
助手回复(流式):Why-did-the-scarecrow-win-an-award?-Because-he-was-outstanding-in-his-field!-
- 流式输出说明:
end='-'让每个token之间用-分隔,直观展示“逐token返回”的特点;实际应用中可改为end='',直接拼接成完整句子(无分隔符)。
5.3 关键说明
resp.content:无论invoke还是stream,每个响应对象的content字段都是当前生成的文本(invoke返回完整文本,stream返回单个token文本);- 会话隔离性:zs1234和lis2323的会话历史完全隔离,lis2323的对话不会影响zs1234的历史,反之亦然;
- 历史记录持久化:本案例的
store是内存字典,重启程序后所有历史记录丢失,生产环境需替换为Redis/数据库。
总结(关键点回顾)
- 核心能力:LangChain通过
RunnableWithMessageHistory+ChatMessageHistory实现会话历史管理,session_id是区分不同用户的核心; - 关键组件:
MessagesPlaceholder是提示模板中关联历史的核心,需与input_messages_key保持一致; - 调用方式:
- invoke:一次性获取完整响应,适合短文本;
- stream:逐token返回,适合实时对话/长文本;
- 实用技巧:
- 内存存储的会话历史仅适合测试,生产环境需用Redis/数据库持久化;
- 会话历史会增加提示词长度,需注意模型的上下文窗口限制(gpt-4-turbo为128k token);
- 可通过
ChatMessageHistory.clear()清空指定session_id的历史记录。
更多推荐


所有评论(0)