文章目录
- [1. 持久化实现的应用能力](#1. 持久化实现的应用能力)
-
- [1.1 记忆(Memory)](#1.1 记忆(Memory))
-
- [1.1.1 记忆概念](#1.1.1 记忆概念)
- [4.1.2 管理短期记忆](#4.1.2 管理短期记忆)
-
- [4.1.2.1 修剪消息](#4.1.2.1 修剪消息)
- [4.1.2.2 删除消息](#4.1.2.2 删除消息)
- [4.1.2.3 总结消息](#4.1.2.3 总结消息)
- [1.2 人机交互(Human-in-the-Loop)](#1.2 人机交互(Human-in-the-Loop))
-
- [1.2.1 中断(Interrupts)](#1.2.1 中断(Interrupts))
- [1.2.2 中断如何实现?](#1.2.2 中断如何实现?)
- [1.2.3 中断法则](#1.2.3 中断法则)
-
- [1.2.3.1 只能传序列化的简单数据](#1.2.3.1 只能传序列化的简单数据)
- [1.2.3.2 不应该将 interrupt() 调用包裹在 try/except 代码块中](#1.2.3.2 不应该将 interrupt() 调用包裹在 try/except 代码块中)
- [1.2.3.3 中断前的动作要"幂等"](#1.2.3.3 中断前的动作要“幂等”)
- [1.2.3.4 中断顺序固定](#1.2.3.4 中断顺序固定)
- [1.2.4 人机交互的应用场景](#1.2.4 人机交互的应用场景)
-
- [1.2.4.1 批准或拒绝(Approve or reject)](#1.2.4.1 批准或拒绝(Approve or reject))
- [1.2.4.2 查看和编辑状态(Review and edit state)](#1.2.4.2 查看和编辑状态(Review and edit state))
- [1.2.4.3 在工具中中断(Interrupts in tools)](#1.2.4.3 在工具中中断(Interrupts in tools))
- [1.2.4.4 验证人工输入(Validating human input)](#1.2.4.4 验证人工输入(Validating human input))
- [1.3 时间旅行(Time Travel)](#1.3 时间旅行(Time Travel))
-
- [1.3.1 时间旅行是什么?](#1.3.1 时间旅行是什么?)
- [1.3.2 时间旅行四步法](#1.3.2 时间旅行四步法)
-
- [1.3.2.1 第一步:初始执行工作流](#1.3.2.1 第一步:初始执行工作流)
- [1.3.2.2 第二步:查看历史检查点](#1.3.2.2 第二步:查看历史检查点)
- [1.3.2.3 第三步:修改状态(可选)](#1.3.2.3 第三步:修改状态(可选))
- [1.3.2.4 第四步:从检查点恢复执行](#1.3.2.4 第四步:从检查点恢复执行)
- [1.3.3 完整示例](#1.3.3 完整示例)
-
- [1.3.3.1 状态类型定义](#1.3.3.1 状态类型定义)
- [1.3.3.2 节点函数实现](#1.3.3.2 节点函数实现)
- [1.3.3.3 工作流构建](#1.3.3.3 工作流构建)
- [4.3.3.4 时间旅行调试过程](#4.3.3.4 时间旅行调试过程)
- [5. 持久化小结](#5. 持久化小结)
- [2. LangGraph其他核心能力](#2. LangGraph其他核心能力)
-
- [2.1 运行时上下文(Runtime context)](#2.1 运行时上下文(Runtime context))
-
- [2.1.1 什么是运行时上下文?](#2.1.1 什么是运行时上下文?)
-
- [2.1.1.1 上下文定义与分类](#2.1.1.1 上下文定义与分类)
- [2.1.1.2 场景练习](#2.1.1.2 场景练习)
- [2.1.2 配置运行时上下文](#2.1.2 配置运行时上下文)
- [2.2 流(Streaming)](#2.2 流(Streaming))
-
- [2.2.1 概念](#2.2.1 概念)
- [2.2.2 五种流模式](#2.2.2 五种流模式)
- [2.2.3 流式输出状态值](#2.2.3 流式输出状态值)
- [2.2.4 流式传输自定义数据](#2.2.4 流式传输自定义数据)
-
- [2.4.1 基本用法](#2.4.1 基本用法)
-
- [2.4.1.1 从节点和工具中输出用户自定义数据](#2.4.1.1 从节点和工具中输出用户自定义数据)
- [2.4.1.2 设置多种传输模式](#2.4.1.2 设置多种传输模式)
- [2.4.2 应用场景](#2.4.2 应用场景)
-
- [2.4.2.1 创建自定义监控面板](#2.4.2.1 创建自定义监控面板)
- [2.2.5 流式传输 LLM tokens](#2.2.5 流式传输 LLM tokens)
-
- [2.2.5.1 基本用法](#2.2.5.1 基本用法)
- [2.2.5.2 高级功能](#2.2.5.2 高级功能)
-
- [2.2.5.2.1 按 Tags 过滤 Tokens](#2.2.5.2.1 按 Tags 过滤 Tokens)
- [2.2.5.2.2 按节点名称过滤](#2.2.5.2.2 按节点名称过滤)
1. 持久化实现的应用能力
1.1 记忆(Memory)
1.1.1 记忆概念
记忆,是一种能够记住之前互动信息的系统。对于人工智能代理来说,记忆至关重要,因为它使他们能够记住任务,这一能力对反馈中学习,并根据用户偏好进行调整。随着代理处理涉及大量用户交互的复杂任务之前的互动,从效率和用户满意度都变得至关重要。
区分记忆和持久化的概念:
- 持久化为LangGraph底层能力,包含【线程级】持久化和【跨会话】持久化
- 记忆为LangGraph能实现的应用层能力,包含【短期记忆】和【长期记忆】
在应用层,短期记忆就由线程级持久化实现,长期记忆由跨会话持久化实现。
#mermaid-svg-oHux9Fj8RT7dgDUq{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-oHux9Fj8RT7dgDUq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oHux9Fj8RT7dgDUq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oHux9Fj8RT7dgDUq .error-icon{fill:#552222;}#mermaid-svg-oHux9Fj8RT7dgDUq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oHux9Fj8RT7dgDUq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oHux9Fj8RT7dgDUq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oHux9Fj8RT7dgDUq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oHux9Fj8RT7dgDUq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oHux9Fj8RT7dgDUq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oHux9Fj8RT7dgDUq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oHux9Fj8RT7dgDUq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oHux9Fj8RT7dgDUq .marker.cross{stroke:#333333;}#mermaid-svg-oHux9Fj8RT7dgDUq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oHux9Fj8RT7dgDUq p{margin:0;}#mermaid-svg-oHux9Fj8RT7dgDUq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oHux9Fj8RT7dgDUq .cluster-label text{fill:#333;}#mermaid-svg-oHux9Fj8RT7dgDUq .cluster-label span{color:#333;}#mermaid-svg-oHux9Fj8RT7dgDUq .cluster-label span p{background-color:transparent;}#mermaid-svg-oHux9Fj8RT7dgDUq .label text,#mermaid-svg-oHux9Fj8RT7dgDUq span{fill:#333;color:#333;}#mermaid-svg-oHux9Fj8RT7dgDUq .node rect,#mermaid-svg-oHux9Fj8RT7dgDUq .node circle,#mermaid-svg-oHux9Fj8RT7dgDUq .node ellipse,#mermaid-svg-oHux9Fj8RT7dgDUq .node polygon,#mermaid-svg-oHux9Fj8RT7dgDUq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oHux9Fj8RT7dgDUq .rough-node .label text,#mermaid-svg-oHux9Fj8RT7dgDUq .node .label text,#mermaid-svg-oHux9Fj8RT7dgDUq .image-shape .label,#mermaid-svg-oHux9Fj8RT7dgDUq .icon-shape .label{text-anchor:middle;}#mermaid-svg-oHux9Fj8RT7dgDUq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oHux9Fj8RT7dgDUq .rough-node .label,#mermaid-svg-oHux9Fj8RT7dgDUq .node .label,#mermaid-svg-oHux9Fj8RT7dgDUq .image-shape .label,#mermaid-svg-oHux9Fj8RT7dgDUq .icon-shape .label{text-align:center;}#mermaid-svg-oHux9Fj8RT7dgDUq .node.clickable{cursor:pointer;}#mermaid-svg-oHux9Fj8RT7dgDUq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oHux9Fj8RT7dgDUq .arrowheadPath{fill:#333333;}#mermaid-svg-oHux9Fj8RT7dgDUq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oHux9Fj8RT7dgDUq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oHux9Fj8RT7dgDUq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oHux9Fj8RT7dgDUq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oHux9Fj8RT7dgDUq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oHux9Fj8RT7dgDUq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oHux9Fj8RT7dgDUq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oHux9Fj8RT7dgDUq .cluster text{fill:#333;}#mermaid-svg-oHux9Fj8RT7dgDUq .cluster span{color:#333;}#mermaid-svg-oHux9Fj8RT7dgDUq 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-oHux9Fj8RT7dgDUq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oHux9Fj8RT7dgDUq rect.text{fill:none;stroke-width:0;}#mermaid-svg-oHux9Fj8RT7dgDUq .icon-shape,#mermaid-svg-oHux9Fj8RT7dgDUq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oHux9Fj8RT7dgDUq .icon-shape p,#mermaid-svg-oHux9Fj8RT7dgDUq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oHux9Fj8RT7dgDUq .icon-shape .label rect,#mermaid-svg-oHux9Fj8RT7dgDUq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oHux9Fj8RT7dgDUq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oHux9Fj8RT7dgDUq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oHux9Fj8RT7dgDUq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LangGraph能力
AI应用能力
短期记忆
长期记忆
线程级持久化
跨会话持久化
- 短期记忆:单次会话中保持的上下文信息
- 长期记忆:跨会话保存的用户或应用数据
4.1.2 管理短期记忆
讲述具体场景时,如何对记忆进行管理。例如当消息记录过多,需要进行消息裁剪、总结消息、消息删除等操作。
4.1.2.1 修剪消息
大多数LLM都有一个最大支持的上下文窗口。决定何时截断消息的一种方法是对消息历史记录中的令牌进行计数,并在接近该限制时截断,这与狼chain中的消息裁剪非常相似。
python
from langchain_core.messages.utils import trim_messages
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, MessagesState
model = init_chat_model("gpt-4o-mini", temperature=0)
def call_model(state: MessagesState):
# 只保留最近的128个token的消息
messages = trim_messages(
state["messages"],
strategy="last", # 策略: 保留最后的部分
token_counter=model, # 计算token数量
max_tokens=128, # 最大token数
start_on="human", # 从用户消息开始
end_on=("human","tool") # 结束于用户消息或工具消息
)
response = model.invoke(messages)
return {"messages": [response]}
checkpointer = InMemorySaver()
builder = StateGraph(MessagesState)
builder.add_node(call_model)
builder.add_edge(START, "call_model")
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": "hi, my name is bob"}, config)
graph.invoke({"messages": "write a short poem about dogs"}, config)
graph.invoke({"messages": "now do the same but for cats"}, config)
final_response = graph.invoke({"messages": "what's my name?"}, config)
final_response["messages"][-1].pretty_print()
运行结果如下:
plain
================================== Ai Message ==================================
[{'type': 'text', 'text': "I don't know your name unless you tell me.", 'annotations': [], 'id': 'msg_08397679739155b4016a5cb37fcb38819992ec6859b3a988c9', 'phase': 'final_answer'}]
4.1.2.2 删除消息
可以从图状态中删除消息以管理消息历史记录。当想要删除特定消息或清除整个消息历史记录时,这非常有用。
python
def call_model(state: MessagesState):
messages = state["messages"]
if len(messages) > 6:
# 删除最早的6条消息
return {
"messages": [RemoveMessage(id=m.id) for m in messages[:6]]
}
response = model.invoke(messages)
return {"messages": [response]}
# .....
# 测试: 可以发现只剩最后一条消息了
for message in final_response["messages"]:
message.pretty_print()
删除所有消息:
python
from langgraph.graph.message import REMOVE_ALL_MESSAGES
def call_model(state: MessagesState):
return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)]}
4.1.2.3 总结消息
实际上,修剪或删除消息也会存在问题:可能会因剔除消息而丢失信息。因此,某些应用更希望将消息历史记录进行总结,把旧的对话内容总结成简短摘要,保留关键信息,以代替冗长的历史记录。
#mermaid-svg-40B8r51AF9M6mk5r{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-40B8r51AF9M6mk5r .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-40B8r51AF9M6mk5r .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-40B8r51AF9M6mk5r .error-icon{fill:#552222;}#mermaid-svg-40B8r51AF9M6mk5r .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-40B8r51AF9M6mk5r .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-40B8r51AF9M6mk5r .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-40B8r51AF9M6mk5r .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-40B8r51AF9M6mk5r .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-40B8r51AF9M6mk5r .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-40B8r51AF9M6mk5r .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-40B8r51AF9M6mk5r .marker{fill:#333333;stroke:#333333;}#mermaid-svg-40B8r51AF9M6mk5r .marker.cross{stroke:#333333;}#mermaid-svg-40B8r51AF9M6mk5r svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-40B8r51AF9M6mk5r p{margin:0;}#mermaid-svg-40B8r51AF9M6mk5r .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-40B8r51AF9M6mk5r .cluster-label text{fill:#333;}#mermaid-svg-40B8r51AF9M6mk5r .cluster-label span{color:#333;}#mermaid-svg-40B8r51AF9M6mk5r .cluster-label span p{background-color:transparent;}#mermaid-svg-40B8r51AF9M6mk5r .label text,#mermaid-svg-40B8r51AF9M6mk5r span{fill:#333;color:#333;}#mermaid-svg-40B8r51AF9M6mk5r .node rect,#mermaid-svg-40B8r51AF9M6mk5r .node circle,#mermaid-svg-40B8r51AF9M6mk5r .node ellipse,#mermaid-svg-40B8r51AF9M6mk5r .node polygon,#mermaid-svg-40B8r51AF9M6mk5r .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-40B8r51AF9M6mk5r .rough-node .label text,#mermaid-svg-40B8r51AF9M6mk5r .node .label text,#mermaid-svg-40B8r51AF9M6mk5r .image-shape .label,#mermaid-svg-40B8r51AF9M6mk5r .icon-shape .label{text-anchor:middle;}#mermaid-svg-40B8r51AF9M6mk5r .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-40B8r51AF9M6mk5r .rough-node .label,#mermaid-svg-40B8r51AF9M6mk5r .node .label,#mermaid-svg-40B8r51AF9M6mk5r .image-shape .label,#mermaid-svg-40B8r51AF9M6mk5r .icon-shape .label{text-align:center;}#mermaid-svg-40B8r51AF9M6mk5r .node.clickable{cursor:pointer;}#mermaid-svg-40B8r51AF9M6mk5r .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-40B8r51AF9M6mk5r .arrowheadPath{fill:#333333;}#mermaid-svg-40B8r51AF9M6mk5r .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-40B8r51AF9M6mk5r .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-40B8r51AF9M6mk5r .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-40B8r51AF9M6mk5r .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-40B8r51AF9M6mk5r .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-40B8r51AF9M6mk5r .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-40B8r51AF9M6mk5r .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-40B8r51AF9M6mk5r .cluster text{fill:#333;}#mermaid-svg-40B8r51AF9M6mk5r .cluster span{color:#333;}#mermaid-svg-40B8r51AF9M6mk5r 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-40B8r51AF9M6mk5r .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-40B8r51AF9M6mk5r rect.text{fill:none;stroke-width:0;}#mermaid-svg-40B8r51AF9M6mk5r .icon-shape,#mermaid-svg-40B8r51AF9M6mk5r .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-40B8r51AF9M6mk5r .icon-shape p,#mermaid-svg-40B8r51AF9M6mk5r .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-40B8r51AF9M6mk5r .icon-shape .label rect,#mermaid-svg-40B8r51AF9M6mk5r .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-40B8r51AF9M6mk5r .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-40B8r51AF9M6mk5r .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-40B8r51AF9M6mk5r :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Summarize
Filter
过滤后对话
Human message
AI message
Human message
原始对话
Human message
AI message
Human message
AI message
Human message
LLM
Summary
先将State进行扩展,除了对话记录,还包含一个总结摘要字段:
python
from langgraph.graph import MessagesState
class State(MessagesState):
summary: str
现在要求:
- 对话记录:记录新的对话与结果
- 摘要:每次对话完成,需要进行总结。
- 完成总结摘要后,可以删除历史对话。
那么,在每次调用LLM时,便可以根据【新的请求】与【总结摘要信息】共同构建提示词来完成请求。
完整代码如下所示:
python
from langchain_core.messages import HumanMessage, RemoveMessage
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, MessagesState
model = ChatOpenAI(
model="gpt-5.4",
api_key=os.getenv("PACKYAPI_API_KEY"),
base_url="https://www.packyapi.com/v1/",
use_responses_api=True,
output_version="responses/v1",
)
# 每次聊天后自动总结,压缩上下文
class State(MessagesState):
summary: str
def call_model(state: State):
"""调用模型,带上总结+用户问题生成AI回复"""
# 使用历史总结+最新消息发起调用
summary = state.get("summary", "")
messages = [HumanMessage(content=summary)] + state["messages"]
return {"messages": [model.invoke(messages)]}
def summarize_conversation(state: State):
"""根据旧summary+最新消息生成新的summary"""
# 生成历史总结
# 1. 创建总结提示词
summary = state.get("summary", "")
if summary: # 有摘要(扩展)
summary_message = (
f"这是到目前为止的对话摘要: {summary}\n\n"
"基于上面的新消息扩展摘要: "
)
else: # 无摘要,新增
summary_message = "创建上面对话的摘要: "
# 2. 生成新总结: 消息列表 + 历史总结 调用模型
messages = state["messages"] + [HumanMessage(content=summary_message)]
response = model.invoke(messages)
# 3. 删除历史对话: 除了最新的AI消息,都可以删除
return {
"summary": response.content, # 历史总结
"messages": [RemoveMessage(id=m.id) for m in state["messages"][:-1]] # 保留最后的消息是为了打印结果
}
builder = StateGraph(State)
builder.add_node(call_model)
builder.add_node("summarize", summarize_conversation)
builder.add_edge(START, "call_model")
builder.add_edge("call_model", "summarize") # 每次对话完,进行总结
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": "hi, my name is bob"}, config)
graph.invoke({"messages": "write a short poem about dogs"}, config)
graph.invoke({"messages": "now do the same but for cats"}, config)
final_response = graph.invoke({"messages": "what's my name?"}, config)
print("\n=========== final response ===========")
print(final_response["messages"][-1].content)
print("\n=========== summary ===========")
print(final_response["summary"])
# 打印结果如下:
# =================================== Ai Message
# Your name is Bob.
#
# Summary: 对话摘要: 用户自我介绍为Bob,并询问如何获得帮助。随后,用户请求写一首关于猫的短诗。接着,用户又请求写一首关于狗的短诗。用户对动物诗歌表现出兴趣,可能希望进一步采访与宠物相关的主题或创作。
扩展:LangMem是一个由LangChain维护的库。它提供了可与任何存储系统一起使用的功能原语,也提供了与LangGraph中存储层的本机集成 。例如上述实现的记忆-消息摘要功能,在LangMem中专门提供了记忆管理库(如:SummarizationNode),简化了总结消息的过程。
1.2 人机交互(Human-in-the-Loop)
1.2.1 中断(Interrupts)
中断是 LangGraph 实现人机协同(Human-in-the-Loop)的核心原语,指在状态图的执行流程中主动暂停工作流,将控制权交还给外部调用方(人类用户、业务系统),等待外部输入后再从中断点无缝恢复执行的机制。
两大类型
-
静态中断
在图编译阶段或每次运行调用时,通过
interrupt_before(指定节点执行前暂停)、interrupt_after(指定节点执行后暂停)声明固定断点,属于声明式的暂停方式,适合流程中明确需要人工介入的固定节点,也可在运行时动态调整断点位置。 -
动态中断
在节点的业务逻辑内部调用
interrupt()函数主动触发,可根据当前图的运行状态做条件判断,还能自定义中断提示信息返回给调用方,灵活性更高,适合条件性的人工介入场景。
1.2.2 中断如何实现?
在工作流中,想要实现暂停与恢复需要:
- 通过调用
interrupt()方法中断执行流程,依靠持久化能力,保存当前状态。 - 外部用户通过发送
Command对象,使得工作流恢复执行流程。
交互流程如下图所示:
外部/人 LangGraph 外部/人 LangGraph #mermaid-svg-FBChQvsf6Yj5i8ZT{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-FBChQvsf6Yj5i8ZT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FBChQvsf6Yj5i8ZT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FBChQvsf6Yj5i8ZT .error-icon{fill:#552222;}#mermaid-svg-FBChQvsf6Yj5i8ZT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FBChQvsf6Yj5i8ZT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FBChQvsf6Yj5i8ZT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FBChQvsf6Yj5i8ZT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FBChQvsf6Yj5i8ZT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FBChQvsf6Yj5i8ZT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FBChQvsf6Yj5i8ZT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FBChQvsf6Yj5i8ZT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FBChQvsf6Yj5i8ZT .marker.cross{stroke:#333333;}#mermaid-svg-FBChQvsf6Yj5i8ZT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FBChQvsf6Yj5i8ZT p{margin:0;}#mermaid-svg-FBChQvsf6Yj5i8ZT .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FBChQvsf6Yj5i8ZT text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-FBChQvsf6Yj5i8ZT .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-FBChQvsf6Yj5i8ZT .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-FBChQvsf6Yj5i8ZT .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-FBChQvsf6Yj5i8ZT .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-FBChQvsf6Yj5i8ZT #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-FBChQvsf6Yj5i8ZT .sequenceNumber{fill:white;}#mermaid-svg-FBChQvsf6Yj5i8ZT #sequencenumber{fill:#333;}#mermaid-svg-FBChQvsf6Yj5i8ZT #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-FBChQvsf6Yj5i8ZT .messageText{fill:#333;stroke:none;}#mermaid-svg-FBChQvsf6Yj5i8ZT .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FBChQvsf6Yj5i8ZT .labelText,#mermaid-svg-FBChQvsf6Yj5i8ZT .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-FBChQvsf6Yj5i8ZT .loopText,#mermaid-svg-FBChQvsf6Yj5i8ZT .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-FBChQvsf6Yj5i8ZT .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-FBChQvsf6Yj5i8ZT .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-FBChQvsf6Yj5i8ZT .noteText,#mermaid-svg-FBChQvsf6Yj5i8ZT .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-FBChQvsf6Yj5i8ZT .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FBChQvsf6Yj5i8ZT .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FBChQvsf6Yj5i8ZT .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FBChQvsf6Yj5i8ZT .actorPopupMenu{position:absolute;}#mermaid-svg-FBChQvsf6Yj5i8ZT .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-FBChQvsf6Yj5i8ZT .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FBChQvsf6Yj5i8ZT .actor-man circle,#mermaid-svg-FBChQvsf6Yj5i8ZT line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-FBChQvsf6Yj5i8ZT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 运行节点 执行interrupt() 暂停并保存状态 状态快照已保存 __interrupt__返回 控制权移交 人类决策/审批 系统处理/输入 Command(resume="结果") 收到外部指令 恢复保存的状态 继续执行节点
直接看代码:
python
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt,Command
from langgraph.graph import StateGraph, START, MessagesState,END
class State(MessagesState):
input: str
output: str
def node(state: State):
"""进行动态中断操作"""
result = interrupt("结束还是继续?是或否") # 可以传入普通类型
if result == "是":
return {"output": "继续执行"}
else:
return {"output": "结束执行"}
builder=StateGraph(State)
builder.add_node(node)
builder.add_edge(START, "node")
builder.add_edge("node", END)
graph=builder.compile(checkpointer=InMemorySaver())
# 必须指明同一个线程,与checkpoinrt搭配使用
config={"configurable": {"thread_id": "thread_1"}}
print(graph.invoke({"input":"开始调用"},config=config)) # 这是第一次调用,执行到中断点
print(graph.invoke(Command(resume="否"), config=config)) # 输入中断结果,继续执行
输出结果:
plain
{'messages': [], 'input': '开始调用', '__interrupt__': [Interrupt(value='结束还是继续?是或否', id='32d339e3f00a3afdc8c5c4d559ef9c2c')]}
{'messages': [], 'input': '开始调用', 'output': '结束执行'}
代码关键点:
- 编译图时:必须指定
checkpointer,以在每个步骤后保存图状态。 - 调用
interrupt()时:表示主动喊"停!",并传递提示信息。 - 使用
invoke/stream恢复执行,需使用Command(resume=...)语法。resume表示传回AI的响应值- 必须使用
thread_id运行Graph,相当于告诉系统读哪个存档。
1.2.3 中断法则
1.2.3.1 只能传序列化的简单数据
复杂值无法进行传递,例如不要传函数、类实例、数据库连接等。只传能序列化的简单数据,如字符串、数字、布尔、简单字典/列表。
正面示例:
python
def node_a(state: State):
# 正确: 传递可序列化的简单类型
name = interrupt("What's your name?")
count = interrupt(42)
approved = interrupt(True)
return {"name": name, "count": count, "approved": approved}
python
def node_a(state: State):
# 正确: 传递带有简单值的字典
response = interrupt({
"question": "Enter user details",
"fields": ["name", "email", "age"],
"current_values": state.get("user", {})
})
return {"user": response}
反面示例:
python
def validate_input(value):
return len(value) > 0
def node_a(state: State):
# 错误: 传递一个函数来实现中断
# 函数不能被序列化
response = interrupt({
"question": "What's your name?",
"validator": validate_input
})
return {"name": response}
python
class DataProcessor:
def __init__(self, config):
self.config = config
def node_a(state: State):
processor = DataProcessor({"mode": "strict"})
# 错误: 传递一个类实例来实现中断
# 实例不能被序列化
response = interrupt({
"question": "Enter data to process",
"processor": processor
})
return {"result": response}
1.2.3.2 不应该将 interrupt() 调用包裹在 try/except 代码块中
错误做法是:如果将 interrupt() 调用包裹在通用的 try/except Exception 或 try/except(空)代码块中,你编写的代码会提前捕获这个特殊异常。这会导致运行时系统无法感知到中断,从而使 interrupt() 功能失效。
反面示例:
python
def node_a(state: State):
# 错误: 在try/except中包装中断
try:
interrupt("What's your name?")
except Exception as e:
print(e)
return state
根本原因是 interrupt() 函数内部通过抛出一个特殊的异常来实现暂停执行。这个异常需要被LangGraph的运行时系统捕获,以触发状态的保存和等待。
正确做法:
- 分离逻辑:将
interrupt()调用与可能引发其他异常的代码分开。先调用interrupt(),然后再处理可能出错的操作。 - 精确捕获:在
try/except块中只捕获你预期会发生的、非常具体的异常类型(例如NetworkException)。这样,interrupt()抛出的特殊异常就不会被你的代码捕获,而能顺利传递给运行时系统。
正面示例:
python
def node_a(state: State):
# 正确: 先中断,再处理
interrupt("What's your name?")
try:
# 将中断调用与易出错代码分开
fetch_data()
except Exception as e:
print(e)
return state
python
def node_a(state: State):
# 正确: 捕捉特定的异常类型
name = interrupt("What's your name?")
try:
fetch_data()
except NetworkException as e:
print(e)
return state
使用常规的错误处理模式会导致 interrupt() 机制失效。必须让 interrupt() 抛出的特殊异常能够"逃逸"出你编写的节点函数,以便被LangGraph运行时正确处理。
1.2.3.3 中断前的动作要"幂等"
LangGraph 的 interrupt() 恢复时,不是从 interrupt() 下一行继续跑,而是会重新执行整个节点函数。

如果这些代码包含非幂等的副作用操作(如创建记录、发送消息、扣款等),每次恢复都会重复这些操作。这可能导致数据重复、不一致或意外行为。
幂等性:一个操作无论执行一次还是多次,产生的效果都相同。
正面示例:
python
# 使用幂等操作
def node_a(state: State):
# 正确: 使用 upsert (更新或插入) 操作,多次执行结果一致
db.upsert_user(
user_id=state["user_id"],
status="pending_approval"
)
approved = interrupt("Approve this change?")
return {"approved": approved}
python
# 将副作用放在中断之后
def node_a(state: State):
# 正确: 先中断,获得批准后再执行副作用
approved = interrupt("Approve this change?")
if approved:
db.create_audit_log(
user_id=state["user_id"],
action="approved"
)
return {"approved": approved}
python
# 将副作用分离到独立节点
def approval_node(state: State):
# 只处理中断
approved = interrupt("Approve this change?")
return {"approved": approved}
def notification_node(state: State):
# 正确: 副作用在独立节点中,仅在获得批准后执行一次
if state["approved"]:
send_notification(user_id=state["user_id"], status="approved")
return state
反面示例:
python
# 在中断前创建新记录
def node_a(state: State):
# 错误: 每次恢复都会创建新的审计记录
audit_id = db.create_audit_log({
"user_id": state["user_id"],
"action": "pending_approval",
datetime.now()
})
approved = interrupt("Approve this change?")
return {"approved": approved, "audit_id": audit_id}
python
# 在中断前追加到列表
def node_a(state: State):
# 错误: 每次恢复都会重复追加相同条目
db.append_to_history(
state["user_id"],
"approval_requested"
)
approved = interrupt("Approve this change?")
return {"approved": approved}
这一规则的核心是:确保在 interrupt() 调用之前执行的所有操作都是幂等的 ,或者将非幂等操作移到 interrupt() 调用之后。这是为了避免因节点重新执行而导致的重复副作用,确保系统的数据一致性和预期行为。
1.2.3.4 中断顺序固定
在同一个节点中使用多个 interrupt() 调用时需要注意的顺序和索引匹配规则。LangGraph使用严格的索引顺序来匹配恢复值:
- 恢复执行从头开始:节点恢复时会从开头重新运行,而不是从中断的精确行继续。
- 索引匹配:LangGraph为每个执行任务维护一个恢复值列表。遇到
interrupt()时,按顺序从这个列表中取对应的值。 - 顺序必须一致:中断调用的顺序在每次执行中必须完全相同。
正面示例:
python
def node_a(state: State):
# 正确: 中断调用顺序固定
name = interrupt("What's your name?") # 索引0
age = interrupt("What's your age?") # 索引1
city = interrupt("What's your city?") # 索引2
return {"name": name, "age": age, "city": city}
反面示例:
python
# 条件性跳过中断
def node_a(state: State):
name = interrupt("What's your name?")
# 错误: 索引可能跳过,恢复时可能不跳过,导致索引错乱
if state.get("needs_age"):
age = interrupt("What's your age?") # 索引1 (有时存在)
city = interrupt("What's your city?") # 索引2 (不确定)
python
# 基于非确定性数据的循环中断
def node_a(state: State):
# 错误: 中断数量随动态列表变化
results = []
for item in state["dynamic_list"]:
result = interrupt(f"Approve {item}?")
results.append(result)
1.2.4 人机交互的应用场景
使用中断来实现需要人工介入的交互式工作流有四种常见模式:
- 审批或拒绝:在执行关键操作(如API调用、数据库更改)之前暂停流程,等待人工批准或拒绝。根据返回的指令,流程图会路由到不同的分支。
- 审查和编辑状态:暂停流程,让人工可以审查并修改流程图当前的状态(例如,LLM生成的文本内容),然后将编辑后的内容传回,更新状态并继续执行。
- 在工具中中断:将中断直接置于工具函数内部。当LLM调用该工具时,流程会自动暂停,允许人工在工具实际执行前审查、编辑其调用参数或直接取消调用。
- 验证人工输入:通过循环使用中断,反复收集和验证输入,直到输入内容通过验证(例如,确保输入一个有效的正年龄)。这适用于需要收集和验证数据的场景。
这部分的核心思想是:中断功能解锁了"暂停执行并等待外部输入"的能力,从而使得构建人机交互(human-in-the-loop)的应用成为可能。每个模式都附带了简明的代码示例,展示了如何在中途暂停、如何将信息传递给外部系统,以及如何在获得响应后恢复执行。
1.2.4.1 批准或拒绝(Approve or reject)
这是中断功能最常见的一种用途。在执行关键性操作(例如调用API、修改数据库、进行金融交易等)之前,暂停图(graph)的执行,等待人工(如管理员、用户)的批准或拒绝。
#mermaid-svg-GsJxi4tYR7PLj00Z{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-GsJxi4tYR7PLj00Z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GsJxi4tYR7PLj00Z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GsJxi4tYR7PLj00Z .error-icon{fill:#552222;}#mermaid-svg-GsJxi4tYR7PLj00Z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GsJxi4tYR7PLj00Z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GsJxi4tYR7PLj00Z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GsJxi4tYR7PLj00Z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GsJxi4tYR7PLj00Z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GsJxi4tYR7PLj00Z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GsJxi4tYR7PLj00Z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GsJxi4tYR7PLj00Z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GsJxi4tYR7PLj00Z .marker.cross{stroke:#333333;}#mermaid-svg-GsJxi4tYR7PLj00Z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GsJxi4tYR7PLj00Z p{margin:0;}#mermaid-svg-GsJxi4tYR7PLj00Z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GsJxi4tYR7PLj00Z .cluster-label text{fill:#333;}#mermaid-svg-GsJxi4tYR7PLj00Z .cluster-label span{color:#333;}#mermaid-svg-GsJxi4tYR7PLj00Z .cluster-label span p{background-color:transparent;}#mermaid-svg-GsJxi4tYR7PLj00Z .label text,#mermaid-svg-GsJxi4tYR7PLj00Z span{fill:#333;color:#333;}#mermaid-svg-GsJxi4tYR7PLj00Z .node rect,#mermaid-svg-GsJxi4tYR7PLj00Z .node circle,#mermaid-svg-GsJxi4tYR7PLj00Z .node ellipse,#mermaid-svg-GsJxi4tYR7PLj00Z .node polygon,#mermaid-svg-GsJxi4tYR7PLj00Z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GsJxi4tYR7PLj00Z .rough-node .label text,#mermaid-svg-GsJxi4tYR7PLj00Z .node .label text,#mermaid-svg-GsJxi4tYR7PLj00Z .image-shape .label,#mermaid-svg-GsJxi4tYR7PLj00Z .icon-shape .label{text-anchor:middle;}#mermaid-svg-GsJxi4tYR7PLj00Z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GsJxi4tYR7PLj00Z .rough-node .label,#mermaid-svg-GsJxi4tYR7PLj00Z .node .label,#mermaid-svg-GsJxi4tYR7PLj00Z .image-shape .label,#mermaid-svg-GsJxi4tYR7PLj00Z .icon-shape .label{text-align:center;}#mermaid-svg-GsJxi4tYR7PLj00Z .node.clickable{cursor:pointer;}#mermaid-svg-GsJxi4tYR7PLj00Z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GsJxi4tYR7PLj00Z .arrowheadPath{fill:#333333;}#mermaid-svg-GsJxi4tYR7PLj00Z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GsJxi4tYR7PLj00Z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GsJxi4tYR7PLj00Z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GsJxi4tYR7PLj00Z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GsJxi4tYR7PLj00Z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GsJxi4tYR7PLj00Z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GsJxi4tYR7PLj00Z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GsJxi4tYR7PLj00Z .cluster text{fill:#333;}#mermaid-svg-GsJxi4tYR7PLj00Z .cluster span{color:#333;}#mermaid-svg-GsJxi4tYR7PLj00Z 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-GsJxi4tYR7PLj00Z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GsJxi4tYR7PLj00Z rect.text{fill:none;stroke-width:0;}#mermaid-svg-GsJxi4tYR7PLj00Z .icon-shape,#mermaid-svg-GsJxi4tYR7PLj00Z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GsJxi4tYR7PLj00Z .icon-shape p,#mermaid-svg-GsJxi4tYR7PLj00Z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GsJxi4tYR7PLj00Z .icon-shape .label rect,#mermaid-svg-GsJxi4tYR7PLj00Z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GsJxi4tYR7PLj00Z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GsJxi4tYR7PLj00Z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GsJxi4tYR7PLj00Z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 人工拒绝
人工批准
start
批准此操作?
interrupt
拒绝后操作
批准后操作
end
实现方式:
- 在节点中使用
interrupt()函数暂停执行。传入一个包含审批问题、操作详情等信息的JSON可序列化对象,该对象会显示在调用结果result["interrupt"]中。 - 当图被暂停后,外部系统(如UI界面)可以根据
interrupt中的信息向用户展示审批请求。 - 人工做出决定(批准或拒绝)后,通过再次调用图并传入
Command(resume=...)来恢复执行。 - 恢复时,传入
Command(resume=True)表示批准,传入Command(resume=False)表示拒绝。 - 节点代码会接收这个
resume值作为interrupt()函数的返回值,然后根据该值,使用Command(goto=...)将流程路由到不同的后续节点(例如"proceed"节点或"cancel"节点)。Command(goto=...)表示要导航到的下一个节点的名称
【练习】AI转账前进行人工审批:
python
from typing import Literal, Optional, TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
class ApprovalState(TypedDict):
action_details: str # 操作详情
status: Optional[Literal["等待", "批准", "拒绝"]]
# 节点中,直接判断后续执行流程
def approval_node(state: ApprovalState):
"""审批节点"""
decision=interrupt({
"question": "是否批准该操作?",
"options": ["批准", "拒绝"],
"details": state["action_details"]
})
if decision=="批准":
next_node="processed_node"
else:
next_node="cancel_node"
return Command(goto=next_node) #直接跳转到后续节点,不用添加条件边了
def processed_node(state: ApprovalState):
"""处理节点"""
print(f"审批结果: {state['status']}")
return {"status":"批准"}
def cancel_node(state: ApprovalState):
"""取消节点"""
print("拒绝审批,操作取消")
return {"status":"拒绝"}
builder=StateGraph(ApprovalState)
builder.add_node(approval_node)
builder.add_node(processed_node)
builder.add_node(cancel_node)
builder.add_edge(START, "approval_node")
def approval_edge(state: ApprovalState):
if state["status"]=="批准":
return "processed_node"
else:
return "cancel_node"
# builder.add_conditional_edge("approval_node", approval_edge,["processed_node","cancel_node"])
builder.add_edge("processed_node", END)
builder.add_edge("cancel_node", END)
graph=builder.compile(checkpointer=InMemorySaver())
config={"configurable": {"thread_id": "thread_2"}}
print(graph.invoke({"action_details":"支付宝到账300000元","status":"pending"},config=config))
print(graph.invoke(Command(resume="批准"), config=config))
执行结果:
plain
{'action_details': '支付宝到账300000元', 'status': 'pending', '__interrupt__': [Interrupt(value={'question': '是否批准该操作?', 'options': ['批准', '拒绝'], 'details': '支付宝到账300000元'}, id='d1d14dae58d8888ec17ed30338acc4a0')]}
审批结果: pending
{'action_details': '支付宝到账300000元', 'status': '批准'}
1.2.4.2 查看和编辑状态(Review and edit state)
该场景表示在流程执行过程,使用中断功能让人进行审查和编辑状态内容。
#mermaid-svg-OsRPRGyCtHoboTiF{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-OsRPRGyCtHoboTiF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OsRPRGyCtHoboTiF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OsRPRGyCtHoboTiF .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-OsRPRGyCtHoboTiF .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-OsRPRGyCtHoboTiF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OsRPRGyCtHoboTiF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OsRPRGyCtHoboTiF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OsRPRGyCtHoboTiF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OsRPRGyCtHoboTiF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OsRPRGyCtHoboTiF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OsRPRGyCtHoboTiF .marker{fill:#222222;stroke:#222222;}#mermaid-svg-OsRPRGyCtHoboTiF .marker.cross{stroke:#222222;}#mermaid-svg-OsRPRGyCtHoboTiF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OsRPRGyCtHoboTiF p{margin:0;}#mermaid-svg-OsRPRGyCtHoboTiF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OsRPRGyCtHoboTiF .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-OsRPRGyCtHoboTiF .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-OsRPRGyCtHoboTiF .cluster-label span p{background-color:transparent;}#mermaid-svg-OsRPRGyCtHoboTiF .label text,#mermaid-svg-OsRPRGyCtHoboTiF span{fill:#333;color:#333;}#mermaid-svg-OsRPRGyCtHoboTiF .node rect,#mermaid-svg-OsRPRGyCtHoboTiF .node circle,#mermaid-svg-OsRPRGyCtHoboTiF .node ellipse,#mermaid-svg-OsRPRGyCtHoboTiF .node polygon,#mermaid-svg-OsRPRGyCtHoboTiF .node path{fill:#e8e0f8;stroke:#8a6fd1;stroke-width:1px;}#mermaid-svg-OsRPRGyCtHoboTiF .rough-node .label text,#mermaid-svg-OsRPRGyCtHoboTiF .node .label text,#mermaid-svg-OsRPRGyCtHoboTiF .image-shape .label,#mermaid-svg-OsRPRGyCtHoboTiF .icon-shape .label{text-anchor:middle;}#mermaid-svg-OsRPRGyCtHoboTiF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OsRPRGyCtHoboTiF .rough-node .label,#mermaid-svg-OsRPRGyCtHoboTiF .node .label,#mermaid-svg-OsRPRGyCtHoboTiF .image-shape .label,#mermaid-svg-OsRPRGyCtHoboTiF .icon-shape .label{text-align:center;}#mermaid-svg-OsRPRGyCtHoboTiF .node.clickable{cursor:pointer;}#mermaid-svg-OsRPRGyCtHoboTiF .root .anchor path{fill:#222222!important;stroke-width:0;stroke:#222222;}#mermaid-svg-OsRPRGyCtHoboTiF .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-OsRPRGyCtHoboTiF .edgePath .path{stroke:#222222;stroke-width:2.0px;}#mermaid-svg-OsRPRGyCtHoboTiF .flowchart-link{stroke:#222222;fill:none;}#mermaid-svg-OsRPRGyCtHoboTiF .edgeLabel{background-color:#f0f0f0;text-align:center;}#mermaid-svg-OsRPRGyCtHoboTiF .edgeLabel p{background-color:#f0f0f0;}#mermaid-svg-OsRPRGyCtHoboTiF .edgeLabel rect{opacity:0.5;background-color:#f0f0f0;fill:#f0f0f0;}#mermaid-svg-OsRPRGyCtHoboTiF .labelBkg{background-color:rgba(240, 240, 240, 0.5);}#mermaid-svg-OsRPRGyCtHoboTiF .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-OsRPRGyCtHoboTiF .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-OsRPRGyCtHoboTiF .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-OsRPRGyCtHoboTiF 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(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OsRPRGyCtHoboTiF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OsRPRGyCtHoboTiF rect.text{fill:none;stroke-width:0;}#mermaid-svg-OsRPRGyCtHoboTiF .icon-shape,#mermaid-svg-OsRPRGyCtHoboTiF .image-shape{background-color:#f0f0f0;text-align:center;}#mermaid-svg-OsRPRGyCtHoboTiF .icon-shape p,#mermaid-svg-OsRPRGyCtHoboTiF .image-shape p{background-color:#f0f0f0;padding:2px;}#mermaid-svg-OsRPRGyCtHoboTiF .icon-shape .label rect,#mermaid-svg-OsRPRGyCtHoboTiF .image-shape .label rect{opacity:0.5;background-color:#f0f0f0;fill:#f0f0f0;}#mermaid-svg-OsRPRGyCtHoboTiF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OsRPRGyCtHoboTiF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OsRPRGyCtHoboTiF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-OsRPRGyCtHoboTiF .pink>*{fill:#fce4ec!important;stroke:#8a6fd1!important;stroke-width:1px!important;}#mermaid-svg-OsRPRGyCtHoboTiF .pink span{fill:#fce4ec!important;stroke:#8a6fd1!important;stroke-width:1px!important;} UI展示内容
传入编辑内容
恢复流程
初始状态
中断触发
人工编辑
更新状态
【练习】人工审核AI文档内容,并进行编辑:
python
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
class State(TypedDict):
text: str
def review_node(state: State):
"""人工审核节点"""
result=interrupt({
"question":"请审核该文本是否符合要求",
"text":state["text"]
})
return {"text":result}
builder=StateGraph(State)
builder.add_node(review_node)
builder.add_edge(START, "review_node")
builder.add_edge("review_node", END)
graph=builder.compile(checkpointer=InMemorySaver())
config={"configurable": {"thread_id": "thread_3"}}
print(graph.invoke({"text":"这是一段需要审核的文本"},config=config))
print(graph.invoke(Command(resume="这是修改过后的文本"), config=config))
除此之外,还允许:
- 人工审查和修改LLM生成的内容(如文本、数据)
python
# 生成营销文案后让营销专家审核
interrupt({
"instruction": "为社交媒体优化营销文案",
"content": "...", # 待审核文案
"platform": "douyin"
})
- 在继续执行前纠正错误、添加信息或进行微调
python
# 提取结构化数据后让专家验证
interrupt({
"instruction": "验证和纠正提取的产品规格",
"content": "...", # 提取的规格
"required_fields": ["尺寸", "重量", "材料"]
})
- 适用于需要质量控制或专业审核的自动化流程
python
# 生成代码后让开发人员审查
interrupt({
"instruction": "查看生成的Python函数的效率和最佳实践",
"content": "...", # 待审核代码
"language": "Python"
})
注意恢复时传入的内容会完全替换原始内容。如果需要部分编辑,可以在中断载荷中标记可编辑部分,然后再手动拼接。
你可能以为会自动合并成这样:
python
{
"title": "请确认转账信息",
"amount": 300000,
"receiver": "张三",
"remark": "改为预付款"
}
但实际上为:
python
{
"remark": "改为预付款"
}
1.2.4.3 在工具中中断(Interrupts in tools)
还支持将中断功能直接嵌入到工具(tool)函数内部,从而实现在工具调用前进行人工审查和干预的能力。
#mermaid-svg-4OxYyilAfZD3Wsiv{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-4OxYyilAfZD3Wsiv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4OxYyilAfZD3Wsiv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4OxYyilAfZD3Wsiv .error-icon{fill:#552222;}#mermaid-svg-4OxYyilAfZD3Wsiv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4OxYyilAfZD3Wsiv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4OxYyilAfZD3Wsiv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4OxYyilAfZD3Wsiv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4OxYyilAfZD3Wsiv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4OxYyilAfZD3Wsiv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4OxYyilAfZD3Wsiv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4OxYyilAfZD3Wsiv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4OxYyilAfZD3Wsiv .marker.cross{stroke:#333333;}#mermaid-svg-4OxYyilAfZD3Wsiv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4OxYyilAfZD3Wsiv p{margin:0;}#mermaid-svg-4OxYyilAfZD3Wsiv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4OxYyilAfZD3Wsiv .cluster-label text{fill:#333;}#mermaid-svg-4OxYyilAfZD3Wsiv .cluster-label span{color:#333;}#mermaid-svg-4OxYyilAfZD3Wsiv .cluster-label span p{background-color:transparent;}#mermaid-svg-4OxYyilAfZD3Wsiv .label text,#mermaid-svg-4OxYyilAfZD3Wsiv span{fill:#333;color:#333;}#mermaid-svg-4OxYyilAfZD3Wsiv .node rect,#mermaid-svg-4OxYyilAfZD3Wsiv .node circle,#mermaid-svg-4OxYyilAfZD3Wsiv .node ellipse,#mermaid-svg-4OxYyilAfZD3Wsiv .node polygon,#mermaid-svg-4OxYyilAfZD3Wsiv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4OxYyilAfZD3Wsiv .rough-node .label text,#mermaid-svg-4OxYyilAfZD3Wsiv .node .label text,#mermaid-svg-4OxYyilAfZD3Wsiv .image-shape .label,#mermaid-svg-4OxYyilAfZD3Wsiv .icon-shape .label{text-anchor:middle;}#mermaid-svg-4OxYyilAfZD3Wsiv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4OxYyilAfZD3Wsiv .rough-node .label,#mermaid-svg-4OxYyilAfZD3Wsiv .node .label,#mermaid-svg-4OxYyilAfZD3Wsiv .image-shape .label,#mermaid-svg-4OxYyilAfZD3Wsiv .icon-shape .label{text-align:center;}#mermaid-svg-4OxYyilAfZD3Wsiv .node.clickable{cursor:pointer;}#mermaid-svg-4OxYyilAfZD3Wsiv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4OxYyilAfZD3Wsiv .arrowheadPath{fill:#333333;}#mermaid-svg-4OxYyilAfZD3Wsiv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4OxYyilAfZD3Wsiv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4OxYyilAfZD3Wsiv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4OxYyilAfZD3Wsiv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4OxYyilAfZD3Wsiv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4OxYyilAfZD3Wsiv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4OxYyilAfZD3Wsiv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4OxYyilAfZD3Wsiv .cluster text{fill:#333;}#mermaid-svg-4OxYyilAfZD3Wsiv .cluster span{color:#333;}#mermaid-svg-4OxYyilAfZD3Wsiv 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-4OxYyilAfZD3Wsiv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4OxYyilAfZD3Wsiv rect.text{fill:none;stroke-width:0;}#mermaid-svg-4OxYyilAfZD3Wsiv .icon-shape,#mermaid-svg-4OxYyilAfZD3Wsiv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4OxYyilAfZD3Wsiv .icon-shape p,#mermaid-svg-4OxYyilAfZD3Wsiv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4OxYyilAfZD3Wsiv .icon-shape .label rect,#mermaid-svg-4OxYyilAfZD3Wsiv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4OxYyilAfZD3Wsiv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4OxYyilAfZD3Wsiv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4OxYyilAfZD3Wsiv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 需要调用工具
人工决定不执行工具
人工决定执行工具
初始状态
调用模型
工具执行前先执行中断
更新状态
执行工具
关键特点如下:
- 中断逻辑内置于工具,而非图的节点中。
- 工具变得"智能",知道何时需要人工批准。
- 工具可以在任何图中使用,自动具备中断能力
【练习】AI发送邮件前,人工审查邮件内容:
python
import operator
from typing import TypedDict, Annotated
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain_core.messages import AnyMessage, SystemMessage, ToolMessage, HumanMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
# 在工具中使用中断
@tool
def send_email(to:str,subject:str,body:str):
"""发送邮件工具"""
result=interrupt({
"action":"我要发送邮件了",
"to":to,
"subject":subject,
"body":body,
"message":"请确认是否发送邮件"
})
if result["action"]=="拒绝":
return "邮件发送失败"
else:
final_to=result.get("to",to)
final_subject=result.get("subject",subject)
final_body=result.get("body",body)
print(f"发送邮件给{final_to},主题:{final_subject},内容:{final_body}") # 放在interrupt后面,防止重复发送邮件
return f"最终发送邮件给{final_to},主题:{final_subject},内容:{final_body}"
model = ChatOpenAI(
model="gpt-5.4",
api_key=os.getenv("PACKYAPI_API_KEY"),
base_url="https://www.packyapi.com/v1/",
use_responses_api=True,
output_version="responses/v1",
)
model_with_tool=model.bind_tools([send_email])
def llm_call(state:MessagesState):
"""调用模型,带上总结+用户问题生成AI回复"""
result=model_with_tool.invoke(
[SystemMessage(content="你是一个邮件发送助手")]
+ state["messages"]
)
# 判断是否调用工具
if result.tool_calls:
tool_call=result.tool_calls[0]
# 调用工具"
tool_result=send_email.invoke(tool_call["args"])
return {"messages": [ToolMessage(tool_call_id=tool_call["id"],content=tool_result)]}
return {
"messages": [result]
}
builder=StateGraph(MessagesState)
builder.add_node(llm_call)
builder.add_edge(START, "llm_call")
builder.add_edge("llm_call", END)
graph=builder.compile(checkpointer=InMemorySaver())
config={"configurable": {"thread_id": "thread_4"}}
# 第一次调用:启动图,执行到 interrupt 时暂停
print(graph.invoke(
{
"messages": [
HumanMessage(content="请发送一封邮件给 test@example.com,主题是测试邮件,正文是这是一段需要审核的文本")
]
},
config=config
))
# 恢复调用:同意,并修改部分邮件内容
print(graph.invoke(
Command(resume={
"action": "同意",
"to": "user@example.com",
"body": "这是一封测试邮件"
}),
config=config
))
输出结果:
plain
{'messages': [HumanMessage(content='请发送一封邮件给 test@example.com,主题是测试邮件,正文是这是一段需要审核的文本', additional_kwargs={}, response_metadata={}, id='5beaa32e-abd0-4a94-9839-5c1e9a109551')], '__interrupt__': [Interrupt(value={'action': '我要发送邮件了', 'to': 'test@example.com', 'subject': '测试邮件', 'body': '这是一段需要审核的文本', 'message': '请确认是否发送邮件'}, id='4b6b5dda3dc9b1b1a5af00e29735f9b2')]}
发送邮件给user@example.com,主题:测试邮件,内容:这是一封测试邮件
{'messages': [HumanMessage(content='请发送一封邮件给 test@example.com,主题是测试邮件,正文是这是一段需要审核的文本', additional_kwargs={}, response_metadata={}, id='5beaa32e-abd0-4a94-9839-5c1e9a109551'), ToolMessage(content='最终发送邮件给user@example.com,主题:测试邮件,内容:这是一封测试邮件', id='120e2758-2994-4f50-ad55-fdf49569e207', tool_call_id='call_Bk7jYtJCC5yRNjAtgvrcs32I')]}
1.2.4.4 验证人工输入(Validating human input)
该场景使用中断功能在循环中验证人类输入,直到输入有效为止。
用户输入验证流程:
#mermaid-svg-wQoUEcZhr1HaNNOd{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-wQoUEcZhr1HaNNOd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wQoUEcZhr1HaNNOd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wQoUEcZhr1HaNNOd .error-icon{fill:#552222;}#mermaid-svg-wQoUEcZhr1HaNNOd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wQoUEcZhr1HaNNOd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wQoUEcZhr1HaNNOd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wQoUEcZhr1HaNNOd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wQoUEcZhr1HaNNOd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wQoUEcZhr1HaNNOd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wQoUEcZhr1HaNNOd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wQoUEcZhr1HaNNOd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wQoUEcZhr1HaNNOd .marker.cross{stroke:#333333;}#mermaid-svg-wQoUEcZhr1HaNNOd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wQoUEcZhr1HaNNOd p{margin:0;}#mermaid-svg-wQoUEcZhr1HaNNOd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wQoUEcZhr1HaNNOd .cluster-label text{fill:#333;}#mermaid-svg-wQoUEcZhr1HaNNOd .cluster-label span{color:#333;}#mermaid-svg-wQoUEcZhr1HaNNOd .cluster-label span p{background-color:transparent;}#mermaid-svg-wQoUEcZhr1HaNNOd .label text,#mermaid-svg-wQoUEcZhr1HaNNOd span{fill:#333;color:#333;}#mermaid-svg-wQoUEcZhr1HaNNOd .node rect,#mermaid-svg-wQoUEcZhr1HaNNOd .node circle,#mermaid-svg-wQoUEcZhr1HaNNOd .node ellipse,#mermaid-svg-wQoUEcZhr1HaNNOd .node polygon,#mermaid-svg-wQoUEcZhr1HaNNOd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wQoUEcZhr1HaNNOd .rough-node .label text,#mermaid-svg-wQoUEcZhr1HaNNOd .node .label text,#mermaid-svg-wQoUEcZhr1HaNNOd .image-shape .label,#mermaid-svg-wQoUEcZhr1HaNNOd .icon-shape .label{text-anchor:middle;}#mermaid-svg-wQoUEcZhr1HaNNOd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wQoUEcZhr1HaNNOd .rough-node .label,#mermaid-svg-wQoUEcZhr1HaNNOd .node .label,#mermaid-svg-wQoUEcZhr1HaNNOd .image-shape .label,#mermaid-svg-wQoUEcZhr1HaNNOd .icon-shape .label{text-align:center;}#mermaid-svg-wQoUEcZhr1HaNNOd .node.clickable{cursor:pointer;}#mermaid-svg-wQoUEcZhr1HaNNOd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wQoUEcZhr1HaNNOd .arrowheadPath{fill:#333333;}#mermaid-svg-wQoUEcZhr1HaNNOd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wQoUEcZhr1HaNNOd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wQoUEcZhr1HaNNOd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wQoUEcZhr1HaNNOd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wQoUEcZhr1HaNNOd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wQoUEcZhr1HaNNOd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wQoUEcZhr1HaNNOd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wQoUEcZhr1HaNNOd .cluster text{fill:#333;}#mermaid-svg-wQoUEcZhr1HaNNOd .cluster span{color:#333;}#mermaid-svg-wQoUEcZhr1HaNNOd 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-wQoUEcZhr1HaNNOd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wQoUEcZhr1HaNNOd rect.text{fill:none;stroke-width:0;}#mermaid-svg-wQoUEcZhr1HaNNOd .icon-shape,#mermaid-svg-wQoUEcZhr1HaNNOd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wQoUEcZhr1HaNNOd .icon-shape p,#mermaid-svg-wQoUEcZhr1HaNNOd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wQoUEcZhr1HaNNOd .icon-shape .label rect,#mermaid-svg-wQoUEcZhr1HaNNOd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wQoUEcZhr1HaNNOd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wQoUEcZhr1HaNNOd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wQoUEcZhr1HaNNOd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 无效,错误反馈
有效
开始
中断
用户输入
验证输入
更新状态
结束
【练习】用户注册流程中的年龄验证:
python
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
# 人工验证
class State(TypedDict):
age: int | None
def get_age_node(state: State):
"""循环获取用户年龄,直到正确"""
prompt="请输入年龄:"
while True:
age=interrupt(prompt) # 注意恢复时会从头开始执行
if isinstance(age,int) and age>0:
return {"age":age}
else:
prompt="重新输入:"
builder=StateGraph(State)
builder.add_node(get_age_node)
builder.add_edge(START, "get_age_node")
builder.add_edge("get_age_node", END)
graph=builder.compile(checkpointer=InMemorySaver())
config={"configurable": {"thread_id": "thread_5"}}
print(graph.invoke({"age": None}, config=config))
print(graph.invoke(
Command(resume=-12345),
config=config
))
print(graph.invoke(
Command(resume=12345),
config=config
))
输出结果:
plain
{'age': None, '__interrupt__': [Interrupt(value='请输入年龄:', id='101a5717686429d8fe4640b97285c29d')]}
{'age': None, '__interrupt__': [Interrupt(value='重新输入:', id='101a5717686429d8fe4640b97285c29d')]}
{'age': 12345}
1.3 时间旅行(Time Travel)
1.3.1 时间旅行是什么?
AI工作流具有非确定性:大语言模型每次运行可能产生不同结果。且复杂任务需要多个AI调用协同完成时,错误可能出现在任何步骤,难以定位。其实这里就是之前重放功能的实现,但重放是langgraph提供的系统级功能,这里的时间机器则是我们实现的特有功能。
LangGraph的工作方式:每个节点执行后都会自动"存档"。时间旅行允许用户重放先前的执行以查看或调试特定的步骤。
#mermaid-svg-zYjwNX2YTT7HxKn0{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-zYjwNX2YTT7HxKn0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zYjwNX2YTT7HxKn0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zYjwNX2YTT7HxKn0 .error-icon{fill:#552222;}#mermaid-svg-zYjwNX2YTT7HxKn0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zYjwNX2YTT7HxKn0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zYjwNX2YTT7HxKn0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zYjwNX2YTT7HxKn0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zYjwNX2YTT7HxKn0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zYjwNX2YTT7HxKn0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zYjwNX2YTT7HxKn0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zYjwNX2YTT7HxKn0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zYjwNX2YTT7HxKn0 .marker.cross{stroke:#333333;}#mermaid-svg-zYjwNX2YTT7HxKn0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zYjwNX2YTT7HxKn0 p{margin:0;}#mermaid-svg-zYjwNX2YTT7HxKn0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zYjwNX2YTT7HxKn0 .cluster-label text{fill:#333;}#mermaid-svg-zYjwNX2YTT7HxKn0 .cluster-label span{color:#333;}#mermaid-svg-zYjwNX2YTT7HxKn0 .cluster-label span p{background-color:transparent;}#mermaid-svg-zYjwNX2YTT7HxKn0 .label text,#mermaid-svg-zYjwNX2YTT7HxKn0 span{fill:#333;color:#333;}#mermaid-svg-zYjwNX2YTT7HxKn0 .node rect,#mermaid-svg-zYjwNX2YTT7HxKn0 .node circle,#mermaid-svg-zYjwNX2YTT7HxKn0 .node ellipse,#mermaid-svg-zYjwNX2YTT7HxKn0 .node polygon,#mermaid-svg-zYjwNX2YTT7HxKn0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zYjwNX2YTT7HxKn0 .rough-node .label text,#mermaid-svg-zYjwNX2YTT7HxKn0 .node .label text,#mermaid-svg-zYjwNX2YTT7HxKn0 .image-shape .label,#mermaid-svg-zYjwNX2YTT7HxKn0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zYjwNX2YTT7HxKn0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zYjwNX2YTT7HxKn0 .rough-node .label,#mermaid-svg-zYjwNX2YTT7HxKn0 .node .label,#mermaid-svg-zYjwNX2YTT7HxKn0 .image-shape .label,#mermaid-svg-zYjwNX2YTT7HxKn0 .icon-shape .label{text-align:center;}#mermaid-svg-zYjwNX2YTT7HxKn0 .node.clickable{cursor:pointer;}#mermaid-svg-zYjwNX2YTT7HxKn0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zYjwNX2YTT7HxKn0 .arrowheadPath{fill:#333333;}#mermaid-svg-zYjwNX2YTT7HxKn0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zYjwNX2YTT7HxKn0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zYjwNX2YTT7HxKn0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zYjwNX2YTT7HxKn0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zYjwNX2YTT7HxKn0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zYjwNX2YTT7HxKn0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zYjwNX2YTT7HxKn0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zYjwNX2YTT7HxKn0 .cluster text{fill:#333;}#mermaid-svg-zYjwNX2YTT7HxKn0 .cluster span{color:#333;}#mermaid-svg-zYjwNX2YTT7HxKn0 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-zYjwNX2YTT7HxKn0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zYjwNX2YTT7HxKn0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zYjwNX2YTT7HxKn0 .icon-shape,#mermaid-svg-zYjwNX2YTT7HxKn0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zYjwNX2YTT7HxKn0 .icon-shape p,#mermaid-svg-zYjwNX2YTT7HxKn0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zYjwNX2YTT7HxKn0 .icon-shape .label rect,#mermaid-svg-zYjwNX2YTT7HxKn0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zYjwNX2YTT7HxKn0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zYjwNX2YTT7HxKn0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zYjwNX2YTT7HxKn0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
节点1(检查点)
节点2(检查点)
结束
可读取/更新、可恢复执行
可读取/更新、可恢复执行
这个能力会很有用:
- 分析推理过程:理解AI如何得出最终结果,学习成功的决策路径。(看看AI是怎么"想"出好答案的)
- 定位和修复错误:精确找到错误发生的节点,测试修复方案而不影响原始流程。(找出AI在哪一步"想歪了")
- 探索替代方案:尝试不同的输入或中间状态,比较不同路径的效果。(试试不同的选择会不会更好)
1.3.2 时间旅行四步法
1.3.2.1 第一步:初始执行工作流
示例如下:
python
# 编译需要checkpointer
graph = workflow.compile(checkpointer=InMemorySaver())
# 创建执行线程uuid
import uuid
config = {
"configurable": {
"thread_id": uuid.uuid4(), # 唯一线程标识
}
}
# 执行工作流
state = graph.invoke({}, config)
1.3.2.2 第二步:查看历史检查点
示例如下:
python
# 获取所有历史状态(按时间倒序)
states = list(graph.get_state_history(config))
for state in states:
print(f"检查点ID: {state.config['configurable']['checkpoint_id']}")
print(f"下一步节点: {state.next}")
print(f"当前状态: {state.values}")
print("-" * 50)
# 输出示例:
输出示例:
# 检查点ID: 1f0d4d2b-bdc2-6f06-8902-9b67bbd1b867
# 下一步节点: ()
# 当前状态: {'...': '...'}
# 检查点ID: 1f0d4d2b-9596-6fb8-8901-6af9cdc2fea0
# 下一步节点: ('write_joke',)
# 当前状态: {'...': '...'}
# 检查点ID: 1f0d4d2b-7a6e-6d7d-8900-5cc1931477df
# 下一步节点: ('generate_topic',)
# 当前状态: {}
# 检查点ID: 1f0d4d2b-643e-bafc-bfff-7f054fa556c8
# 下一步节点: ('__start__',)
# 当前状态: {}
1.3.2.3 第三步:修改状态(可选)
update_state更新状态(会创建新的检查点分支)- 原始检查点保持不变
- 新分支可以独立发展
示例如下:
python
# 选择特定检查点
selected_state = states[1] # 写笑话之前的检查点
# 修改状态数据
new_config = graph.update_state(
selected_state.config, # 原始配置
values={"topic": "程序员"} # 修改主题
)
1.3.2.4 第四步:从检查点恢复执行
- 输入为 None,因为状态已在检查点中
- 配置必须包含有效的
checkpoint_id:通过指定thread_id和checkpoint_id来调用图,可以从历史某个检查点开始重放执行,用于调试或探索不同路径。 - 执行从指定检查点继续,生成新的历史分支
示例如下:
python
# 从修改后的检查点继续执行
result = graph.invoke(None, new_config) # 输入为None,因为状态已存在
print(result["joke"]) # 输出关于程序员的新笑话
1.3.3 完整示例
我们要创建一个生成笑话的系统:
- 第一步:想一个主题
- 第二步:根据主题写笑话
1.3.3.1 状态类型定义
python
from typing_extensions import TypedDict, NotRequired
class State(TypedDict):
joke: str
topic : str
1.3.3.2 节点函数实现
python
model = ChatOpenAI(
model="gpt-5.4",
api_key=os.getenv("PACKYAPI_API_KEY"),
base_url="https://www.packyapi.com/v1/",
use_responses_api=True,
output_version="responses/v1",
)
def generate_topic(state: State):
"""生成笑话主题"""
return {
"topic": model.invoke([
SystemMessage(content="你是一个笑话生成专家。"),
HumanMessage(content="请帮我生成一个笑话的主题,字数控制在十个字以内")
])
}
def generate_joke(state: State):
"""生成笑话"""
return {
"joke": model.invoke([
SystemMessage(content="你是一个笑话生成专家。"),
HumanMessage(content=f"请帮我生成一个关于{state['topic']}的笑话")
])
}
1.3.3.3 工作流构建
python
builder=StateGraph(State)
builder.add_sequence([generate_topic, generate_joke])
builder.add_edge(START,"generate_topic")
builder.add_edge("generate_joke",END)
graph=builder.compile(checkpointer=InMemorySaver())
执行工作流:
python
config = {"configurable": {"thread_id": "1"}}
result = graph.invoke({}, config)
print(result["joke"])
4.3.3.4 时间旅行调试过程
-
发现问题:最终笑话主题太宽泛
-
回溯分析:
python
# 查看所有检查点
states = list(graph.get_state_history(config))
# 检查主题生成节点后的状态
topic_state = states[1] # generate_topic之后的检查点
print("AI生成的主题:", topic_state.values["topic"])
- 修改测试:
python
# 修改为更具体的主题
new_config = graph.update_state(
topic_state.config,
values={"topic": "程序员调试代码时的趣事"}
)
# 重新执行
new_result = graph.invoke(None, new_config)
print("改进后的笑话:", new_result["joke"])
5. 持久化小结
在LangGraph中,持久化能力简介:
- 自动化:在使用LangGraph时,持久化基础设施(检查点和存储)是自动处理的,无需手动配置。
- 多种后端:提供多种检查点存储后端,包括内存(InMemorySaver,用于开发测试)、Postgres(PostgresSaver,用于生产)、内存存储等。
基于上述技术,LangGraph支持以下功能:
| 功能 | 说明 |
|---|---|
| 状态查询 | graph.get_state(config):获取图的最新状态 graph.get_state_history(config):获取线程的完整历史(所有检查点),按时间倒序排列,可回溯到过程中的任意状态 |
| 时间旅行与重放 | 通过指定 thread_id 和 checkpoint_id 来调用图,可以从历史某个检查点开始重放执行,用于调试或探索不同路径 |
| 状态编辑 | 允许在执行中修改状态的内容(比如让审查修改后再继续)。修改会通过graph.update_state(config, values) 来直接修改线程的当前状态。 |
| 人机交互 | 允许在状态执行中暂停,让人(如审核)修改状态后再继续。 |
| 记忆 | 短期记忆:完整的交互历史,实现上下文对话的记忆。 当前对话记录:整条对话链中的上下文信息 长期记忆:跨会话保存的用户或应用数据 |
2. LangGraph其他核心能力
2.1 运行时上下文(Runtime context)
2.1.1 什么是运行时上下文?
2.1.1.1 上下文定义与分类
上下文(Context)是程序运行时可访问的数据和环境信息。在LangGraph中,上下文用于传递:
- 用户身份、配置参数
- 数据库连接、API 密钥
- 会话状态、历史记录等
上下文可按两个维度分类:
| 维度 | 类型 | 描述 | 示例 |
|---|---|---|---|
| 可变性 | 静态上下文 | 运行中不变的数据 | 用户ID、数据库连接 |
| 可变性 | 动态上下文 | 运行中会变化的数据 | 对话记录、中间结果 |
| 生命周期 | 运行时上下文 | 单次运行/线程有效 | 当前请求的临时数据 |
| 生命周期 | 跨会话上下文 | 多次会话持久化 | 用户偏好、历史记录 |
因此,在LangGraph中,包含三种上下文:
| 类型 | 可变性 | 生命周期 | 访问方式 |
|---|---|---|---|
| 静态运行时上下文 | 静态 | 单次运行/线程 | context 参数传入 |
| 动态运行时上下文 | 动态 | 单次运行 | 图状态对象 |
| 动态跨会话上下文 | 动态 | 跨会话 | 存储(Store) |
checkpoints 和 store ,则分别代表 动态运行时上下文 和 动态跨会话上下文 。接下来为 静态运行时上下文 的使用方式。
2.1.1.2 场景练习
做个小练习:根据具体场景,分析各种数据如何保存。例如需要开发一个 智能旅行规划助手 ,该助手需要:
- 根据用户的母语提供个性化回答
- 根据用户的会员等级提供不同服务
- 连接到旅游数据库查询信息
- 根据季节推荐不同的活动
- 记住用户的历史查询,提供更精准的建议
下面是这些信息的类型:
| 数据类型 | 上下文类型 | 为什么 |
|---|---|---|
| 数据库连接 | 静态运行时上下文 | 每次查询都需要,单次运行中不变 |
| 用户语言偏好 | 静态运行时上下文 | 个性化回答,单次运行中不变 |
| 用户会员等级 | 静态运行时上下文 | 服务分级,单次运行中不变 |
| 当前季节 | 静态运行时上下文 | 推荐季节性活动,单次运行中不变 |
| 对话历史 | 动态运行时上下文 | 了解上下文,单次运行中变化 |
| 用户旅行偏好 | 跨会话上下文 | 长期记忆,跨会话 |
2.1.2 配置运行时上下文
2.1.2.1 定义上下文模式
首先需要定义一个上下文的数据结构(通常用 dataclass 或 TypedDict)。下面我们演示一个实际示例,在该参数配置了三个参数:用户ID、LLM和系统消息,以便在运行时使用。
python
from dataclasses import dataclass
@dataclass
class ContextSchema:
user_id: str
model_provider: str = "openai" # 默认值
system_message: str = "你是一个乐于助人的助手。"
2.1.2.2 在图中使用上下文模式
创建图时传入 context_schema 参数。
python
from langgraph.graph import StateGraph
builder = StateGraph(
State, # 状态模式
context_schema=ContextSchema # 添加上下文模式
)
2.1.2.3 在节点中访问上下文
节点函数可通过 runtime 参数访问上下文。
python
from langgraph.runtime import Runtime
def my_node(state: State, runtime: Runtime[ContextSchema]):
user_id = runtime.context.user_id
model_provider = runtime.context.model_provider
if model_provider == "openai":
# 使用 OpenAI 模型
pass
elif model_provider == "anthropic":
# 使用 Anthropic 模型
pass
return {"result": f"用户{user_id}处理完成"}
运行图时传入上下文
python
graph.invoke(
{"input": "Hello"},
context={
"user_id": "user123",
"model_provider": "anthropic",
"system_message": "请用中文回答"
}
)
【完整示例】
python
from dataclasses import dataclass
from langgraph.runtime import Runtime
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict
from langchain.messages import AnyMessage
# 静态运行上下文
@dataclass
class Contextschema:
user_id: int
language: str = "en"
# 动态运行上下文
class State(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
user_name: str
# 默认为动态运行上下文,使用runtime来定义静态运行上下文
def node(state: State,runtime:Runtime[Contextschema]):
if runtime.context.language=="en":
greeting = "hello"
else:
greeting = "你好"
user_name=state.get("user_name","Guest")
return {
"messages": [SystemMessage(content=f"{greeting}, {user_name}!")]
}
# 指定动态或静态运行上下文
builder=StateGraph(State,context_schema=Contextschema)
builder.add_node(node)
builder.add_edge(START,"node")
builder.add_edge("node",END)
graph=builder.compile(checkpointer=InMemorySaver(),store=InMemoryStore())
print(graph.invoke({"user_name":"小明"},
context={"user_id": 123, "language": "zh"}, # 静态运行上下文必须指明
config={"configurable": {"thread_id": "1"}}))
2.1.2.4 在工具中访问上下文
工具是调用外部系统、API、数据库交互或执行计算的功能。因此,对于用户身份、配置参数、数据库连接、API密钥等这类调用API的参数信息和配置信息,则需要传递给工具。上下文对工具的重要性:
- 个性化响应:根据用户上下文提供定制化回答
- 权限控制:基于用户身份限制工具访问
- 状态感知:工具可以根据当前状态决定行为
- 依赖注入:避免硬编码配置,提高可测试性
基本用法
工具可以通过 ToolRuntime 参数访问运行时信息。这个参数,为工具提供包括:
State:图状态数据Context:静态上下文Store:持久化存储等
使用 ToolRuntime 时,只需在工具签名中添加 runtime: ToolRuntime,它会自动注入。调用时,无需手动传输。
定义一个带有运行时信息的工具如下所示:
python
from langchain.tools import tool, ToolRuntime
@tool
def get_user_info(runtime: ToolRuntime) -> str:
"""获取当前用户的信息"""
user_id = runtime.context.user_id # 访问上下文
user_state = runtime.state["user_name"] # 访问状态
return f"User {user_id}, state: {user_state}"
【完整示例】
构建一个支持搜索的AI系统,假设调用搜索 API 需要用户数据作为参数,则需要向工具中传入相关信息。关键步骤如下:
- 定义状态、上下文结构
- 定义工具节点(ToolNode)、定义LLM节点
- 构建并编译图,需加入状态和上下文参数
- 执行并验证结果
python
from dataclasses import dataclass
from langchain.chat_models import init_chat_model
from langchain.messages import SystemMessage, HumanMessage
from langchain.tools import tool, ToolRuntime
from langchain.messages import AnyMessage
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict, Annotated
import operator
# 在工具中使用
class State(MessagesState):
user_name: str
@dataclass
class Context:
user_id: int
@tool
def search(runtime:ToolRuntime[Context]):
"""用来搜索天气的得力工具"""
user_id=runtime.context.user_id
user_name=runtime.state["user_name"] # 动态上下文也可以获取,toolruntime相当于全部的上下文
print(f"日志记录,user_id:{user_id}, user_name:{user_name}")
return f"user_id:{user_id}, user_name:{user_name}, 天气已经查询完毕"
model = ChatOpenAI(
model="gpt-5.4",
api_key=os.getenv("PACKYAPI_API_KEY"),
base_url="https://www.packyapi.com/v1/",
use_responses_api=True,
output_version="responses/v1",
)
model_with_tool=model.bind_tools([search])
def llm_call(state:State):
return {"messages": [model_with_tool.invoke([SystemMessage(content="你支持调用工具去查询天气")] + state["messages"])]}
builder=StateGraph(State,context_schema=Context)
builder.add_node(llm_call)
builder.add_node("tool_node",ToolNode([search]))
builder.add_edge(START,"llm_call")
builder.add_conditional_edges(
"llm_call",
tools_condition,
{
"tools":"tool_node",
"__end__":END,
}
)
builder.add_edge("tool_node","llm_call")
graph=builder.compile(checkpointer=InMemorySaver(),store=InMemoryStore())
print(graph.invoke({
"messages": [HumanMessage(content="今天的天气怎么样?")],
"user_name": "小明"},
context={"user_id": 123},
config={"configurable": {"thread_id": "2"}}
))
打印结果如下:
日志记录,user_id:123, user_name:小明
{'messages':
[HumanMessage(content='今天的天气怎么样?', additional_kwargs={}, response_metadata={}, id='253feede-f7e0-484e-bbf3-8ddaef7ac965'),
AIMessage(content=[{'arguments': '{}', 'call_id': 'call_IJUaKVy7EfiaU3Z1Id9T6pqP', 'name': 'search', 'type': 'function_call', 'id': 'fc_0b4a75819535c05a016a5f136d4c9c8195b15c51560150c5b8', 'status': 'completed'}], additional_kwargs={}, response_metadata={'id': 'resp_0b4a75819535c05a016a5f136c340881959854754c52b4fc2f', 'created_at': 1784615788.0, 'metadata': {}, 'model': 'gpt-5.4', 'object': 'response', 'service_tier': 'default', 'status': 'completed', 'model_provider': 'openai', 'model_name': 'gpt-5.4'}, id='resp_0b4a75819535c05a016a5f136c340881959854754c52b4fc2f', tool_calls=[{'name': 'search', 'args': {}, 'id': 'call_IJUaKVy7EfiaU3Z1Id9T6pqP', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 6663, 'output_tokens': 20, 'total_tokens': 6682, 'input_token_details': {'cache_creation': 0, 'cache_read': 6144}, 'output_token_details': {'reasoning': 0}}),
ToolMessage(content='user_id:123, user_name:小明, 天气已经查询完毕', name='search', id='a6b9f33e-955a-4950-aed2-650b168aec74', tool_call_id='call_IJUaKVy7EfiaU3Z1Id9T6pqP'),
AIMessage(content=[{'type': 'text', 'text': '我查到了,但这个工具返回里没有带具体天气内容,只显示"天气已经查询完毕"。\n\n你可以直接告诉我你所在的城市,我再按城市帮你查一次。或者你也可以直接说:\n`查询北京今天天气`\n`查询上海天气`', 'annotations': [], 'id': 'msg_0b4a75819535c05a016a5f137378948195bbc0f1a8ab706045', 'phase': 'final_answer'}], additional_kwargs={}, response_metadata={'id': 'resp_0b4a75819535c05a016a5f1372474c819591bca91ad5585b83', 'created_at': 1784615794.0, 'metadata': {},
'model': 'gpt-5.4', 'object': 'response', 'service_tier': 'default', 'status': 'completed', 'model_provider': 'openai', 'model_name': 'gpt-5.4'}, id='resp_0b4a75819535c05a016a5f1372474c819591bca91ad5585b83', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 6723, 'output_tokens': 94, 'total_tokens': 6818, 'input_token_details': {'cache_creation': 0, 'cache_read': 0}, 'output_token_details': {'reasoning': 0}})], 'user_name': '小明'}
2.2 流(Streaming)
2.2.1 概念
在LangChain中,流式传输用来逐步输出数据,无需等待全部处理完成。这可以提升用户体验,减少等待感,尤其适用于大语言模型(LLM)这类延迟较高的任务。就像看电影时,画面一帧帧播放,而不是等全部下载完再看。
在LangGraph中,流式处理可将图运行的实时数据反馈显示到应用程序中,如:状态、LLM生成的文本、自定义数据等。且支持多种流模式。
2.2.2 五种流模式
LangGraph 支持以下五种流模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| values | 流式输出完整状态 | 需要知道每一步的完整状态 |
| updates | 流式输出状态变化 | 关注每一步更新了哪些字段 |
| messages | 流式输出 LLM 生成的 token | 实时展示 LLM 生成内容 |
| custom | 流式输出自定义数据 | 自定义进度条、日志等 |
| debug | 输出所有调试信息 | 开发调试阶段 |
将一种或多种流模式作为列表传递给 stream 或 astream 方法是其使用姿势。
2.2.3 流式输出状态值
python
from langgraph.graph import StateGraph, START
# 定义状态结构
class State(dict):
topic: str
joke: str
# 创建节点函数
def refine_topic(state):
return {"topic": state["topic"] + "和猫"}
def generate_joke(state):
return {"joke": f"这是一个关于{state['topic']}的笑话"}
# 构建图
graph = (
StateGraph(State)
.add_node(refine_topic)
.add_node(generate_joke)
.add_edge(START, "refine_topic")
.add_edge("refine_topic", "generate_joke")
.compile()
)
# 流式输出状态更新
for chunk in graph.stream(
{"topic": "冰激凌"},
stream_mode="updates" # 只看更新部分
):
print(chunk)
for chunk in graph.stream(
{"topic": "冰激凌"},
stream_mode="values" # 每一步的完整状态
):
print(chunk)
stream_mode="updates" 时输出:
python
1 {'refine_topic': {'topic': '冰激凌和猫'}}
2 {'generate_joke': {'joke': '这是一个关于冰激凌和猫的笑话'}}
stream_mode="values" 时输出:
python
1 {'topic': '冰激凌'}
2 {'topic': '冰激凌和猫'}
3 {'topic': '冰激凌和猫', 'joke': '这是一个关于冰激凌和猫的笑话'}
2.2.4 流式传输自定义数据
LangGraph 不仅支持输出状态这类的的数据,还支持从节点或工具中输出用户自定义的数据。步骤如下:
- 使用
get_stream_writer()访问流编写器并发出自定义数据。 - 调用
.stream()或.astream()时设置stream_mode="custom"以获取流中的自定义数据。还可以组合多种模式(如["updates", "custom"]),但至少必须有一个是"custom"。
2.4.1 基本用法
2.4.1.1 从节点和工具中输出用户自定义数据
在这里改造前面写过的代码:
python
from dataclasses import dataclass
from langchain.chat_models import init_chat_model
from langchain.messages import SystemMessage, HumanMessage
from langchain.tools import tool, ToolRuntime
from langgraph.config import get_stream_writer
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict, Annotated
import operator
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
user_name: str = ""
@dataclass
class Context:
user_id: str
@tool
def search(runtime: ToolRuntime[Context]) -> str:
"""调用搜索工具"""
user_id = runtime.context.user_id # 访问上下文
user_name = runtime.state["user_name"] # 访问状态
# 获取流式写入器
writer = get_stream_writer()
# 发送开始信号
writer({
"type": "search_tool",
"status": "start",
"user_id": user_id,
"user_name": user_name
})
# 模拟搜索过程
writer({
"type": "search_tool",
"status": "searching",
"user_id": user_id,
"user_name": user_name
})
# 模拟处理时间
import time
time.sleep(2)
# 结束
writer({
"type": "search_tool",
"status": "end",
"user_id": user_id,
"user_name": user_name
})
return f"查询天气: 晴天, 15-20度" # 模拟调用
# 绑定工具
model_with_tools = init_chat_model("gpt-4o-mini", temperature=0).bind_tools([search])
def llm_call(state: dict):
"""LLM决定是否调用工具"""
# 获取流式写入器
writer = get_stream_writer()
# 发送开始处理的信号
writer({
"type": "llm_call",
"status": "start",
"message": "开始调用LLM",
"content": state["messages"][-1].content
})
result = model_with_tools.invoke(
[SystemMessage(content="你是一个乐于助人的助手,支持调用工具进行搜索。")]
+ state["messages"]
)
# 调用结束
writer({
"type": "llm_call",
"status": "end",
"message": "调用LLM完成"
})
return {"messages": [result]}
# 定义并编译图
builder = StateGraph(MessagesState, context_schema=Context)
builder.add_node("llm_call", llm_call)
builder.add_node("tool_node", ToolNode([search]))
builder.add_edge(START, "llm_call")
builder.add_conditional_edges(
"llm_call",
tools_condition,
{
"tools": "tool_node", # 将条件输出转换为图中的节点
"__end__": END,
},
)
builder.add_edge("tool_node", "llm_call")
graph = builder.compile()
for chunk in graph.stream(
{
"messages": [HumanMessage(content="今天西安的天气如何?")],
"user_name": "小明"
},
context={"user_id": "123"},
stream_mode="custom"
):
print(chunk)
打印结果:
python
{'custom': {'type': 'llm_call', 'status': 'start', 'message': '开始调用LLM', 'content': '今天西安的天气如何?'}}
{'custom': {'type': 'llm_call', 'status': 'end', 'message': '调用LLM完成'}}
{'custom': {'type': 'search_tool', 'status': 'start', 'user_id': '123', 'user_name': '小明'}}
{'custom': {'type': 'search_tool', 'status': 'searching', 'user_id': '123', 'user_name': '小明'}}
{'custom': {'type': 'search_tool', 'status': 'end', 'user_id': '123', 'user_name': '小明'}}
{'custom': {'type': 'llm_call', 'status': 'start', 'message': '开始调用LLM', 'content': '查询天气: 晴天, 15-20度'}}
{'custom': {'type': 'llm_call', 'status': 'end', 'message': '调用LLM完成'}}
2.4.1.2 设置多种传输模式
还可以组合多种模式(如 ["updates", "custom"]),但至少必须有一个是 "custom"。如下所示:
python
for chunk in graph.stream(
{
"messages": [HumanMessage(content="今天西安的天气如何?")],
"user_name": "小明"
},
context={"user_id": "123"},
stream_mode=["custom", "updates"]
):
print(chunk)
打印结果:
python
1 {'custom': {'type': 'llm_call', 'status': 'start', 'message': '开始调用LLM', 'content': '今天西安的天气如何?'}}
2 {'custom': {'type': 'llm_call', 'status': 'end', 'message': '调用LLM完成'}}
3 {'updates': {'llm_call': {'messages': [AIMessage(content='', additional_kwargs={'refusal': None, 'tool_calls': [{'id': 'call_csl1wboe1awargs=0', 'function': {'name': 'search', 'arguments': '{}'}, 'type': 'function'}], 'logprobs': None, 'accepted_prediction_tokens': 67, 'completion_tokens': 0, 'prompt_tokens': 83, 'total_tokens': 83, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_ea9d2c6db', 'tool_calls': [{'name': 'search', 'args': {}, 'id': 'call_YU9082F9JBdiz3J7CMmJULYNE'}], 'type': 'ai'}]}}}
4 {'custom': {'type': 'search_tool', 'status': 'start', 'user_id': '123', 'user_name': '小明'}}
5 {'custom': {'type': 'search_tool', 'status': 'searching', 'user_id': '123', 'user_name': '小明'}}
6 {'updates': {'tool_node': {'messages': [ToolMessage(content='查询天气: 晴天, 15-20度', 'name': 'search', 'tool_call_id': 'call_csl1wboe1awargs=0')]}}}
7 {'custom': {'type': 'search_tool', 'status': 'end', 'user_id': '123', 'user_name': '小明'}}
8 {'custom': {'type': 'llm_call', 'status': 'start', 'message': '开始调用LLM'}}
9 {'updates': {'llm_call': {'messages': [AIMessage(content='今天西安的天气是晴天,气温在15到20度之间。', additional_kwargs={'refusal': None, 'tool_calls': None, 'logprobs': None, 'completion_tokens': 20, 'prompt_tokens': 83, 'total_tokens': 103, 'reasoning_tokens': 0, 'accepted_prediction_tokens': None, 'rejected_prediction_tokens': None}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_ea9d2c6db', 'tool_calls': [], 'type': 'ai'}]}}}
10 {'custom': {'type': 'llm_call', 'status': 'end', 'message': '调用LLM完成'}}
可以看到LangGraph运行将一种或多种流模式作为列表传递给 stream 或 astream。
2.4.2 应用场景
2.4.2.1 创建自定义监控面板
那么自定义流式数据到底有什么用?改造一下代码。
完整代码如下:
python
from dataclasses import dataclass
from langchain.chat_models import init_chat_model
from langchain.messages import SystemMessage, HumanMessage
from langchain.tools import tool, ToolRuntime
from langchain.messages import AnyMessage
from langgraph.config import get_stream_writer
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict, Annotated
import operator
# 在工具中使用
class State(MessagesState):
user_name: str
@dataclass
class Context:
user_id: int
@tool
def search(runtime:ToolRuntime[Context]):
"""用来搜索天气的得力工具"""
user_id=runtime.context.user_id
user_name=runtime.state["user_name"] # 动态上下文也可以获取,toolruntime相当于全部的上下文
print(f"日志记录,user_id:{user_id}, user_name:{user_name}")
# 获取流式写入器:
writer=get_stream_writer() # 先获取写入器
writer(
{
"type":"search_tool",
"status":"start",
"user_name":user_name,
"user_id":user_id
}
)
search_steps = [
{"name": "搜索1", "time": 1, "result": "晴天,"},
{"name": "搜索2", "time": 2, "result": "15-20度"},
]
all_result = "查询天气:"
for i, step in enumerate(search_steps, 1):
writer(
{
"type":"search_tool",
"status":"searching",
"step": step["name"],
"all_step": len(search_steps),
"cur_step": i,
"user_id":user_id,
"user_name":user_name
}
)
time.sleep(step["time"])
all_result += step["result"]
writer(
{
"type":"search_tool",
"status":"end",
"user_name":user_name,
"user_id":user_id,
"result": all_result
}
)
return all_result
model = ChatOpenAI(
model="gpt-5.4",
api_key=os.getenv("PACKYAPI_API_KEY"),
base_url="https://www.packyapi.com/v1/",
use_responses_api=True,
output_version="responses/v1",
)
model_with_tool=model.bind_tools([search])
def llm_call(state:State):
writer = get_stream_writer() # 先获取写入器
writer(
{
"type": "llm_call",
"status": "start",
"message": "开始调用LLM",
"content": state["messages"][-1].content,
}
)
result = model_with_tool.invoke([SystemMessage(content="你支持调用工具去查询天气")] + state["messages"])
writer(
{
"type": "llm_call",
"status": "end",
"message": "调用LLM完成",
}
)
return {"messages": [result]}
builder=StateGraph(State,context_schema=Context)
builder.add_node(llm_call)
builder.add_node("tool_node",ToolNode([search]))
builder.add_edge(START,"llm_call")
builder.add_conditional_edges(
"llm_call",
tools_condition,
{
"tools":"tool_node",
"__end__":END,
}
)
builder.add_edge("tool_node","llm_call")
graph=builder.compile(checkpointer=InMemorySaver(),store=InMemoryStore())
# print(graph.invoke({
# "messages": [HumanMessage(content="今天的天气怎么样?")],
# "user_name": "小明"},
# context={"user_id": 123},
# config={"configurable": {"thread_id": "2"}}
# ))
# 这里演示自定义信息输出流
for chunk in graph.stream({
"messages": [HumanMessage(content="今天的天气怎么样?")],
"user_name": "小明"},
context={"user_id": 123},
config={"configurable": {"thread_id": "2"}},
stream_mode=["updates", "custom"] # 还可以组合多种模式(如 ["updates", "custom"]),但至少必须有一个是"custom"
):
if isinstance(chunk, tuple) and len(chunk) == 2:
mode, data = chunk
if mode == "custom":
if data.get("type") == "search_tool":
status = data["status"]
if status == "start":
print(f"用户ID:{data['user_id']}, 用户名:{data['user_name']}开始调用工具...")
elif status == "searching":
print(f"[{data['cur_step']}/{data['all_step']}] 正在处理:{data['step']}")
elif status == "end":
print(f"调用完成!结果:{data['result']}")
elif data.get("type") == "llm_call":
pass
elif mode == "updates":
pass
elif isinstance(chunk, dict):
if chunk.get("custom"):
info = chunk["custom"]
if info.get("type") == "search_tool":
status = info["status"]
if status == "start":
print(f"用户ID:{info['user_id']}, 用户名:{info['user_name']}开始调用工具...")
elif status == "searching":
print(f"[{info['cur_step']}/{info['all_step']}] 正在处理:{info['step']}")
elif status == "end":
print(f"调用完成!结果:{info['result']}")
elif info.get("type") == "llm_call":
pass
elif chunk.get("updates"):
pass
下面是运行结果:
plain
日志记录,user_id:123, user_name:小明
用户ID:123, 用户名:小明开始调用工具...
[1/2] 正在处理:搜索1
[2/2] 正在处理:搜索2
调用完成!结果:查询天气:晴天,15-20度
因此,自定义流式数据的用法,可以做到:
- 实时进度反馈:在长时间处理任务中显示进度
- 调试信息输出:输出中间计算结果
- 多源数据整合:同时流式输出不同类型的数据
- 自定义监控:创建自定义的监控面板
2.2.5 流式传输 LLM tokens
Token 是大语言模型处理文本的基本单位。因此 LangGraph 可以:
- 使用
stream_mode="messages"模式可以从 graph 的任何部分(包括节点、工具等)逐Token 流式传输 LLM 输出。 - 输出格式为
(message_chunk, metadata)元组。
2.2.5.1 基本用法
python
from typing import TypedDict
from langgraph.graph import StateGraph, START
from langchain_openai import ChatOpenAI
# 定义状态
class State(TypedDict):
input: str
output: str
# 初始化模型
model = ChatOpenAI(model="gpt-4o-mini")
def llm_node(state: State):
"""生成答案的节点"""
return {"output": model.invoke([
{"role": "system", "content": "你是一个乐于助人的助手。"},
{"role": "user", "content": state["input"]}
])}
# 构建图
builder = StateGraph(State)
builder.add_node("llm_node", llm_node)
builder.add_edge(START, "llm_node")
graph = builder.compile()
# 流式输出 LLM Tokens
# 输出格式为 (message_chunk, metadata) 元组。
for token_chunk, metadata in graph.stream(
{"input": "请解释什么是机器学习?"},
stream_mode="messages"
):
if token_chunk.content:
# 逐 Token 输出
print(token_chunk.content, end="", flush=True)
2.2.5.2 高级功能
2.2.5.2.1 按 Tags 过滤 Tokens
还可以将 tags 与 LLM 调用相关联,以按 LLM 调用筛选流式令牌。
python
from typing import TypedDict
from langgraph.graph import StateGraph, START
from langchain_openai import ChatOpenAI
# 初始化带标签的模型
joke_model = ChatOpenAI(
model="gpt-5.4",
api_key=os.getenv("PACKYAPI_API_KEY"),
base_url="https://www.packyapi.com/v1/",
use_responses_api=True,
output_version="responses/v1",
).with_config(tags=["joke"])# 给模型添加标签
poem_model = ChatOpenAI(
model="gpt-5.5",
api_key=os.getenv("PACKYAPI_API_KEY"),
base_url="https://www.packyapi.com/v1/",
use_responses_api=True,
output_version="responses/v1",
).with_config(tags=["poem"])# 给模型添加标签
class CreativeState(TypedDict):
topic: str
joke: str
poem: str
# 由于中转站与官方的api的返回格式不同,这里需要做一下处理
def extract_text(content):
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
parts.append(block.get("text", ""))
return "".join(parts)
return ""
def generate_creative_content(state: CreativeState):
"""同时生成笑话和诗歌"""
topic = state["topic"]
# 生成笑话
print(f"\n生成关于 {topic} 的笑话: ")
joke_response = joke_model.invoke([
{"role": "user", "content": f"讲一个关于 {topic} 的笑话"}
])
# 生成诗歌
print(f"\n生成关于 {topic} 的诗歌: ")
poem_response = poem_model.invoke([
{"role": "user", "content": f"写一首关于 {topic} 的短诗"}
])
return {
"joke": joke_response.content,
"poem": poem_response.content
}
# 构建图
builder = StateGraph(CreativeState)
builder.add_node("creative", generate_creative_content)
builder.add_edge(START, "creative")
graph = builder.compile()
# 流式输出并过滤
for token_chunk, metadata in graph.stream(
{"topic": "猫"},
stream_mode="messages"
):
# 只输出笑话相关的 Tokens
tags = metadata.get("tags", [])
if "joke" in tags:
print(extract_text(token_chunk.content), end="", flush=True)
# 也可以过滤诗歌
# if "poem" in tags:
# print(token_chunk.content, end="", flush=True)
输出结果如下;
plain
生成关于 猫 的笑话:
有一只猫去应聘,老板问它有什么特长。
猫说:"我会抓老鼠,还会写代码。"
老板不信:"那你现场写一段看看。"
猫抬起爪子就在键盘上一顿拍,屏幕上出现一行:
`catch (Mouse e) {}`
老板沉默了两秒,说:"行,明天来上班,记得别抓产品经理。"
生成关于 猫 的诗歌:
2.2.5.2.2 按节点名称过滤
可以指定特定节点流式传输Tokens,需按流式传输元数据中的 langgraph_node 字段筛选输出:
python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-5.4",
api_key=os.getenv("PACKYAPI_API_KEY"),
base_url="https://www.packyapi.com/v1/",
use_responses_api=True,
output_version="responses/v1"
)
class State(TypedDict):
query: str
summary: str
translation: str
def extract_text(content):
# 中转站/`responses/v1` 可能返回结构化块,而不是直接返回纯字符串。
if isinstance(content, str):
return content
if isinstance(content, list):
return "".join(
block.get("text", "")
for block in content
if isinstance(block, dict) and block.get("type") == "text"
)
return ""
def generate_summary(state: State):
"""生成摘要"""
response = model.invoke([
{"role": "user", "content": f"请为以下内容生成摘要:{state['query']}"}
])
return {"summary": response.content}
def generate_translation(state: State):
"""生成翻译"""
response = model.invoke([
{"role": "user", "content": f"请将以下内容翻译成英文:{state['query']}"}
])
return {"translation": response.content}
# 构建并行处理图
builder = StateGraph(State)
builder.add_node("summarize", generate_summary) # 这里来指定节点名称
builder.add_node("translate", generate_translation)
builder.add_edge(START, "summarize")
builder.add_edge(START, "translate")
builder.add_edge("summarize", END)
builder.add_edge("translate", END)
graph = builder.compile()
# 流式输出并只显示某个节点的 Tokens
target_node = "summarize" # 可以改为 "translate"
printed_prefix = False
for token_chunk, metadata in graph.stream(
{"query": "人工智能是计算机科学的一个分支,致力于创造能够执行通常需要人类智能的任务的机器。"},
stream_mode="messages"
):
# 获取节点名称
node_name = metadata.get("langgraph_node", "")
# 只输出目标节点的 Tokens
if token_chunk.content and node_name == target_node:
# 添加节点标签
if node_name == "translate":
prefix = "【翻译】"
elif node_name == "summarize":
prefix = "【摘要】"
else:
prefix = ""
if not printed_prefix and prefix:
print(prefix, end="", flush=True)
printed_prefix = True
print(extract_text(token_chunk.content), end="", flush=True)
输出格式为:
plain
【摘要】摘要:人工智能是计算机科学的一个分支,目标是让机器具备执行通常需要人类智能任务的能力。