有點感傷但又有點欣慰,這是本系列的最後一篇文章了。
在前二十九天中,我們從間接提示詞注入的底層成因切入,推導了防禦機制在執行期必須掌握的資訊維度,接著在 AgentDojo 上觀察實際的執行紀錄,並研讀了評測護欄模組的論文 TraceSafe。
到了第三十天,我們將這些推論落實為具體的程式碼模組——Toolguard(v0.2.0)。這是一個掛載於代理人(Agent)工具呼叫前夕的輕量防禦原型。
本文將聚焦於兩項重點:
三十天的探索過程,可以收斂為四個階段的推論推進:
| 探索階段 | 階段結論 | 引發的工程問題 |
|---|---|---|
| D1–D9機制限制與防禦位置 | 大型語言模型在架構上無法自底層區分指令與資料,防禦機制必須移至外部執行環境。 | 外部防禦模組需要什麼客觀資訊,才能判定工具呼叫是否合理? |
| D10–D17執行期所需資訊 | 單純檢查工具名稱與權限資格無法防止越權,必須追查參數值的原始來源,並比對任務是否有合法的參考路徑。 | 外部模組如何在不更動代理人內部架構的前提下取得來源資訊? |
| D18–D25AgentDojo 實測觀察 | 使用者正常要求與注入攻擊誘導發出的呼叫,其參數內容可以完全相同;此外,真實部署環境通常沒有預先定義好的標準流程。 | 當呼叫內容完全相同且缺乏參考流程時,防禦邏輯該依據什麼運作? |
| D26–D29TraceSafe 評測解讀 | 護欄的攔截成敗高度取決於結構資料解析能力;通用語言模型擔任審查角色時,容易因內容敏感而產生過度拒絕的偏見。 | 我們能否透過程式碼提供結構化事實,降低語言模型在安全審查時的過度誤擋? |
為了驗證「能否利用確定性程式碼查出來源事實,並以此約束與輔助語言模型決策」,我們實作了 toolguard。
toolguard 是一個以 Python 撰寫的輕量級來源感知守門模組,不依賴龐大的外部框架,核心運作邏輯如下:
messages)、待執行的工具呼叫(ToolCall),以及選填的任務標籤(task)。allow(放行)、block(攔截)或 confirm(轉人工確認),並附帶決策理由與結構化事實物件(Evidence)。block 時直接終止呼叫;判定為 confirm 時觸發確認回呼(Callback)。專案採用分工設計:確定性程式碼負責從歷史紀錄中抽取出客觀事實,語言模型則負責評估操作在情境下的實質危害程度。開源專案附在文末處。
toolguard 的核心功能不是憑空加入的,每一項都對應著先前推導中遭遇的具體限制:
_first_source)toolguard/rules.py 中,系統使用正規表示式從參數中抽取出電子郵件或網址,並逆向檢索先前的訊息序列:
if etype == EntityKind.EMAIL:
entities = extract_emails(mtext)
# ...
if v in entities:
if m.role == "user":
# 標記為使用者親自提供
elif m.role == "tool":
# 標記為外部工具回傳
若該參數值僅出現於外部工具的回傳內容中,系統即將其來源標記為 tool;若出現在使用者的輸入文字中,則標記為 user。_has_negation_near)。,、!?.!?)作為邊界截斷:
def _has_negation_near(text: str, entity_start: int, keywords: set[str], window: int = 60):
# 限制在前 60 字元視窗內搜尋否定詞,遇標點即停止,避免跨子句產生誤判
一旦偵測到目標處於否定修飾下,該實體會被標記為 user_forbidden,提供後續規則作為阻斷依據。hard_floor)rules.py 中設計了分層規則:
def hard_floor(ev: Evidence) -> bool:
# 條件 1:使用者明令禁止該目標 -> 直接阻斷
if any(r.source == "user_forbidden" for r in ev.recipients):
return True
# 條件 2:存在標準流程且工具不在流程內 -> 直接阻斷
if ev.in_plan is False:
return True
# 條件 3:無標準流程,但目標純粹來自工具回傳且為外部網域 -> 直接阻斷
if ev.in_plan is None and ev.external is True:
return True
return False
在缺乏流程依據但目標屬於內部網域的情況下(例如回覆內部同事郵件),系統不強制阻斷,而是轉入 confirm 狀態。first_sink_call)toolguard/trace.py 中,系統會主動檢查未列入清單的工具呼叫。只要參數名稱符合輸出特徵(如 url、endpoint、recipient),或參數值符合電子郵件與網址格式,即自動將其視為敏感輸出工具納入審查。toolguard/llm.py 中,使用 XML 標籤(如 <untrusted_history>、<evidence_facts>)將不同來源的資料明確隔開,並在系統指令中宣告外部紀錄僅供審查、不得作為操作指令執行。為了評估上述機制對審查模型的實際影響,我們使用 AgentDojo 的真實軌跡為基礎,建構了兩組用於對照的測試資料,並在伺服器上使用 Gemini 執行了 312 次審查呼叫:
兩組樣本在最終發出的工具名稱、參數內容上完全相同,差異僅在於該動作的提出來源。
在 C 組的 68 筆惡意樣本中,靜態規則、純 Gemini,以及加入來源證據的 Gemini,三者皆全數予以攔截(68/68)。這證明了在攻擊特徵明確的樣本上,三種方式都能達到阻斷效果。
真正的差異表現在檢驗合法請求的 B 組上:
| 審查方式 | B 組攔截數量(共 68 筆,理想為放行) | 攔截率 | 決策依據說明 |
|---|---|---|---|
| 純靜態規則 | 36/68 | 52.9% | 僅依據來源標記與流程比對判定;在面對無目的地的刪除操作時無法決定。 |
| 純 Gemini 審查(僅提供對話內容) | 46/68 | 67.6% | 模型僅依賴內容本身判斷;看見通訊涉及排程或敏感內容即傾向判定為危險。 |
| Gemini + 結構化來源證據 | 34/68 | 50.0% | 取得來源事實後,攔截數量減少 12 筆(攔截率降低 17.6 個百分點)。 |
將 B 組中模型在「單獨審查」與「加入證據」之下的決策逐筆交叉比對,變化分布如下:
| 純 Gemini 判定結果 \ 加入證據後判定結果 | 維持攔截 | 改判放行 | 合計 |
|---|---|---|---|
| 原本判定為攔截 | 34 筆 | 12 筆 | 46 筆 |
| 原本判定為放行 | 0 筆 | 22 筆 | 22 筆 |
| 合計 | 34 筆 | 34 筆 | 68 筆 |
從這份對比中可以觀察到兩點客觀事實:
confirm(轉人工確認)管道,避免系統在完全放行與完全阻斷之間陷入極端。客觀而言,toolguard 作為概念驗證的原型,其設計範圍存在明確的技術邊界:
回顧這一個月以來的推導與實作,我們從探討提示詞注入的底層機制出發,逐步推導防禦所需的資訊架構,並最終透過 Toolguard 將推論轉化為可執行的模組。
如果將這三十天的探索整理為核心結論,可以歸結為兩點:
這三十天的文章至此收尾,相關的架構邏輯與實測代碼已完整整理並開源,提供後續研究與開發者作為參考基礎。
感覺我會繼續加深加廣這個成品,把它變的更完善並對實際應用更有效。終於結束了,第一次參加鐵人賽,真的很怕哪天多發文章又哪天忘記發文。感謝大家一路走來的陪伴,之後我也還是會在這些領域裡想辦法變得更有實力,好好跟大家交流互動的。
Toolguard(v0.2.0)