> Discover all available pages from the documentation index: https://mastra.zisheng.pro/zh-HK/llms.txt # Agent 審批 Agent 呼叫處理敏感操作(例如刪除資源或執行耗時程序)的 Tool 時,有時需要與 Workflow 相同的[人工介入](https://mastra.zisheng.pro/zh-HK/docs/workflows/human-in-the-loop)監督。透過 Agent 審批,你可以在 Tool call 執行前將其暫停,讓人員批准或拒絕,也可以讓 Tool 自行暫停,以向用戶索取額外背景資料。 ## 何時使用 Agent 審批 - **破壞性或不可逆轉的操作**,例如刪除記錄、傳送電郵或處理付款。 - **成本高昂的操作**,例如呼叫昂貴的第三方 API,而你希望先核實參數。 - **條件式確認**,即 Tool 開始執行後,發現需要用戶確認或提供額外資料才能完成。 ## 快速開始 將 Tool 標記為 `requireApproval: true`,然後檢查串流中的 `tool-call-approval` chunk,以批准或拒絕: ```typescript import { Agent } from '@mastra/core/agent' import { createTool } from '@mastra/core/tools' import { z } from 'zod' const deleteTool = createTool({ id: 'delete-record', description: 'Delete a record by ID', inputSchema: z.object({ id: z.string() }), outputSchema: z.object({ deleted: z.boolean() }), requireApproval: true, execute: async ({ id }) => { await db.delete(id) return { deleted: true } }, }) const agent = new Agent({ id: 'my-agent', name: 'My Agent', model: 'openai/gpt-5-mini', tools: { deleteTool }, }) const stream = await agent.stream('Delete record abc-123') for await (const chunk of stream.fullStream) { if (chunk.type === 'tool-call-approval') { const approved = await agent.approveToolCall({ runId: stream.runId }) for await (const c of approved.textStream) process.stdout.write(c) } } ``` > **備註:** Agent 審批使用快照擷取請求狀態。請在 Mastra 實例上設定[儲存 Provider](https://mastra.zisheng.pro/zh-HK/docs/storage/overview),否則你會看到「snapshot not found」錯誤。 > > Agent 執行的快照是最精簡的恢復成品:只保留恢復已暫停執行所需的內容,並會在執行完成後刪除。請使用 [tracing](https://mastra.zisheng.pro/zh-HK/docs/observability/overview) 保存執行記錄,並使用 [memory](https://mastra.zisheng.pro/zh-HK/docs/memory/overview) 保存對話記錄。 ## 審批的運作方式 Mastra 提供兩種不同的 Tool call 暫停機制:**執行前審批**和**執行階段暫停**。 ### 執行前審批 執行前審批會在 Tool call 的 `execute` 函式執行\_之前\_將其暫停。LLM 仍會決定呼叫哪個 Tool 並提供參數,但在你明確批准之前,`execute` 不會執行。 以下旗標配合 OR 邏輯控制此行為。只要\_其中一個\_為 `true`,呼叫便會暫停: | 旗標 | 設定位置 | 範圍 | | --------------------------- | ---------------------------- | ---------------------- | | `requireToolApproval: true` | `stream()` / `generate()` 選項 | 暫停該請求的**每個** Tool call | | `requireApproval: true` | `createTool()` 定義 | 暫停對**該特定 Tool** 的呼叫 | 串流會發出包含 `toolCallId`、`toolName` 和 `args` 的 `tool-call-approval` chunk。使用串流的 `runId` 呼叫 `approveToolCall()` 或 `declineToolCall()` 以繼續: ```typescript const stream = await agent.stream("What's the weather in London?", { requireToolApproval: true, }) for await (const chunk of stream.fullStream) { if (chunk.type === 'tool-call-approval') { console.log('Tool:', chunk.payload.toolName) console.log('Args:', chunk.payload.args) // Approve const approved = await agent.approveToolCall({ runId: stream.runId }) for await (const c of approved.textStream) process.stdout.write(c) // Or decline const declined = await agent.declineToolCall({ runId: stream.runId }) for await (const c of declined.textStream) process.stdout.write(c) } } ``` #### 使用函式進行條件式審批 除了布林值外,`requireToolApproval` 也接受一個函式,用於逐次決定 Tool call 是否需要審批。函式會收到 `toolName`、模型傳入的 `args`、`requestContext` 和 `workspace`。傳回 `true` 表示該呼叫需要審批,傳回 `false` 則表示允許執行。這讓你可以在執行階段設定審批條件,例如只審批名稱符合某個模式的 Tool: ```typescript const stream = await agent.stream('Clean up old records', { requireToolApproval: ({ toolName }) => /^delete_/.test(toolName), }) ``` Tool 本身的 `requireApproval` 設定優先於上述函式,其規則會決定該 Tool 是否需要審批。如果函式拋出錯誤,為安全起見,該呼叫會要求審批。 > **備註:** 以函式設定的 `requireToolApproval` 僅適用於一般 `stream()` / `generate()` 呼叫。Durable agent 和已儲存的 Agent 會保存其選項,而函式無法序列化,因此它們只接受布林值。如果在這些情況下傳入函式,系統會改為要求審批每個 Tool call。 #### 將審批綁定至確切的 Tool 參數 對於敏感 Tool,應將審批綁定至向審批者顯示的確切 Tool 名稱和參數。如果這些參數在執行前有所變動,Tool 就不應以舊有審批執行。 `tool-call-approval` chunk 已包含 `toolName`、`toolCallId` 和 `args`。你可以在顯示審批請求時為這些欄位建立指紋。以下範例使用 JSON 字串作為指紋,但在正式環境中,應使用 Tool 名稱和參數的穩定雜湊值: ```typescript import { Agent } from '@mastra/core/agent' // For your production usecase, build a stable hash of the tool name and args function actionFingerprint(toolName: string, args: unknown) { const payload = JSON.stringify({ toolName, args }) return `fingerprint-${payload}` } const sensitiveTools = new Set(['issue_refund', 'delete_record']) const approvedFingerprints = new Set() export const approvalBoundAgent = new Agent({ id: 'approval-bound-agent', name: 'Approval Bound Agent', model: 'openai/gpt-5.6-sol', tools: { issueRefundTool, deleteRecordTool }, hooks: { beforeToolCall: ({ toolName, input }) => { if (!sensitiveTools.has(toolName)) return const fingerprint = actionFingerprint(toolName, input) if (!approvedFingerprints.delete(fingerprint)) { return { proceed: false, output: `Tool call blocked: approval did not match ${toolName} arguments.`, } } }, }, }) ``` ```typescript const stream = await approvalBoundAgent.stream('Refund order ord-1042', { requireToolApproval: ({ toolName }) => sensitiveTools.has(toolName), }) async function consumeApprovalStream(currentStream: typeof stream) { for await (const chunk of currentStream.fullStream) { if (chunk.type === 'tool-call-approval') { const { toolName, toolCallId, args } = chunk.payload const fingerprint = actionFingerprint(toolName, args) // Present toolName, args, and fingerprint to your approval UI. const approved = await showApprovalDialog({ toolName, args, fingerprint }) const nextStream = approved ? await approveReviewedToolCall(currentStream.runId, toolCallId, fingerprint) : await approvalBoundAgent.declineToolCall({ runId: currentStream.runId, toolCallId }) await consumeApprovalStream(nextStream) } } } async function approveReviewedToolCall(runId: string, toolCallId: string, fingerprint: string) { approvedFingerprints.add(fingerprint) return approvalBoundAgent.approveToolCall({ runId, toolCallId }) } await consumeApprovalStream(stream) ``` 在正式環境中,請將已批准的指紋儲存在耐久儲存空間,並按用戶、執行、Tool call 和政策版本劃分範圍。上述 `Set` 刻意保持精簡,讓界線清晰可見:審批只會使用一次,而且只適用於審批時所檢視的同一組標準 Tool 參數。 ### 使用 `suspend()` 在執行階段暫停 Tool 也可以在其 `execute` 函式執行\_期間\_呼叫 `suspend()` 來暫停。當 Tool 開始執行後,發現需要用戶提供額外資料或確認才能完成時,這種方式很有用。 串流會發出一個 `tool-call-suspended` chunk,其中包含由 Tool 的 `suspendSchema` 定義的自訂 payload。你可以使用符合 Tool `resumeSchema` 的資料呼叫 `resumeStream()` 來恢復執行。 ```typescript const weatherTool = createTool({ id: 'get-weather', inputSchema: z.object({ location: z.string().optional(), }), suspendSchema: z.object({ question: z.string(), }), resumeSchema: z.object({ location: z.string(), }), execute: async ({ location }, context) => { if (!location) { return await context?.agent?.suspend({ question: 'Which city would you like the weather for?', }) } return await fetchWeather(location) }, }) ``` > **備註:** `suspend()` 不會拋出錯誤,呼叫後請立即傳回(例如 `return await suspend({ ... })`)。在 Tool 暫停前,`await suspend(...)` 之後的程式碼仍會執行。 ## 在 `generate()` 中使用 Tool 審批 Tool 審批也適用於使用 `generate()` 的非串流情況。當 Tool 需要審批時,`generate()` 會立即傳回,其中包括 `finishReason: 'suspended'`、包含 Tool call 詳情(`toolCallId`、`toolName`、`args`)的 `suspendPayload`,以及 `runId`: ```typescript const output = await agent.generate('Find user John', { requireToolApproval: true, }) if (output.finishReason === 'suspended') { console.log('Tool requires approval:', output.suspendPayload.toolName) // Approve const result = await agent.approveToolCallGenerate({ runId: output.runId, toolCallId: output.suspendPayload.toolCallId, }) console.log('Final result:', result.text) // Or decline const result = await agent.declineToolCallGenerate({ runId: output.runId, toolCallId: output.suspendPayload.toolCallId, }) } ``` ### Stream 與 generate 比較 | 項目 | `stream()` | `generate()` | | ---- | ---------------------------- | ------------------------------------------------ | | 回應類型 | 串流 chunk | 完整回應 | | 審批偵測 | `tool-call-approval` chunk | `finishReason: 'suspended'` | | 批准方法 | `approveToolCall({ runId })` | `approveToolCallGenerate({ runId, toolCallId })` | | 拒絕方法 | `declineToolCall({ runId })` | `declineToolCallGenerate({ runId, toolCallId })` | | 結果 | 可反覆運算的串流 | 完整輸出物件 | > **備註:** `toolCallId` 在四個方法中都是可選的。當可能有多個待處理的 Tool call 時(常見於 supervisor agent),請傳入此值。如果省略,Agent 會恢復最近暫停的 Tool call。 ## Tool 層級審批 除了在 Agent 層級暫停每個 Tool call,你也可以將個別 Tool 標記為需要審批。這提供細緻控制:只有指定的 Tool 會暫停,其他則立即執行。 ### 使用 `requireApproval` 進行審批 在 Tool 定義中設定 `requireApproval: true`。無論 Agent 是否已設定 `requireToolApproval`,Tool 都會在執行前暫停: ```typescript export const testTool = createTool({ id: 'test-tool', description: 'Fetches weather for a location', inputSchema: z.object({ location: z.string(), }), outputSchema: z.object({ weather: z.string(), }), resumeSchema: z.object({ approved: z.boolean(), }), execute: async inputData => { const response = await fetch(`https://wttr.in/${inputData.location}?format=3`) const weather = await response.text() return { weather } }, requireApproval: true, }) ``` 當 `requireApproval` 為 `true` 時,串流會像 Agent 層級審批一樣發出 `tool-call-approval` chunk。使用 `approveToolCall()` 或 `declineToolCall()` 以繼續: ```typescript const stream = await agent.stream("What's the weather in London?") for await (const chunk of stream.fullStream) { if (chunk.type === 'tool-call-approval') { console.log('Approval required for:', chunk.payload.toolName) } } const handleApproval = async () => { const approvedStream = await agent.approveToolCall({ runId: stream.runId }) for await (const chunk of approvedStream.textStream) { process.stdout.write(chunk) } process.stdout.write('\n') } ``` ### 使用 `suspend()` 進行審批 使用這種方式時,Agent 和 Tool 都不會使用 `requireApproval`。Tool 的 `execute` 函式會改為呼叫 `suspend()`,在指定位置暫停,並向用戶傳回背景資料或確認提示。當審批取決於執行階段條件,而非無條件要求時,這種方式很有用。 ```typescript export const testToolB = createTool({ id: 'test-tool-b', description: 'Fetches weather for a location', inputSchema: z.object({ location: z.string(), }), outputSchema: z.object({ weather: z.string(), }), resumeSchema: z.object({ approved: z.boolean(), }), suspendSchema: z.object({ reason: z.string(), }), execute: async (inputData, context) => { const { resumeData: { approved } = {}, suspend } = context?.agent ?? {} if (!approved) { return suspend?.({ reason: 'Approval required.' }) } const response = await fetch(`https://wttr.in/${inputData.location}?format=3`) const weather = await response.text() return { weather } }, }) ``` 使用這種方式時,串流會包含 `tool-call-suspended` chunk,而 `suspendPayload` 會包含 Tool `suspendSchema` 所定義的 `reason`。使用 `resumeSchema` 資料和 `runId` 呼叫 `resumeStream` 以繼續: ```typescript const stream = await agent.stream("What's the weather in London?") for await (const chunk of stream.fullStream) { if (chunk.type === 'tool-call-suspended') { console.log(chunk.payload.suspendPayload) } } const handleResume = async () => { const resumedStream = await agent.resumeStream({ approved: true }, { runId: stream.runId }) for await (const chunk of resumedStream.textStream) { process.stdout.write(chunk) } process.stdout.write('\n') } ``` ## 自動恢復 Tool 使用會呼叫 `suspend()` 的 Tool 時,你可以啟用自動恢復,讓 Agent 根據用戶的下一則訊息恢復已暫停的 Tool。在 Agent 的預設選項或個別請求中,將 `autoResumeSuspendedTools` 設為 `true`: ```typescript import { Agent } from '@mastra/core/agent' import { Memory } from '@mastra/memory' const agent = new Agent({ id: 'my-agent', name: 'My Agent', instructions: 'You are a helpful assistant', model: 'openai/gpt-5-mini', tools: { weatherTool }, memory: new Memory(), defaultOptions: { autoResumeSuspendedTools: true, }, }) ``` 啟用後,Agent 會在收到下一則用戶訊息時,從訊息記錄中偵測已暫停的 Tool。它會根據 Tool 的 `resumeSchema` 擷取 `resumeData`,然後自動恢復 Tool。 以下範例展示完整的對話流程: ```typescript import { createTool } from '@mastra/core/tools' import { z } from 'zod' const weatherTool = createTool({ id: 'weather-tool', description: 'Fetches weather for a city', inputSchema: z.object({ city: z.string(), }), outputSchema: z.object({ weather: z.string(), }), suspendSchema: z.object({ message: z.string(), }), resumeSchema: z.object({ city: z.string(), }), execute: async (inputData, context) => { const { resumeData, suspend } = context?.agent ?? {} // If no city provided, ask the user if (!inputData.city && !resumeData?.city) { return suspend?.({ message: 'What city do you want to know the weather for?' }) } const city = resumeData?.city ?? inputData.city const response = await fetch(`https://wttr.in/${city}?format=3`) const weather = await response.text() return { weather: `${city}: ${weather}` } }, }) ``` ```typescript const stream = await agent.stream("What's the weather like?") for await (const chunk of stream.fullStream) { if (chunk.type === 'tool-call-suspended') { console.log(chunk.payload.suspendPayload) } } // User sends follow-up on the same thread const resumedStream = await agent.stream('San Francisco') for await (const chunk of resumedStream.textStream) { process.stdout.write(chunk) } ``` ```text User: "What's the weather like?" Agent: "What city do you want to know the weather for?" User: "San Francisco" Agent: "The weather in San Francisco is: San Francisco: ☀️ +72°F" ``` 第二則訊息會自動恢復已暫停的 Tool。Agent 從用戶訊息中擷取 `{ city: "San Francisco" }`,並將其作為 `resumeData` 傳入。 ### 要求 若要使用自動 Tool 恢復: - **已設定 Memory**:Agent 需要 Memory,才能跨訊息追蹤已暫停的 Tool - **相同 thread**:跟進訊息必須使用相同的 Memory thread 和資源識別碼 - **已定義 `resumeSchema`**:Tool 必須定義 `resumeSchema`,讓 Agent 知道應從用戶訊息中擷取哪種資料結構 ### 手動與自動恢復比較 | 方式 | 使用情況 | | ------------------------------ | ------------------------ | | 手動(`resumeStream()`) | 程式化控制、webhook、按鈕點擊、外部觸發器 | | 自動(`autoResumeSuspendedTools`) | 用戶以自然語言提供恢復資料的對話流程 | 兩種方式使用相同的 Tool 定義。只有當訊息記錄中存在已暫停的 Tool,而且用戶在相同 thread 傳送新訊息時,才會觸發自動恢復。 ## 重新啟動後恢復 上述範例會在暫停和審批之間保留 `stream.runId`。只要程序仍在執行,這種方式便有效;但在正式環境中,審批通常會在稍後才送達,屆時可能已重新整理頁面、重新啟動伺服器,或由負載平衡器後方的另一個伺服器實例處理。 使用 [`listSuspendedRuns()`](https://mastra.zisheng.pro/zh-HK/reference/agents/listSuspendedRuns),從儲存空間重新找出對話中待處理的執行: ```typescript // In the request handler that receives the user's decision const { runs } = await agent.listSuspendedRuns({ threadId: 'thread-123', resourceId: 'user-456', }) const run = runs[0] const toolCall = run?.toolCalls[0] if (run && toolCall) { let stream if (toolCall.requiresApproval) { // Suspended by requireApproval — approve or decline the tool call stream = await agent.approveToolCall({ runId: run.runId, toolCallId: toolCall.toolCallId }) } else { // Suspended by suspend() — resume with the data the tool asked for console.log('Tool asked:', toolCall.suspendPayload) stream = await agent.resumeStream({ name: 'San Francisco' }, { runId: run.runId }) } for await (const chunk of stream.textStream) process.stdout.write(chunk) } ``` 每個傳回的執行都包含已暫停的 Tool call(`toolCallId`、`toolName`、`args` 和 `requiresApproval`)。審批暫停(`requiresApproval: true`)使用 `approveToolCall()` / `declineToolCall()` 回應,而以 `suspend()` 觸發的暫停則包含其 `suspendPayload`,並預期透過 `resumeStream()` 傳入恢復資料。這樣,即使不在記憶體中保留任何狀態,你仍可為兩種流程重建正確的 UI。 `sendToolApproval()` 會自動使用相同的儲存空間探索機制:如果在記憶體中找不到該 thread 的作用中執行,它會先在儲存空間查找已暫停的執行,然後才會失敗。如果有多個已暫停的執行符合該 thread,請傳入 `toolCallId` 以消除歧義。 同一探索功能也可透過 HTTP 的 `GET /agents/:agentId/suspended-runs`,以及客戶端 SDK 的 [`agent.listSuspendedRuns()`](https://mastra.zisheng.pro/zh-HK/reference/client-js/agents) 使用,讓建基於瀏覽器的審批 UI 可以直接重新找出待處理的執行。 > **備註:** 只有在 Mastra 實例已設定持久[儲存 Provider](https://mastra.zisheng.pro/zh-HK/docs/storage/overview)時,已暫停的執行才能在重新啟動後保留。預設的記憶體內儲存空間會在程序結束時遺失快照。 ## Tool 審批:Supervisor agent [Supervisor agent](https://mastra.zisheng.pro/zh-HK/docs/capabilities/subagents) 使用 `.stream()` 或 `.generate()` 協調多個 subagent。當 subagent 呼叫需要審批的 Tool 時,請求會沿委派鏈向上傳遞,並在 supervisor 層級顯示: 1. Supervisor 將工作委派給 subagent。 2. Subagent 呼叫具有 `requireApproval: true` 或使用 `suspend()` 的 Tool。 3. 審批請求向上傳遞至 supervisor。 4. 你在 supervisor 層級批准或拒絕。 5. 決定向下傳遞回 subagent。 Tool 審批也會透過多層委派傳遞。如果 supervisor 委派給 subagent A,而 subagent A 再委派給 subagent B,且 subagent B 有一個設定了 `requireApproval: true` 的 Tool,審批請求仍會在最上層 supervisor 顯示。 ### 在 Supervisor agent 中批准和拒絕 以下範例建立一個具有需審批 Tool 的 subagent。當 Tool 觸發審批請求時,請求會在 supervisor 的串流中顯示為 `tool-call-approval` chunk: ```typescript import { Agent } from '@mastra/core/agent' import { createTool } from '@mastra/core/tools' import { Memory } from '@mastra/memory' import { z } from 'zod' const findUserTool = createTool({ id: 'find-user', description: 'Finds user by ID in the database', inputSchema: z.object({ userId: z.string(), }), outputSchema: z.object({ user: z.object({ id: z.string(), name: z.string(), email: z.string(), }), }), requireApproval: true, execute: async input => { const user = await database.findUser(input.userId) return { user } }, }) const dataAgent = new Agent({ id: 'data-agent', name: 'Data Agent', description: 'Handles database queries and user data retrieval', model: 'openai/gpt-5-mini', tools: { findUserTool }, }) const supervisorAgent = new Agent({ id: 'supervisor', name: 'Supervisor Agent', instructions: `You coordinate data retrieval tasks. Delegate to data-agent for user lookups.`, model: 'openai/gpt-5.6-sol', agents: { dataAgent }, memory: new Memory(), }) const stream = await supervisorAgent.stream('Find user with ID 12345') for await (const chunk of stream.fullStream) { if (chunk.type === 'tool-call-approval') { console.log('Tool requires approval:', chunk.payload.toolName) console.log('Arguments:', chunk.payload.args) // Approve the tool call const resumeStream = await supervisorAgent.approveToolCall({ runId: stream.runId, toolCallId: chunk.payload.toolCallId, }) for await (const resumeChunk of resumeStream.textStream) { process.stdout.write(resumeChunk) } // To decline instead, use: const declineStream = await supervisorAgent.declineToolCall({ runId: stream.runId, toolCallId: chunk.payload.toolCallId, }) } } ``` ### 在 Supervisor agent 中使用 `suspend()` Tool 也可以使用 [`suspend()`](#approval-using-suspend) 暫停執行,並向用戶傳回背景資料。這種方式會像 `requireApproval` 一樣貫穿 supervisor 委派鏈:暫停會在 supervisor 層級顯示: ```typescript const conditionalTool = createTool({ id: 'conditional-operation', description: 'Performs an operation that may require confirmation', inputSchema: z.object({ operation: z.string(), }), suspendSchema: z.object({ message: z.string(), }), resumeSchema: z.object({ confirmed: z.boolean(), }), execute: async (input, context) => { const { resumeData } = context?.agent ?? {} if (!resumeData?.confirmed) { return context?.agent?.suspend({ message: `Confirm: ${input.operation}?`, }) } // Proceed with operation return await performOperation(input.operation) }, }) ``` ```typescript // When using this tool through a subagent in supervisor agents for await (const chunk of stream.fullStream) { if (chunk.type === 'tool-call-suspended') { console.log('Tool suspended:', chunk.payload.suspendPayload.message) // Resume with confirmation const resumeStream = await supervisorAgent.resumeStream( { confirmed: true }, { runId: stream.runId }, ) for await (const resumeChunk of resumeStream.textStream) { process.stdout.write(resumeChunk) } } } ``` ### 在 `generate()` 中使用 Supervisor 審批 Tool 審批傳遞也適用於 Supervisor agent 的 `generate()`: ```typescript const output = await supervisorAgent.generate('Find user with ID 12345', { maxSteps: 10, }) if (output.finishReason === 'suspended') { console.log('Tool requires approval:', output.suspendPayload.toolName) // Approve const result = await supervisorAgent.approveToolCallGenerate({ runId: output.runId, toolCallId: output.suspendPayload.toolCallId, }) console.log('Final result:', result.text) } ``` ## 相關內容 - [Tools](https://mastra.zisheng.pro/zh-HK/docs/agents/using-tools) - [Agent 概覽](https://mastra.zisheng.pro/zh-HK/docs/agents/overview) - [MCP 概覽](https://mastra.zisheng.pro/zh-HK/docs/mcp/overview) - [Memory](https://mastra.zisheng.pro/zh-HK/docs/memory/overview) - [請求背景資料](https://mastra.zisheng.pro/zh-HK/docs/server/request-context)