LangGraph SDK 人机环路,从中断处恢复

LangGraph SDK 人机环路与恢复机制

LangGraph SDK 的人机环路的操作,其实和 LangGraph 差不多,下面会演示两种断点场景:
1,在节点之前打断点,也就是在将要进入节点之前打断点,即 边上的断点
2,节点内部使用 interrupt()函数打的断点,多用于人机交互

一, 定义带中断点的图

这是我的图:

import operator
from typing import Literal, TypedDict, Annotated
from langgraph.constants import START
from langgraph.graph import StateGraph
from langgraph.types import Command, interrupt



# 定义状态类型,定义一个 贷款 AI 智能体审批流
class MoneyLoanState(TypedDict):
    user_id: str
    user_name: str
    # 贷款基本信息
    loan_amount: int  # 贷款金额
    loan_term: int  # 贷款期限(月)
    user_credit: str  # 用户信用评级
    user_property: int  # 用户的所有财产
    execution_log: Annotated[list[str], operator.add]
    # 审批结果
    approval_result: str  # 最终结果


# 信息获取节点
def loan_info_supplement_node(state: MoneyLoanState):
    """自动收集用户贷款信息(实际场景可对接表单/数据库)"""
    return {
        **state,
        "user_credit": "A",  # 信用评级:A(良好)
        "user_property": 1000000,
        "execution_log": ["信息补充节点:正在补充信息......."]
    }


# 人工审批节点
def approval_human_node(state: MoneyLoanState) -> Command[Literal["money_disbursed", "send_reject_email"]]:
    """ 请求人工审批 """
    loan_info_str = f"贷款信息:用户 {state["user_name"]}, 金额 {state["loan_amount"]}元,期限 {state["loan_term"]}个月,信用评级 {state["user_credit"]},财产 {state["user_property"]}元。"
    # 🚀 关键:中断执行
    approval_request = interrupt(
        {
            "question": "是否同意这笔贷款?",
            "loan_details": loan_info_str
        }
    )
    # 同意,则发放贷款;拒绝,则通知邮件,告诉被拒绝的理由
    if approval_request["user_response"] == "approve":
        return Command(goto="money_disbursed", update={"execution_log": ["人工审批节点:人工审批同意"]})
    else:
        return Command(goto="send_reject_email", update={"execution_log": ["人工审批节点:人工审批同意"]})


# 同意的话,发放贷款节点
def money_disbursed(state: MoneyLoanState):
    return {"approval_result": "同意发放贷款", "execution_log": ["同意贷款,正在发放金额(模拟调用转账 API)........"]}


# 拒绝
def send_reject_email(state: MoneyLoanState):
    return {"approval_result": "拒绝发放贷款,我佛不渡穷逼!", "execution_log": ["拒绝贷款,正在发放金额(模拟调用邮箱 API)........"]}


graph_with_interrupt = (StateGraph(MoneyLoanState)
         .add_node("loan_info_supplement_node", loan_info_supplement_node)
         .add_node("approval_human_node", approval_human_node)
         .add_node("money_disbursed", money_disbursed)
         .add_node("send_reject_email", send_reject_email)
         .add_edge(START, "loan_info_supplement_node")
         .add_edge("loan_info_supplement_node", "approval_human_node")
         .compile(name="graph_with_interrupt")
)

在这里插入图片描述

二,在人工节点之前,打断点

注意安装 langgraph-sdk 。
这里在开始执行的时候,使用 interrupt_before 参数传递你指定的节点名称即可

async def main():
    client = get_client(url="http://localhost:8123")
    general_assistant = await client.assistants.create(
        graph_id="graph with interrupt",  # 图的名称
        name="my_assistant",  # 你起的助手名
        config={
            "configurable": {
                "model_name": "gpt-4o",
                "system_prompt": "你是乐于助人的AI代理,负责处理一般问题"
            }
        }
    )
    assistant_id = general_assistant["assistant_id"]

    thread = await client.threads.create()
    thread_id = thread["thread_id"]
    print(f"thread_id: {thread_id}")


    input_msg = {"user_id": "uid_001", "user_name": "张三", "loan_amount": 50000, "loan_term": 36}

    # 在 approval_human_node 节点前打断点
    res = await client.runs.wait(
   		thread_id, assistant_id, 
   		input=input_msg, 
    	interrupt_before=["approval_human_node"]  # 🚀 这里
    )
    # 执行结果
    print(res)

    state = await client.threads.get_state(thread_id)
    print(f"打断点后的状态:{state}")


asyncio.run(main())

"""
thread_id: ec3b5bf7-61e3-471a-912e-ad181cc71bee
{'user_id': 'uid_001', 'user_name': '张三', 'loan_amount': 50000, 'loan_term': 36, 'user_credit': 'A', 'user_property': 1000000, 'execution_log': ['信息补充节点:正在补充信息.......']}
打断点后的状态:{'values': {'user_id': 'uid_001', 'user_name': '张三', 'loan_amount': 50000, 'loan_term': 36, 'user_credit': 'A', 'user_property': 1000000, 'execution_log': ['信息补充节点:正在补充信息.......']}, 'next': ['approval_human_node'], 'tasks': [{'id': '92a68ebc-396a-74ca-1745-00d3b7dd1e59', 'name': 'approval_human_node', 'path': ['__pregel_pull', 'approval_human_node'], 'error': None, 'interrupts': [], 'checkpoint': None, 'state': None, 'result': None}], 'metadata': {'model_name': 'gpt-4o', 'system_prompt': '你是乐于助人的AI代理,负责处理一般问题', 'langgraph_auth_user': None, 'langgraph_auth_user_id': '', 'langgraph_auth_permissions': [], 'langgraph_request_id': 'ff1991ff-9928-4f14-a3f5-c94e9cad51fe', 'graph_id': 'graph with interrupt', 'assistant_id': '9f4c0061-dc14-4557-adc7-05c2a75bb259', 'user_id': '', 'run_attempt': 1, 'langgraph_version': '0.6.8', 'langgraph_api_version': '0.4.38', 'langgraph_plan': 'developer', 'langgraph_host': 'self-hosted', 'langgraph_api_url': 'http://127.0.0.1:8123', 'run_id': '0199d746-6fe2-7596-9133-d8b28e134d78', 'thread_id': 'ec3b5bf7-61e3-471a-912e-ad181cc71bee', 'source': 'loop', 'step': 1, 'parents': {}}, 'created_at': '2025-10-12T07:15:42.633339+00:00', 'checkpoint': {'checkpoint_id': '1f0a73b4-3874-66d0-8001-71b0ae9ff3fc', 'thread_id': 'ec3b5bf7-61e3-471a-912e-ad181cc71bee', 'checkpoint_ns': ''}, 'parent_checkpoint': {'checkpoint_id': '1f0a73b4-386d-61ce-8000-66e7cbfd91dd', 'thread_id': 'ec3b5bf7-61e3-471a-912e-ad181cc71bee', 'checkpoint_ns': ''}, 'interrupts': [], 'checkpoint_id': '1f0a73b4-3874-66d0-8001-71b0ae9ff3fc', 'parent_checkpoint_id': '1f0a73b4-386d-61ce-8000-66e7cbfd91dd'}
"""

三,搜索中断的线程

线程在中断后,我们可能想搜索所有中断的线程,获取中断任务:

res = await client.threads.search(status="interrupted")
for interrupted_thread in res:
    print(interrupted_thread)

"""
{'thread_id': 'ec3b5bf7-61e3-471a-912e-ad181cc71bee', 'created_at': '2025-10-12T07:15:41.663814+00:00', 'updated_at': '2025-10-12T07:15:42.636340+00:00', 'metadata': {'graph_id': 'graph with interrupt', 'assistant_id': '9f4c0061-dc14-4557-adc7-05c2a75bb259'}, 'status': 'interrupted', 'config': {'configurable': {'model_name': 'gpt-4o', 'system_prompt': '你是乐于助人的AI代理,负责处理一般问题'}}, 'values': {'user_id': 'uid_001', 'user_name': '张三', 'loan_amount': 50000, 'loan_term': 36, 'user_credit': 'A', 'user_property': 1000000, 'execution_log': ['信息补充节点:正在补充信息.......']}, 'interrupts': {}, 'error': None}
{'thread_id': '0545f424-a751-4256-b7f1-327ca103265a', 'created_at': '2025-10-12T07:11:05.528371+00:00', 'updated_at': '2025-10-12T07:11:07.047592+00:00', 'metadata': {'graph_id': 'graph with interrupt', 'assistant_id': 'fd30b564-ace1-47d5-95c5-fb2fa4b3586d'}, 'status': 'interrupted', 'config': {'configurable': {'model_name': 'gpt-4o', 'system_prompt': '你是乐于助人的AI代理,负责处理一般问题'}}, 'values': {'user_id': 'uid_001', 'user_name': '李四', 'loan_amount': 50000, 'loan_term': 36, 'user_credit': 'A', 'user_property': 1000000, 'execution_log': ['信息补充节点:正在补充信息.......', '信息补充节点:正在补充信息.......']}, 'interrupts': {'7e3e1606-d6bc-9834-898b-659df3b2c980': [{'id': '36c1a997fbc75e87b697807b50ac67cf', 'value': {'question': '是否同意这笔贷款?', 'loan_details': '贷款信息:用户 李四, 金额 50000元,期限 36个月,信用评级 A,财产 1000000元。'}}]}, 'error': None}
{'thread_id': '14bc48c6-d002-4cbb-bbf9-d6f210fe6d2d', 'created_at': '2025-10-12T07:10:47.064375+00:00', 'updated_at': '2025-10-12T07:10:48.414529+00:00', 'metadata': {'graph_id': 'graph with interrupt', 'assistant_id': 'b350617c-fc87-442a-a236-023d3b2c2a95'}, 'status': 'interrupted', 'config': {'configurable': {'model_name': 'gpt-4o', 'system_prompt': '你是乐于助人的AI代理,负责处理一般问题'}}, 'values': {'user_id': 'uid_001', 'user_name': '李四', 'loan_amount': 50000, 'loan_term': 36, 'user_credit': 'A', 'user_property': 1000000, 'execution_log': ['信息补充节点:正在补充信息.......', '信息补充节点:正在补充信息.......']}, 'interrupts': {'11aacc1b-e5e0-a75d-3227-a4b83efa9c04': [{'id': 'd69e98fa6929052575ecd72ebcabbf8d', 'value': {'question': '是否同意这笔贷款?', 'loan_details': '贷款信息:用户 李四, 金额 50000元,期限 36个月,信用评级 A,财产 1000000元。'}}]}, 'error': None}
{'thread_id': 'e486836a-a84d-4f28-8c2d-852032af69b0', 'created_at': '2025-10-12T07:06:04.494705+00:00', 'updated_at': '2025-10-12T07:06:05.699647+00:00', 'metadata': {'graph_id': 'graph with interrupt', 'assistant_id': 'd971e8a3-cdc0-4649-85bf-398c2eb2cbac'}, 'status': 'interrupted', 'config': {'configurable': {'model_name': 'gpt-4o', 'system_prompt': '你是乐于助人的AI代理,负责处理一般问题'}}, 'values': {'user_id': 'uid_001', 'user_name': '李四', 'loan_amount': 50000, 'loan_term': 36, 'user_credit': 'A', 'user_property': 1000000, 'execution_log': ['信息补充节点:正在补充信息.......', '信息补充节点:正在补充信息.......']}, 'interrupts': {'317518a1-ed55-bdb3-007c-6e4c677dac3d': [{'id': '55117a844817bef13503fb927e203521', 'value': {'question': '是否同意这笔贷款?', 'loan_details': '贷款信息:用户 李四, 金额 50000元,期限 36个月,信用评级 A,财产 1000000元。'}}]}, 'error': None}
"""

四,恢复执行

从恢复处恢复,你只需要开始运行调用的时候,传递 thread_idCommand(update={}, resume={}) 对象。

Command 参数解释:

  • update:你要更新的状态字段,字典格式
  • resume:返回中断时,传递的参数。(节点内部获取,见第 5 节)

下面,我仅仅是修改了状态,因为 边上的断点,是 interrupt_before 指定的,没有接收中断参数的逻辑。

async def main():
    client = get_client(url="http://localhost:8123")
    
     # 恢复执行
    thread_id = "310ea2f2-8803-4979-911f-4b788ecc596a"
    assistant_id = "186d67b5-9c7b-49a6-9639-16ec05b15e2a"
    input_msg = {"user_name": "李四"}
    async for chunk in client.runs.stream(
        thread_id, 
        assistant_id, 
        command=Command(update=input_msg),   # 使用 Command 对象,update 仅更新状态
        stream_mode="updates"
    ):
        print(chunk)
    state = await client.threads.get_state(thread_id)
    print(f"恢复后的状态:{state}")

asyncio.run(main())

五,节点内部的中断

approval_human_node 是一个人工交互的节点,里面使用了 interrupt() 函数接收中断参数,即 人工输入的值。
当恢复运行的时候,一样的配方,传递 thread_idCommand

  • 这里我用 resume 参数,传递 中断参数,这样节点内部可以接收到我传递的 user_response 参数,从而恢复执行 (参考 approval_human_node 函数实现)
async def main():
    client = get_client(url="http://localhost:8123")
    
    # 5, 获取中断任务
    thread_id = "ec3b5bf7-61e3-471a-912e-ad181cc71bee"
    # 5.1 获取中断节点
    state = await client.threads.get_state(thread_id)
    print(f"中断节点:{state["next"]}")
    # 5.2 获取中断参数
    res = await client.threads.get(thread_id)
    if res["status"] == "interrupted" and res["interrupts"]:
        for interrupt_id, interrupt_list in res["interrupts"].items():
            for interrupt in interrupt_list:
                question = interrupt["value"].get("question")
                loan_details = interrupt["value"].get("loan_details")
                print("问题:", question)
                print("贷款详情:", loan_details)
    # 5.3 中断处恢复运行
    assistant_id = "9f4c0061-dc14-4557-adc7-05c2a75bb259"
    res = await client.runs.wait(thread_id, assistant_id, command=Command(resume={"user_response": "approve"}))
    print(res)

asyncio.run(main())

六,更新状态

补充:如果仅仅想更新状态,示例如下

thread_id = "ec3b5bf7-61e3-471a-912e-ad181cc71bee"
# values 包含要更新的字段; as_node 是刚刚执行过的节点,即最近执行过的节点
res = await client.threads.update_state(
    thread_id=thread_id, 
    values={"loan_amount": 100000}, 
    as_node="loan_info_supplement_node"
)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值