feat(collector): 卡片上雲前先問「這些宣稱在原文的哪裡」——H7 憑空指稱/H8 宣稱通膨(InkStoneCo#44)

2026-08-16 實測:同一個 220 bytes 的來源檔、同一個模型(workers-ai)、同一個
雲端版本(1.4.46),geek6688 萃出四條全部追得回原文,youlin 萃出八條、其中四條
(Windows/macOS/Linux/每 30 天)在原文零命中——**而那張假卡帶著真實存在的
source_path**。出處在,內容是編的;引用沒有阻止幻覺,反而讓幻覺更可信。

缺的不是更好的模型(機率性,換模型只會降低頻率、讓它更難被發現),
也不是更嚴的提示詞(同一件事的較弱版本,且無法驗證)。
缺的是一道驗證:卡片產出之後、寫進知識庫之前,沒有一步問過「原文的哪裡」。

線畫在哪(grounding.go 檔頭全文):**改寫改的是「怎麼說」,編造加的是「在說什麼」**。
摘要/換句話說/把 EARS 句翻成人話全是正當加工,所以刻意**不比對 CJK 詞**
(原文是英文、卡片是中文時,逐字比對會把忠實翻譯全部誤殺)。
只檢查翻譯與改寫都帶不走的錨點:拉丁專名、兩位數以上的數字。
另加 H8 從數量那一軸看:宣稱數 > 原文斷言數 × 3 = 輸入貧乏卻產出豐富。

分級:兩項都是**軟項**不是硬缺——使用者的知識被丟掉比被標記可疑更糟。
掛載點沿用 direct.go:1206 既有的 LintCard(萃完、POST 前),不新增資料寫入路徑。

順手修:`collector lint <卡> --source <原稿>` 的旗標被 Go flag 靜默忽略,
H6/H7/H8 全跳過還印出漂亮的空結果 `{}`——查幻覺的工具自己靜默不查。

未動(依 SDD 生命週期第 3 條寫成 pending-changes.md P12 等 confirm):
- H6 長度上限對薄原稿是反的,正把**忠實的卡**打成「疑似整段照抄」(實測 469 字 vs 上限 132)
- 軟項沒有出海口:ingest 工作流一個 quality 欄位都沒讀,標了等於沒標

驗證(離線夾具 collector/testdata/grounding/,不依賴任何線上實例):
- 正面:youlin 卡 → H7 抓到 windows/macos/linux/30,H8 抓到 8 條 vs 上限 6
- 反面:geek6688 卡 → H7/H8 零命中
- 邊界:跨語言(英→中)與同語言整段換詞兩組合理改寫 → 零誤殺
This commit is contained in:
2026-08-16 21:38:52 +08:00
parent 345be49d4d
commit 021b3962e2
11 changed files with 750 additions and 10 deletions
+24 -8
View File
@@ -268,20 +268,36 @@ func fail(err error) int {
return 1
}
// runLint 是 `collector lint <卡片.md>` 子命令:對一張卡跑 B2 品質檢查(H1–H6)。
// runLint 是 `collector lint <卡片.md>` 子命令:對一張卡跑 B2 品質檢查(H1–H8)。
// 印出分級 JSON;被擋(硬缺,或 --strict 下含軟項)=exit 1,方便腳本/CI 銷帳(施工順序步驟 4)。
func runLint(args []string) int {
fs := newFlagSet()
strict := fs.Bool("strict", false, "軟項(H3/H4/H6)也算擋(測試/CI")
source := fs.String("source", "", "原稿檔路徑(H6 相似度需要;不給=跳過 H6")
if err := fs.Parse(args); err != nil {
return 2
strict := fs.Bool("strict", false, "軟項(H3/H4/H6/H7/H8)也算擋(測試/CI")
source := fs.String("source", "", "原稿檔路徑(H6 相似度H7 落地/H8 通膨都需要;不給=三項全跳過)")
// 🔴 旗標必須也能出現在檔名**後面**InkStoneCo#44 2026-08-16 實撞)。
// Go 的 flag 套件遇到第一個非旗標就停止解析 ⇒ 照上面用法字串寫的
// `lint <卡> --source <原稿>` 會讓 --source **靜默地不生效**
// 於是 H6/H7/H8 全部跳過、印出一個漂亮的空結果 `{}`、exit 0。
// **一個查幻覺的工具自己靜默不查**,正是本票在講的那種病 ⇒ 就地修掉。
var positional []string
rest := args
for {
if err := fs.Parse(rest); err != nil {
return 2
}
if fs.NArg() == 0 {
break
}
positional = append(positional, fs.Arg(0))
rest = fs.Args()[1:]
}
if fs.NArg() < 1 {
if len(positional) < 1 {
fmt.Fprintln(os.Stderr, "用法:collector lint <卡片.md> [--source <原稿>] [--strict]")
return 2
}
card, err := os.ReadFile(fs.Arg(0))
cardPath := positional[0]
card, err := os.ReadFile(cardPath)
if err != nil {
return fail(err)
}
@@ -300,7 +316,7 @@ func runLint(args []string) int {
Strict bool `json:"strict"`
Blocked bool `json:"blocked"`
Result LintResult `json:"result"`
}{fs.Arg(0), *strict, blocked, res}, "", " ")
}{cardPath, *strict, blocked, res}, "", " ")
fmt.Println(string(out))
if blocked {
return 1