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() 都不知道之前聊了什么。要实现多轮对话,需要:
- 保存历史:每轮对话结束后,保存用户和 AI 的消息
- 注入历史:下一轮对话时,把历史消息注入到提示词中
- 管理会话:不同用户/会话的历史互相隔离
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"必须与后面RunnableWithMessageHistory的history_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_key 与 MessagesPlaceholder 不匹配
问题: 历史消息没有注入,每次对话都像新的一样。
原因: 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"})