← 博客书房

Agent 实战:从 Dify 到 LangGraph·Human-in-the-loop 与检查点

实战20 分钟

Human-in-the-loop 与检查点

中断 / 恢复 / 时间旅行——关键节点交还人类决策。

模型有了「手」之后,你要回答一个问题:什么时候让它自己干,什么时候拦下来问人?

为什么需要人工审批

模型的工具调用是有副作用的——读文件无害,但 rm -rf / 呢?

权限三级模型 ✓ 自动放行 Read · Grep · Glob pwd · ls · git status 只读 · 无副作用 · 不问 ? 需要确认 Write · Edit · Bash npm install · git commit 有副作用 · 拦下来问用户 ✗ 直接拒绝 rm · sudo · git push shutdown · reset --hard 危险 · 不问 · 直接 block easy-agent Step 7 的实现:isReadOnly() → allow · 非只读 → ask · 危险前缀 → deny

easy-agent 的权限模型

easy-agent Step 7 实现了一个三级权限模型,核心函数只需要一个 checkPermission:

async function checkPermission({ tool, input, mode }) {
  // Plan Mode 下只放行只读工具
  if (mode === "plan" && !tool.isReadOnly()) {
    return { behavior: "deny", reason: "plan mode blocks write actions" };
  }

  // Bash 命令需要额外检查
  if (tool.name === "Bash") {
    if (isDangerousCommand(input.command)) {
      return { behavior: "deny", reason: "dangerous shell command" };
    }
    if (isReadOnlyCommand(input.command)) {
      return { behavior: "allow", reason: "read-only shell command" };
    }
    return { behavior: "ask", reason: "shell command may change local state" };
  }

  // 其他工具按 isReadOnly 分级
  if (tool.isReadOnly()) {
    return { behavior: "allow" };
  }
  return { behavior: "ask", reason: "tool writes local state" };
}

碎开来说这个函数的三层判断:

第一层:模式检查。 Plan Mode(后面讲)下,非只读工具直接拒绝。这是「先看后做」的硬约束。

第二层:Bash 特殊处理。 因为 Bash 是万能工具——既能 ls 也能 rm -rf,所以要看具体命令:

const READ_ONLY_SHELL_PREFIXES = [
  "pwd", "ls", "cat", "find", "rg", "grep",
  "git status", "git diff", "git log",
];

const DANGEROUS_BASH_PREFIXES = [
  "rm ", "sudo ", "git push",
  "git reset --hard", "shutdown", "reboot",
];

两个白名单/黑名单决定了命令的走向。

第三层:工具自身的 isReadOnly()。 这就是 lc-tools 章讲的 Tool 契约里的那个字段——现在你知道它为什么存在了。

Plan Mode:先看后做

权限系统解决的是「单次操作要不要拦」,Plan Mode 解决的是「整个任务先规划再动手」。

+ + Plan Mode:先看后做 ① 进入规划 EnterPlanMode 只允许 Read/Grep/Glob ② 探索 + 写计划 读代码 · 分析结构 计划写入 plan.md ③ 用户审批 人类看计划 ✓ 批准 或 ✗ 修改 ④ 执行计划 恢复完整工具 ExitPlanMode → 动手 规划阶段不能写源代码、不能跑危险命令——只能读和写 plan 文件 这就是「先看后做」:模型的探索能力不受限,但动手能力被锁住

easy-agent Step 13 的 Plan Mode 实现,核心只是两个工具:

  • EnterPlanMode:切换到规划模式,只读工具可用
  • ExitPlanMode:提交计划给用户审批,批准后恢复完整工具
// Plan Mode 下的权限检查
function checkPermissionInPlanMode({ toolName, input }) {
  if (PLAN_ALLOWED_TOOLS.has(toolName)) {
    return { behavior: "allow" };  // Read/Grep/Glob 放行
  }
  if (toolName === "Bash" && isReadOnlyCommand(input.command)) {
    return { behavior: "allow" };  // 只读 Bash 放行
  }
  if (toolName === "Write" && isPlanFile(input.file_path)) {
    return { behavior: "allow" };  // 只能写计划文件
  }
  return { behavior: "deny" };  // 其他一律拒绝
}

LangGraph 的检查点

Plan Mode 是产品层面的 HITL。LangGraph 在框架层面提供了更原语的能力:检查点(checkpoint)。

import { StateGraph, MemorySaver } from "@langchain/langgraph";

const checkpointSaver = new MemorySaver();

const graph = new StateGraph(AgentState)
  .addNode("agent", callModel)
  .addNode("tools", executeTools)
  .addNode("humanReview", askHuman)  // ← 新增审批节点
  .addConditionalEdges("agent", shouldContinue)
  .compile({ checkpointer: checkpointSaver });
产品层(Plan Mode)框架层(LangGraph checkpoint)
工具粒度:每个工具调用前检查图粒度:在某个节点暂停
规则驱动(isReadOnly + 前缀匹配)状态驱动(checkpoint 持久化)
没有时间旅行可以回到之前任一检查点
不支持恢复中断后可从检查点恢复执行

实际产品两者结合用:权限系统做日常防线(成本低、延迟小),checkpoint 做关键节点的人工审批(复杂任务的保险机制)。

本章要点

  1. 权限三级:只读 → 自动放行(绿)、写操作 → 需确认(黄)、危险 → 直接拒绝(红)
  2. Bash 需要特殊处理——同一个工具既能 ls 也能 rm,看命令前缀
  3. isReadOnly() 不仅是元信息,是权限系统的核心依据
  4. Plan Mode = 锁住写能力、保留读能力,让模型先探索再规划
  5. LangGraph checkpoint 是框架级 HITL,支持中断恢复和时间旅行

→ 下一章:多 Agent 协作