实战:用 Interrupt 暂停 Agent 轮次并在恢复点继续执行)
Genkit JS 智能体人机协同Human-in-the-Loop实战用 Interrupt 暂停 Agent 轮次并在恢复点继续执行【免费下载链接】skillsAgent Skills for Google products and technologies项目地址: https://gitcode.com/GitHub_Trending/skills29/skills导读本文聚焦 Genkit JSTypeScript/Node.jsAgent API 中的人机协同能力——Interrupt中断。它允许 Agent 在敏感操作前如转账审批、方案确认、缺失信息收集暂停当前轮次把控制权交回你的代码或真实用户随后从精确的暂停点恢复执行。读完本文你将掌握ai.defineInterrupt的定义方式、服务端chat.resume/resumeStream的检测与恢复流程、浏览器端remoteAgent的审批弹窗实现以及 Interrupt 与 Session Store、Client-Managed State、toolApproval中间件之间的协作边界。Interrupt 的本质把工具调用当作控制流在 Genkit JS 的 Agent 参考文档 中interrupt 被明确定义为一种tool call used as control flowinterrupt 工具永远不会在服务器端真正执行它存在的唯一目的就是暂停当前轮次turn。当模型认为需要人工介入时它会调用这个 interrupt 工具而运行时拦截这次调用、返回res.interrupts列表把控制权交还给调用方你的代码或人类之后你再通过恢复resume机制从暂停点继续。一个完整的人机协同轮次遵循固定的三步流程chat.send(...) → 响应中包含 res.interrupts → 收集人类输入 → chat.resume({ respond: [...] })其中每个res.interrupts条目都对应一次被暂停的 turn且每次恢复必须使用同一个chat实例或其携带的快照/状态否则无法定位到正确的暂停点。Interrupt 与持久化正交Interrupt 与 Agent 的持久化机制完全正交orthogonal——无论 Agent 使用 Session Store服务端持有会话历史每轮产生不可变快照还是 Client-Managed State服务端无状态客户端持有状态 blobinterrupt 的工作方式完全一致。区别仅在于暂停的 turn 如何被带回恢复调用——有 store靠快照snapshot自动定位无 store客户端需要自行往返携带状态 blob而remoteAgent客户端会自动帮你完成这件事。这一设计意味着你可以在不引入任何服务端存储的前提下实现审批流程也可以在服务端持久化会话的同时使用 interrupt。定义第一个 Interrupt银行转账审批示例Interrupt 的 API 形态与工具tool极其相似——它同样拥有name、description、inputSchema和outputSchema并且需要像工具一样添加到 Agent 的tools数组中。区别在于语义inputSchema描述的是模型暂停时传入的数据这些数据会展示给人类看而outputSchema描述的是人类/你的代码恢复时返回的数据。以下示例来自 agents-human-in-the-loop.md实现了一个银行助手系统提示词要求模型在转账前必须调用userApprovalinterrupt 征得用户同意import { z } from genkit; import { InMemorySessionStore } from genkit/beta; import { ai } from ./genkit.js; export const userApproval ai.defineInterrupt({ name: userApproval, description: Ask the user for approval before a sensitive action., // What the model passes in when it pauses (shown to the human): inputSchema: z.object({ action: z.string(), details: z.string() }), // What the human/your code returns to resume: outputSchema: z.object({ approved: z.boolean(), feedback: z.string().optional(), }), }); export const transferMoney ai.defineTool( { name: transferMoney, description: Transfer money to a specified account., inputSchema: z.object({ amount: z.number(), toAccount: z.string() }), outputSchema: z.object({ success: z.boolean(), transactionId: z.string() }), }, async ({ amount, toAccount }) ({ success: true, transactionId: txn-${Date.now()}, }) ); export const bankingAgent ai.defineAgent({ name: bankingAgent, system: You are a banking assistant. ALWAYS use the userApproval interrupt to confirm before executing transferMoney., tools: [userApproval, transferMoney], store: new InMemorySessionStore(), });代码中的注释点出了关键语义模型调用userApproval时传入的是{ action, details }例如{ action: transferMoney, details: Transfer $500 to savings }这段数据会被原样展示给人类人类做出决定后代码以{ approved, feedback }形式把结果喂回给模型模型据此继续。关于 import 路径的版本前提整个 Agent API含 interrupt都是Beta / preview API。服务端 API 必须从genkit/beta导入而不是稳定的genkit入口浏览器客户端从genkit/beta/client导入且要求genkit 1.39.0参见 SKILL.md 与 agents.md。本文示例中的InMemorySessionStore同样来自genkit/beta。服务端检测与恢复res.interrupts与chat.resume当 Agent 暂停时send返回的响应中res.interrupts为非空数组。每个中断条目AgentInterrupt暴露以下能力成员类型说明.namestring中断的名称即defineInterrupt中定义的name。.input由inputSchema推导模型暂停时传入的数据展示给人类使用。.respond(output)builder返回一个toolResponsepart用于携带工具的输出恢复执行不会真正执行该工具。此方法不会发送需要后续调用chat.resume(...)。.restart()builder重新发起原始工具请求用于重试 / 让工具真正执行一次。同样不会发送。恢复动作必须通过同一个chat调用chat.resume(...)或chat.resumeStream(...)它是send({ resume })的语法糖const chat bankingAgent.chat(); let res await chat.send(Transfer $500 to my savings account.); const approval res.interrupts.find((i) i.name userApproval); if (approval) { console.log(approval.input); // { action, details } — show this to the human // Collect the human decision, then resume with the interrupts output: res await chat.resume({ respond: [approval.respond({ approved: true, feedback: Looks good })], }); } console.log(res.text); // final confirmation流式恢复变体如果希望以流式方式恢复例如在 CLI 或 Web UI 中边生成边渲染使用chat.resumeStreamconst turn chat.resumeStream({ respond: [approval.respond({ approved: true })], }); for await (const chunk of turn.stream) process.stdout.write(chunk.text ?? ); const res await turn.response;一次恢复多个中断respond与restart混用在一个轮次中可能出现多个待处理的中断例如 Agent 同时请求批准两项操作。chat.resume接受一个由多个 builder 组成的数组并且可以把提供输出的respond与重跑工具的restart混在一起await chat.resume({ respond: [a.respond({ approved: true })], restart: [b.restart()], });注意区分二者的业务含义a.respond(...)表示人类批准了 a直接把它当作已执行并携带结果继续b.restart()表示人类要求重试 b让 b 这个工具真正执行一次。浏览器端客户端Human-in-the-LoopremoteAgent审批弹窗同一套模式可以通过 HTTP 在浏览器中落地。客户端使用genkit/beta/client导出的remoteAgent与类型AgentChat、AgentInterrupt、AgentResponse。remoteAgent客户端会自动跟踪状态快照因此恢复同一个chat就能精确地从暂停点继续无需你手动管理 snapshotIdimport { remoteAgent, type AgentChat, type AgentInterrupt, type AgentResponse, } from genkit/beta/client; const agent remoteAgent({ url: /api/bankingAgent }); const chat: AgentChat agent.chat(); // 1. Send and detect the pause. const res: AgentResponse await chat.send(Transfer $500 to savings.); const pending: AgentInterrupt | undefined res.interrupts.find( (i) i.name userApproval ); if (pending) { // pending.input → { action, details }; render an approval dialog. // 2. After the human approves/denies, resume the SAME chat. const respond pending.respond({ approved: true, feedback: ok }); const turn chat.resumeStream({ respond: [respond] }); for await (const chunk of turn.stream) { /* render chunk.text */ } const final await turn.response; // If final.interrupts is non-empty, the agent paused again — repeat. }UX 提示来自原文档对于被中断的轮次res.interrupts.length 0不要渲染一条模型消息气泡——此时模型并没有产出真正的回复。正确做法是根据interrupt.input渲染审批 UI如确认转账 $500 到储蓄账户[批准/拒绝]待恢复并拿到最终回复后再渲染模型消息。在浏览器场景下服务端需要正确配置 CORS。特别地流式传输要求请求与响应暴露X-Genkit-Stream-Id头否则浏览器端的remoteAgent流式调用会被 CORS 拦截详见 agents-deployment.mdapp.use((req, res, next) { res.header(Access-Control-Allow-Origin, *); res.header( Access-Control-Allow-Headers, Content-Type, Accept, X-Genkit-Stream-Id ); res.header(Access-Control-Expose-Headers, X-Genkit-Stream-Id); res.header(Access-Control-Allow-Methods, GET, POST, OPTIONS); if (req.method OPTIONS) { res.sendStatus(204); return; } next(); });持久化正交性深入Session Store 与 Client-Managed State 两种形态原文档反复强调 interrupt 与持久化无关下面结合仓库中的相关参考文档展开这两种形态的差异二者均可与 interrupt 搭配。形态一Session Store服务端持久化给 Agent 挂上store后服务端拥有会话历史每轮产生一个不可变快照snapshot快照链承载对话状态。InMemorySessionStore适合测试/开发重启即丢失FileSessionStore把快照持久化到dir/global/snapshotId.json并支持maxPersistedChainLength裁剪链长度生产环境可选用genkit-ai/google-cloud/beta的FirestoreSessionStore基于 JSON Patch 增量 分片检查点规避单文档 1 MiB 限制。详见 agents-sessions.md。import { InMemorySessionStore, FileSessionStore } from genkit/beta; const memStore new InMemorySessionStore(); // tests/dev const fileStore new FileSessionStore(./.snapshots); // file-backed const pruning new FileSessionStore(./.snapshots, { maxPersistedChainLength: 3, // keep last 3 });形态二Client-Managed State无 store无状态服务端如果 Agent没有store服务端完全无状态会话状态 blob消息 自定义状态 制品归调用方所有。remoteAgent客户端自动跟踪并随每次 turn 往返携带状态。直接调用例如在 flow 内调用无状态 Agent时你必须手动往返状态async function turn(state: unknown | undefined, text: string) { const chat weatherAgentStateless.chat(state ? { state } : undefined); const res await chat.send(text); return { state: res.raw.state, text: res.text }; // 把新状态带回下一次调用 }完整示例见 agents.md。选择依据需要服务端拥有历史、或需要分支/后台执行时用 Session Store希望客户端自行持久化历史、不想运行服务端存储时用 Client-Managed State——interrupt 在两种形态下都可用。另外若你在defineAgent上声明了stateSchemaZod还可以为会话附加类型化的自定义状态任务列表、审批记录等工具内通过ai.currentSessionS()与session.updateCustom(...)读写remoteAgent客户端会自动同步——详见 agents-state.md。Interrupt 在仓库中的联动能力与边界围绕 interrupt 这一主题仓库内的相关参考文档还揭示了它与周边机制的配合与限制与toolApproval中间件的配合中间件文档 中genkit-ai/middleware包的toolApproval({ approved: [...] })会把工具执行限制在白名单内对名单外的工具调用抛出ToolInterruptError——该错误可通过 interrupt 机制恢复。这是 interrupt 的典型生产用法默认拦截所有写操作/危险工具让模型通过 interrupt 请求人工批准后再放行。配合 agents.md 中的 codingAgent 示例一个具备文件系统访问、技能加载、写入审批的编码助手几乎全是use: [...]配置import { filesystem, retry, skills, toolApproval } from genkit-ai/middleware; import { FileSessionStore } from genkit/beta; export const codingAgent ai.defineAgent({ name: codingAgent, system: You are an expert AI coding assistant working in a sandboxed workspace., tools: [runShell, askUser], // 你自己的工具/interrupt use: [ toolApproval({ approved: [list_files, read_file, use_skill, run_shell, ask_user], }), filesystem({ rootDirectory: WORKSPACE_DIR, allowWriteAccess: true }), skills({ skillPaths: [SKILLS_DIR] }), retry(), ], store: new FileSessionStore(./.snapshots-coding), // tool approval 需要 store maxTurns: 30, });子 Agent 的中断边界多 Agent 编排中如果子 Agent 触发了 interrupt它会被当作普通的工具响应回传给编排者而不会作为可恢复的中断向上传播——交互式、有状态子 Agent 的中断属于未来特性。因此实际项目中应把自包含的任务委托给子 Agent详见 agents-multi-agent.md 末尾的 Note。与快照状态机的关系interrupt 的只恢复completed快照约束与 后台 Agent 的状态机pending/completed/failed/aborted/expired相互印证只有处于completed终态的快照可被恢复failed/aborted/pending快照仅保留用于检查不可恢复。分支branching机制同样基于不可变快照从同一snapshotId可 fork 出多条独立时间线——详见 agents-branching.md。注意事项与踩坑清单Notes Gotchas最后汇总原文档列出的五条关键注意事项它们也是接入 interrupt 时最常见的坑不需要 store。interrupt 兼容 Session Store 与 Client-Managed State持久化与其正交。只需在同一个chat上恢复对于底层原始调用则需要把返回的状态/快照带回恢复调用。respond/restart只是 builder。它们返回用于resume载荷的 part本身不会发送。你仍然需要显式调用chat.resume(...)。恢复校验Resume validation。服务端会把每一条respond/restart条目与对话历史逐项校验name/ref 必须匹配restart的 input 必须保持不变不匹配会被拒绝。因此务必从响应中的 interrupt 对象构造条目approval.respond(...)而不要手工拼装 part。只有completed快照可恢复。failed/aborted/pending快照仅保留用于检查不能恢复。可能再次暂停。恢复后产生的新响应可能再次触发 interrupt例如 Agent 连续需要多次审批。正确写法是循环处理直到res.interrupts为空。小结Interrupt 是 Genkit JS Agent 体系中实现人机协同、可控执行的核心原语它以工具调用为控制流天然复用工具 Schema 体系inputSchema/outputSchema暂停与恢复完全对称且与持久化正交、与toolApproval等中间件深度配合。无论是服务端 CLI 审批、浏览器审批弹窗还是多轮审批循环chat.send → res.interrupts → chat.resume({ respond })这一最小闭环足以覆盖绝大多数敏感操作确认场景。相关完整 API 细节可继续参阅仓库内的 agents.md、agents-sessions.md、agents-state.md 与 middleware.md。【免费下载链接】skillsAgent Skills for Google products and technologies项目地址: https://gitcode.com/GitHub_Trending/skills29/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考