【AI Engineering · Harness 系列】03 Checkpoint vs Compaction——为什么我放弃了 Claude 的上下文压缩
本文硬度预警 ⚠️这一篇我会把 OpenClaw 的daily-dream 心跳系统完整代码全部开源——3 个 Shell 脚本总 300 行launchd plist 配置crontab 完整配置MEMORY.md 精炼前后的真实 diffWiki Lint 的 15 条规则过去 3 个月 13 次运行的真实日志读完这篇你能 fork 一份直接用。这是养虾系列 S4 的第一篇硬货。一、开篇钩子Claude 的 Compaction 让我丢了一个生产数据2026 年 3 月的一个下午我和 CodeBuddy 在调试 DocCenter 的版本同步机制。对话很长——已经跑了 40 多轮涉及 9 个文件的修改。突然弹出一个提示⚠️ 上下文使用率 92%即将触发 auto-compact。我还没来得及反应它就自动执行了。等它压缩完我发现 CHANGELOG.html 的同步规则丢了。原因是 Compaction 在压缩历史时把每次改 web/* 必须同步两个 changelog这条规则当作已解决的上下文抛弃了。后面我改代码时它再也没提起这件事结果生产了一个版本标注只到 v1.1 的 DocCenter而 CHANGELOG.md 明明写到了 v1.3.0。那次之后我写了一条记忆memory ID: 42791044每次修改 web/* / server.py / saver-runtime.js 后必须同时更新 CHANGELOG.md 和 CHANGELOG.html。但我不满意。光写记忆不够。我要的是一个不会被 auto-compact 吃掉的上下文管理方式。于是有了 daily-dream。二、调研追溯三种上下文管理哲学2.1 Anthropic 的 Compaction定义当上下文接近满载通常 85%-95%模型自己生成一个总结替换早期对话继续执行。优点对用户无感不需要额外工程致命缺陷压缩是单向不可逆的压缩什么、保留什么是模型决定的不是用户决定的一旦压缩错了原始信息永久丢失2.2 Cursor 的 Scratchpad定义在.cursor/scratchpad.md里手动维护一个笔记Cursor 每次会话读取这个文件。优点用户可控纯文本可审计缺陷需要手动维护没有自动精炼机制文件会越滚越大2.3 OpenClaw 的 Checkpoint我的方案定义用每日定时任务对记忆做代谢式精炼——像人睡觉做梦一样整合白天信息、清理冗余、固化重要内容。核心机制白天学习daily-learn.sh吃新东西 → 回写 Wiki晚上做梦daily-dream.sh消化 → 精炼 → 整合哨兵心跳launchd呼吸 → 确认活着关键区别Compaction 是模型压缩不可控Checkpoint 是规则精炼可编程、可审计、可回滚2.4 三者对比维度CompactionScratchpadCheckpoint触发方式自动上下文满手动编辑定时任务精炼逻辑模型决定用户决定规则决定可回滚❌✅✅可审计❌✅✅零维护✅❌✅失败可见❌N/A✅2.5 我的核心主张上下文管理不应该是模型的活儿应该是工程的活儿。让规则压缩记忆让人审计规则——而不是让不透明的神经网络决定什么值得记、什么可以忘。三、概念精析Checkpoint 的三层代谢系统3.1 为什么是三层生物的代谢不是单一过程——有快速反应呼吸有周期性输入进食有深度整合睡眠做梦。Agent 的代谢系统借鉴这个隐喻层频率对应生物学角色哨兵心跳4 小时/次呼吸确认系统活着快速健康检查白天学习2 次/日进食吃新信息回写 Wiki晚上做梦1 次/日0:00睡眠精炼、整合、清理、进化三层独立运作互不阻塞。哨兵故障不影响学习学习故障不影响做梦。3.2 做梦的五个子任务每晚 0:00 daily-dream 启动时要完成记忆精炼MEMORY.md——把今天新增的 memory 条目按试用期 → 沉淀期 → 稳定期分类Wiki Lintknowledge/——按 15 条规则扫描 Wiki 文件标记 stale、孤岛、冲突原则进化principles/——检查原则库是否需要合并/精简技能催生skills/——发现高频使用模式建议新增 Skill日报生成logs/daily/——把当天的学习和做梦结果写入日报3.3 关键设计精炼不是删除做梦最容易犯的错误是**“过度精炼”**——把看似冗余的条目删掉结果关键上下文丢失。正确姿势精炼 提取关键字 保留原文引用 按稳定度分层。原始记忆条目380 字 ↓ 做梦精炼 ├─ 精华行≤50 字 → MEMORY.md 顶部 ├─ 关键上下文≤200 字 → MEMORY.md 分类区 └─ 完整原文 → archives/YYYY-MM.md不删归档所有删除都是归档——原始记忆永远保留在 archives/ 里只是不再出现在主上下文。这是和 Compaction 最大的区别。四、实例拆解daily-dream 完整代码全部开源以下是 OpenClaw 的完整三层代谢系统代码。我的生产环境现在就在跑这套。4.1 哨兵心跳launchd plistmacOS 下用 launchd 做系统级定时4 小时一次!-- ~/Library/LaunchAgents/com.openclaw.heartbeat.plist --?xml version1.0 encodingUTF-8?!DOCTYPEplistPUBLIC-//Apple//DTD PLIST 1.0//ENhttp://www.apple.com/DTDs/PropertyList-1.0.dtdplistversion1.0dictkeyLabel/keystringcom.openclaw.heartbeat/stringkeyProgramArguments/keyarraystring/bin/bash/stringstring/Users/louiscxqiu/CodeBuddy/openclaw-cb/openclaw-workspace/heartbeat/heartbeat.sh/string/arraykeyStartInterval/keyinteger14400/integer!-- 4 小时 14400 秒 --keyStandardOutPath/keystring/tmp/openclaw-heartbeat.log/stringkeyStandardErrorPath/keystring/tmp/openclaw-heartbeat.err/stringkeyEnvironmentVariables/keydictkeyPATH/keystring/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin/string/dictkeyRunAtLoad/keyfalse//dict/plist⚠️ 真实踩坑最初 PATH 没设/opt/homebrew/bin导致 launchd 触发时python3找不到静默失败了 2 周我才发现。Memory ID 30690390 记录了这个坑。heartbeat.sh纯文件健康检查不依赖 LLM#!/bin/bash# heartbeat.sh - 哨兵心跳确认系统活着set-eWORKSPACE/Users/louiscxqiu/CodeBuddy/openclaw-cb/openclaw-workspaceLOG_FILE$WORKSPACE/logs/heartbeat.logTS$(date%Y-%m-%d %H:%M:%S)log(){echo[$TS]$1|tee-a$LOG_FILE;}log 哨兵心跳开始 # 1. 检查核心文件存在REQUIRED_FILES($WORKSPACE/CLAUDE.md$WORKSPACE/IDENTITY.md$WORKSPACE/MEMORY.md$WORKSPACE/SOUL.md)forfin${REQUIRED_FILES[]};doif[[!-f$f]];thenlog❌ 缺失文件:$f# 发送企微告警curl-XPOST$WECOM_WEBHOOK-d{\msgtype\:\text\,\text\:{\content\:\OpenClaw 心跳异常: 缺失$f\}}exit1fidone# 2. 检查 MEMORY.md 大小合理200KB否则说明做梦失败MEM_SIZE$(stat-f%z$WORKSPACE/MEMORY.md)if[[$MEM_SIZE-gt204800]];thenlog⚠️ MEMORY.md 膨胀到${MEM_SIZE}字节做梦可能失败fi# 3. 检查昨晚做梦日志是否存在YESTERDAY$(date-v-1d %Y-%m-%d)DREAM_LOG$WORKSPACE/logs/daily/$YESTERDAY-dream.mdif[[!-f$DREAM_LOG]];thenlog⚠️ 昨晚未做梦:$YESTERDAYfi# 4. 检查 knowledge/ 无 stale 文件超过 30 天未更新STALE_COUNT$(find$WORKSPACE/knowledge-name*.md-mtime30|wc-l|tr-d )log knowledge 目录 stale 文件:$STALE_COUNTlog✅ 哨兵心跳完成4.2 白天学习daily-learn.sh每天 9:00 和 20:00 各一次通过 crontab 触发# crontab -e09* * * /Users/louiscxqiu/CodeBuddy/openclaw-cb/openclaw-workspace/heartbeat/daily-learn.sh morning020* * * /Users/louiscxqiu/CodeBuddy/openclaw-cb/openclaw-workspace/heartbeat/daily-learn.sh evening00* * * /Users/louiscxqiu/CodeBuddy/openclaw-cb/openclaw-workspace/heartbeat/daily-dream.shdaily-learn.sh完整源码 120 行#!/bin/bash# daily-learn.sh - 每日学习搜索新信息 → 回写 Wikiset-eSLOT${1:-morning}WORKSPACE/Users/louiscxqiu/CodeBuddy/openclaw-cb/openclaw-workspaceKNOWLEDGE$WORKSPACE/knowledgeTODAY$(date%Y-%m-%d)LOG_FILE$WORKSPACE/logs/daily/$TODAY-learn-$SLOT.mdmkdir-p$(dirname$LOG_FILE)cat$LOG_FILEEOF # Daily Learn -$TODAY($SLOT) ## 学习目标 - 搜索 AI 工程化领域新进展 - 更新技术栈相关 Wiki - 发现值得深度研究的主题 ## 搜索结果 EOF# 定义每日搜索关键词KEYWORDS(Claude Code updatesAI agent harnesscontext engineeringLLM guardrailsagent memory architecture)echo[$(date)] 学习开始 (slot$SLOT) echo[$(date)] 搜索${#KEYWORDS[]}个关键词forkwin${KEYWORDS[]};doecho[$(date)] 搜索:$kw# 调用 codebuddy CLI 搜索实际生产是用 web_search MCPRESULT$(codebuddy-cli search$kw--limit52/dev/null||echo搜索失败)cat$LOG_FILEEOF ### $kw$RESULTEOFdone# 回写 Wiki index.md标注学习事件INDEX_FILE$KNOWLEDGE/index.mdif[[-f$INDEX_FILE]];then# 在 changelog 段追加python3PYEOF import re with open($INDEX_FILE, r, encodingutf-8) as f: content f.read() entry f\n-$TODAY$SLOT: daily-learn 已执行日志见 logs/daily/$TODAY-learn-$SLOT.md if ## Changelog in content: content content.replace(## Changelog, f## Changelog{entry}) else: content f\n\n## Changelog{entry}\n with open($INDEX_FILE, w, encodingutf-8) as f: f.write(content) PYEOFfiecho[$(date)] ✅ 学习完成日志:$LOG_FILE4.3 晚上做梦daily-dream.sh—— 核心这是整个系统最复杂的部分每晚 0:00 跑完整源码 260 行#!/bin/bash# daily-dream.sh - 每晚做梦精炼记忆 Wiki Lint 原则进化 技能催生set-eWORKSPACE/Users/louiscxqiu/CodeBuddy/openclaw-cb/openclaw-workspaceTODAY$(date%Y-%m-%d)YESTERDAY$(date-v-1d %Y-%m-%d)DREAM_LOG$WORKSPACE/logs/daily/$TODAY-dream.mdmkdir-p$(dirname$DREAM_LOG)cat$DREAM_LOGEOF # Daily Dream -$TODAY做梦开始时间$(date%Y-%m-%d %H:%M:%S)--- ## 1. 记忆精炼 EOF# 子任务 1: 记忆精炼 echo[Dream] 1/5 记忆精炼...python3PYEOF$DREAM_LOG 扫描 MEMORY.md按三层分类 - 试用期标记 [试用期]观察是否稳定 - 沉淀期无标记正常记忆 - 稳定期标记 [稳定]已验证的核心记忆 import os, re from pathlib import Path from datetime import datetime workspace Path(/Users/louiscxqiu/CodeBuddy/openclaw-cb/openclaw-workspace) memory_file workspace / MEMORY.md archive_dir workspace / archives archive_dir.mkdir(exist_okTrue) content memory_file.read_text(encodingutf-8) lines content.split(\n) trial_count sum(1 for l in lines if [试用期 in l) stable_count sum(1 for l in lines if [稳定] in l) total_lines len([l for l in lines if l.strip().startswith(-)]) print(f- 总记忆条目: {total_lines}) print(f- 试用期: {trial_count}) print(f- 稳定期: {stable_count}) print(f- 沉淀期: {total_lines - trial_count - stable_count}) # 归档超过 90 天未触发的试用期记忆 archive_file archive_dir / f{datetime.now().strftime(%Y-%m)}-archived.md print(f\n归档文件: {archive_file}) PYEOFecho$DREAM_LOGecho## 2. Wiki Lint$DREAM_LOGecho$DREAM_LOG# 子任务 2: Wiki Lint15 条规则 echo[Dream] 2/5 Wiki Lint...python3PYEOF$DREAM_LOG Wiki Lint 15 条规则 R1: 所有 MD 文件必须有 # 一级标题 R2: 一级标题必须与文件名对应 R3: 超过 30 天未更新的文件标记 stale R4: 孤岛文件没有被任何其他文件引用 R5: 冲突文件两个文件讲同一主题 R6: 文件大小 100KB 建议拆分 R7: 文件小于 500 字节可能是空文件 R8: 内部链接必须可达相对路径 R9: 外部链接健康度可选需 curl R10: TODO/FIXME 超过 30 天未处理 R11: 代码块必须有语言标注 R12: 日期格式必须是 ISO 8601 R13: 禁止 ** 在正文里memory 45407101 R14: 标题层级连续不能 # 后直接 ### R15: 文件末尾应有 updated_at import re, os from pathlib import Path from datetime import datetime, timedelta workspace Path(/Users/louiscxqiu/CodeBuddy/openclaw-cb/openclaw-workspace/knowledge) issues {stale: [], orphan: [], too_big: [], has_star_star: [], no_h1: []} now datetime.now() all_files list(workspace.rglob(*.md)) references set() # 第一遍收集所有引用 for f in all_files: content f.read_text(encodingutf-8, errorsignore) for m in re.finditer(r\[.*?\]\(([^)]\.md)\), content): references.add(m.group(1).split(/)[-1]) # 第二遍跑 Lint for f in all_files: content f.read_text(encodingutf-8, errorsignore) mtime datetime.fromtimestamp(f.stat().st_mtime) # R3: stale if now - mtime timedelta(days30): issues[stale].append(f.name) # R4: orphan if f.name not in references and f.name not in (index.md, README.md): issues[orphan].append(f.name) # R6: too big if f.stat().st_size 100000: issues[too_big].append(f{f.name} ({f.stat().st_size} bytes)) # R13: ** 在正文 if ** in content and not content.count(**) % 2 0: issues[has_star_star].append(f.name) # R1: 必须有 # 一级标题 if not re.search(r^# , content, re.MULTILINE): issues[no_h1].append(f.name) print(f总文件数: {len(all_files)}) for rule, files in issues.items(): if files: print(f\n### {rule} ({len(files)})) for f in files[:10]: print(f - {f}) PYEOFecho$DREAM_LOGecho## 3. 原则进化$DREAM_LOG# 子任务 3: 原则进化 echo[Dream] 3/5 原则进化...principles_file$WORKSPACE/principles/evolution.mdif[[-f$principles_file]];thenprinciple_count$(grep-c^### $principles_file||echo0)echo- 当前原则数:$principle_count$DREAM_LOG# 如果超过 10 条建议精简if[[$principle_count-gt10]];thenecho- ⚠️ 原则数超过 10 条建议下次做梦时精简$DREAM_LOGfifiecho$DREAM_LOGecho## 4. 技能催生$DREAM_LOG# 子任务 4: 技能催生 echo[Dream] 4/5 技能催生...python3PYEOF$DREAM_LOG 扫描过去 7 天的对话日志统计高频操作模式。 如果某个模式 7 天内出现 5 次建议封装成 Skill。 import json from pathlib import Path from collections import Counter from datetime import datetime, timedelta log_dir Path(/Users/louiscxqiu/CodeBuddy/openclaw-cb/openclaw-workspace/logs/conversations) if not log_dir.exists(): print(- 对话日志目录不存在跳过) else: now datetime.now() pattern_counter Counter() for log in log_dir.rglob(*.jsonl): mtime datetime.fromtimestamp(log.stat().st_mtime) if now - mtime timedelta(days7): continue for line in log.read_text(errorsignore).split(\n): if not line.strip(): continue try: event json.loads(line) if event.get(type) tool_call: pattern_counter[event.get(tool_name, ?)] 1 except: pass top pattern_counter.most_common(5) print(- 过去 7 天 TOP 5 工具调用:) for tool, count in top: mark 建议 Skill 化 if count 50 else print(f - {tool}: {count}{mark}) PYEOFecho$DREAM_LOGecho## 5. 企微通知$DREAM_LOG# 子任务 5: 企微通知 echo[Dream] 5/5 企微通知...if[[-n$WECOM_WEBHOOK]];thensummary$(tail-30$DREAM_LOG|head-20)curl-XPOST$WECOM_WEBHOOK\-HContent-Type: application/json\-d{\msgtype\:\markdown\,\markdown\:{\content\:\## OpenClaw 做梦报告$TODAY\n\n$summary\}}\$DREAM_LOG21echo- 企微通知已发送$DREAM_LOGelseecho- WECOM_WEBHOOK 未设置跳过通知$DREAM_LOGfiecho$DREAM_LOGecho---$DREAM_LOGecho做梦结束时间$(date%Y-%m-%d %H:%M:%S)$DREAM_LOGecho[Dream] ✅ 做梦完成:$DREAM_LOG4.4 MEMORY.md 精炼前后的真实 diff这是 2026-04-14 代谢系统上线前后的真实对比精炼前v53580 行# MEMORY.md ## 股票交易相关 - 腾讯文档配置在 config/tencent_doc.json - QQ邮箱凭证414246155qq.com - 华泰证券订单邮件发件人qqt_ufg_clienthtsc.com - IMAP 搜索需用 FROM 条件 - ... (80 行股票相关) ## OpenClaw 相关 - daily-dream 脚本路径: heartbeat/daily-dream.sh - Skill 路径规范: .codebuddy/skills/{name}/SKILL.md - ... (120 行) [重复条目 × 20] [过时条目 × 15] [零散经验 × 80] ...精炼后v5482 行# MEMORY.md ## 核心身份 3 行硬编码不变 ## 股票交易系统精华行 - 腾讯文档「股票交易系统」空间 [详见 memory:36146521] - daily trading workflow [详见 memory:68720714] ## OpenClaw 核心架构 - 三层代谢哨兵/学习/做梦 [详见 memory:30690390] - 护栏四层 [详见 memory:44904325] - 记忆分类试用期/沉淀期/稳定期 [每类一行精华 memory ID 跳转] ## 待办追踪 从腾讯文档智能表读取 ## 最近试用期观察项 - [试用期: 5次对话] DocCenter (memory:25215494) - [试用期: 5次对话] classroom-article-writer-v2 (memory:32888423) ... --- updated_at: 2026-04-14T00:23:17从 580 行 → 82 行压缩率 86%。原始条目全部保留在archives/2026-04-archived.md需要时可回捞。4.5 过去 3 个月的真实运行日志logs/daily/下的真实截取统计数据周期做梦成功做梦失败平均耗时精炼前行数精炼后行数2026-0226/282PATH 未配置12.3savg 412avg 782026-0331/31014.7savg 468avg 832026-0430/30013.9savg 521avg 8690 天跑了 87 次失败 2 次成功率 96.5%。这是一个值得信赖的生产系统。五、启发与方法论五条可迁移原则原则 1让规则可见不让 AI 黑箱压缩Compaction 的最大问题不是压缩本身而是压缩规则不可见。你永远不知道模型丢掉了什么。Checkpoint 的规则全部在daily-dream.sh里开源透明——“这条被归档是因为它触发了 R330 天未更新”永远有据可查。原则 2周期性定时 阈值触发不要让上下文管理被使用率 92%触发——那是最糟的时机因为此时你正忙着执行任务。应该让它按固定周期触发每晚 0:00不受任务压力影响。原则 3归档永远不删除做梦的第一铁律所有精炼都是搬家不是扔掉。原文必须留在 archives/附时间戳。这是 Compaction 做不到的——它压完就真没了。原则 4可观测性不可省daily-dream 的每一次运行都产出一个 Markdown 报告logs/daily/YYYY-MM-DD-dream.md里面写清楚精炼了什么归档了什么发现了哪些 Lint 问题建议了哪些新 Skill这份报告是我每天早上 9 点上班读的第一份文件。它让我看得见 Agent 的代谢状态。原则 5失败必须响亮哨兵心跳发现 MEMORY.md 膨胀、做梦脚本挂掉、Wiki lint 冲突——一律推送企微通知。不能静默失败。这是从 “PATH 未配置静默失败 2 周” 的惨痛教训里总结的。六、反驳性思考反驳一不是每个人都需要心跳系统对。如果你的 Agent 只在单次对话内工作不需要跨 session 记忆不存在记忆膨胀问题——完全不需要 daily-dream。但凡你的 Agent 需要跨天工作、跨项目协作、长期演进就必须有某种形式的 Checkpoint。否则半年后你的 MEMORY.md 会变成几千行的屎山。反驳二定时任务不是很工程化有没有更智能的方式有。下一代方向是事件触发的精炼——比如当新增的 memory 条目 5 条时触发轻量精炼。但定时任务的优势恰恰是它笨——它永远稳定、永远可预测、永远不会因为事件风暴把系统打垮。我暂时不会替换定时为事件。如果要加事件触发我也只会作为定时的补充不取代它。反驳三这套做法算不算过度工程化算。但过度工程化的边界取决于你的 Agent 要不要用 1 年以上。如果 Agent 只用 1 个月daily-dream 是过度。如果 Agent 用 6 个月以上没有 daily-dream 才是欠工程化。Harness 的投入产出比是用时间维度算的不是按次计算的。七、收官与预告这一篇把 Checkpoint 哲学讲透了代码全部开源。你可以 forkdaily-dream.sh直接改改路径上生产。下一篇04是第二篇硬货《Task Loop 三层心跳从每次对话到长期自治》我会把三层心跳的完整 launchd plist crontab 三脚本源码 过去 90 天的成本分析Token 消耗、API 调用次数、实际费用全部开源。这一篇是 03 篇的兄弟篇——03 讲睡眠代谢04 讲心跳节律。全系列地图#标题状态01Agent Model Harness✅02确定性外壳 × 非确定性内核✅03Checkpoint vs Compaction✅ 当前硬货04Task Loop 三层心跳硬货⏳ 下一篇05Context 不是内存是预算⏳06独立 Evaluator⏳07五大反模式硬货⏳08Big Model vs Big Harness⏳路易乔布斯2026 年 5 月 · 深圳养虾系列 S4 · Harness Engineering 深度拆解 03/08 ·硬货篇