Skip to content

Latest commit

 

History

History
542 lines (431 loc) · 47.8 KB

devlog.md

File metadata and controls

542 lines (431 loc) · 47.8 KB

SelectRAG 開發歷程紀錄

English | 繁體中文

素材用途:個人求職網頁的專案敘事。失敗與繞路是最有說服力的部分,保留原樣。

2026-08-31 — 第一組真實評測數字:BM25 baseline 對 25 題真 eval set

起因:語料 pipeline(8,029 chunks)與 BM25 檢索器完工後,缺的是「真題目 + 人工判定真值」。 沒有它,所有檢索數字都只是合成資料的煙霧。目標:建立 100 題 eval set 的第 1 批 25 題並凍結。

嘗試過程:

  • 先設計「AI 出題 + BM25 輔助窮舉等價 chunk + 人工逐列判定」流程(方案 A), 否決了純人工出題(成本 5 倍)與 LLM 自動判真值(真值污染無法事後補救)。
  • 產出 255 列標註對照表(25 題 × 每題約 10 個候選),人工逐列判 ✓/✗。
  • 判定過程本身抓到三個工具鏈陷阱:①預覽只給 160 字,答案在第 219 字的來源 chunk 被誤判 ✗; ②有個 chunk 的答案(收件截止日)只存在標題路徑、不在正文,而索引只收正文; ③同一診療代碼在不同生效期間有兩個點數,題目不指定期間就有兩個互斥正解——改題收斂。

關鍵考量與抉擇:真值集不是「一題一 chunk」——同一資訊常見於多份文件 (藥品仿單同成分他牌、給付方案與支付標準互相重載)。漏標等價 chunk, 之後的 dense 檢索器找到它會被冤枉判錯,直接扭曲 A 軸對照結論。 所以寧可讓人工多掃 200 多列 ✗,也要窮舉 ✓。

最終結果:第 1 批凍結(hash aa91a0b26af1),BM25 baseline: recall@1=0.66、recall@3=0.82、recall@10=0.86、MRR=0.786。 四個漏檢全數歸因成功,且各代表一種結構性弱點:

  • q04 同義文件排擠(他牌同成分 chunk 擠不進 top-10)
  • q07/q24 標題資訊流失(答案在 heading_path,索引只收正文)——同根因出現兩次
  • q11 中英詞彙鴻溝(「雌激素經皮貼片」vs「Estradiol Transdermal Patch」)
  • q16 詞面重疊不足(「如何處理」vs「簽名黏貼於病歷表」)

這組弱點清單就是後續 dense/hybrid 對照實驗「預期增益從哪來」的假設來源—— baseline 的弱點即實驗的賣點。遺留問題:heading_path 要不要進索引(待對照實驗)、 BM25 輔助建真值的採樣偏差(第 2 批改用關鍵詞改寫多輪查詢緩解)。

2026-09-01 — 假設被證據推翻的一天:heading_path 救不了漏檢,doc_title 才行

起因:50 題 eval set 上 BM25 漏檢歸因顯示「答案的關鍵詞不在 chunk 正文」是主導失效 (藥品仿單的藥名、章節語意都不在切塊後的正文裡)。原計畫:把 heading_path 加進索引。

嘗試過程:動工前先查證——結果推翻命題:漏檢題缺的「藥名」根本不在 heading_path, 也不在 doc_id(那是許可證字號),只在每份文件的第一行。若不查證就實作, 會做出一個對照不出差異的無效實驗組。方向改為:Chunk schema 新增 doc_title (建置期從文件首行提取),BM25 加 context_fields 選項可掛 doc_title 與 heading_path。 同場加映 hit@k 指標:真值集含多個等價 chunk 時 recall 被稀釋 (找到 5 個中的 1 個只得 0.2),hit@k 回答「答案到底有沒有拿到」。

關鍵考量與抉擇:①控制組不可污染——context_fields 預設為空,改動後控制組 50 題數字 與改動前 bit-for-bit 相同才算過關;②變異測試對 property 子集單獨重跑,揭露 hit 的三條 不變量全是單邊的(≥recall、單調、值域),擋不住「永遠回報命中」的實作—— 補上雙邊鎖定 hit == (recall > 0) 才殺死;③被否決方案:用 chunk 序號 #0000 推導標題 (零 schema 變更但依賴隱性排序耦合)。

最終結果(見 screenshots/20260901_bm25_ctx對照_gauntlet全過.png): 50 題對照,recall@10 0.844 → 0.916、hit@10 0.88 → 0.92;三個目標漏檢題翻正兩個 (q03 recall 0.2→0.8、q04 0→1)外加一題意外收穫,q06 仍 miss(誠實保留)。 代價如 pre-mortem 預測:recall@3 微降(0.776→0.773)——文件脈絡讓深層召回變好、 早段精度微損,這正是 A 軸備忘錄的第一個 trade-off。 gauntlet:208 tests、變異 45/45、supply chain 歸零、全層通過。

2026-09-01 — 100 題 eval set 全數凍結:A 軸實驗地基完工

起因:立案時定下的「100 題、人工判定真值、分批凍結」目標,四批完成。

過程數字:四批共產出 1,151 列候選標註、人工判定全數完成;建議欄錯誤率 1/255、1/298、3/301、13/297——第四批的跳高拆開看是 10 列來自兩道壞題 (同一條規定在語料重複出現 5~7 個 chunk,單一正解撐不住),真正的判斷差異只有 3 列。

流程演進(每批都比前一批準):第 1 批單輪查詢 → 第 2 批三輪改寫查詢擴大等價候選網 → 第 3 批摘錄放全文(消滅「推定 ✓」)→ 第 4 批「檢閱優先序」欄(必看/抽查)減半人工量。 兩道壞題的教訓沉澱為出題規則:出題前先掃「答案句在語料出現幾次」,超過兩次改問唯一值。

最終數字(100 題,v1-full100,hash bdea697a): BM25 純正文 recall@10=0.829 / hit@10=0.860;+doc_title+heading_path 0.905 / 0.920。 副產品:31 筆人工標記 hard negatives(整理後)(矛盾型、文字全對藥品錯、數字對實體錯), 是之後 faithfulness 評測的現成考題——「文字全對、來源錯」的陷阱連人都要靠 chunk_id 才判得準,正是 RAG citation 紀律要防的事。

2026-09-01 — 教科書預設值不是預設值:BM25 k1/b 網格調參

起因:100 題 eval set 完工後,dense 對照前需要「調參後」的 BM25 才公平—— 教科書預設 k1=1.5 / b=0.75 從未在本語料驗證過。

嘗試過程:粗網格 6×5(k1 0.5–2.2 × b 0–1)發現純正文版最佳點落在 b=0 邊界, 依「取高原中點、不取邊界」原則細化第二輪(k1 0.2–0.7 × b 0–0.3),確認高原橫跨 b 0–0.1、k1 0.4–0.6。 ctx 版(+文件脈絡)的高原反而寬(k1 0.3–1.1 × b 0.5–0.75),教科書預設本來就在上面—— 解釋了它「看起來不用調」。runner 補 k1/b 參數透傳時,第一版測試是空轉的(斷言無鑑別力), 被自己抓出重寫成「b=0 時排序翻轉」才算數。

關鍵考量與抉擇:①用 recall@10 選高原、MRR 決勝負;②調參與評測共用同一份 100 題、 無 held-out——選擇如實揭露為「上界估計」而非另切驗證集(100 題再切會讓兩邊都不夠用); ③調參值不改程式預設,以獨立設定檔承載,控制組定義不受污染。

最終結果:純正文 k1=0.5/b=0.1 → recall@10 0.829→0.870;+脈絡 k1=0.7/b=0.5 → MRR 0.732→0.808。 根因是 b:chunk 長度已被切塊參數約束,再做長度懲罰反而傷害含完整條文的長 chunk。 遺留:dense 進場時兩邊都要「調參後 vs 調參後」才公平。

2026-09-08 — 量化格式的效果拆不開引擎:C 軸實驗設計的取捨

起因:盤點 NVIDIA 技術棧時發現 C 軸(AWQ / GPTQ / FP8)有個結構性問題—— 每個量化數字都是「格式 × 引擎 kernel × 引擎排程」的乘積;FP8 的官方主路是 ModelOpt → TensorRT-LLM,vLLM 對 Ada FP8 的支援未確認,兩引擎連「同一份權重」都不成立 (checkpoint 需各自轉換)。引擎不固定,C 軸會偷到 B 軸的結論。

三個方案:①單引擎三格式(鎖 TRT-LLM,因果最乾淨,但結論只對 TRT-LLM 成立); ②雙引擎 × 三格式完整矩陣,跑不起來的格子誠實標 N/A(最完整,「格式與引擎綁定」本身 就是 SA 該給客戶的知識,但時間 ×2 且仍不能斜著比);③最無聊但最穩:AWQ/GPTQ 交集做 2×2, FP8 只在支援的引擎上做單點附註(保證做得完,但把最有 NVIDIA 味的那格降級)。

關鍵考量與抉擇:拍板「②為目標、③為底線」——先跑完 2×2 交集當保底交付,再補 FP8 與 空格;報告鐵律:只在同引擎內比格式、同格式內比引擎,禁止跨引擎排名。 動工前寫下 pre-mortem:六個月後最可能的挑戰是「FP8 勝出恰好等於 B 軸量到的引擎差距」, 防線就是每張表標引擎、不做跨引擎排名。另一個常被忽略的維度:量化影響的不只速度還有 生成品質,所以 §5.3 的 faithfulness judge 成了 C 軸前置——否則只會得出「越小越快」的廢話。

同場拍板:A 軸 embedding 兩者都測(bge-m3 省 VRAM、NV-Embed-v2 有 NV 訊號但 7B 級, 預設離線建索引);NIM / NeMo Retriever 進實驗但設兩小時時間盒,消費卡不支援即改開源 reranker。 遺留:全部待 4090 桌機首日驗證(七步順序已寫進 requirements-gpu.txt)。

2026-09-13 — 第一次跨機重現:雲端 CPU 開發的一切在 4090 桌機一行指令全部對上

起因:A 軸不需 GPU 的部分(語料、eval set、BM25 baseline、gauntlet)全在 Claude Code 雲端容器完成,桌機一行程式都沒跑過。今天要證明「不需 GPU 的結論可以原封不動搬到有 GPU 的機器」, 否則之後 dense / hybrid 的數字沒有可信的對照組。

嘗試過程:①git clone 只拿到程式與 SOURCES.md——語料檔本來就在 gitignore,只活在拋棄式容器裡, 一度以為「clone 下來就該有」。②改走 SOURCES.md 重抓被否決:chunk_id 是 doc_id+序號+內文的 sha256, 健保署 HTML 已更新,重抓的位元組對不上就會讓 100 題真值集全部失效。③最後由雲端把 42 份原始檔打包 (79 MB,附件單檔上限 30 MiB,切三段),桌機合併後 sha256 比對再解壓。 ④環境層踩了三個小坑:wsl 預設進到 docker-desktop 而非 Ubuntu、Ubuntu 24.04 沒有 python3.11 (deadsnakes PPA)、gauntlet 用 | tail 接尾造成 10 分鐘完全無輸出而誤按 Ctrl+C。

關鍵考量與抉擇:桌機已有另一套 conda 環境,選擇「單一 WSL 發行版、每專案獨立 venv」 而非再開一個發行版——conda deactivate 後用 python3.11 建 .venv,套件版本由 requirements 釘死。 語料備份問題浮上檯面:gitignore 保護 repo 乾淨,代價是語料沒有版本控制的副本,只能靠人工另存。

最終結果:8029 chunks(csv 6350 / html 19 / pdf 1660)、123/123 真值存在、四組 BM25 recall@10 0.8287 / 0.8703 / 0.9047 / 0.9047 與雲端逐位相同,gauntlet 全層通過(218 tests、 99% 覆蓋、47/47 變異、pip-audit 歸零)。跨機重現的可信度來自三件事:套件版本全部釘選、 語料以 hash 驗證、基準是一行指令。下一步:GPU 套件首日驗證與 requirements-gpu.txt 釘選。

2026-09-14 — A 軸一天三個 ablation:鄰塊擴展被否決、hybrid 補漏檢、reranker 補排序

起因:dense(bge-m3)首組數字 recall@10 0.911 只比調參後的 BM25 多 0.006,而失敗模式抽驗分出四類 (藥名盲、相鄰切塊、同語意場錯文件、整題崩壞)。A 軸的問題從「向量庫值不值得」變成「哪個對策吃掉哪類失敗」。 拍板順序:先 chunking、後 hybrid + rerank,每步都對同一份 100 題(v1.1,反向抽驗後真值 123→127)跑基準。

嘗試過程:①檢索期鄰塊擴展(把命中 chunk 的前後 12 塊一起帶回)——BM25 與 dense 全面退步, recall@3 掉 0.100.13:k 預算鎖死時,7 筆相鄰漏檢的收益抵不過原命中被擠出榜,當天否決。 ②hybrid 先算「兩邊 top-10 聯集 recall 0.975」再做網格——聯集比任一單邊高 0.07,證明兩個檢索器漏的題幾乎不重疊, 這才是融合的理論空間;RRF 網格 rrf_k 三格是高原、pool 是主變因且小 pool 反而好,代表點 pool=12 得 recall@10 0.963。 ③reranker(bge-reranker-v2-m3)重排 hybrid 前 N 筆:N ∈ {10,20,30} 呈山形,20 為峰;recall@3 0.855 → 0.943、 MRR 0.837 → 0.921,recall@10 只 +0.012——hybrid 負責撈進前 20,reranker 負責推到前 3,兩者吃的是不同指標。 42 筆 hard negatives 檢核真值勝 93%。

關鍵考量與抉擇:每個 ablation 動工前都先寫下可被推翻的假設與 pre-mortem——hybrid 的 pre-mortem 要求 「增益必須超過單邊放大池」,結果 hybrid@10(0.963)高於單邊 recall@20(0.958),排除「只是看更多」。 reranker 的取捨是延遲換召回:candidates 20 比 10 每題多 ~116 ms 換 recall@10 +0.012,A 軸主指標是 recall,延遲留給效能軸。 另一個要分開解讀的訊號:doc_title 在索引端提升 recall,在重排端只提升 MRR,同一份欄位在兩個階段是不同機制。

最終結果:A 軸主線數字定案 BM25 0.905 → dense 0.911 → hybrid 0.963 → reranker 0.975(recall@10), 頭部 recall@3 0.855 → 0.943。同一份題組、無 held-out,全部標為上界估計。 遺留:NIM reranker 兩小時時間盒、A 軸 trade-off 結論、反向抽驗第 2~4 批、「真值 chunk 在語料中有幾塊高度相似」掃描工具。

2026-09-15 — B 軸一天跑完:引擎不是變因,數值格式才是

起因:立案時 B 軸(vLLM vs TensorRT-LLM)被當成 NVIDIA 敘事的主軸,預期 TRT-LLM 在 4090 上會明顯快。 前置花了整天:NIM reranker 兩個時間盒(第一個判錯根因、第二個發現是 Docker Desktop 太舊)、NV-Embed-v2 因 remote code 與 transformers 4.57 兩處不相容放棄、ModelOpt FP8 轉換一次過(校正用語料 512 筆,46 秒)。

嘗試過程:①量測工具先在雲端用假 OpenAI 串流伺服器把 SSE 解析、TTFT、百分位、一致性比對走完 gauntlet (80 個變異全殺,property 子集揭露三個核心語意盲點後補不變量);②桌機首輪 vLLM c=1 三題各停 5 分鐘—— log 指向 Triton JIT 在推論中編譯 kernel,5 題暖機沒蓋到形狀,該輪作廢改 10 題;③vLLM 預設套用模型的 generation_config(repetition_penalty 1.05),與 TRT-LLM 條件不同,加 --generation-config vllm 重跑; ④bf16 兩引擎三併發等級差 ≤ 10%;⑤同一份 FP8 checkpoint 餵兩引擎都能跑,加速 ×1.4–1.7。

關鍵考量與抉擇:最容易犯的錯是把「吞吐」當成引擎的功勞——pre-mortem 預先要求每題記輸出 token 數與 一致性比對,結果證明兩引擎工作量相同(長度差 p50 0),數字可比。另一個是 FP8 的品質:同引擎 bf16→FP8 的 greedy 輸出只有一半逐字相同,速度表若單獨出現會誤導,所以結論明寫「C 軸品質判準是 FP8 結論的前置」。

最終結果:4090 短回答 RAG 負載下,「先做 FP8、再談引擎」;最穩是 vLLM bf16(424 tok/s @ c=8), 最快是 TRT-LLM FP8(645),但對 vLLM FP8 只多 6%,不值得多養一個 venv。立案時列的「vLLM 對 Ada FP8 待驗證」 有了答案。遺留:長輸出負載一輪、C 軸 faithfulness judge、NV-Embed-v2 的第三 venv 路線。

2026-09-17 — C 軸:FP8 被 4-bit 超車,然後 judge 被人工複核打臉

起因:B 軸把 FP8(×1.4)當成 Ada 上的終點,C 軸要回答 AWQ / GPTQ 值不值得多養一種格式。 原本想用 ModelOpt 對同一份語料自行校正三格式(嚴格受控),評估後改用 Qwen 官方預量化 checkpoint: 少兩次轉換,代價是校正資料與 FP8 不同源,報告每張表標明。TRT-LLM 載 HF 格式 AWQ 直接失敗 (PyTorch 後端只認 weight,AutoAWQ 是 qweight/qzeros/scales),矩陣從 2×4 縮成 vLLM 單引擎四格式—— 這是立案假設「格式綁引擎」的第一個實證。

嘗試過程:①先驗證四格式 tokenizer 同源(100 題逐題 token 數一致),排除 prompt 端混淆;②每格式跑短回答 三併發、長輸出 c=8、faithfulness judge、對 bf16 逐字一致率;③W4A16 兩格式 c=8 吞吐 1000 tok/s,對 bf16 ×2.36、 對 FP8 ×1.6,AWQ 與 GPTQ 差 ≤ 5%(同一個 Marlin kernel,差別只在校正演算法);④品質:GPTQ 0.960 與 bf16 0.959 打平, AWQ 0.938 唯一低於 0.95;⑤把 AWQ 的 8 條 contradicted 攤成 xlsx 對照 bf16 同題人工複核—— 真錯 2、retrieval 共犯 1、judge 誤判 5;而 bf16 的 5 條 contradicted 全是同型誤判或共犯。

關鍵考量與抉擇:第一個反直覺是「輸出改變 ≠ 品質下降」第二次出現:量化愈深逐字一致率愈低(FP8 0.51 → W4 0.25), judge 卻看不到差異。第二個是 judge v2 校準 κ 0.845 沒抓到 false-contradicted 5/8——校準集裡逐字命中句太少, κ 只證明「校準集上一致」。誤判分三型,對策各不同:逐字命中型不該靠 prompt 而該用確定性前置層擋掉; 斷頭 chunk 是切塊的病,歸 A 軸 parent-child;棄答判定要先問對象歸屬再問有無。粒度在「整句語意」與 「原子拆解(RAGAS 式)」之間選前者:本 judge 是拿來對照格式與引擎,句級噪音對各格式對稱,原子拆解會讓主張膨脹、 多一個要校準的切分步驟、且與 v1/v2 數字不可比。

最終結果:推薦 vLLM GPTQ-Int4(×2.36、品質打平、權重 5.3 GiB、KV cache 容量 2.5 倍);FP8 降為「需跨引擎 又不想自行做 W4 校正」的中間選項;AWQ 真實差距 2 條在噪音內,結論不變。另記兩個量化副作用:中英夾雜的成分行 AWQ 錯誤棄答(bf16 答對)、一題輸出漂成簡體字。遺留:judge 逐字命中前置層、prompt v3 棄答規則、語言漂移獨立計、 TRT-LLM ModelOpt-AWQ 路線、KV cache 量化第二輪。

❌ 失敗紀錄

  • judge v2 的 κ 0.845 被當成「可信」的證據,實際 contradicted 類 false rate 5/8;κ 只對校準集的分布負責, 校準集沒覆蓋的句型(逐字命中、棄答)照樣錯。教訓:校準集要按「判定難度型」分層抽,不能只隨機抽。
  • 第一版 prompt_tokens 驗證指令抓錯欄位(bench 沒記 prompt_tokens),改成用各 checkpoint tokenizer 離線逐題計數才是對的驗證物。

2026-09-17 — 逐字命中前置層:先量再做,量完發現收益比想像小,仍做成零誤放的安全層

起因:AWQ 複核揭露 judge 8 條 contradicted 有 5 條誤判,其中兩條是主張逐字出自語料原句。 直覺解法是「主張是 context 子字串就直接判 supported,不送 LLM」,預期能吃掉一大半誤判。

嘗試過程:動手前先對 48 筆人工校準表量 3-gram 覆蓋率對人工標籤的分布——結果打臉: ①只改一個數字的合成負例覆蓋率仍在 0.70–0.73,相鄰條文誤歸高達 0.84,覆蓋率單獨不安全; ②真正逐字的主張很少,校準集 34 條支持只有 3 條在 0.85 以上,AWQ 5 條誤判裡只有 1 條是真逐字, 另一條是改寫過的句子(0.57)。設計改成「覆蓋率 ≥ 門檻 + 數字守門 + 短句守門 + 對單一 chunk 判定」, 門檻用網格掃取零誤放高原(0.85–1.00)中點 0.925。old-coder 走完:第一輪變異存活「門檻改嚴格大於」 (沒釘等於門檻要命中),第二輪 property 子集只殺 2/7,補三條不變量後 5/7,存活 2 個是工具層計數。

關鍵考量與抉擇:量完就知道這層修不了主力誤判,還做的理由是它零誤放、零成本、且把 judge 對 「原句照抄」的不穩定(同句拆兩條判不同)確定性地封掉;修主力靠 prompt v3 的棄答對象歸屬規則。 沒有測量的話會做出「覆蓋率 0.7 就放行」的版本,把數字竄改負例全放進 supported——這是 judge 最不能犯的錯。

最終結果:verbatim_hit 進 judge 管線(缺 config 鍵即停用,舊行為不變),verbatim_grid 隨校準表換版重掃門檻; gauntlet 91/91 變異全殺。遺留:prompt v3、語言漂移欄、校準集按判定難度型分層抽樣。

❌ 失敗紀錄

  • 用 git checkout <檔> 還原一次性變異,把同檔尚未 commit 的實作一起抹掉,重寫一次才回綠。 還原一次性變異要用 sed 逆向或先 git diff 看清楚,不整檔 checkout。

2026-09-18 — judge 的真實 κ 是 0.49 不是 0.85:分層抽樣、兩版 prompt、一次拆問,最後承認極限

起因:AWQ 品質複核發現 judge 判 contradicted 的 8 條裡 5 條是 judge 自己判錯,但校準 κ 0.845 看起來很健康。 懷疑的不是 judge,是校準集:隨機抽 30 題,逐字型與簡單支持句佔多數,κ 被撐高。

嘗試過程:①提三個分層抽樣方案,選「四層等配額」——用確定性規則(3-gram 覆蓋率、棄答字樣)把四格式的 主張去重後分成逐字 / 改寫 / 低重疊 / 棄答四層各抽固定筆數,人工重標 42 列;v2 在新集只剩 κ 0.488,錯分成 判太嚴 4(改寫句判反、正確棄答判反)與判太鬆 5(單位錯置、假稱存在、錯誤棄答)。過程順手抓到兩個 bug: 簡體「参考資料未提及」繞過繁體拒答標記被當主張送判,四格式同題同序號 claim_id 撞號讓 score 無聲掉 8 列。 ②prompt v3 針對九筆錯各寫一條明文規則,重跑 42 筆:判定一筆未變,只有一筆 evidence_chunk 編號和一筆 JSON 換行不同。 ③拆成三個子問(對象歸屬 / 數值與計量基準 / 列舉項目)各自問、程式合併:8 筆翻對 3,負例 7/7。棄答型 2/2 翻對; 但單位錯置在明確要求比計量基準下仍看不見,無數字句被 schema 逼出假數值(pre-mortem 寫的失效模式當場出現), 相鄰條文誤歸拆問一樣錯。

關鍵考量與抉擇:v1→v2 加規則有效、v2→v3 無效,差別在前者補的是「模型沒想到要查」,後者要的是「查了但判反」的裁量, 規則文字推不動裁量。拆問只對「對象歸屬」一型有效。換更大 judge 要動 §5.3 定案且所有校準數字作廢。 三選一,選了最不好看但最誠實的:judge 定位改為「四格式相對比較」,κ 0.488 與錯型明列成適用邊界,prompt 退回 v2。

最終結果:C 軸結論不受影響(錯型對每個格式對稱);「棄答型走對象歸屬拆問」列第二階段候選; 分層抽樣、逐字命中前置層、拆問探針三個工具留在 repo。今天最有價值的產出不是任何一個 κ,是知道舊 κ 為什麼高估。

❌ 失敗紀錄

  • prompt v3:四條規則、零判定改變。以為明文規則萬能,實際只對「漏查」有效。
  • 拆問 B1:pre-mortem 預想「schema 允許空陣列模型仍硬填」,實驗第一輪就出現;預想到不等於防得住,要在合併規則裡擋。
  • 分層抽樣第一版 claim_id 撞號,score 無聲掉 8 列到 n=34 才發現;「列數對不上」是最便宜的偵測訊號,之後 score 應直接對 n 斷言。

2026-09-18 — Demo 跑通:一個只看 pydantic 的 dry-run,漏掉整棵相依樹

起因:發布前要一個能現場展示「選型差異」的介面。定案雙欄並排:同一題,左右各一個檢索 preset, eval set 真值綠框、只有本欄撈到的黃框,底下各自串流 vLLM 回答。純邏輯進 gauntlet,UI 層用 --check 真跑驗證。

嘗試過程:雲端連不到 HF,只能 pip install --dry-run 檢查 gradio 會不會動到 pydantic(vLLM 已把它升到 2.13.5,是已知衝突點)。 dry-run 沒報衝突就釘了 6.27.0。桌機一裝,huggingface-hub 從 0.36 被拉到 1.32,transformers 4.57.6 要求 <1.0, dense 檢索器 import 直接崩——bm25 那欄好好的,hybrid 那欄死在建置。

關鍵考量與抉擇:三條路。降 transformers 會連動 vLLM 的釘選,否決;demo 另開 venv 就不能直接用主 venv 的檢索器,否決; 剩下找一個 gradio 版本同時滿足兩邊。逐版查 PyPI metadata:6.17.3 是最後一個接受 hf-hub 0.x 的版本,6.18.0 起硬要 1.2+。 改釘 6.17.3 並顯式釘 hf-hub 0.36.2,--check 一次過。

最終結果:demo 跑通。VRAM 23879/24564 MiB,vLLM 0.75 + bge-m3 + reranker 同卡只剩 685 MiB,能用但沒有餘裕。 第一題(b1-q05 四環素妊娠)兩欄 top-2 都命中真值但順序相反,第三筆各自撈到不同的黃框——正是想展示的差異。 教訓寫進環境坑 F9:dry-run 要看整棵相依樹的上界,不是盯一個已知衝突套件。

❌ 失敗紀錄

  • 高風險假設①「gradio 6.27.0 與主 venv 無衝突,dry-run 可驗」寫得太窄:驗證方法只涵蓋 pydantic,推翻它的正是沒被列進驗證的套件。

2026-09-18 — 引用防線:UI 那行「引用有效 [] / 無效 []」一直是空的,因為 prompt 根本沒要求引用

起因:demo 錄影前檢視成品,發現回答尾行的引用檢查從沒亮過——cited_indices 是為 judge 第二階段預埋的, 但 demo prompt 刻意與實驗 prompt 同源,沒有要求標 [n]。實務 RAG 沒有引用等於不可追溯,決定補上。

嘗試過程:prompt 加一句「每句結尾以 [段落編號] 標明依據;無法回答只寫『參考資料未提及』且不標引用」, core 加 cite_stats(有效/越界/是否引到真值/未標引用句數)與批次摘要,demo_app 加 --cite-rate 模式量 100 題。 gauntlet 第一輪 5l 死在 harness(scripted 劇本只有第 1 題),第二輪 property 子集對 4 個新錨點全存活, 補「獨立重算」不變量後第三輪殺掉句界與分母兩個核心語意錨點。

關鍵考量與抉擇:引用規則加在 demo prompt 還是實驗 prompt?加實驗 prompt 會讓 B/C 軸 200 多筆 bench 與 judge 數字全部要重跑, 否決;demo prompt 自此分岔並在設定檔明示。引用「語意是否支持該句」留給 judge 第二階段,本次只做確定性的格式與真值對應。

最終結果:7B-GPTQ 遵循率 100%、零越界;top-3 含真值題數 84 vs 97 正好重現 A 軸 hit@3;真值在 top-3 時引到它的比例兩邊都 0.90—— 「引對段落」是模型能力,與檢索配置無關。三成句子沒標引用——隔天截圖發現是切句缺陷(模型把 [n] 放在句號後),修正後重量只剩 0.019 / 0.000,幾乎每句都有引用。下一件:檢索信心門檻拒答(b2-q05 那種錯撈仍會被「有效引用」)。

❌ 失敗紀錄

  • 一開始把 truth_cite_rate 分母寫成「有 valid 的回答」,自由題(無真值)會壓低比例;公開修正規格、加 has_truth 欄位。
  • property 策略直接建 CiteStats 不經過 cite_stats,對「cites_truth 恆 True」永遠殺不到——這種存活是預期的,由手算案例覆蓋; 真正該補的是「句界」與「分母」這種 property 能獨立重算的核心語意。

2026-09-19 — 拒答門檻:一邊乾淨得像教科書,另一邊根本沒有高原

起因:demo 錄影前那兩題(b2-q05、b3-q08)暴露了「檢索錯,回答照樣自信」。想加一道確定性防線: top-1 相關分數低於門檻就不送 LLM。門檻不手猜,用 100 題 + 20 題語料外題網格搜尋取高原中點。

嘗試過程:先把 reranker 和 BM25 的分數從檢索器露出來(協定只回 id,分數算完就丟);寫 20 題語料外題交人工逐題核 (退回 2 題:一題頻率句型與給付規定同構卻沒標誘撈,一題是易變事實;另補標 2 題意外的誘撈題)。桌機一次取分數、離線掃 40 格。

關鍵考量與抉擇:兩欄都搜還是只搜 reranker?選了都搜,因為「BM25 沒有可校準的信心」應該是量出來的結論,不是假設。 結果 reranker 側語料外 20 題 top-1 全部低於 0.026、有真值 97 題只有 1 題低於 0.10,高原寬到 0.03–0.59; BM25 側分數隨查詢長度在 7–167 之間漂,所謂高原只有一格,下一格誤拒就 3.6%。門檻只掛 reranker 側。

最終結果:hybrid + reranker 門檻 0.308;BM25 不掛,demo 旁白多一句「最穩配置的隱藏代價」。 誠實限制:top-3 沒撈到真值的 3 題裡,2 題 reranker 仍給 > 0.85——錯撈但語意相近的題,門檻分不出來; 這類要靠 judge 或更好的檢索,不是門檻能解的。

❌ 失敗紀錄

  • S1 只跑 pytest 就 commit,兩個既有變異錨點因重構過期,gauntlet 殘留偵測把整層擋下;變異層不是「最後再跑」的東西。
  • 語料外題第一版把「多久檢查一次」標成純事實型,人工審查指出它與仿單頻率句型同構——出題者自己最容易看不見誘撈面。

2026-09-19 — 為了補一個爭議點,反而挖到 demo 最有價值的發現

起因:面試前自查 demo 還有什麼會被挑。列了九點,其中「引用有效 ≠ 引用正確」最沒底氣—— UI 顯示「引用 1」只證明編號存在,不證明那句話真的來自那段。補了一層確定性檢查: 每句算「字元 3-gram 被所引段落覆蓋的比例」,零 LLM 成本。

嘗試過程:桌機量到低覆蓋句佔三成,遠超我預設的 0.1 判讀線。寫了抽樣工具把低覆蓋句連同所引段落輸出成表, 人工逐條判「改寫 / 綜合 / 編造」。23 條判完:改寫 12、編造 11、綜合 0。

關鍵考量與抉擇:原本打算依佔比決定「調門檻」或「改指標」,結果三個反例把整個方向推翻了—— 覆蓋率 0 的一筆是逐字忠實的改寫,覆蓋率 0.351 的一筆卻是錯藥(問 A 藥的神經警語,引到止瀉藥仿單的「疲勞、眩暈」), 0.484 那筆是因果倒置(把藥性說成時間點的性質)。覆蓋率高低與正確與否根本不相關,調門檻是白費工。 決定不把它當分數,只當「這句要人工查證」的排序依據,程式與 UI 措辭全部改掉。 另一個判斷:11 筆編造裡有 4 筆其實是「內容對、引錯段落」,同一句在另一個配置引對就完全有據—— 獨立成類,否則會把幻覺率說得比實際嚴重。

最終結果:11/74 ≈ 15% 的引用句在所引段落裡找不到依據,而且這是下界(只查了低覆蓋那 23 句)。 兩個檢索配置的編造數相當,所以「檢索做好生成就會忠實」是錯的。這個發現比原本想補的那個爭議點有價值得多: 它用可重跑的方法量出了 RAG 生成端的真實缺陷,而不是又一個漂亮數字。

❌ 失敗紀錄

  • 我設的 0.1 判讀線建立在「低覆蓋 ≈ 編造」的假設上,這個假設本身就是錯的;幸好動工前把它寫成高風險假設要求驗證,才沒直接拿去當指標。
  • 綜合型 0 筆是意外:模型引用多段時,內容其實全來自其中一段。引用清單有裝飾成分,這點原本沒人想到要查。

2026-09-20 — gauntlet 自己也需要 gauntlet:檢測工具連續四輪給不出有效結果

起因:給多成分題(「補助對象與頻率為何」這類要答兩件獨立事情的題)加一層「必要成分命中」判定。 既有指標全看不到這個缺陷——檢索層真值齊全、忠實度層答的那半有據,只有「答不完整」沒人量。 程式本身兩天就能寫完,但要通過 gauntlet 才算完成,而 gauntlet 連續四輪都沒能給出一次有效結果。

嘗試過程:第一輪跑到變異層時,設計被指出兩個缺口(禁止表述欄、正規化層該取代逐題列舉變體), 主動中止重做。第二輪 139/140,存活的是我自己剛加的錨點——一個冪等性洞當初由 property 測試隨機找到, 修好實作後只留 property 守著,換一個 seed 就湊不出那個字元序列,於是變異測試突然報存活。 第三輪終於跑到新加的成分層,連爆兩個 bug:那段被放在假 LLM 伺服器已經關閉之後(Connection refused), 而且假伺服器預設回覆不含引用標記,所以那層永遠零列——我寫的 assert rows 前兩輪根本沒被執行過, 我卻在報告裡把它當成已驗證的一層。第四輪為了省事把 commit、push、gauntlet 串成一條 && 鏈丟進背景, 同時犯三個錯:無法觀察進度導致誤判 commit 沒發生、通過閘門取到 tail 的 exit code(這是我自己寫進規則 明令禁止的寫法)、log 檔名撞到三週前的殘留檔,差點把那份舊的「全層通過」當成本輪結果。 最後那條鏈的外層 shell 被殺掉時,gauntlet 當子進程一起陪葬,SIGTERM 沒讓還原邏輯跑完,留下變異殘留。

關鍵考量與抉擇:每一輪失敗都有「放寬斷言讓它過」與「把檢測工具本身修對」兩條路。放寬的誘因很強—— 第三輪那個 assert rows,只要改成「零列也接受、只驗表頭」就立刻綠燈。但那等於用一個永遠不會失敗的斷言 換報告上的綠色,比沒有斷言更糟:它會讓人以為那一層有在守。四輪下來沉澱進常駐規則的四條, 全部是關於檢測工具的可信度而不是產品程式碼——錨點順序、property 反例必須固化成手算測試、 斷言必須確認真的執行過(判準是 log 裡看到它自己印的訊息)、背景執行只包單一指令。

最終結果:程式與清單完成(16 題 36 成分)。設計上最有價值的一點是把兩種禁止表述分開: 「每日2粒」會假性滿足「2粒」這個成分,屬保護型,掛在該成分上歸零;「迷走神經刺激術」不命中任何別名, 它證明的是整題答到別的藥去了,屬懲罰型,整題歸零。共用一個欄位會把「答不全」與「答錯題」統計成同一個數字。 實測四個案例全部按設計運作。真正的收穫是意識到 gauntlet 自己也需要 gauntlet—— 一個從未執行過的斷言、一個只靠隨機測試守著的不變量、一個撞到舊檔的 log, 都能讓「全層通過」變成一句沒有內容的話。

❌ 失敗紀錄

  • 連續兩輪把一個從未執行到的斷言當成已驗證。判準錯了:我檢查的是「腳本裡有這行」, 而不是「log 裡有它印出的訊息」。前面某層先 EXIT=1,後面的斷言就是死碼。
  • 剛把「閘門一律取工具本身 exit code」寫成規則,下一個小時就自己寫了 pytest | tail && git commit。 規則寫進文件不等於會遵守,尤其是在「只是順手串一下」的時候。
  • 查一個題目的成分時順手查了語料,發現要求的關鍵詞(某症候群名)在全語料出現 0 次—— PDF 抽取階段就掉字了(同份文件 19/1046 chunks 有同型的括號錯位)。 原本想把它列為必要成分,那等於獎勵幻覺:模型唯一拿分方式是從參數記憶生成語料裡不存在的內容。 也順手更正了先前寫進備忘錄的診斷——我原本判成「切塊邊界問題」,實際病灶更前面。

2026-09-23:錯誤棄答的三段治理——可見化成功、prompt 治療反效果、最終落在確定性守門

問題點:人工複核第六輪跑分時發現,評測表上一直「少了一整類錯誤」。RAG 系統回「參考資料未提及」 (棄答)時,句級支撐分析會整句跳過它,於是每輪有 20~30 個「題 × 檢索配置」組合從表上完全消失—— 從首輪起就是如此,而我們渾然不覺。更糟的是棄答本身會錯:21 筆棄答裡有 11 筆,含答案的正確段落 就在模型眼前的 context 裡,多數還是模型自己標了引用的那一段——它引了 [3],然後說 [3] 沒提到。 這種「錯誤棄答」在展示時比答錯更難看:使用者眼睜睜看著系統拿著答案說沒有答案。

怎麼處理(三段): 第一段先讓它可見:評測表新增「棄答列」列型,並加一欄確定性計算的「真值最佳名次」—— 棄答時自動標出正確段落被檢索排到第幾名。「該取到卻棄答」從此會自己浮到人工視線前面, 不再依賴有人碰巧去翻原始輸出。這一步同時逼出真值判準的顯式化(正文含答案才算真值, 答案只在標題路徑的塊不算——因為生成層根本讀不到標題)。 第二段嘗試用 prompt 治療:既有規則只推單方向(「段落沒有的一律答未提及」),補上反向的 成對規則「參考段落已載明的內容,不得宣稱未提及;回答未提及前必須逐段確認」。 單一變更、引用規則保持句尾、與前一輪同 seed 同檢索,一輪就能量出淨效果。 第三段是收拾:量出來的結果決定回退,守門職責轉移給第一段建好的確定性層。

結果如何:第二段災難級反效果。檢索結果 200 個組合與前一輪逐一比對完全相同, 變化全在生成端,歸因乾淨——而生成端的變化是:棄答 22 → 66,新增的 44 筆全部是 原本正常作答(而且多數答對)的題變成純棄答,棄答率 11% → 33%;原有 11 筆錯誤棄答零修復; 連規則瞄準的那個原始案例,尾句也只從「未提及其他限制」變成「未提及」。 機制事後看很清楚:那條規則把「參考資料未提及」這個字串在系統提示裡又重複了一次, 並把模型的注意力引向「找不到的可能性」。7B 量化模型對這種「要求自我檢查」的 meta 指令, 反應不是執行檢查,而是更頻繁地輸出被反覆強調的字串——規則文字本身成了促發源。 好在判定不需要任何人工複核:「拒答原本能答的題」無論新回答內容如何都是退步,機械比對即可定案, 從收到數據到回退推送不到一小時。

以後怎麼改善(最終解法):

  1. prompt 層的可行空間收斂成一條經驗律:對這個規模的模型,格式/句式約束可以加 (禁 meta 前言那條成功,3→0 零副作用),「要求模型自我檢查」的 meta 約束不能加 (藥名照抄失敗、禁宣稱缺席反效果,兩次實證)。規則能收斂「怎麼寫」,不能新增「先想再寫」。
  2. 錯誤棄答的守門定案走確定性層:棄答列 + 真值名次欄是不依賴模型遵守任何規則的檢查, 每輪自動把嫌疑筆排出來,人工只判「段落是否真含答案」這一步。修不動的行為,改成守得住的流程。
  3. 實驗紀律是這次唯一全程沒失手的部分,保留:單一變更、同 seed、檢索層逐一驗證相同才歸因生成層、 預期(含失敗預期)寫在動工前、回退原文預載在設定檔註解裡。反效果出現時,回退是十分鐘的事。
  4. 對外敘事(面試)如實講:錯誤棄答 11/21 是已知限制,prompt 治療經實驗證偽, 系統以確定性工具把它納入例行監控——「我量到了、修不動、但守得住」。

❌ 失敗紀錄

  • 動工前的預期是「錯誤棄答下降、不預期歸零」,實際方向完全相反。錯在把「規則能收斂輸出格式」 外推成「規則能新增檢查行為」——後者對 7B 量化模型不成立,而且外推當下沒有任何一輪數據支持它, 只有「聽起來合理」。合理性不是證據。
  • 這條規則在更大的模型上是驗證過的成功模式(成對規則封死兩個方向), 直接移植時沒有把「模型規模」當成前提條件。成功經驗攜帶著它的適用範圍,搬家時範圍不會自動跟過來。

2026-09-29:一篇文章的一句話「judge 的誤差是對稱的」,花了六輪審計才從假設變成量測

問題點:英文文章 #2 的結論是「W4 量化輸出改變很多,但品質沒變」,依據是本機 14B judge 的 faithfulness 分數(bf16 0.959、GPTQ 0.960、AWQ 0.938)。外部審稿指出,這個結論其實靠一句沒被量過的話撐著: 「judge 適合做格式間的相對比較,因為它的誤差是對稱的」。審稿同時點出四個具體的洞: ①只審了 AWQ 的 contradicted,拿來比沒審過的 bf16(後來查證 bf16 其實同批審過,只是文章沒寫); ②棄答沒有主張可判,整句離開分母,分數是在一個隨格式變動的子集上算的; ③罕用字附近的藥名寫壞(不存在的藥名、亂碼)不是「矛盾」,judge 的框架根本看不到; ④W4 讓簡體漂移從 2 題增加到 6~9 題。如果 judge 對簡體判得比較嚴,量化格式反而被低估。 換句話說,推薦結論的品質證據,一半是量測,一半是假設。

怎麼處理(每一步都是「審稿提問 → 找到最便宜的量測 → 桌機跑 → 人工判定 → 入冊」):

  1. 先改措辭再補證據:文章寫明 judge 看的是取回的三段全部(不是只看被引用那段),以整句為單位判; 棄答、實體退化列為「分數不涵蓋」;對稱性改寫成「未驗證假設」。先讓文章不說謊,再決定要補哪些量測。
  2. GPTQ contradicted 補審(5 條,不需 GPU,用既有 judge 檔攤成複核表):真錯 0,judge 誤判 4, 其中 3 條是 bf16 / AWQ / GPTQ 三格式的同一句都被判錯,這是對稱性的第一筆直接證據。 另 1 條查到是管線假影:GPTQ 的 judge 檔早於「簡體棄答標記正規化」修正半天, 依現行程式碼那句根本不會送給 judge。
  3. 追一個「消失的題」挖到盲點:為了查某題為何沒出現在 GPTQ 的 contradicted 裡,調出該題判定, 發現 GPTQ 寫「每日 2 粒,每 6 小時一次」,judge 判 supported;回語料查仿單原文是「2 粒,每 6 小時一次」 (每次 2 粒),日劑量少了 4 倍。審計只看 contradicted,等於只查 judge 抓到的,不查它放過的。
  4. supported 側劑量句補審(三格式各 35~39 條,以正則篩「數字+單位」或「每日/每次」): judge 放過的錯誤,bf16 3/39、GPTQ 3/36、AWQ 3/35,三格式幾乎相同。 九筆錯誤同一型:字面都在段落裡,關係錯了(每日/每次互換、適應症間的頻次錯植、公告日當實施日、 互斥類別合併、條件遺漏),新型只有一個實體錯置(把長效針劑的注射間隔答成一般劑型的)。 其中兩題是兩個格式犯同一個錯,judge 兩次都放行,可見是模型共有的錯,不是量化引入的。
  5. 簡體轉繁重判(要開 14B judge,唯一需要 GPU 的一步):取三格式所有含簡體專用字的主張 16 條, 同一輪各判「原句」與「OpenCC 轉繁版」,並關掉逐字命中前置層,避免轉繁後覆蓋率變高、繞過 LLM, 量到的是前置層而不是 judge。轉換套件裝在 /tmp 臨時 venv,不污染主環境。

結果如何:

  • 16 條中 15 條判定不變,唯一翻轉是 bf16 的一句,方向是轉繁後判得更嚴,與假設相反。 原先懷疑「被簡體干擾」的那筆 AWQ Letrozole 棄答(b4-q12#1),核對 claim_id 後確認轉繁後仍判 contradicted, 錯的原因不在字形。
  • judge 誤差在三個格式、兩個方向上都接近:contradicted 側誤判 4/5、5/8、4/5,supported 劑量句漏放 3/39、3/35、3/36。 「誤差對稱」在 bf16 / AWQ / GPTQ 上從假設變成量測;FP8 沒審,文中寫明。
  • 審後差距(對 bf16):GPTQ 兩側皆 0;AWQ 在 contradicted 側多 2 條。推薦 GPTQ 的結論站住了,而且比之前更有底, 但措辭同步收緊:文章不說「GPTQ 跟 bf16 一樣好」,只說「在這個 judge 量得到的範圍內沒有差距,量不到的範圍列清楚」。
  • 備忘錄新增發現 56~58;文章 Result 3 從一段「分數 + 一句保證」變成「兩個方向的審計表 + 三個明確的不涵蓋項」。

以後怎麼改善:

  1. 審 judge 一律兩個方向一起審:只看被判 contradicted 的句子,是在審 judge 的「告發」,不是審它的「判斷」。 supported 側至少要抽高風險子集(劑量、數值、日期、條件),否則漏放的錯型永遠不會出現在任何表上。
  2. 用 judge 做相對比較之前,先量對稱性,不要先寫對稱性。最便宜的量法有兩個: 看誤判句是否跨格式重疊(同一句各格式都錯 = 誤差綁在句子上,不綁在格式上); 對疑似偏誤來源做配對重判(同一輪、只改一個變因、關掉會混淆的前置層)。
  3. 評測產物要記錄產生時的程式碼版本。這次靠檔案時間戳對 commit 時間才發現管線假影; 如果 judge 輸出檔自帶 commit hash,這種「判定時的程式碼 ≠ 現在的程式碼」一眼就能看出來。
  4. 文章的每一個形容詞都要能指回一個表格。「對稱」「不可分辨」「沒有差距」這類字, 寫之前先問:哪一份審計表支持它、它涵蓋哪個方向、不涵蓋什麼。

❌ 失敗紀錄

  • 原稿寫「its errors are symmetric」時,手上的證據只有 κ 值和 AWQ 單邊複核,沒有任何一筆直接量測。 這句是用來支撐結論的,自己卻沒有支撐——而且同一篇文章裡就放著反向證據(簡體漂移),當時沒把兩件事接起來。
  • 審計表只列 contradicted,心裡把它當成「審過了」。直到為了追一個不相干的問題, 才撞見一條四倍劑量錯誤被判 supported。審計的方向本身就是一個會被遺漏的變因。
  • 三格式審計中,審稿人和我各算錯過一次分母(bf16 劑量句是 39 條,被寫成 36 條)。 入冊前逐筆對原表計數的習慣抓到了它,但這提醒:數字從對話轉抄到文件時,每一次轉抄都要回原始檔核對。

2026-09-30:從私有開發到公開發布——內容審稿、repo 搬移與上線後才抓到的錯

起因:開發一直在私有 repo 進行,裡面混著只給自己看的工作規則、規格書與桌機操作手冊。要當作品集公開, 必須做到三件事:①公開檔案不得引用任何不上傳的文件 ②數字與說法經得起外部審稿 ③只懂英文的人也看得懂。

怎麼處理:

  1. 引用清理:公開文件中約 70 處引用規格書、操作手冊或內部規則的句子逐句改寫,49 個檔案套用機械規則, 最後以全 repo 搜尋確認殘留為 0。過期截圖刪除,補上 MIT 授權。
  2. 兩篇文章與語料清單交給外部審稿(共四輪)。審稿抓到的問題全部回到資料查證,不是只改措辭: 文章 #1 開頭把 hit@10 當成 recall@10;文章 #1 說標題路徑「沒用」、語料清單卻說增益「正是靠標題路徑」, 兩邊都沒量過——補跑一次欄位拆分 ablation(幾分鐘、只換索引欄位),量到 title +0.039、heading +0.015、 兩者合用 +0.086,兩份文件才講同一個故事;語料清單的授權依據對藥品仿單不成立(作者是藥商不是機關), 改為兩類分開寫。文章 #2 的 judge 審計則延伸成 09-29 那篇六輪審計。
  3. README 改寫:對齊其他作品集的格式(英文在前、中文在後,問題 → 方法 → 比較 → 工程歷程 → 結果 → 限制), 並主動補上 2026 年面試官最可能先問的兩題:為什麼用 RAG 而不直接塞長上下文(用 KV cache 實測容量說明語料放不下)、 為什麼用這幾個模型(只寫符合的條件,不寫沒做過的比較)。
  4. 搬到公開 repo:下載 ZIP → 刪除五項不公開檔案 → 新 repo 以 noreply 身分做第一個 commit。 沿途三個環境坑:資料夾擁有者是 Administrators 導致 git 拒認 repo(safe.directory); Windows 憑證管理員記著另一個帳號導致推送 403(在 remote 網址指定帳號);貼指令時換行把分支名切成獨立指令。 之後的更新流程定為:私有 repo 改好 → 公開 repo 端 fetch 後只 checkout 指定檔案 → commit → push, 舊歷史與不公開檔案不會被帶上去(實際只推送該次 commit 的 13 個物件)。
  5. CI:GitHub Actions 跑 gauntlet 中 CPU 可跑的層(測試+覆蓋率、mypy、ruff、隨機順序)。 推送前先在全新 venv 照 workflow 步驟實跑一次,第一次上線即綠燈。變異測試與供應鏈稽核不放 CI (前者 80–100 分鐘;後者會因新 CVE 在程式碼沒動時轉紅)。
  6. 英文化:Decision Memo(1,168 行)與 Devlog(494 行)全文翻譯。切成 8 塊平行翻譯, 再用程式逐塊比對行數、標題數、表格列數、反引號內容與每一個數字,差異逐一檢視後才合併。
  7. Demo 影片:4 個經典案例(基本對照、藥名盲、語料外拒答)+ 結論表收尾,YouTube 中英兩條字幕軌。

結果如何:公開 repo 與中英雙語 Pages 上線,CI 綠燈,求職頁加入專案。上線後才抓到、並已修正的有: Pages 上兩張 C 軸截圖是過期版本(bf16 KV cache 寫成 111,216,正確為 113,488,另有兩格「未抄」),改成 HTML 表格; prompt 改版圖文字重疊;英文頁的圖仍是中文。

以後怎麼改善:

  1. 截圖不是資料。數字會更新,截圖不會;能用表格或由資料產生的圖,就不要放數字截圖。
  2. 發布前讓外部審稿人讀一次。四輪審稿抓到的每一個問題,都是我自己讀過好幾遍沒發現的。
  3. 兩份文件講同一件事時,要有同一個量測撐著。說法互相矛盾時,不要挑一邊改措辭,去補那個還沒量過的數字。
  4. 環境狀態先查再說。

❌ 失敗紀錄

  • 語料清單原本寫「全部為政府公文,不受著作權保護」,對 30 份藥商撰寫的仿單並不成立; README 的合規段也沿用了同一句。授權依據是寫給人看的承諾,寫錯比不寫更糟。
  • 文章 #1 開頭寫「v1.2 重跑每個數字變動 ≤ 0.003」,但純正文 BM25 實際差 0.012—— ≤ 0.003 只對四個代表點成立,當初把局部結論套到了全部。
  • Pages 的過期截圖在上線前的多輪檢查中都沒被發現,直到確認英文頁「圖片是否為英文」時才順帶看到數字不對。 檢查項目是「版面」而不是「內容」時,內容錯誤就會一路通過。
  • 把「舊 repo 要私有、保留還是刪除」列成待決事項,其實它早就是私有——沒先查證就把一個不存在的決定丟給使用者。