本文记录一次真实的工程实践:在 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 写出来的代码好不好,最终取决于它在下笔那一刻手里有多少准确的信息。