昨天我把「企業共享資源協調」做成一套與我的工作無關、可以公開的 Reference System。成功路徑已經能從 NEW 走到 SUCCEEDED,也真的在 SQLite 留下一筆 booking。
但我回頭看整條 trace 時,發現最值得在意的不是「Agent 會不會找會議室」,而是這個問題:
如果模型說「可以預約」,誰來確定真的可以?
這就是今天要拆的邊界。
一開始我也很容易把任務分成「聯明的事交給 LLM,其他交給程式」。但這種分法太模糊。模型能做出一個看起來合理的判斷,不代表適合成為最後的決定者。
我改用一個更實際的問法:這個步驟如果判斷錯誤,能不能只是重新算一次?還是已經會改變權限、金錢或系統狀態?
依這個標準,我把流程分成五段:
| 階段 | 共享資源範例 | LLM 的位置 | 最後決定者 |
|---|---|---|---|
| Understand | 「明天下午晚一點」是什麼時段 | 解釋、抽取、標記不確定 | Typed schema |
| Propose | 哪些資源符合人數與設備 | 排候選、產生說明 | 查詢結果與硬性過濾 |
| Authorize | 這個人可不可以用受限資源 | 可解釋原因,不可自行放行 | RBAC、policy、人工核准 |
| Commit | 真正寫入預約 | 不持有資料庫權限 | Typed tool 與 transaction |
| Recover | 寫入後的下游步驟失敗 | 可協助分類錯誤 | retry、compensation、workflow state |
這張表改變了我對 Agent 的想像。LLM 不是從輸入一路接管到寫入資料庫;比較像在可控流程裡,專門處理模糊資訊的一個組件。
假設使用者說:
幫我找明天下午晚一點、十個人、要有投影機的空間。
模型可以轉成候選的 structured intent,但我不會讓這個 JSON 直接進 tool。在 Reference System 裡,這個物件還要通過確定性檢查:
proposal = llm_extract(user_message) # 只是提案
intent = service.validate_typed_intent(proposal)
run = service.start_booking_workflow(
actor_id="alice", # synthetic fixture 中的使用者
intent_payload=intent.to_dict(),
idempotency_key=request_id,
raw_request=user_message,
)
validate_typed_intent() 會檢查 action、resource type、timezone、duration 與 capacity;start_booking_workflow() 才會進入狀態機、policy 與 RBAC。即使模型產生了 action: create_booking,也不等於已獲得執行權。
我特別在意這個名稱:proposal。如果一開始就把模型輸出叫做 command,後面的程式很容易不知不覺把當成已核准的指令。
這一天沒有連接外部模型或真實系統。我用暫存 SQLite 與 synthetic identities 重跑現有的 integration tests,因為我想驗證的是「模型外面的邊界有沒有站住」。
第一個 test 先送進沒有 timezone 的時間,再送進 17 分鐘的 duration。兩者都必須在 tool call 前被拒絕;合法輸入則會正規化為 UTC。
test_typed_intent_requires_explicit_timezone_and_valid_contract ... ok
我從這裡觀察到,「明天下午」可以讓模型解釋,但時區尚未確認時,系統應該停下來追問,而不是自行猜一個時區後寫入。
第二個 test 用 180 分鐘預約觸發人工核准。我先直接呼叫 booking tool,系統拒絕;接著讓 workflow 通過申請人確認與管理者核准,才能進入 SUCCEEDED。
test_confirmation_approval_state_audit_and_trace_are_persisted ... ok
我看到的重點不是 test 變綠色,而是中間狀態真的被持久化。服務重新建立後,仍然讀得到 AWAIT_CONFIRMATION,不需要由模型根據對話重新推測。
第三個 test 先讓一個 synthetic user 嘗試取消別人的預約,再把「bypass RBAC」類型的文字放進 raw request。兩條路徑都被拒絕,原本預約仍是 active,且 audit log 多了一筆 denied security event。
test_rbac_and_prompt_injection_tool_abuse_are_denied ... ok
這裡的邏輯很直接:不管使用者怎麼說,權限來自 actor identity 與 policy,不是來自 prompt 的說服力。
這次結果只證明三件事:輸入 contract 會擋下已知的非法形狀;核准狀態不能被 tool call 繞過;已寫入的 RBAC 與一組惡意文字樣本能被拒絕。
沒有證明:
後面這些會各自有專門的實驗。我不想因為今天有三行 ok,就把結論寫成「Agentic Workflow 已通過安全驗證」。
我最後留下五個問題。只要其中一題的答案是「是」,我就不讓 LLM 單獨做最後決定:
對這些問題,LLM 仍然可以提供摘要、解釋與候選方案;只是最後那條不可越過的邊界,必須寫在模型之外。
我原本以為 Agentic Workflow 的關鍵是讓模型知道「下一步要做什麼」。真的把流程跑過一次後,我反而認為更重要的是:
LLM 用來處理模糊性;系統用來限定可以發生什麼,並證明最後真的發生了什麼。
明天我會把這些責任擺回一張完整架構裡,分清 Conversation、Orchestrator、Tool、Policy 與 State 之間的關係。
下一篇:Day 04|Agentic Workflow 參考架構:Conversation、Orchestrator、Tool、Policy、State
要在另一個環境重跑這三條邊界測試,請使用 docs/day03-operations/04-ai-automation-control-boundary.md。實驗只使用 synthetic identities 與暫存 SQLite,不需要雲端資源或真實公司系統。
本篇的使用者、資源、時間與規則均為 synthetic data,與我手上的CASE無關。