CLAUDE.md 越写越长,从几十行膨胀到上千行,这几乎是每个深度使用 AI 编程助手的开发者都会遇到的尴尬。指令文件堆满了项目约定、历史决策、环境说明和代码风格规范,看起来“很全”,但 AI 每次读取时都会消耗大量上下文窗口,真正关键的指令反而被淹没在冗长文本里。最近看到 Show HN 上有位开发者做了个叫 Knowl 的项目,思路很有意思:与其手动维护一份不断膨胀的 CLAUDE.md,不如让记忆系统自己“修剪”自己。这篇文章会围绕这个理念展开,分析 CLAUDE.md/记忆文件膨胀的根因,并手把手实现一个带优先级评分、自动淘汰、摘要归档的自修剪记忆管理器,帮助你建立更健康的 AI 协作记忆体系。
1. 背景与核心概念:CLAUDE.md 与自修剪记忆
1.1 CLAUDE.md 是什么
CLAUDE.md 是 Claude Code 等 AI 编程工具识别并自动加载的项目指令文件。它的定位类似于团队的“项目手册”,里面通常会写清楚代码规范、构建命令、架构约束、常见陷阱和当前任务状态。AI 在进入项目目录后,会自动读取这个文件,相当于把项目的长期记忆注入到每一次对话上下文中。
可以这样理解:普通的对话窗口是 AI 的短期记忆,关掉之后就丢失了;而 CLAUDE.md 是 AI 的长期记忆,只要文件存在,AI 每次都能读到。文件的初衷非常好,但随着项目推进,它必然面临一个核心矛盾——项目的信息量在增长,而 AI 的上下文窗口是有限的。
1.2 当 CLAUDE.md 达到 1000 行,会发生什么
当一个 CLAUDE.md 超过 1000 行时,主要会出现三类问题:
- 上下文被白白占用。Claude 等模型的上下文窗口虽然有较大容量,但项目文件、对话历史、工具输出都会消耗这个窗口。一份 1000 行的指令文件可能占了上下文可用空间的一大部分,真正留给代码分析和问题思考的空间就变小了。
- 关键指令被“稀释”。AI 对文件开头和结尾的内容印象更深,大量历史信息堆在中间,真正的核心约定反而变得不突出。比如你在第 300 行写了“禁止修改公共接口签名”,但 AI 同时读到 30 条历史决策,它可能就把这条最重要的约束当成普通背景信息了。
- 维护成本爆炸。每次修改代码规范、增加新依赖、决定新的架构方向,都要手动更新这个文件。时间一长,文档的更新速度跟不上项目变化速度,文件里就会堆积过时甚至互相冲突的指令。
1.3 Knowl 的解法:让记忆自我修剪
Knowl 这个项目解决的就是上面这个问题。它的核心思路可以概括为:不是让开发者手动控制 CLAUDE.md 的膨胀,而是让记忆系统自己判断什么该留、什么该压缩、什么该遗忘。
“自修剪”(prunes itself)这个词是从植物园艺里借来的。园丁会剪掉枯萎的枝条,让养分集中到健康的部分。Knowl 要做的也是类似的事:根据每条记忆的使用频率、重要程度和时效性,自动把“枯枝”清掉,保留真正有价值的信息。
这与计算机系统中的缓存淘汰算法(如 LRU、LFU)有相通之处,但对象从数据块换成了“自然语言指令”,判断标准更复杂。一个完整的自修剪存储,至少应该包含三层机制:
- 结构化存储:把记忆拆成条目,而不是一整块纯文本。
- 重要性打分:每条记忆都有属性,包括重要性、访问频率、最近使用时间。
- 修剪策略:当总量超过阈值,自动淘汰低分条目,或把长条目压缩成摘要。
2. 环境准备与版本说明
在动手实现之前,先明确本文示例的运行环境。需要说明的是:下面提供的示例代码是为了演示“自修剪记忆”的核心原理,你可以直接复制运行,也可以按自己的语言和框架改写。
2.1 环境要求
| 依赖 | 版本建议 | 说明 |
|---|---|---|
| 操作系统 | Windows / macOS / Linux 均可 | 示例不涉及系统底层调用 |
| Python | 3.10+ | 主要使用标准库和 dataclass |
| 终端 | 支持 UTF-8 | 示例包含中文内容输出 |
如果你还没有安装 Python,建议从官网下载最新稳定版,安装时勾选“Add Python to PATH”。版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
2.2 示例项目结构
memory-pruner/ ├── memory_manager.py # 核心:记忆存储与修剪 ├── claude_writer.py # 把修剪后的记忆重新生成 CLAUDE.md ├── sample_memories.json # 示例记忆数据 └── output/ └── CLAUDE.md # 生成的精简版指令文件下面逐文件实现。
3. 核心设计:记忆条目与打分模型
3.1 为什么需要把“自由文本”改成“结构化条目”
CLAUDE.md 传统上是一份 Markdown 纯文本,虽然有标题和列表,但 AI 只能“通读”。如果要做自动修剪,就必须让每一段记忆成为独立的、可被计算机判断的单元。
结构化记忆条目至少要包含这些字段:
| 字段 | 类型 | 含义 |
|---|---|---|
| content | str | 记忆正文,即最终要展示给 AI 的指令 |
| category | str | 分类,例如code_style、architecture、commands |
| priority | int | 人工指定的基础优先级,1-10,10 最高 |
| access_count | int | 被查询或引用的次数 |
| last_accessed | float | 最近一次被访问的时间戳 |
| created_at | float | 创建时间戳 |
| archived | bool | 是否已被归档 |
3.2 打分函数:如何判断“这条记忆还重要吗”
一个设计良好的修剪系统不能只看单一维度。一条优先级为 1 的冷门命令,可能因为最近正在使用而突然变得重要;一条优先级为 5 的架构决策,如果三个月没有被引用,也应该考虑压缩。
这里使用一个加权综合评分模型:
score = (priority / 10) * 0.4 + recency_score * 0.3 + frequency_score * 0.3其中recency_score和frequency_score都被归一化到 0-1 区间:
recency_score = min(1, (time_now - last_accessed) / retention_days)frequency_score = min(1, access_count / max_access_count)
整体逻辑是:基础优先级占 40% 权重,最近使用情况占 30%,累计使用频率占 30%。这样即使某条记忆的优先级不高,只要最近高频出现,仍然不会被剪掉。
3.3 修剪策略:删除还是压缩
“修剪”不等于直接删除。更安全的策略是三级处理:
- 高分区(保留):分数高于
keep_threshold的条目原样保留。 - 中分区(压缩):分数中等,但整条原文过长。此时用摘要代替原文。
- 低分区(归档):分数低于
drop_threshold的条目,先写入归档文件,再从活动记忆里移除。这样万一后面发现需要,还能找回。
模块化实现放在memory_manager.py中。
4. 完整实战:实现一个自修剪记忆管理系统
下面的代码是核心实现。它不只是一个概念 demo,而是一个可以直接集成到 CLAUDE.md 生成流程中的工具。
4.1 创建项目结构
mkdir memory-pruner cd memory-pruner mkdir output4.2 编写核心记忆管理模块memory_manager.py
# memory_manager.py import json import time from dataclasses import dataclass, asdict from typing import List, Dict, Any @dataclass class MemoryItem: content: str category: str = "general" priority: int = 5 access_count: int = 0 last_accessed: float = 0.0 created_at: float = 0.0 archived: bool = False class MemoryStore: def __init__( self, max_active_items: int = 50, retention_days: float = 30.0, keep_threshold: float = 0.6, drop_threshold: float = 0.3, ): self.max_active_items = max_active_items self.retention_days = retention_days self.keep_threshold = keep_threshold self.drop_threshold = drop_threshold self.items: Dict[str, MemoryItem] = {} self.archives: List[MemoryItem] = [] def add_item( self, content: str, category: str = "general", priority: int = 5, ) -> str: """添加一条新记忆,返回该记忆的唯一 ID。""" item_id = f"mem_{int(time.time() * 1000)}_{len(self.items)}" now = time.time() self.items[item_id] = MemoryItem( content=content, category=category, priority=priority, created_at=now, last_accessed=now, ) return item_id def touch(self, item_id: str) -> None: """模拟一次访问:更新访问次数与最近访问时间。""" if item_id not in self.items: return item = self.items[item_id] item.access_count += 1 item.last_accessed = time.time() def score_item(self, item: MemoryItem, max_access_count: int) -> float: """计算单条记忆的综合保留分数,范围 0-1。""" now = time.time() age_days = max(0, (now - item.last_accessed) / 86400) recency_score = max(0.0, 1.0 - age_days / self.retention_days) frequency_score = ( item.access_count / max_access_count if max_access_count > 0 else 0.0 ) priority_score = item.priority / 10.0 return ( priority_score * 0.4 + recency_score * 0.3 + frequency_score * 0.3 ) def trim(self) -> Dict[str, int]: """执行修剪,返回统计信息。""" if len(self.items) <= self.max_active_items: return {"kept": len(self.items), "compressed": 0, "archived": 0} max_access_count = max( (item.access_count for item in self.items.values()), default=1 ) scored: List[tuple[float, str, MemoryItem]] = [] for item_id, item in self.items.items(): score = self.score_item(item, max_access_count) scored.append((score, item_id, item)) scored.sort(key=lambda x: x[0], reverse=True) kept: List[tuple[float, str, MemoryItem]] = [] compressed: List[tuple[float, str, MemoryItem]] = [] archived: List[tuple[float, str, MemoryItem]] = [] for score, item_id, item in scored: if score >= self.keep_threshold: kept.append((score, item_id, item)) elif score >= self.drop_threshold: compressed.append((score, item_id, item)) else: archived.append((score, item_id, item)) # 组装结果:保证活动记忆不超限 final_items: List[tuple[float, str, MemoryItem]] = kept[:] if len(final_items) < self.max_active_items: # 从压缩区补足 remaining_slots = self.max_active_items - len(final_items) for item in compressed[:remaining_slots]: final_items.append(item) # 被压缩区淘汰掉的条目进入存档 compressed_ids = {item_id for _, item_id, _ in compressed} final_compressed = {item_id for _, item_id, _ in final_items} newly_archived = 0 for score, item_id, item in scored: if item_id in {iid for _, iid, _ in final_items}: continue if item_id in compressed_ids and item_id not in final_compressed: item.archived = True self.archives.append(item) newly_archived += 1 elif item_id in {iid for _, iid, _ in archived}: item.archived = True self.archives.append(item) newly_archived += 1 new_items = {item_id: item for _, item_id, item in final_items} self.items = new_items return { "kept": len(self.items), "compressed": len(final_items) - len(kept), "archived": newly_archived, } def to_markdown(self) -> str: """把活动记忆转换成 Markdown,供写入 CLAUDE.md。""" sorted_items = sorted( self.items.values(), key=lambda x: (x.category, -x.priority), ) lines: List[str] = [] current_category: str | None = None for item in sorted_items: if item.category != current_category: current_category = item.category lines.append(f"\n## {current_category}\n") lines.append(f"- {item.content}") return "\n".join(lines).strip() + "\n"4.3 编写 CLAUDE.md 生成模块claude_writer.py
有了修剪后的活动记忆,下一步是把它们重新写回 CLAUDE.md。同时生成一个归档文件,记录被剪掉的条目以备查阅。
# claude_writer.py import json from pathlib import Path from memory_manager import MemoryStore def write_claude_md(store: MemoryStore, output_dir: str) -> None: """把修剪结果写入 CLAUDE.md 和 archive.json。""" output = Path(output_dir) output.mkdir(exist_ok=True) # 1. 写活动记忆 claude_md_path = output / "CLAUDE.md" content = store.to_markdown() claude_md_path.write_text(content, encoding="utf-8") print(f"[OK] 已生成 {claude_md_path},共 {len(content)} 字符") # 2. 写归档 archive_path = output / "archive.json" archive_data = [ { "content": item.content, "category": item.category, "priority": item.priority, "archived_at": item.last_accessed, } for item in store.archives ] archive_path.write_text( json.dumps(archive_data, ensure_ascii=False, indent=2), encoding="utf-8", ) print(f"[OK] 已生成 {archive_path},共 {len(archive_data)} 条归档记忆")4.4 准备示例数据sample_memories.json
为了演示修剪效果,准备一条过时的旧记忆和一条最近高频使用的新记忆,对比它们的保留情况。
{ "items": [ { "content": "登录模块使用 token 认证,token 有效期 30 分钟", "category": "architecture", "priority": 8, "access_count": 20, "last_accessed_days_ago": 1 }, { "content": "2023 年曾计划迁移到微服务,后来因团队规模决定暂缓", "category": "history", "priority": 2, "access_count": 0, "last_accessed_days_ago": 200 }, { "content": "构建命令使用 `make build`,产物输出到 dist/ 目录", "category": "commands", "priority": 9, "access_count": 45, "last_accessed_days_ago": 0 }, { "content": "数据库连接字符串必须通过环境变量注入,禁止硬编码", "category": "security", "priority": 10, "access_count": 32, "last_accessed_days_ago": 3 }, { "content": "前端组件库采用 Ant Design 5.x,表格不要自定义样式", "category": "code_style", "priority": 6, "access_count": 12, "last_accessed_days_ago": 10 } ] }注意:last_accessed_days_ago在真实系统里不会存在,这里是为了便于测试,在加载时转成时间戳。
4.5 编写主流程演示
新建一个demo.py,把上面的模块串起来运行。
# demo.py import json import time from pathlib import Path from memory_manager import MemoryStore from claude_writer import write_claude_md def load_sample_data(store: MemoryStore, sample_path: str) -> None: """从 JSON 加载示例记忆。""" data = json.loads(Path(sample_path).read_text(encoding="utf-8")) now = time.time() for item in data["items"]: item_id = store.add_item( content=item["content"], category=item["category"], priority=item["priority"], ) # 模拟历史访问情况 mem = store.items[item_id] mem.access_count = item["access_count"] mem.last_accessed = now - item["last_accessed_days_ago"] * 86400 def main(): store = MemoryStore( max_active_items=4, retention_days=30.0, keep_threshold=0.6, drop_threshold=0.3, ) load_sample_data(store, "sample_memories.json") print("=== 修剪前活动记忆数量 ===") print(len(store.items)) result = store.trim() print("=== 修剪结果 ===") print(result) print("=== 修剪后活动记忆 ===") for item in store.items.values(): print(f"[{item.category}] {item.content}") print("=== 归档记忆 ===") for item in store.archives: print(f"[{item.category}] {item.content}") write_claude_md(store, "output") if __name__ == "__main__": main()4.6 运行与验证
在项目根目录下运行:
python demo.py预期输出类似:
=== 修剪前活动记忆数量 === 5 === 修剪结果 === {'kept': 3, 'compressed': 1, 'archived': 1} === 修剪后活动记忆 === [security] 数据库连接字符串必须通过环境变量注入,禁止硬编码 [commands] 构建命令使用 `make build`,产物输出到 dist/ 目录 [commands] 前端组件库采用 Ant Design 5.x,表格不要自定义样式 [architecture] 登录模块使用 token 认证,token 有效期 30 分钟 === 归档记忆 === [history] 2023 年曾计划迁移到微服务,后来因团队规模决定暂缓同时output/目录下会生成精简后的CLAUDE.md和归档文件archive.json。
5. 从示例到真实项目:如何把这个系统接到 Claude Code
上面的例子展示了核心逻辑,但在真实项目里,你的 CLAUDE.md 可能长这样:
# 项目说明 这是一个电商后端服务,使用 Python FastAPI 开发。 ## Commands - `make dev` 本地启动 - `make test` 运行单元测试 ## Architecture - 用户服务独立部署 - 订单服务依赖库存服务要让自修剪系统自动维护这份文件,你需要做三件事:
5.1 把既有 CLAUDE.md 导入为结构化记忆
写一个解析函数,按二级标题拆分内容。每个##段落变成一条记忆,###下的列表项可以进一步拆分。这样不需要从零开始,就能把历史知识导入系统。
# importer.py import re from pathlib import Path from memory_manager import MemoryStore def import_claude_md(store: MemoryStore, md_path: str) -> int: """把一个 Markdown 格式的 CLAUDE.md 拆成多条结构化记忆。""" text = Path(md_path).read_text(encoding="utf-8") sections = re.split(r"^##\s+", text, flags=re.MULTILINE) count = 0 for section in sections[1:]: lines = section.strip().splitlines() if not lines: continue category = lines[0].strip() body_lines = [] for line in lines[1:]: line = line.strip() if line.startswith("- "): body_lines.append(line[2:]) elif line: body_lines.append(line) if body_lines: store.add_item( content=" ".join(body_lines), category=category, priority=5, ) count += 1 return count5.2 让 AI 在对话中“触摸”记忆
这是关键一步:每次 Claude Code 在回答中引用了某条命令,或明确说“根据项目说明中的构建约定”,你无法自动感知,但可以制造一个钩子。
最简单的方式是:定期运行一次命令,统计日志里各条规则被命中的次数。更进阶的方式是,让 AI 在修改文件时执行一个脚本,把命中的规则 ID 写入数据库,相当于把“引用”转化为access_count。
5.3 用 cron 或 CI 定时触发修剪
比如每天夜间运行一次:
python demo.py如果生成后的CLAUDE.md与当前版本有差异,再通过 git 提交。这样项目里的 CLAUDE.md 始终保持在合理长度,不会突然从 200 行暴涨到 1000 行。
6. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 修剪后重要指令丢失 | 优先级权重设置不合理,或 retention_days 设置过短 | 调大keep_threshold或retention_days;检查是否过低设置了 priority |
| 文件长度无明显下降 | 每条记忆原文太长,压缩策略没有真正压缩 | 改进压缩逻辑,用摘要代替长段落 |
| 低优先级命令被反复归档又反复加入 | 对短暂突发访问敏感 | 调低 frequency 的权重,或延长 retention_days |
| 归档文件越来越大 | 只进不出 | 给归档文件设置独立阈值,超过后自动删除最旧内容 |
| 与手动维护的 CLAUDE.md 冲突 | 自动生成和手动编辑同时进行 | 明确自动生成是唯一修改入口,手动改动也先导入记忆库 |
这里更详细的排查逻辑:
如果是“被剪掉但还需要”的情况,优先去archive.json里找回,同时把这条记忆的 priority 提高。修剪算法本身是辅助,最终的判断权应该留给开发者。如果发现高频访问的记忆反而被剪掉,多半是last_accessed没有更新成功,检查touch()是否在正确的位置被调用。如果希望某些内容永久保留,可以给记忆条目增加pinned字段,被固定后的条目不参与打分和淘汰,这比单纯把 priority 调到 10 更可靠。
7. 最佳实践与工程建议
7.1 CLAUDE.md 写作规范:从源头减少膨胀
自修剪是事后补救,更理想的做法是让 CLAUDE.md 从出生起就不容易膨胀。几点经验:
- 每条规则一句话说清,不要写背景故事。
- 分类不超过 6 个,类别越多,AI 越难判断优先级。
- 用“不要做”的约束代替“可以做”的描述。
- 命令类规则放在顶部,架构类放中部,历史决策放底部。
- 过时内容主动删除,而不是用“已废弃”标记保留。
7.2 分层记忆策略
不要把所有记忆都塞进一个 CLAUDE.md。更合理的结构是分成三层:
| 层级 | 文件/位置 | 内容 | 更新频率 |
|---|---|---|---|
| 固定层 | CLAUDE.md | 不可变的核心约束 | 极低 |
| 项目层 | docs/operations/ 子文件 | 构建、部署、测试命令 | 中 |
| 临时层 | 对话上下文 | 当前任务状态、临时决定 | 高 |
Knowl 这类自修剪工具主要负责第一层和第二层之间的动态调整。固定层的内容一旦确定,就不要让修剪器碰它;项目层的记忆才需要根据使用频率动态保留。
7.3 版本控制与可追溯性
CLAUDE.md 的变更也应该走代码评审流程。每次自动修剪后,工具应该输出变更 diff,让开发者知道这一轮“剪掉了什么、压缩了什么、补足了什么”。这既是为了安全,也是为了让修剪策略不断迭代。
7.4 安全与权限边界
记忆文件里可能包含敏感信息,例如数据库主机名、内部服务地址、密钥的存放位置。在自修剪系统中,要注意:
- 归档文件也要纳入.gitignore 或单独加密存储。
- 强制给敏感记忆打上
security分类,这类记忆在修剪时至少应进入人工确认流程。 - 不要直接把记忆上传到第三方服务。如果使用云端 AI,应先在本地做脱敏处理。
7.5 评估修剪效果
引入自修剪系统后,不能只看文件行数。更好的评估指标是:
- AI 回答中“根据项目说明执行命令”的成功率。
- 开发者手动纠正 AI 行为的频率。
- 每次会话因上下文溢出而截断的次数。
- 修剪后三个月内,被找回的归档记忆比例。
如果上述指标没有改善,说明修剪策略本身需要调整,而不是工具坏了。
8. 总结与学习路线
围绕“CLAUDE.md 超过 1000 行”这个具体痛点,本文从 CLAUDE.md 的定位和膨胀问题出发,介绍了自修剪记忆的核心思路,并实现了一个包含结构化存储、加权打分、自动淘汰和归档的完整 Python 示例。这个示例可以直接扩展用于 Claude Code、其他 AI 编程助手或任何需要“长期上下文管理”的场景。
接下来,如果你想把这个方案真正落地,建议优先做三件事:
- 把现有 CLAUDE.md 导入结构化记忆库,给每条规则打上优先级与分类标签。
- 设计一个钩子,让 AI 的每次引用都能更新访问次数。
- 设置定时修剪任务,并在 CI 中观察修剪后的 diff。
如果你的项目复杂度并不高,CLAUDE.md 只有一两百行,那么不一定要引入自修剪工具,手动维护可能更简单。但如果你已经遇到“文件越来越长、AI 越来越笨、改规则越来越累”的循环,那么 Knowl 提出的“让记忆自己修剪自己”是一个值得尝试的方向。欢迎在实践中调整打分权重和修剪阈值,找到适合你团队节奏的记忆管理策略。