昨天,我把 HTTP 回應標頭與 HTML 交給 Gemini,觀察它能不能根據資料整理安全分析。
它能辨識 Cookie、搜尋表單與輸入參數,但也出現一個問題:前面把 CSP、HSTS 描述成缺少的防護,後面卻又承認,資料只是節錄,無法判斷完整回應有沒有設定。
這讓我發現,後續如果要讓程式處理這些回答,光是要求它附上證據還不夠。我還需要知道每個判斷的類型,以及引用究竟來自哪份資料。
所以第 3 天,我想先做出這個流程:
為輸入資料標記來源
↓
要求模型依固定格式回答
↓
檢查 JSON、欄位與引用
↓
人工核對結論是否成立
如果模型今天使用「風險」,明天改成「潛在問題」,後天又把分析寫成一整段文章,人工閱讀可能沒問題,但程式就很難固定取出需要的資訊。
Google 的 Structured outputs 可以透過 JSON Schema 約束輸出結構,例如指定欄位、資料型別,以及分類允許使用的值。不過,官方也提醒,應用程式仍要驗證內容,處理符合格式但語意錯誤的結果。
對這個專案而言,我會分成三件事:
| 檢查層次 | 要確認的問題 |
|---|---|
| 格式 | 回答能不能解析?欄位與型別對不對? |
| 來源 | 引用是否真的出現在指定的證據中? |
| 語意 | 這段證據足以支持這個結論嗎? |
Structured outputs 主要協助第一層。後面兩層,仍然需要另外設計檢查方式(也有可能是工人智慧)。
今天沿用昨天的 HTTP 與 HTML 節錄,內容仍然是自行編寫的模擬資料,沒有實際部署,也沒有執行網站掃描。
我替兩份資料加上來源編號:
| 來源 | 內容 | 請求編號 | 是否完整 |
|---|---|---|---|
E1 |
HTTP 回應標頭節錄 | mock-request-01 |
false |
E2 |
HTML 節錄 | mock-request-01 |
false |
相同的 request_id 表示這兩份模擬資料屬於同一個請求;is_complete: false 則明確提醒模型,它拿到的不是完整內容。
HTTP 資料會包成這樣:
{
"source_id": "E1",
"kind": "http_response_headers_excerpt",
"request_id": "mock-request-01",
"is_complete": false,
"content": "HTTP/1.1 200 OK\nServer: nginx\nContent-Type: text/html\nX-Frame-Options: SAMEORIGIN\nSet-Cookie: session=abc123"
}
只有 evidence_sources 裡面的 content 能作為網站證據,後續會依 source_id 核對每段引用。
報告最外層包含 scope、findings 與 limitations。每個 finding 再使用以下欄位:
| 欄位 | 用途 |
|---|---|
id |
這項判斷的編號,方便定位錯誤 |
category |
區分事實觀察、潛在風險與未知事項 |
claim |
具體主張 |
evidence |
來源編號與逐字引用 |
conditions |
風險成立需要哪些條件 |
next_step |
下一步要補充或核對什麼 |
其中,category 只允許三個值:
observation
potential_risk
unknown
observation 表示直接觀察到的資訊,並不是「已確認漏洞」。例如,看到表單宣告 method="GET",能確認的是表單宣告的提交方式,還不能確認後端有 XSS。
potential_risk 必須列出成立條件。unknown 則提供一個明確的位置,讓模型能回答「現有資料無法判斷」,不必為了填滿報告而猜測。
這次使用的 Schema 如下,省略了不影響結構的欄位說明:
{
"type": "object",
"properties": {
"scope": { "type": "string" },
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"category": {
"type": "string",
"enum": ["observation", "potential_risk", "unknown"]
},
"claim": { "type": "string" },
"evidence": {
"type": "array",
"items": {
"type": "object",
"properties": {
"source_id": { "type": "string" },
"quote": { "type": "string" }
},
"required": ["source_id", "quote"]
}
},
"conditions": {
"type": "array",
"items": { "type": "string" }
},
"next_step": { "type": "string" }
},
"required": [
"id", "category", "claim", "evidence", "conditions", "next_step"
]
}
},
"limitations": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["scope", "findings", "limitations"]
}
這份 Schema 只定義基本結構。required 要求欄位存在,不代表字串一定有內容,也不代表風險一定寫了成立條件。這些規則會在本機再檢查。
今天,我先開啟 Google AI Studio 的新對話,在 Run settings 啟用 Structured outputs,再切換到 Code Editor,貼上 Schema 並儲存。
除了原本「未知不要當成不存在」的要求,我也在 System instructions 加上幾項規則:
只分析 evidence_sources 內的網站證據。
每段 evidence 都要指定 source_id。
quote 必須逐字引用該來源 content 中的連續片段。
節錄沒出現的標頭,不等於完整回應未設定。
潛在風險必須列出成立條件。
無法從資料支持判斷時,可以使用 unknown 與空 evidence。
本次實際設定如下:
| 項目 | 設定 |
|---|---|
| 日期 | 2026 年 9 月 17 日 |
| 模型 | Gemini 3.8 Flash(gemini-3.8-flash) |
| Thinking level | Medium |
| Structured outputs | 開啟 |
| Maximum output tokens | 65536 |
| Google Search、URL context、Code execution、Function calling 等工具 | 關閉 |
下面是我請AI協助編寫的單一判斷範例,不是 Gemini 的實測輸出:
{
"id": "F1",
"category": "observation",
"claim": "提供的這一行 Set-Cookie 沒有明確列出 Secure、HttpOnly、SameSite。",
"evidence": [
{
"source_id": "E1",
"quote": "Set-Cookie: session=abc123"
}
],
"conditions": [],
"next_step": "確認 Cookie 用途、完整設定與適用瀏覽器。"
}
這裡刻意把範圍寫成「提供的這一行」。這行資料支持對屬性文字的觀察,卻沒有提供 Cookie 被竊取、帳號遭入侵或 CSRF 已經成立的證據。
本機程式先用 json.loads() 解析完整報告,再用 jsonschema 檢查欄位與型別。確認結構正確後,才檢查來源引用。
引用檢查的核心邏輯如下,sources 是用來源編號對應到原始 content 的字典:
for finding in report["findings"]:
for ref in finding["evidence"]:
source_id = ref["source_id"]
quote = ref["quote"]
if source_id not in sources:
errors.append("來源不存在")
elif not quote.strip() or quote not in sources[source_id]:
errors.append("引用未出現在指定來源")
比對必須在指定來源裡進行。即使某段 HTML 出現在 E2,把它標成 E1 的 HTTP 標頭證據,仍然應該失敗。
另外也要拒絕空字串,因為在 Python 裡,空字串會被視為任何字串的子字串。
這版程式還檢查了發現編號不能重複、觀察與風險需要引用,以及潛在風險至少要有一個非空白的成立條件。檢查失敗時回報錯誤,保留原文,不自動幫模型補寫證據。
成功回答共有 5 項判斷:
| 編號 | 分類 | 回答內容 |
|---|---|---|
FIND-01 |
observation |
觀察 Cookie 設定行未列出安全屬性 |
FIND-02 |
potential_risk |
提出 Cookie 外洩風險,列出用途、XSS 與傳輸條件 |
FIND-03 |
observation |
辨識使用 GET、指向 /search 且帶有 q 的表單 |
FIND-04 |
unknown |
無法確認 CSP 設定 |
FIND-05 |
unknown |
無法確認 HSTS 設定 |
完整 JSON 通過格式、引用與已實作規則的檢查。引用片段同時存在於最初資料與重新送出的資料中;我對兩份來源分別核對,都能通過。這只能確認文字對得上,無法反推模型究竟使用了哪一輪資料。
接著逐項閱讀,FIND-03 的問題就出現了。它的主張是:
網頁包含一個使用 GET 方法將查詢參數 q 傳送至 /search 的搜尋表單。
但它只引用:
<form action="/search" method="GET">
這段支持 GET 與 /search,沒有包含 q。輸入資料確實另有名稱為 q 的欄位,但模型沒有把那段一起列入證據。
比較完整的引用,應再包含有 name="q" 的 <input> 片段;若要符合今天的逐字引用規則,還要保留所選來源實際的換行。另一個做法是縮小主張,只描述目前引用能支持的內容。
今天的成果,是把原本整段的安全分析,拆成可以逐項檢查的資料結構。
缺少欄位時,可以指出哪個欄位;來源不對時,可以定位到哪個發現;引用沒有出現時,也可以直接回到原始資料核對。
但格式與引用都正確之後,判斷仍然可能是錯的。這個限制已經在今天的人工測試中具體呈現。
今天只取得一份成功回答,而且同時調整了提示詞、輸入格式與輸出設定,重新送出的資料也有差異。因此,可以記錄這次哪些地方符合要求、哪些地方仍需修正,但不能把差異全部歸因於 Structured outputs,更不能據此估計模型的穩定性。
第 4 天預計朝 Gemini API 串接前進,讓輸入、原始回答與驗證結果都能被保存。
那就…
明天見!