LangGraph 中断、工具调用与部署

文章目录

  • 一、中断
    • [1. 动态中断](#1. 动态中断)
      • [1.1. 概述](#1.1. 概述)
      • [1.2. 启用中断](#1.2. 启用中断)
      • [3. 恢复中断](#3. 恢复中断)
      • [1.4. 常见使用模式](#1.4. 常见使用模式)
        • [1.4.1. 基础 HITL 模式](#1.4.1. 基础 HITL 模式)
        • [1.4.2. 多个并行中断](#1.4.2. 多个并行中断)
        • [1.4.3. 审批模式](#1.4.3. 审批模式)
        • [1.4.4. 审核与编辑模式](#1.4.4. 审核与编辑模式)
        • [1.4.5. 工具执行审批模式](#1.4.5. 工具执行审批模式)
        • [1.4.6. 单节点串行中断模式](#1.4.6. 单节点串行中断模式)
      • [1.5. 使用规范](#1.5. 使用规范)
        • [1.5.1. 不要用 `try/except` 包裹 `interrupt()` 调用](#1.5.1. 不要用 try/except 包裹 interrupt() 调用)
        • [1.5.2. 不要更改单个节点内 `interrupt` 的调用顺序](#1.5.2. 不要更改单个节点内 interrupt 的调用顺序)
        • [1.5.3. 不要在 `interrupt()` 中传递复杂类型](#1.5.3. 不要在 interrupt() 中传递复杂类型)
        • [1.5.4. 断点之前的副作用操作必须是幂等的](#1.5.4. 断点之前的副作用操作必须是幂等的)
      • [1.6. 中断触发和恢复前后的检查点](#1.6. 中断触发和恢复前后的检查点)
    • [2. 静态断点](#2. 静态断点)
      • [2.1. 用法说明](#2.1. 用法说明)
      • [2.2. 底层原理](#2.2. 底层原理)
      • [2.3. 示例](#2.3. 示例)
        • [2.3.1. 编译时设置断点](#2.3.1. 编译时设置断点)
        • [2.3.2. 调用时设置断点](#2.3.2. 调用时设置断点)
  • 二、项目部署
    • [1. 本地部署并对接 LangSmith](#1. 本地部署并对接 LangSmith)
      • [1.1. 补充依赖](#1.1. 补充依赖)
      • [1.2. 准备本地项目](#1.2. 准备本地项目)
        • [1.2.1. 项目结构](#1.2.1. 项目结构)
        • [1.2.2. 代码清单](#1.2.2. 代码清单)
      • [1.3. 启动项目](#1.3. 启动项目)
      • [1.4. 调试](#1.4. 调试)
      • [1.5. 调试时添加静态断点](#1.5. 调试时添加静态断点)
    • [2. 本地部署并对接 AgentChatUI](#2. 本地部署并对接 AgentChatUI)
      • [2.1. 准备文件](#2.1. 准备文件)
        • [2.1.1. `chat_agent.py`](#2.1.1. chat_agent.py)
        • [2.1.2. 更改 `langgraph.json`](#2.1.2. 更改 langgraph.json)
      • [2.2. 对接 AgentChatUI](#2.2. 对接 AgentChatUI)
        • [2.2.1. 重启本地服务](#2.2.1. 重启本地服务)
        • [2.2.2. 访问云服务](#2.2.2. 访问云服务)
      • [2.3. 测试](#2.3. 测试)
        • [2.3.1. 对话](#2.3.1. 对话)
        • [2.3.2. 工具调用](#2.3.2. 工具调用)
        • [2.3.3. 查看历史记录](#2.3.3. 查看历史记录)
  • 三、工具调用节点
    • [1. 工具节点的实现](#1. 工具节点的实现)
      • [1.1. 手动处理工具调用](#1.1. 手动处理工具调用)
      • [1.2. 使用 ToolNode 处理工具调用](#1.2. 使用 ToolNode 处理工具调用)
    • [2. 进阶用法](#2. 进阶用法)
      • [2.1. ToolRuntime 介绍](#2.1. ToolRuntime 介绍)
      • [2.2. 在工具中更新状态](#2.2. 在工具中更新状态)
      • [2.3. 工具节点执行与容错机制](#2.3. 工具节点执行与容错机制)
        • [2.3.1. 实现重试机制](#2.3.1. 实现重试机制)
        • [2.3.2. 实现缓存机制](#2.3.2. 实现缓存机制)
    • 结语

LangGraph 的价值,不只是把几个 LLM 节点连成一条链,而是把 Agent 的状态、分支、暂停和恢复都变成可观察、可持久化的流程。

一、中断

1. 动态中断

1.1. 概述

LangGraph 提供动态中断和静态断点两种机制。

  • 动态中断在节点中调用 interrupt(),可以根据业务条件触发,适合人工审批、内容审核和补充信息。
  • 静态断点通过 interrupt_beforeinterrupt_after 设置,适合调试和观察节点执行过程。

动态中断触发后,图会把当前状态写入 checkpointer,并返回一个 Interrupt 对象。调用方收集人工输入,再通过 Command(resume=...) 恢复图的执行。

1.2. 启用中断

要让中断能够恢复,必须同时满足三个条件:配置 checkpointer、提供 thread_id、在节点内调用 interrupt()

python 复制代码
from typing import TypedDict

from dotenv import load_dotenv
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import interrupt

load_dotenv(override=True)


class OverallState(TypedDict, total=False):
    username: str


def ask_name(state: OverallState) -> dict:
    username = interrupt({"question": "请输入您的姓名"})
    return {"username": username}


builder = StateGraph(OverallState)
builder.add_node("ask_name", ask_name)
builder.add_edge(START, "ask_name")
builder.add_edge("ask_name", END)

graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "user-001"}}

interrupt() 的参数应当是字符串、数字、列表或字典等可 JSON 序列化的数据。建议把前端需要的字段直接组织成字典,例如 questionoptionscontext

3. 恢复中断

第一次调用会暂停在 interrupt()

python 复制代码
paused = graph.invoke({}, config=config)
item = paused["__interrupt__"][0]
print(item.value)

拿到人工输入后,使用相同的配置恢复:

python 复制代码
from langgraph.types import Command

finished = graph.invoke(
    Command(resume="小黄"),
    config=config,
)
print(finished["username"])

恢复时节点函数会从头重新执行,而不是从 interrupt() 所在的那一行继续。因此中断前的写库、扣款、发消息等副作用必须幂等,或者拆分到中断之后的独立节点中。

1.4. 常见使用模式

1.4.1. 基础 HITL 模式

HITL(Human In The Loop)指人在 Agent 执行过程中提供输入、修改或审批。最小模式就是"提问 -> 暂停 -> 恢复":

python 复制代码
def collect_address(state: OverallState) -> dict:
    address = interrupt({
        "question": "请输入收货地址",
        "required": True,
    })
    return {"address": address}

中断数据负责描述问题,恢复数据负责表达人的决定。两者不要混在状态字段中,便于前端和服务端分别处理。

1.4.2. 多个并行中断

多个并行节点可以同时暂停。恢复时按中断 ID 传入映射:

python 复制代码
resume_map = {
    item.id: get_human_answer(item.value)
    for item in paused["__interrupt__"]
}
graph.invoke(Command(resume=resume_map), config=config)

不要依赖 __interrupt__ 列表的顺序,因为并行节点完成的先后顺序并不固定。

1.4.3. 审批模式

审批节点可以同时更新状态并决定下一跳,此时使用 Command(goto=...)

python 复制代码
from typing import Literal
from langgraph.types import Command, interrupt


def approval_node(
    state: OverallState,
) -> Command[Literal["approved", "rejected"]]:
    approved = interrupt({"question": "是否批准本次操作?"})
    return Command(
        goto="approved" if approved else "rejected",
        update={"approved": bool(approved)},
    )

使用 Command 路由的节点不要再挂普通下游边,避免同一节点存在两套路由逻辑。

1.4.4. 审核与编辑模式

审核节点可以把模型生成的内容交给人修改,再将修改后的内容写回状态:

python 复制代码
def review_poem(state: OverallState) -> dict:
    edited = interrupt({
        "instruction": "请审核并修改内容",
        "content": state["draft"],
    })
    return {"draft": edited}

恢复值可以是修改后的完整文本,也可以是包含 actioncontent 的结构化字典。

1.4.5. 工具执行审批模式

工具调用审批通常放在 ToolNode 前面。模型先产生 tool_calls,审批节点展示工具名和参数,人同意后才进入工具节点:

python 复制代码
def approve_tool(state) -> Command[Literal["tools", "reject_tool"]]:
    call = state["messages"][-1].tool_calls[0]
    decision = interrupt({
        "tool": call["name"],
        "args": call["args"],
        "question": "是否允许执行?",
    })
    return Command(goto="tools" if decision == "approve" else "reject_tool")
1.4.6. 单节点串行中断模式

同一个节点可以多次调用 interrupt(),例如依次收集姓名、年龄和性别:

python 复制代码
def collect_profile(state: OverallState) -> dict:
    username = interrupt("请输入姓名")
    age = interrupt("请输入年龄")
    return {"username": username, "age": age}

恢复时必须保持相同的调用顺序。更复杂的表单建议拆成多个节点,让每个节点只负责一个稳定的中断。

1.5. 使用规范

1.5.1. 不要用 try/except 包裹 interrupt() 调用

interrupt() 依靠运行时的内部信号暂停图。捕获这个信号会让运行时无法识别中断。输入校验可以放在恢复之后,校验失败时再次调用 interrupt()

1.5.2. 不要更改单个节点内 interrupt 的调用顺序

恢复值按调用顺序消费。动态改变中断数量或顺序,会让恢复值对应到错误的问题。

1.5.3. 不要在 interrupt() 中传递复杂类型

传递给前端的数据应当可以被 JSON 序列化。数据库连接、函数、生成器和自定义实例都不适合作为中断值。

1.5.4. 断点之前的副作用操作必须是幂等的

中断恢复会重跑节点。写入使用幂等键或 upsert,支付、发信等不可重复操作则应放在恢复后的节点执行。

1.6. 中断触发和恢复前后的检查点

InMemorySaver 适合 Notebook、单进程脚本和测试:

python 复制代码
from langgraph.checkpoint.memory import InMemorySaver

graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "thread-001"}}

实例重建后历史会消失。需要跨进程或生产持久化时,应使用数据库型 checkpointer,并继续复用稳定的 thread_id。检查点保存的是图状态,不是外部系统的副作用,因此业务操作仍需要自行保证幂等。

2. 静态断点

2.1. 用法说明

静态断点适合调试节点执行前后的状态,不承载业务输入:

python 复制代码
graph = builder.compile(
    checkpointer=InMemorySaver(),
    interrupt_before=["ask_name"],
)

也可以在调用时设置:

python 复制代码
result = graph.invoke(
    {},
    config=config,
    interrupt_after=["ask_name"],
)

2.2. 底层原理

静态断点发生在节点边界,运行时保存当前快照并暂停。恢复时输入传 None,并保持相同的 thread_id 和断点配置。业务审批应使用动态 interrupt(),因为动态中断可以携带问题、选项和上下文。

2.3. 示例

2.3.1. 编译时设置断点
python 复制代码
debug_graph = builder.compile(
    checkpointer=InMemorySaver(),
    interrupt_before=["tools"],
)
debug_graph.invoke(input_data, config=config)
2.3.2. 调用时设置断点
python 复制代码
debug_graph.invoke(
    input_data,
    config=config,
    interrupt_after=["llm"],
)
debug_graph.invoke(
    None,
    config=config,
    interrupt_after=["llm"],
)

二、项目部署

1. 本地部署并对接 LangSmith

1.1. 补充依赖

powershell 复制代码
pip install "langgraph-cli[inmem]==0.4.30"

1.2. 准备本地项目

1.2.1. 项目结构
text 复制代码
hitl-demo/
├─ src/
│  ├─ __init__.py
│  └─ agent.py
├─ .env
└─ langgraph.json
1.2.2. 代码清单

src/agent.py 暴露名为 graph 的对象:

python 复制代码
from dotenv import load_dotenv

load_dotenv(override=True)

# build_graph 来自前文
graph = build_graph().compile()

使用 langgraph dev 时不要在这里传入 checkpointer,本地 Agent Server 会负责管理检查点。

langgraph.json

json 复制代码
{
  "dependencies": ["."],
  "graphs": {
    "weather_agent": "./src/agent.py:graph"
  },
  "env": ".env"
}

.env 可以加入 LangSmith 追踪配置:

properties 复制代码
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=your-langsmith-key
LANGSMITH_PROJECT=langgraph-weather-agent

1.3. 启动项目

powershell 复制代码
$env:PYTHONUTF8 = "1"
langgraph dev

启动后可以通过 Studio 查看图结构、输入状态和每个节点的执行记录:

https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024

1.4. 调试

在 Studio 中创建线程,提交用户消息。图运行到 interrupt() 时会显示工具名称和参数,提交 approvereject 后继续执行。每个线程对应一个 thread_id,因此可以多次打开同一线程查看历史状态。

1.5. 调试时添加静态断点

需要观察工具执行前的消息时,可以临时将图编译为:

python 复制代码
graph = build_graph().compile(interrupt_before=["tools"])

调试结束后移除静态断点,业务审批仍然保留在动态 interrupt() 中。

2. 本地部署并对接 AgentChatUI

2.1. 准备文件

2.1.1. chat_agent.py

前端只需要消费图返回的 messages__interrupt__ 字段。中断值建议使用稳定的 JSON 结构,便于 UI 渲染审批按钮。

2.1.2. 更改 langgraph.json

如果对话入口变量名为 graph,配置保持为:

json 复制代码
{
  "graphs": {
    "chat_agent": "./src/chat_agent.py:graph"
  },
  "env": ".env"
}

2.2. 对接 AgentChatUI

2.2.1. 重启本地服务

修改 langgraph.json 或 Python 文件后,重启 langgraph dev,让服务重新加载图定义。

2.2.2. 访问云服务

将本地服务地址配置到 AgentChatUI 的后端入口,前端发送用户消息,后端返回 messages;出现 __interrupt__ 时渲染审批控件,再用 Command(resume=...) 提交结果。

2.3. 测试

2.3.1. 对话

发送"北京天气怎么样",确认模型先产生工具调用,随后出现人工审批。

2.3.2. 工具调用

点击批准后,确认 ToolNode 执行工具,并且消息历史中存在匹配的 ToolMessage

2.3.3. 查看历史记录

刷新页面后复用原线程,确认历史消息和中断状态仍然可以读取。生产环境需要使用持久化 checkpointer,不要依赖进程内存。

三、工具调用节点

1. 工具节点的实现

1.1. 手动处理工具调用

手动实现工具节点有助于理解底层协议:读取最后一条 AIMessagetool_calls,查找工具并执行,再构造 ToolMessage

python 复制代码
from langchain.messages import ToolMessage


def manual_tool_node(state: MessagesState) -> dict:
    call = state["messages"][-1].tool_calls[0]
    result = get_weather.invoke(call["args"])
    return {
        "messages": [
            ToolMessage(content=result, tool_call_id=call["id"])
        ]
    }

这个写法适合教学和极简实验。真实项目还需要处理无效工具名、参数校验、异常、并发调用和 Command 传播。

1.2. 使用 ToolNode 处理工具调用

推荐将手写节点替换为:

python 复制代码
from langgraph.prebuilt.tool_node import ToolNode

tool_node = ToolNode(tools=[get_weather])

ToolNode 封装了标准工具执行流程,也支持多个工具调用。只要模型绑定了同一组工具,工具名和参数就能自动匹配。

2. 进阶用法

2.1. ToolRuntime 介绍

需要访问状态、上下文、tool_call_id 或流式写出器时,可以声明 runtime: ToolRuntime

python 复制代码
from langgraph.prebuilt.tool_node import ToolRuntime


@tool(parse_docstring=True)
def get_user_city(runtime: ToolRuntime) -> str:
    """读取当前用户所在城市。"""
    return runtime.state.get("city", "北京")

ToolRuntimeToolNode 自动注入。它与普通节点使用的 Runtime 不是同一个类型。

2.2. 在工具中更新状态

工具除了返回普通文本,也可以返回 Command(update=...)。此时要把对应的 ToolMessage 一起写入消息字段:

python 复制代码
from langchain.messages import ToolMessage
from langgraph.types import Command


@tool(parse_docstring=True)
def save_weather(city: str, runtime: ToolRuntime) -> Command:
    """查询天气并保存到状态。"""
    result = f"{city}:晴,25°C"
    return Command(
        update={
            "weather": result,
            "messages": [
                ToolMessage(
                    content=result,
                    tool_call_id=runtime.tool_call_id,
                )
            ],
        }
    )

2.3. 工具节点执行与容错机制

工具属于外部系统边界,网络波动和限流都很常见。建议把重试、超时和缓存放在工具层或 ToolNode 的包装器中,而不是让模型无限重复调用。

2.3.1. 实现重试机制
python 复制代码
def wrap_tool_call(request, execute):
    last_error = None
    for _ in range(3):
        try:
            return execute(request)
        except TimeoutError as exc:
            last_error = exc
    raise last_error


tool_node = ToolNode(tools, wrap_tool_call=wrap_tool_call)

只对可重试的异常进行重试。扣款、发货等有副作用的工具必须通过业务幂等键防止重复执行。

2.3.2. 实现缓存机制

天气、汇率等短时间内变化不大的查询可以缓存;缓存键至少应包含用户权限、城市和数据版本。带副作用的工具不要直接缓存执行结果。

结语

中断让 Agent 在关键节点停下来,工具节点让 Agent 获得真实世界的执行能力,部署服务则把这条流程交给可视化和持久化系统管理。三者组合起来,LangGraph 才不仅是一个调用模型的库,而是一套可以进入业务系统的 Agent 编排框架。

相关推荐
闲猫2 天前
LangGraph / Capabilities / Fault tolerance
python·agent·langgraph
闲猫2 天前
LangGraph / Capabilities / Stores
python·agent·langgraph
赵广陆4 天前
企业实战:Web服务端搭建
前端·langchain·langgraph
赵广陆4 天前
RAG企业实战:SSE快速入门
pycharm·langchain·langgraph
rising start4 天前
LangGraph 从状态图到可恢复 Agent:一篇入门与实践指南
langgraph
jjh+++(求关注版)4 天前
LangGraph Agent Checkpointer 持久化完全指南:从内存到生产的实战
python·langgraph
赵广陆5 天前
企业实战:数据图与状态定义
pycharm·langchain·langgraph
AI大佬的小弟5 天前
大模型名词精讲14:Multi-Agent(多智能体协作)
多智能体·multiagent·langgraph·ai协作·ai入门·a2a·大模型基础概念
赵广陆7 天前
企业实战:主体识别
langchain·pdf·langgraph