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_id、Command(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_id 和 Command :
- 这里我用 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"
)
LangGraph SDK 人机环路与恢复机制

1806

被折叠的 条评论
为什么被折叠?



