实战20 分钟
Human-in-the-loop 与检查点
中断 / 恢复 / 时间旅行——关键节点交还人类决策。
模型有了「手」之后,你要回答一个问题:什么时候让它自己干,什么时候拦下来问人?
为什么需要人工审批
模型的工具调用是有副作用的——读文件无害,但 rm -rf / 呢?
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 解决的是「整个任务先规划再动手」。
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 做关键节点的人工审批(复杂任务的保险机制)。
本章要点
- 权限三级:只读 → 自动放行(绿)、写操作 → 需确认(黄)、危险 → 直接拒绝(红)
- Bash 需要特殊处理——同一个工具既能
ls也能rm,看命令前缀 isReadOnly()不仅是元信息,是权限系统的核心依据- Plan Mode = 锁住写能力、保留读能力,让模型先探索再规划
- LangGraph checkpoint 是框架级 HITL,支持中断恢复和时间旅行
→ 下一章:多 Agent 协作