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

相关推荐
郭老二2 小时前
【Python】Web框架 FastAPI 详解
python·fastapi
DFT计算杂谈2 小时前
交错磁研究进展材料物性与交叉应用
数据库·人工智能·python·opencv·算法
敖行客 Allthinker2 小时前
docker容器安装Python反推镜像步骤(适用于临时调试用)
python·docker·容器
玉鸯2 小时前
旧概念还是新范式?Agent 框架集体"图化"背后的矛盾与必然
后端·llm·agent
流量猎手3 小时前
GitHub 使用说明
大数据·elasticsearch·github
倾颜3 小时前
断线之后,不要重跑 AI:在 POST + NDJSON 中实现可恢复 Agent 流
前端·后端·agent
rrrjqy3 小时前
夏普比率与 Beta——Quant-for-Beginners 量化入门Task7
python·金融
中国搜索直付通3 小时前
防沉迷新规下的棋牌游戏生存术:从合规底线到用户体验升级
大数据·人工智能·游戏
志栋智能4 小时前
超自动化安全:提升安全服务满意度的隐形引擎
大数据·安全·自动化