跳到主要內容

【2026 最新】搜尋 Agent 過期文件測試怎麼做?六步建立可追查的故障注入實驗

最後更新: ·
搜尋 Agent 過期文件測試:舊文件、時間與引用追蹤示意

你的研究助理回答「新版 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;那等於把答案寫在考卷背面。

搜尋 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 文章總覽 補齊基礎。

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

週日 Weekly 週報+週二、週四 Alpha Signal,每週最多三封。

我們不會 spam,隨時可退訂。已訂閱?管理訂閱偏好(會寄登入連結到你的信箱)

主題篩選與退訂說明

寄送日期以台灣時間為準。主題偏好會篩選每期精選,並非每篇新文章的即時通知;當期符合主題的文章不足時會略過寄送。

未勾選任何主題會收到所有主題的精選,並不會停止寄信。

取消勾選 Alpha Signal 可停止週二、週四短報,仍保留週日週報。若要停止所有電子報,請使用信件底部的「取消訂閱」。