[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 = 拒绝")
View Code

 

# ============================================================
# 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: {}
Print Log demo
# ============================================================
# 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 零帧起手,实现多智能体工作流

image

 

 

 

 

 

 

 

To do List.

 

posted @ 2026-06-28 13:03  郝壹贰叁  阅读(25)  评论(0)    收藏  举报