昨天我先把 retrieval task 寫清楚:7 份文件、7 筆 query、9 筆 relevance judgment,每一筆 query 都有至少一份正相關文件。資料契約通過後,原本可以直接開始做 embedding。
但我把 documents.jsonl 逐筆重新看一次後,意識到 Day 02 檢查的只是「這份 retrieval task 有沒有斷掉」,還沒有回答:
Embedding 只會把收到的內容變成向量,不會知道某份規章已經過期,也不會主動刪除網頁頁首或合併重複內容。如果 corpus 本身有問題,換更大的 embedding model 通常只是更穩定地把問題寫進 index。
每份文件要有穩定、唯一的 doc_id。檔名不一定適合當 ID,因為搬動目錄或改名就會產生新身份。如果 ID 不穩定,後面的 qrels、版本記錄和刪除操作都會跟著漂移。
不只檢查原始檔案能不能打開,而是看送進 index 前的實際文字。空字串、只剩標題、重複頁首頁尾、亂碼與過短內容,都要在這一層被擋下來。
去重不能只做 byte hash。同一份內容多了空白、換行或 Unicode 形態差異,原始 hash 就會不同,但對 retrieval 來說可能仍然是同一份文件。
反過來,內容相似也不代表可以直接刪除。如果是新舊版本,應該先根據 effective date、version 與 status 選出生效版,而不是單純保留「最像」的一份。
本次公開 corpus 的每份文件至少有:
{
"domain": "synthetic-it-support",
"language": "zh-Hant",
"classification": "synthetic/public-safe"
}
真正的系統還會需要 source、version、effective time、owner 與 access scope。這些欄位要由 ingestion pipeline 明確產生,不能期待 embedding 或 LLM 之後從內容猜回來。
如果 corpus 持續改變,但實驗只記「使用最新資料」,後面的 BM25、Dense、Hybrid 與 Rerank 就沒有公平對照組。因此會對整份 JSONL 產生 snapshot hash,實驗只引用固定版本。
這一天還不需要 GPU,也不需要下載模型。labs/retrieval/audit_corpus.py 直接從 JSONL 檢查:
doc_id 是否唯一且非空。synthetic/public-safe。正規化處理只做 Unicode NFKC、case folding,並將連續空白壓成單一空白:
def normalize_text(value: str) -> str:
normalized = unicodedata.normalize("NFKC", value).casefold()
return re.sub(r"\s+", " ", normalized).strip()
沒有在這裡加入「語意相似就自動刪除」。語意 near-duplicate 可以當候選訊號,但是不是同一份文件、哪個版本生效,還是需要 domain rule 或人工複核。
執行:
python3 labs/retrieval/audit_corpus.py
這次結果是:
status: clean
document_count: 7
exact_duplicate_group_count: 0
normalized_duplicate_group_count: 0
issue_count: 0
snapshot_sha256: 3e0d4ad0945574f34f9d6ed976f89f8fbab19eac99bbcd368cb3178f74e541a7
對這個 clean 的解讀很保守,只代表現有 7 份文件通過了這六類結構性檢查,不代表文件內容必然正確,也不代表 query 一定找得到答案。
另一個值得保留的現象是:Day 02 的 task validator 與 Day 03 的 corpus audit 都可以通過,但回答的問題不同。前者檢查 documents、queries 與 qrels 有沒有斷掉;後者檢查送進 index 的文件是否有明顯污染。兩個 success 不能互相取代。
會保留 raw corpus,再產生新的 processed snapshot,並記錄每一種處理的數量:
raw documents
→ parse
→ normalize
→ reject empty / malformed
→ exact duplicate grouping
→ version selection
→ metadata validation
→ processed snapshot + manifest
希望後面能回答「哪些文件被排除、為什麼排除」,而不是只留一份被手動改好的最終檔。否則一旦清理規則寫錯,原始內容也一起消失,就無法複現或回滾。
這一步還決定了後面實驗的邊界:
doc_id。所以 corpus cleaning 規則也必須版本化。如果清理規則改了,就應該產生新 snapshot,而不是讓同一個 corpus 名稱在實驗過程中悄悄變化。
今天沒有執行 BM25、Embedding 或 Reranker。做的是把 corpus 從「一堆可以讀取的文件」,變成「有身份、有 metadata、能檢查重複,也能用 hash 固定的 snapshot」。
Embedding 能學習文字的語意關係,卻不會替我們決定文件身份、生效版本、存取邊界與品質規則。
明天會在固定 corpus snapshot 之後,開始設計 Query Set。到時我要避免的是另一種污染:只挑自己知道系統會答對的問題。
下一篇:Day 04|Query Set 怎麼設計,才不會只測自己想看到的答案
要對新 corpus 重跑 audit,或刻意加入重複、空內容與缺漏 metadata 驗證失敗路徑時,請使用 docs/day03-operations/03-ai-engineering-corpus-hygiene.md。只可使用 synthetic/public-safe 資料。
本篇只使用為公開實驗從零建立的 synthetic corpus,與我手上的Case無關。