[langgraph] Multi-agents
Link: https://www.youtube.com/watch?v=28APvzg-ld0&list=PLChN7AYOq0ZgB5wngh_kUqO2W44jkhoyT&index=2
用 stream_mode="messages" 流式输出大模型结果
from config.load_key import load_key from langchain_community.chat_models import ChatTongyi from langgraph.graph import StateGraph, MessagesState, START # ============================================================ # 1. 构建大模型对象 # ============================================================ # 这里使用的是阿里云百炼里的通义千问模型。 # # 注意: # - 这一步还不是 LangGraph 的内容; # - 它只是先准备一个可以被 LangGraph 节点调用的 LLM; # - 后面真正进入 LangGraph 的地方,是 call_model 这个节点函数。 llm = ChatTongyi( model="qwen-plus", api_key=load_key("BAILIAN_API_KEY"), ) # ============================================================ # 2. 定义 LangGraph 节点:call_model # ============================================================ # 在 LangGraph 里,一个节点通常就是一个 Python 函数。 # # 这个函数的输入是 state。 # 这里使用的是 MessagesState,所以 state 里最重要的字段就是: # # state["messages"] # # 它保存了当前对话的消息列表,例如: # # [ # {"role": "user", "content": "湖南的省会是哪里?"} # ] # # 这个节点做的事情非常简单: # # 读取 messages # ↓ # 调用大模型 # ↓ # 返回大模型回复 # # 所以这个例子本质上是: # # START → call_model # # 还不是多智能体,只是一个“单节点 LLM 调用图”。 def call_model(state: MessagesState): response = llm.invoke(state["messages"]) # 返回值必须是一个 dict,用来更新图的 state。 # # MessagesState 会把新的 AI 回复合并回 messages 里。 # # 可以简单理解为: # # 原来的 messages + 模型回复 = 新的 messages return {"messages": response} # ============================================================ # 3. 创建状态图 StateGraph # ============================================================ # StateGraph 用来定义一个“有状态的执行流程图”。 # # MessagesState 表示这个图的状态结构以 messages 为核心。 # # 也就是说,整个图在运行过程中,主要围绕聊天消息进行传递和更新。 builder = StateGraph(MessagesState) # ============================================================ # 4. 添加节点 # ============================================================ # 把 call_model 函数注册成图里的一个节点。 # # 如果不手动指定名字,LangGraph 会默认使用函数名: # # "call_model" # # 所以下面这句大致等价于: # # builder.add_node("call_model", call_model) builder.add_node(call_model) # ============================================================ # 5. 添加边 Edge # ============================================================ # Edge 用来控制流程走向。 # # 这句话的意思是: # # 图从 START 开始后,进入 call_model 节点。 # # 当前图的结构非常简单: # # START → call_model # # 如果将来做多智能体,这里会变成更复杂的结构,例如: # # START → supervisor → researcher / coder / reviewer builder.add_edge(START, "call_model") # ============================================================ # 6. 编译图 # ============================================================ # builder 只是“图的定义”。 # # compile() 之后,才得到一个真正可以运行的 graph 对象。 graph = builder.compile() # ============================================================ # 7. 流式运行图:graph.stream(...) # ============================================================ # 普通调用一般是: # # result = graph.invoke(...) # # 它会等整个模型回答生成完,再一次性返回结果。 # # 这里使用的是: # # graph.stream(...) # # 它会在图运行过程中持续输出中间结果。 # # stream_mode="messages" 的意思是: # # 重点监听大模型生成的消息片段。 # # 直观理解: # # invoke:等完整回答结束后,一次性返回 # stream:模型一边生成,程序一边接收 # # 所以这个模式适合: # # - 聊天机器人 # - 实时问答界面 # - 展示 Agent 的输出过程 # - 模拟 ChatGPT 那种逐步输出效果 for chunk in graph.stream( { "messages": [ { "role": "user", "content": "湖南的省会是哪里?", } ] }, stream_mode="messages", ): # chunk 是流式输出过程中的一个片段。 # # 在不同版本的 LangGraph / LangChain 中, # chunk 的具体结构可能略有不同。 # # 学习阶段可以直接 print(chunk),先观察它到底长什么样。 print(chunk)
消息持久化
checkpointer = 仓库
thread_id = 箱子编号
messages = 箱子里的对话记录。
compile(checkpointer=checkpointer) 表示:启用一个仓库,用来保存 graph 的运行状态。 config 里的 thread_id 表示:这次对话应该存到哪个箱子里。 所以:
-
- 有 checkpointer + 有 thread_id: 可以保存并恢复同一段对话历史。
- 只有 thread_id,没有 checkpointer: 有箱子编号,但没有仓库,历史消息没地方保存。
- 只有 checkpointer,没有 thread_id: 有仓库,但箱子没有编号,系统不知道该存到哪里,也不知道下次从哪里取。
from config.load_key import load_key from langchain_community.chat_models import ChatTongyi from langgraph.graph import StateGraph, MessagesState, START from langgraph.checkpoint.memory import InMemorySaver # ============================================================ # 1. 构建大模型客户端 # ============================================================ # 这里使用阿里云百炼的通义千问模型。 # # 注意: # - LLM 本身没有“长期记忆”; # - 它每次能记住什么,取决于我们传给它的 messages; # - LangGraph 的 checkpointer 负责帮我们保存和恢复 messages。 llm = ChatTongyi( model="qwen-plus", api_key=load_key("BAILIAN_API_KEY"), ) # ============================================================ # 2. 定义 LangGraph 节点:call_model # ============================================================ # 这是一个最简单的大模型调用节点。 # # 输入: # state: MessagesState # # MessagesState 的核心字段是: # # state["messages"] # # 它保存当前会话中的历史消息,例如: # # [ # HumanMessage(content="湖南的省会是哪里?"), # AIMessage(content="湖南的省会是长沙。"), # HumanMessage(content="湖北呢?") # ] # # 也就是说,只要 checkpointer 能恢复之前的 messages, # 大模型就可以理解后续问题里的上下文。 def call_model(state: MessagesState): # 把当前会话历史消息全部传给大模型。 # 这就是大模型“看起来有记忆”的根本原因。 response = llm.invoke(state["messages"]) # 返回新的 AI 回复。 # # MessagesState 会把这个 response 追加到已有 messages 后面。 # # 可以理解为: # # 旧 messages + 本次 AI 回复 = 新 messages return {"messages": response} # ============================================================ # 3. 创建 LangGraph 状态图 # ============================================================ # StateGraph 表示一个有状态的流程图。 # # MessagesState 表示这个图的状态主要围绕 messages 展开, # 非常适合用来做聊天机器人、Agent 对话、多轮问答。 builder = StateGraph(MessagesState) # ============================================================ # 4. 添加节点和边 # ============================================================ # 添加一个节点:call_model。 # # 如果不指定节点名,默认使用函数名 "call_model"。 builder.add_node(call_model) # 定义流程: # # START → call_model # # 当前例子只有一个节点,所以还不是多智能体。 # 它是一个“带消息持久化能力的单节点 LLM 图”。 builder.add_edge(START, "call_model") # ============================================================ # 5. 创建 checkpointer:保存图的运行状态 # ============================================================ # checkpointer 是这一节的重点。 # # 它的作用是: # # 保存每一次图运行后的 state # # 对于 MessagesState 来说,保存的核心就是 messages。 # # 这里使用的是 InMemorySaver: # # - 优点:简单,适合学习和本地测试; # - 缺点:只保存在当前 Python 进程内; # - 程序一重启,历史消息就会消失; # - 生产环境一般要换成 SQLite、Postgres、Redis 等持久化方案。 checkpointer = InMemorySaver() # ============================================================ # 6. 编译图,并挂载 checkpointer # ============================================================ # 没有 checkpointer: # # 每次调用 graph 都像是一次新的对话。 # # 有了 checkpointer: # # LangGraph 可以根据 thread_id 找回之前的对话状态。
#
# LangGraph 需要在compile()时提供 checkpointer,才会 把 graph state 保存成 checkpoints; graph = builder.compile(checkpointer=checkpointer) # ============================================================ # 7. 配置 thread_id:区分不同会话 # ============================================================ # thread_id 可以理解为“会话 ID”。 # # LangGraph 会用它来判断: # # 这次调用属于哪一个历史对话? # # 如果两次调用使用相同的 thread_id, # 那么第二次调用可以继承第一次调用留下来的 messages。 # # 如果换成不同的 thread_id, # 那就相当于开启一个全新的对话。 config = { "configurable": { "thread_id": "1" } } # ============================================================ # 8. 第一次提问 # ============================================================ # 这一次用户问: # # 湖南的省会是哪里? # # 图执行结束后,checkpointer 会保存这轮对话的 messages: # # User: 湖南的省会是哪里? # AI: 湖南的省会是长沙。 for chunk in graph.stream( { "messages": [ { "role": "user", "content": "湖南的省会是哪里?", } ] }, config, stream_mode="values", ): # stream_mode="values" 表示输出每一步之后的完整 state。 # # chunk["messages"] 是当前 state 中的完整消息列表。 # chunk["messages"][-1] 表示取最后一条消息。 # # pretty_print() 用更易读的格式打印这条消息。 chunk["messages"][-1].pretty_print() # ============================================================ # 9. 第二次提问:验证消息是否被保存 # ============================================================ # 这一次用户只问: # # 湖北呢? # # 这个问题本身是不完整的。 # 但因为它和上一次使用了同一个 thread_id = "1",<---- 在 config这个参数中 # LangGraph 会自动恢复之前保存的 messages。 # # 所以大模型看到的上下文大致是: # # User: 湖南的省会是哪里? # AI: 湖南的省会是长沙。 # User: 湖北呢? # # 因此模型可以理解: # # 用户是在继续问“湖北的省会是哪里?” for chunk in graph.stream( { "messages": [ { "role": "user", "content": "湖北呢?", } ] }, config, stream_mode="values", ): chunk["messages"][-1].pretty_print()
Human-in-the-loop
它只演示一件事: 用户已经提交问题之后, 在真正调用大模型之前, 先暂停一下,让人工确认: “这次是否允许调用 LLM?” 完整流程是:
1. 用户提交问题: “湖南的省会是哪里?”
2. 问题进入 LangGraph 的 state/messages。
3. graph 不会马上调用 LLM。 它会先进入 human_approval 节点。
4. human_approval 节点调用 interrupt()。 interrupt() 会让 graph 暂停。
5. 人工决定: True = 同意调用 LLM False = 不同意调用 LLM
6. ----> 如果人工同意: graph 继续进入 call_llm 节点,调用大模型。
7. ----> 如果人工拒绝: graph 直接 END,不调用大模型。
from operator import add from typing import Annotated, List, Literal, TypedDict from config.load_key import load_key from langchain_community.chat_models import ChatTongyi from langchain_core.messages import AnyMessage, HumanMessage from langgraph.checkpoint.memory import InMemorySaver from langgraph.constants import START, END from langgraph.graph import StateGraph from langgraph.types import interrupt, Command # ============================================================ # 1. 准备大模型 # ============================================================ llm = ChatTongyi( model="qwen-plus", api_key=load_key("BAILIAN_API_KEY"), ) # ============================================================ # 2. 定义 State:graph 运行时携带的数据包 # ============================================================ class State(TypedDict): messages: Annotated[List[AnyMessage], add] # ============================================================ # 3. 节点一:human_approval # ============================================================ # LangGraph 里的 node 通常就是一个 Python 函数。这个节点的名字叫:human_approval。 # 它的作用是: # 在调用 LLM 之前, # 暂停 graph, # 让人工确认是否允许继续调用 LLM。 # 注意顺序: # 用户问题已经进入 messages。 # 但还没有调用 LLM。 # # 所以这个节点审批的是:是否允许把当前这个问题发送给 LLM? # 而非审批:用户能不能提问。
def human_approval(state: State) -> Command[Literal["call_llm", "__end__"]]: is_approved = interrupt( { "question": "是否同意调用大语言模型?" } ) if is_approved: return Command(goto="call_llm") return Command(goto=END) # ============================================================ # 4. 节点二:call_llm # ============================================================ # 这个节点才是真正调用大模型的地方。 # # 只有一种情况会走到这里: # # 人工审批通过。 # # 如果人工拒绝,graph 会直接 END, # 这个函数不会被执行。 def call_llm(state: State): # state["messages"] 里保存了当前对话消息。 # # 在本例中,至少包含用户问题: # # HumanMessage(content="湖南的省会是哪里?") # # 这里把 messages 交给 LLM。 response = llm.invoke(state["messages"]) # 返回模型回答。 # # 注意这里返回的是 list: # # [response] # # 因为 State 里 messages 使用了 add 作为 reducer, # 所以这个 response 会被追加到原 messages 后面。 # # 最终 messages 会变成: # # [用户问题, 模型回答] return {"messages": [response]} # ============================================================ # 5. 创建 StateGraph:开始搭建 graph # ============================================================ # StateGraph(State) 的意思是: # 创建一个 graph。 # 这个 graph 运行时 使用 State 作为数据结构。 # 换句话说: # graph 里面的每个节点, # 都会围绕 State 这个数据包来读写数据。 builder = StateGraph(State) # ============================================================ # 6. 注册 节点 add_node() 节点一 节点二 的“定义”在前面,这里只是“注册”。 # ============================================================ builder.add_node("human_approval", human_approval) builder.add_node("call_llm", call_llm) # ============================================================ # 7. 添加边 add_edge() # ============================================================ builder.add_edge(START, "human_approval")
# ============================================================ # 8. 创建 checkpointer:保存暂停现场 # ============================================================ checkpointer = InMemorySaver() # ============================================================ # 9. 编译 graph compile() # ============================================================ graph = builder.compile(checkpointer=checkpointer) # ============================================================ # 10. 配置 thread_id:给这次运行一个编号 # ============================================================ thread_config = { "configurable": { "thread_id": "1" } }
# 我们把接下来的代码单独放在另一个单独的文件中。
如下。
""" run_approval_demo.py 这个文件只负责一件事: 触发 / 运行 graph。 也就是: 1. 等最终用户输入问题 2. 第一次 graph.invoke(...),提交问题 3. graph 在 interrupt() 暂停,并返回 initial_result 4. 等人工审批人输入 y/n 5. 第二次 graph.invoke(Command(resume=...)),恢复 graph 注意: graph 的定义不在这个文件里。 graph 是从 approval_graph.py import 进来的。 """ from langchain_core.messages import HumanMessage from langgraph.types import Command from approval_graph import graph # ============================================================ # 1. thread_id:这次任务的编号 # ============================================================ thread_config = { "configurable": { "thread_id": "demo-thread-1" } } # ============================================================ # 2. 等待最终用户输入问题 # ============================================================ # 这里 block,等待“最终用户”输入问题。 # 注意: # a) 最终用户只负责提问。 # b) 他不是人工审批人。 user_question = input("请输入用户问题: ").strip() if not user_question: raise ValueError("用户问题不能为空。") # ============================================================ # 3. 第一次 invoke:提交用户问题 # ============================================================ # 这次 invoke 会启动 graph。 # # 执行流程: # START → human_approval → interrupt() # 注意: # 这次不会调用 LLM。 # # 因为 graph 会在 interrupt() 暂停, # 然后返回 initial_result。 initial_result = graph.invoke( { "messages": [ HumanMessage(content=user_question) ] }, config=thread_config, ) print("\n第一次运行结果:graph 已经暂停,等待人工审批。") print(initial_result) # ============================================================ # 4. 读取 interrupt 暂停信息(optional) # ============================================================
# interrupt(...) 抛出来的信息通常放在 "__interrupt__" 里。 # # 它里面包含: # 用户问题 # 审批问题 # 期待的回答类型 interrupts = initial_result.get("__interrupt__") if not interrupts: raise RuntimeError("没有发现 interrupt 信息,graph 可能没有在审批节点暂停。") approval_request = interrupts[0].value print("\n需要人工审批:") print("用户问题:", approval_request.get("user_question")) print("审批问题:", approval_request.get("approval_question")) print("回答方式:y = 同意,n = 拒绝")
# ============================================================ # 5. 等待人工审批人输入 # ============================================================ # 这里 block,等待“人工审批人”回复。 # # 审批人不需要输入代码。 # # 审批人只需要输入: # y = 同意调用 LLM # n = 拒绝调用 LLM while True: approval_input = input("\n是否同意调用 LLM?请输入 y/n: ").strip().lower() if approval_input == "y": human_decision = True break if approval_input == "n": human_decision = False break print("输入无效,请输入 y 或 n。") # ============================================================ # 6. 第二次 invoke:恢复 graph # ============================================================ # 这里是在恢复刚才 interrupt() 暂停的位置。 # # Command(resume=human_decision) 的作用: # 把审批人的 True/False 送回 interrupt()。 # # 如果 human_decision=True: # human_approval → call_llm # # 如果 human_decision=False: # human_approval → END final_result = graph.invoke( Command(resume=human_decision), config=thread_config, ) print(final_result) # 大模型的回复结果
Advanced Human-in-the-loop
关注我的紫色注释。
""" langgraph_human_approval_full_demo.py 这个 demo 演示一个接近真实应用的 LangGraph 流程: 1. 最终用户输入一个问题 2. graph 启动,但不会马上调用 LLM 3. graph 在 human_approval 节点暂停 4. 人工审批人决定是否允许调用 LLM 5. 如果审批通过: call_llm 节点生成直接答案 summarize_llm 节点总结直接答案 6. 如果审批拒绝: graph 直接结束,不调用 LLM 核心理解: State 是总表。 节点 return 是往总表里更新字段。 invoke 返回的是 graph 停止时的最终 State。 运行方式: python langgraph_human_approval_full_demo.py """ from operator import add from typing import Literal from typing_extensions import Annotated, TypedDict from uuid import uuid4 from config.load_key import load_key from langchain_community.chat_models import ChatTongyi from langchain_core.messages import AnyMessage, HumanMessage from langgraph.checkpoint.memory import InMemorySaver from langgraph.graph import StateGraph, START, END from langgraph.types import interrupt, Command # ============================================================ # 1. 准备 LLM # ============================================================ # 这里只是创建模型对象。 # 真正调用模型的是后面的 call_llm 和 summarize_llm 节点。 llm = ChatTongyi( model="qwen-plus", api_key=load_key("BAILIAN_API_KEY"), ) # ============================================================ # 2. 定义 State # ============================================================ # State 是整个 graph 运行时共享的数据表。 # # messages: # 保存用户问题、LLM 直接回答、LLM 总结。 # # direct_answer: # 专门保存第一个 LLM 节点的直接回答。 # # final_summary: # 专门保存第二个 LLM 节点的总结结果。 class State(TypedDict, total=False): messages: Annotated[list[AnyMessage], add] direct_answer: str final_summary: str # ============================================================ # 3. 节点 1:人工审批节点 # ============================================================ # 这个节点发生在: # 用户问题已经进入 graph 之后; # 真正调用 LLM 之前。 # 它审批的是: # 是否允许把当前用户问题发送给 LLM? def human_approval(state: State) -> Command[Literal["call_llm", "__end__"]]: user_question = state["messages"][-1].content # 最近的一条消息,自然是用户的提问内容 # 第一次运行到 interrupt(): # graph 暂停,并把下面这个 dict 返回给外部。 # # 第二次恢复时: # 外部通过 Command(resume=True/False) # 把审批人的决定送回这里。
# interrupt(...) 是一个“暂停并等待外部输入”的洞。
# Command(resume=human_decision) 就是把 human_decision 塞回这个洞里。
# 塞回去以后:
# is_approved = human_decision
is_approved =interrupt( { "approval_question": "是否同意把当前用户问题发送给 LLM?", "user_question": user_question, "answer_type": "boolean", "options": { True: "同意调用 LLM", False: "拒绝调用 LLM", }, } ) if not isinstance(is_approved, bool): raise ValueError("审批结果必须是 bool:True 表示同意,False 表示拒绝。") if is_approved: return Command(goto="call_llm") return Command(goto=END) # ============================================================ # 4. 节点 2:LLM 直接回答节点 # ============================================================ # 只有人工审批通过后,才会执行到这里。 # # 这个节点负责: # 根据用户问题生成直接答案。 def call_llm(state: State) -> dict: response = llm.invoke(state["messages"]) return { "messages": [response], # 但这里会“追加”,而非“替换” "direct_answer": response.content, } # ============================================================ # 5. 节点 3:LLM 总结节点 # ============================================================ # 这个节点在 call_llm 后面执行。 # # 它不重新回答用户问题, # 而是总结 call_llm 生成的 direct_answer。 def summarize_llm(state: State) -> dict: user_question = state["messages"][0].content direct_answer = state["direct_answer"] summary_prompt = HumanMessage( content=( "请用一句话总结下面的问答。\n\n" f"用户问题:{user_question}\n" f"直接回答:{direct_answer}" ) ) summary = llm.invoke([summary_prompt]) return { "messages": [summary], "final_summary": summary.content, } # ============================================================ # 6. 构建 graph # ============================================================ # 这里是“画图 / 注册节点 / 注册路线”。 builder = StateGraph(State)
# 构建节点 builder.add_node("human_approval", human_approval) builder.add_node("call_llm", call_llm) builder.add_node("summarize_llm", summarize_llm) # 固定入口: # # START → human_approval builder.add_edge(START, "human_approval")
# Human_approval引出的“Edge”不需要再定义,已经在interrupt节点中通过Command定义了。 # 审批通过后,human_approval 会用 Command(goto="call_llm") # 动态跳到 call_llm。 # # call_llm 后面是固定路线: # call_llm → summarize_llm → END
builder.add_edge("call_llm", "summarize_llm") builder.add_edge("summarize_llm", END) # ============================================================ # 7. checkpointer # ============================================================ # interrupt() 会让 graph 暂停。 # # checkpointer 用来保存暂停现场。 # # 比喻: # checkpointer = 仓库 # thread_id = 箱子编号 # # 注意: # InMemorySaver 只适合 demo。 # 程序退出后,内存里的 checkpoint 会消失。 checkpointer = InMemorySaver() graph = builder.compile(checkpointer=checkpointer) # ============================================================ # 8. 工具函数:读取 interrupt 信息 # ============================================================ def get_interrupt_payload(graph_result: dict) -> dict:
interrupts = graph_result.get("__interrupt__") if not interrupts: raise RuntimeError("没有发现 __interrupt__,说明 graph 没有在审批节点暂停。") first_interrupt = interrupts[0] # 不同 LangGraph 版本里 interrupt 对象显示略有不同。 # 常见情况下,payload 在 .value 里。 return getattr(first_interrupt, "value", first_interrupt) # ============================================================ # 9. 工具函数:等待最终用户提问 # ============================================================ def ask_user_question() -> str:
while True: question = input("请输入用户问题: ").strip() if question: return question print("用户问题不能为空,请重新输入。") # ============================================================ # 10. 工具函数:等待审批人输入 # ============================================================ def ask_human_approval(approval_request: dict) -> bool:
print("\n需要人工审批:") print("用户问题:", approval_request.get("user_question")) print("审批问题:", approval_request.get("approval_question")) print("请输入 y/n:y = 同意调用 LLM,n = 拒绝调用 LLM") while True: approval_input = input("审批人输入 y/n: ").strip().lower() if approval_input == "y": return True if approval_input == "n": return False print("输入无效,请输入 y 或 n。") # ============================================================ # 11. 主流程:真实触发过程 # ============================================================ def main() -> None: thread_config = { "configurable": { "thread_id": f"approval-demo-{uuid4()}" } } # 第一步:等待最终用户提问。 user_question = ask_user_question() # 第二步:第一次 invoke,提交用户问题。 # 这次会运行: START → human_approval → interrupt() initial_graph_result = graph.invoke( { "messages": [ HumanMessage(content=user_question) ] }, config=thread_config, ) print("\n第一次 invoke 返回:graph 已暂停,等待人工审批。") # 第三步:从 interrupt 结果里取出审批请求。 approval_request = get_interrupt_payload(initial_graph_result) # 第四步:等待审批人输入 y/n。 human_decision = ask_human_approval(approval_request) # 第五步:第二次 invoke,恢复 graph。 # 如果 human_decision=True: # human_approval → call_llm → summarize_llm → END # # 如果 human_decision=False: # human_approval → END graph_state_after_approval = graph.invoke( Command(resume=human_decision), config=thread_config, ) # 第六步:展示最终 State。 # # graph_state_after_approval 不是“审批结果本身”, # 而是 graph 根据审批结果继续运行后的最终 State。 print("\n第二次 invoke 返回:graph 根据审批结果继续执行后的最终 State。") if human_decision: print("\n审批结果:同意调用 LLM") print("\nLLM 直接回答:") print(graph_state_after_approval.get("direct_answer")) print("\nLLM 总结结果:") print(graph_state_after_approval.get("final_summary")) else: print("\n审批结果:拒绝调用 LLM") print("因此 call_llm 和 summarize_llm 都没有执行。") print("\n完整 State:") print(graph_state_after_approval) if __name__ == "__main__": main()
Time Travel 时间回溯
开启下面之后,LangGraph 会在运行过程中 保存多个中间状态。
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
反悔,修改某个中间状态,示范如下。
""" langgraph_checkpoint_replay_demo.py 这个 demo 演示 LangGraph 的 checkpoint / time travel / replay 能力。 流程本身很简单: START ↓ author_node ↓ joke_node ↓ END 正常流程: 1. author_node 让 LLM 推荐一位作家 2. joke_node 用这位作家的风格写一个笑话 高级功能: 1. 查看 graph 运行过程中保存的 checkpoint 2. 找到 “author_node 已经执行完,joke_node 还没执行” 的 checkpoint 3. 手动把 author 改成“郭德纲” 4. 从这个修改后的 checkpoint 继续执行 joke_node 核心理解: State 是总表。 node return 是对 State 的局部更新。 graph.invoke(...) 返回的是 graph 停止时的最终 State。 get_state_history(...) 返回的是 checkpoint 快照,不是普通 State。 """ import uuid from typing_extensions import NotRequired, TypedDict from config.load_key import load_key from langchain_community.chat_models import ChatTongyi from langgraph.checkpoint.memory import InMemorySaver from langgraph.constants import START, END from langgraph.graph import StateGraph # ============================================================ # 1. 创建 LLM 客户端 # ============================================================ llm = ChatTongyi( model="qwen-plus", api_key=load_key("BAILIAN_API_KEY"), ) # ============================================================ # 2. 定义 State # ============================================================ # State 是整个 graph 共享的数据结构。 # # author: # author_node 生成。 # joke: # joke_node 生成。 # NotRequired[str]: # 表示这个字段不是一开始就必须存在。 # 因为 graph.invoke({}) 一开始传入的是空 dict。 class GraphState(TypedDict): author: NotRequired[str] joke: NotRequired[str] # ============================================================ # 3. node 1:生成作家 # ============================================================ # 输入: # 当前 graph state。 # 输出: # 对 graph state 的局部更新。 # 注意: # 这里返回 {"author": author_name} # 不是返回完整 State。 def author_node(current_state: GraphState) -> dict: prompt = "帮我推荐一位受人们欢迎的作家。只需要给出作家的名字即可。" response = llm.invoke(prompt) author_name = response.content return { "author": author_name } # ============================================================ # 4. node 2:根据作家风格生成笑话 # ============================================================ # 这个节点依赖 author_node 已经写入 State 的 author 字段。 # # 所以这里可以读取: # current_state["author"] def joke_node(current_state: GraphState) -> dict: author_name = current_state["author"] prompt = f"用作家 {author_name} 的风格,写一个 100 字以内的笑话。" response = llm.invoke(prompt) joke_text = response.content return { "joke": joke_text } # ============================================================ # 5. 构建 graph # ============================================================ # StateGraph(GraphState) 这一步告诉 LangGraph: # 这个 graph 的共享状态结构是 GraphState。 # # 后面每个 node 都会接收当前 GraphState, # 并返回 GraphState 的局部更新。 builder = StateGraph(GraphState) builder.add_node("author_node", author_node) builder.add_node("joke_node", joke_node) builder.add_edge(START, "author_node") builder.add_edge("author_node", "joke_node") builder.add_edge("joke_node", END) # ============================================================ # 6. 启用 checkpoint # ============================================================ # checkpointer 会保存 graph 运行过程中的中间状态。 # # InMemorySaver 只适合 demo。 # 程序退出后,checkpoint 会消失。 checkpointer = InMemorySaver() graph = builder.compile(checkpointer=checkpointer) # ============================================================ # 7. 正常运行一次 graph # ============================================================ thread_config = { "configurable": { "thread_id": str(uuid.uuid4()) } } # final_state 是 graph 正常跑完后的最终 State。 # 因为没有interrupt,所以一次性地就跑遍了,直接得到最终结果。 # 它不是 checkpoint snapshot。 # 它就是最终的状态 dict。 final_state = graph.invoke( {}, config=thread_config, ) print("====== 正常运行后的最终 State ======") print("作家:", final_state["author"]) print("笑话:", final_state["joke"]) print() # ============================================================ # 8. 查看所有 checkpoint 快照 # ============================================================ # get_state_history(...) 返回的是 StateSnapshot 列表。 # # 注意: # snapshot 不是普通 dict。 # # 每个 snapshot 里重要的内容有: # snapshot.values 当前 checkpoint 保存的 State 内容 # snapshot.next 从这里继续运行时,下一步要执行的 node # snapshot.config 当前 checkpoint 的定位信息 state_snapshots = list(graph.get_state_history(thread_config)) print("====== checkpoint 历史 ======") for snapshot in state_snapshots: print("next node:", snapshot.next) print("checkpoint_id:", snapshot.config["configurable"]["checkpoint_id"]) print("values:", snapshot.values) # <-- 这个 checkpoint 当时保存下来的 graph state 内容。 print()
====== checkpoint 历史 ====== next node: () checkpoint_id: 1f048581-xxxx-xxxx-xxxx-xxxxxxxxxxxx values: { "author": "鲁迅", "joke": "某个鲁迅风格的笑话..." } next node: ('joke_node',) checkpoint_id: 1f048581-xxxx-xxxx-xxxx-xxxxxxxxxxxx values: { "author": "鲁迅" } next node: ('author_node',) checkpoint_id: 1f048581-xxxx-xxxx-xxxx-xxxxxxxxxxxx values: {} next node: ('__start__',) checkpoint_id: 1f048581-xxxx-xxxx-xxxx-xxxxxxxxxxxx values: {}
# ============================================================ # 9. 找到 joke_node 执行前的 checkpoint # ============================================================ # 我们不使用 states[1] 这种写法。 # 因为 states[1] 依赖列表顺序,容易让人误解。 # # 更清楚的写法是: # 找到 snapshot.next == ("joke_node",) 的 checkpoint。 # # 这个 checkpoint 表示: # author_node 已经执行完; # joke_node 还没有执行; # 如果从这里继续,下一步就是 joke_node。 snapshot_before_joke = next( snapshot for snapshot instate_snapshots if snapshot.next == ("joke_node",) ) print("====== 选中的 checkpoint ======") print("这个 checkpoint 的下一步是:", snapshot_before_joke.next) print("这个 checkpoint 当前保存的 State 是:", snapshot_before_joke.values) print() # ============================================================ # 10. 基于旧 checkpoint 修改 State,创建一个新 checkpoint # ============================================================ # 这里不是直接修改旧 snapshot。 # # update_state(...) 的意思是: # 基于 snapshot_before_joke 这个 checkpoint, # 创建一个新的 checkpoint, # 并把 author 改成“郭德纲”。 # # fork_config 指向这个新 checkpoint。 fork_config = graph.update_state( # config里有checkpoint id,其实是更新了整个checkpoint,也就是创建了一个新的checkpoint(游戏存档) snapshot_before_joke.config, values={ "author": "郭德纲" }, ) print("====== 修改 State 后生成的新 config ======") print(fork_config) print() # ============================================================ # 11. 从新的 checkpoint 继续运行 # ============================================================ # 这里传入 None,表示: # # 不重新提交新的初始输入; # 而是从 fork_config 指定的 checkpoint 继续运行。 # # 因为这个 checkpoint 的下一步是 joke_node, # 所以这里只会继续执行 joke_node。 forked_final_state = graph.invoke( None, config=fork_config, # 里面有checkpoint id,自然可以从该存档位置重新开始 ) print("====== 从修改后的 checkpoint 继续运行后的最终 State ======") print("作家:", forked_final_state["author"]) print("笑话:", forked_final_state["joke"])
多智能体架构
Link: 9 langgraph8 零帧起手,实现多智能体工作流

To do List.

浙公网安备 33010602011771号