昨天整理的三個維度裡,control policy 問的是由誰決定存什麼、取什麼、丟什麼。今天把這個問題換成可以量的形式:寫一則偏好進去、把 session 結束掉、重開一個新的 session 問它,然後量這則記憶在 system prompt 裡實際佔多少 byte。以下依序釐清三件事:寫入由誰觸發、取回走的是哪一條路徑、一則記憶的固定成本是多少。
hermes memory status 把這一層的組態列在一起:
Memory status
────────────────────────────────────────
Built-in (MEMORY.md / USER.md):
Memory injection: enabled ✓
User profile: enabled ✓
Memory tool: enabled ✓
Provider: (none — built-in only)
內建記憶永遠啟用,外部供應者同時只能啟用一個。供應者的清單有兩個來源,數量差一項:
hermes memory --help 的說明文字:七個供應者status 實際印出來的:八個,多出來的是 supermemory
說明文字與實際安裝的外掛清單相差一項,接下來的量測都停在內建這一層。
實測程序的第一步是一個 one-shot 請求,內容是請它把「所有輸出的日期一律使用 YYYY-MM-DD 格式」記進長期記憶。這個請求沒有指定檔案,結果它選了使用者檔案:
| 檔案 | 狀態 | 大小 |
|---|---|---|
memories/USER.md |
新建 | 69 B |
memories/MEMORY.md |
未建立 | — |
檔案內容是一行純文字,沒有標題、沒有時間戳、沒有 front matter。第二步改請它記一條關於本機服務的操作事實,MEMORY.md 這才被建出來,65 B。
兩個檔案的分工由模型在呼叫工具時挑 target 決定,工具的參數只有兩個值可選,memory 與 user。
記憶工具的 action 列舉只有三個值:
add:附加一則新條目replace:把符合的舊條目換成新內容remove:刪掉符合的條目三個都是寫入側的操作,讀取那一側由注入負責。
第三步重開五個新 session,每次問同一個問題,五次的回答都是 YYYY-MM-DD。這個成功率的來源在原始碼裡寫得很明確,tools/memory_tool.py 的 format_for_system_prompt() 回傳的是 load_from_disk() 當下凍結的快照:
這回傳的是 load_from_disk() 當下捕捉的狀態,而非即時狀態,session 中途的寫入不影響它,這讓 system prompt 在所有回合之間保持穩定,維持前綴快取
兩個推論跟著成立:
注入之前檔案還會過一次威脅樣式掃描。命中的條目在快照裡被換成 [BLOCKED: <檔名> entry contained threat pattern: <ids>. Removed from system prompt.],原始文字留在檔案裡,註解寫明這樣使用者才看得到被污染的條目並自行刪除。
同一個 profile、同一個空的執行目錄,四個狀態各量一次:
| 狀態 | memory | user profile | volatile | system prompt |
|---|---|---|---|---|
| 兩個檔案都不存在 | 0 | 0 | 6,357 | 16,829 |
USER.md 第一則(磁碟 69 B) |
0 | 402 | 6,761 | 17,233 |
加上 MEMORY.md 第一則(磁碟 65 B) |
396 | 402 | 7,159 | 17,631 |
USER.md 第二則(磁碟 45 B) |
396 | 451 | 7,208 | 17,680 |
三次增量分別是 404、398、49 B。第一則貴,第二則之後幾乎只付內容本身。差距來自區塊的外框,_render_block() 的組法是兩條分隔線夾一行標頭:
| 組成 | 字元 | byte |
|---|---|---|
分隔線兩條,各 46 個 ═ |
92 | 276 |
標頭 USER PROFILE (who the user is) [2% — 31/1,375 chars] |
52 | 54 |
| 條目內容 | 31 | 69 |
| 換行三個 | 3 | 3 |
| 合計 | 178 | 402 |
═ 是 U+2550,UTF-8 一個字元 3 byte,光是兩條分隔線就 276 B。開一個記憶檔的固定成本約 330 byte,兩個檔案都開就是兩份。
標頭那段 2% — 31/1,375 chars 是即時算出來的使用率,分母就是這個檔案的字元上限。
| 鍵 | 預設值 | 換算 |
|---|---|---|
memory.memory_char_limit |
2,200 | 註解寫約 800 token |
memory.user_char_limit |
1,375 | 註解寫約 500 token |
memory.nudge_interval |
10 | 每十輪做一次內建記憶回顧 |
memory.write_approval |
False |
核准閘門預設關閉 |
條目之間的分隔符在程式裡是 "\n§\n",三個字元,寫到 Windows 的磁碟上換行變成 CRLF,§ 是 U+00A7 佔 2 byte,一個分隔符實際是 6 byte。前面那個檔案兩則條目 69 加 45 加 6,磁碟上 120 B 對得起來。
加超過上限時 add() 回的是錯誤而非截斷,訊息把現況、超出多少與要做的事一起給出來,大括號的部分在執行時填入實際數字:
Memory at {current}/{limit} chars. Adding this entry ({n} chars) would exceed the limit. Consolidate now: use 'replace' to merge overlapping entries into shorter ones or 'remove' stale or less important entries (see current_entries below), then retry this add — all in this turn
整併的責任交回模型,在同一回合內完成。 write_approval 那個鍵預設是 False,註解點名開啟它是為了擋掉背景自我改進分支寫進來的那些「錯誤假設」條目。
hermes journey --json 把技能與記憶畫成一條時間軸。兩則條目寫完之後它的輸出是兩個節點、零條邊:
memory 1790046649 這台機器上的...
profile 1790046711 使用者要求所有輸出的日期...
profile 1790046712 我的文件一律使用台灣繁體中文。
這些時間戳對得上檔案的修改時間。第一則條目原本記到的是 1790046451,加了第二則之後變成 1790046711,也就是整個檔案被改寫時的 mtime,第二則再加一秒。
這條時間軸看得出記憶長出來的順序,看不出個別條目原本是什麼時候寫下的
stats 那一組數字另外把兩者分開算,memory_nodes 是 2,nodes 與 learned_skills 都是 0,技能與記憶各自計數。
五次取回全中,這個數字量到的是注入這條路徑通不通。整份檔案每一輪都在 system prompt 裡,模型要做的只是把它讀出來,檢索能力在這個設計下留在量測範圍之外。
代價在那三個增量裡。三則條目在磁碟上合計 179 B,兩個區塊在 prompt 裡合計 847 B,外框佔掉其中將近八成。兩個上限換算成 token 約是 1,300,跟工具 schema 的 34,093 B 比起來是個小數字,但它每一輪都在。
時間戳那件事是量到才發現的。原本以為記憶帶著自己的寫入時間,實際上它跟著檔案走,加一則就把前面全部的時間往後推。要知道某條偏好是什麼時候寫下來的,得回去翻 session 紀錄。
記憶檔案保存的是內容,保存不了內容是何時產生的
Curator 這個背景程序實際改了什麼,以及封存與回復留下的稽核軌跡。