併 bge-m3 換代(#59)+補上分支漏做的另一半:Vectorize index 也換
leo 2026-08-05:「換 embed model 當然要合併,當然要換 vectorize,**原本的根本不能用**」
——我把已拍板的事當成待裁決,錯了。
## 為什麼非換不可(08-03 leo 拍板,5 組中文測資實證)
@cf/baai/bge-base-en-v1.5 768 排序正確 2/5 margin -0.0413 1660ms ← 舊
@cf/baai/bge-m3 1024 5/5 +0.1410 959ms ← 新
舊英文模型嵌中文:分數全擠在 0.65-0.81,區辨力接近沒有=**根本不能用**。
## 🔴 分支只做了一半,另一半我補上
feat/embed-model-m3-t59 只改 kbdb(embed.ts / types.ts / 測試),**完全沒動 deploy.ts**
⇒ 就算合併,安裝器仍會建 **768 維的舊 index**,新向量根本收不進去。
(git show f3af5d3 --stat 實查:只有 3 個檔,全在 kbdb/)
本次補上安裝器那半:
· KBDB_VECTORIZE_INDEX: arcrun-kbdb-embed → **arcrun-kbdb-embed-m3**(換名字,非只換維度)
· ensureVectorizeIndex: dimensions 768 → **1024**
· kbdb/wrangler.toml 說明與 metadata-index 指令同步改新名(照抄不會建錯 index)
· embed.ts docstring「768 維向量」→ 1024(分支漏改,會誤導)
## 為什麼要換「名字」不是只改維度
① 維度 768→1024,舊 index 收不進新向量
② 就算維度相同也不能沿用——不同模型的向量混在同一 index 比對出來是垃圾,
而 #58(Vectorize vector delete 未接)代表舊向量刪不掉
⇒ **開新名字反而順手繞開 #58**,且新舊並存可回滾。
## 查證(歷史警察 Q0-Q3,非假設)
· git log --all -S"arcrun-kbdb-embed-m3" / -S"dimensions: 1024" 皆空 ⇒ 無分支改過,非重造輪子
· **metadata index 用同一個常數**(deploy.ts:426 ensureVectorizeMetadataIndexes)
⇒ t36 的四個 metadata index(owner_id/entry_type/source/library,Arcrun#11 根因修復)
會自動建在新 index 上,**不會因改名而遺失**——這點特地查過,不是假設。
## 驗
· kbdb 測試 87/87 綠(含新增 4 項 embed-model-config)
· cli tsc --noEmit **零錯誤**
· 三處一致性機械確認:embed.ts=bge-m3/deploy.ts=dimensions 1024/index=arcrun-kbdb-embed-m3
· 踩到並記錄:註解寫 `**dimensions=1024**/metric` 會因 `*/` 提早關掉 block comment(TS1127)
## 既有實例遷移(尚未執行,需對實例操作)
建新 index → 重部署 kbdb(binding 指新 index)
→ POST /embed/backfill {"reindex":true} 到 remaining=0 → 舊 index 可刪。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+26
-5
@@ -163,8 +163,27 @@ export interface DeployContext {
|
||||
kbdbEmbed?: boolean;
|
||||
}
|
||||
|
||||
/** Vectorize index 名(kbdb embed 模組用)。bge-base-en-v1.5 = 768 維、cosine。 */
|
||||
export const KBDB_VECTORIZE_INDEX = 'arcrun-kbdb-embed';
|
||||
/**
|
||||
* Vectorize index 名(kbdb embed 模組用)。**bge-m3 = 1024 維、cosine。**
|
||||
*
|
||||
* 🔴 2026-08-03 換代(leo 拍板;5 組中文測資實證:舊 `bge-base-en-v1.5` 排序 2/5、
|
||||
* margin −0.0413=**中文根本不能用**;`bge-m3` 5/5、+0.1410、959ms)。
|
||||
* leo 08-05:「換 embed model 當然要合併,當然要換 vectorize,原本的根本不能用」。
|
||||
*
|
||||
* **換模型必須換 index,且必須換「名字」**:
|
||||
* ① 維度 768→1024,舊 index 收不進新向量
|
||||
* ② 就算維度相同也不能沿用——不同模型的向量混在同一 index,比對出來是垃圾;
|
||||
* 而 #58(Vectorize vector delete 未接)代表舊向量刪不掉
|
||||
* ⇒ **開新名字的 index 反而順手繞開 #58**,且新舊並存可回滾。
|
||||
*
|
||||
* ⚠️ 這個常數同時被 `ensureVectorizeMetadataIndexes()` 使用(deploy.ts:426)
|
||||
* ⇒ t36 的四個 metadata index(owner_id/entry_type/source/library,Arcrun#11 根因修復)
|
||||
* 會自動建在新 index 上,**不會因為改名而遺失**(已查證,非假設)。
|
||||
*
|
||||
* 既有實例遷移:部署後 `POST /embed/backfill {"reindex":true}` 重嵌到 remaining=0,
|
||||
* 確認語意查詢正常後,舊的 `arcrun-kbdb-embed` 可自行刪除。
|
||||
*/
|
||||
export const KBDB_VECTORIZE_INDEX = 'arcrun-kbdb-embed-m3';
|
||||
|
||||
export interface DeployResult {
|
||||
implemented: boolean;
|
||||
@@ -388,7 +407,9 @@ async function applyD1Migration(ctx: DeployContext, sql: string): Promise<void>
|
||||
|
||||
/**
|
||||
* 確保 KBDB embed 用的 Vectorize index 存在(issue #7 / T2.4)。
|
||||
* REST `POST /accounts/{id}/vectorize/v2/indexes`(dimensions=768/metric=cosine,對齊 bge-base-en-v1.5)。
|
||||
* REST `POST /accounts/{id}/vectorize/v2/indexes`(dimensions=1024 / metric=cosine,對齊 bge-m3)。
|
||||
* ⚠️ 這行別寫成 `**dimensions=1024**/metric`——`*` 緊接 `/` 會提早關掉 block comment(實撞 TS1127)。
|
||||
* 維度必須與 `kbdb/src/embed.ts` 的 `DEFAULT_EMBED_MODEL` 一致——不一致時 upsert 直接被 CF 拒絕。
|
||||
* 冪等:已存在(CF 回「already exists」類錯)視為成功,不報錯。用 init 已驗的 apiToken+accountId。
|
||||
*/
|
||||
async function ensureVectorizeIndex(ctx: DeployContext): Promise<void> {
|
||||
@@ -398,8 +419,8 @@ async function ensureVectorizeIndex(ctx: DeployContext): Promise<void> {
|
||||
headers: { Authorization: `Bearer ${ctx.apiToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: KBDB_VECTORIZE_INDEX,
|
||||
config: { dimensions: 768, metric: 'cosine' },
|
||||
description: 'arcrun KBDB optional embed module (issue #7)',
|
||||
config: { dimensions: 1024, metric: 'cosine' },
|
||||
description: 'arcrun KBDB embed module — bge-m3 1024d (issue #7 / #59)',
|
||||
}),
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
});
|
||||
|
||||
+32
-4
@@ -13,18 +13,46 @@
|
||||
|
||||
import type { Bindings, Entry } from './types';
|
||||
|
||||
const EMBED_MODEL = '@cf/baai/bge-base-en-v1.5'; // 768-dim,與 Vectorize index dimensions=768 對齊
|
||||
// ── 嵌入模型(Arcrun#59:模型應可配置+index 版本化,支援換代重刷)────────────────
|
||||
//
|
||||
// 2026-08-03 換代:`@cf/baai/bge-base-en-v1.5`(768-dim)→ `@cf/baai/bge-m3`(1024-dim)。
|
||||
//
|
||||
// 為什麼換(實測,不是憑感覺):舊模型是**英文模型**,拿來嵌中文等於嵌一堆看不懂的 token。
|
||||
// 用 5 組中文問答測資(每組 1 問 + 2 段相關 + 3 段無關,無關的刻意放同一知識庫裡的其他主題),
|
||||
// 算 margin = min(相關分數) − max(無關分數),margin ≤ 0 代表**排序是錯的**:
|
||||
// @cf/baai/bge-base-en-v1.5 768 排序正確 2/5 平均 margin -0.0413 1660 ms ← 舊
|
||||
// @cf/google/embeddinggemma-300m 768 4/5 +0.1275 1174 ms
|
||||
// @cf/baai/bge-m3 1024 **5/5** **+0.1410** 959 ms ← 新(品質最好且最快)
|
||||
// @cf/qwen/qwen3-embedding-0.6b 1024 4/5 +0.1381 3238 ms
|
||||
// 舊模型最刺眼的一組:問「知識庫問答為什麼要標出處?」→「**會議室預約規則**」0.7789
|
||||
// 竟然高於真正相關的 0.7306。這正是 leo 2026-07-18 回報的「問 RAG 卻引用會議室規範」。
|
||||
//
|
||||
// 🔴 換模型=**必須換 Vectorize index**,兩個理由:
|
||||
// ① 維度不同(768→1024),舊 index 收不進新向量;
|
||||
// ② 就算維度相同也不能沿用——不同模型的向量混在同一個 index,比對出來是垃圾,
|
||||
// 而 Arcrun#58(Vectorize vector delete 未接)代表舊向量**刪不掉**。
|
||||
// ⇒ 開新 index 反而順手繞開 #58:新 index 天生乾淨,舊的整個丟掉。
|
||||
//
|
||||
// 換代步驟(installer 已把新 index 名與維度對齊):建新 index → 重新部署 kbdb(binding 指新 index)
|
||||
// → 打 backfill 的 `reindex=true`(把 embed=1 的既有 entry 全部重嵌)→ 舊 index 可刪。
|
||||
const DEFAULT_EMBED_MODEL = '@cf/baai/bge-m3'; // 1024-dim,與 Vectorize index dimensions=1024 對齊
|
||||
|
||||
/** 實際使用的嵌入模型:env 可覆寫(#59),未設用預設。 */
|
||||
function embedModel(env: Bindings): string {
|
||||
const m = (env.EMBED_MODEL ?? '').trim();
|
||||
return m || DEFAULT_EMBED_MODEL;
|
||||
}
|
||||
|
||||
/** embed 模組是否啟用(binding 都在才算開)。base 一切 embed 動作先過這關。 */
|
||||
export function embedEnabled(env: Bindings): boolean {
|
||||
return !!(env.VECTORIZE && env.AI);
|
||||
}
|
||||
|
||||
/** 一段文字 → 768 維向量(Workers AI bge)。空字串回 null(不 embed)。 */
|
||||
/** 一段文字 → 1024 維向量(Workers AI bge-m3,可由 env.EMBED_MODEL 覆寫)。空字串回 null(不 embed)。 */
|
||||
async function embedText(env: Bindings, text: string): Promise<number[] | null> {
|
||||
const t = (text ?? '').trim();
|
||||
if (!t || !env.AI) return null;
|
||||
const res = (await env.AI.run(EMBED_MODEL, { text: [t] })) as { data: number[][] };
|
||||
const res = (await env.AI.run(embedModel(env), { text: [t] })) as { data: number[][] };
|
||||
return res?.data?.[0] ?? null;
|
||||
}
|
||||
|
||||
@@ -149,7 +177,7 @@ export async function backfillEmbeddings(
|
||||
const embeddable = rows.filter((e) => (e.content ?? '').trim().length > 0);
|
||||
if (embeddable.length > 0 && env.AI && env.VECTORIZE) {
|
||||
const texts = embeddable.map((e) => (e.content ?? '').trim());
|
||||
const out = (await env.AI.run(EMBED_MODEL, { text: texts })) as { data: number[][] };
|
||||
const out = (await env.AI.run(embedModel(env), { text: texts })) as { data: number[][] };
|
||||
const data = out?.data ?? [];
|
||||
const vectors = embeddable
|
||||
.map((e, i) => ({ e, vec: data[i] }))
|
||||
|
||||
@@ -16,6 +16,10 @@ export type Bindings = {
|
||||
// requires them; code checks `if (env.VECTORIZE && env.AI)` before touching embed.
|
||||
VECTORIZE?: VectorizeIndex;
|
||||
AI?: Ai;
|
||||
// 嵌入模型(Arcrun#59)。未設=用 embed.ts 的預設。設成別的模型時,**Vectorize index 的
|
||||
// dimensions 必須跟著對**(維度不合 upsert 會被 CF 拒絕),且換模型必須換 index:
|
||||
// 不同模型的向量不可共存於同一個 index(比對出來是垃圾),詳見 embed.ts 檔頭。
|
||||
EMBED_MODEL?: string;
|
||||
};
|
||||
|
||||
export type EntryType =
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 嵌入模型可配置+換代(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);
|
||||
});
|
||||
});
|
||||
+14
-7
@@ -31,14 +31,21 @@ ENVIRONMENT = "production"
|
||||
|
||||
# ── Optional embed module (issue #7 / SDD T2.4) ────────────────────────────────
|
||||
# Base 預設不開(free-tier 友善)。self-host 開語義查詢時,deploy.ts 偵測 config kbdb_embed:true
|
||||
# → 取消下面兩段註解(注入 active binding)並 `wrangler vectorize create arcrun-kbdb-embed
|
||||
# --dimensions=768 --metric=cosine`(bge-base-en-v1.5 = 768 維)。官方帳號同理由 deploy 注入。
|
||||
# → 取消下面兩段註解(注入 active binding)並 `wrangler vectorize create arcrun-kbdb-embed-m3
|
||||
# --dimensions=1024 --metric=cosine`(**bge-m3 = 1024 維**)。官方帳號同理由 deploy 注入。
|
||||
# 🔴 2026-08-03 換代(leo 拍板,5 組中文測資實證:舊英文模型排序 2/5、margin −0.0413=根本不能用;
|
||||
# bge-m3 5/5、+0.1410、959ms):`bge-base-en-v1.5`(768) → `bge-m3`(1024)。
|
||||
# **換模型必須換 index**:① 維度 768→1024,舊 index 收不進新向量
|
||||
# ② 就算維度相同也不能沿用——不同模型的向量混在同一 index 比對出來是垃圾,
|
||||
# 而 #58(Vectorize vector delete 未接)代表舊向量刪不掉 ⇒ 開新 index 反而順手繞開 #58。
|
||||
# 既有實例遷移:建新 index → 重部署 kbdb(binding 指新 index)
|
||||
# → `POST /embed/backfill {"reindex":true}` 重嵌到 remaining=0 → 舊 index 可刪。
|
||||
# ⚠️ Arcrun#11:光建 index 不夠。要對 owner_id/entry_type/source 下 filter(owner-scoped/類型-scoped 語意查詢),
|
||||
# 必須另建 metadata index,否則帶過濾一律回 0 命中:
|
||||
# wrangler vectorize create-metadata-index arcrun-kbdb-embed --property-name owner_id --type string
|
||||
# wrangler vectorize create-metadata-index arcrun-kbdb-embed --property-name entry_type --type string
|
||||
# wrangler vectorize create-metadata-index arcrun-kbdb-embed --property-name source --type string
|
||||
# wrangler vectorize create-metadata-index arcrun-kbdb-embed --property-name library --type string
|
||||
# wrangler vectorize create-metadata-index arcrun-kbdb-embed-m3 --property-name owner_id --type string
|
||||
# wrangler vectorize create-metadata-index arcrun-kbdb-embed-m3 --property-name entry_type --type string
|
||||
# wrangler vectorize create-metadata-index arcrun-kbdb-embed-m3 --property-name source --type string
|
||||
# wrangler vectorize create-metadata-index arcrun-kbdb-embed-m3 --property-name library --type string
|
||||
# (library=portal-auth P1「庫」filter;upsert 端把未標記正規化成 'general',查詢走 $in)
|
||||
# metadata index 只收「建立後 upsert」的向量 → 既有向量須 `POST /embed/backfill {"reindex":true}` 重推
|
||||
# (建 library index 後同樣要 reindex,否則舊向量帶 library filter 一律 0 命中)。
|
||||
@@ -48,7 +55,7 @@ ENVIRONMENT = "production"
|
||||
#
|
||||
# [[vectorize]]
|
||||
# binding = "VECTORIZE"
|
||||
# index_name = "arcrun-kbdb-embed"
|
||||
# index_name = "arcrun-kbdb-embed-m3" # bge-m3 1024d(2026-08-03 換代)
|
||||
#
|
||||
# [ai]
|
||||
# binding = "AI"
|
||||
|
||||
Reference in New Issue
Block a user