iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
AI 自動化

協定、框架、架構:一條龍搞懂 AI Agent 是怎麼被造出來的系列 第 20 篇

Day 20:危險操作先問人:人工核准、操作紀錄與 Single-Agent 完成版

  • 分享至 

  • xImage
  •  

二十天了。

現在這個 Agent 已經會:

讀程式碼
使用 MCP Tool
根據 Observation 繼續下一步
遇到限制時停止

昨天又補上最小權限,讓模型就算要求讀 .env,Server 也不會照做。

但有些操作不是「永遠禁止」。

例如:

執行測試
修改檔案
執行部署
對外送出資料

真正需要的是:

模型可以提出要求,但不能自己批准自己的要求。

所以今天補上 Single-Agent 最後一塊:

Human Approval
+
Audit Log

這次只開放一個高風險操作:

執行 days/day02_typing/test_day02_typing.py

不接受任意 Shell、不允許臨時換另一份測試,也不把一次核准變成永久通行證。

一、模型想做,不等於使用者允許做

前面的 Agent Loop 大概是:

模型提出 Tool Call
↓
Host 檢查
↓
執行

今天在高風險 Tool 前面再加一道:

模型提出 Tool Call
↓
Host 檢查
↓
需要人工核准?
↓
是
↓
等待操作者批准
↓
確認批准的是同一個操作
↓
執行

也就是把:

Intent

和:

Authorization

拆開。

模型可以說:

我要執行 run_tests

但它沒有權限自己接著說:

我批准了。

核准能力只存在 Host 裡。

二、核准要綁定「真正要執行的東西」

假設確認畫面顯示:

要執行 test_a.py 嗎?

使用者按下允許。

結果真正跑的是:

test_b.py

那這個確認視窗就沒有任何意義。

所以今天先把:

Tool 名稱
+
完整參數

正規化後產生 fingerprint。

例如:

arguments = {
    "target": TestExecutor.TARGET,
}

approval = Approval(
    fingerprint(
        "run_tests",
        arguments,
    )
)

概念上可以理解成:

run_tests
+
{"target": "days/day02_typing/test_day02_typing.py"}

↓
fingerprint

↓
這張 Approval 只認這一組操作

後面真正執行時,再重新計算一次。

對得上才放行。

這裡的雜湊不是:

登入系統
授權伺服器
數位簽章

它只是在今天這個單行程範例裡,把:

使用者看到並核准的內容

和:

真正準備執行的內容

綁在一起。

真正的授權來源仍然是操作者。

模型不能自己建立有效 Approval。

三、核准只使用一次

今天的 Approval 還有另一條規則:

一次性

假設使用者核准:

run_tests(
    target="days/day02_typing/test_day02_typing.py"
)

第一次執行完成後,這張 Approval 就失效。

如果再拿同一張票執行一次:

→ 拒絕

即使第一次測試失敗,也不會自動拿原本的 Approval 重跑。

因為:

再次執行

本身就是另一個具有副作用的操作。

如果真的要 Retry,就應該重新確認。

這跟前面 Tool Budget 的概念有點像:

程式不能因為模型覺得「再試一次應該沒關係」,就替使用者擴大原本的授權。

四、先證明「不核准就真的不會執行」

今天的 Demo 不會一開始就走成功路線。

先送一次沒有 Approval 的 Request:

output, denied = runner.run(arguments)

print(output)

assert denied

這時預期結果一定是:

拒絕

而且不能偷偷啟動 pytest。

只有操作者明確傳入:

--approve-tests

才建立:

Approval(
    fingerprint("run_tests", arguments)
)

然後讓 Agent 真正跑完整流程。

簡化後的入口大概是:

import argparse
import asyncio

from ironman.agent import run_agent
from ironman.llm import OllamaClient
from ironman.mcp_bridge import ROOT, devbench
from ironman.security import (
    Approval,
    GovernedTools,
    TestExecutor,
    fingerprint,
)


async def main() -> None:
    parser = argparse.ArgumentParser()

    parser.add_argument(
        "--approve-tests",
        action="store_true",
    )

    args = parser.parse_args()

    arguments = {
        "target": TestExecutor.TARGET,
    }

    runner = TestExecutor(...)

    # 先證明沒有核准時一定被拒絕
    _, denied = runner.run(arguments)
    assert denied

    if args.approve_tests:
        runner.approval = Approval(
            fingerprint(
                "run_tests",
                arguments,
            )
        )

        async with (
            devbench(...) as bridge,
            OllamaClient() as llm,
        ):
            result = await run_agent(
                llm,
                GovernedTools(
                    bridge,
                    runner,
                ),
                "...",
            )

        assert result.stop_reason == "answered"

        assert any(
            obs.name == "run_tests"
            and not obs.is_error
            for obs in result.observations
        )

        # 同一張 Approval 不能重播
        _, denied = runner.run(arguments)
        assert denied


if __name__ == "__main__":
    asyncio.run(main())

整個測試順序是:

1. 沒有 Approval
→ 拒絕

2. 操作者核准固定操作
→ 執行

3. 拿同一張 Approval 再執行
→ 再次拒絕

這些都是程式真正走過的分支。

不是在 System Prompt 裡寫:

請務必先取得使用者同意

然後相信模型一定會照做。

五、人工核准也不能只顯示一句「允許嗎?」

如果確認畫面只有:

Agent 想執行一個操作,是否允許?

其實幫助不大。

操作者至少應該知道:

要使用哪個 Tool
要操作什麼目標
帶了哪些重要參數
可能造成什麼效果

例如這次可以顯示:

待核准操作:

Tool:
run_tests

Target:
days/day02_typing/test_day02_typing.py

影響:
pytest 將執行 Python 程式碼

這樣核准才是針對一個具體操作,而不是:

「我相信這個 Agent」

Human-in-the-loop 的目的不是讓使用者一直按「是」。

而是讓高風險操作在真正發生前,有一個可以檢查與拒絕的邊界。

六、執行紀錄不要只存在模型 Context

高風險操作真的執行後,還要留下紀錄。

今天使用 JSON Lines:

audit.jsonl

每一行記一個事件。

例如:

{
  "tool": "run_tests",
  "target": "days/day02_typing/test_day02_typing.py",
  "result": "approved"
}

實際紀錄還會包含時間與執行結果。

這份 Audit Log 放在模型之外。

因為對話內容不是可靠的稽核來源。

模型可能:

摘要掉內容
理解錯誤
漏掉細節

所以要記錄的是程式真正發生的事件。

今天至少區分:

Denied
Timeout
Non-zero Exit Code
Success

不能全部塞成一句:

執行失敗

不然事後根本不知道是:

權限被拒絕
測試沒通過
還是 Process 卡住

七、Audit Log 也不是法遵系統

今天的 JSONL 只是第一版的可觀察性。

它還不是完整的企業稽核系統。

正式環境至少還會需要考慮:

檔案權限
保存期限
誰能查看
誰能修改
防竄改
集中式 Log
敏感資料遮蔽

而且 Audit Log 也不代表:

所有 Prompt
所有原始碼
所有 Credential

都應該全部存下來。

今天反而刻意不把完整 Prompt、原始檔案內容或憑證寫進 Audit Log。

目的應該是留下:

足以知道發生過什麼的紀錄。

不是建立第二份敏感資料倉庫。

八、Single-Agent 到這裡終於收斂

把前面幾天全部疊起來,現在的 Agent 已經有:

LLM
↓
Agent Loop
↓
Tool Policy
↓
MCP Bridge
↓
Server Policy
↓
Workspace Boundary

高風險操作則另外走:

Agent Request
↓
GovernedTools
↓
Human Approval
↓
TestExecutor
↓
Audit Log

也就是:

模型
可以提出需求

Host
決定需求能不能真的發生

這就是今天最重要的分工。

九、這張 Approval 還有一個限制

今天的 fingerprint 綁定了:

Tool 名稱
+
參數

但還沒有綁:

工作目錄當下的版本

假設使用者核准:

執行 test_day02_typing.py

結果核准之後、真正執行之前,有人把這個檔案改掉了。

Tool 和 path 都沒變。

但真正執行的內容已經不是剛才看到的版本。

所以更嚴謹的設計還可以把:

Git Commit
檔案 Hash
Artifact Version

一起放進 Approval fingerprint。

這樣才能確認:

使用者批准的,不只是這個檔名,而是這一版內容。

今天的範例先停在單人、本機、短時間操作的情境。

但這個限制要明確留下來。


Day 19 做的是:

什麼東西 Agent 根本不能碰

今天再補上:

有些東西可以碰
但必須先有人批准

到這裡,Single-Agent 的主線可以先告一段落。

我們已經有:

Tool Calling
MCP
ReAct
Plan-and-Execute
LangGraph
Google ADK
Least Privilege
Human Approval
Audit Log

下一步不是繼續把更多權限塞給同一個 Agent。

而是開始拆角色。

例如:

一個 Agent 負責研究
一個 Agent 負責執行
一個 Agent 負責審查
一個 Supervisor 決定任務交給誰

不過角色變多之後,今天的安全原則不能消失。

三個 Agent 不會讓一個危險 Tool 自動變安全。

每個角色仍然只能拿到完成自己任務真正需要的權限。

Day 21:

一個 Agent 不夠嗎?Multi-Agent 與 Supervisor 架構。


上一篇
Day 19:Agent 會動手後,誰來踩煞車?安全治理與最小權限
下一篇
Day 21:一個 Agent 不夠嗎?Multi-Agent 與 Supervisor 架構
系列文
協定、框架、架構:一條龍搞懂 AI Agent 是怎麼被造出來的 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言