一、定位
1.1 为什么需要 Memory?
一个 Coding Agent 每轮对话都是独立的------LLM 调用结束后,上下文就消失了。下次启动新 session,Agent 对之前的项目一无所知:
Session 1: 用户说"用 FastAPI 做后端"
Session 2: 用户说"加个接口" → Agent 不知道后端用什么框架
Session 3: 用户说"修复之前那个 bug" → Agent 完全不知道什么 bug
Memory 系统解决的就是跨 session 知识保留------像一个海马体,把短期记忆转化为长期记忆。
补充:对话历史存在哪里? Memory 系统不存储原始对话。对话历史分两处:①
~/.mini-code/history.json------用户的输入命令历史(类似 shell 的.bash_history),上限 200 条;② Session Memory------上下文压缩时SessionMemoryCompactEngine将当前会话的关键信息摘要化后存入 LOCAL scope(不是原始对话,是压缩后的摘要)。本项目不像 Claude Code 那样将完整对话 JSON 持久化到 session 文件。
1.2 一句话定位
Memory 是 MiniCode 的跨对话记忆系统------在每次任务前后,自动从历史记忆中检索相关内容注入到 LLM 的 system prompt 中,并在任务结束后把新经验写入记忆库,形成"使用→反馈→积累"的闭环。
1.3 核心设计:三层存储 + 四层分级
┌─────────────────────────────────────────────┐
│ Memory 系统总览 │
│ │
│ 三层存储(物理隔离) │
│ ┌──────────┐ ~/.mini-code/memory/ │
│ │ USER │ 跨项目共享(你的编码习惯) │
│ ├──────────┤ .mini-code-memory/ │
│ │ PROJECT │ 项目内共享(团队可提交 git) │
│ ├──────────┤ .mini-code-memory-local/ │
│ │ LOCAL │ 本地私用(不提交 git) │
│ └──────────┘ │
│ │
│ 四层分级(时间维度) │
│ WORKING → SHORT_TERM → LONG_TERM → ARCHIVAL │
│ 当前会话 < 7 天 < 30 天 永久压缩 │
└─────────────────────────────────────────────┘
1.4 文件地图
minicode/
├── memory.py (1986行) 核心层:数据类型、BM25搜索、MemoryManager
├── memory_pipeline.py (570行) 门面:read/inject/write/maintain/feedback
├── memory_injector.py (484行) PID控制注入:自适应上下文感知
├── memory_reranker.py (346行) LLM精选:BM25结果二次过滤
├── memory_curator_agent.py(438行) 后台管家:去重/验证/合并/分层
├── working_memory.py (267行) 工作记忆保护:压缩时保留关键信息
├── timeline_memory.py (1324行) 时间线记忆:状态/事件记录
└── vector_memory.py (221行) 向量搜索:TF-IDF + 可选SBERT
1.5 为什么不用数据库?
这是一个有意的 MVP 设计选择,四个原因:
| 原因 | 说明 |
|---|---|
| 零依赖 | JSON 文件读写是 Python 标准库覆盖的,不引入 SQLite/Redis 等额外依赖 |
| 数据量小 | 每个 scope 上限 200 条 / 25KB,三个 scope 总共 600 条 / 75KB,内存索引足够 |
| 人可读 | MEMORY.md 可在 IDE 里直接打开看/改,团队可通过 git diff 审查记忆变更 |
| Git 友好 | PROJECT scope 的 MEMORY.md 可入 git,团队成员 clone 后自动获得项目记忆 |
如果记忆量到几千条级别,JSON 文件确实会成为瓶颈,届时需要迁移到嵌入式数据库(如 SQLite)。
1.6 一句话回答每个文件"为什么存在"
| 文件 | 一句话 |
|---|---|
memory.py |
提供记忆的存、取、搜能力,是数据底座 |
memory_pipeline.py |
把读写维护串成统一入口,外部只需调 4 个方法 |
memory_injector.py |
根据上下文压力自适应控制注入记忆的量和粒度 |
memory_reranker.py |
BM25 搜出 15 条,LLM 精选出 3-5 条真正相关的 |
memory_curator_agent.py |
后台去重、合并、验证、分层,防止记忆膨胀腐烂 |
working_memory.py |
上下文压缩时保护当前任务的关键信息不被吞掉 |
timeline_memory.py |
从对话历史中提取状态快照,支持"上次改了什么"类查询 |
vector_memory.py |
BM25 关键词搜不到的,用语义向量搜 |
二、核心数据模型(memory.py)
2.1 MemoryScope --- 三层物理存储
class MemoryScope(str, Enum):
USER = "user" # ~/.mini-code/memory/ 跨项目共享
PROJECT = "project" # .mini-code-memory/ 项目共享(可入 git)
LOCAL = "local" # .mini-code-memory-local/ 本地私用(不入 git)
为什么存在? 隔离不同粒度的记忆。USER 存你个人的编码偏好("我喜欢用 async/await"),PROJECT 存项目约定("这个项目用 FastAPI + SQLAlchemy"),LOCAL 存本机临时笔记("上次调试到这里卡住了")。
跨项目共享 :USER scope 存储在 ~/.mini-code/memory/------这个路径与当前工作目录无关,所有项目共享。在项目 A 记录的 "我喜欢用 async/await 风格",切换到项目 B 后仍然会被搜到并注入。
输入/输出:枚举值。被 MemoryManager 映射到不同的文件路径。
交互:被 MemoryManager、MemoryPipeline、MemoryInjector 使用。
2.2 MemoryTier --- 四层时间分级
class MemoryTier(str, Enum):
WORKING = "working" # 当前会话产生的,全量保留
SHORT_TERM = "short_term" # 最近 7 天,全量保留
LONG_TERM = "long_term" # < 30 天,已合并压缩
ARCHIVAL = "archival" # 30 天以上,高度摘要化
为什么存在? 记忆数量会随时间膨胀。如果不分层,搜索越来越慢,而且 3 个月前的调试笔记和昨天的架构决策混在一起,检索质量下降。分层实现了**"越久远的记忆越压缩"**。
升级路径 (promote_memories()):
WORKING ──(session结束)──→ SHORT_TERM
SHORT_TERM ──(usage≥5 + age>7d)──→ LONG_TERM
LONG_TERM ──(30天未访问)──→ ARCHIVAL(内容被摘要化)
ARCHIVAL ──(7天内重新被访问)──→ SHORT_TERM(复活)
2.3 MemoryEntry --- 一条记忆的 12 个字段
@dataclass
class MemoryEntry:
id: str # 唯一标识,格式为 "{scope}-{时间戳}-{当前条目数}",如 "project-1722000000-3"
scope: MemoryScope # 属于哪层存储
category: str # 分类:architecture/convention/decision/pattern
content: str # 记忆正文
created_at: float # 创建时间戳
updated_at: float # 更新时间戳
tags: list[str] # 标签,如 ["fastapi", "auth"]
usage_count: int # 被引用的次数(影响搜索排名)
domains: list[str] # 领域分类,如 ["backend", "database"]
tier: MemoryTier # 当前分层
last_accessed: float # 最后访问时间
related_to: list[str]# 关联的其他记忆 ID(图结构)
id 的真实格式 :不是内容的 hash。源码 memory.py:1194:
entry_id = f"{scope.value}-{int(time.time())}-{len(self.memories[scope].entries)}"
# 例如:project-1722000000-3
这意味着同一条内容写入两次会有两个不同 ID------去重靠的是后台 _archive_duplicates() 中的 Jaccard 相似度检测,而非写入时幂等。
usage_count 的积累方式 :每次 MemoryFile.search() 返回结果时,top-10 的条目 usage_count += 1(memory.py:883-884)。此外 feedback() 成功后额外 +2。所以 §2.2 中 usage>=5 的条件意味着该记忆被至少引用 5 次------说明它确实常用。
related_to 的图结构:就是一个无向图的邻接表。
# memory.py:1855-1875
def link_memories(self, similarity_threshold=0.4):
for i, a in enumerate(entries):
for j, b in enumerate(entries):
if jaccard(a.content, b.content) >= 0.4:
a.related_to.append(b.id)
b.related_to.append(a.id) # 双向边
- 节点是 MemoryEntry,边条件是 Jaccard ≥ 0.4
- 遍历时用 BFS:
get_linked_memories(entry_id, depth=1)按深度遍历 - 激活扩散(
_spread_activation)沿着图传播一层,decay=0.5(邻居相关性只保留 50%,防止无限传播)
每条记忆不仅有内容本身,还记录了分类、标签、领域、使用频次、时间戳。这些元数据不是装饰------每一项都参与搜索排序。usage_count 高的排在前面(越常用越重要),updated_at 近的加分(越新鲜越相关)。related_to 构建了一个记忆关联图,检索到一条时,它的邻居记忆也会被顺带激活。
2.4 MemoryFile --- 单层记忆的索引容器
@dataclass
class MemoryFile:
scope: MemoryScope
entries: list[MemoryEntry] # 原始列表
_id_index: dict[str, MemoryEntry] # ID 索引 → O(1) 查找
_tag_index: dict[str, set] # 标签索引 → O(1) 按标签查
_category_index: dict[str, list] # 分类索引 → O(1) 按分类查
_tokens_cache: dict[str, list] # 分词缓存 → 避免重复分词
max_entries: int = 200 # Claude Code 限制
max_size_bytes: int = 25 * 1024 # 25KB 限制
为什么存在? MemoryFile 是 MemoryManager 和磁盘之间的缓存层。三个索引让查找不需要每次都遍历整个列表。容量限制(200 条 / 25KB)防止记忆文件膨胀到影响 LLM 上下文------超过限制时自动删除最旧的条目。
_tokens_cache 分词缓存 :_tokenize() 不是简单的 str.split()------它涉及正则匹配英文词 + CJK 单字 + CJK bigram。BM25 搜索时要对所有候选条目分词,200 条每次全部分词开销不小。缓存后只有新条目/修改过的条目需要重新分词。
两层缓存:
- Entry 级 :
MemoryEntry._cached_tokens------每条记忆分词一次后缓存,内容修改后通过invalidate_tokens()清除 - File 级 :
MemoryFile._tokens_cache------按 entry id 存储分词结果
核心方法:
search(query)--- BM25 搜索(详见第三章)add_entry(entry)--- 添加并更新三个索引delete_entry(id)--- 删除并从所有索引中移除format_as_markdown()--- 输出为 MEMORY.md 格式(人可读)
2.5 MemoryManager --- 全局管理者
class MemoryManager:
memories: dict[MemoryScope, MemoryFile] # 三个 scope 各有一个 MemoryFile
paths: MemoryPaths # 三个 scope 的文件路径
为什么存在? 将三个 scope 的操作统一管理------跨 scope 搜索、批量写入、完整性检查。
六大职责:
| 职责 | 方法 | 说明 |
|---|---|---|
| CRUD | add_entry(), update_entry(), delete_entry() |
基本的增删改 |
| 搜索 | search(query, scope, limit) |
跨 scope 搜索,BM25 + 子串 + 标签 + 领域 |
| 注入 | get_relevant_context(max_entries, max_tokens) |
格式化为 prompt 可用的 markdown |
| 持久化 | _save_scope() |
原子写入 (先写临时文件,再 os.replace) |
| 自愈 | check_integrity(), _recover_scope() |
加载时自动检测并修复损坏数据 |
| 维护 | compress_scope(), promote_memories(), link_memories(), decay_memories() |
后台优化 |
MemoryManager 是所有记忆操作的唯一入口。它在启动时从磁盘加载三个 scope 的 MEMORY.md,构建内存索引;在每次修改后原子写回磁盘------先写临时文件再 os.replace,保证写入过程中断电也不会损坏数据。启动时还会自动做完整性检查和自愈------如果检测到重复 ID 或空内容,自动修复。
三、搜索与检索
3.1 为什么需要搜索?
当 Agent 接到任务 "add login endpoint" 时,Memory 系统需要从几百条记忆中找出和这个任务最相关的 3-5 条。不能全部注入(会撑爆上下文),也不能随机选(毫无帮助)。
3.2 搜索评分公式
搜索不是简单的关键词匹配。MemoryFile.search() 的评分由 5 项组成:
总分 = 0.7 × (BM25 + 子串分 + 标签分) + 0.3 × 领域分 + 使用频次加分 + 新近度加分
各项的含义:
| 评分项 | 计算方式 | 作用 |
|---|---|---|
| BM25 | 标准 Okapi BM25,k1=1.5, b=0.75 | 核心关键词相关性(TF-IDF 改进版,非语义) |
| 子串匹配 | 完整匹配 +2.0,部分匹配 +1.0 | 精确命中优先 |
| 标签匹配 | 精确匹配 +5.0,部分匹配 +1.5 | 标签是人工标注的强信号 |
| 领域 Jaccard | ` | entry.domains ∩ active_domains |
| 使用频次 | log(1 + usage_count) × 0.3 |
越常用越靠前 |
| 新近度 | 1 / (1 + age_hours/24) × 0.5 |
越新鲜越靠前 |
3.3 BM25 公式
# BM25 参数
_BM25_K1 = 1.5 # term frequency 饱和参数
_BM25_B = 0.75 # 文档长度归一化参数
# 核心公式
score(q,d) = Σ IDF(qi) × (tf(qi,d) × (k1 + 1)) / (tf(qi,d) + k1(1 - b + b·|d|/avgdl))
物理直觉:
k1=1.5控制 term frequency 的饱和速度。一个词出现 1 次和出现 5 次有关,但出现 50 次不会线性加 50 倍b=0.75控制文档长度的影响。b=1 意味着长文档被严重惩罚(因为 match 可能只是碰巧),b=0 完全不管长度。0.75 是经典的折中值- 分子
tf × (k1+1)随 tf 增长,但分母里的tf + k1(...)让增长逐渐饱和
3.4 查询扩展 --- 中英跨语言搜索
def _expand_query_terms(terms, active_domains):
"""把中文术语扩展为对应的英文,反之亦然"""
这是一个硬编码的双语词典(约 420 行),覆盖 frontend、backend、database、devops、testing 五个领域。例如:
"component"→["组件", "widget", "control", "element"]"缓存"→["cache", "redis"]"migration"→["迁移", "schema change", "ddl", "alembic", "flyway"]
为什么存在? 用户的输入可能是中文("加一个缓存层"),但记忆内容可能是英文("Added Redis cache layer")。不用向量模型的情况下,这是处理跨语言搜索的最轻量方案。
3.5 _tokenize 分词原理
"分词" 就是把一段自然语言文本拆成最小的语义单元。英文天然有空格分隔("fix login bug" → ["fix", "login", "bug"]),但中文没有空格("修复登录bug"),需要特殊处理。
_WORD_RE = re.compile(r'[a-zA-Z0-9]+|[一-鿿]') # 英文词 + 单个汉字
_CJK_BIGRAM_RE = re.compile(r'[一-鿿]{2}') # 连续两个汉字
def _tokenize(text):
tokens = [w.lower() for w in _WORD_RE.findall(text)] # 第一步
cjk_bigrams = [m.lower() for m in _CJK_BIGRAM_RE.findall(text)] # 第二步
return tokens + cjk_bigrams
对 "修复login Bug" 的处理过程:
第一步(_WORD_RE):["修", "复", "login", "bug"]
- 正则分别匹配到两个单汉字和两个英文词
第二步(_CJK_BIGRAM_RE):["修复"]
- 找连续两个汉字的组合
最终 tokens:["修", "复", "login", "bug", "修复"]
为什么这样做? 如果只拆单字,搜索"修复"时匹配不到"修复完成"中的内容(因为单字太散了);加上 bigram "修复"后,就有了双字级别的匹配能力。这是不引入 jieba 等第三方分词库的最轻量方案。
CJK = Chinese, Japanese, Korean(中日韩统一表意文字,Unicode 范围
一-鿿)。之所以特别处理这个范围的字符,是因为这三种语言都不像英文那样用空格分词。
与 Claude Code/Codex 的分词方式对比
Claude Code/Codex 根本不需要做 MiniCode 这种分词 。它们直接调 tiktoken(OpenAI)或 Anthropic 的 tokenizer:
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4")
tokens = enc.encode("修复login Bug") # → [token_id_1, token_id_2, ...]
这些 tokenizer 用的是 BPE(Byte Pair Encoding) ,有一个几万到十几万个 subword 的庞大词表,训练自海量语料。中文词、成语、常见搭配都已在词表里,不需要手动处理 CJK bigram------BPE 已经把 "人工智能" 当成一个 token 了。
但 MiniCode 不能 这样做。BM25 需要显式的词项 来计算 TF/IDF------BPE token ID 对 BM25 没有意义(你不可能在文本里搜 token_id_38472)。所以 MiniCode 的分词是为 BM25 搜索服务的轻量方案,和 Claude Code 为 LLM API 服务的 tokenize 是两种完全不同的东西。
为什么只做 bigram,不做 trigram/4-gram?
核心原因:收益递减 + 组合爆炸。
| gram | 组合数(5000 常用汉字) | 实际有用的比例 |
|---|---|---|
| unigram | 5,000 | 几乎 100%,但信息量太低 |
| bigram | ~2,500 万(理论上) | 实际出现的远少于此,大量是有效词组 |
| trigram | ~1,250 亿 | 绝大多数无意义("画蛇添"只在一个成语中浮现) |
| 4-gram | ~625 万亿 | 基本只有成语和固定搭配有意义 |
现代汉语的词长分布 :约 70% 的词是 1-2 个字,双字词占绝对主导。三字词("计算机""数据库")和四字成语("画蛇添足")当然存在,但它们由更小的有意义的 bigram 组成。
以 "人工智能" 为例:
bigram 切分: "人工" + "工智" + "智能"
↑ ↑
有效词组 有效词组
"工智" 虽然无意义,但 "人工" 和 "智能" 都是高频有效词组。
搜索 "人工智能" 时,query 侧也会切出 "人工" 和 "智能",
两个 bigram 同时命中 → 高分匹配。
如果加了 trigram:切出 "人工智" + "工智能"------两个都无意义,反而增加了索引体积和搜索开销。bigram 是性价比最高的点------刚好踩在"信息量够用"和"噪音可控"的交界处。
成语怎么办?
靠 unigram(单字)+ 部分匹配兜底。"画蛇添足" 的处理:
unigram: "画" "蛇" "添" "足"
bigram: "画蛇" "蛇添" "添足"
当用户搜索 "画蛇添足" 时,query 侧同样切出这些 token。如果记忆里有一条 "不要画蛇添足,功能已经够用了":
- "画蛇" 命中 → 有一点信号
- "画"、"蛇"、"添"、"足" 四个单字各自独立命中 → 额外信号
- 整体 BM25 分数可能不如完整匹配高,但比完全不命中好
这是轻量方案的天然局限。不用 jieba 等专业分词库就无法完美处理成语。但实际场景中,中文 Coding 对话里的成语/俗语出现频率远低于技术术语("登录""缓存""部署"这类双字词占主导),所以 bigram 方案在日常使用中够用。
一句话总结:MiniCode 的分词是为 BM25 关键词搜索服务的轻量方案,设计目标不是"完美分词",而是"在不引入第三方库的前提下,让中文搜索能工作"。bigram 是性能-效果的平衡点,成语是已知的边缘 case。
3.6 与 Claude Code/Codex 方式的差异
一个常见的误解是:顶级 Agent 每次调用 LLM 都把全部对话历史塞进去。实际上 MiniCode 也是这样做的 ------while step < max_steps 的每一轮,current_messages(所有历史消息)都传给 LLM。Memory 系统做的事情不是"替代"全量历史,而是补充跨 session 的知识。
两者的分工:
| 维度 | 对话历史 (current_messages) | Memory 系统 |
|---|---|---|
| 时间范围 | 当前 session 内 | 跨 session、跨项目 |
| 内容 | 用户说了什么、LLM 回了什么 | 项目架构、编码规范、历史决策 |
| 传递方式 | 直接放在 messages list 里 | 检索后追加到 system prompt |
当上下文接近上限时,MiniCode 的 ContextCybernetics 触发压缩------这和 Claude Code 的 "xx% Context Used" 提示后自动摘要化是同一套逻辑。
四、MemoryPipeline --- 统一门面
4.1 设计哲学:1 类 4 方法
所有记忆操作只通过 MemoryPipeline 这一个入口。外部调用者不需要知道 BM25、Reranker、Curator 的存在。
class MemoryPipeline:
def read(...) # 搜索检索
def inject(...) # 搜索 + 注入 prompt
def write(...) # 任务结束后写入新记忆
def maintain(...)# 后台维护(去重/合并/分层)
def feedback(...)# 闭环:任务成败→调整记忆权重
4.2 read() --- 五步检索管线
输入: task_description="add login endpoint", current_files=["src/auth.py"]
输出: [{id, content, domain, relevance, source}, ...]
流程:
Step 1: 域分类 → 根据 current_files + task 推断 active_domains
Step 2: BM25 搜索 → 跨三个 scope 搜索,查询扩展 + 子串 + 标签 + 领域
Step 3: 向量搜索 → 可选,TF-IDF 向量 + cosine similarity,RRF 融合
Step 4: LLM 精选 → Reranker 从 top-15 精选 3-5 条,拒绝跨域噪声
Step 5: 激活扩散 → 沿 related_to 图扩散一层(depth=1, decay=0.5)
为什么不是简单调 search()? 单纯的 BM25 搜 15 条返回------前端相关的记忆可能在 back-end 任务中也被搜出来(因为 "router" 这个词两边都有)。Reranker(LLM 精选)解决了这个问题,激活扩散解决了"搜到一条,但漏了它关联的另一条"的问题。
decay(衰减)的含义 :在激活扩散中,
decay=0.5意味着通过related_to图从一条记忆扩散到邻居时,邻居的相关性分数只保留原来的 50%。扩散一层就衰减一半,防止无限传播导致所有记忆都被激活。
4.3 inject() --- 注入 prompt
def inject(task_description, current_files, messages, context_usage=0.5):
"""向 system prompt 尾部追加相关记忆"""
自适应冷却 (_adaptive_cooldown):
cooldown = 30.0 * (1.0 - context_usage) # 基础 30 秒
# context_usage=0.9 → cooldown=3s(压力大,需要更频繁注入记忆来帮助决策)
# context_usage=0.3 → cooldown=21s(压力小,少注入,节省 token)
注入形式:在 system message 的 content 末尾追加:
## Relevant Project Memory
- [architecture] 本项目使用 FastAPI + SQLAlchemy + PostgreSQL
- [convention] 所有 API 路由统一放在 src/routes/ 下
- [decision] 认证方案选择了 JWT + OAuth2
4.4 记忆注入的生命周期
注入发生在主循环之前,一次管整个 turn,不是每次 LLM 调用都注入。
# agent_loop.py:722-727(主循环 while 之前)
if orch and task:
current_messages = orch.inject_memories(task_desc, current_messages)
# 此时 system message 已被修改
# 之后 while step < max_steps 中每次 _model_next(model, messages) 都带上了这份修改过的 system message
system message 的存活时间 :一次 run_agent_turn() 调用期间。current_messages 是 Python list,system message 是它的第一个元素。上下文压缩操作的是 user/assistant 消息对(把早期消息摘要化),system message 不被压缩影响。但下次 turn 会重新构建新的 system message,记忆需要重新注入。
注意:system message 只会被 MemoryManager 管理,不会被 MemoryManager 持久化到磁盘。它只存在于内存中,存活一次 turn。
4.5 write() --- 任务结束后写入
def write(task_description, execution_trace):
"""通过 ReflectionEngine 反思任务,提取 TaskContext,写入记忆"""
流程: 1.ReflectionEngine.reflect(task, trace) → 从执行轨迹中提取文件、库、领域标签
2.置信度 >= min_confidence 才写入(防止写入低质量记忆)
3.MemoryManager.add_entry(scope=PROJECT, ...) → 持久化到 .mini-code-memory/
4.6 feedback() --- 质量闭环
def feedback(task_success, injected_memory_ids):
if task_success:
entry.usage_count += 2 # 正面反馈:该记忆确实有用
else:
entry.usage_count = max(0, entry.usage_count - 1) # 轻微衰减
为什么重要? 这就是记忆系统的"学习"机制------如果注入的记忆帮助任务成功了,下次它们的排名更高;如果任务失败(记忆可能误导了 Agent),它们的影响力下降。
已知局限 :
feedback()方法虽已实现,但当前agent_loop.py中并未调用它。usage_count只在搜索时自动+1(memory.py:883-884),任务成功/失败后的显式+2/-1未连线。这是反馈闭环中的一个缺口------和之前控制论模块中的 FeedforwardController 不调 setpoint、解耦矩阵没闭环类似。
4.7 maintain() --- 后台维护
每 ~10 个任务触发一次,委托给 MemoryCuratorAgent。详见第七章。
五、PID 控制注入(memory_injector.py)
5.1 为什么需要控制注入?
最简单的做法是:每次任务开始,搜 top-5 记忆,注入 prompt。但问题在于:
- 上下文已经很满(90%)还注 5 条全量记忆 → 加剧 overflow
- 搜出来的记忆质量很低(relevance < 0.3)也强行注入 → 噪声误导
MemoryInjectionController 做的事:根据当前状态,自适应决定注入多少、怎么注。
context_usage >= 90% 为什么还会出现? 前面控制论部分讲过上下文压力达阈值自动压缩,但压缩不是瞬时的:1.压缩触发到实际生效有几轮延迟;2.压缩后水位降到 60-70%,随着新消息进来再次上涨;3.LLM 一次性产生大量输出(如生成 3000 行代码),水位可能从 70% 瞬间跳到 95%,跳过了 85% 的压缩触发线。此时
MemoryInjectionController.decide()看到 90%+,直接返回 NONE------不注入任何记忆,保命优先。压缩全流程复习:检测(PredictiveController/ContextCybernetics 检测 usage > 85%)→ 决策(CyberneticFeedbackLoop.run_cycle 决定策略)→ 执行(Full Compact,不调 LLM,纯规则)→ 记录(record usage_before/after,更新 _compaction_history)→ 反馈(结果通过 to_system_state → FeedbackController 影响 oscillation_index)。
5.2 四种注入模式
class MemoryInjectionMode(str, Enum):
NONE = "none" # 不注入(上下文爆满时保命)
SUMMARY = "summary" # 只注 1-2 条摘要版(<= 80 tokens/条)
STANDARD = "standard"# 正常注 5 条(<= 200 tokens/条)
STRONG = "strong" # 强注入 7 条(降低相关性门槛)
5.3 决策逻辑
def decide(signal: MemoryInjectionSignal, base_max_memories, base_min_relevance, base_max_tokens):
# 规则 1:上下文压力 >= 90% → NONE 模式,保命
if signal.context_usage >= 0.90:
return NONE
# 规则 2:上下文压力 >= 75% → SUMMARY 模式,节流
if signal.context_usage >= 0.75:
return SUMMARY (max 2 条, <= 80 tokens/条, min_relevance >= 0.55)
# 规则 3:检索质量 < 0.35 → 收紧(少注、高门槛)
if signal.retrieval_quality < 0.35:
max_memories -= 2, min_relevance += 0.20
# 规则 4:检索质量高 + 上下文不紧张 → STRONG 模式
if signal.retrieval_quality >= 0.75 and signal.context_usage < 0.65:
return STRONG
# 规则 5:最近刚刚失败 → 多注(需要更多上下文帮助)
if signal.recent_failure:
max_memories += 1, min_relevance -= 0.10
# 规则 6:用户纠正过 Agent → 少注(记忆可能有误导)
if signal.user_correction_count > 0:
max_memories -= corrections, min_relevance += 0.10 * corrections
5.4 MemoryInjector --- 执行注入
class MemoryInjector:
def inject_for_task(task_description, current_files, signal):
# 1. 调用 Controller.decide() 决定注入模式
# 2. 冷却检查:同一任务 30 秒内不重复注入
# 3. 缓存检查:相同 task_hash 直接返回缓存结果
# 4. 跨 scope 搜索 → BM25 → Reranker(LLM精选)→ 去重
# 5. 补充标签匹配的结果
# 6. 如果 Reranker 有总结,插入为第一条
交互对象:
- 上游:被 MemoryPipeline.inject() 调用
- 下游:调用 MemoryManager.search() 获取候选,调用 MemoryReranker.curate() 精选
- 并行:与 MemoryInjectionController 协作,一个决策/一个执行
六、LLM 精选器(memory_reranker.py)
6.1 为什么 BM25 不够?
BM25 是关键词匹配。对于 "add login form",它能把包含 "login"、"form"、"auth" 的记忆搜出来。但它分不清:
- "Login form uses React Hook Form" ← 相关(前端表单)
- "Database login credentials stored in .env" ← 也匹配 "login",但不相关(后端配置)
当 BM25 返回的 15 条中混入了不相干的记忆,直接注入会浪费 token + 误导 Agent。
6.2 Reranker 做什么
class MemoryReranker:
def curate(candidates, task_description, active_domains, current_files):
# → 返回 RerankResult(selected_ids, rejected, conflicts, summary)
输入 :BM25 返回的 top-15 + 任务描述 + 当前文件 输出:精选的 3-5 条 ID + 拒绝列表 + 冲突检测 + 策展总结
核心机制: 1.构建 prompt:列出 15 条候选记忆(ID、领域、标签、使用次数、内容摘要)
2.调用轻量 LLM
3.通过 prompt 文本 要求 LLM 返回 JSON(未使用 function calling / tool_use):
Return ONLY valid JSON:
{"selected": ["id1"], "rejected": [{"id":"id2","reason":"..."}], ...}
4.解析 JSON,过滤记忆
为什么不用 Cross-Encoder 模型? Cross-Encoder(如 BAAI/bge-reranker-v2-m3)专为相关性打分训练,比通用 LLM 更擅长此任务且成本更低。但当前选择 LLM 的原因是:LLM 不仅排序,还能做冲突检测和生成 summary------这两项纯 reranker 模型做不到。一个更好的方案是混合使用:Cross-Encoder 打分 + LLM 只做冲突检测和 summary,但当前未采用。
为什么不用 function calling 约束输出? 当前只用 prompt 文本约束,存在 LLM 返回格式不符合预期的风险。用 Anthropic 的 tool_use 或 OpenAI 的 function calling 可以保证返回结构化 JSON,不需要正则解析。但 function calling 在不同 provider/模型上的支持程度不一致(特别是 OpenRouter 代理的模型),纯 prompt 约束是兼容性最高的做法------代价是需要在 _parse_response() 中做大量防御性解析。
6.3 JSON 解析的防御层 + 冲突检测的实际状态
JSON 解析 (_parse_response(), memory_reranker.py:286-325):有多层防御,任何失败都不会导致记忆注入完全中断:
1.去除 markdown 代码块包裹(json ... )
2.找第一个 { 和最后一个 },只解析中间部分
3.json.loads 失败 → fallback 返回 BM25 top-5(confidence=0.3)
4.JSON 解析成功但 selected 字段全部无效 → fallback 返回 top-3
5.selected 为空列表 → 同上
6.整个 LLM 调用抛异常 → curate() 外层 catch → fallback
冲突检测的实际状态 :RerankResult.conflicts 字段确实被 LLM 返回并解析了,但 memory_injector.py:257-264 只把它打到日志:
if rerank_result.conflicts:
logger.info("Reranker: %d selected, %d conflicts", ...)
然后什么也没做。 conflicts 没有被注入到 prompt 中,没有被传递给任何下游控制器,也没有触发任何 Agent 行为。这是一个未完成的功能点------检测到了冲突但没利用。
关于 "Agent 注意到冲突":这是一个常见的表述误区。Agent 不会"看到日志并注意"。要让 Agent 利用冲突信息,需要把 conflicts 格式化为文本追加到 system message(如 "Warning: found conflicting memories: ..."),当前代码没有做这一步。
七、后台记忆管家(memory_curator_agent.py)
7.1 为什么需要后台维护?
记忆系统如果不维护,会出现三种退化: 1.重复膨胀:同一个事实被记录了 3 次,搜索返回 3 条几乎一样的内容
2.过时信息:文件已被删除但记忆仍引用它
3.碎片化:10 条相关记忆分散在各处,但从未被归纳为一条洞察
7.2 6 步维护循环
def run_cycle():
# 每 ~10 个任务触发一次
1. _collect_stats() # 统计 tier/domain 分布,生成建议
2. _archive_duplicates() # Jaccard > 0.9 → 标记为 ARCHIVAL
3. _validate_memories() # 检查引用的文件是否还存在
4. _consolidate_insights() # 聚类相似记忆 → LLM 合成洞察
5. promote_memories() # 按时间+使用量升降 tier
6. link_memories() # 内容相似度 > 0.4 → 建立关联边
Jaccard 相似度 (_jaccard_similarity, memory.py:1725-1748):
Jaccard(A, B) = |A ∩ B| / |A ∪ B|
对文本来说是将两段文字分别分词为 token 集合,取交集大小除以并集大小:
tokens_a = set(_tokenize("用 FastAPI 做 JWT 认证"))
tokens_b = set(_tokenize("用 FastAPI 做 OAuth 认证"))
# 交集 = {"用", "FastAPI", "做", "认证"} = 4
# 并集 = {"用", "FastAPI", "做", "JWT", "OAuth", "认证"} = 6
# Jaccard = 4/6 ≈ 0.67
范围 0, 1。_archive_duplicates() 用 0.9 高阈值(几乎完全一样才归档),link_memories() 用 0.4 低阈值(有一定交集就建立关联边)。
第 4 步(consolidate)的具体机制:
1.从 related_to 图 + Jaccard 相似度找出记忆簇(cluster)
2.每个簇调用 LLM:总结这 3-5 条记忆的共同点,生成一条 LONG_TERM 洞察。例如输入 "用 FastAPI 做路由"、"用 Pydantic 做校验"、"用 SQLAlchemy 做 ORM" → 输出 "本项目使用 FastAPI + Pydantic + SQLAlchemy 作为核心后端技术栈"
3.如果 LLM 不可用,回退到规则匹配(提取共享关键词作为摘要)
4.生成的洞察作为一条新的 MemoryEntry(tier=LONG_TERM) 写入记忆库
5.每周期最多生成 3 条洞察,防止 LLM 成本过高
八、工作记忆保护(working_memory.py)
8.1 为什么存在?
上下文压缩时(见 context_cybernetics),早期的消息会被摘要化。但有些信息绝对不能丢:
- 用户最开始说的任务目标
- 已经做出的关键决策
- 当前正在执行的子任务
WorkingMemory 在压缩前"保护"这些信息------压缩器看到它们时会跳过。
8.2 两个组件
WorkingMemoryTracker:
class WorkingMemoryTracker:
max_entries: int = 15
max_tokens: int = 4000
def add(content, entry_type="active_task", ttl_seconds=None, importance=1.0):
"""添加一条受保护的内容"""
def get_protected_content():
"""返回所有未过期的保护内容(供压缩器读取)"""
ConversationContinuityManager:
class ConversationContinuityManager:
def add_marker(marker_type, description):
"""标记关键对话节点:task_start / decision_point / error_recovered / user_redirect"""
在 agent_loop.py 中通过模块级单例使用:
from minicode.working_memory import protect_context, mark_continuity
protect_context("用户要求实现 JWT 认证", entry_type="active_task")
九、时间线记忆(timeline_memory.py)
9.1 为什么存在?
常规记忆回答"这个项目用 FastAPI"。但有些问题需要时间维度:
- "上次我改了哪个文件?" → latest state
- "修复 auth bug 之前做了什么?" → event order
- "从上次 deploy 到现在过了多久?" → date difference
9.2 核心数据结构
@dataclass(frozen=True)
class StateRecord:
subject: str # 主体(如 "auth_module")
attribute: str # 属性(如 "status")
value: str # 值(如 "fixed")
date: str # 日期(如 "2026-07-24")
evidence: str # 证据文本片段
record_type: str # "state"(持续状态)或 "event"(一次性事件)
9.3 StateReasoner --- 7 种确定性推理
StateReasoner 是一个不调 LLM 的确定性推理器:
| 推理方法 | 问题示例 | 实现方式 |
|---|---|---|
answer_latest_state() |
"auth 模块现在什么状态?" | 查最新日期的 state record |
answer_event_order() |
"先修复的 login 还是 signup?" | 按日期排序 event records |
answer_date_difference() |
"从重构到现在过了几天?" | 计算两个 record 的日期差 |
answer_age_difference() |
"login 和 signup 哪个更早?" | compare date keys |
answer_distinct_event_day_count() |
"这周有几个独立事件?" | 去重计数 |
answer_duration_sum() |
"这些事件总共持续了多久?" | 累加 duration |
answer_since_consecutive_events() |
"连续出过几次事件?" | 遍历检查连续性 |
TimelineMemory 是独立的轻量模块。它在压缩对话时从摘要中提取结构化的状态记录------日期、主体、属性、值------然后用这些记录做确定性推理。不需要调 LLM,纯规则匹配 + 日期计算就能回答'最近改了什么''事件先后顺序'这类时间问题。
十、向量搜索补充(vector_memory.py)
10.1 为什么 BM25 不够?
BM25 需要关键词匹配。如果用户说"加一个缓存层"但记忆里写的是"Added Redis integration",BM25 搜不到------因为 "cache" 和 "Redis" 没有关键词重叠。
向量搜索通过语义相似度解决这个问题。
10.2 双路搜索
# 路 1:稀疏向量(TF-IDF),零依赖,永远可用
class SparseVectorStore:
def search(query, top_k=10):
# TF-IDF 向量 + cosine similarity
# 路 2:稠密向量(sentence-transformers),可选
class VectorMemoryStore:
def search(query, top_k=10):
# all-MiniLM-L6-v2 编码 + cosine similarity
all-MiniLM-L6-v2 是 sentence-transformers 的一个预训练模型(约 90MB),将文本映射为 384 维语义向量。需要 pip install sentence-transformers。默认不启用(enable_vector=False)。
模型配置说明 :MiniCode 在 main.py:190-222 有配置加载步骤------加载 ~/.mini-code/settings.json,如果用户没有配置 API key,回退到 MockModelAdapter(不调任何 API)。Reranker 如果拿到 mock model,LLM 调用会走 mock → JSON 解析失败 → fallback 到 BM25 top-5。功能不会崩,但所有需要 LLM 的组件(Reranker 精选、Curator 洞察合成)都降级了。这确实不如 Claude Code 那样有完善的前置配置引导------MiniCode 假设开发者知道怎么配环境变量。
10.3 RRF 融合
def merge_bm25_vector(bm25_results, vector_results, k=60):
"""Reciprocal Rank Fusion: 合并 BM25 和向量搜索的排序结果"""
BM25 和向量搜索各自排名,然后用 RRF 公式融合------一个条目在两个排名中都靠前,最终排名会更高。
现实情况 :向量搜索默认是关闭的(enable_vector=False),因为需要额外安装 sentence-transformers。BM25 + 查询扩展 + Reranker 在大多数场景下已经够用。
十一、完整数据流:一次任务中 Memory 在做什么
Task 开始: "add login endpoint to src/auth.py"
│
├─ Phase 1: 初始化(agent_loop.py,主线程,非异步)
│ ├─ orch = CyberneticOrchestrator() → 控制器门面
│ ├─ orch.initialize(model, tools, runtime) → 15 个控制器就绪
│ │ └─ orch.memory_ctrl = MemoryInjectionController() → 注入决策器
│ ├─ MemoryManager(project_root=cwd) → 加载三个 scope 的 MEMORY.md
│ ├─ MemoryReranker(model_adapter=model) → LLM 精选器就绪
│ ├─ MemoryInjector(mgr, ctrl, reranker) → 注入执行器就绪
│ ├─ orch.wire_memory(memory_mgr) → Pipeline 初始化
│ │ └─ MemoryPipeline.initialize(model, workspace)
│ │ ├─ MemoryReranker(model) → 子组件装配
│ │ ├─ MemoryInjector(mgr, reranker)
│ │ ├─ MemoryCuratorAgent(mgr, model)
│ │ └─ ReflectionEngine(mgr)
│ └─ [控制论初始化:Feedforward, SmartRouter, ModelSelection...]
│
├─ Phase 2: 记忆注入(主循环前,一次)
│ ├─ MemoryInjectionController.decide(signal)
│ │ └─ context_usage=0.5 → STANDARD 模式, 5 条, relev≥0.3
│ │
│ └─ orch.inject_memories("add login endpoint...", messages)
│ └─ MemoryPipeline.inject()
│ ├─ MemoryInjector.inject_for_task()
│ │ ├─ MemoryManager.search("add login endpoint...") → BM25 top-15
│ │ │ └─ 找到: "FastAPI project structure", "JWT auth pattern",
│ │ │ "SQLAlchemy model conventions", "Login failed debugging"...
│ │ ├─ MemoryReranker.curate(top-15, task) → LLM 精选 3-5 条
│ │ │ └─ 选中: "FastAPI project structure" (领域匹配)
│ │ │ "JWT auth pattern" (领域匹配)
│ │ │ "SQLAlchemy model conventions" (领域匹配)
│ │ │ └─ 拒绝: "Login failed debugging" (领域不匹配,是 debug 不是 dev)
│ │ └─ dedup + tag match
│ └─ 追加到 system message
│
├─ Phase 3: Agent 循环执行(主循环内)
│ ├─ WorkingMemory 保护当前任务上下文
│ └─ [Agent 使用注入的记忆完成编码任务]
│
├─ Phase 4: 任务结束(step_end 中)
│ ├─ MemoryPipeline.write(task, trace)
│ │ └─ ReflectionEngine.reflect(trace)
│ │ └─ 提取: files=["src/auth.py"], libs=["FastAPI", "PyJWT"],
│ │ tags=["auth", "login", "jwt"], confidence=0.85
│ │ └─ MemoryManager.add_entry(category="implementation",
│ │ content="Implemented JWT login endpoint in src/auth.py...")
│ │
│ ├─ MemoryPipeline.feedback(success=True, injected_ids=[...])
│ │ └─ 注入的 3 条记忆 usage_count += 2(正面反馈)
│ │
│ └─ MemoryPipeline.maintain() [每 ~10 个任务]
│ └─ MemoryCuratorAgent.run_cycle()
│ ├─ 去重: 找到 2 对 Jaccard > 0.9 的记忆 → 归档旧版本
│ ├─ 验证: 检查记忆引用的 3 个文件 → 2 个还存在, 1 个已删除(mark stale)
│ ├─ 合并: 发现 3 条 auth 相关记忆 → LLM 合成 1 条洞察
│ └─ 分层: 7 条 memories 从 SHORT_TERM → LONG_TERM(年龄够 + 使用量够)
十二、项目 infra-ai 层分析
12.1 当前架构
项目有 一层 API 抽象,但不够规范:
model_registry.py
├── Provider (Enum): ANTHROPIC | OPENAI | OPENROUTER | CUSTOM | MOCK
├── ProviderConfig: 统一配置(provider, model, base_url, api_key)
├── create_model_adapter(): 工厂函数
│ ├── is_openai_compatible → OpenAIModelAdapter
│ └── Anthropic → AnthropicModelAdapter
│ └── force_mock → MockModelAdapter
anthropic_adapter.py → AnthropicModelAdapter(原生 Messages API)
openai_adapter.py → OpenAIModelAdapter(Chat Completions API)
mock_model.py → MockModelAdapter(无 API,假响应)
OpenAI-compatible 路径(OpenAI / OpenRouter / 自定义端点 / 硅基流动)统一走 OpenAIModelAdapter,通过不同的 base_url + api_key 区分。Anthropic 走独立 adapter。新增厂商只需配 base_url + api_key。
12.2 设计缺陷
1.没有统一的 Adapter 抽象基类 。AnthropicModelAdapter、OpenAIModelAdapter、MockModelAdapter 之间无继承关系------只是恰好有同名方法(鸭子类型)。看代码时无法确定"一个 ModelAdapter 必须实现哪些方法"。
2.Provider 和 Adapter 不是 1:1 。CUSTOM 和 OPENROUTER provider 都走 OpenAIModelAdapter,通过 runtime dict 里塞不同字段区分行为------用配置替代多态,运行时容易出错。
3.Reranker 调模型靠 duck typing :hasattr(self._model, 'generate')------本质上在猜 adapter 有什么方法。
一个好的设计应该是:
class BaseModelAdapter(ABC):
@abstractmethod
def generate(self, prompt: str) -> str: ...
@abstractmethod
def next(self, messages: list[dict]) -> AgentStep: ...
class AnthropicAdapter(BaseModelAdapter): ...
class OpenAIAdapter(BaseModelAdapter): ...
class SiliconFlowAdapter(OpenAIAdapter): # 只改 base_url
def __init__(self):
super().__init__(base_url="https://api.siliconflow.cn/v1")
十三、已知局限与改进空间
以下局限均基于源码核实,非推测。
| # | 局限 | 影响 | 改进方向 |
|---|---|---|---|
| 1 | feedback() 未接入主链路 |
任务成功/失败不能显式调整记忆权重,只靠搜索时自动 +1 | 在 agent_loop.py 的 finally 块中调用 pipeline.feedback() |
| 2 | 冲突检测结果只打日志 | 检测到矛盾的记忆但 Agent 不知道 | 将 conflicts 格式化为 prompt 文本追加到 system message |
| 3 | Reranker 只用 prompt 约束 JSON | LLM 格式不遵循时需多层防御解析 | 对 Anthropic 原生用 tool_use 约束输出 |
| 4 | 未使用 Cross-Encoder | 通用 LLM 做重排序不如专用模型精准且更贵 | 混合:Cross-Encoder 打分 + LLM 只做冲突检测和 summary |
| 5 | agent_loop.py 和 main.py 各自创建 MemoryManager 实例 |
两个实例数据可能不一致 | 改为单例或共用同一实例 |
| 6 | MemoryEntry.id 用时间戳而非内容 hash |
同一内容写入两次产生两条记录,只能靠后台去重 | 改为 hash(content + category) 实现写入时幂等 |
| 7 | 缺少 Adapter 抽象基类 | 新增 provider 不知道必须实现哪些方法 | 定义 BaseModelAdapter(ABC),所有 adapter 继承 |
| 8 | 无前置配置引导 | 新用户不知道怎么配 API key,只能看终端提示 | 首次启动时交互式配置(类似 Claude Code 的登录流程) |
十四、要点
| 问题 | 答案要点 |
|---|---|
| Memory 解决什么问题? | 跨 session 知识保留。Agent 重启后仍然知道项目架构、编码规范、历史决策 |
| 三层存储分别存什么? | USER(跨项目个人偏好)→ PROJECT(团队共享,可入 git)→ LOCAL(本地临时) |
| 四层分级如何流转? | WORKING→SHORT_TERM→LONG_TERM→ARCHIVAL,按使用量和时间自动升级/降级 |
| 搜索为什么不是简单的 grep? | 5 项评分融合(BM25 + 子串 + 标签 + 领域 Jaccard + 使用量 + 新近度),中英跨语言扩展 |
| 为什么要 LLM 精选? | BM25 关键词匹配会产生领域噪声------"login" 在前端和后端记忆里都有,LLM 判断哪个真正相关 |
| 注入为什么需要 PID 控制? | 上下文压力高时自动降级为摘要或零注入,防止记忆注入加剧 overflow |
| 后台维护做什么? | 去重(Jaccard > 0.9)、验证(文件是否还存在)、合并(聚类 → LLM 洞察)、分层(升降 tier) |
| 反馈闭环怎么工作? | 任务成功 → 注入的记忆 usage_count +2 → 下次排名更高;失败 → -1 衰减 |
| 原子写入怎么保证? | 先写临时文件 → os.replace(temp, target) → POSIX 保证原子性 |
| WorkingMemory 什么时候用? | 上下文压缩前,保护当前任务的关键信息不被摘要化吞掉 |
| TimelineMemory 和普通 Memory 区别? | 普通记忆回答"是什么",TimelineMemory 回答"什么时候发生的、顺序如何" |
| Reranker 用什么模型?为什么? | Haiku,~500 tokens prompt,精选不需要深度推理,Haiku 够快够便宜 |
| Reranker 如何约束 LLM 输出? | 纯 prompt 文本约束("Return ONLY valid JSON:"),非 function calling;有多层 JSON 解析防御 |
| 冲突检测有用起来吗? | 没有。只打日志,未注入 prompt 或触发任何 Agent 行为,是个未完成的功能点 |
| feedback() 闭环完整吗? | 不完整。方法已实现但 agent_loop.py 未调用------任务成败不能显式调整记忆权重 |
| BM25 是关键词匹配还是语义搜索? | 关键词匹配(TF-IDF 改进版)。语义搜索靠可选的 sentence-transformers 向量和 LLM Reranker 补充 |
十五、可能的追问
Q1:如果记忆库有 1000 条记录,搜索会不会很慢?
A:目前不会。原因有三:
1.搜索是在 MemoryFile 的内存索引上做的(不是每次读磁盘)
2.每个 scope 上限 200 条、25KB,总量可控
3.1108 行的 _parse_memory_md() 在加载时就已经构建好了 _id_index、_tag_index、_category_index 三个索引
如果真的到 1000 条级别,瓶颈会在 Reranker(LLM 精选 15 条的 prompt 会变长),可以通过先过滤低分候选(只传 top-8 给 Reranker)来优化。
Q2:如果 Reranker 的 LLM 调用失败了怎么办?
A :MemoryReranker.curate() 有完整的 try/except,任何异常都会 fallback 到返回 BM25 的 top-5。而且有 LRU 缓存(256 条,60s TTL)------相同查询在 1 分钟内不重复调 LLM。
Q3:怎么防止记忆"污染"------错误的记忆被反复注入导致 Agent 持续犯错?
A :有三层防护: 1.write() 只在 ReflectionEngine 置信度 >= min_confidence 时才写入(低质量记忆不会入库)
2.feedback() 机制------失败的记忆会被衰减(usage_count - 1),排名逐渐下降直至不再被注入
3._validate_memories() 定期检查记忆引用的文件是否还存在,过时的会被标记。但坦白说,目前没有"如果一条记忆连续导致 3 次失败就删除"的机制------这是可以改进的点。
Q4:为什么不用向量数据库(Chroma/Milvus/Pinecone)?
A:这是一个有意的设计选择。作为 Coding Agent 的记忆系统,必须轻量,不能要求用户装一个向量数据库
BM25 + 中英查询扩展 + LLM 精选的组合,对于几百条级别的记忆已经足够。VectorMemoryStore 提供了可选的 sentence-transformers 路径,但默认关闭------不增加安装门槛。
Q5:MEMORY.md 和 memory.json 的关系?
A :同一份数据的两种格式。memory.json 是结构化存储(含所有元数据),MEMORY.md 是人可读的 markdown。写入时两份同时更新(都是原子写入)。如果用户在 IDE 里手动编辑 MEMORY.md,_parse_memory_md() 能在下次加载时解析回来------但会丢失元数据(usage_count 等),所以推荐通过 Agent 命令操作。