- 我以為 RAG 檢索很爛——「段級命中率」只有 27%。試了 reranker、query 前綴、hybrid 三招,全部撞在 ~36% 這道牆,還把 top-5 越調越差。
- 差點下「調不動」結論前,我做了一件該一開始就做的事:把撈回來的內文印出來,自己判對不對。結果 10/11 ≈ 91% 一直是對的——27% 是一把壞掉的尺量出來的假象。
- 根因是切片器把「表格」放到每頁最前面,害章節標籤標歪。改一行(表格接在內文之後)→ 標籤修對、出處誠實、命中率回到真實水準。
- 三個心法:評估要兩段分開量、對指標調參前先驗證指標本身、撞牆時根因常在上游(切片),不在你正調的地方。
這是一篇除錯實錄。主角是一套工廠設備手冊的 RAG(pgvector + bge-m3),但故事的核心跟你用什麼技術棧無關:我對著一個「看起來很低」的準確率調了老半天,最後發現準確率一直很好,是我量錯了。把每一步、每個數字、每段改了什麼、以及「R(檢索)到底怎麼做」全部攤開,當你是完全新手也看得懂。
一片(chunk)到底存了什麼
很多人(包括一開始的我)以為:一本書切成很多片,每一片 = 向量 + 內容,就這樣。錯,少了一半。一片實際上還帶著「出處」。從資料庫拉一片真的出來看:
| 欄位 | 值(範例) | 這是什麼 |
|---|---|---|
text | 「五、簡易故障排除 …症狀:泵浦漏氣→處置…」 | 內容 |
embedding | vector(1024 維) | 向量 |
book_id | pump_nantou | 出處:哪本書 |
section | 五、 簡易故障排除 | 出處:哪一章節 ← 就是「標題」 |
page_from | 9 | 出處:哪一頁 |
所以正確的公式是:一片 = { 內容 + 向量 + 出處(哪本書·哪章節·哪頁) }。
為什麼非要記出處?因為向量和內容只回答「它說了什麼」,回答不了「它出自哪裡」。給人的答案一定要能講「出自《南投抽水機手冊》第五章故障排除第 9 頁」,對方才能翻到那頁自己核對、才敢信。沒有出處的答案 = 一段沒來源的文字,在工安場景(答錯=事故)不能用。這個「章節出處」就是後面整個故事的主角。
R 怎麼做:一次檢索的完整流程
RAG = R(檢索)+ A(增強)+ G(生成)。這篇只講 R——「拿到問題,怎麼找出該讀的那幾片」。流程只有三步:
- 把問題翻成向量:embedding 模型(bge-m3)只做這一件事——把一句話變成 1024 個數字。它不做搜尋。
- 資料庫找最近的幾片:pgvector 拿問題向量,跟庫裡每一片的向量算「餘弦距離」,回傳最近的 top-k。
- 回傳的片自帶出處:每片都帶著它的 book/section/page(見上一節)。
# 1) 問題 → 向量(normalize 後才能用 cosine)
qvec = model.encode([question], normalize_embeddings=True)[0]
# 2) pgvector 找最近的 top-k(<=> = 餘弦距離,越小越近)
# 注意回傳的每一片都帶 section / page = 出處
SELECT text, book_id, section, page_from,
1 - (embedding <=> %(qvec)s::vector) AS score -- 相似度(越大越像)
FROM manual_chunks
ORDER BY embedding <=> %(qvec)s::vector
LIMIT 5;
記住這裡冒出一個數字:score(相似度)——問題跟這片有多像,0~1。等一下會用到它做「查不到就閉嘴」。它跟後面的「命中率」是兩個不同的東西,先別混。
pgvector 的 cosine similarity 到底在算什麼
這是最多人有看沒有懂的一段,拆到最白。
1)每片、每個問題,都是 1024 維空間裡的一支「箭頭」。bge-m3 把一段文字變成 1024 個數字,你可以想成一支指向某方向的箭頭(向量)。意思相近的文字,箭頭方向也相近。
2)cosine 量的是「兩支箭頭的夾角」,只看方向、不看長短。夾角越小 → 越相似:
| 兩箭頭方向 | cosine 相似度 | 意思 |
|---|---|---|
| 完全同方向 | 1 | 最像 |
| 垂直(90°) | 0 | 無關 |
| 完全相反 | -1 | 最不像 |

3)公式與「normalize」為什麼重要。cosine 相似度 = 兩向量的點積 ÷ 各自長度:
cos(A, B) = (A · B) / (|A| × |B|) # 點積 ÷ 兩者長度
# 小例子(用 3 維好算):
A = [1, 0, 0] B = [1, 1, 0]
A·B = 1×1 + 0×1 + 0×0 = 1
|A| = 1, |B| = √2 ≈ 1.414
cos = 1 / (1 × 1.414) ≈ 0.71 # 夾角 45°,中等相似
# 關鍵技巧:如果先把每個向量「normalize」成長度 1(|A|=|B|=1),
# 分母就變成 1 → cosine 直接等於點積 A·B,算起來又快又一致。
# 所以建庫和查詢都用 normalize_embeddings=True。
4)pgvector 的 <=> 是「cosine 距離」,不是相似度——差一個 1 減。
embedding <=> qvec -- cosine 距離:0=最近(最像),2=最遠
1 - (embedding <=> qvec) -- = cosine 相似度:1=最像,這就是我們的 score
-- 找最近的 5 片 = 照「距離」由小到大排,取前 5:
SELECT text, section, page_from,
1 - (embedding <=> qvec) AS score -- 給人看的相似度(越大越像)
FROM manual_chunks
ORDER BY embedding <=> qvec -- 排序用「距離」(越小越近)
LIMIT 5;
<=> = 距離(越小越近),1 - <=> = 相似度(越大越像)。ORDER BY <=> LIMIT 5 就是「把跟問題方向最接近的 5 支箭頭撈出來」。
5)資料多才需要 HNSW 索引。最單純的做法是「問題向量跟庫裡每一片都算一次距離」(暴力法,O(N))——1251 片這種量級,暴力掃也才毫秒級。等到幾十萬片,就建 HNSW 索引(一種近似最近鄰的圖結構),不用全部比、查詢快很多。cosine 場景建索引用 vector_cosine_ops。
• 放進向量的:我們把「書名/機種/章節」當前綴,跟內文一起餵給 bge-m3 → 幫它找得更準(方向更聚焦)。
• 存成欄位的:book / section / page 也原封存成資料庫欄位 → 給人核對出處用。頁碼這種只存欄位、不進向量。
同一個「章節」,在向量裡幫忙聚焦、在欄位裡當門牌,兩個用途。
進階:為什麼是向量、bge-m3 怎麼散、為什麼是球面不是球體
(數理背景取向;一般讀者可跳過,不影響前面理解。)
(i) 方向載語意、半徑載雜訊。 經驗上 embedding 的方向帶語意,範數傾向跟詞頻 / 多 generic / 資訊量相關,不是核心語意。用裸內積 ⟨a,b⟩ = ‖a‖‖b‖cos θ,高範數向量會不管夾角就霸榜 top-k → 撈到「大聲的」不是「相關的」。normalize 消掉這個 magnitude bias,排序純看方向。
(ii) 訓練–推論一致。 模型用 cosine(normalized dot + 溫度)當損失訓,優化的是夾角;推論就必須也用 cosine(在球面上比),才對齊「被賦予意義的量」。用球體內歐氏距離 = 量一個模型沒被訓練過的東西。
(iii) 度量塌縮成一致。 球面上 內積 = cosine = 歐氏弦距 = 夾角 單調對應(‖u−v‖² = 2(1−cos θ))→ 一個 ANN 索引即可正確按 cosine 排。留在球體內、範數不一,則 L2 排序 ≠ cosine 排序 ≠ 內積排序,三者打架。
(iv) 本質是「商掉半徑」。 宣告 magnitude 那一維無語意 → 把它 quotient 掉;我們在乎的是從原點出發的射線(ray / 方向)不是點,normalize 就是取每條射線與單位球的交點當代表。球體內部那一維正是被判為 nuisance 的維度,所以塌掉。
附帶:normalize 後分數落在 [−1,1]、跨 query 可比,前面那個 abstain 門檻 0.52 才有意義。
兩段式評估:檢索 vs 生成,分開量
RAG 有兩個不確定點,最重要的一句話:分開量。只量端到端,出錯你不知道是哪一段爛。
| 階段 | 性質 | 怎麼量 |
|---|---|---|
| ① 檢索(問題→撈出該讀的段) | 確定性(同題同結果) | Hit@k / MRR,可像單元測試反覆跑 |
| ② 生成(拿那些段→產答案) | 非確定性(每次不一樣) | faithfulness(有無幻覺)+ 跑 N 次看變異 + LLM 當評審 |
如果你走抽取式(直接秀原文 + 出處、不讓模型自由生成),第二段的不確定性幾乎工程掉——答案就是那段原文、引用天生精確。所以量測重心壓倒性落在第一段:檢索。這篇也聚焦在這。
檢索評估怎麼做:gold set + Hit@k + abstain
檢索是確定性的(同一題→同一向量→同一排序),所以可以像考試一樣量。三個步驟:
- 出一份考古題(gold set):每題 = (問題 + 這題「應該」命中的出處)。例:「漏氣怎麼處理」→ 應命中 pump 的「故障排除」段。這步找老師傅標最準。
- 跑:問題→向量→撈 top-k→每片帶出處。
- 比對:撈回來的出處,有沒有等於我標的「應該命中的出處」?有=命中(1),沒有=沒命中(0)。
# gold set:(問題, 應命中的 book, 應命中的 section 關鍵詞)
GOLD = [
("抽水機進水管漏氣怎麼處理", "pump_nantou", "故障排除"),
("空壓機排氣溫度過高怎麼辦", "compressor_hanbell", "排氣溫度"),
# … 以及幾題「6 本裡根本沒有」的離題題(期望 abstain)
("員工年假怎麼申請", None, None),
]
# 對每題撈 top-k,檢查「對的 book 且 section 含關鍵詞」排第幾
def first_rank(rows, book, kw):
for i, r in enumerate(rows, 1):
if r.book_id == book and kw in (r.section or ""):
return i # 命中的名次
return 0 # 沒命中
算三種數字:Hit@k(正確出處有沒有在 top-k 裡)、MRR(排多前面)、abstain(離題題的相似度分數夠不夠低,能不能被門檻擋掉)。第一次跑的結果:
| 指標 | 數字 | 意思 |
|---|---|---|
| Book-Hit@1(對的書) | 92% | 撈到對的手冊很穩 |
| Section-Hit@1(對的段) | 27% | top-1 常是「對的書、錯的段」? |
| Section-Hit@5 | 91% | 對的段幾乎一定在前 5,只是沒排第一 |
• 相似度分數(0.69、0.81…):pgvector 算向量距離,「問題跟這片多像」。用來做 abstain(低於門檻就說查不到)。
• 命中率(Hit@k)(27%、91%…):我的腳本算,「撈回的出處對不對」。用來量準確率。
這篇從 27 到 91 變的是命中率(對不對),不是相似度(像不像)。
調參四連敗:reranker / prefix / hybrid
段級 top-1 只有 27%,想調高。試了幾招常見的「撈完再重排」:
# ① cross-encoder reranker:撈 top-20 → 逐段重新打分重排
scores = CrossEncoder("BAAI/bge-reranker-v2-m3").predict([(q, chunk_text) ...])
# ② query 前綴:給問題加一句指示再編碼
qvec = encode(["為以下設備問題找相關的操作或故障排除段落:" + q])
# ③ hybrid(字面 + 向量,RRF 融合):技術術語靠字面精準命中
rrf = 1/(60 + 向量排名) + 1/(60 + 字面排名)
| 方法 | Section-Hit@1 | @5 |
|---|---|---|
| baseline | 27% | 91% |
| + reranker | 36% | 55% ↓ |
| + query 前綴 | 36% | 64% ↓ |
| + hybrid | 36% | 73% ↓ |
轉折:先驗證你的「尺」——27% 是假象
下結論前,我做了一件該一開始就做的事:把每題 top-1 撈到的內文印出來,自己當老師傅判「這段到底對不對」,不看我標的關鍵詞。
問:抽水機進水管漏氣怎麼處理
期望詞[故障排除] → 比對 section 標題「三、啟動後注意事項」→ 不中 → 算 miss ✗
但 top-1 的『內文』是:
「[抽水機無法抽水] 症狀:泵浦漏氣 → 處置:清潔孔螺絲未鎖緊、逆止閥…」
← 內容 100% 正確!是被「錯的章節標題」騙了。
最貴的一課:對一個指標調參之前,先驗證那個指標本身是對的。 「27% / 調不動 / 撞 36% 的牆」全是我拿關鍵詞比對髒的章節標題量出來的假象。四招調參,全部在追一個幻影——檢索本來就 ~91% 好,而那個 91,其實從第一次量(Section-Hit@5=91%)就露過臉了。
根因:section 標籤為什麼髒(一個表格擺放 bug)
章節標題(section)是切書的時候記下來的:程式用「這片前面最近的那個標題」當它的 section。問題出在切片器組每頁文字的方式——它把該頁的表格攤平後放到頁首,結果表格跑到了「自己該屬的標題」前面:
這頁真實內容: 五、簡易故障排除(標題) + 故障表:「漏氣→處置…」
原本(表格前置): 【故障表:漏氣→處置】 五、簡易故障排除(標題)…
↑ 表格在「五、」標題前面 → 前面最近的標題是上一頁的「三、」
→ section 被標成「三、啟動後注意事項」❌(錯的門牌)
注意:內容(text)和向量(embedding)完全沒問題、撈得到。壞的只有 section 這個「出處門牌」。這正是為什麼一個小小的位置 bug 有這麼大殺傷力——它是系統性的:每一頁有表格的地方都標歪,而且不會報錯,只會讓你的評估與出處默默失真。
修法 + 重建 + 前後對照
修法就一行:把表格從「放頁首」改成「接在內文之後」,它就跟到前面最近的正確標題。
# chunker.py 的 _page_text() —— 只改這一行
# 修前(表格前置 → 掛到前一個標題,標歪):
return ("\n".join(parts) + "\n" + body) if parts else body
# 修後(表格接在 body 之後 → 掛到正確標題):
return (body + "\n" + "\n".join(parts)) if parts else body
因為切出來的文字變了,要重新 build(重算向量)+ 重灌資料庫才生效。重建 6 本(1435 片)後實測:
| 指標 | 修前 | 修後 |
|---|---|---|
| Book-Hit@1 | 92% | 100% |
| Section-Hit@1(關鍵詞尺) | 27% | 45%(真內文更高) |
| 漏氣內容的出處 | ❌ 啟動後注意事項 | ✅ 五、簡易故障排除 |
要看清楚這一步「變的是什麼」:不是檢索變聰明了(內容本來就 ~91% 對)。變的是章節標籤/出處變誠實了——所以「量標籤對不對」的尺,數字自然跳上來。真正對使用者的意義是:出處不再騙人。
出處止血:以原文+頁為主 + abstain 門檻
根治(修切片)之外,serving 端也做了兩個「止血」改動:①出處以「原文+頁碼+分數」為主,section 標題為輔(它可能還有沒修乾淨的);②加 abstain 門檻,查不到就閉嘴。
# 請求加一個 abstain 門檻(實測:in-scope 最低 0.65、離題最高 0.47 → 取 0.52~0.56)
class SearchBody(BaseModel):
question: str
k: int = 4
min_score: float | None = None # None = 不啟用(向後相容)
for r in rows:
score = float(r.score)
if body.min_score is not None and score < body.min_score:
continue # 低於門檻 → 不回 = 查不到(答錯=工安,寧可閉嘴)
results.append({
"text": r.text, "page_from": r.page_from, "score": round(score, 4),
"book_title": r.book_title, "section": r.section, # section = 輔,勿當唯一權威
})
思考邏輯:內文與頁碼是可信的(撈對了、頁也對),章節標題被切歪不可信——那就把可信的當主出處、可疑的當輔。再用門檻把離題擋掉:實測離題題最高分 0.47 < 該答題最低分 0.65,兩群無重疊,切得乾淨。這是「答錯=事故」前提下的止血:內容已經對了,先讓出處別騙人、該閉嘴就閉嘴。
每個數字到底在量什麼(防被騙表)
整個故事最容易讓人看走眼的,就是「一堆百分比到底在量什麼」。一張表看懂:
| 數字 | 它其實在量什麼 | 真相 |
|---|---|---|
| 92% / 100% | 對的「書」在 top-1 | 一直很好 |
| 27% | 關鍵詞比對「章節標題」 | 假的——標題髒 |
| 91% | 對的段在 top-5 / 內文判對 | 真實品質,從頭就在 |
| 36% | 調參後的關鍵詞 @1 | 追幻影,撞牆 |
| 45% | 修標籤後的關鍵詞 @1 | 尺變準了(仍低估) |
能穩定做好的是「上游」,不是查詢時的花招
很自然會有一個直覺:「要提升準確率,就去調查詢那端的演算法,或提問時多抓幾個關鍵詞讓向量更準。」——這正是這篇從頭到尾證明「低槓桿、常常在追幻影」的那條路。reranker、query 前綴、hybrid(就是「調算法 / 提問抓關鍵」)三招,全部撞在 36% 牆、還把 top-5 越調越差。
真正能「穩定做好」的,幾乎都在上游(建庫階段),不在查詢時補救:
| 槓桿 | 在哪一端 | 本文的證據 |
|---|---|---|
| ✅ 切片品質(邊界切乾淨、出處標對) | build | 改一行表格擺放 → 出處誠實、Book@1 92→100% |
| ✅ 餵給向量的文字(embed_text) 乾淨、有好章節前綴 | build | 文字髒 → 向量就髒;前綴幫聚焦 |
| ✅ 檢索範圍鎖定(只搜「這台機器」的手冊) | build+serve | 硬過濾杜絕跨機台污染 |
| ✅ 一把準的尺(gold set) | 評估 | 尺壞了,你連「有沒有變好」都不知道 |
| ⚠️ 查詢時 reranker / 前綴 / hybrid | serve | 本文全失敗,低槓桿 |
附:一個完全獨立的測試 kit,也重現了同一件事
後來我另做了一份零依賴的測試 kit(不同的簡單切法、numpy 記憶體索引、在 Kaggle GPU 上跑),拿同一批 6 本手冊、7 題重測。第一版我偷懶把「正確答案」標成 book 名 → 一跑 Hit@1 = 100%,漂亮。改成判「正確那一段的內文」後:
| 指標(同一批資料、同一組題) | Hit@1 |
|---|---|
| Book-Hit(撈到對的『書』) | 100% |
| Content-Hit(撈到對的『那一段』) | 71%(5/7) |
一個完全不同的實作,卻獨立重現了「book 級 100% 是假安心、content 級才是真」。而且它老實抓出兩題(進水管漏氣、CNC 開機步驟)的正確段落根本不在 top-5——那個簡單 window 切法把答案切散了。
附:多格式 + 自動判切法(700 本不用一本一本看)
前面都拿 6 本 PDF 講。但真實情況是:手上700 本、還混 Word / Excel / PPT / TXT / ODT。不可能一本一本開來看「這本該用哪種切法」。要兩個機制:多格式抽字 + 自動判切法。
① 多格式抽取(零安裝優先)
| 格式 | 怎麼抽 | 需裝? |
|---|---|---|
| docx / xlsx / pptx / odt / ods | 純標準庫 zipfile+regex 拆 OOXML/ODF | 零安裝 |
| pymupdf | pymupdf | |
| txt / csv | 直接讀(試 utf-8/big5/gb18030) | 無 |
| xls(舊) | xlrd | pip install xlrd |
② 自動判切法的決策樹(靠可量訊號)
是試算表(xls/xlsx/ods/csv)? ── 是 ──▶ table(逐列切,表格是結構化的)
│否
是 PDF?
├ 平均字/頁很低 且 多數頁有圖無字 ──▶ scanned(掃描/圖片型 → 先 OCR)
├ 有 ≥8 個書籤/目錄 ──▶ toc(依章節切,標題當出處)
├ 偵到明顯標題字級(大於內文) ──▶ heading(依標題切)
└ 都沒有 ──▶ window(固定視窗保底)
│否(docx/txt/pptx/odt…)
純文字類 ──▶ window
判斷依據全是可量的訊號:書籤數、字/頁密度、字級分布、副檔名。所以能對整個資料夾**批次自動判**。實跑一個混格式資料夾,每份自動選 + 印出原因:
| 檔 | 自動選 | 為什麼 |
|---|---|---|
| plc / vfd(pdf) | toc | 有 89 / 120 個書籤 |
| cnc / pump(pdf) | heading | 偵到標題字級大於內文 |
| docx / pptx / txt | window | 無 PDF 結構訊號 |
③ 真的下載真檔來驗,不是嘴上「支援」
「支援多格式」很好講,但程式碼寫得出 from_xlsx(),不代表它切出來是對的。所以 notebook 裡真的去公開來源下載真的 Office 檔(filesamples.com 的 .xlsx / .docx / .xls),抽出來、切出來、再用一句檔案裡真的有的英文片語當 gold 去查,看檢索有沒有命中那一片。抽字量對得上、content-hit 命中,才算「這格式真的通」。
| 真檔 | 抽出 | gold 片語(檔案裡真的有) |
|---|---|---|
| accessibility.docx | 36,855 字 | eight section headings |
| excel_tips.xlsx | 逐列 | 30 Excel Functions in 30 Days |
| sample.pptx | 5,856 字 | — |
④ 讓別人也能一鍵跑:預設「對方環境是空的」
要把 notebook 丟上 Kaggle 給別人 Run All,一個根本認知:預設對方機器什麼都沒有。你本機有的套件、有的模型快取,對方通通沒有。所以第一格就 pip install / apt install 把環境裝齊,不要靠「我這台剛好有」。
os.environ['HF_HUB_OFFLINE']='1'——那是「我本機已經有快取」才成立的假設。搬到 Kaggle 空環境,這行反而禁止下載,直接 LocalEntryNotFoundError。拿本機當基準,就是搬家會爆的原因。
嵌入模型(bge-m3,約 2GB)的載入寫成雙路徑,失敗給明確指引:先找有沒有掛上來的本地模型(不靠網路、最穩),沒有再線上下載,兩條都失敗就印出「怎麼辦」而不是丟一坨 traceback。
# 第一格:預設空環境,先裝齊(不靠「我這台有」)
!pip -q install pymupdf sentence-transformers numpy xlrd
!apt-get -qq -y install tesseract-ocr tesseract-ocr-chi-tra tesseract-ocr-chi-sim
# bge-m3 雙路徑:掛上來的本地模型優先 → 否則線上下載 → 都失敗給指引
def _find_local_bge():
# Kaggle 模型掛得很深(.../bge-m3/<框架>/<變體>/<版本>/config.json),
# config.json 常在 bge-m3 目錄再下好幾層 → 用「路徑含 bge+m3 的 config.json」定位才穩。
import glob, os
for cfg in glob.glob('/kaggle/input/**/config.json', recursive=True):
low = cfg.lower()
if 'bge' in low and 'm3' in low:
return os.path.dirname(cfg)
return None
_local = _find_local_bge()
try:
if _local:
print('用本地掛載模型:', _local); emb = SentenceTransformer(_local, device=device)
else:
print('線上下載 bge-m3(需 Internet On)…'); emb = SentenceTransformer('BAAI/bge-m3', device=device)
except Exception as e:
msg = ('bge-m3 載入失敗 —— 兩解擇一:\n'
' (A) 右側 Add Input -> Models -> 搜 bge-m3 掛上(不吃網路,最穩)\n'
' (B) Settings -> Internet -> On(線上下載 ~2GB)\n'
f'原始錯誤: {e}')
print(msg)
raise SystemExit(msg) from None # from None:切斷鏈式例外,免得 Kaggle 的 IPython 格式器爆 NoneType
emb.max_seq_length = 512 # 封頂,防 VRAM 爆
| 拿模型的方式 | 動作 | 適合 |
|---|---|---|
| (A) 掛 Kaggle Model | Add Input → Models → 搜 bge-m3 掛上 | HF 被擋、要可重現,不吃網路 |
| (B) 開 Internet | Settings → Internet → On | 網路通、懶得掛模型 |
心法總結
- 一片 = 內容 + 向量 + 出處。 出處(書·章·頁)不是裝飾,是「答案能不能被追、被信」的關鍵,尤其工安場景。
- 評估要兩段分開量。 檢索(確定性,Hit@k 可回歸)、生成(非確定性,faithfulness)。混在一起你不知道哪裡爛。
- 對指標調參前,先花 10 分鐘驗證指標本身是對的。 尤其你的「正確答案」是用某個 metadata 標籤(章節、分類)比對出來的——先確認那些標籤可信。我沒驗,結果對著一把髒尺調了半天、還差點下錯結論。
- 不同方法收斂到同一個數,是「天花板不在你正調的地方」的強訊號。 該往上游(切片/標籤)或往量測本身找,不是繼續加檢索花招。
- 一行改動的價值,不一定在「讓它變強」,而在「讓一個系統性 bug 消失」。 這次那一行讓章節標籤不再標歪 → 出處誠實、評估也終於量得準。
發佈留言