把 Obsidian 知识库接进 Claude Code:400 行代码实现 AI 长期记忆

0 阅读15分钟

本文记录一次真实的工程实践:在 Claude Code 项目中,从零搭建一套让 AI 编码助手在每次写代码前自动调取规范、踩坑记录和组件 API 文档的长期记忆系统。从知识存储到自动召回,再到错误后的回流写入,整个闭环用一套 Harness 工作流串起来。


AI 编码助手总是"忘事"

如果你在团队里用 Claude Code 超过一个月,应该遇过这种事:

  • 两周前踩过 Arco Form 校验脱节的坑,写新表单时 AI 把同样的错误又犯了一遍
  • 项目有严格的 API 模块规范,但 AI 生成的代码还是绕过 @core/api 直接访问
  • 用了 NormalTable 组件,AI 对 Props 的描述和实际组件文档对不上

这不是模型能力的问题。AI 每次会话都从空白开始——它不知道你项目里沉淀了什么规范,不知道上周刚踩过什么坑,不知道那个封装好的业务组件到底接受什么 Props。你的团队知识库只有你们知道,对 AI 来说是透明的。

唯一的解法:在 AI 开始写代码之前,主动把相关知识塞给它。


为什么用 Obsidian 存知识

在搭建这套系统之前,需要先解决一个问题:团队知识存在哪里?

用普通 Markdown 文件夹也能存,但 Obsidian 有三个功能让它比普通编辑器高一个量级,而且和 AI 检索天然契合。

双向链接 [[]]

在任何笔记里输入 [[,Obsidian 弹出搜索框列出所有笔记,选中即建立链接。比如在 API 规范里写 [[types 规范]],点一下直接跳过去。反过来,打开 types 规范的 Backlinks 面板,能看到所有引用它的笔记——哪些规范依赖了 types,哪些踩坑涉及了 types,一目了然。

不需要维护任何索引,链接关系是自动追踪的。在这套系统里,规范之间的跨域引用、踩坑和组件的关联,全靠 [[双链]] 显式标记。相比 grep 碰运气,链接关系是明确的、可遍历的。

标签 #tag

一条笔记只能在一个文件夹里,但可以打任意多个标签。一个踩坑文件同时属于 #vue3#arco-form#form-validation,哪个维度都能检索到。标签不需要提前定义,想到什么打什么,Obsidian 自动收集。

这套系统里,tags: [pit, vue3, arco-form] 既是 Obsidian 的分类标签,也是踩坑匹配的检索字段,一个字段两用。

全文搜索

Cmd+Shift+F,搜索整个 vault 所有内容,支持正则、按标签/文件夹/时间范围筛选。你记得"好像在哪里写过关于表单校验的分析",模糊搜就能找到,不需要精确记忆文件名。

三个功能加在一起:双链建立明确关联,标签提供多维分类,全文搜索兜底模糊发现。对人有用,对 AI 一样有用——知识写进 Obsidian,AI 就能沿着结构检索,而不是靠猜。


三层知识结构

整套系统把团队知识分成三层,每层解决一类遗忘:

┌─────────────────────────────────────────────┐
│           编码任务(路径 / 描述 / 原型)       │
└──────────────────────┬──────────────────────┘
                       │
                       ▼
              ┌────────────────┐
              │  recall 召回层  │  ← PreToolUse 钩子自动触发
              └───────┬────────┘
                      │
         ┌────────────┼────────────┐
         ▼            ▼            ▼
      规范层        踩坑层       组件 API 层
   (怎么写)      (别踩什么)    (用什么砖)
解决的问题存储位置
规范层AI 不知道这个项目的代码约定docs/规范/*.md(Obsidian vault)
踩坑层AI 不记得上次踩过的坑docs/踩坑记录/*.md(一坑一文件)
组件 API 层AI 不了解项目封装的业务组件comp-lookup/references/*.md

接下来从一个真实的编码任务出发,看这三层是怎么在工作流里被用起来的。


一个编码任务的完整生命周期

起点:/Harness:developer

Harness如何编排可以看我之前写:AI 写代码总翻车?我用 Harness:developer 把它管成“右侧打工人”

Harness:developer 是团队的主编码工作流 skill。用法很简单:

# 参数分别是:
# 任务描述
# 子窗口会话类型(codex / claude)
# 启动参数(可选)
# 原型地址(可选)

/Harness:developer "商户管理模块开发" "codex" "" "http://localhost:3000"

触发后,它会按 Step 0 → Step 8 的顺序走完整个开发流程:原型勘察 → 计划落盘 → 启动右侧编码会话 → 自检 → 日志归档。

Step 1 里有两项强制执行的记忆召回,这是整套系统第一个介入点。


介入点 1:任务前显式召回(Harness:developer Step 1)

在开始任何编码之前,Harness:developer 强制执行两件事:

规范与踩坑预加载

基于原型勘察识别本次任务涉及的规范域(routes/components/layout/api/types/mocks),逐域读取规范文档,同时全量扫描踩坑库,将摘要附加到计划文档的 ## 规范与踩坑预加载 章节:

# 读取规范
cat docs/doc/前端通用文档/规范/api.md
cat docs/doc/前端通用文档/规范/components.md
# ...

# 扫描踩坑
obsidian search query="tag:pit" vault="ips-admin" limit=20

RULES_MEMORY_LOADED 未置为 true 会直接阻断 Step 1,后续编码不能继续。

组件记忆召回

从原型页面清单和数据字段中提取 UI 组件(表格/表单/日期/上传/弹窗等),映射为 comp-lookup 组件名,批量查询文档:

node .agents/skills/comp-lookup/fetch.mjs \
  normal-table form date-picker modal --api-only

输出(组件 API + 已知踩坑)追加到计划文档末尾的 ## 组件与记忆清单 章节。COMPONENT_MEMORY_LOADED 未置为 true 同样阻断 Step 1。

这两步的价值在于:编码还没开始,AI 就已经知道了"这个模块用到哪些组件、这些组件有什么历史坑"。


介入点 2:编码过程中隐式召回(PreToolUse 钩子)

Step 1 的预加载是一次性的、面向模块整体的。但编码过程中,AI 每次落笔写具体文件时,都有机会注入更精准的上下文。这靠 Claude Code 的 PreToolUse 钩子实现。

PreToolUse 在 Write / Edit 工具执行触发。钩子脚本 recall.sh 接收工具调用参数(即将编辑的文件路径和内容),stdout 输出直接注入 AI 上下文。时序是关键——必须在写之前注入,写完了注入没有意义。

.claude/settings.json 注册钩子:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/recall.sh",
        "timeout": 30
      }]
    }]
  }
}

recall.sh 根据即将编辑的文件类型和路径,决定注入什么:

接收 JSON(file_path + content)
    │
    ├─ 非 .vue / .ts → exit 0(零输出,零开销)
    │
    ├─ .ts 文件
    │   └─ 路径匹配规范域 → 注入对应规范(最多 60 个非空行)+ 匹配踩坑
    │
    └─ .vue 文件
        ├─ 提取最外层 <template> 块(含嵌套 slot 深度追踪)
        ├─ 识别 Arco 组件(<a-xxx>)+ 自定义组件(PascalCase)
        ├─ node fetch.mjs <comps> --api-only --compact
        └─ 路径命中规范域 → 同时注入规范 + 踩坑

规范域映射(路径 → 注入哪份规范):

core/api/**                    → api 规范
core/main/src/stores/**        → stores 规范
core/main/types/**             → types 规范
core/main/src/components/**    → components 规范
mocks/**                       → mocks 规范
*/src/router/**                → routes 规范
core/main/src/layout/**
apps/main/src/layout/**
apps/merchant/src/layout/**    → layout 规范

以编辑 core/api/src/payment-orders/index.ts 为例,AI 收到的上下文:

━━━ 🧠 recall: index.ts 匹配规范域 [api] ━━━

## 📋 规范:api

# API 模块规范(当前仓库)

## 1. 模块定位
- API 能力统一放在 `core/api`,由 `@core/api` 对外导出。
- 业务应用(apps/main、apps/merchant)只从 `@core/api` 引用,
  不直接跨层访问其他目录。

## 2. 实际目录结构
...(规范正文,最多 60 个非空行)...

━━━ recall 结束 ━━━

以编辑含 Arco Form 的表单页为例,AI 收到的是 Form 组件 Props/Events 完整表格,外加校验脱节踩坑的详细描述,全部在它落笔之前就已经在上下文里了。

Step 1 是面向模块的宽泛预加载,PreToolUse 钩子是针对每个文件的精准注入,两者叠加,覆盖了"任务启动时"和"每次落笔时"两个时间点。


介入点 3:编码完成后的组件 API 核查(Harness:verify)

编码完成后,Harness:developer Step 7 强制调用 /Harness:verify。Verify skill 有三项和记忆系统直接相关的检查:

组件 API 合规核查

对变更范围内所有 .vue 文件,扫描使用到的组件,批量查询 comp-lookup:

node .agents/skills/comp-lookup/fetch.mjs \
  normal-table form modal --api-only

逐组件核对 props 名称与类型、必填 props 是否传入、events 名称是否正确、slots 名称是否正确。禁止凭印象填写——每一项都必须有文档依据。结果以 COMP-<组件名>-<序号> 标识追加到核查矩阵。

踩坑触碰核查

comp-lookup 返回结果里包含 ## ⚠️ 已知踩坑 段落。Verify 会逐条核对:改动是否触碰了踩坑描述的问题场景?触碰了记 fail,未触碰记 pass

### 踩坑触碰核查
| 踩坑标题 | 相关组件 | 是否触碰 | 结论 |
|---|---|---|---|
| Arco Form 必填提示与提交校验脱节 | arco/form | 是 | fail |

踩坑回流(Step 7.1)

修复完成后,Verify 会检查本次 fail 列表里有没有尚未被踩坑记录覆盖的新问题模式。如果发现新问题,提示:

💡 建议固化为踩坑记录:
- 问题:<描述>
- 涉及组件:<comp-key>
- 建议 tags:pit, <技术栈>, <场景>
- 是否现在通过 /pit-record 保存?(y/n)

用户确认后调用 /pit-record 写入踩坑库。这是整个系统的"回流"环节:新的错误经验被固化到知识库,下次同类任务时自动召回。


踩坑是怎么被写入的:/pit-record

/pit-record 专门负责把一条错误经验结构化为可被系统检索的踩坑文件。它的核心是强制写 frontmatter:

---
title: Arco Form 必填提示与提交校验脱节
date: 2026-04-21
type: pit
tags: [pit, vue3, arco-form, form-validation]
related_components: [arco/form]
paths:
  - apps/main/src/views/**/form*.vue
keywords: [required, validate, rules, 表单校验]
---

> 所属:[[踩坑记录|踩坑记录 MOC]]

# [Vue3/Arco Form] 必填提示与提交校验脱节

## 问题描述
...

四个字段后来成为踩坑匹配的评分依据:

字段含义权重
paths哪些路径最容易触发这个坑(支持 glob)+10
related_components哪个组件引发了这个坑+5
tags领域标签+2
keywords关键词兜底+1

写完文件后,pit-record 自动更新 MOC(踩坑记录.md),按标签分组、按组件分组、按日期倒序排列,供 Obsidian Graph 可视化展示所有知识关联。

顶部的 > 所属:[[踩坑记录|踩坑记录 MOC]] 不可省略——Obsidian Graph 只渲染显式 wikilink 形成的边,缺少这行反向链接,pit 文件在图谱中就是孤立节点。


独立路径:/api-add 的踩坑预检

除了主开发流程,项目还有一个 /api-add skill 专门负责从 Swagger 文档快速生成 API function、TypeScript 类型和 Mock 实现。

api-add 在 Step 1 解析需求时,domain 确定后立即做踩坑预检:

obsidian vault="ips-admin" search query="tag:pit" limit=20
# 过滤:paths 包含 domain 关键字 或 keywords 匹配业务描述

找到相关踩坑后,将踩坑内容(标题 + 解决方案摘要)附加到 api_define 和 mock_create agent 的 prompt 末尾,作为"已知问题提示"——让负责生成代码的 agent 在动笔之前就知道这个 domain 历史上踩过什么坑。

这条路径独立于 Harness:developer 存在,说明记忆召回不是只在某一个入口触发,而是嵌入在每个涉及代码生成的 skill 里。


整个系统的信息流

把上面几个介入点串起来,一个完整的编码任务信息流是这样的:

用户: /Harness:developer 商户管理模块开发
         │
         ▼
  Step 1: 原型勘察 + 规范预加载 + 组件记忆召回
  (面向模块整体,一次性,写入计划文档)
         │
         ▼
  Step 2-6: 右侧 pane 开始编码
         │
         │  每次 Write/Edit 触发 recall.sh
         │  ├─ .ts → 注入规范(精准域匹配)
         │  └─ .vue → 注入组件 API + 规范 + 踩坑
         │  (针对单文件,自动,实时)
         │
         ▼
  Step 7: /Harness:verify
  ├─ 组件 API 合规核查(comp-lookup)
  ├─ 踩坑触碰检测
  └─ 新问题 → 提示 /pit-record 回流
         │
         ▼
  Step 8: 日志归档
         │
         ▼
  下次同类任务: 新踩坑自动参与匹配

整套流程里,AI 写代码之前总有知识在等着它:宏观的规范预加载、微观的文件级注入、验证阶段的踩坑触碰检测,以及错误经验写回形成的闭环。


组件 API 查询工具:comp-lookup

三层里的组件 API 层,实现在 comp-lookup 这个 CLI 工具上。它把两类组件文档统一放在 references/{custom,arco}/ 下:

node fetch.mjs NormalTable --api-only   # 查自定义组件
node fetch.mjs form --api-only          # 查 Arco 组件
node fetch.mjs form normal-table --api-only --compact  # 批量查,每组件限 80 行
node fetch.mjs --list                   # 列出所有可查询组件

查询结果自动附带踩坑:

## API

...(Arco Form 完整 Props/Events 表格)...

---

## ⚠️ 已知踩坑(来自 Obsidian vault)

- **Arco Form 必填提示与提交校验脱节** (2026-04-21)
  - tags: vue3, arco-form
  - 详情:`docs/doc/前端通用文档/踩坑记录/2026-04-21-arco-form-校验脱节.md`

> 编码前请阅读以上踩坑,避免重复踩雷。

components 域的组件 key 列表从文件系统动态读取。新增组件文档放进 references/custom/,不需要手动维护任何索引:

_custom_keys = [
    os.path.splitext(f)[0]
    for f in os.listdir(custom_refs)
    if f.endswith('.md')
]

--compact 限制每组件输出 80 行。一个页面可能有十几个组件,不截断会把 AI 上下文撑爆。


recall.sh 里一个值得说的 Bug

recall.sh 处理 .vue 文件时,需要提取最外层 <template> 块里的所有组件。最初用非贪婪正则:

m = re.search(r'<template[^>]*>([\s\S]*?)</template>', content)

[\s\S]*? 遇到第一个 </template> 就停了。问题是这个"第一个"往往是某个 slot 的闭合标签,不是最外层。

实际页面结构:

<template>
  <ContentContainer>
    <template #header>
      <SearchContainer>
        <a-select />  <!-- ❌ 漏掉了 -->
      </SearchContainer>
    </template>
    <template #default>
      <NormalTable />  <!-- ❌ 也漏掉了 -->
    </template>
  </ContentContainer>
</template>

修法是嵌套深度追踪,找到真正与第一个 <template> 对应的闭合位置:

opens  = list(re.finditer(r'<template(?:\s[^>]*)?>', content))
closes = list(re.finditer(r'</template>', content))
depth = 0; i = j = 0; end = None
while i < len(opens) or j < len(closes):
    no = opens[i].start()  if i < len(opens)  else float('inf')
    nc = closes[j].start() if j < len(closes) else float('inf')
    if no < nc:
        depth += 1; i += 1
    else:
        depth -= 1
        if depth == 0:
            end = closes[j].end(); break
        j += 1

这类 bug 不报错,只是静默地少提取几个组件,组件 API 和踩坑就不会被注入。没有回归测试很难发现。


回归测试

recall.sh 涉及 bash + Python + 外部 CLI 调用,边界 case 多,写了 33 个 bash 断言:

── 路径域映射(14 个)
  ✅ core/api/src/payment-orders/index.ts → api
  ✅ apps/merchant/src/layout/default.vue → layout
  ✅ random/path/file.ts → 空(未知路径不注入)

── tool_input 嵌套格式兼容
  ✅ tool_input 嵌套格式 → 含 '规范:api'
  ✅ tool_input.path 格式 → 含 '规范:stores'

── 嵌套 slot template 组件提取
  ✅ 嵌套 template → 识别 select(slot 内)
  ✅ 嵌套 template → 识别 normal-table
  ✅ 嵌套 template → 识别 tag(深层 slot)

── .vue + 域规范双注入
  ✅ .vue in components/ → 组件 API 注入
  ✅ .vue in components/ → 域规范注入

══════════════════════════════════════════════
  结果:PASS=33  FAIL=0  TOTAL=33
══════════════════════════════════════════════

comp-lookup 另有 10 个 Node.js 断言,覆盖截断阈值、踩坑命中强断言、--no-memory 抑制等。


几个设计选择

为什么是 PreToolUse 而不是 PostToolUse?

PostToolUse 在文件写完之后触发,只能用于 lint / 检查。PreToolUse 在写之前触发,注入的内容 AI 写代码时能用到。时序差是整个系统能起作用的前提。

规范内容为什么只取 60 个非空行?

完整规范往往超过 100 行。全部注入反而会淹没关键约束,AI 扫到重要规则的概率下降。60 个非空行覆盖核心规则,超出部分指向原始文件。

未收录组件为什么静默?

项目里有大量布局组件没有独立文档。如果报错,每次编辑布局文件都会出现"查询失败"。有文档的正常显示,没文档的跳过,比报错更有用。

规范双源问题怎么解的?

docs/规范/*.md(Obsidian)和 .claude/rules/*.md(AI 读)两份规范。内容漂移是早晚的事——开始时就已经出现了(rules 写 apps/merchant,docs 写 apps/login)。以 docs/规范/ 为权威源,.claude/rules/ 全部改为软链接,从根本上消除双源。


文件结构

.claude/
├── hooks/
│   ├── recall.sh              # PreToolUse 钩子主体(~250 行 bash + Python)
│   └── recall.test.sh         # 33 个回归测试
└── settings.json              # 钩子注册

.agents/skills/
├── harness-developer/         # 主编码工作流(含记忆预加载)
│   └── SKILL.md
├── harness-verify/            # 代码审查(含组件 API + 踩坑触碰核查)
│   └── SKILL.md
├── api-add/                   # API 生成(含踩坑预检)
│   └── SKILL.md
├── pit-record/                # 踩坑结构化写入
│   └── SKILL.md
└── comp-lookup/               # 组件 API + 踩坑 CLI 查询
    ├── fetch.mjs
    ├── fetch.test.mjs         # 10 个回归测试
    └── references/
        ├── custom/            # 项目自定义组件文档
        └── arco/              # Arco Design 组件文档

docs/doc/前端通用文档/
├── 规范/                      # 10 份规范文档(权威源,软链接到 .claude/rules/)
└── 踩坑记录/
    ├── 踩坑记录.md            # MOC 索引
    └── 2026-04-21-*.md        # 一坑一文件

效果和局限

有效的部分:编辑 core/api/ 下任意文件,AI 自动拿到 API 规范,不再生成绕过 @core/api 的代码。写含 Arco Form 的表单页,AI 在下笔前就已经看到校验脱节那个坑。Verify 阶段发现的新问题,通过 pit-record 写回知识库,下次同类任务自动命中。

还没解决的views/ 下的页面文件踩坑匹配精度还依赖 tags 关键词,不如 paths glob 准。踩坑记录还是人工写,没有从 AI 编码经验自动蒸馏新坑的闭环——这是最想做但还没做的部分。Obsidian 的 search / backlinks API 基本没用上,目前全靠文件系统遍历,扩展性有限。


这套系统的核心很朴素:把团队沉淀的知识(规范、踩坑、组件用法)变成 AI 每次下笔前的上下文,并在发现新问题时自动写回。一个 bash 钩子 + 一组 skill + 几百行 Python 和 JS,没有向量数据库,没有 RAG,没有任何框架依赖。

AI 写出来的代码好不好,最终取决于它在下笔那一刻手里有多少准确的信息。