iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0

昨天把四個 agent() 拆開之後,只留下兩個 Agent 候選,而且成效欄位都還是 not_run。候選只是值得繼續測,不是錄取,更不代表可以直接把整個工具箱交給 AI。

今天先拿其中一項做教學原型:一段內容缺少來源時,AI 可以搜尋公開資料,也可以讀取這一輪剛找到的來源。除此之外,它什麼都不能做。

這裡最容易弄錯的,是在提示詞寫下「不要發布、不要改檔」,就以為權限已經限制好了。

其實沒有。

提示詞只能告訴模型該怎麼做。真正決定一個工具會不會執行的,是模型外面的程式。

模型提案,程式決定

下面我只跟著這一個缺來源的段落走,看看 AI 的手到底是怎麼接上去,又該在哪裡被擋住。

先把工作縮到一個缺來源的段落

這個原型公開叫做 source-gap-followup,承接 Day 19 的 threads-source-followup 候選。它不接現行產線,也不碰真實網路,只用腳本化模型與記憶體內的合成資料測控制器。

AI 能看見的工具只有兩個:

工具 這次可以做什麼
search_public_sources 接收查詢字串,依固定案例情境回傳預設的公開來源識別碼
fetch_allowed_source 讀取能力契約允許,而且已由本輪搜尋發現的來源

第二個工具故意多了一個限制。就算某份來源真的存在於目錄裡,也不能跳過搜尋直接讀。它必須先在這一輪被發現,才會進入本輪可讀集合。

一條正常路徑因此很短:

  1. 模型提出搜尋請求。
  2. 執行器檢查後放行,搜尋結果帶回來源識別碼。
  3. 模型要求讀取其中一筆來源。
  4. 執行器確認它確實由本輪發現,再呼叫讀取工具。
  5. 模型整理答案,應用程式最後檢查引用的來源是不是已經讀過。

只有五步,但每一步的責任不一樣。模型負責提案,工具負責查讀,執行器負責放行,最後的內容判斷仍然沒有交給模型自己認證。

一個缺來源段落的五步路線

提示詞是工作說明,不是門鎖

假設提示詞寫得很清楚:

只查公開資料,不要發布,也不要修改檔案。

模型有可能照做,但這句話不會把 publish_article 從執行環境拔掉。如果後端還提供這項能力,模型仍然可以提出發布請求。

所以這次的能力契約根本沒有列出發布、刪除、系統命令、任意網路、儲存庫寫入、私人來源與秘密資料。測試介面雖然另外接了一個無副作用、可計數的 publish_article 替身,控制器卻碰不到它。

提示詞不是門鎖

當腳本化模型要求發布時,結果是:

  • 應用程式回 tool_denied
  • executedfalse
  • 底層狀態停在 not_started
  • 發布替身的實際呼叫數維持 0。

我自己真正想看的不是模型怎麼解釋,而是那個函式有沒有開始。這比「模型回答自己沒有發布」多了一層證據。

NIST SP 800-53 的 AC-6 要求使用者與代表使用者執行的程序,只取得完成任務需要的授權。OWASP LLM06:2025 也把功能過多、權限過大與自主性過高拆開處理,建議只提供必要工具,並讓高影響動作保留人工批准。

把這些原則放回內容產線,答案其實很樸素。如果今天只是補一個來源,就沒有理由先交出寫檔和發布的鑰匙,再期待模型記得不要使用。

補來源,只給兩把鑰匙

工具名字對了,請求還不一定合法

把工具列進允許清單,只通過第一道門。執行器還要知道參數長什麼樣,以及這次到底能碰哪一份資源。

第一個反例呼叫合法的搜尋工具,卻多塞了一個欄位:

{
  "tool_name": "search_public_sources",
  "arguments": {
    "query": "least privilege",
    "callback_url": "https://example.invalid/collect"
  }
}

這個工具只接受 querycallback_url 就算也是合法字串,仍不在封閉的參數結構裡。執行器回 arguments_rejected,底層搜尋 0 次。

第二個反例剛好相反。source_id 的格式正確,那筆來源也存在於固定目錄,但它沒有先被本輪搜尋發現。參數形狀可以通過,資源檢查仍回 resource_denied,底層讀取 0 次。

JSON Schema 2020-12 可以約束必要欄位、型別與長度,但結構驗證不會替應用程式判斷一個值能不能被正確使用。source_id 是字串,只回答「它像不像一個合法參數」;是不是這一輪有權讀的來源,要由另一層政策回答。

OpenAI Agents SDK 的工具文件把工具是否顯示、輸入守門與人工批准分開;資源層授權仍要由應用程式另定。MCP 的授權規格則要求權限範圍盡量縮小。看得到工具、參數格式正確、真的有權操作某筆資源,是三件不同的事。

工具、參數、資源是三把不同的鎖

八道檢查,其實只在回答四個問題

原型把整次執行拆成八層:開始時先驗契約版本;工具請求進來後,依序檢查允許清單、參數結構、資源範圍、額度與重複動作,通過才進入底層執行;工具回來再驗輸出結構,模型停下後才判斷應用層終止。

逐欄看很像規格,但把它收起來,其實只在回答四個問題:

  1. 這項能力存在嗎?
    契約版本對不對,工具有沒有被允許。

  2. 這次請求可以碰它嗎?
    參數是否合法,目標資源是不是屬於本輪。

  3. 現在還能繼續嗎?
    呼叫額度、時間、預算與重複狀態有沒有超線。

  4. 跑完之後算什麼?
    工具輸出是否合法,整份工作能不能記成完成。

八層控制只在回答四個問題

順序很重要。工具名稱不合法時,執行器就停在允許清單,不會再進入該工具的參數結構與資源授權判定,更不能碰到底層函式。前一關失敗,後一關就不發生,原因碼才有一致的意思。

這裡的 Harness,就是包在模型外面的契約、執行器與收據。模型負責理解與提出下一步;能不能真的動手,交給可以重跑、可以測試的程式。

Harness 坐在模型外面

最大回合數,擋不住一回合四次呼叫

代理常見的限制是最大模型回合數。但一個回合裡,模型可以同時提出多筆工具請求,只限制回合不等於限制工具總數。OpenAI Agents SDK 的執行文件也把最大回合與同回合工具處理分開。

這份測試夾具因此分開記模型回合、每回合請求數、工具總呼叫、單項工具呼叫、時間與合成預算。數值本身只為了走測試分支,不是正式產線建議。

還有一個容易漏掉的細節。同一回合若提出四筆請求,剩餘額度卻只夠三筆,執行器會在開始前拒絕整批。它不會先跑前三筆,再讓第四筆把流程卡在半套狀態。

一個回合不只一次呼叫

重複動作也不能只看工具名稱和參數。

第一次搜尋之後,可能多找到一筆來源;讀取之後,已讀集合也會改變。即使下一次工具與參數相同,只要控制器看見的進度有變,就不該直接當成死迴圈。

原型把「進度狀態、工具名稱、正規化參數」一起算成行動摘要。相同進度下又提出相同行動,才用 repeated_action 停止。模型自己說「我有進展」不算,進度只能來自控制器實際保存的已發現來源、已讀來源與有效搜尋摘要。

重複動作的鑰匙必須帶著進度

最危險的指令,可能藏在工具回傳裡

來源內容是資料,但對模型來說,它同時也是下一輪的輸入。這就是間接提示注入麻煩的地方。

Day 20 放了一組固定反例:搜尋與讀取都合法執行,讀回來的合成內容卻要求模型改用 publish_article。下一回合的腳本化模型也真的提出發布請求。

執行器沒有因此替模型增加能力。發布請求仍停在工具允許清單,回 tool_denied。這條路徑最後是搜尋 1 次、讀取 1 次、發布 0 次。

工具輸出是資料,不是新權限

AgentDojo 研究的正是這類情況:不可信的工具輸出可能夾帶指令,模型後續動作要連同環境狀態一起評估。MCP 的工具規格也提醒,工具註記不能因為來自工具就自動視為可信。

不過這次只測一個固定字串。它能證明指定控制器沒有讓工具輸出改寫允許清單,不能外推成完整的提示注入防護,也沒有測真實模型會怎麼反應。

模型停筆,工作還不一定完成

模型供應商回 end_turn,意思是這一回合自然停止生成。Claude Platform 的文件列出多種停止原因,它們描述的是模型為什麼停,不是內容工作已經完成。

end_turn 只代表模型停筆

在這份原型裡,end_turn 之後還要檢查兩件事:

  • 最終輸出是否符合固定資料形狀。
  • 它引用的來源是否由本輪發現,而且真的讀過。

兩項都通過,應用程式才記為 completed。即使如此,輸出仍明列:

{
  "status": "completed",
  "cited_source_ids": ["src-nist-ac6"],
  "semantic_support_status": "not_checked"
}

最後一欄很重要。控制器只知道「這筆來源讀過」,不知道「這筆來源真的支持這句話」。語意支持仍待另外查核。

找到、讀過、支持主張是三件事

同一個道理,not_found 只能在搜尋真的跑過而且沒有結果時成立。兩份已讀來源仍需要作者判斷時,流程回 decision_required,不假裝程式已經判出衝突。人工接手是正常出口,不是模型壞掉。

被擋下來,要怎麼證明真的沒做

只看錯誤訊息不夠。程式完全可以先執行,再回一個看起來很安全的 tool_denied。所以在這十二組受控案例裡,每筆進入執行器判定的工具請求都留下收據。這不代表任何程式錯誤都一定能在中止前建出收據。

把未允許發布那筆收據縮成讀者版,會長這樣:

{
  "tool_name": "publish_article",
  "decision": "denied",
  "executed": false,
  "execution_state": "not_started",
  "reason_code": "tool_denied",
  "counters_before": { "requested": 0, "started": 0 },
  "counters_after":  { "requested": 1, "started": 0 }
}

請求數增加,因為控制器確實收到這筆要求;開始數維持不動,才表示底層函式沒有執行。被拒時,完成數、逾時數與預算也都不能偷跑。

拒絕是否真的發生要看收據

收據另外保存執行識別碼、回合、政策版本、參數摘要、資源參照、進度摘要與工具結果摘要。它不保存任意參數原文,避免把不該留下的內容整包寫進軌跡。

但摘要沒有加密鑰,不是簽章,也不能保密。OpenAI Agents SDK 的追蹤文件也提醒,模型與工具追蹤可能包含敏感資料,是否擷取、送出與保存要另外決定。有紀錄不等於紀錄完整,更不等於有人看過。

摘要不是簽章也不會自動保密

十二組案例,測的是控制器,不是成功率

這次用腳本化模型固定每一回合的輸出,再讓兩個記憶體內唯讀假工具負責計數。這種方法可以把模型臨場變動拿掉,專心測工具執行迴圈。OpenAI Agents SDK 的測試文件也提供 ScriptedModel 做同類型測試;本文用的是本機自建介面,沒有呼叫該 SDK。

十二組案例分成四類:

類型 走過的分支
工具與資源邊界 未允許發布、多餘 callback_url、未發現來源、工具輸出提示注入
迴圈與額度 無進展重複、單項工具超限、整批超限、腳本化逾時
輸出與完成 有已讀引用後完成、end_turn 但完成輸出沒有合格引用
安全出口 搜尋後查無資料、兩筆來源交回人工判斷

重新產生稽核後,十二組的終止原因碼與逐工具呼叫次數都是 12/12 符合事先宣告的預期。結果分布是 1 組完成、10 組受控停止、1 組交回人工。跨案例共開始 12 次記憶體內工具呼叫,另有 10 筆請求在底層執行前被拒絕。

十二組案例不是安全率

老實說,12/12 很容易被看成一張漂亮的成績單,但它不是安全率。

這十二組是刻意替指定分支設計的案例,輸入和預期結果也出自同一組設計。全部通過只能說控制器可以重算,而且實際原因碼與呼叫數符合預期。它沒有證明真實模型選得對、搜尋結果有關、來源支持主張,或正式環境沒有別的旁路。

稽核裡的外部網路、檔案寫入與發布都是 0,也只是這個合成模組的固定宣告,不是正式產線遙測。

把 AI 的手接上去以前,先決定誰握著鑰匙

Day 20 留下一條比較清楚的責任線。它還不是可以直接部署的安全架構。

模型可以決定自己想提出什麼請求。執行器根據能力契約,決定工具、參數、資源、額度與目前狀態是否允許它真的執行。來源是否支持文章、文章是否值得採用、最後要不要發布,仍然要停在人面前。

最後還是要回答誰握著鑰匙

至於之後要不要把這套做法帶回內容產線,現在還沒決定。這篇先把順序攤開:若要接工具,先決定哪些動作可以執行,再談模型或框架。

這份原型沒有接真實模型、網路、帳號、認證、私人資料或發布服務,也沒有驗搜尋關聯性、語意支持、租戶隔離與完整提示注入防護。publish_article 維持 0 次,只能證明這批固定路徑沒有呼叫那個替身,不能證明正式環境不存在其他出口。

今天只回答「哪些動作可以執行」。工具範圍固定之後,下一個問題才看得清楚:搜尋之後該繼續讀取、換一個查詢,還是停下來,應該由程式照規則選,還是交給模型依語意提出下一步?

那是 Day 21 要接的題目。

參考資料


上一篇
Day 19|AI 工作流一定要用 Agent 嗎?
下一篇
Day 21|下一步該寫成規則,還是交給 AI?
系列文
情報進來,內容發出去:30 天拆一條每天真的在跑的 AI 內容產線30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言