【LangGraph新手村系列】(4)人机协作中断:让 Agent 在关键节点停下来等你

简介: 解决"全自动Agent无法审查"的问题。在tools节点前插入interrupt_before中断点,改用stream流式运行实现暂停,通过get_state().next判断中断位置,审查工具调用后用Command(resume=True)恢复执行,实现人机协作的三重干预:审查放行、修改参数、拒绝执行。

第四章 人机协作中断:让 Agent 在关键节点停下来等你

"完全自动化的 Agent 是危险的——你永远不知道它会调用什么工具、花掉多少钱、泄露什么数据。真正可控的 Agent 知道在哪里停下来,等人类点头。"

一、问题

前三章的图是全自动的:模型自动思考、自动调用工具、自动返回结果。但放到真实业务中,"全自动"是双刃剑:

  • 不可审查的决策:模型决定调用 send_emaildelete_user,没有设闸就会真的执行。
  • 不可控的副作用:第三方 API 可能花钱、可能触发不可逆操作、可能泄露隐私。
  • 调试困难:自动执行的流程出错后只能翻日志。如果能暂停在"执行工具"之前,排查会快得多。
  • 信任危机:用户不信任一言不合就自动执行的系统。

核心矛盾:我们解决了"状态怎么流转"和"状态怎么持久化",但没有解决"流程怎么被人类控制"。Agent 需要暂停点

LangGraph 把这个能力叫做 Human-in-the-loop(HIL)——在图的任意节点之前或之后插入"中断点",执行流到达时停下来,暴露状态给调用方,等待人类介入后再继续。

二、解决方案

三件事:

  1. 声明中断点:编译图时告诉 LangGraph,在 tools 节点前暂停。
  2. 改用 stream 运行:用 stream 把执行变成"可暂停的直播"。
  3. 实现审查与恢复:用 get_state() 查看断点,用 input() 收集用户意愿,用 Command(resume=True) 继续。
START → agent → [should_continue 返回 "tools"] → [中断点] → 人类审查 → tools → agent → END
                                              ↑
                                        interrupt_before=["tools"]

三、工作原理

1. 为什么 invoke 不行?

final_state = app.invoke(inputs, config)
# 跑到 tools 前被 interrupt 拦住 → 抛出 GraphInterrupt → 程序崩溃

invoke 的设计是"一次性跑完并给我最终结果"。遇到中断时它不知道该怎么返回,只能抛异常。人机协作需要的是"平静地停下来"。

2. stream:把"黑盒"变成"直播"

# ❌ 不遍历 = 图根本不跑
app.stream(inputs, config, stream_mode="values")

# ✅ for 循环驱动生成器,图一步步执行
for chunk in app.stream(inputs, config, stream_mode="values"):
    pass  # pass = 不看中间战报,但要遍历完

stream 返回生成器,Python 生成器是"懒"的——你不索要下一个值,它就原地不动。遇到中断点时,stream 平静结束,不抛异常,你可以在循环结束后查看状态。

3. stream_mode="values"

每个 chunk 是当前完整状态快照

# 第一次 stream:跑完 agent,在中断点停住
{
   'messages': [HumanMessage(...), AIMessage(tool_calls=[...])], ...}

# 第二次 stream(Command(resume=True)):继续跑完剩余流程
{
   'messages': [..., ToolMessage(...), AIMessage("上海现在30℃...")], ...}

4. 判断中断:get_state().next

state = app.get_state(config)
if state.next:  # 空 () 表示跑完;('tools',) 表示被拦住了
    print(f">>> 中断:图即将进入节点 {state.next}")
属性 含义
state.values 当前所有状态字段的值
state.next 待执行的节点名元组。空=跑完,有值=中断暂停

5. 审查工具调用请求

last_message = state.values["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
    for tc in last_message.tool_calls:
        print(f"工具名: {tc['name']}, 参数: {tc['args']}")

中断发生时,模型已经输出了 tool_calls,但工具函数还没有执行。这是人类介入的最佳时机。

6. Command(resume=True) 的含义

# 用户同意,继续执行
for chunk in app.stream(Command(resume=True), config, stream_mode="values"):
    pass

Command(resume=True) 告诉 LangGraph:"从中断点继续执行,被拦住的节点(tools)自己去状态里读取所需数据,继续跑。"

resume 参数是"喂给被中断节点的新输入"。但 ToolNode 是全自动的——它会自己从 state["messages"][-1].tool_calls 读取工具调用指令,不需要你额外喂数据。所以 True 就是"我同意,继续"。

7. 完整执行流程

# 第1步:启动 stream,图跑到中断点停住
for chunk in app.stream(inputs, config, stream_mode="values"):
    pass

# 第2步:检查是否真的被拦住了
state = app.get_state(config)
if state.next:
    # 第3步:审查
    print("模型想调用:", state.values["messages"][-1].tool_calls)
    user_input = input("是否同意?(y/n): ")

    # 第4步:恢复或终止
    if user_input.lower() == "y":
        for chunk in app.stream(Command(resume=True), config, stream_mode="values"):
            pass
    else:
        print("用户拒绝执行")

# 第5步:输出最终结果
final = app.get_state(config)
print(final.values["messages"][-1].content)

四、核心组件一览

组件 类 / 函数 / 参数 作用
中断声明 interrupt_before=["tools"] 编译图时声明:执行流进入 tools 节点前自动暂停
流式执行 app.stream(...) 返回生成器,遍历驱动图执行,遇中断平静结束
战报格式 stream_mode="values" 每次 chunk 返回当前完整状态快照
状态读取 app.get_state(config) 获取指定 thread 的最新检查点 + 中断信息
断点判断 state.next 元组,空表示跑完,有元素表示在中断点暂停
恢复命令 Command(resume=True) 从中断点继续执行被拦住的节点
用户输入 input() 阻塞等待用户键盘输入,实现真正的人机协作

五、试一试

前置条件

.env 配置(同第三章):

OPENAI_API_KEY=sk-...
POSTGRES_URI=postgresql://postgres:你的密码@localhost:5432/langgraph_demo

执行脚本

cd demo-01
python langgraph-04.py

预期交互:

>>> [人机协作中断] 图即将进入节点: ('tools',)
模型请求调用以下工具:
  - 工具名: search
    参数: {'query': '上海天气'}

是否同意执行?(y/n): y
=== 最终回答 ===
上海目前的天气情况:温度30℃,有雾。

验证中断机制

  1. 输入 n 拒绝:程序打印"用户拒绝执行"并结束,工具函数不会被执行。
  2. 查看检查点:用 SQL 查询 checkpoints 表,能看到步骤数只累加到中断前的那一轮——拒绝后不会生成新的完整快照。
  3. 脏数据问题:如果之前运行失败(比如流被意外中断),同一个 thread_id 可能残留"模型喊了 tool_calls 但没等工具结果"的脏状态,下次运行会报 400 错误。解决方案:换个新 thread_id 或清理数据库。

六、完整代码差异

与第三章相比,只有执行逻辑变了。图的定义、状态、工具、模型、检查点——全部复用。

from langgraph.types import Command  # 新增导入

# 编译图时声明中断点
app = workflow.compile(
    checkpointer=checkpointer,
    interrupt_before=["tools"]
)

thread_id = "chapter-5"
config = {
   "configurable": {
   "thread_id": thread_id}}

inputs = {
   
    "messages": [HumanMessage(content="你好,我想查询一下上海的天气")],
    "temperature_unit": "摄氏度"
}

# 第1步:启动,跑到中断点停住
for chunk in app.stream(inputs, config, stream_mode="values"):
    pass

# 第2步:检查中断
state = app.get_state(config)
if state.next:
    print(f">>> [人机协作中断] 图即将进入节点: {state.next}")

    last_message = state.values["messages"][-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        print("模型请求调用以下工具:")
        for tc in last_message.tool_calls:
            print(f"  - 工具名: {tc['name']}")
            print(f"    参数: {tc['args']}")

    # 第3步:人工确认
    user_input = input("是否同意执行?(y/n):")
    if user_input.lower() == "y":
        # 第4步:恢复执行
        for chunk in app.stream(Command(resume=True), config, stream_mode="values"):
            pass
    else:
        print("用户拒绝执行")

# 第5步:输出最终回答
final_state = app.get_state(config)
print("\n=== 最终回答 ===")
print(final_state.values["messages"][-1].content)

七、其他

三种人工干预模式

LangGraph 的中断点不是只能"看和放行",你可以根据业务需求选择三种干预深度:

模式 操作 代码体现
审查放行 只看不改,用户确认后继续 Command(resume=True)
修改参数 审查后改掉工具调用的参数 app.update_state(config, {...}) + Command(resume=True)
拒绝执行 不让工具跑,塞一条假结果让模型重新回答 app.update_state(config, {"messages": [...]}) + Command(resume=True)

interrupt_before vs interrupt_after

参数 中断时机 适用场景
interrupt_before=["tools"] 进入节点之前 审查模型想调用什么工具,人工确认后再执行
interrupt_after=["tools"] 离开节点之后 审查工具返回的结果,人工修正后再交给模型

可以同时使用:interrupt_before=["tools"], interrupt_after=["tools"],实现"进入前审查请求 + 离开后审查结果"的双重控制。

关于 resume 参数的版本差异

理论上 Command(resume=None) 表示"不给被中断节点额外输入,让它自己读状态"。但部分 LangGraph 版本在处理 None 时存在内部变量未初始化的 bug,会报 UnboundLocalError: cannot access local variable 'resume_is_map'。如果你的版本遇到这个问题,改用 Command(resume=True) 即可。

脏数据陷阱

每次实验建议用新的 thread_id,或在数据库中清理旧记录:

DELETE FROM checkpoints WHERE thread_id = 'chapter-5';
DELETE FROM checkpoint_writes WHERE thread_id = 'chapter-5';
DELETE FROM checkpoint_blobs WHERE thread_id = 'chapter-5';

原因是:如果之前某次运行流被意外中断(比如程序崩溃),状态里可能残留一条"模型喊了 tool_calls 但还没等工具结果"的消息。下次恢复运行时,模型 API 会拒绝这种不完整的历史,报 400 - tool_call_ids did not have response messages

目录
相关文章
|
24天前
|
JSON 前端开发 API
【LangGraph新手村系列】(1)LangGraph 入门:StateGraph 与带记忆的 ReAct 循环
介绍 LangGraph 核心思想:用 StateGraph 把单次 LLM 调用串成可循环的 ReAct 工作流。通过节点、边与公共状态黑板,实现模型思考、工具调用、条件跳转的闭环,并引入检查点让 Agent 拥有跨轮次记忆。
365 1
【LangGraph新手村系列】(1)LangGraph 入门:StateGraph 与带记忆的 ReAct 循环
|
24天前
|
IDE 数据可视化 安全
【LangGraph新手村系列】(2)自定义状态与归约器:让 LangGraph 记住更多东西
从 MessagesState 扩展到 TypedDict 自定义状态,用 Annotated 声明字段归约策略。messages 挂载 add_messages 实现追加合并,其他字段默认覆盖。节点函数可读取自定义字段,让 Agent 记住用户偏好与业务元数据。
170 1
|
人工智能 NoSQL Java
【SpringAIAlibaba新手村系列】(8)持久化会话与 Redis 内存管理
本文详解 Spring AI 的会话记忆机制,从内存版 MemorySaver 到 Redis 版 RedisSaver,实现 AI 对话的上下文连续性。文章以 ReactAgent 为核心,讲解如何通过 threadId 管理会话线程,并将 Agent 状态持久化到 Redis 中。
792 5
|
1月前
|
NoSQL Java 数据库
【SpringAIAlibaba新手村系列】(11)Embedding 向量化与向量数据库
本文围绕 Embedding 与向量数据库展开,讲解了文本向量化、相似度检索和 VectorStore 的基本用法,并结合 SimpleVectorStore 示例说明了 Spring 中自动装配与手动注册 Bean 的区别,为后续学习 RAG 打下基础。
704 5
【SpringAIAlibaba新手村系列】(11)Embedding 向量化与向量数据库
|
1月前
|
人工智能 JSON 编解码
【SpringAIAlibaba新手村系列】(15)MCP Client 调用本地服务
本章从 MCP Client 视角说明如何连接上一章提供的本地服务,并把远端工具接入 ChatClient。重点讲解 Streamable-HTTP 配置、ToolCallbackProvider 的注入方式,以及模型如何通过 JSON-RPC 消息完成工具调用与结果回传。
442 21
|
人工智能 JavaScript Java
【SpringAIAlibaba新手村系列】(1)初识 Spring AI Alibaba 框架
本文介绍了SpringAIAlibaba框架的基本概念和使用方法。作为Spring官方AI框架的阿里云实现版本,它简化了Java开发者调用AI模型的过程。文章详细讲解了核心概念如ChatModel、ChatClient,以及阿里云百炼平台的功能。通过HelloWorld项目示例,展示了如何配置APIKey、编写控制层代码,实现普通调用和流式输出两种AI交互方式。重点阐述了SpringAI与SpringAIAlibaba的关系,以及自动配置机制的工作原理,帮助开发者快速上手这一框架。
4290 5
|
1月前
|
人工智能 JSON Java
【SpringAIAlibaba新手村系列】(7)结构化输出与对象映射
本文详解 Spring AI 结构化输出功能,通过 Java Record 与 .entity() 方法,实现 AI 的 JSON 响应自动映射为 Java 对象,解决纯文本难以集成的问题。文中还对比了 Lambda 写法并提供 Prompt 设计最佳实践。
392 4
|
1月前
|
人工智能 Java 定位技术
【SpringAIAlibaba新手村系列】(14)MCP 本地服务与工具集成
本章从 MCP Server 视角出发,说明如何将本地天气查询能力整理并暴露为标准化工具服务。内容涵盖 @Tool、ToolCallbackProvider、MethodToolCallbackProvider 的作用,以及 Streamable-HTTP 协议下服务端的能力注册与对外提供逻辑。
478 13
|
1月前
|
安全 前端开发 Java
【SpringSecurity新手村系列】(1)初识安全框架
本文从零开始引入 Spring Security,演示默认登录页与接口保护效果,并解释认证、授权与过滤器链的基础机制,帮助你快速建立安全开发的整体认知。
160 1

热门文章

最新文章