ai-memory 技术深度剖析:我是如何让 AI 记住三个月的开发历史的

GitHub:https://github.com/hyxnj666-creator/ai-memory
npm:https://www.npmjs.com/package/ai-memory-cli
版本:v2.6.2(含内置免费模型,零配置可用)


背景与问题定义

在使用 Cursor、Claude Code 等 AI 编程工具进行长期项目开发时,开发者面临一个共同痛点:每次新开对话窗口,AI 对之前所有的技术决策、架构选择、踩坑经验一无所知

开发者不得不花费大量时间反复向 AI 铺垫背景信息,例如:

  • “我们上次确定了用 event sourcing 实现账单模块”
  • “这个接口之前讨论过要加幂等校验”
  • “WebView 里的 OAuth 是通过 Bridge 页中转的,不能用 Deep Link”

这些隐性知识完全依赖人工传递,效率极低。ai-memory 的设计目标是:自动从已有对话中提取结构化知识,在新对话开始时精准注入相关历史上下文,让 AI 拥有持续记忆能力。

本文重点阐述 ai-memory 在以下三个核心问题上的技术方案:

  1. 如何处理大量历史对话——分块与噪声过滤
  2. 如何保证提取内容的准确性——五层质量过滤体系
  3. 如何在新对话中精准检索相关记忆——三路混合检索

适用场景

在进入技术实现之前,先明确 ai-memory 解决的不是“保存聊天记录”这个问题,而是让 AI 在长期项目中持续理解项目上下文。它更适合以下场景:

1. 长期维护型项目

对于持续迭代数月甚至数年的项目,很多关键信息不会写在 README 中,例如:

  • 某个接口为什么必须保持兼容
  • 某张表为什么不能直接改字段
  • 某个移动端方案为什么排除了 Deep Link
  • 某个权限设计背后的安全约束

这些信息通常只存在于历史对话和开发者记忆中。ai-memory 可以把它们提取成结构化知识,后续新对话可以直接复用。

2. AI 编程重度使用者

Cursor、Claude Code、Windsurf 等工具的对话窗口天然是“临时上下文”。当项目对话数量达到几十甚至几百个时,新窗口无法自动理解之前的技术决策。

ai-memory 的价值在于:从历史对话中提取可复用的技术结论,让新会话不再从零开始。

3. 多人协作与知识交接

提取后的记忆以 Markdown 存储在本地,可以放进 Git 管理。团队成员不仅能看到“最终代码是什么”,还可以看到“为什么当时选择这个方案”。

这对以下场景很有帮助:

  • 新成员接手项目
  • 架构方案评审
  • 重构前回顾历史约束
  • 排查已经讨论过的历史问题

4. 复杂重构、迁移和开源维护

数据库迁移、鉴权改造、支付链路重构、跨端兼容方案等任务通常会跨多个会话、多个阶段推进。ai-memory 可以把每次讨论沉淀成持续可检索的项目知识。

对于开源项目,Issue 和版本迭代中反复出现的设计取舍,也可以沉淀为项目长期知识库。

5. 不适合的场景

如果只是一次性 demo、临时脚本,或者项目对话量很少,ai-memory 的价值不会特别明显。它真正适合的是:上下文会反复被使用、技术决策需要长期保留、AI 经常需要重新理解背景的工程项目。


一、大量历史对话的处理策略

1.1 问题规模分析

一个月度活跃的 Cursor 项目通常会累积:

  • 100-300 个对话会话
  • 每个深度对话 30-60 轮,展开后 2-8 万字符
  • 累计可能有数百万字符的对话文本

直接将整个对话文本送给 LLM 处理有三个明显问题:

  1. 超出主流模型的 context window 限制
  2. LLM 对超长输入的"注意力"集中度下降,早期内容容易被忽略
  3. API 调用成本高、延迟大

1.2 基于对话轮次边界的分块策略

ai-memory 将每个对话文本分割为多个独立块,每块独立提交 LLM 处理:

const CHUNK_SIZE = 20_000;   // 约 5k tokens,经测试的最优值
const CHUNK_OVERLAP = 2_000; // 块间重叠,防止边界信息丢失

关键设计:按自然对话边界截断,而不是硬截字符位置:

function splitIntoChunks(text: string): string[] {
  while (start < text.length) {
    const slice = text.slice(start, start + CHUNK_SIZE);
  
    // 找到最后一个对话轮次边界
    const lastUser = slice.lastIndexOf("\n\nUser:");
    const lastAssistant = slice.lastIndexOf("\n\nAssistant:");
    const boundary = Math.max(lastUser, lastAssistant);
  
    // 如果边界在块的中后段(>50%处),就在此截断
    if (boundary > CHUNK_SIZE * 0.5) {
      chunkEnd = start + boundary;
    }
  }
}

这样确保每个 LLM 调用的输入都是语义完整的对话片段,而不是被截断在句子中间。

1.3 预处理噪声过滤

在分块后、LLM 调用前,先过滤掉对知识提取无价值的内容:

export function stripConversationNoise(text: string): string {
  // 过滤工具调用 XML(Cursor 内部 invoke/tool_call 块)
  cleaned = cleaned.replace(/<(?:tool_call|invoke|function_call)[\s\S]*?<\/...>/g, "[tool call]");
  
  // 过滤长哈希和 base64 编码(Git commit hash、图片数据)
  cleaned = cleaned.replace(/\b[0-9a-f]{32,}\b/gi, "[hash]");
  cleaned = cleaned.replace(/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, "[base64]");
  
  // 截断超长日志行(堆栈跟踪、数据 dump)
  cleaned = cleaned.replace(/^.{500,}$/gm, (line) =>
    line.slice(0, 120) + `... [truncated ${line.length} chars]`
  );
}

实测可将对话文本体积缩减 20-40%,同时提升 LLM 聚焦于真实技术内容的能力。


二、五层质量过滤体系

这是 ai-memory 的核心竞争力所在。提取出的知识如果质量低下,不仅没有帮助,还会在新对话中引入噪声。为此设计了五层递进的过滤机制。

第一层:Prompt 级别的结构化约束

提取 Prompt 中强制要求每条记忆满足 5 项质量指标:

QUALITY CHECKLIST(每条记忆必须同时满足全部条件):
□ SPECIFIC    - 明确引用文件名、函数名、API 路径、配置键名或数据结构
□ ACTIONABLE  - 其他开发者无需重读对话就可以直接执行
□ NON-OBVIOUS - 不是任何开发者都知道的通用知识
□ DURABLE     - 数周/数月后依然有参考价值(排除临时调试步骤)
□ COMPLETE    - 包含完整技术图景(问题 + 方案 + 原因)

ONE-MEMORY-PER-DECISION 规则防止过度拆分:一个技术决策对应一条记忆,所有相关的实现细节、配置要点、子步骤统一并入这条记忆。

❌ 错误输出(3 条记忆):
  [架构:billing 模块用 event sourcing]
  [TODO:REVOKE events 表的 UPDATE/DELETE 权限]
  [TODO:构建每日 hash chain 完整性检查]

✅ 正确输出(1 条记忆):
  [架构:billing 模块用 event sourcing
   影响:billing_events 表(REVOKE UPDATE/DELETE),每日 hash chain 检查
   文件:billing/events.ts, migrations/002_events.sql]

第二层:技术具体性密度评分

LLM 按规则输出后,代码层面再做一轮量化评估。使用 20+ 个正则模式统计内容的技术密度:

const SPECIFICITY_PATTERNS: RegExp[] = [
  /[./\\][\w-]+\.\w{1,5}\b/g,               // 文件路径 (./src/auth.ts)
  /(?:function|class|const|def)\s+\w+/g,    // 函数/类声明
  /\/api\/[\w/]+/g,                          // API 路由 (/api/users)
  /`[^`\n]{2,}`/g,                           // 行内代码 (`useState`)
  /\bv?\d+\.\d+(?:\.\d+)?\b/g,              // 版本号 (v1.2.3)
  /\b(?:GET|POST|PUT|DELETE)\s+\/[\w/-]*/g, // HTTP 方法+路由
  // ... 15+ 更多模式
];

不仅检查"是否存在",而是统计所有命中次数之和,给出密度分。低于阈值的内容判定为"技术具体性不足",直接过滤。

第三层:双语模糊短语检测

维护一份中英双语的"废话短语"黑名单,来源于真实 LLM 输出的反面案例:

const VAGUE_PHRASES_ZH = [
  "影响到整个项目", "优化了用户体验", "提高了代码质量",
  "符合最佳实践", "实现了功能", "满足了需求", "需要注意",
  // 50+ 条
];

const VAGUE_PHRASES_EN = [
  "affects the entire project", "improves user experience",
  "follows best practices", "is a good approach",
  // 50+ 条
];

命中黑名单且技术具体性得分为 0 的内容,直接过滤。

第四层:批次内去重(3-gram Shingle 相似度)

同一段对话文本的不同块可能对同一技术决策产生两条相似记忆。使用 3-gram shingle + Jaccard 相似度 检测内容重复:

// Shingle 生成:将文本转换为 3 字符滑动窗口集合
export function shingles(text: string, n = 3): Set<string> {
  const clean = text.toLowerCase().replace(/\s+/g, " ").trim();
  const result = new Set<string>();
  for (let i = 0; i <= clean.length - n; i++) {
    result.add(clean.slice(i, i + n));
  }
  return result;
}

两个相似度阈值:

  • Jaccard 相似度 > 0.55:认为是同一内容的重复表述
  • 包含度 > 0.75:较短的记忆被较长的记忆 75% 覆盖,认为是子集

发现重复时保留内容更完整的那条,而不是先出现的。

第五层:跨批次增量去重

每次提取时,将当前数据库中已有记忆的标题列表注入 Prompt:

const existingBlock = existingTitles
  ? `ALREADY EXTRACTED (DO NOT re-extract similar items):
${existingTitles.map(t => `- ${t}`).join("\n")}`
  : "";

LLM 生成新记忆前会主动检查是否已存在类似条目,从根本上防止增量提取时产生重复。


三、三路混合检索系统

提取存储只是第一步。更关键的是:新对话开始时,如何从可能几百条记忆中找到最相关的 5-10 条

3.1 设计决策:为何不能单一方案

检索方案 优点 缺点
纯关键词匹配 精确、不需要外部 API 召回率低,同义词无法匹配
纯语义向量 理解意图,召回率高 对专有名词不够精确,需要 embedding API
纯时间排序 实现简单 忽视相关性,旧项目的重要决策被压制

三者单独使用都有明显缺陷,ai-memory 将三路信号做加权融合:

最终得分 = 语义相似度 × 0.55 + 关键词匹配 × 0.30 + 时间衰减 × 0.15

权重经过实际项目测试调优,语义信号权重最高但不超过 60%,关键词精确匹配作为重要补充。

3.2 CJK 感知分词器

对中文文本,不能用空格分词,必须基于字符 n-gram:

export function tokenize(text: string): string[] {
  const tokens: string[] = [];
  
  for (let i = 0; i < text.length; ) {
    const cp = text.codePointAt(i)!;
  
    if (isCJK(cp)) {
      // CJK 字符:生成 bigram 和 trigram
      if (i + 2 <= text.length) tokens.push(text.slice(i, i + 2));  // bigram
      if (i + 3 <= text.length) tokens.push(text.slice(i, i + 3));  // trigram
      i++;
    } else {
      // 非 CJK 字符:按空白分词
      const m = text.slice(i).match(/^[^\s\u4e00-\u9fff]+/);
      if (m) { tokens.push(m[0].toLowerCase()); i += m[0].length; }
      else i++;
    }
  }
  
  return tokens.filter(t => !STOPWORDS.has(t));
}

搜索"内存提取策略"时,"内存提取"和"提取策略"这两个 bigram 都会匹配,显著提升中文场景下的召回率。

3.3 字段加权关键词评分

命中记忆不同字段时给予不同权重:

function keywordScore(m: ExtractedMemory, keywords: string[]): number {
  for (const kw of keywords) {
    const len = kw.length;
    const lengthBonus = len >= 3 ? 2 : len >= 2 ? 1.2 : 1;  // 长词更有判别力

    if (titleLow.includes(kw))         score += 10 * lengthBonus;  // 标题命中 ×10
    if (contentLow.includes(kw))       score += 5 * lengthBonus;   // 内容命中 ×5
    if (contextLow.includes(kw))       score += 2 * lengthBonus;   // 上下文命中 ×2
    if (m.reasoning?.toLowerCase().includes(kw)) score += 1 * lengthBonus;
    if (m.impact?.toLowerCase().includes(kw))    score += 1 * lengthBonus;
  }
  return score;
}

标题命中权重是内容的 2 倍,是上下文的 5 倍,符合信息检索领域的标准实践。

3.4 时间衰减:90 天半衰期

function recencyScore(dateStr: string): number {
  const daysAgo = (Date.now() - new Date(dateStr).getTime()) / 86400000;
  return Math.exp(-daysAgo / 90);  // 90 天半衰期
}

90 天内的记忆保持较高衰减分,更久远的记忆权重逐渐降低,但不会归零——旧的架构决策仍然可以通过关键词和语义匹配浮出。

3.5 无 Embedding 的降级策略

当用户未配置 embedding 服务时,语义得分设为 0,关键词和时间重新归一化:

const weights = hasEmbeddings
  ? { semantic: 0.55, keyword: 0.30, recency: 0.15 }
  : { semantic: 0,    keyword: 0.85, recency: 0.15 };

降级后的关键词+时间模式在大多数场景下依然有良好效果,开箱即用不依赖外部 API。


四、上下文输出格式设计

检索到相关记忆后,如何格式化成对 AI 最有用的上下文?

近期记忆:全量详情格式

### Key Technical Decisions
- **WebView OAuth 用 Bridge 页中转** _(2026-03-15)_
  App 内 WebView 无法接收 OAuth redirect_uri 回调,采用 static/oauth-bridge.html 接收后 postMessage 传回 token。
  Why: Deep Link 在 Android/iOS 行为不一致;Custom URL Scheme 被部分浏览器拦截
  Rejected: Deep Link, Custom URL Scheme, Server-side redirect
  Affects: src/pages/login.tsx, static/oauth-bridge.html, backend/routes/oauth.ts

旧记忆:压缩索引格式(节省 token)

### Older Memories (ask for details if needed)
- [D] billing 模块 Event Sourcing 架构 _(2026-01-10)_
- [A] Redis Session 替换 JWT 的迁移方案 _(2025-12-20)_

新对话的 system prompt 粘贴这段上下文后,AI 立刻知道:

  1. 哪些技术决策已经确定,无需重新讨论
  2. 有哪些历史经验可以追问,按需展开

五、准确性验证:CCEB 基准测试

项目内置 CCEB(Cursor Conversation Extraction Benchmark)测试集:

  • 30 条手工标注的 fixture,覆盖"应提取"和"不应提取"两类
  • 正例:明确技术决策、架构选型、踩坑方案
  • 负例:闲聊讨论、临时调试步骤、过时的尝试、vague 表述

gpt-4o-mini 跑该基准,F1 = 76.2%

这个数字的实际含义:在精确率(不产生误报)和召回率(不遗漏重要决策)之间取得了较好平衡,在真实开发项目中表现为:大多数关键技术决策都被捕获,且几乎不产生"废话记忆"。


六、架构总览

对话文本
    │
    ├── 噪声过滤(tool call / hash / 长日志)
    │
    ├── 分块(20k char,对话轮次边界)
    │
    └── 并发 LLM 提取(max 6 并发)
            │
            ├── 第一层:Prompt checklist 约束
            ├── 第二层:技术具体性评分
            ├── 第三层:双语模糊短语检测
            ├── 第四层:批次内 Shingle 去重
            └── 第五层:跨批次 existingTitles 去重
                    │
                    └── 结构化记忆 → Markdown 持久化
                              │
                    ┌─────────┴──────────┐
                    │   三路混合检索     │
                    ├── 语义向量 (0.55)  │
                    ├── CJK 关键词(0.30) │
                    └── 时间衰减 (0.15)  │
                              │
                    上下文注入新对话 ←──┘

快速上手

# 无需 API Key,内置免费模型(v2.6.2+)
npx ai-memory-cli extract

# 配置自己的模型(支持 OpenAI / Claude / SiliconFlow 等)
npx ai-memory-cli config

# 在新对话中获取相关历史上下文
npx ai-memory-cli context "当前任务描述"

结语

ai-memory 不是简单地"把对话发给 AI 总结一下",而是在每个环节都针对开发者场景做了针对性设计:分块策略考虑了对话边界,质量过滤针对 LLM 的常见输出问题,混合检索平衡了准确性和召回率。

开源地址:github.com/hyxnj666-creator/ai-memory,欢迎 Star、Issue 和 PR。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐