1. 代码功能简要说明

该代码基于LangChain框架整合OpenAI的gpt-4-turbo模型,实现带会话历史的多语言聊天机器人

  • 核心能力1:通过ChatMessageHistoryRunnableWithMessageHistory管理会话历史,按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 会话历史工作流程(可视化)

session_id不存在

session_id已存在

用户调用invoke/stream

传入config中的session_id

调用get_session_history函数

创建新的ChatMessageHistory对象,存入store

从store读取对应的ChatMessageHistory

拼接:历史记录 + 新用户消息

传入提示模板(填充MessagesPlaceholder)

调用gpt-4-turbo模型

返回模型响应

将响应添加到该session_id的ChatMessageHistory

返回最终响应给用户

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/数据库。

总结(关键点回顾)

  1. 核心能力:LangChain通过RunnableWithMessageHistory+ChatMessageHistory实现会话历史管理,session_id是区分不同用户的核心;
  2. 关键组件MessagesPlaceholder是提示模板中关联历史的核心,需与input_messages_key保持一致;
  3. 调用方式
    • invoke:一次性获取完整响应,适合短文本;
    • stream:逐token返回,适合实时对话/长文本;
  4. 实用技巧
    • 内存存储的会话历史仅适合测试,生产环境需用Redis/数据库持久化;
    • 会话历史会增加提示词长度,需注意模型的上下文窗口限制(gpt-4-turbo为128k token);
    • 可通过ChatMessageHistory.clear()清空指定session_id的历史记录。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐