MiniCode 项目详解7:Memory 记忆系统

一、定位

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 += 1memory.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 只在搜索时自动 +1memory.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 抽象基类AnthropicModelAdapterOpenAIModelAdapterMockModelAdapter 之间无继承关系------只是恰好有同名方法(鸭子类型)。看代码时无法确定"一个 ModelAdapter 必须实现哪些方法"。

2.Provider 和 Adapter 不是 1:1CUSTOMOPENROUTER provider 都走 OpenAIModelAdapter,通过 runtime dict 里塞不同字段区分行为------用配置替代多态,运行时容易出错。

3.Reranker 调模型靠 duck typinghasattr(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.pymain.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 调用失败了怎么办?

AMemoryReranker.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 命令操作。

相关推荐
飞飞传输几秒前
机器人行业数据流转深度研究:内外网数据摆渡平台应用现状与趋势
大数据·运维·安全
樊小肆5 分钟前
DeepSeeker-Code源码导读04-上下文压缩
人工智能·agent
长谷深风1116 分钟前
Agent 何时该 Replan:五个关键判断
java·大数据·开发语言·ai agent·ai智能体·agent设计·clarify机制
weixin_471383039 分钟前
17 Self-RAG —— 幻觉检测 + 答案质量评估
python·agent
叠层归一研究院16 分钟前
如何用程序搭建一个 AGI 种子系统(三):生长如何对接物理与数学宇宙
人工智能·python·算法·机器学习·transformer·agi
兴趣使然黄小黄26 分钟前
【AI-agent】让 AI 输出可依赖:LLM 工程化的四道防线
大数据·人工智能
jufeng130727 分钟前
【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 5 篇】
python·ai agent·上下文压缩
AndrewHZ28 分钟前
图像处理入门008 | 阶段总结:环境测试与基础概念测验
图像处理·python·opencv·计算机视觉·cv
阿弱28 分钟前
graph-core 的边与命令模式设计
java·后端·agent
武子康33 分钟前
DeepSeek Harness:Cordis 如何让插件可卸载、可依赖、可重组
人工智能·llm·agent