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

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

一、本章概览

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

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

#mermaid-svg-ML5Syl2bzx08HwoC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ML5Syl2bzx08HwoC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ML5Syl2bzx08HwoC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ML5Syl2bzx08HwoC .error-icon{fill:#552222;}#mermaid-svg-ML5Syl2bzx08HwoC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ML5Syl2bzx08HwoC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ML5Syl2bzx08HwoC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ML5Syl2bzx08HwoC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ML5Syl2bzx08HwoC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ML5Syl2bzx08HwoC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ML5Syl2bzx08HwoC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ML5Syl2bzx08HwoC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ML5Syl2bzx08HwoC .marker.cross{stroke:#333333;}#mermaid-svg-ML5Syl2bzx08HwoC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ML5Syl2bzx08HwoC p{margin:0;}#mermaid-svg-ML5Syl2bzx08HwoC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ML5Syl2bzx08HwoC .cluster-label text{fill:#333;}#mermaid-svg-ML5Syl2bzx08HwoC .cluster-label span{color:#333;}#mermaid-svg-ML5Syl2bzx08HwoC .cluster-label span p{background-color:transparent;}#mermaid-svg-ML5Syl2bzx08HwoC .label text,#mermaid-svg-ML5Syl2bzx08HwoC span{fill:#333;color:#333;}#mermaid-svg-ML5Syl2bzx08HwoC .node rect,#mermaid-svg-ML5Syl2bzx08HwoC .node circle,#mermaid-svg-ML5Syl2bzx08HwoC .node ellipse,#mermaid-svg-ML5Syl2bzx08HwoC .node polygon,#mermaid-svg-ML5Syl2bzx08HwoC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ML5Syl2bzx08HwoC .rough-node .label text,#mermaid-svg-ML5Syl2bzx08HwoC .node .label text,#mermaid-svg-ML5Syl2bzx08HwoC .image-shape .label,#mermaid-svg-ML5Syl2bzx08HwoC .icon-shape .label{text-anchor:middle;}#mermaid-svg-ML5Syl2bzx08HwoC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ML5Syl2bzx08HwoC .rough-node .label,#mermaid-svg-ML5Syl2bzx08HwoC .node .label,#mermaid-svg-ML5Syl2bzx08HwoC .image-shape .label,#mermaid-svg-ML5Syl2bzx08HwoC .icon-shape .label{text-align:center;}#mermaid-svg-ML5Syl2bzx08HwoC .node.clickable{cursor:pointer;}#mermaid-svg-ML5Syl2bzx08HwoC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ML5Syl2bzx08HwoC .arrowheadPath{fill:#333333;}#mermaid-svg-ML5Syl2bzx08HwoC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ML5Syl2bzx08HwoC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ML5Syl2bzx08HwoC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ML5Syl2bzx08HwoC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ML5Syl2bzx08HwoC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ML5Syl2bzx08HwoC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ML5Syl2bzx08HwoC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ML5Syl2bzx08HwoC .cluster text{fill:#333;}#mermaid-svg-ML5Syl2bzx08HwoC .cluster span{color:#333;}#mermaid-svg-ML5Syl2bzx08HwoC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ML5Syl2bzx08HwoC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ML5Syl2bzx08HwoC rect.text{fill:none;stroke-width:0;}#mermaid-svg-ML5Syl2bzx08HwoC .icon-shape,#mermaid-svg-ML5Syl2bzx08HwoC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ML5Syl2bzx08HwoC .icon-shape p,#mermaid-svg-ML5Syl2bzx08HwoC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ML5Syl2bzx08HwoC .icon-shape .label rect,#mermaid-svg-ML5Syl2bzx08HwoC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ML5Syl2bzx08HwoC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ML5Syl2bzx08HwoC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ML5Syl2bzx08HwoC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 并行执行链
输入 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 完整开发流程(六步)

#mermaid-svg-GDWIwl14I3N3TGIg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GDWIwl14I3N3TGIg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GDWIwl14I3N3TGIg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GDWIwl14I3N3TGIg .error-icon{fill:#552222;}#mermaid-svg-GDWIwl14I3N3TGIg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GDWIwl14I3N3TGIg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GDWIwl14I3N3TGIg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GDWIwl14I3N3TGIg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GDWIwl14I3N3TGIg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GDWIwl14I3N3TGIg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GDWIwl14I3N3TGIg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GDWIwl14I3N3TGIg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GDWIwl14I3N3TGIg .marker.cross{stroke:#333333;}#mermaid-svg-GDWIwl14I3N3TGIg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GDWIwl14I3N3TGIg p{margin:0;}#mermaid-svg-GDWIwl14I3N3TGIg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GDWIwl14I3N3TGIg .cluster-label text{fill:#333;}#mermaid-svg-GDWIwl14I3N3TGIg .cluster-label span{color:#333;}#mermaid-svg-GDWIwl14I3N3TGIg .cluster-label span p{background-color:transparent;}#mermaid-svg-GDWIwl14I3N3TGIg .label text,#mermaid-svg-GDWIwl14I3N3TGIg span{fill:#333;color:#333;}#mermaid-svg-GDWIwl14I3N3TGIg .node rect,#mermaid-svg-GDWIwl14I3N3TGIg .node circle,#mermaid-svg-GDWIwl14I3N3TGIg .node ellipse,#mermaid-svg-GDWIwl14I3N3TGIg .node polygon,#mermaid-svg-GDWIwl14I3N3TGIg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GDWIwl14I3N3TGIg .rough-node .label text,#mermaid-svg-GDWIwl14I3N3TGIg .node .label text,#mermaid-svg-GDWIwl14I3N3TGIg .image-shape .label,#mermaid-svg-GDWIwl14I3N3TGIg .icon-shape .label{text-anchor:middle;}#mermaid-svg-GDWIwl14I3N3TGIg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GDWIwl14I3N3TGIg .rough-node .label,#mermaid-svg-GDWIwl14I3N3TGIg .node .label,#mermaid-svg-GDWIwl14I3N3TGIg .image-shape .label,#mermaid-svg-GDWIwl14I3N3TGIg .icon-shape .label{text-align:center;}#mermaid-svg-GDWIwl14I3N3TGIg .node.clickable{cursor:pointer;}#mermaid-svg-GDWIwl14I3N3TGIg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GDWIwl14I3N3TGIg .arrowheadPath{fill:#333333;}#mermaid-svg-GDWIwl14I3N3TGIg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GDWIwl14I3N3TGIg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GDWIwl14I3N3TGIg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GDWIwl14I3N3TGIg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GDWIwl14I3N3TGIg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GDWIwl14I3N3TGIg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GDWIwl14I3N3TGIg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GDWIwl14I3N3TGIg .cluster text{fill:#333;}#mermaid-svg-GDWIwl14I3N3TGIg .cluster span{color:#333;}#mermaid-svg-GDWIwl14I3N3TGIg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GDWIwl14I3N3TGIg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GDWIwl14I3N3TGIg rect.text{fill:none;stroke-width:0;}#mermaid-svg-GDWIwl14I3N3TGIg .icon-shape,#mermaid-svg-GDWIwl14I3N3TGIg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GDWIwl14I3N3TGIg .icon-shape p,#mermaid-svg-GDWIwl14I3N3TGIg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GDWIwl14I3N3TGIg .icon-shape .label rect,#mermaid-svg-GDWIwl14I3N3TGIg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GDWIwl14I3N3TGIg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GDWIwl14I3N3TGIg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GDWIwl14I3N3TGIg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 构建提示词模板
创建模型
构建链
包装历史管理
对话循环
运行

第一步:构建提示词模板(关键:MessagesPlaceholder)
python 复制代码
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 保持一致。

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

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

python 复制代码
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
python 复制代码
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")

运行时内部流程:
chain ChatMessageHistory RunnableWithMessageHistory 用户 chain ChatMessageHistory RunnableWithMessageHistory 用户 #mermaid-svg-EMqJDZE1evxIgHrm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EMqJDZE1evxIgHrm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EMqJDZE1evxIgHrm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EMqJDZE1evxIgHrm .error-icon{fill:#552222;}#mermaid-svg-EMqJDZE1evxIgHrm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EMqJDZE1evxIgHrm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EMqJDZE1evxIgHrm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EMqJDZE1evxIgHrm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EMqJDZE1evxIgHrm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EMqJDZE1evxIgHrm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EMqJDZE1evxIgHrm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EMqJDZE1evxIgHrm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EMqJDZE1evxIgHrm .marker.cross{stroke:#333333;}#mermaid-svg-EMqJDZE1evxIgHrm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EMqJDZE1evxIgHrm p{margin:0;}#mermaid-svg-EMqJDZE1evxIgHrm .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EMqJDZE1evxIgHrm text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-EMqJDZE1evxIgHrm .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EMqJDZE1evxIgHrm .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-EMqJDZE1evxIgHrm .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-EMqJDZE1evxIgHrm .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-EMqJDZE1evxIgHrm #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-EMqJDZE1evxIgHrm .sequenceNumber{fill:white;}#mermaid-svg-EMqJDZE1evxIgHrm #sequencenumber{fill:#333;}#mermaid-svg-EMqJDZE1evxIgHrm #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-EMqJDZE1evxIgHrm .messageText{fill:#333;stroke:none;}#mermaid-svg-EMqJDZE1evxIgHrm .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EMqJDZE1evxIgHrm .labelText,#mermaid-svg-EMqJDZE1evxIgHrm .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-EMqJDZE1evxIgHrm .loopText,#mermaid-svg-EMqJDZE1evxIgHrm .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-EMqJDZE1evxIgHrm .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EMqJDZE1evxIgHrm .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-EMqJDZE1evxIgHrm .noteText,#mermaid-svg-EMqJDZE1evxIgHrm .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-EMqJDZE1evxIgHrm .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EMqJDZE1evxIgHrm .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EMqJDZE1evxIgHrm .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EMqJDZE1evxIgHrm .actorPopupMenu{position:absolute;}#mermaid-svg-EMqJDZE1evxIgHrm .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-EMqJDZE1evxIgHrm .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EMqJDZE1evxIgHrm .actor-man circle,#mermaid-svg-EMqJDZE1evxIgHrm line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-EMqJDZE1evxIgHrm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} invoke({"question": "你好"}, session_id=001)get_session_history("001")返回历史消息列表 \[\]注入历史到 chat_historyinvoke({"question": "你好", "chat_history": \[\]})"你好!有什么可以帮你的?"保存本轮消息"你好!有什么可以帮你的?"

第五步:对话循环
python 复制代码
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 代码实现

python 复制代码
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 执行流程

#mermaid-svg-iyx8Wye6ZVDgPi2v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iyx8Wye6ZVDgPi2v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iyx8Wye6ZVDgPi2v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iyx8Wye6ZVDgPi2v .error-icon{fill:#552222;}#mermaid-svg-iyx8Wye6ZVDgPi2v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iyx8Wye6ZVDgPi2v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iyx8Wye6ZVDgPi2v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iyx8Wye6ZVDgPi2v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iyx8Wye6ZVDgPi2v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iyx8Wye6ZVDgPi2v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iyx8Wye6ZVDgPi2v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iyx8Wye6ZVDgPi2v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iyx8Wye6ZVDgPi2v .marker.cross{stroke:#333333;}#mermaid-svg-iyx8Wye6ZVDgPi2v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iyx8Wye6ZVDgPi2v p{margin:0;}#mermaid-svg-iyx8Wye6ZVDgPi2v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iyx8Wye6ZVDgPi2v .cluster-label text{fill:#333;}#mermaid-svg-iyx8Wye6ZVDgPi2v .cluster-label span{color:#333;}#mermaid-svg-iyx8Wye6ZVDgPi2v .cluster-label span p{background-color:transparent;}#mermaid-svg-iyx8Wye6ZVDgPi2v .label text,#mermaid-svg-iyx8Wye6ZVDgPi2v span{fill:#333;color:#333;}#mermaid-svg-iyx8Wye6ZVDgPi2v .node rect,#mermaid-svg-iyx8Wye6ZVDgPi2v .node circle,#mermaid-svg-iyx8Wye6ZVDgPi2v .node ellipse,#mermaid-svg-iyx8Wye6ZVDgPi2v .node polygon,#mermaid-svg-iyx8Wye6ZVDgPi2v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iyx8Wye6ZVDgPi2v .rough-node .label text,#mermaid-svg-iyx8Wye6ZVDgPi2v .node .label text,#mermaid-svg-iyx8Wye6ZVDgPi2v .image-shape .label,#mermaid-svg-iyx8Wye6ZVDgPi2v .icon-shape .label{text-anchor:middle;}#mermaid-svg-iyx8Wye6ZVDgPi2v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iyx8Wye6ZVDgPi2v .rough-node .label,#mermaid-svg-iyx8Wye6ZVDgPi2v .node .label,#mermaid-svg-iyx8Wye6ZVDgPi2v .image-shape .label,#mermaid-svg-iyx8Wye6ZVDgPi2v .icon-shape .label{text-align:center;}#mermaid-svg-iyx8Wye6ZVDgPi2v .node.clickable{cursor:pointer;}#mermaid-svg-iyx8Wye6ZVDgPi2v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iyx8Wye6ZVDgPi2v .arrowheadPath{fill:#333333;}#mermaid-svg-iyx8Wye6ZVDgPi2v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iyx8Wye6ZVDgPi2v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iyx8Wye6ZVDgPi2v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iyx8Wye6ZVDgPi2v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iyx8Wye6ZVDgPi2v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iyx8Wye6ZVDgPi2v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iyx8Wye6ZVDgPi2v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iyx8Wye6ZVDgPi2v .cluster text{fill:#333;}#mermaid-svg-iyx8Wye6ZVDgPi2v .cluster span{color:#333;}#mermaid-svg-iyx8Wye6ZVDgPi2v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iyx8Wye6ZVDgPi2v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iyx8Wye6ZVDgPi2v rect.text{fill:none;stroke-width:0;}#mermaid-svg-iyx8Wye6ZVDgPi2v .icon-shape,#mermaid-svg-iyx8Wye6ZVDgPi2v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iyx8Wye6ZVDgPi2v .icon-shape p,#mermaid-svg-iyx8Wye6ZVDgPi2v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iyx8Wye6ZVDgPi2v .icon-shape .label rect,#mermaid-svg-iyx8Wye6ZVDgPi2v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iyx8Wye6ZVDgPi2v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iyx8Wye6ZVDgPi2v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iyx8Wye6ZVDgPi2v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} {topic: AI}
RunnableParallel
joke_chain: 讲笑话
poem_chain: 写诗歌
{joke: AIMessage, poem: AIMessage}

执行时间对比:

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

3.4 RunnableParallel 参数说明

python 复制代码
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 硬编码历史

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

五、开发踩坑记录

5.1 input_messages_key 不匹配

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

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

python 复制代码
# 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=...) 一致。

python 复制代码
# prompt 中:
MessagesPlaceholder(variable_name="chat_history")

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

5.3 pipe 语法的理解

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

python 复制代码
# 管道语法(简洁)
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 速查

python 复制代码
# ---- 提示词模板 ----
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"})