可重做的工作流 / 先審動作,再讓工具執行

AI Agent 要寄信或改資料,先讓人核對這次操作

人工審閱答案,與執行工具前核准,是兩個不同關卡。用 n8n 官方機制與一份虛構演練,確認誰能放行、看哪些參數,以及沒有核准時如何停止。

AI Agent 提出寄信與修改資料請求後,由人工檢視;核准路徑繼續,等待與拒絕路徑在執行前停止

讀完這篇,你會帶走

  • 寄外部訊息、更新或刪除資料等工具,可在執行前加人工核准。
  • 審核者應看得到工具名稱、目標和 AI 準備送出的參數;只看最終答案不夠。
  • 等待、拒絕與未知工具都要有停止規則;範例只模擬分類,未連線 n8n。

AI Agent 寫好一封信,主管看過文字才按送出,和 Agent 自己呼叫寄信工具,是兩件事。後者可能直接向外傳送資訊或改動資料。n8n 官方文件提供對指定工具加上 Human review 的做法:Agent 準備呼叫工具時先暫停,交由人核准或拒絕,核准後才執行。[1][3] 企業首先要定義的,是哪些動作值得放在這個關卡後面。

01 / 先分清兩種『轉人工』

情況人工介入的位置要避免的誤會
Agent 答不出問題改請人回答或接手對話有人補答,不代表工具動作已被審核
Agent 要寄信、更新或刪除特定工具執行之前,審核本次參數看過最終回覆,不代表工具尚未執行

n8n 另有『答不出問題時送往 Slack 請人協助』的範例。[2] 它解決的是無法回答的交接;本文討論的是高影響工具呼叫前的核准。兩者可以同時存在,但不能互相取代。

02 / 把關卡放在會改變外部狀態的工具上

n8n 文件列出的例子包括外部訊息、修改紀錄與刪除資料;可對全部工具或部分工具要求人工核准。[1] 編輯建議是先列出每個工具能做的最重動作,再決定是否需要審核。唯讀查詢也要受原系統權限控制,不能因為不寫入就任意開放資料。

示範工具核准前要看到建議規則
寄外部 Email收件對象與內容摘要先核准,再允許寄送
更新客戶紀錄紀錄 ID、欄位與新舊值指定負責人核對
刪除資料目標 ID、影響範圍與回復方式沒有明確核准就停止

03 / 審核卡要顯示『做什麼』和『對誰做』

n8n 的 Human review 設定可用 $tool.name 和 $tool.parameters 組出審核訊息,讓審核者看見 Agent 要使用的工具與輸入參數。[1] 實務上還應標出核准者、請求時間、資料來源與對外影響;這是編輯建議,不是 n8n 保證自動產生的欄位。訊息只放審核所需的最少資料,避免把完整敏感內容複製到通知頻道。

官方說明:拒絕時工具不執行,Agent 會收到拒絕結果;文件也建議在系統提示中說明哪些工具需核准、被拒時如何回應。[1] 沒有回覆的請求在本文的演練中一律維持等待。正式上線前還應由團隊決定逾時、代理審核者與撤銷規則;不要假設文件已替你定好這些政策。

04 / 用六筆虛構請求演練放行與阻擋

下面的 CSV、Python 與輸出都使用虛構 ID 與 .invalid 信箱。它們只模擬『工具白名單+核准狀態』的分類,不會寄信、刪資料或連線 n8n。把前兩段各存成 requests.csv 與 approval_check.py,再執行指令核對結果。

requests.csv
request_id,tool,target,review
r-001,read_internal_note,note-demo-01,none
r-002,send_external_email,reviewer@example.invalid,approved
r-003,update_record,record-demo-17,denied
r-004,delete_record,record-demo-09,pending
r-005,send_external_email,team@example.invalid,none
r-006,transfer_funds,account-demo-03,approved
approval_check.py
"""Practice approval decisions with fictional tool calls; this never calls a tool."""

import csv
import re
import sys
from pathlib import Path

FIELDS = ["request_id", "tool", "target", "review"]
READ_ONLY = {"read_internal_note"}
GATED = {"send_external_email", "update_record", "delete_record"}
REVIEWS = {"none", "pending", "approved", "denied"}


def load_requests(path):
    with Path(path).open(encoding="utf-8-sig", newline="") as stream:
        reader = csv.DictReader(stream)
        if reader.fieldnames != FIELDS:
            raise ValueError("欄位必須是 request_id,tool,target,review")
        rows = []
        seen = set()
        for number, raw in enumerate(reader, 2):
            if None in raw or any(value is None for value in raw.values()):
                raise ValueError(f"第 {number} 列欄位數不符")
            row = {key: value.strip() for key, value in raw.items()}
            if not re.fullmatch(r"r-[0-9]{3}", row["request_id"]):
                raise ValueError(f"第 {number} 列 ID 格式錯誤")
            if row["request_id"] in seen:
                raise ValueError(f"第 {number} 列 ID 重複")
            if not re.fullmatch(r"[a-z_]{2,40}", row["tool"]):
                raise ValueError(f"第 {number} 列工具名稱格式錯誤")
            if not re.fullmatch(r"[A-Za-z0-9@._-]{1,80}", row["target"]):
                raise ValueError(f"第 {number} 列目標格式錯誤")
            if row["review"] not in REVIEWS:
                raise ValueError(f"第 {number} 列核准狀態錯誤")
            seen.add(row["request_id"])
            rows.append(row)
    if not rows:
        raise ValueError("至少需要一筆虛構請求")
    return rows


def outcome(row):
    tool = row["tool"]
    if tool in READ_ONLY:
        return "唯讀示範可繼續"
    if tool not in GATED:
        return "阻擋未知工具"
    return {
        "approved": "核准後可進下一步",
        "denied": "拒絕並停止",
        "pending": "等待人工核准",
        "none": "等待人工核准",
    }[row["review"]]


def report(rows):
    lines = [
        "# 虛構 AI 工具核准演練", "",
        "本程式只分類請求,不寄信、不修改資料、不呼叫 n8n。", "",
        "| ID | 工具 | 目標 | 審閱狀態 | 分類結果 |",
        "| --- | --- | --- | --- | --- |",
    ]
    for row in rows:
        lines.append("| {} | {} | {} | {} | {} |".format(
            row["request_id"], row["tool"], row["target"], row["review"], outcome(row)
        ))
    return "\n".join(lines) + "\n"


if __name__ == "__main__":
    if len(sys.argv) != 2:
        raise SystemExit("用法:python approval_check.py requests.csv")
    try:
        print(report(load_requests(sys.argv[1])), end="")
    except (OSError, ValueError, csv.Error) as exc:
        raise SystemExit(f"停止:{exc}") from None
終端機指令
python -X utf8 approval_check.py requests.csv
本範例的實際輸出
# 虛構 AI 工具核准演練

本程式只分類請求,不寄信、不修改資料、不呼叫 n8n。

| ID | 工具 | 目標 | 審閱狀態 | 分類結果 |
| --- | --- | --- | --- | --- |
| r-001 | read_internal_note | note-demo-01 | none | 唯讀示範可繼續 |
| r-002 | send_external_email | reviewer@example.invalid | approved | 核准後可進下一步 |
| r-003 | update_record | record-demo-17 | denied | 拒絕並停止 |
| r-004 | delete_record | record-demo-09 | pending | 等待人工核准 |
| r-005 | send_external_email | team@example.invalid | none | 等待人工核准 |
| r-006 | transfer_funds | account-demo-03 | approved | 阻擋未知工具 |

實際執行得到六筆分類:一筆唯讀示範可繼續、一筆已核准可進下一步、一筆拒絕、兩筆等待、一筆未知工具即使標示 approved 仍阻擋。程式沒有執行任何真正的後續動作;因此這項演練只驗證示範政策,不代表 n8n 已完成設定或安全測試。

05 / 接到正式流程前,再問四個問題

  • 所有可寄送、寫入或刪除的工具是否都經過關卡?是否還有未列出的旁路?
  • 審核者能否從參數看清目標與影響?通知是否只暴露必要資料?
  • 拒絕、逾時、代理審核與重試時,誰負責處理並留下紀錄?
  • 用無敏感資料演練核准、拒絕與沒有回覆,確認外部系統的實際結果。

資料來源

  1. Human-in-the-loop for tools ↗n8n Docs

    2026-10-09 以 Firecrawl maxAge=0 查閱官方全文;頁面未列固定發表日期。核對執行前核准、Approve/Deny、$tool 參數及系統提示建議。

  2. Set a human fallback for AI workflows ↗n8n Docs

    2026-10-09 以 Firecrawl maxAge=0 查閱官方全文;頁面未列固定發表日期。此範例是答不出問題時轉人工,與工具執行前核准不同。

  3. Tools Agent ↗n8n Docs

    2026-10-09 以 Firecrawl maxAge=0 查閱官方全文;頁面未列固定發表日期。核對 Tools Panel 中的 Human review 設定與工具連接方式。

來源查閱:2026-10-10。本文含 AI 協助整理;已完成編輯核准。

把方法帶回工作,也把問題帶回來。與我們討論 ↗