【LangGraph】House_Agent 实战(四):预定流程 —— 中断与人工干预

【LangGraph】LangGraph 实战(四):预定流程 ------ 中断与人工干预

    • 一、预定流程架构
      • [1.1 为什么预定流程不做成子图?](#1.1 为什么预定流程不做成子图?)
      • [1.2 流程概览](#1.2 流程概览)
      • [1.3 整体流程图](#1.3 整体流程图)
      • [1.4 节点说明](#1.4 节点说明)
      • [1.5 与推荐子图的对比](#1.5 与推荐子图的对比)
    • [二、interrupt 机制详解](#二、interrupt 机制详解)
      • [2.1 什么是 interrupt?](#2.1 什么是 interrupt?)
      • [2.2 interrupt 的工作原理](#2.2 interrupt 的工作原理)
      • [2.3 interrupt 的两种用法](#2.3 interrupt 的两种用法)
      • [2.4 interrupt 的客户端调用](#2.4 interrupt 的客户端调用)
    • 三、循环验证模式
      • [3.1 为什么需要循环验证?](#3.1 为什么需要循环验证?)
      • [3.2 get_title 节点](#3.2 get_title 节点)
      • [3.3 get_phone 节点](#3.3 get_phone 节点)
      • [3.4 get_id 节点](#3.4 get_id 节点)
      • [3.5 取消机制](#3.5 取消机制)
    • 四、工具定义与调用
      • [4.1 自定义工具 generate_orders](#4.1 自定义工具 generate_orders)
      • [4.2 ToolRuntime 与 InjectedStore](#4.2 ToolRuntime 与 InjectedStore)
      • [4.3 持久化存储逻辑](#4.3 持久化存储逻辑)
    • 五、主图中的预定流程集成
      • [5.1 ReserveState 状态定义](#5.1 ReserveState 状态定义)
      • [5.2 add_reserve_message 节点](#5.2 add_reserve_message 节点)
      • [5.3 call_orders 节点](#5.3 call_orders 节点)
      • [5.4 cancel_node 节点](#5.4 cancel_node 节点)
      • [5.5 在主图中添加预定节点](#5.5 在主图中添加预定节点)
      • [5.6 条件边配置](#5.6 条件边配置)
      • [5.7 路由入口](#5.7 路由入口)
    • [六、tools_condition 详解](#六、tools_condition 详解)
      • [6.1 什么是 tools_condition?](#6.1 什么是 tools_condition?)
      • [6.2 判断逻辑](#6.2 判断逻辑)
    • 七、执行效果展示
      • [7.1 正常预定流程](#7.1 正常预定流程)
      • [7.2 验证重试示例](#7.2 验证重试示例)
      • [7.3 取消预定示例](#7.3 取消预定示例)
    • 八、副作用处理最佳实践
      • [8.1 什么是副作用?](#8.1 什么是副作用?)
      • [8.2 预定流程的副作用控制](#8.2 预定流程的副作用控制)
    • 九、本文总结

上一章------>【LangGraph】House_Agent 实战(三):推荐子图 ------ 数据库交互与工具调用

一、预定流程架构

1.1 为什么预定流程不做成子图?

在第二篇中我们提到,推荐流程以子图 形式嵌入主图

但预定流程不同------它的节点直接集成在主图中,而不是封装为独立子图

原因在于 interrupt 机制的事件传递:

方案A:预定子图(interrupt 事件传递路径长)

方案B:直接节点(interrupt 事件直接到达)

interrupt 事件需要从子图传递到主图再到客户端时,事件转发链路变长,增加了调试难度和出错概率,

将预定节点直接放在主图中,interrupt 事件可以直接从主图发出,前端接收更可靠


1.2 流程概览

预定流程负责收集用户信息、生成预定工单

需要多轮人工交互------用户必须依次输入房源名称、手机号、身份证号,每一步都需要验证

主要流程:

  1. 收集房源名称:用户输入要预定的房源
  2. 收集手机号:用户输入手机号,格式校验
  3. 收集身份证号:用户输入身份证号,格式校验
  4. 生成工单:调用工具生成预定工单并持久化存储

1.3 整体流程图

python 复制代码
# 生成命令(预定流程节点在主图中,直接打印主图即可)
print(graph.get_graph(xray=True).draw_mermaid())

1.4 节点说明

节点 职责 输入 输出
get_title 收集房源名称 用户输入(interrupt) title
get_phone 收集手机号 用户输入(interrupt) phone_number
get_id 收集身份证号 用户输入(interrupt) id_card
add_reserve_message 构建预定消息 title + phone + id HumanMessage
call_orders LLM 决定是否调用工具 消息列表 工具调用或最终回复
tool_node 执行 generate_orders 工具 工具调用参数 工单结果
cancel_node 取消预定 - 取消提示消息

1.5 与推荐子图的对比

维度 推荐子图 预定流程
集成方式 子图(独立编译) 主图直接节点
交互模式 单轮查询 多轮收集
核心技术 SQLDatabaseToolkit interrupt + ToolNode
循环机制 SQL 生成→执行循环 验证→重试循环
副作用 生成工单、持久化存储
工具调用 系统内置工具 自定义工具

二、interrupt 机制详解

2.1 什么是 interrupt?

interrupt 是 LangGraph 提供的人工干预原语

当节点执行到 interrupt() 时,图会暂停执行,等待外部输入

用户输入后,图从暂停点恢复继续执行

python 复制代码
from langgraph.types import interrupt

def my_node(state):
    # 执行到这里时,图会暂停
    user_input = interrupt("请输入你的名字:")

    # 用户输入后,图恢复执行
    return {"name": user_input}

执行流程


2.2 interrupt 的工作原理

LangGraph 的 interrupt 机制依赖于 Checkpoint(检查点)

  1. 暂停时:图的状态被保存到 Checkpoint
  2. 恢复时 :从 Checkpoint 加载状态,将用户输入注入到 interrupt() 的返回值
  3. 继续执行 :节点从 interrupt() 处继续执行

这意味着即使服务重启,只要有 Checkpoint,就能恢复到暂停点继续执行


2.3 interrupt 的两种用法

用法一:收集用户输入(本项目使用)

python 复制代码
def get_title(state):
    prompt = '请输入要预定的房源名称'
    title = interrupt(prompt)    # 等待用户输入
    return {"title": title}

用法二:人工审核确认

python 复制代码
def review_node(state):
    ai_content = state["generated_content"]
    user_decision = interrupt(f"AI 生成的内容:\n{ai_content}\n\n请确认或修改:")
    return {"final_content": user_decision}

两种用法的核心机制相同,只是语义不同。在预定流程中,interrupt 用于逐步收集信息


2.4 interrupt 的客户端调用

客户端通过 LangGraph API 恢复被中断的图:

python 复制代码
# 首次调用 - 图会在 interrupt 处暂停
result = graph.invoke({"messages": [HumanMessage("我要预定")]})

# 恢复执行 - 传入用户输入
from langgraph.types import Command
result = graph.invoke(Command(resume="长安花园"), config)

在 SSE 流式场景中,客户端会收到一个 interrupt 事件,包含提示信息;用户输入后,发送 resume 请求恢复执行。


三、循环验证模式

3.1 为什么需要循环验证?

用户输入不可信,手机号可能是无效格式,身份证号可能少一位

我们需要在每个收集节点中实现验证-重试循环:


3.2 get_title 节点

python 复制代码
# src/agent/node/reserve.py
from langgraph.types import interrupt

_CANCEL_KEYWORDS = {"取消", "退出", "不需要", "跳过", "算了"}

def _is_cancel(text: str) -> bool:
    return text.strip() in _CANCEL_KEYWORDS

def get_title(state: ReserveState):
    prompt = '请输入要预定的房源名称(输入"取消"可退出)'
    while True:
        title = interrupt(prompt)
        if _is_cancel(title):
            return {"cancel": True}
        if not title or not title.strip():
            prompt = "房源名称不能为空,请重新输入。"
        else:
            return {"title": title.strip()}

关键设计

  1. while True 循环:节点内部循环验证,直到输入有效才返回
  2. 取消支持 :检测取消关键词,设置 cancel 标志
  3. 动态提示:验证失败时更新提示信息,引导用户正确输入
  4. 空值检查:防止用户提交空内容

3.3 get_phone 节点

python 复制代码
import re

def get_phone(state: ReserveState):
    prompt = '请输入要预定的手机号(输入"取消"可退出)'
    while True:
        phone_number = interrupt(prompt)
        if _is_cancel(phone_number):
            return {"cancel": True}
        if not phone_number or not phone_number.strip():
            prompt = "手机号不能为空,请重新输入。"
        elif not re.match(r"^1[3-9]\d{9}$", phone_number.strip()):
            prompt = f"'{phone_number}' 不是有效的手机号,请输入11位手机号码。"
        else:
            return {"phone_number": phone_number.strip()}

验证规则

  • 不能为空
  • 必须匹配 ^1[3-9]\d{9}$(11位手机号,以1开头,第二位3-9)
  • 验证失败时返回具体的错误信息

3.4 get_id 节点

python 复制代码
def get_id(state: ReserveState):
    prompt = '请输入要预定的身份证号码(输入"取消"可退出)'
    while True:
        id_card = interrupt(prompt)
        if _is_cancel(id_card):
            return {"cancel": True}
        if not id_card or not id_card.strip():
            prompt = "身份证号码不能为空,请重新输入。"
        elif not re.match(r"^\d{17}[\dXx]$", id_card.strip()):
            prompt = f"'{id_card}' 不是有效的身份证号码,请输入18位身份证号码。"
        else:
            return {"id_card": id_card.strip()}

验证规则

  • 不能为空
  • 必须匹配 ^\d{17}[\dXx]$(18位,前17位数字,最后一位数字或X)
  • 最后一位支持大小写 X

3.5 取消机制

三个收集节点都支持取消操作:

python 复制代码
_CANCEL_KEYWORDS = {"取消", "退出", "不需要", "跳过", "算了"}

def _is_cancel(text: str) -> bool:
    return text.strip() in _CANCEL_KEYWORDS

当用户输入取消关键词时:

  1. 节点返回 {"cancel": True}
  2. 条件边 _should_continue 检查 cancel 标志
  3. 跳转到 cancel_node,返回友好的取消提示
python 复制代码
def _should_continue(state: ReserveState):
    return "cancel" if state.get("cancel") else "continue"

四、工具定义与调用

4.1 自定义工具 generate_orders

python 复制代码
# src/agent/node/reserve.py
import uuid
from typing import Annotated, Any
from langchain.tools import tool
from langgraph.prebuilt import ToolNode, ToolRuntime, InjectedStore
from src.agent.common.store import ReservedInfo, UserPreferences

@tool
def generate_orders(
    phone_number: str,
    id_card: str,
    title: str,
    runtime: ToolRuntime,
    store: Annotated[Any, InjectedStore()]
) -> str:
    """
    根据用户电话,身份证,预定房源。

    Args:
        phone_number: 用户电话
        id_card: 身份证
        title: 用户要预定的房源标题
        runtime: 工具的运行时信息
        store: 注入工具的持久存储
    """
    # 1. 生成工单号
    order_id = str(uuid.uuid4())

    # 2. 构建预定信息
    reserved_info = ReservedInfo(
        order_id=order_id,
        phone_number=phone_number,
        title=title,
    )

    # 3. 持久化存储
    user_id = runtime.context.get("user_id") if runtime.context else None
    if not user_id:
        return f"预定失败:无法获取用户信息"

    namespace = (user_id, "preferences")
    prefs_result = store.search(namespace)

    if len(prefs_result) == 0:
        # 无持久化信息,新增
        prefs = UserPreferences(reserved_info=[reserved_info])
        store.put(
            namespace,
            str(uuid.uuid4()),
            prefs.model_dump(exclude_none=True)
        )
    else:
        # 有偏好数据,更新
        prefs = prefs_result[0].value or {}
        prefs.setdefault("reserved_info", []).append(reserved_info.model_dump())
        store.put(namespace, prefs_result[0].key, prefs)

    return f"已预定房源为:{title},预定工单号为:{order_id}"

4.2 ToolRuntime 与 InjectedStore

这是 LangGraph 提供的两个重要机制:

ToolRuntime :工具的运行时上下文,提供对 context 的访问

python 复制代码
# 获取运行时上下文中的 user_id
user_id = runtime.context.get("user_id")

InjectedStore:将持久化存储注入到工具中

python 复制代码
# 类型注解方式注入 store
store: Annotated[Any, InjectedStore()]

这两个参数不会出现在工具的 schema 中,LLM 不会尝试填充它们------它们由框架在运行时自动注入。

设计意图

参数 来源 用途
phone_number LLM 工具调用 用户电话
id_card LLM 工具调用 身份证号
title LLM 工具调用 房源名称
runtime 框架注入 获取 user_id
store 框架注入 持久化存储

4.3 持久化存储逻辑

工具中的存储逻辑遵循查询-更新/新增模式:

python 复制代码
namespace = (user_id, "preferences")
prefs_result = store.search(namespace)

if len(prefs_result) == 0:
    # 首次预定:创建新的偏好记录
    prefs = UserPreferences(reserved_info=[reserved_info])
    store.put(namespace, str(uuid.uuid4()), prefs.model_dump(exclude_none=True))
else:
    # 已有记录:追加预定信息
    prefs = prefs_result[0].value or {}
    prefs.setdefault("reserved_info", []).append(reserved_info.model_dump())
    store.put(namespace, prefs_result[0].key, prefs)

Store 的 namespace 设计

复制代码
namespace: (user_id, "preferences")
    │
    ├── key: uuid-1
    └── value: {
            "budget_min": 3000,
            "budget_max": 5000,
            "reserved_info": [
                {"order_id": "xxx", "title": "长安花园", "phone_number": "138..."}
            ]
        }

五、主图中的预定流程集成

5.1 ReserveState 状态定义

python 复制代码
# src/agent/state/reserve.py
from typing import Optional
from langgraph.graph import MessagesState

class ReserveState(MessagesState):
    title: Optional[str] = None       # 预定的房源
    phone_number: Optional[str] = None # 预定电话
    id_card: Optional[str] = None      # 身份证

设计特点

  • 继承 MessagesState,自动拥有 messages 字段
  • 添加三个私有字段用于收集预定信息
  • 使用 Optional 类型,初始值为 None

5.2 add_reserve_message 节点

收集完所有信息后,需要将它们组装成一条消息:

python 复制代码
# src/agent/node/reserve.py
from langchain_core.messages import HumanMessage

def add_reserve_message(state: ReserveState):
    reserve_prompt = """根据提供的信息,帮我预定房源。
    - 预定的房源标题: {title}
    - 用户预定号码: {phone_number}
    - 用户身份证号码: {id_card}"""

    reserve_message = HumanMessage(content=reserve_prompt.format(
        title=state['title'],
        phone_number=state['phone_number'],
        id_card=state['id_card']
    ))

    return {"messages": [reserve_message]}

为什么需要这个节点?

将结构化的状态字段转换为自然语言消息,供 LLM 理解并决定是否调用工具。


5.3 call_orders 节点

python 复制代码
# src/agent/node/reserve.py
from langchain_core.messages import SystemMessage
from src.agent.common.llm import model

def call_orders(state: ReserveState):
    result = model.bind_tools([generate_orders]).invoke(
        [SystemMessage(content="你是一个工单生成的助手,支持调用工具进行房源预定工单生成。"
                               "支持查看查询的结果并返回最终答案")]
        + state["messages"]
    )
    return {"messages": [result]}

关键点

  • LLM 绑定 generate_orders 工具
  • LLM 根据消息内容决定是否调用工具
  • 如果 LLM 认为信息完整,会生成工具调用
  • 如果信息不足,会返回自然语言回复

5.4 cancel_node 节点

python 复制代码
def cancel_node(state: ReserveState):
    return {"messages": [AIMessage(content="已取消预定,如需帮助请随时告诉我~")]}

当用户在任何收集步骤中输入取消关键词时,流程跳转到此节点


5.5 在主图中添加预定节点

预定流程的节点直接在 graph.py 中注册:

python 复制代码
# src/agent/graph.py(预定流程部分)
from src.agent.node.reserve import (
    get_title, get_phone, get_id,
    add_reserve_message, call_orders,
    tool_node, cancel_node, _should_continue
)

# 预定流程节点(直接集成在主图,确保 interrupt 事件能转发到前端)
builder.add_node(get_title)
builder.add_node(get_phone)
builder.add_node(get_id)
builder.add_node(add_reserve_message)
builder.add_node(call_orders)
builder.add_node("tool_node", tool_node)
builder.add_node(cancel_node)

5.6 条件边配置

每个收集节点之后都通过 _should_continue 检查是否取消:

python 复制代码
# 每步检查取消
builder.add_conditional_edges("get_title", _should_continue, {"continue": "get_phone", "cancel": "cancel_node"})
builder.add_conditional_edges("get_phone", _should_continue, {"continue": "get_id", "cancel": "cancel_node"})
builder.add_conditional_edges("get_id", _should_continue, {"continue": "add_reserve_message", "cancel": "cancel_node"})

# 正常流程
builder.add_edge("add_reserve_message", "call_orders")

# 工具调用循环
builder.add_conditional_edges("call_orders", tools_condition, {"tools": "tool_node", "__end__": END})
builder.add_edge("tool_node", "call_orders")

# 取消出口
builder.add_edge("cancel_node", END)

流程图


5.7 路由入口

从意图识别和推荐子图都可以进入预定流程:

python 复制代码
# 意图识别直接路由
def router_message(state):
    ...
    elif user_intent == "reserve_house":
        return "get_title"          # 直接路由到预定流程

# 推荐完成后的询问
def should_reserve(state):
    if state["reserve"] == "需要":
        return "get_title"          # 跳转到预定流程
    else:
        return END

六、tools_condition 详解

6.1 什么是 tools_condition?

tools_condition 是 LangGraph 内置的条件路由函数,用于判断 LLM 是否生成了工具调用:

python 复制代码
from langgraph.prebuilt import tools_condition

builder.add_conditional_edges(
    "call_orders",
    tools_condition,   # 内置条件路由
    {
        "tools": "tool_node",   # 有工具调用 → 执行工具
        "__end__": END,          # 无工具调用 → 结束
    }
)

6.2 判断逻辑

python 复制代码
# 简化版实现
def tools_condition(state):
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tools"    # LLM 生成了工具调用
    else:
        return "__end__"  # LLM 返回了最终回复

工作流程


七、执行效果展示

7.1 正常预定流程

(此处留白,插入实际运行效果)

示例对话

复制代码
系统:请输入要预定的房源名称(输入"取消"可退出)
用户:长安花园

系统:请输入要预定的手机号(输入"取消"可退出)
用户:13800138000

系统:请输入要预定的身份证号码(输入"取消"可退出)
用户:110101199001011234

助手:已预定房源为:长安花园,预定工单号为:a1b2c3d4-e5f6-7890-abcd-ef1234567890

7.2 验证重试示例

复制代码
系统:请输入要预定的手机号(输入"取消"可退出)
用户:abc

系统:'abc' 不是有效的手机号,请输入11位手机号码。
用户:123

系统:'123' 不是有效的手机号,请输入11位手机号码。
用户:13800138000

系统:请输入要预定的身份证号码(输入"取消"可退出)

7.3 取消预定示例

复制代码
系统:请输入要预定的房源名称(输入"取消"可退出)
用户:取消

助手:已取消预定,如需帮助请随时告诉我~

八、副作用处理最佳实践

8.1 什么是副作用?

在 Agent 开发中,副作用(Side Effect) 指的是对外部系统产生影响的操作:

操作 副作用类型 风险等级
查询数据库 只读
生成工单 写入
发送通知 不可逆
调用支付接口 资金变动 极高

8.2 预定流程的副作用控制

在预定流程中,副作用被集中在 generate_orders 工具中:

设计要点

  1. 收集阶段无副作用:get_title、get_phone、get_id 只做输入收集和验证
  2. 工具层执行副作用:generate_orders 是唯一的写入操作
  3. LLM 作为决策层:call_orders 让 LLM 决定是否执行工具,增加了灵活性
  4. 可追溯记录:每笔预定生成唯一的 order_id(UUID)

九、本文总结

本文详细介绍了预定流程的实现:

  1. 为什么不做子图:interrupt 事件直接从主图发出,传递更可靠
  2. interrupt 机制:暂停执行等待人工输入,依赖 Checkpoint 实现状态保存
  3. 循环验证模式:while True + interrupt 实现输入验证和重试
  4. 自定义工具:generate_orders 工具的定义和持久化逻辑
  5. ToolRuntime 与 InjectedStore:框架自动注入的运行时参数
  6. tools_condition:内置的工具调用条件路由
  7. 副作用处理:收集阶段无副作用,工具层执行写入操作

下一篇文章

我们将深入探讨持久化、流式输出与部署,学习如何让 Agent 在生产环境中稳定运行


本文是 House_Agent 实战系列的第二篇
如果觉得有帮助,欢迎点赞和分享!
咱们下篇再见~~~~

相关推荐
AI玫瑰助手7 小时前
Python运算符:比较运算符(等于不等等于大于小于)与返回值
android·开发语言·python
AI技术控7 小时前
LangChain 是什么?从零开始学会 LangChain 的工程实践指南
人工智能·语言模型·自然语言处理·langchain·nlp
陈天伟教授7 小时前
图解人工智能(32)深度学习前沿
人工智能·深度学习
RSTJ_16257 小时前
PYTHON+AI LLM DAY FIFITY-TWO
人工智能
Ting-yu7 小时前
Spring AI Alibaba零基础速成(5) ---- Memory(记忆)
java·人工智能·后端·spring
幂律智能7 小时前
从AI使用风险到合同智能审查重构企业风控能力
人工智能·重构
GIOTTO情7 小时前
Infoseek舆情处置系统的技术实现与落地实践
python
视***间7 小时前
端侧大模型落地新标杆:视程空间将GPT-OSS边缘AI深度导入NVIDIA Jetson平台
人工智能·gpt·边缘计算·nvidia·ai算力·gpt-oss·视程空间
189228048618 小时前
NY379固态MT29F32T08GSLBHL8-36QA:B
大数据·服务器·人工智能·科技·缓存