总结之LangChain 链式编排与多轮对话

LangChain 链式编排与多轮对话学习笔记

一、本章概览

本模块包含两个核心案例,演示 LangChain 的链式编排能力:

文件内容
multi_turn_chat.py多轮对话:基于 RunnableWithMessageHistory + MessagesPlaceholder 实现带历史记忆的对话
parallel_chain_test.py并行执行链:基于 RunnableParallel 同时执行多个独立任务链

并行执行链

输入 topic

RunnableParallel

joke_chain

poem_chain

{joke: ..., poem: ...}

多轮对话

用户输入

RunnableWithMessageHistory

注入历史消息

prompt → llm → parser

AI 回复

保存到 Session


二、多轮对话(RunnableWithMessageHistory)

2.1 为什么需要多轮对话

默认的 LLM 调用是无状态的——每次 llm.invoke() 都不知道之前聊了什么。要实现多轮对话,需要:

  1. 保存历史:每轮对话结束后,保存用户和 AI 的消息
  2. 注入历史:下一轮对话时,把历史消息注入到提示词中
  3. 管理会话:不同用户/会话的历史互相隔离

RunnableWithMessageHistory 封装了以上三个能力。

2.2 完整开发流程(六步)

构建提示词模板

创建模型

构建链

包装历史管理

对话循环

运行

第一步:构建提示词模板(关键:MessagesPlaceholder)
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个技术专家,擅长解决各种Web开发中的技术问题"),
    MessagesPlaceholder(variable_name="chat_history"),  # ← 历史消息注入点
    ("human", "{question}"),                           # ← 用户输入
])

MessagesPlaceholder 是什么?

它是 ChatPromptTemplate 中的动态占位符,运行时会被替换为历史消息列表。

最终消息列表:
┌──────────────────────────────────────────────┐
│ SystemMessage: 你是一个技术专家...             │  ← 固定
│ HumanMessage:  你好,我叫 Sam                  │  ← 历史(自动注入)
│ AIMessage:     你好 Sam!                     │  ← 历史(自动注入)
│ HumanMessage:  你还记得我的名字吗?            │  ← 当前输入
└──────────────────────────────────────────────┘

注意: variable_name="chat_history" 必须与后面 RunnableWithMessageHistoryhistory_messages_key 保持一致。

第二步:创建大模型
from app.common import llm
# llm.llm 是预初始化的 ChatOpenAI 实例
第三步:构建链式调用

有两种写法——管道语法和显式函数调用:

from langchain_core.output_parsers import StrOutputParser

output_parser = StrOutputParser()

# 方式1:管道语法(简洁)
chain = prompt | llm.llm | StrOutputParser()

# 方式2:显式函数调用(清晰,便于调试)
def chain(inputs):
    """显式链式调用:prompt → llm → parser"""
    messages = prompt.invoke(inputs)          # 格式化提示词,注入历史消息
    ai_message = llm.llm.invoke(messages)     # 调用大模型
    result = output_parser.invoke(ai_message) # 从 AIMessage 提取纯文本
    return result

管道语法 vs 显式调用:

方式代码优点缺点
管道语法prompt | llm | parser简洁、声明式不方便调试、不直观
显式调用函数内 invoke清晰、可加断点代码稍多

管道语法 A | B | C 本质上是 LangChain 重载了 __or__ 运算符,等价于依次调用 A.invoke() → B.invoke() → C.invoke()

第四步:构建 RunnableWithMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory

# 4.1 创建 session 存储(内存存储,key 为 session_id)
store = {}

# 4.2 获取 session 的回调函数
def get_session_history(session_id: str):
    """根据 session_id 获取对话历史,不存在时自动创建"""
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 4.3 创建带历史管理的 Runnable
chain_with_history = RunnableWithMessageHistory(
    runnable=chain,                    # 基础链(第三步创建的)
    get_session_history=get_session_history,  # 获取历史的回调
    input_messages_key="question",     # 用户输入对应的变量名 → {question}
    history_messages_key="chat_history",  # 历史消息对应的变量名 → MessagesPlaceholder
)

RunnableWithMessageHistory 构造参数详解:

参数作用对应关系
runnable基础链第三步创建的 chain
get_session_history获取历史消息的回调返回 ChatMessageHistory
input_messages_key用户输入的变量名对应 prompt 中的 {question}
history_messages_key历史消息的变量名对应 MessagesPlaceholder(variable_name="chat_history")

运行时内部流程:

chainChatMessageHistoryRunnableWithMessageHistory用户chainChatMessageHistoryRunnableWithMessageHistory用户invoke({"question": "你好"}, session_id=001)get_session_history("001")返回历史消息列表 []注入历史到 chat_historyinvoke({"question": "你好", "chat_history": []})"你好!有什么可以帮你的?"保存本轮消息"你好!有什么可以帮你的?"
第五步:对话循环
import uuid

def run_conversation():
    session_id = uuid.uuid4()  # 每个会话一个唯一 ID

    while True:
        user_input = input("用户:")
        if user_input.lower() == "exit":
            break

        # invoke 时通过 config 传入 session_id
        response = chain_with_history.invoke(
            {"question": user_input},
            config={"configurable": {"session_id": session_id}},
        )
        print("助手:", response)

关键: session_id 通过 config 参数传入,RunnableWithMessageHistory 会自动调用 get_session_history(session_id) 获取对应的历史。

2.3 session_id 隔离效果

session_id对话内容Agent 是否记得
session-001你好,我叫 Sam → 你还记得我吗?记得(Sam)
session-002我叫什么名字?不记得(全新会话)

完整案例
"""
LangChain 多轮对话测试模块

演示基于 RunnableWithMessageHistory 的多轮对话能力:
    第一步:构建提示词模板(注入 MessagesPlaceholder)
    第二步:创建大模型实例
    第三步:构建链式调用(prompt | llm | StrOutputParser)
    第四步:构建基于历史消息的 Runnable 实例
    第五步:构建多轮对话循环
    第六步:运行

运行方式:
    python -m app.conversation.multi_turn_chat
    或
    python ./app/conversation/multi_turn_chat.py
"""

import sys
import os
import uuid

# 将项目根目录加入 sys.path,支持直接运行
_project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if _project_root not in sys.path:
    sys.path.insert(0, _project_root)

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

from app.common import llm


# ============================================================
# 第一步:构建提示词模板
# ============================================================
# MessagesPlaceholder 用于注入对话历史,让 LLM 能感知上下文
# variable_name 必须与 RunnableWithMessageHistory 的 history_messages_key 一致

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个技术专家,擅长解决各种Web开发中的技术问题"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{question}"),
])


# ============================================================
# 第二步:创建大模型实例
# ============================================================
# 已在 app/common/llm.py 中统一初始化,直接使用 llm.llm

print(f"[INFO] 当前使用模型: {llm.LLM_MODEL_NAME}")
print(f"[INFO] 当前 Base URL: {llm.LLM_BASE_URL}")


# ============================================================
# 第三步:构建链式调用
# ============================================================
# 将提示词模板、大模型、输出解析器组合:
#   prompt  → 构建完整消息(含历史)
#   llm     → 调用大模型生成回复
#   parser  → 将 AIMessage 提取为纯字符串
# chain = prompt | llm.llm | StrOutputParser()
output_parser = StrOutputParser()

def chain(inputs):
    """显式链式调用:prompt → llm → parser"""
    messages = prompt.invoke(inputs)          # 第一步:格式化提示词,生成消息列表
    ai_message = llm.llm.invoke(messages)     # 第二步:调用大模型,生成回复
    result = output_parser.invoke(ai_message) # 第三步:解析 AIMessage,提取纯文本
    return result


# ============================================================
# 第四步:构建基于历史消息的 Runnable 实例
# ============================================================

# 4.1 创建 session 存储对象(内存存储,key 为 session_id)
store = {}

# 4.2 创建获取 session 的函数
def get_session_history(session_id: str):
    """根据 session_id 获取对话历史,不存在时自动创建"""
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    print(f"[Session] {dict(store)}")
    return store[session_id]

# 4.3 创建 RunnableWithMessageHistory 实例
#   runnable: 基础链
#   get_session_history: 获取历史消息的回调函数
#   input_messages_key: 用户输入对应的变量名(与 prompt 中的 {question} 对应)
#   history_messages_key: 历史消息对应的变量名(与 MessagesPlaceholder 的 variable_name 对应)
chain_with_history = RunnableWithMessageHistory(
    runnable=chain,
    get_session_history=get_session_history,
    input_messages_key="question",
    history_messages_key="chat_history",
)


# ============================================================
# 第五步:构建多轮对话
# ============================================================

def run_conversation():
    """启动多轮对话循环,输入 exit 退出"""
    session_id = uuid.uuid4()
    print(f"\n[Session ID] {session_id}")
    print("输入 'exit' 退出对话\n")

    while True:
        user_input = input("用户:")
        if user_input.lower() == "exit":
            print("对话结束。")
            break

        # invoke 时通过 config 传入 session_id
        response = chain_with_history.invoke(
            {"question": user_input},
            config={"configurable": {"session_id": session_id}},
        )

        print("助手:", end="")
        print(response)
        print()


# ============================================================
# 第六步:运行
# ============================================================

if __name__ == "__main__":
    print("=" * 60)
    print("LangChain 多轮对话测试")
    print("=" * 60)
    run_conversation()

三、并行执行链(RunnableParallel)

3.1 什么是并行执行链

RunnableParallel 可以同时执行多个独立的任务链,互不等待,合并结果。

适用场景:

  • 同一个输入需要多种不同处理(如同时生成笑话和诗歌)
  • 多个独立子任务可以并行执行,提升效率

3.2 代码实现

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel

# 第一步:定义独立的执行链(管道语法)
joke_chain = ChatPromptTemplate.from_template("给我讲一个关于 {topic} 的笑话") | llm.llm
poem_chain = ChatPromptTemplate.from_template("写一首关于 {topic} 的诗歌") | llm.llm

# 第二步:构建并行链
parallel_chain = RunnableParallel(joke=joke_chain, poem=poem_chain)

# 第三步:调用
result = parallel_chain.invoke({"topic": "AI"})

# result 是字典:
# {
#     "joke": AIMessage(content="为什么 AI 不会感冒?因为它有防火墙..."),
#     "poem": AIMessage(content="硅脑初醒万物新..."),
# }

print(result["joke"].content)  # 笑话内容
print(result["poem"].content)  # 诗歌内容

3.3 执行流程

{topic: AI}

RunnableParallel

joke_chain: 讲笑话

poem_chain: 写诗歌

{joke: AIMessage, poem: AIMessage}

执行时间对比:

方式耗时
串行执行(先笑话后诗歌)T1 + T2
并行执行(RunnableParallel)max(T1, T2)

3.4 RunnableParallel 参数说明

RunnableParallel(
    key1=runnable1,  # key1 是自定义的结果键名
    key2=runnable2,  # key2 是自定义的结果键名
)
  • 每个参数是一个 (名称, Runnable)
  • 名称用于结果字典的 key
  • 所有 Runnable 共享相同的输入
  • 所有 Runnable 同时触发,互不阻塞

四、核心 API 对比

4.1 串行 vs 并行 vs 多轮

API用途输入 → 输出
prompt | llm | parser串行链一个问题 → 一个回答
RunnableParallel(a=chain1, b=chain2)并行链一个输入 → 多个输出
RunnableWithMessageHistory(chain, ...)多轮对话一个问题 + 历史 → 一个回答(自动管理历史)

4.2 MessagesPlaceholder vs 硬编码历史

方式代码优缺点
MessagesPlaceholderChatPromptTemplate.from_messages([..., MessagesPlaceholder("chat_history")])框架自动管理,推荐
硬编码历史ChatPromptTemplate.from_messages([..., ("history", [HumanMessage(...), AIMessage(...)])])手动拼接,繁琐易错

五、开发踩坑记录

5.1 input_messages_key 不匹配

问题: 调用时报错 KeyError: 'question'

原因: input_messages_key 必须与 prompt 中的变量名一致。

# prompt 中用的是 {question}
prompt = ChatPromptTemplate.from_messages([..., ("human", "{question}")])

# 这里必须是 "question"
chain_with_history = RunnableWithMessageHistory(
    runnable=chain,
    input_messages_key="question",  # ← 必须匹配
    history_messages_key="chat_history",
)

# 调用时也要用 "question"
chain_with_history.invoke({"question": "你好"}, config=...)

5.2 history_messages_keyMessagesPlaceholder 不匹配

问题: 历史消息没有注入,每次对话都像新的一样。

原因: history_messages_key 必须与 MessagesPlaceholder(variable_name=...) 一致。

# prompt 中:
MessagesPlaceholder(variable_name="chat_history")

# RunnableWithMessageHistory 中:
history_messages_key="chat_history"  # ← 必须匹配

5.3 pipe 语法的理解

prompt | llm | parser 不是 Python 的位运算符,而是 LangChain 重载了 __or__ 方法:

# 管道语法(简洁)
chain = prompt | llm.llm | StrOutputParser()

# 等价的显式调用
def chain(inputs):
    messages = prompt.invoke(inputs)
    ai_message = llm.llm.invoke(messages)
    result = StrOutputParser().invoke(ai_message)
    return result

六、关键 API 速查

# ---- 提示词模板 ----
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个助手"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{question}"),
])

# ---- 链式调用(管道语法)----
from langchain_core.output_parsers import StrOutputParser
chain = prompt | llm | StrOutputParser()

# ---- 多轮对话 ----
from langchain_core.runnables.history import RunnableWithMessageHistory

chain_with_history = RunnableWithMessageHistory(
    runnable=chain,
    get_session_history=get_session_history,
    input_messages_key="question",
    history_messages_key="chat_history",
)
response = chain_with_history.invoke(
    {"question": "你好"},
    config={"configurable": {"session_id": "001"}},
)

# ---- 并行执行链 ----
from langchain_core.runnables import RunnableParallel

parallel = RunnableParallel(
    joke=ChatPromptTemplate.from_template("讲个笑话:{topic}") | llm,
    poem=ChatPromptTemplate.from_template("写首诗:{topic}") | llm,
)
result = parallel.invoke({"topic": "AI"})
内容概要:本文围绕可变桨叶四旋翼无人机的规范控制点对点运动模拟展开,重点研究优化推力分配策略在翻转动作中的应用性能比较。通过Matlab代码实现,构建了四旋翼动力学模型,并设计了多种控制算法以实现精确的姿态调整轨迹跟踪。研究对比了不同推力分配方案在执行高机动性翻转动作时的稳定性、能耗效率响应速度,旨在提升无人机在复杂飞行任务中的动态性能控制精度。该仿真研究为无人机飞控系统的设计优化提供了理论依据和技术支持。; 适合人群:具备一定自动控制理论基础和Matlab编程能力,从事无人机控制、飞行器动力学或机器人系统研究的科研人员及研究生。; 使用场景及目标:① 实现四旋翼无人机在三维空间中的精确点对点运动控制;② 对比分析不同推力分配策略在执行翻转等高难度动作时的控制效果能耗表现,优化飞行性能;③ 为无人机自主飞行、特技飞行及复杂环境下的机动控制提供算法验证平台。; 阅读建议:此资源以Matlab仿真为核心,建议读者结合相关控制理论知识,深入理解代码实现细节,重点关注动力学建模、控制律设计推力分配模块。在学习过程中,应动手调试参数,复现文中翻转动作的仿真结果,并尝试拓展至其他复杂飞行任务,以加深对无人机控制机理的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值