一、为什么需要 Human-in-the-loop?
LLM 可以自动规划并调用工具,但转账、发送邮件、删除数据、发布内容等操作不能完全交给模型。LangGraph 的 Human-in-the-loop 机制允许工作流在关键节点暂停,等待人工批准、拒绝或修改参数,再继续执行。
基于 HumanReviewExperience 下的 6 个示例,其核心组合是:
interrupt() + checkpointer + thread_id + Command(resume=...)
二、示例1:暂停与恢复工作流
示例1:节点通过 interrupt() 暂停:
python
def review_node(state: State):
decision = interrupt("是否继续?")
if decision == "yes":
return {"output": "继续执行"}
return {"output": "人工终止"}
图必须配置 checkpointer:
graph = builder.compile(checkpointer=InMemorySaver())
第一次执行触发中断:
python
config = {"configurable": {"thread_id": "111"}}
result = graph.invoke(
{"input": "hi", "output": ""},
config=config,
)
print(result["__interrupt__"][0].value)
获得人工输入后,使用相同的 thread_id 恢复:
python
result = graph.invoke(
Command(resume="yes"),
config=config,
)
resume 中的数据会成为 interrupt() 的返回值。
三、示例2:人工审批与条件分支
示例2展示了转账审批:
python
def approval_node(state: ApprovalState):
decision = interrupt({
"question": "是否批准此操作?",
"details": state["action_details"],
"options": ["批准", "拒绝"],
})
return {
"status": "批准" if decision == "批准" else "取消"
}
审批完成后使用条件边分流:
python
def route_by_status(state: ApprovalState):
if state["status"] == "批准":
return "proceed_node"
return "cancel_node"
流程为:
提交转账 -> 人工审批 -> 批准执行 / 拒绝取消
四、示例3:使用 Command 动态跳转
示例3使用 Command 同时更新状态和指定下一个节点:
python
if decision == "批准":
return Command(
goto="proceed_node",
update={"status": "批准"},
)
return Command(
goto="cancel_node",
update={"status": "取消"},
)
条件边适合固定且清晰的业务分支;Command(goto, update) 更适合节点内部需要动态决定跳转的场景。
五、示例4:人工编辑内容
示例4展示了把待审核内容交给人工修改:
python
def review_node(state: State):
updated = interrupt({
"instruction": "查看并编辑内容",
"content": state["text"],
})
if updated["success"]:
return {}
return {"text": updated["content"]}
恢复时可以传入结构化数据:
python
Command(resume={
"success": False,
"content": "人工修改后的文章",
})
原代码漏掉了节点注册,需要补充:
builder.add_node("review_node", review_node)
同时建议使用布尔值 True、False,不要使用字符串 "true"、"false"。
六、示例5:在工具执行前增加人工审核
示例5把 interrupt() 放在邮件工具内部:
python
@tool
def send_email(to: str, subject: str, body: str):
review = interrupt({
"action": "send_email",
"to": to,
"subject": subject,
"body": body,
"message": "是否同意发送邮件?",
})
if review["decision"] == "cancel":
return "用户取消发送邮件"
final_to = review.get("to", to)
final_subject = review.get("subject", subject)
final_body = review.get("body", body)
return send_email_to_provider(
final_to,
final_subject,
final_body,
)
人工可以批准、取消,也可以修改模型生成的收件人、主题和正文。
原代码用 "不同意,需要修改主题" 与 "不同意" 比较,两者并不相等。建议设计稳定的审批协议:
json
{
"decision": "edit",
"changes": {
"subject": "修改后的主题"
}
}
七、示例6:循环获取合法输入
示例6演示了不断请求用户输入,直到年龄合法:
python
def get_age_node(state: State):
prompt = "请输入年龄:"
while True:
age = interrupt(prompt)
if isinstance(age, int) and age > 0:
return {"age": age}
prompt = "请输入有效年龄:必须是大于 0 的整数"
原代码虽然更新了 prompt,但调用的始终是固定文本 interrupt("请输入年龄:"),所以错误提示不会生效。
八、中断节点必须具备幂等性
恢复工作流时,包含 interrupt() 的节点会从头重新执行,因此中断前的代码可能执行多次。
错误写法:
python
charge_credit_card()
approved = interrupt("是否批准?")
恢复时可能重复扣款。
正确写法:
python
approved = interrupt("是否批准扣款?")
if approved:
charge_credit_card()
邮件发送、扣款、写数据库等副作用应放在中断之后,并增加业务幂等键。
九、生产环境建议
使用 PostgreSQL 等持久化 checkpointer,不能依赖 InMemorySaver。
用订单号、任务 ID 或 UUID 作为稳定的 thread_id。
使用结构化审批数据,避免解析自然语言决定。
保存审批人、时间、原始参数、修改参数和审批结论。
校验审批人是否有权恢复对应工作流。
防止同一个审批请求被重复恢复。
为循环输入和多轮审核设置最大次数。
权限判断和金额阈值必须由业务代码控制,不能交给模型。
十、总结
LangGraph 人机交互并不是简单调用一次 input(),而是一套可暂停、可持久化、可恢复、可审计的工作流机制。补上持久化、权限、幂等和审计后,它就能用于真正的生产级人机协作系统。