本體層
turn 完整地存在保存平面裡,不被任何切割動到。它的 digest 在建立索引之前、兩個不同方案之後、以及每一次檢索之後,都是同一個。
一個能精確保存位元組、又能隨時把任何一段展開回來的封存格式,同時也就描述了一個模型對自己的上下文需要什麼。同一個套件因此帶了一層上下文,透過 MCP 使用:無損地記住、以語義定址、而拿回來的是記錄本身 —— 不是它的摘要。
| 工具 | 它必須撐住的主張 |
|---|---|
context_capture | 逐位元組地存下整份對話記錄,否則就拒絕。一個會砍掉對話前段的上限是錯誤,不是安靜的截斷。 |
context_segment | 在這些 turn 上建立一族索引 —— 而封存檔在前後是位元組完全相同的,這一點是被檢查的,不是被宣稱的。 |
context_segment_export | 要送去做 embedding 的視角,連同必須跟著回來的身分,以及一份會說明自己涵蓋了記錄哪一部分的取樣。 |
context_attach_vectors | 向量進入輔助平面:封存檔「旁邊」的一個附檔,永遠不是它「裡面」的一筆記錄。 |
context_address | 問題進去,(turn, start_byte, end_byte) 出來,而且該 turn 的digest 會重新對照索引當初建立時的值。 |
出自 Neo.K 的同一性微積分:切割 = 索引 —— 一刀下去只是多了一個視角,物件本身完好如初。所以 segment 是輔助平面裡的 (turn, start_byte, end_byte),多個方案可以並存於同一份記錄之上,重新切割不改寫任何東西,而且切割器被允許是錯的。
turn 完整地存在保存平面裡,不被任何切割動到。它的 digest 在建立索引之前、兩個不同方案之後、以及每一次檢索之後,都是同一個。
segment 是指進那個 turn 的原始位元組偏移量。之後更好的切割器只是產生新的一族索引;記錄沒被動過,所以在方案之間做選擇是一次量測,而不是一次遷移。
下面每一個數字都由 bench/context_bench.py 產生並寫進一份 JSON,這個頁面就是從那份 JSON 生成的,並且蓋上了量測時的版本號與語料digest。
6,581 個 turn
16.68 MiB → 11.00 MiB (66%)
無損 —— 對話記錄的每一個位元組都在封存檔裡
61,458 個 segment
中位數 249 B · 覆蓋率 1.0000
沒有任何一個 turn 的任何一個位元組是索引不到的,也沒有任何一個被涵蓋兩次 · 建立索引前後,保存平面的 digest 未變
61,458 × 768
| 大小 | 載入 | |
|---|---|---|
| JSON 十進位陣列 | 932.74 MiB | 28.6 s |
| float32 加一行 JSON 標頭 | 182.95 MiB | 0.13 s |
體積小 5.1 倍 · 載入快 221 倍
183 ms 有 NumPy
13 s 純 Python
NumPy 是選用的,保存平面永遠不需要它。純 Python 這條路只有在自己的推估超過明列的 30 秒預算時才會拒絕,而且它會把推估值寫在拒絕訊息裡 —— 你可以去反對那個估計值,而不是去反對一個常數。
2.54 s 一次完整定址查詢的中位數
5/5 次回傳了經 digest 驗證的精確位元組. 用 64 維的查詢去問 768 維的語料 → INCOMPARABLE: dimensions differs — 64 against 768
標準答案是用一個獨特的錨定字串精確搜尋出來的 —— 然後問題本身被刻意寫成完全避開那個錨。所以標籤來自一個檢索器永遠看不到的比對,而查詢正好就是詞彙比對答不出來的那一類。
| 方案 | segment 數 | p95 | R@1 | R@5 | MRR | 排名中位數 |
|---|---|---|---|---|---|---|
whole-turn-v1 (基線) | 6,581 | +0.443 | 0.17 | 0.42 | 0.280 | 7.5 |
structural-v1 | 18,814 | +0.356 | 0.50 | 0.58 | 0.545 | 2 |
sized-900-v1 (對照組) | 23,036 | +0.361 | 0.58 | 0.75 | 0.656 | 1 |
changepoint-v1 | 61,458 | +0.219 | 0.75 | 1.00 | 0.847 | 1 |
透過 Ollama 的 nomic-embed-text —— 137M 參數、Apache-2.0、768 維,正好是這個向量平面本來就在用的寬度。完全相同的立足點:同一份語料 digest、同樣的查詢、同樣的方案、同樣的預算。雲端 → 本地。
| 方案 | R@1 | MRR | p95 |
|---|---|---|---|
whole-turn-v1 | 0.17 → 0.42 | 0.280 → 0.569 | +0.443 → +0.500 |
structural-v1 | 0.50 → 0.58 | 0.545 → 0.704 | +0.356 → +0.351 |
sized-900-v1 | 0.58 → 0.50 | 0.656 → 0.631 | +0.361 → +0.315 |
changepoint-v1 | 0.75 → 0.75 | 0.847 → 0.861 | +0.219 → +0.203 |
紀錄裡直接寫著的帶型別邊 —— 一個回覆的父節點、一次工具呼叫跟它的結果、同一個檔名被兩邊都提到。每條邊帶的是型別跟依據,永遠不帶分數。對照兩個基準量:隨機配對,以及兩個只是相鄰的回合 —— 後者是對話順序本來就免費送的那一份。
| 關係 | n | 平均餘弦 | 對相鄰的倍數 | 本來就相鄰 |
|---|---|---|---|---|
random (地板) | 7,999 | +0.0074 | 0.04× | — |
adjacent (免費基準) | 6,580 | +0.1850 | 1.00× | — |
mentions-path | 2,069 | +0.4723 | 2.55× | 23.8% |
mentions-path (gap>1) | 1,576 | +0.4473 | 2.42× | — |
tool-result-of | 1,737 | +0.2785 | 1.51× | 90.6% |
tool-result-of (gap>1) | 164 | +0.1324 | 0.72× | — |
replies-to | 5,244 | +0.1974 | 1.07× | 90.6% |
replies-to (gap>1) | 495 | +0.0887 | 0.48× | — |
兩個都是 768 維、但來自不同模型的向量 —— 或同一個模型但經過兩種不同前處理 —— 比對出來會是一個很有自信而毫無意義的數字,而且下游沒有任何東西分辨得出來。模型、版本、維度、投影版本與切段方案必須全部一致,否則答案是 INCOMPARABLE 而不是一個數值。維度寬度不等於身分。
一個會砍掉對話記錄前段的位元組上限會被拒絕。如果是刻意要這麼做,結果會被標記為不完整,並且指名它丟掉的位元組範圍 —— 否則下游的每一個主張,都是建立在一份呼叫者以為是完整的記錄上。
一份只涵蓋索引十分之一的向量語料,一樣會回傳它範圍內最接近的結果,而那個答案跟一次完整搜尋是無法區分的 —— 除非把涵蓋比例講出來。每一次呼叫都會講。
23 個工具,走 stdio。這裡沒有任何東西會去算 embedding:向量來自代理本來就有的模型,而身分會跟著向量一起走,所以本地或瀏覽器端的模型可以取代這裡用的那一個,而不會有任何東西在無聲地跨兩個向量空間做比較。
pip install "mcp>=1.10,<2"
python tools/mcp/anla_mcp.py
python bench/context_bench.py <transcript.jsonl>
python bench/segment_retrieval.py <transcript.jsonl>
檢索那張表需要一個 embedding 模型;這個頁面上其他每一項都可以離線跑。 原始碼採私人存取 · 原始碼採私人存取