你的研究助理回答「新版 API 每次可取 11 筆」,還附上文件引用;你點開才發現,那是上一版的說明。搜尋 Agent 過期文件測試要抓的,正是這種看起來有憑有據、卻把舊規則搬到新問題的錯誤。Agent(能呼叫工具完成工作的 AI)找得到資料,和它選對證據,是兩道關卡。
這篇寫給第一次設計 AI 品質測試的讀者。前半用白話建立觀念,後半提供可複製的 Python 小實驗;執行段需要電腦已有 Python 3,並會建立文字檔。你會先製作十份虛構 API 文件,用搜尋替身控制錯誤比例,再把回答、引用與當時的文件串起來。起步程式不連網,也不呼叫付費模型。
本文受 DeepStress 研究啟發。論文最初於 2026 年 7 月 15 日提交,11 月 12 日修訂;這裡借用「控制證據品質來施壓」的思路;原設計是每次搜尋動態生成一份文件,這裡改成固定十份回傳與引用追蹤,另加過期版本情境,做自己的縮小實驗,並非重現論文完整基準。
先說結論:可靠回答=答案正確+證據適用+過程可追查
把搜尋 Agent 想成拿著一疊說明書的客服。你要測的,不只是客服算對了沒,還包括他翻的是哪一頁、那頁是否適用,以及資料不足時會不會停下來。先固定考卷,再一次只換一種壞文件,才能知道哪個環節需要修。
- 把「來源不可信、內容不相關、事實錯誤、相關但過期」分開測。
- 答案對錯、引用品質、棄答與執行錯誤分開記;總正確率只是一欄。
- 先用規則替身驗證測試管線,再接真實 Agent;日期過濾的效果要重新量。
一、搜尋 Agent 過期文件測試,和一般引用稽核差在哪?
引用稽核像事後查帳:這句話由哪份資料支持?故障注入則像消防演練:我們預先安排一種問題,再觀察系統怎麼反應。這裡的搜尋 stub,就是「會照劇本發文件的搜尋替身」,讓每次回傳內容受你控制。先理解執行層,可接著讀 Agent Harness 入門。
DeepStress 的研究設計把來源可信度、相關性與事實正確性分開控制;本篇把「版本過期」另列第四軸。這樣做是因為舊版文件可能非常相關、來源也可靠,卻回答不了新版問題。論文頁的實務討論也提出了這個需求,但留言是問題線索,不是過期測試的研究成績。
另一個有用觀念來自 ALCE 引用評估研究:答案正確性與引用品質要分開評估。用在自己的系統時,還得拆開「有引用 ID」「引用確實來自本次搜尋」「文件支持這句話」「文件適用指定版本」。四格中任一格出問題,都值得留下紀錄。
二、先鎖住五個零件,別讓待測 Agent 偷看答案
- ① 問題:例如「Fable-0 的 v2 每次可取幾筆?」API 是虛構的,避免真實產品知識混入。
- ② 真值:測試作者預先指定 v2 為 37、v1 為 11,保存在評分端。這些數字不是任何真實 API 規格。
- ③ 文件快照:每次回傳十份片段,保存文件 ID、API 名稱、版本、有效起訖日、來源標記及全文。
- ④ 回答介面:只把問題、目標版本、查詢日期和可見文件交給 Agent;要求輸出 answer/abstain、數值與引用 ID。
- ⑤ 評分與紀錄:評分器另讀真值,保存原始回答、文件快照雜湊、耗時與用量。真值、退化類型和壞文件位置都不傳給待測模型。
範例十份乾淨片段都支持同一個數值,是為了讓「替換幾份」很容易理解。它刻意放大證據重複,不能代表真實網站分布。熟悉流程後,要加入只有一份關鍵證據、多跳問題和缺少關鍵頁的題目。也別把整支含真值的測試程式貼給待測 Agent;那等於把答案寫在考卷背面。

三、一次只換一種:四條故障軸怎麼做
相關但過期:保留 API 名稱和問題主題,改成 v1、已結束的有效期與舊數值 11。問題仍問 v2,觀察 Agent 是否照抄舊規則。日期要區分發布日與生效日;剛重新發布的舊版指南,也可能不適用。
不相關:把文件改成另一個服務的外觀設定,移除可回答問題的數值。事實錯誤:保留 v2 和有效期,卻把數值改為 999。前者測能不能辨識沒有證據,後者測會不會被貌似相關的錯誤內容帶走。
來源不可信:只把來源標記換成匿名鏡像,數值仍為 37。這條軸故意不改事實,因此正確率可能完全不動;你還要記錄匿名來源採納率,以及 Agent 是否依任務規則要求可信來源。來源可靠與內容正確,是不同變數。
每條軸依序替換 0、2、5、8、10 份,也就是 0%、20%、50%、80%、100%。比例的分母是本次回傳的十份文件,不是全資料庫。排名先固定;若要研究壞文件排第一的效應,另開一組只改順序的測試。別把比例、排名與文章長度同時改掉,再把差異全部歸給過期。
四、複製這個 Python 搜尋替身,先驗收測試管線
在空資料夾建立 stress_demo.py,貼入下面程式,再執行 python3 stress_demo.py。它使用 Python 標準函式庫;json 官方文件說明了序列化方式。輸出採 JSONL:每行一筆獨立 JSON,方便逐案搜尋,不把整份檔案當成單一 JSON 解析。
兩個待測者都是規則替身,並非語言模型:first 只讀第一份,讀不到 limit 就棄答,version_date 先篩目標版本和有效期。這裡用它們確認紀錄與評分能抓到已知錯誤,不用它們推論真實 Agent 的可靠性。NOW 是固定測試日期,重跑時保留原值;要測另一日期,建立新的案例版本。
import copy, hashlib, json, random, time
from pathlib import Path
NOW = '2026-11-15'
TARGET = 'v2'
AXES = ('stale', 'irrelevant', 'false', 'untrusted')
def search_stub(qid, axis, k, seed):
# Ten synthetic snippets per query; every clean snippet supports one fact.
docs = []
bad_slots = set(random.Random(seed).sample(range(10), k))
for i in range(10):
d = dict(id=f'q{qid}-doc{i}', api=f'Fable-{qid}',
version='v2', valid_from='2026-10-01',
valid_to='2099-12-31', issuer='fixture-manual',
text=f'limit={37 + qid}')
if i in bad_slots:
if axis == 'stale':
d.update(version='v1', valid_from='2025-10-01',
valid_to='2026-09-30', text='limit=11')
elif axis == 'irrelevant':
d.update(api='Other-Service', text='theme=dark')
elif axis == 'false':
d['text'] = 'limit=999'
elif axis == 'untrusted':
d['issuer'] = 'anonymous-mirror'
d['id'] = 'D' + hashlib.sha256(
json.dumps(d, sort_keys=True).encode()).hexdigest()[:16]
docs.append(d)
return docs
def agent(docs, policy):
# Rule-based stand-in, NOT an LLM. No gold/axis/k/seed is passed here.
candidates = docs
if policy == 'version_date':
candidates = [d for d in docs if d['version'] == TARGET
and d['valid_from'] <= NOW <= d['valid_to']]
if not candidates:
return dict(status='abstain', value=None, citations=[])
d = candidates[0]
value = int(d['text'].split('=')[1]) if d['text'].startswith('limit=') else None
if value is None:
return dict(status='abstain', value=None, citations=[])
return dict(status='answer', value=value, citations=[d['id']])
def grade(answer, docs, gold):
if answer['status'] == 'error':
return 'error'
if answer['status'] == 'abstain':
return 'abstain'
if answer['value'] == gold:
return 'correct'
# Presence/identity only; semantic citation support needs a separate check.
returned_ids = {d['id'] for d in docs}
cited = any(c in returned_ids for c in answer['citations'])
return 'wrong_cited' if cited else 'wrong_uncited'
def run(out):
rows = []
for qid in range(10):
for axis in AXES:
for k in (0, 2, 5, 8, 10):
for seed in (7, 19, 31):
docs = search_stub(qid, axis, k, seed)
packed = json.dumps(docs, sort_keys=True).encode()
for policy in ('first', 'version_date'):
t0 = time.perf_counter()
try:
a = agent(copy.deepcopy(docs), policy)
except Exception as e:
a = dict(status='error', value=None, citations=[],
error_type=type(e).__name__)
elapsed = (time.perf_counter() - t0) * 1000
rows.append(dict(qid=qid, axis=axis, k=k, seed=seed,
policy=policy, query=f'Fable-{qid} v2 limit?',
snapshot_sha256=hashlib.sha256(packed).hexdigest(),
documents=docs, answer=a,
outcome=grade(a, docs, 37+qid), latency_ms=elapsed,
calls=0, input_tokens=None, output_tokens=None,
cost_usd=None))
out.write_text(''.join(json.dumps(r, ensure_ascii=False)+'\n' for r in rows))
print(f'{len(rows)} rows -> {out}')
if __name__ == '__main__':
run(Path('trace.jsonl'))
這段會留下 1,200 筆紀錄:10 題 × 4 軸 × 5 個比例 × 3 個種子 × 2 種規則。種子控制壞文件位置;相同題目與種子重用同一組文件,讓兩種規則能成對比較。這不是 1,200 個獨立世界,也不是模型排名。calls=0 代表未呼叫模型;token 與金額留 null,沒有量到就不填數字。
AlphaLab 在本次執行跑過上述程式,保存程式、完整 trace.jsonl 和檢查紀錄。選出 qid=0、axis=stale、k=10、seed=7:第一份規則回 11,引用該快照第一份文件的雜湊 ID,被列為 wrong_cited;版本日期規則則棄答。這個結果由程式規則決定,只證明管線能記下這一種失敗。
驗收時至少檢查:每次十份文件且 ID 不重複;相同輸入得到相同文件快照;全過期案例會被日期規則擋下;不存在的引用 ID 不算「引用了回傳文件」;執行例外記為 error。原生測試可用 Python unittest包住這些斷言,再把它接進每次改版都跑的回歸檢查。
五、搜尋 Agent 過期文件測試怎麼計分,才不會獎勵亂答?
先訂互斥的主要結果:correct、wrong_cited、wrong_uncited、abstain、error,所有嘗試都進分母。這是本篇自訂的工程分類,不是 DeepStress 原論文的五分類,也沒有計算論文的 RAS 或 CoPRAS。
wrong_cited 在程式中的精確意思,是「答案錯,而且至少一個引用 ID 出現在回傳文件內」;它不會自動判定文件是否真的支持該句話。答案答對但引錯頁仍可能先進 correct,所以還要另加引用支持與版本適用欄,逐一核對「主張 → 引用 ID → 文件句子」。這個最小評分器刻意只做數值與 ID 檢查。
- 整體正確率:correct ÷ 全部嘗試;同時報 error,避免系統故障被藏掉。
- 錯答附引用率:wrong_cited ÷ 全部嘗試;另報 wrong_cited ÷ 全部錯答,並寫清楚分母。
- 棄答品質:先由私有測試規則判定是否有足夠且適用的證據,再分「該停而停」與「有答案卻停」。全過期且限定根據 v2 文件回答的題目,應接受棄答。
- 回答覆蓋率與條件正確率:實際回答的比例,以及只在回答案例中的正確率,一起看才知道是不是靠少答提高分數。
- 成本:接真 Agent 後,保存實際模型呼叫次數、供應商回傳用量、延遲與當時費率;缺用量時標示缺值,不用字數猜帳單;工具、重試與評分呼叫的費用也要分項記錄。
看見錯答引用了壞文件,代表你找得到可疑路徑,還不能單靠這件事證明因果。要多做一次成對測試:保留問題、其他文件與排序,只替換那份可疑內容,再觀察回答是否改變。多步搜尋還要保存每輪 query 和每次工具回傳,不能只留最後一輪。
六、接回自己的研究 Agent:先做小批次,再擴大
第一步,保留真實模型、提示、工具格式與預算,把原搜尋工具的回傳改由 stub 提供;若原本有摘要、截斷或重新排序,也保存轉換前後內容。從 自己建立 Harness 的工具接點開始,通常比重寫整個系統清楚。
第二步,把範例的 agent() 換成你的呼叫程式。只傳可見文件及問題,固定輸出格式,驗證數值型別、狀態與引用 ID;不合法輸出另記為執行/格式錯誤。設定單次逾時、最大搜尋輪數與整批費用上限,先跑少量個案檢查接線。範例沒有外部 SDK、網路重試及付費用量擷取,這些要在你的呼叫層補齊。
第三步,先跑原流程,再加入版本/有效期檢查。比較同一批題、同一份文件和同一預算,每案使用新會話、清除前案歷史與快取干擾;隨機模型還要多次重跑,報每格樣本數與變動範圍。日期規則能排除範例中的 v1,但擋不住「日期新、內容卻錯」的 999,也不會辨認匿名來源。真實來源的日期與版本標籤還可能缺漏或造假,不能把自報日期當成可信保證。
第四步,把原始輸出寫成可回放案例,保留模型識別、提示雜湊、工具設定、文件 SHA-256、順序、原始回答與評分版本;每份文件的 ID 由內容產生,跨快照仍連同快照雜湊辨識。雜湊像文件指紋,能檢查快照是否改過,並不證明內容真實。先用虛構資料建立保存流程,再決定真實客戶資料哪些欄位可留、保存多久。
最後加入保留題組:你調整提示時看過的題目,和最後驗收的新題分開。單題應有成本與品質門檻,整批也要檢查是否為了多棄答而犧牲正常問題。需要拆出擷取問題,可讀 RAG 網頁擷取品質檢查;需要找證據有沒有進到閱讀器,則看 RAW-RAG 證據覆蓋診斷。
七、常見問題:先把八個容易誤讀的地方講清楚
有引用,就代表回答有根據嗎?
不一定。先確認引用出現在本次搜尋,再檢查它是否支持這句話、版本是否適用;連結能開啟只是起點。
舊文件一定應該丟掉嗎?
不一定。問題若問歷史版本,舊文件可能正是正確來源。測試必須同時標明目標版本、查詢日期與任務要回答的時間範圍。
文件最新,就代表事實正確嗎?
不代表。時間是其中一個條件,錯誤內容也能帶新日期,所以要另外測事實錯誤軸。
全是錯文件卻答對,算通過嗎?
只在答案正確欄通過。若任務要求依據文件,還要檢查引用支持;也可加空搜尋對照,觀察模型是否靠既有知識猜中。
為什麼不用真實產品數字?
先用虛構數字比較容易知道真值,也降低既有知識干擾;之後仍要用經授權、去識別化的實際案例補測。
這個程式能直接量模型成本嗎?
不能直接量。它沒有呼叫模型;接上你的供應商用量回傳與費率快照後,才能建立金額欄。
把棄答率提高就比較安全嗎?
不一定。正常題也答不出來會降低可用性。把可回答與不可回答案例分開,對照回答覆蓋率和錯答率。
這是官方 DeepStress 一鍵重跑嗎?
不是。這是自行編寫的縮小管線;壞文件比例、過期軸、規則替身與結果分類都依本篇教學定義,不能拿成績和原論文系統互比。
給新手的三個重點
- 先讓評分器抓到你故意安排的錯誤,再評別人的模型。
- 先追到哪一份文件、哪一輪搜尋,再用對照測試檢查原因。
- 版本檢查、來源判斷、事實核對各有工作,分軸觀察才能選對修法。
接著閱讀
左右滑動查看更多推薦
下一步:先留下一筆「錯得可以追查」的紀錄
今天先跑 python3 stress_demo.py,找出全過期的那一筆,沿著引用 ID 回到原文件,再把其中一個規則替身換成你的 Agent。記住:可靠回答=答案正確+證據適用+過程可追查。把這條鏈接好,你才有依據決定下一步要修搜尋、版本檢查,還是回答規則。想把流程延伸成可交付的應用,可從 AlphaLab 課程 選擇下一段學習路徑,也能回到 AI 文章總覽 補齊基礎。






