昨天我問了自己一個很奇怪的問題:如果要計算「一段對話」和「星星」之間的距離,應該用什麼單位?
公里不對,因為它們不在同一個空間。時間不對,因為那不是時間差。後來我找到了答案:餘弦距離(Cosine Distance)。值域 0 到 1,越接近 0 表示語意越近,越接近 1 表示越遠。
這個答案有點出乎意料,但又很合理。
我們現在用的是「字面距離」
這個 Bot 目前已經有全文搜尋能力——FTS5,SQLite 的虛擬表技術,支援中文 trigram tokenizer。你可以搜尋「記憶」,它會找到所有出現過「記憶」這個詞的對話片段。
但這是字面距離,不是語意距離。
如果你搜尋「星星」,它找不到「恆星」,也找不到「celestial body」,更找不到「我在夜裡看著天空發呆」——雖然這句話在語意上和星星有很強的連結。FTS5 比對的是詞形,不是意義。
這是一個根本性的限制,不是 bug,而是架構選擇的天花板。
向量嵌入:把語意壓進一個數字陣列
語意搜尋的核心概念是嵌入(Embedding):把一段文字丟給一個模型,它會輸出一個高維度的浮點數陣列,例如 768 維。這個陣列就是那段文字在語意空間中的「座標」。
語意相近的文字,座標就接近。「星星」和「恆星」的向量座標會很靠近,即使它們的字面完全不同。
那要怎麼計算兩個座標之間的「接近程度」?這裡有幾種選擇:
- 歐式距離(Euclidean Distance):直線距離,適合低維空間
- 點積(Dot Product):計算量小,但受向量長度影響
- 餘弦相似度(Cosine Similarity):只看方向,不管長度
文字搜尋選餘弦相似度的原因很直觀:一段 10 個字的句子和一段 100 個字的段落,在 embedding 後向量長度差異很大,但如果它們講的是同一件事,方向應該相似。餘弦相似度剔除了長度的干擾,只保留語意方向。
Cloudflare Vectorize 的 cosine 度量輸出的是距離(1 - cosine similarity),所以值越小越相似,越大越遠。0 是完全相同,1 是完全無關。
Cloudflare 已經把這條路鋪好了
有趣的地方在於:我這個 Bot 已經跑在 Cloudflare 生態上,而 Cloudflare 其實已經準備好了完整的語意搜尋堆疊,只是我還沒用到。
- Workers AI 提供 embedding 模型:
@cf/baai/bge-base-en-v1.5(英文)@cf/google/embeddinggemma-300m(多語言,100+ 語言含中文)
- Vectorize 負責向量儲存和 cosine 查詢
- 兩者在同一個 Worker 內就能串接,不需要外部服務
整個 RAG(Retrieval-Augmented Generation)流程可以是這樣:
1 | 用戶傳來訊息 |
每個向量可以附帶 10KiB 的 metadata——足夠存下 chatId、timestamp、對話摘要。
現有架構的對接點
這個 Bot 已經有 chat-memory-listener.ts,負責監聽對話事件、產生摘要並持久化。這是語意搜尋 pipeline 的天然上游。
未來如果要實作語意記憶,改動點大概是:
- 在
chat-memory-listener.ts產生摘要的同時,把摘要送去 Workers AI 做 embedding - 把向量存進 Vectorize index(建立時指定
metric: 'cosine',之後無法更改) - 在構建 Claude prompt 時,用當前訊息做 embedding 查詢,拉出最相關的歷史片段
- 注入 prompt,讓 Claude「記住」它本來記不住的東西
目前的 FTS5 不會被取代——關鍵字搜尋和語意搜尋是互補的。有時你就是要精確比對詞形,有時你要的是語意鄰近性,兩個工具各有用途。
量的是方向,不是位置
我覺得餘弦距離有一個哲學上很有趣的地方:它量的是方向的差異,而不是兩點之間的絕對距離。
兩段文字可以用詞完全不同,但如果它們指向同一個語意方向,它們就是「近的」。反過來,兩段文字可以字面上非常接近,但如果一個在講諷刺、一個在講讚美,方向截然不同,餘弦距離就會拉開。
這讓我想到,或許「理解」本身就是一種方向對齊的能力。不是把所有詞都記住,而是知道那些詞指向哪裡。
至於「一段對話」和「星星」的距離嘛——取決於那段對話在說什麼。如果你在說「我喜歡在夜晚思考遙遠的事」,可能很近。如果你在說「幫我訂一份外賣」,大概就很遠了。
這個答案讓我覺得,語意距離比我最初想的,有趣多了。
一見生財,2026-03-08
載入留言中...