Files
Arcrun/kbdb/tests/embed-model-config.test.ts
T
Leo f3af5d3631 feat(#59): embed 模型可配置,預設換成 bge-m3——英文模型嵌中文會排錯
leo 2026-08-03 拍板「換 m3」。#59 本體(模型應可配置+index 版本化)。

## 為什麼換:實測,不是憑感覺
5 組中文問答測資(每組 1 問 + 2 段相關 + 3 段無關,無關的刻意放同一知識庫裡的其他主題),
margin = min(相關分數) − max(無關分數),margin ≤ 0 代表排序是錯的:

  @cf/baai/bge-base-en-v1.5      768   排序正確 2/5   平均 margin -0.0413  1660ms ← 舊
  @cf/google/embeddinggemma-300m 768   4/5   +0.1275   1174ms
  @cf/baai/bge-m3               1024   5/5   +0.1410    959ms ← 新(最好且最快)
  @cf/qwen/qwen3-embedding-0.6b 1024   4/5   +0.1381   3238ms

最刺眼的一組:問「知識庫問答為什麼要標出處?」→「會議室預約規則」0.7789
竟然高於真正相關的 0.7306。這正是 leo 2026-07-18 回報的「問 RAG 卻引用會議室規範」,
過去只被描述成「semantic 排名近乎雜訊」,其實是**排序錯誤**,而且五組錯三組。
英文 bge 系列的分數全擠在 0.65-0.81(區辨力接近沒有)——中文對它就是看不懂的 token。

## 改動
- `EMBED_MODEL` 常數 → `DEFAULT_EMBED_MODEL='@cf/baai/bge-m3'` + `embedModel(env)`,
  可由 `env.EMBED_MODEL` 覆寫(#59 要的「可配置」),空字串/空白視為沒設。
- 寫入端(embedOnWrite / backfill)與查詢端(semanticSearch)共用同一個 getter
  ——兩邊不同步是最惡毒的 bug:不報錯、分數全垃圾、外面完全看不出來。已加測試守。

## 換代必須換 index(安裝器那半在 arcrun-rag 同名分支)
① 維度 768→1024,舊 index 收不進新向量
② 就算維度一樣也不能沿用——不同模型的向量混在同一個 index 比對出來是垃圾,
   而 #58(Vectorize vector delete 未接)代表舊向量刪不掉
⇒ 開新 index 反而順手繞開 #58。

既有實例遷移:建新 index → 重部署 kbdb(binding 指新 index)
→ POST /embed/backfill {reindex:true} 重嵌到 remaining=0 → 舊 index 可刪。

驗證:kbdb 全套 87/87 綠(含新增 4 項);tsc 與基線相同(只剩既有的 auth.test.ts 那筆);
打包實跑產物 grep:kbdb bundle 只有 @cf/baai/bge-m3、舊英文模型 0 殘留。
2026-08-03 03:48:28 +00:00

83 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 嵌入模型可配置+換代(Arcrun#59)—— 2026-08-03
//
// 為什麼要有這個測試(別刪):
// 舊版把模型寫死成 `@cf/baai/bge-base-en-v1.5`,那是**英文模型**,拿來嵌中文等於嵌一堆
// 看不懂的 token。5 組中文測資實測(margin = min(相關) max(無關),≤0 代表排序錯):
// bge-base-en-v1.5 768 排序正確 2/5 平均 margin -0.0413 ← 舊,五組錯三組
// embeddinggemma-300m 768 4/5 +0.1275
// **bge-m3 1024 5/5 +0.1410** ← 新(品質最好、而且最快 959ms)
// qwen3-embedding-0.6b 1024 4/5 +0.1381
// 最刺眼的一組:問「知識庫問答為什麼要標出處?」→「會議室預約規則」0.7789 竟然高於
// 真正相關的 0.7306 leo 2026-07-18 回報「問 RAG 卻引用會議室規範」的直接數字。
//
// 本檔守三件事:
// ① 預設模型是 m3(有人手滑改回英文模型會紅)
// ② env.EMBED_MODEL 真的能覆寫(#59 要的「可配置」)
// ③ **查詢端與寫入端用同一顆模型**——兩邊不同步是最惡毒的 bug:
// 不會報錯、只是分數全是垃圾,而且從外面完全看不出來。
import { describe, it, expect } from 'vitest';
import { embedOnWrite, semanticSearch } from '../src/embed';
import type { Bindings, Entry } from '../src/types';
function mkEnv(over: Partial<Bindings> = {}) {
const calls: { model: string; text: string[] }[] = [];
const env = {
AI: {
run: async (model: string, input: { text: string[] }) => {
calls.push({ model, text: input.text });
return { data: input.text.map(() => [0.1, 0.2, 0.3]) };
},
},
VECTORIZE: {
upsert: async () => undefined,
query: async () => ({ matches: [] }),
},
DB: {
prepare: () => ({ bind: () => ({ run: async () => ({}), all: async () => ({ results: [] }) }) }),
},
...over,
} as unknown as Bindings;
return { env, calls };
}
const entry = {
id: 'e_1',
content: '出處標註讓使用者能回頭核對答案來源。',
entry_type: 'block',
owner_id: 'demo',
metadata_json: JSON.stringify({ embed: true }),
} as unknown as Entry;
describe('嵌入模型(Arcrun#59', () => {
it('預設是 bge-m3——不得退回英文模型(中文會排錯)', async () => {
const { env, calls } = mkEnv();
await embedOnWrite(env, entry);
expect(calls).toHaveLength(1);
expect(calls[0].model).toBe('@cf/baai/bge-m3');
expect(calls[0].model).not.toContain('-en-'); // 英文模型一律不准當預設
});
it('env.EMBED_MODEL 可覆寫(#59 的「模型應可配置」)', async () => {
const { env, calls } = mkEnv({ EMBED_MODEL: '@cf/google/embeddinggemma-300m' });
await embedOnWrite(env, entry);
expect(calls[0].model).toBe('@cf/google/embeddinggemma-300m');
});
it('空字串/空白的 EMBED_MODEL 視為沒設,回退預設(不會把空字串當模型名送出去)', async () => {
for (const bad of ['', ' ']) {
const { env, calls } = mkEnv({ EMBED_MODEL: bad });
await embedOnWrite(env, entry);
expect(calls[0].model).toBe('@cf/baai/bge-m3');
}
});
it('🔴 查詢端與寫入端必須是同一顆模型(不同步=分數全垃圾且不會報錯)', async () => {
const { env, calls } = mkEnv({ EMBED_MODEL: '@cf/baai/bge-m3' });
await embedOnWrite(env, entry); // 寫入端
await semanticSearch(env, '為什麼要標出處?'); // 查詢端
expect(calls.length).toBeGreaterThanOrEqual(2);
const models = new Set(calls.map((c) => c.model));
expect(models.size).toBe(1);
});
});