同一把 API Key 同時能查資料、開工單、重設帳號、寄通知時,Agent 的工具清單就不再只是功能清單。
我沒有公司的 API Key。這個專案用的是 mock 工單和 mock SOP;「萬用 API Key」是我替這個練習場景放進去的危險前提。
Day 2 的 Agent 只有 create_ticket。它能根據問題開一張 mock 工單,能力小到幾乎不會讓人緊張。接著我想加查 SOP、重設密碼、寄通知。功能一列出來,我才發現一個很直接的問題:如果它們背後都共用同一個高權限憑證,模型每次選工具,實際上都在動用同一份過大的權限。
就算 prompt 寫著「只有必要時才能重設密碼」,API Key 也不會讀 prompt。後端看見的是一個帶著有效憑證的請求;如果 API 本身沒有再做權限判斷,誰送出請求都一樣。
這篇我沒有真的做出帳號重設 API。相反地,我先把它排除在 Agent 看得到的能力之外,並讓目前兩個工具各自待在清楚的邊界內。
假設我把 Agent 寫成這樣:
tools = [
search_it_sop,
create_ticket,
reset_password,
send_notification,
]
表面上只是多四個工具。實際上,模型會知道每個工具的名稱、描述與參數。任何一段使用者輸入、被檢索到的文件,甚至工具回傳的錯誤訊息,都可能讓它把任務帶到 reset_password 或 send_notification。
如果這些工具又共享一把可以做所有事的 Key,風險不在模型有沒有「故意作惡」。模型只要判斷錯一次,後端就可能接受一個本來不該被發出的請求。
我把這件事拆成兩層來看:
模型能看見什麼工具?
↓
該工具背後的身分能做什麼?
第一層是 Agent 的能力邊界。第二層是後端與 API 的授權邊界。少任何一層都不夠:不把 reset_password 放進工具清單,模型就選不到它;即使未來加回來,帳號系統仍必須拒絕不符合角色、對象與核准條件的請求。
目前 Agent 只能看到這兩個工具:
self.agent = create_agent(
model=model,
tools=[search_it_sop, create_ticket],
context_schema=HelpdeskContext,
system_prompt=SYSTEM_PROMPT,
)
search_it_sop 是唯讀工具,查的是程式內固定的 mock SOP。create_ticket 會建立一張只存在記憶體中的 mock 工單。LangChain 只把 tools 清單中的工具交給模型選擇。LangChain Tools 文件
我沒有把「以後可能會用到」的能力先掛上去。現在不存在的工具,模型不能選;目前不存在的 API Key,也沒有機會被它帶著跑。
search_it_sop 看起來最安全,因為它只讀資料:
@tool
def search_it_sop(
query: str,
runtime: ToolRuntime[HelpdeskContext],
) -> list[dict[str, str]]:
"""Search read-only IT SOP articles before answering a procedural IT question."""
return runtime.context.workflow.search_it_sop(query)
但只讀工具仍會把資料送回模型。今天它查的是我自己寫的三篇 mock SOP,內容可控;未來接外部文件或向量資料庫,檢索結果就得被視為不可信輸入。那會是 retrieval rails 的工作,不該因為它叫做「搜尋」就被當成安全的例外。
在目前版本,這個工具還有另一個用途:先查 SOP,才有資格建立工單。
if not self.sop_checked:
self.trace.add(
kind="guardrail",
name="sop_first",
status="blocked",
detail="尚未查詢 SOP,拒絕建立工單。",
)
return {
"status": "blocked",
"reason": "Search the read-only SOP source before creating a ticket.",
}
我沒有只在 prompt 裡寫「先查 SOP」。模型可能跳過那句話,所以開單工具本身會檢查 sop_checked。沒有搜尋紀錄,工具回傳拒絕結果,不會建立工單。
這條規則不是 RBAC 或 ACL,它只是這個 mock Helpdesk 的流程限制。不過它讓我確認了一件事:模型的建議和後端是否執行,可以是兩件不同的事。
兩個工具都透過 runtime.context 取用後端建立的流程物件:
@dataclass
class HelpdeskContext:
workflow: HelpdeskWorkflow
HelpdeskWorkflow 由後端建立,裡面才有 requested_by、mock ticket store、mock knowledge base 與 trace。模型只能決定查詢字串,或整理工單的標題、描述與優先級;它不能自行把自己變成管理員,也不能把工單改掛到別人名下。
目前 requested_by 仍是寫死的 demo.user,不是驗證後的登入身分。這不是完整的身分設計,我不會假裝它已經能處理多使用者權限;我只是先避免把這個欄位交給模型生成。
這次我沒有只留一個終端機指令。專案有本機頁面,可以輸入問題後查看 Agent 回覆、工具軌跡和 mock 工單。
uvicorn app.web:app --reload
開啟 http://127.0.0.1:8000 後,Day 2 的「SOP 優先」會跑成功開單的流程。Day 3 改用「先開單會被擋」,刻意在沒有查 SOP 時就要求寫入:
01 tool: create_ticket requested
02 guardrail: sop_first blocked
03 model: final_response 說明沒有建立工單
這個按鈕不需要 API Key,因為它不是在呼叫模型,而是可重現的 mock 流程。create_ticket 的呼叫確實進到後端,但在寫入前被 sop_first 擋下,所以不會出現 mock 工單。真正執行 LangChain Agent 的功能才需要模型 API Key;兩條路徑都會走同一個 SOP-first 檢查。

目前的測試直接呼叫後端流程,而不是期待模型每次都聽懂一句規則:
python -m unittest discover -s tests -v
這些測試還不能證明「萬用 API Key」已經被解決。Day 3 根本沒有接真實帳號重設、通知或外部 API。它們證明的是目前的 Agent 沒被預先塞進高風險能力,而且流程規則確實在後端執行。
之後真的要接 API,我不會用同一把 Key 讓所有工具共用。至少要分成唯讀與可寫的身分,縮小每個憑證的範圍;高風險動作還需要針對使用者、資源與核准狀態做後端授權。下一篇我會讓工具故意失敗,再看 Agent 的重試會不會把一個錯誤擴大。