修復中文路徑 preflight 與 BM25 核心詞落空,測資改照現實建

兩個 bug 同源於自檢測資「照能過建、不照現實建」:

- ingest preflight:git status --porcelain 預設會把含中文或空格的路徑加引號
  轉義,raw/ 白名單比對因此失效,中文檔名的 convert 產出被誤判為 raw/ 以外的
  未提交變更而中止攝入。舊測資用 ASCII 檔名,整條路徑從未被走過。

- search:BM25Okapi 的 idf 在 df >= N/2 時 <= 0(rank_bm25 對負 idf 的替代值
  epsilon x average_idf 在 average_idf 為負時同樣為負),與 search() 的 s > 0
  過濾相乘,會讓「每頁都提到的核心詞」查詢全數落空——知識庫愈小、詞愈核心
  愈嚴重,MCP 的 search_wiki 一併受害。改用 Lucene 式恆正 idf。
  舊測資每個查詢詞都只出現在一頁(df=1),恰好避開此配置。

變更:
- tools/ingest.py:preflight 比對前剝除 git 的轉義引號
- tools/search.py:_BM25 子類覆寫 _calc_idf 為 log(1 + (N-df+0.5)/(df+0.5))
- tools/selfcheck_ingest.py:新增 preflight 中文檔名放行 / 非 raw 擋下的檢查
- tools/selfcheck_search.py:新增與既有頁共用核心詞的測資,鎖住 idf 退化
- tools/convert/selfcheck.py、tools/selfcheck_lint.py:測資檔名改中文+空格
- AGENTS.md §13.4:新增「測資照現實建,不是照能過建」規則
- README.md §3.2:補 convert 產出應保持未提交、由 ingest 一併 commit 的流程

驗證:四個自檢全數通過;分別移除兩個修法後對應檢查會失敗。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
LittleYellow
2026-07-22 07:21:45 +08:00
parent cfd1876359
commit 0b4c2bbb15
8 changed files with 108 additions and 43 deletions

View File

@@ -44,6 +44,10 @@ def main():
["aml", "compliance"], "大額交易與分散交易樣態的監控測試要求。")
page(root, "wiki/summaries/long-doc.md", "長文件摘要", "截斷測試用",
["test"], "逾時。" * 500)
# 與 payment-gateway 共用核心詞「支付閘道」→ df=2, N=4正是 BM25Okapi
# 原式 idf 歸零的配置;沒有這頁就測不出核心詞落空的 bug
page(root, "wiki/summaries/gateway-report.md", "支付閘道版本沿革", "支付閘道改版紀錄",
["gateway"], "支付閘道於 2026 年改版,新增分期付款。")
hits = search.search(root, "支付閘道 逾時")
assert hits and hits[0]["path"] == "wiki/entities/payment-gateway.md", hits
@@ -53,6 +57,14 @@ def main():
assert search.search(root, "zzz-nonexistent-term") == []
print("PASS: BM25 檢索排序(繁中 bigram")
# 核心詞(出現在多頁)不得因 idf <= 0 被整批濾掉
core = search.search(root, "支付閘道")
paths = {h["path"] for h in core}
assert {"wiki/entities/payment-gateway.md",
"wiki/summaries/gateway-report.md"} <= paths, core
assert all(h["score"] > 0 for h in core), core
print("PASS: 核心詞跨頁共用仍可檢索idf 恆正)")
os.environ["PP_QA_ROOT"] = str(root)
import importlib
sys.path.insert(0, str(pathlib.Path(__file__).parents[1] / "mcp"))