A ANLA代理原生無損封裝格式 English
這個專案後來變成的第二件事

讓代理精確地記得自己的歷史

一個能精確保存位元組、又能隨時把任何一段展開回來的封存格式,同時也就描述了一個模型對自己的上下文需要什麼。同一個套件因此帶了一層上下文,透過 MCP 使用:無損地記住、以語義定址、而拿回來的是記錄本身 —— 不是它的摘要。

這個迴圈,以及每一步必須撐住的主張

工具它必須撐住的主張
context_capture逐位元組地存下整份對話記錄,否則就拒絕。一個會砍掉對話前段的上限是錯誤,不是安靜的截斷。
context_segment在這些 turn 上建立一族索引 —— 而封存檔在前後是位元組完全相同的,這一點是被檢查的,不是被宣稱的。
context_segment_export要送去做 embedding 的視角,連同必須跟著回來的身分,以及一份會說明自己涵蓋了記錄哪一部分的取樣。
context_attach_vectors向量進入輔助平面:封存檔「旁邊」的一個附檔,永遠不是它「裡面」的一筆記錄。
context_address問題進去,(turn, start_byte, end_byte) 出來,而且該 turn 的digest 會重新對照索引當初建立時的值。
整個設計立足的那個想法

segment 是索引,永遠不是被保存的新碎片

出自 Neo.K 的同一性微積分:切割 = 索引 —— 一刀下去只是多了一個視角,物件本身完好如初。所以 segment 是輔助平面裡的 (turn, start_byte, end_byte),多個方案可以並存於同一份記錄之上,重新切割不改寫任何東西,而且切割器被允許是錯的。

本體層

turn 完整地存在保存平面裡,不被任何切割動到。它的 digest 在建立索引之前、兩個不同方案之後、以及每一次檢索之後,都是同一個。

呈現層

segment 是指進那個 turn 的原始位元組偏移量。之後更好的切割器只是產生新的一族索引;記錄沒被動過,所以在方案之間做選擇是一次量測,而不是一次遷移。

在這個儲存庫自己的開發記錄上實測

它的代價,以及它回傳什麼

下面每一個數字都由 bench/context_bench.py 產生並寫進一份 JSON,這個頁面就是從那份 JSON 生成的,並且蓋上了量測時的版本號與語料digest。

量測於 2026-08-15 05:12 UTC · 71a719f 38c5455779cbe268 changepoint-v1

記錄

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 MiB28.6 s
float32 加一行 JSON 標頭182.95 MiB0.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

切段到底有沒有用?

十二個有標準答案的查詢,一份釘死的語料

標準答案是用一個獨特的錨定字串精確搜尋出來的 —— 然後問題本身被刻意寫成完全避開那個錨。所以標籤來自一個檢索器永遠看不到的比對,而查詢正好就是詞彙比對答不出來的那一類。

模型: text-embedding-3-small · 768d 6,581 個 turn · 38c5455779cbe268 12 個有標準答案的查詢
方案segment 數 p95R@1R@5MRR 排名中位數
whole-turn-v1 (基線)6,581+0.4430.170.420.2807.5
structural-v118,814+0.3560.500.580.5452
sized-900-v1 (對照組)23,036+0.3610.580.750.6561
changepoint-v161,458+0.2190.751.000.8471

同樣那十二個查詢,跑在一個在這台機器上執行的模型上

透過 Ollama 的 nomic-embed-text —— 137M 參數、Apache-2.0、768 維,正好是這個向量平面本來就在用的寬度。完全相同的立足點:同一份語料 digest、同樣的查詢、同樣的方案、同樣的預算。雲端 → 本地。

ollama:nomic-embed-text:latest768d38c5455779cbe268
方案R@1MRRp95
whole-turn-v10.17 → 0.420.280 → 0.569+0.443 → +0.500
structural-v10.50 → 0.580.545 → 0.704+0.356 → +0.351
sized-900-v10.58 → 0.500.656 → 0.631+0.361 → +0.315
changepoint-v10.75 → 0.750.847 → 0.861+0.219 → +0.203
▸ 在關鍵的那一列兩者打平,而本地模型能做一件雲端做不到的事。 在獲勝的方案上,兩者都達到 Recall@1 0.75 與 Recall@5 1.00,本地模型在 MRR 與擁擠度上還略微領先。它同時能釘住自己的權重:Ollama 會回報 content digest,所以一個查詢只有在權重是同樣的位元組時才與語料可比。雲端模型是一個「名字」,而名字可以在不改變的情況下被重新指向。另外兩件事是單靠雲端那次跑不出來的 —— 切段的收益是「單位」與「模型」的共同性質(雲端 4.5 倍、本地 1.8 倍,終點相同),以及 p95 門檻第二次被推翻:在整個 turn 的那一列,本地模型更擁擠、檢索卻好得多,也就是說門檻量的那個東西,動的方向跟它本來要預測的東西相反。
▸ 對照組贏過了它本來要對照的那個方案。 每 900 位元組切一刀,表現比讀取文件自己的標題、段落分隔與程式碼圍欄還好。所以那些結構並沒有在承載資訊,而讀取它的方案並沒有賺到它比算術多出來的複雜度。真正有效的是在詞彙改變的地方切 —— 三者之中唯一會去看內容的那一個。
當初明訂的 p95 門檻,每一列都沒過,包含冠軍那一列。 它要求置中後的隨機配對 p95 低於 +0.15,而校準它的基線是在只有這份語料三分之一大小時量到的 +0.238 —— 現在那個基線是 +0.443。獲勝的方案把擁擠程度砍了一半,那正是這道門檻真正想要的東西,而門檻照它寫的樣子仍然沒過。這裡和 JSON 裡都記為失敗,因為一個事後被重新解讀成剛好支持結果的門檻,就不是門檻了。
關係邊

這張圖,以及它實際上是什麼

紀錄裡直接寫著的帶型別邊 —— 一個回覆的父節點、一次工具呼叫跟它的結果、同一個檔名被兩邊都提到。每條邊帶的是型別跟依據,永遠不帶分數。對照兩個基準量:隨機配對,以及兩個只是相鄰的回合 —— 後者是對話順序本來就免費送的那一份。

9,050 條邊 ollama:nomic-embed-text:latest · 768d 6,581 個 turn · 38c5455779cbe268
關係n 平均餘弦對相鄰的倍數 本來就相鄰
random (地板)7,999+0.00740.04×
adjacent (免費基準)6,580+0.18501.00×
mentions-path2,069+0.47232.55×23.8%
mentions-path (gap>1)1,576+0.44732.42×
tool-result-of1,737+0.27851.51×90.6%
tool-result-of (gap>1)164+0.13240.72×
replies-to5,244+0.19741.07×90.6%
replies-to (gap>1)495+0.08870.48×
三種裡有兩種,只是換了個名字的「相鄰」。 replies-to 裡有 90.6% 的邊,兩端本來就緊鄰著;而剩下那十分之一的分數還低於純順序 —— +0.089 對上相鄰的 +0.185。tool-result-of 是一模一樣的形狀。只有 mentions-path 撐過這個切分:76% 是遠距離,最遠跨 5,103 個回合,把相鄰的那些拿掉之後依然是相鄰的 2.42 倍。9,050 條邊裡,只有 2,035 條連的是順序沒有已經排在一起的配對。
▸ 在檢索上,它小於這把尺子量得出來的度。 對那十二個標註查詢加上圖的加權,12 個裡動了 3 個 —— 2 個往上,1 個往下 —— MRR +0.056,而一個查詢就是 0.083。β = 0 的對照組完全重現了已發布的基線,所以架構是接對的,答案就只是十二個查詢分不出來。而且它是雙向的:在某一組參數下,這個加權把一個正確答案擠出了前五名。
三種拒絕

真正承重的,是它拒絕回答的那些

01

先確認身分,再談相似度

兩個都是 768 維、但來自不同模型的向量 —— 或同一個模型但經過兩種不同前處理 —— 比對出來會是一個很有自信而毫無意義的數字,而且下游沒有任何東西分辨得出來。模型、版本、維度、投影版本與切段方案必須全部一致,否則答案是 INCOMPARABLE 而不是一個數值。維度寬度不等於身分。

02

一次不會是無損的擷取

一個會砍掉對話記錄前段的位元組上限會被拒絕。如果是刻意要這麼做,結果會被標記為不完整,並且指名它丟掉的位元組範圍 —— 否則下游的每一個主張,都是建立在一份呼叫者以為是完整的記錄上。

03

只涵蓋部分記錄的搜尋

一份只涵蓋索引十分之一的向量語料,一樣會回傳它範圍內最接近的結果,而那個答案跟一次完整搜尋是無法區分的 —— 除非把涵蓋比例講出來。每一次呼叫都會講。

跑跑看

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 模型;這個頁面上其他每一項都可以離線跑。 原始碼採私人存取 · 原始碼採私人存取