iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Claude AI

從 LLM 到 Agent:用 Claude 拆解現代 AI 工程的每一層系列 第 14 篇

讓 Claude 忘記一些事:壓縮是有損的寫入,清除要留一行紀錄

  • 分享至 

  • xImage
  •  

上一篇把記憶接上了:模型開口要,你的程式碼負責存。收尾那句話是記憶越寫越多,窗口跟帳單都不會等你。

今天處理相反的動作:把東西丟掉。先講結論:忘記是一個要設計的動作。 壓縮是一次你沒有親手寫的寫入,清除要留下一行紀錄,而「把最舊的砍掉」是最糟的做法,因為它挑的是唯一一個跟重要性無關的條件。


30 秒實驗

找一段跑了一陣子的 Claude Code 工作階段,前面有你交代過的規則,中間有試過但失敗的做法。

  1. 先寫下三件你希望它之後還知道的事:一條很前面交代的約束(例如「不要動 migration」)、一個它讀過的檔案路徑、一個已經證明行不通的做法。
  2. 直接下 /compact,一件一件問它那三件事。
  3. 下次同樣的情況,改下 /compact 保留:我交代過的約束、試過但失敗的做法與原因,再問一次。

我預期第一次掉最多的是「試過但失敗的做法」:摘要的本能是留結論、省略過程,而失敗的嘗試就是過程。第二次多出來的那一句,就是你的寫入規則。


兩種忘法,加上一種最省事的

面向 壓縮(compaction) 清除 先進先出(first in, first out)
做了什麼 把一段換成摘要 把某一類內容整段拿掉 從最舊的開始砍
誰決定丟什麼 摘要模型 你(按內容種類) 時間
Claude 上的對應 Claude Code 的 /compact、API 的伺服器端壓縮 API 的脈絡編輯(context editing) 聊天介面可以這樣滾動管理
要不要多一次模型呼叫 要 不用 不用

壓縮是一次有損的寫入。 它跟第 12 篇的滾動摘要是同一個動作,差別只在寫的人:你沒交代,就照摘要模型自己的判準。第 12 篇引過 LongMemEval(Wu et al., ICLR 2025):把記憶壓成事實,表現會變差。一段對話被壓過兩次,第一次掉的東西,第二次連「曾經有過」都不會留下。

所以交給 /compact 的那一句,我直接從第 12 篇的寫入規則改過來:

/compact 摘要時一定保留四種東西,每條一行,照原話不要改寫:
1. 約束:我明確說過的規則(包括「不要做什麼」)
2. 結論:已經定案的決定,以及定案的理由
3. 教訓:試過但不行的做法,以及為什麼不行
4. 線索:讀過、改過的檔案路徑與指令,不用內容
其餘的討論過程可以濃縮成一兩句。

「照原話」是為了約束:我最怕摘要把「不要動 migration」寫成「migration 要小心」,前者是規則,後者只剩提醒。第 4 項的理由在下一節的圖裡。不想每次手打,就把這四條寫進 CLAUDE.md 的 # Compact instructions 段落。


/compact 按下去之後,實際發生了什麼

三欄的連線圖:左欄是磁碟上的檔案與資料夾,包括 .claude/settings.json、對話紀錄 .jsonl、CLAUDE.md、.claude/rules、MEMORY.md、~/.claude/plans、專案檔案、SKILL.md;中欄是 /compact 的步驟,依序是觸發、PreCompact hook、摘要請求、摘要、PostCompact hook,另有從磁碟重新載入與 SessionStart hook;右欄上半是壓縮後留在窗口裡的東西,下半是被忘掉的三樣:對話原文、第 6 個之後的檔案、子目錄的 CLAUDE.md 與有 paths: 的規則,都用虛線連過去

這張圖照 Claude Code 的提示詞快取、脈絡窗口與 hooks 三頁官方文件整理(2026-09-28 查),線只表示誰交給誰。左欄是磁碟上的檔案,中欄是 /compact 做的事,右欄是壓縮完窗口裡剩下的。重點有三個:

  • 摘要請求比你想的便宜。 它帶著跟你這段對話一模一樣的系統提示、工具與歷史,最後接一則摘要指示;前綴相同,快取還在就讀快取(第 10 篇講過的那種)。隔了很久才回來壓縮,快取過期,整段歷史會以原價重算一次,這是 /compact 最貴的時候。
  • 右欄上半只有「摘要」是新寫的,其他是從磁碟放回來的:CLAUDE.md、自動記憶、計畫,以及最多 5 個最近改過的檔案。所以真正不能丟的約束,寫在專案根目錄的 CLAUDE.md 比寫在對話裡可靠;第 6 個之後的檔案,摘要沒寫路徑,它就不知道要回去看,這就是上面那段指示第 4 項的理由。
  • 右欄下半是被忘掉的:對話原文只剩摘要,子目錄的 CLAUDE.md 與有 paths: 的規則要等它再讀到相關檔案才回來。

中欄兩個 hook 設在 .claude/settings.json:PreCompact 拿得到你的 custom_instructions 與對話紀錄的 transcript_path,但只能擋(結束碼 2),不能替摘要加東西;PostCompact 拿得到剛寫好的 compact_summary,官方文件舉的用途正是把摘要記下來。後面講「丟掉的要留一行紀錄」,在 Claude Code 裡這就是現成的位置。


忘記也是一項要被量的能力

MemoryAgentBench(Hu, Wang, McAuley, 2025)整理出記憶代理的四項核心能力:精準取回、測試時學習、長程理解,以及選擇性遺忘(selective forgetting),並指出既有基準沒有一個涵蓋全部四項。該忘的沒忘,跟該記的沒記一樣,是量得出來的失敗。

這個系列的評估集就缺這一項。第 4 篇那十題裡,第 5、6 題量的是記不記得,沒有一題在量它會不會忘。假設團隊把規則改成「review 可以寫風格意見,但要標成 nit」,你照第 12 篇把舊約束 DELETE 掉;可是舊的那句早被壓進某一版摘要,還在窗口裡。代理照哪一條做,第 6 題量不出來。


為什麼不能砍最舊的

第 11 篇提過 StreamingLLM(Xiao et al., ICLR 2024):只留最近那一段,模型會壞掉,因為它把很強的注意力放在最前面幾個詞元上,也就是注意力沉降(attention sink)。H2O(Zhang et al., 2023)補上另一半:鍵值快取(KV cache)裡一小部分詞元貢獻了大部分的注意力價值,作者叫它們 Heavy Hitters。兩篇指向同一件事:重要性集中在少數詞元上,新舊不是判斷它的標準。

這兩篇量的是模型內部的鍵值快取,不是你送出去的 messages。 我借的只有結論:連模型自己挑東西丟,都不能照時間順序來。

回到應用這一側,理由更直接:最舊的那一段通常就是任務本身與你交代的約束;時間是唯一跟內容無關的條件;砍掉的東西不留任何痕跡。Claude 的官方文件寫明,claude.ai 這類聊天介面可以用滾動的先進先出管理窗口(2026-09-17 查)。文件說的是「可以」;但如果長聊天裡它不再照你一開始交代的格式回答,這是值得先懷疑的原因。


在 Claude 上,兩個開關各管一種

壓縮:Claude Code 接近上限會自動壓縮,也可以自己下 /compact;要重新開始而不是延續,/clear 不花錢(官方文件,2026-09-17 查)。API 的伺服器端壓縮分兩種(2026-09-28 查):依詞元門檻(compact-2026-01-12,預設 15 萬詞元觸發)與隨需(compact-2026-09-04,你決定什麼時候壓),官方建議能用隨需就用隨需。用門檻版要記得把回應的整個 content 接回 messages,只存文字的話,壓縮區塊會悄悄消失。

清除是 API 的脈絡編輯(2026-09-28 查):不做摘要,直接把舊的工具回傳(tool result)或思考區塊(thinking block)拿掉。

response = client.beta.messages.create(
    model="claude-opus-5",
    max_tokens=16000,
    betas=["context-management-2025-06-27"],
    context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},
    tools=tools,
    messages=messages,
)

clear_tool_uses_20250919 預設在輸入超過 10 萬詞元時,從最舊的開始把工具回傳換成佔位文字,保留最近 3 組;exclude_tools 指定哪些永遠不清。清思考區塊的是 clear_thinking_20251015。兩個都還是 beta,介面可能會改,所以你的做法最好寫成一個改得動的函式。

兩個開關的分工,照丟掉的東西能不能拿回來分:

窗口裡的東西 拿得回來嗎 怎麼處理
工具回傳:讀過的檔案、跑過的指令輸出 重跑一次就好 清除
討論的過程、中間的推理 拿不回來,但細節多半不需要 壓縮,並說要保留什麼
約束、定案的決定、失敗的教訓 拿不回來,而且錯不得 先寫進記憶(上一篇的記憶工具),再讓它被壓
任務本身、系統提示 不該丟 放在系統提示或第一則訊息

能重拿的就清除,拿不回來的才壓縮,錯不得的先寫進記憶。


丟掉的東西,要留一行紀錄

上一篇講過:記憶錯了,它不會告訴你那句話是哪天寫進去的。遺忘更難查:模型突然不知道某件事,你分不出它是從來沒看過,還是看過但被清掉了。

所以我清東西一定留兩份痕跡:窗口裡一行佔位(placeholder),讓模型知道需要時可以重拿;窗口外一行紀錄,讓你查得到清了什麼。

import json

def find_call(messages, tool_use_id):
    for m in messages:
        if m["role"] == "assistant":
            for b in m["content"]:
                if b["type"] == "tool_use" and b["id"] == tool_use_id:
                    return b

def forget_tool_results(messages, keep_last, today, log):
    results = [b for m in messages if m["role"] == "user" and isinstance(m["content"], list)
               for b in m["content"] if b["type"] == "tool_result"]
    for block in results[:max(len(results) - keep_last, 0)]:
        if block["content"].startswith("[已清除"):
            continue  # 清過的不要再清一次
        call = find_call(messages, block["tool_use_id"])
        what = f'{call["name"]} {json.dumps(call["input"], ensure_ascii=False)}'
        log.append(f'{today}\t清除\t{what}\t原長 {len(block["content"])} 字元')
        block["content"] = f"[已清除 {today}:{what},需要時重新呼叫]"
    return messages

拿一段假的除錯對話跑它:代理依序讀了三個檔案,只保留最後一個工具回傳,而且故意呼叫兩次。本機 Python 3.13 的輸出:

[已清除 2026-09-28:read_file {"path": "src/refund.py"},需要時重新呼叫]
[已清除 2026-09-28:read_file {"path": "src/pricing.py"},需要時重新呼叫]
def test_partial_refund():
---
2026-09-28	清除	read_file {"path": "src/refund.py"}	原長 979 字元
2026-09-28	清除	read_file {"path": "src/pricing.py"}	原長 657 字元

上半是窗口裡剩下的,前兩個變成佔位;下半是紀錄檔,第二次呼叫沒有重複記。檔案內容是假資料,長度用字元數;要知道省下多少詞元,用第 11 篇的 count_tokens 量前後各一次。改用脈絡編輯讓 API 替你清,我一樣在自己這邊記一筆:從哪天開始清、清哪一類。


這麼做的代價

第一,每清一次,快取就斷一次。 快取認的是一模一樣的前綴,改了第 3 輪,之後全部重寫。所以累積到一個量再清;脈絡編輯的 clear_at_least 就是為這件事留的。

第二,壓縮多花一次呼叫,而且寫錯了沒人知道。 摘要漏了什麼,你只有在它照著錯的摘要做事時才會發現。

第三,忘記的範圍要自己畫。 平台只給開關,界線是你的。第 12 篇的寫入規則管什麼能進來,這一篇的判準表管什麼能出去,兩份要一起維護。

我現在的習慣是:任務寫在系統提示裡;約束與教訓一出現就寫進記憶;工具回傳累積到一個量就清,清之前先記一筆;真的要壓縮,一定附上「要保留什麼」。


這一篇多了什麼,又多付了什麼

多了什麼能力:你的代理可以跑得比窗口還長,而且丟東西有依據。你分得清壓縮、清除與先進先出,知道 /compact 背後放回了什麼、忘掉了什麼,也有一張按「拿不拿得回來」決定怎麼丟的判準表,和一個清除前先記一筆的函式。

多付了什麼代價:一次次斷掉的快取、壓縮時多花的呼叫,以及一個你必須自己畫的遺忘範圍。摘要寫錯的東西,會變成之後唯一的版本。


下一篇

這一篇處理的是「該忘的,怎麼忘」。反過來,如果記住的那一句本身就是錯的呢?

前面那個例子其實已經露出一角:你把「review 不寫風格意見」刪掉了,舊的那句卻還躲在某一版摘要裡。錯的記憶不只來自你寫錯,也可能是更新時判錯、留下了該刪的那條,甚至是別人跟代理聊天時寫進去的。

第 6 篇對付錯話的辦法是開一個新對話,讓它留在舊窗口裡。記憶打開之後,這條退路就沒了:錯的那一句會跟著你進到每一個新對話,被一次次讀出來當成依據。下一篇把寫入這條線收尾:記錯的東西留下來會怎樣、怎麼發現它,以及整個第四層到底讓你多付了多少。


延伸閱讀


上一篇
記憶不在 Claude 那裡:它只負責開口要,存在哪是你的事
下一篇
記錯比不記更糟:錯的代理記憶會跟著你進每一個新對話
系列文
從 LLM 到 Agent:用 Claude 拆解現代 AI 工程的每一層 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言