Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Add human oversight to LangChain agents using HITL middleware - includes interrupts, approval workflows, edit/reject decisions, and checkpoints
.claude/skills/majiayu000-langchain-human-in-the-loop/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 7% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 23% | 0% |
| case-03 | ✗→✓ | ▲ Improved | -14% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 2% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 32% | 0% |
人工在环(Human-in-the-Loop,HITL)让您可以向代理工具调用添加人工监督。当代理提出敏感操作(如数据库写入或发送电子邮件)时,执行会暂停以供人工批准、编辑或拒绝。
核心概念:
| 场景 | 使用 HITL? | 原因 | |----------|-----------|-----| | 数据库写入 | ✅ 是 | 防止数据损坏 | | 发送电子邮件/消息 | ✅ 是 | 发送前审查 | | 金融交易 | ✅ 是 | 执行前确认 | | 删除数据 | ✅ 是 | 防止意外丢失 | | 只读操作 | ❌ 否 | 低风险 | | 内部计算 | ❌ 否 | 无外部影响 |
| 决策 | 效果 | 何时使用 | |----------|--------|-------------| | approve | 按原样执行工具 | 工具调用看起来正确 | | edit | 修改参数然后执行 | 需要更改参数 | | reject | 不执行,提供反馈 | 工具调用错误 |
typescriptimport { createAgent, humanInTheLoopMiddleware } from "langchain"; import { MemorySaver } from "@langchain/langgraph"; import { tool } from "langchain"; import { z } from "zod"; const sendEmail = tool( async ({ to, subject, body }) => { // 发送电子邮件逻辑 return `已发送电子邮件到 ${to}`; }, { name: "send_email", description: "发送电子邮件", schema: z.object({ to: z.string().email(), subject: z.string(), body: z.string(), }), } ); const agent = createAgent({ model: "gpt-4.1", tools: [sendEmail], checkpointer: new MemorySaver(), // HITL 所必需 middleware: [ humanInTheLoopMiddleware({ interruptOn: { send_email: { allowedDecisions: ["approve", "edit", "reject"], }, }, }), ], });
typescriptimport { Command } from "@langchain/langgraph"; const config = { configurable: { thread_id: "session-1" } }; // 步骤 1:代理运行直到需要调用工具 const result1 = await agent.invoke({ messages: [{ role: "user", content: "发送电子邮件到 john@example.com 说你好" }] }, config); // 检查中断 if ("__interrupt__" in result1) { const interrupt = result1.__interrupt__[0]; console.log("等待批准:", interrupt.value); // 中断包含:{toolCall: {...}, allowedDecisions: [...]} } // 步骤 2:人工批准 const result2 = await agent.invoke( new Command({ resume: { decisions: [{ type: "approve" }], }, }), config ); // 工具现在执行,代理完成 console.log(result2.messages[result2.messages.length - 1].content);
typescriptconst config = { configurable: { thread_id: "session-2" } }; // 代理想要发送电子邮件 const result1 = await agent.invoke({ messages: [{ role: "user", content: "给 Alice 发送关于会议的电子邮件" }] }, config); // 人工编辑参数 const result2 = await agent.invoke( new Command({ resume: { decisions: [{ type: "edit", args: { to: "alice@company.com", // 修正的电子邮件 subject: "项目会议 - 已更新", // 更好的主题 body: "...", // 编辑的正文 }, }], }, }), config );
typescriptconst config = { configurable: { thread_id: "session-3" } }; // 代理想要删除记录 const result1 = await agent.invoke({ messages: [{ role: "user", content: "删除旧的客户数据" }] }, config); // 人工拒绝 const result2 = await agent.invoke( new Command({ resume: { decisions: [{ type: "reject", feedback: "未经经理批准无法删除客户数据", }], }, }), config ); // 代理接收反馈并可以尝试替代方法
typescriptimport { humanInTheLoopMiddleware } from "langchain"; const agent = createAgent({ model: "gpt-4.1", tools: [sendEmail, readEmail, deleteEmail], checkpointer: new MemorySaver(), middleware: [ humanInTheLoopMiddleware({ interruptOn: { send_email: { allowedDecisions: ["approve", "edit", "reject"], }, delete_email: { allowedDecisions: ["approve", "reject"], // 无编辑 }, read_email: false, // 读取无 HITL }, }), ], });
typescriptconst config = { configurable: { thread_id: "session-4" } }; // 流式传输直到中断 for await (const [mode, chunk] of await agent.stream( { messages: [{ role: "user", content: "发送报告给团队" }] }, { ...config, streamMode: ["updates", "messages"] } )) { if (mode === "messages") { const [token, metadata] = chunk; if (token.content) { process.stdout.write(token.content); } } else if (mode === "updates") { if ("__interrupt__" in chunk) { console.log("\n等待批准..."); break; // 处理中断 } } } // 批准后继续流式传输 for await (const [mode, chunk] of await agent.stream( new Command({ resume: { decisions: [{ type: "approve" }] } }), { ...config, streamMode: ["messages"] } )) { // 继续流式传输 }
typescriptimport { createMiddleware } from "langchain"; const customHITL = createMiddleware({ name: "CustomHITL", wrapToolCall: async (toolCall, handler, runtime) => { // 自定义逻辑决定是否需要中断 if (toolCall.name === "database_write") { const value = toolCall.args.value; if (value > 1000) { // 为大值中断 const decision = await runtime.interrupt({ toolCall, reason: "大型数据库写入需要批准", }); if (decision.type === "approve") { return await handler(toolCall); } else if (decision.type === "edit") { return await handler({ ...toolCall, args: decision.args }); } else { throw new Error(decision.feedback || "已拒绝"); } } } // 不需要中断 return await handler(toolCall); }, });
✅ 哪些工具需要批准:每个工具的策略 ✅ 允许的决策类型:批准、编辑、拒绝 ✅ 自定义中断逻辑:条件中断 ✅ 反馈消息:解释拒绝原因 ✅ 修改的参数:编辑工具参数
❌ 跳过检查点:HITL 需要持久化 ❌ 执行后中断:必须在中断前 ❌ 强制模型不调用工具:HITL 在模型决定后响应 ❌ 修改模型的决策:仅工具执行
typescript// ❌ 问题:没有检查点 const agent = createAgent({ model: "gpt-4.1", tools: [sendEmail], middleware: [humanInTheLoopMiddleware({...})], // 错误! }); // ✅ 解决方案:始终添加检查点 import { MemorySaver } from "@langchain/langgraph"; const agent = createAgent({ model: "gpt-4.1", tools: [sendEmail], checkpointer: new MemorySaver(), // 必需 middleware: [humanInTheLoopMiddleware({...})], });
typescript// ❌ 问题:缺少 thread_id await agent.invoke(input); // 没有配置! // ✅ 解决方案:始终提供 thread_id await agent.invoke(input, { configurable: { thread_id: "user-123" } });
typescript// ❌ 问题:错误的恢复格式 await agent.invoke({ resume: { decisions: [...] } // 错误! }); // ✅ 解决方案:使用 Command import { Command } from "@langchain/langgraph"; await agent.invoke( new Command({ resume: { decisions: [{ type: "approve" }] } }), config );
typescript// ❌ 问题:未检测到中断 const result = await agent.invoke(input, config); console.log(result.messages); // 可能未完成! // ✅ 解决方案:检查 __interrupt__ if ("__interrupt__" in result) { // 处理人工决策 } else { // 代理完成 }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 24,712 | 15,871 | -36% | 1 | 1 | 0% | 3,912 | 4,186 | +7% | 0 | 0 | — |
case-02 | fail→pass | 23,320 | 19,967 | -14% | 1 | 1 | 0% | 4,636 | 5,697 | +23% | 0 | 0 | — |
case-03 | fail→pass | 35,351 | 19,616 | -45% | 1 | 1 | 0% | 6,042 | 5,206 | -14% | 0 | 0 | — |
case-04 | pass→pass | 14,337 | 10,808 | -25% | 1 | 1 | 0% | 2,628 | 4,120 | +57% | 0 | 0 | — |
case-05 | fail→pass | 25,540 | 19,906 | -22% | 1 | 1 | 0% | 4,651 | 4,748 | +2% | 0 | 0 | — |
case-06 | fail→pass | 15,728 | 10,385 | -34% | 1 | 1 | 0% | 3,276 | 4,326 | +32% | 0 | 0 | — |
case-12 | fail→pass | 13,052 | 5,132 | -61% | 1 | 1 | 0% | 2,432 | 3,483 | +43% | 0 | 0 | — |
case-07 | pass→pass | 10,158 | 6,887 | -32% | 1 | 1 | 0% | 1,878 | 3,597 | +92% | 0 | 0 | — |
case-08 | pass→pass | 14,504 | 7,779 | -46% | 1 | 1 | 0% | 2,117 | 4,091 | +93% | 0 | 0 | — |
case-09 | pass→pass | 16,132 | 10,836 | -33% | 1 | 1 | 0% | 2,304 | 4,053 | +76% | 0 | 0 | — |
case-10 | pass→pass | 13,528 | 5,429 | -60% | 1 | 1 | 0% | 2,010 | 3,591 | +79% | 0 | 0 | — |
case-11 | fail→pass | 16,299 | 7,475 | -54% | 1 | 1 | 0% | 3,027 | 3,943 | +30% | 0 | 0 | — |
case-13 | fail→pass | 12,136 | 4,583 | -62% | 1 | 1 | 0% | 2,238 | 3,287 | +47% | 0 | 0 | — |
case-14 | fail→pass | 19,453 | 6,381 | -67% | 1 | 1 | 0% | 4,038 | 3,781 | -6% | 0 | 0 | — |
case-15 | pass→pass | 16,016 | 8,717 | -46% | 1 | 1 | 0% | 2,878 | 4,171 | +45% | 0 | 0 | — |
case-16 | fail→pass | 14,808 | 8,258 | -44% | 1 | 1 | 0% | 2,604 | 4,053 | +56% | 0 | 0 | — |
case-22 | pass→pass | 5,039 | 2,071 | -59% | 1 | 1 | 0% | 698 | 2,810 | +303% | 0 | 0 | — |
case-17 | fail→pass | 22,280 | 5,143 | -77% | 1 | 1 | 0% | 3,865 | 3,434 | -11% | 0 | 0 | — |
case-18 | pass→pass | 12,193 | 10,889 | -11% | 1 | 1 | 0% | 2,415 | 4,701 | +95% | 0 | 0 | — |
case-19 | pass→pass | 17,309 | 10,014 | -42% | 1 | 1 | 0% | 3,073 | 4,520 | +47% | 0 | 0 | — |
case-20 | fail→pass | 21,693 | 3,261 | -85% | 1 | 1 | 0% | 3,576 | 3,074 | -14% | 0 | 0 | — |
case-21 | fail→pass | 13,604 | 9,608 | -29% | 1 | 1 | 0% | 2,412 | 4,255 | +76% | 0 | 0 | — |
case-23 | fail→pass | 22,563 | 2,311 | -90% | 1 | 1 | 0% | 4,508 | 2,804 | -38% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +61 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.