LangGraph 与 Human-in-the-Loop 实战指南

LangGraph 与 Human-in-the-Loop 实战指南

LangGraph 是一个把 AI 工作流建模成有状态图的框架。它解决了普通 LLM 调用无法处理的几类问题:多步骤分支、循环重试、流程中断与恢复、以及人类介入审批。

本文系统讲解 LangGraph 的五大核心概念(State、Node、Edge、条件边、终止条件),并深入剖析 Human-in-the-Loop(HITL)的三个核心机制(interrupt、Checkpointer、Command),最后用一个完整可运行的示例串联所有知识点。


一、何时需要 LangGraph

判断标准很简单------对比下表:

特征 普通 LLM 调用 LangGraph
流程 一问一答 多步骤、有分支
状态 无需保存 需要跨步骤共享数据
决策路径 固定 根据中间结果动态跳转
人类介入 不需要 关键节点需要确认
循环 可能需要重试、迭代

满足其中 2 项以上,就值得用 LangGraph。

典型场景:

  • AI 理财顾问下单(涉及资金,必须人类确认)
  • 合同审查(AI 标记风险点,人类决定是否签)
  • 客服工单流转(简单问题自动答,复杂问题转人工)
  • 代码 Review Agent(AI 提建议,人决定接受哪些)
  • 邮件群发助手(AI 起草,人审核后发出)

二、核心三要素:State、Node、Edge

LangGraph 的整个心智模型,就是这张图:

复制代码
      ┌─────────────────────────────────────┐
      │  State(共享数据,所有节点都能读写)    │
      └─────────────────────────────────────┘
                       ▲
                       │ 读 / 写
                       │
   START ──→ [Node A] ──→ [Node B] ──→ END
              ↑           ↑
              └───────────┘
              Edge(连接节点的边)

State------流程的"共享内存"

TypedDict 定义,类似一张表单结构。每个节点都能读写其中的字段。

python 复制代码
from typing import TypedDict

class PostState(TypedDict):
    idea:     str   # 用户的想法
    content:  str   # AI 生成的文案
    decision: str   # 用户的决定

关键规则 :节点返回的字典不是"覆盖整个 State",而是"只更新返回的字段"。其他字段保持不变。LangGraph 内部会自动合并。

Node------一个普通的 Python 函数

python 复制代码
def write(state: PostState) -> dict:
    response = llm.invoke([HumanMessage(content=state["idea"])])
    return {"content": response.content}   # 只返回变化的字段

节点函数的三条规范:

  1. 接收 state(整个共享内存)
  2. 返回 一个字典,只包含要更新的字段
  3. 不要直接修改 state,永远用 return 的方式更新

Edge------节点之间的连接

普通边表示固定方向:

python 复制代码
builder.add_edge(START, "write")       # 流程从 write 开始
builder.add_edge("write", "confirm")   # write 完了 → confirm

条件边表示动态路由(下一节详述)。


三、条件边------流程的动态分支

条件边的本质是一个路由函数 :根据当前 State 的内容,返回下一个节点的名字(字符串)

python 复制代码
def route(state: PostState) -> str:
    if state["decision"] == "post":
        return "post"      # 跳到 post 节点
    else:
        return "cancel"    # 跳到 cancel 节点

builder.add_conditional_edges("confirm", route)

循环的实现

LangGraph 没有专门的循环语法。条件边返回上游节点的名字,就形成循环:

python 复制代码
def route(state):
    if state["satisfied"]:
        return "publish"      # 满意 → 发布 → 结束
    else:
        return "write_draft"  # 不满意 → 回到起草节点 → 形成循环

这就是一个 "AI 写 → 人审 → 不满意重写 → 再审" 的循环,直到满意为止。

终止条件

返回特殊节点 END,流程结束:

python 复制代码
builder.add_edge("post", END)
builder.add_edge("cancel", END)

把三个机制画出来:

复制代码
       START
         │
         ▼
     [Node A]
         │
         ▼
     [Node B] ────┐
         │        │  条件边
         ▼        ▼
     [Node C]  [Node D]
         │        │
         └───┬────┘
             ▼
            END

四、Human-in-the-Loop:让人类介入的三大机制

普通流程图只能描述"输入 → AI 处理 → 输出"。要让人类在中间插入决策,需要三个机制配合:

复制代码
  ┌──────────────┐    ┌────────────────┐    ┌──────────────────┐
  │  interrupt() │ →  │  Checkpointer  │ →  │  Command(resume) │
  │   暂停执行    │    │   保存现场     │    │    恢复执行       │
  └──────────────┘    └────────────────┘    └──────────────────┘

1. interrupt()------暂停点

python 复制代码
from langgraph.types import interrupt

def confirm(state):
    decision = interrupt({"文案": state["content"]})
    return {"decision": decision}

执行到 interrupt(...) 时:

  • 把括号里的内容"扔出去"给外部调用者(用户看到的审核内容)
  • Graph 暂停,当前函数中止
  • 等待外部传入新数据后从这一行继续

2. Checkpointer------状态持久化

HITL 的基础设施。interrupt() 暂停后,整个 State 必须有地方保存,否则恢复时数据就丢了。

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

graph = builder.compile(checkpointer=MemorySaver())
  • 开发 :MemorySaver(保存在内存)
  • 生产 :PostgresSaver / SqliteSaver,服务器重启也能恢复

3. Command(resume=...)------恢复执行

python 复制代码
from langgraph.types import Command

graph.invoke(
    Command(resume="post"),     # 携带用户的决定
    config=config,              # 必须用同一个 thread_id
)

thread_id 必须前后一致------它是 Checkpointer 找回之前保存 State 的钥匙。


五、关键数据流:Command 是如何影响路由的

这是 HITL 中最容易混淆的地方。当外部调用:

python 复制代码
graph.invoke(
    Command(resume={"action": "post"}),
    config=config,
)

{"action": "post"} 究竟是怎么传递到路由判断里的?追踪完整链路:

复制代码
   外部调用:
   Command(resume={"action": "post"})
                       │
                       │  LangGraph 内部:
                       │  找到上次暂停的位置(confirm 节点)
                       │  把 {"action": "post"} 塞回 interrupt() 的返回值
                       ▼
   ┌───────────────────────────────────────────────┐
   │  def confirm(state):                          │
   │      decision = interrupt(...)                │ ← decision = {"action": "post"}
   │      return {"decision": decision["action"]}  │ ← State["decision"] = "post"
   └───────────────────────────────────────────────┘
                       │
                       │  confirm 返回后,LangGraph 调用条件边路由函数
                       ▼
   ┌───────────────────────────────────────────────┐
   │  def route(state):                            │
   │      return state["decision"]                 │ ← 读 "post",返回 "post"
   └───────────────────────────────────────────────┘
                       │
                       │  LangGraph 拿到字符串 "post"
                       ▼
              跳转到名为 "post" 的节点

三个关键传递点

  1. Command(resume=X) 里的 X ,会成为 interrupt() 的返回值
  2. interrupt() 的返回值,由当前节点写进 State
  3. 路由函数读 State,返回下一节点名字符串,LangGraph 据此跳转

State 是中间人:节点写进去,路由函数读出来。没有任何隐藏机制。

一个理解模型

interrupt() 像一个快递柜:

复制代码
AI 把内容"放进快递柜"   →   Graph 暂停
                              │
                              │  外部看到内容
                              │  把决定放回快递柜
                              ▼
节点"开柜取件",拿到 {"action": "post"}

六、完整示例:朋友圈文案生成器

需求:用户描述想发什么,AI 生成文案,用户确认后发布,否则取消。

流程:

复制代码
   START
     │
     ▼
  [write]       AI 写文案
     │
     ▼
  [confirm]     ⏸ 暂停,等用户确认
     │
     ├── "post"   →  [post]   → END
     └── "cancel" →  [cancel] → END

完整代码:

python 复制代码
from typing import TypedDict, Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import MemorySaver


# ① State:流程共享数据
class PostState(TypedDict):
    idea:     str   # 用户的想法
    content:  str   # AI 生成的文案
    decision: str   # 用户的决定


# ② LLM
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.9)


# ③ 节点
def write(state: PostState) -> dict:
    """AI 根据想法写文案"""
    response = llm.invoke([
        SystemMessage(content="写朋友圈文案,带 emoji 和 hashtag,50 字以内。"),
        HumanMessage(content=state["idea"]),
    ])
    return {"content": response.content}


def confirm(state: PostState) -> dict:
    """⏸ 暂停,等用户决定"""
    decision = interrupt({"文案": state["content"]})
    return {"decision": decision["action"]}


def post(state: PostState) -> dict:
    print(f"🚀 已发布:{state['content']}")
    return {}


def cancel(state: PostState) -> dict:
    print("🗑️ 已取消")
    return {}


# ④ 条件边路由
def route(state: PostState) -> Literal["post", "cancel"]:
    return "post" if state["decision"] == "post" else "cancel"


# ⑤ 构建 Graph
builder = StateGraph(PostState)
builder.add_node("write",   write)
builder.add_node("confirm", confirm)
builder.add_node("post",    post)
builder.add_node("cancel",  cancel)

builder.add_edge(START,    "write")
builder.add_edge("write",  "confirm")
builder.add_edge("post",   END)
builder.add_edge("cancel", END)
builder.add_conditional_edges("confirm", route)

graph = builder.compile(checkpointer=MemorySaver())


# ⑥ 运行
if __name__ == "__main__":
    config = {"configurable": {"thread_id": "post-001"}}

    # 第一次调用:执行到 interrupt() 自动暂停
    graph.invoke(
        {"idea": "今天爬山了,累但很开心", "content": "", "decision": ""},
        config=config,
    )

    # 用户决定
    choice = input("发布吗?(post / cancel):").strip()

    # 第二次调用:恢复执行
    graph.invoke(
        Command(resume={"action": choice}),
        config=config,
    )

知识点与代码的对应

代码 知识点
class PostState(TypedDict) State
def write / confirm / post / cancel Node
add_edge(START, "write") 普通边
add_conditional_edges("confirm", route) 条件边
interrupt(...) HITL 暂停点
MemorySaver() Checkpointer
Command(resume=...) HITL 恢复
add_edge("post", END) 终止条件

七、常见误区

误区 1:两次 invoke() 是两个独立的程序

HITL 流程通常需要两次 graph.invoke(),中间间隔一段时间(等待人类输入)。它们是同一个流程的两个阶段 ,靠 thread_id + Checkpointer 串联。可以用 graph.stream() + while 循环让代码更连贯,本质上等价。

误区 2:Command(resume=X) 里的 X 有固定格式

X 可以是字符串、字典、列表,完全自由。传什么进去,interrupt() 就返回什么。

误区 3:节点 return 字典会覆盖整个 State

不会。LangGraph 会自动合并------只更新返回的字段,其他字段保持不变。

误区 4:路由函数可以返回任意内容

必须返回字符串 ,且必须是已注册的节点名或 END。返回错了会报错。

误区 5:thread_id 可以随便改

thread_id 是 Checkpointer 找回 State 的钥匙。暂停时用的 thread_id,恢复时必须完全一致,否则找不到之前的状态。


核心五要素------State / Node / Edge / 条件边 / interrupt------已经能覆盖 80% 的实际场景。


总结

LangGraph 的设计哲学是把 AI 工作流抽象成图结构:

  • 节点是步骤
  • 是顺序
  • State 是数据
  • 条件边是分支
  • interrupt 是人类介入点

理解了这五个抽象,遇到任何复杂业务需求,第一步不再是写 prompt,而是先画图:

这个流程有几个节点?哪里需要分支?哪里需要人类介入?

图画清楚了,代码只是把图翻译出来。

相关推荐
岳小哥AI2 小时前
一文读懂AI应用技术:自然语言处理、语音识别/合成、可解释AI
ai·ai基础
DS随心转插件2 小时前
Kimi 转 pdf 怎么压缩但清晰?AI 导出鸭一站式优化,压缩文件同时留存原版高清内容
人工智能·ai·pdf·豆包·deepseek·ai导出鸭
钱多多_qdd2 小时前
claude code(十):【企业级应用实战1】:章节介绍与前言
ai·claude
xiezhr2 小时前
Hermes官方桌面版发布了
人工智能·ai·agent·codex·hermes
CoderJia程序员甲2 小时前
GitHub 热榜项目 - 周榜(2026-06-14)
ai·大模型·llm·github
毒爪的小新10 小时前
Linux 环境极速部署 vLLM:从零搭建生产级大模型推理服务
linux·人工智能·ai·语言模型·vllm
BLSxiaopanlaile11 小时前
《智慧的疆界》读书笔记
ai
JaydenAI12 小时前
[对比学习LangChain和MAF-07]如何引入人机交互的审批流程
python·ai·langchain·c#·agent·hitl·maf
尘埃落定wf12 小时前
Claude Code 深度拆解:执行机制 + 实际工作流融合指南
ai·cladue