边做边学 RAG:一个本地运行的个人笔记 RAG Agent,从朴素向量检索一路迭代到 production-grade 完整链路 —— 每一步优化都有 eval 数据证明它值得做。
A hands-on project for learning production-grade RAG: a local AI agent that answers questions over your own markdown notes, built step by step in TypeScript.
把你的 markdown 笔记目录指给它,跑一次索引,然后用自然语言问它任何笔记里的问题 —— 它会综合多个来源给出统一回答,并告诉你答案来自哪里。提供 CLI(REPL) 和 Electron 桌面 app 两种形态。
- 💬 多轮对话:REPL / 桌面 app,持续追问,上下文保留,超长对话自动压缩
- 🔍 混合检索:bge-m3 向量召回 ⊕ BM25/FTS 全文召回 → RRF 融合 → cross-encoder 精排
- 🧠 智能综合:Agent 不罗列原文,把多个 chunk 综合成自然回答,带引用溯源
- ⚡ 流式输出:回答逐字蹦出来,支持 reasoning 思考链展示(桌面 app)
- 🛠️ 4 个工具:list / read / grep / semantic_search,agent 自主选用
- 📈 可量化:内置分层 eval(recall@5 / MRR),每个优化都能跑出前后对比
- 📊 完整可观测:每次调用都在 Langfuse dashboard 看 trace + token 用量
这个项目把 RAG 的"教科书优化"逐个真实落地,且都有实测数据:
| 能力 | 一句话 | 实测效果 |
|---|---|---|
| Contextual Retrieval | 每个 chunk 在 embed 前由 LLM 加 50-100 字上下文摘要 | 召回失败率显著下降(Anthropic 2024 方案) |
| Hybrid + RRF | BM25 补向量对精确术语的盲区,RRF 只看排名绕开量纲 | 术语类查询 recall@5 0.2 → 0.6 |
| Cross-encoder Rerank | bge-reranker-v2-m3 精排,~100ms/次 | 比 LLM-as-reranker 快 600x |
| 结构感知 chunking | 按标题切 + 代码块原子保护 + 三级兜底 | 稳定 chunk 边界,衍生 chunk 级增量 |
| chunk 级增量索引 | 稳定 chunk_id + 两级 diff,改一段只重做那段 | 改 3 文件 → 6 chunk / 4 秒(vs 全量 ~12 分钟) |
| 分层 Eval | 语义组 / 术语组分开测,LLM 反向生成考卷 | 不分层会掩盖 hybrid 的真实价值 |
| 失败处理框架 | 10 类错误分类 + 指数退避 + sanitize + 失败队列 | 3000+ chunks 实测打磨 |
| 意图分类看门人 | 规则 + LLM hint(非硬路由),闲聊不走 RAG | 省钱省延迟 |
用户输入:"我学过哪些 RAG 优化?"
↓
[意图分类 intent.ts] 规则 + DeepSeek 判 4 类 → chitchat 直接答,其余注入 hint
↓
┌─────────────────────────────────────────────────────────────┐
│ Agent Loop (agent.ts) — 大脑,任意 OpenAI 协议 LLM │
│ ├─ system prompt 从 Langfuse Prompt Management 动态拉取 │
│ └─ 决定调哪些工具 │
│ ┌──────────┬──────────┬──────────┬──────────────┐ │
│ │list_files│search_kb │read_file │semantic_search│ │
│ │ (列目录) │ (grep) │ (读全文) │ (RAG) │ │
│ └──────────┴──────────┴──────────┴───────┬──────┘ │
└─────────────────────────────────────────────┬───────────────┘
↓
┌────────────────────────────────┐
│ semantic_search 内部(hybrid): │
│ 1. bge-m3 向量召回 top-20 │
│ 2. FTS/BM25 召回 top-20(ngram)│
│ 3. RRF 融合 → top-20 │
│ 4. bge-reranker-v2-m3 → top-5 │
└────────────────────────────────┘
↓
回答流式输出
↓
Langfuse trace 自动嵌套显示
| 层 | 选型 | 说明 |
|---|---|---|
| 语言 | TypeScript + Node 20+ | — |
| 主对话 LLM | 自配(任意 OpenAI 协议:Kimi / DeepSeek / GLM / Grok…) | baseURL + model + key 三项配置 |
| Contextual LLM(索引时) | DeepSeek-chat | 便宜 + 自动 Prompt Caching |
| Embedding | bge-m3(SiliconFlow 云,默认;或 Ollama 本地) | 1024 维,多语言,两家同模型可互换 |
| Reranker | bge-reranker-v2-m3 via SiliconFlow | Cross-encoder,~100ms |
| Vector DB | LanceDB(嵌入式) | 无 Docker,数据存本地 |
| Observability | Langfuse v4(OpenTelemetry) | Trace + Prompt Management |
| Agent 框架 | 自写 agent loop | streaming + tool use + 上下文压缩,无框架依赖 |
| 桌面 app | Electron + React 18 + Vite | 主进程直接 import 现有 .ts,零重写 |
- Node.js 20+
- 一个 markdown 笔记目录(Obsidian vault、普通 md 文件夹都行)
git clone https://github.com/Chaixueyuan/learn-rag.git
cd learn-rag
npm install| 用途 | 哪里申请 | 必须? |
|---|---|---|
| DeepSeek(Contextual 索引 + 意图分类) | https://platform.deepseek.com/api_keys | ✅ |
| SiliconFlow(embedding + reranker) | https://cloud.siliconflow.cn/account/ak | ✅ |
| Langfuse(observability,免费版够用) | https://cloud.langfuse.com → Project → API Keys | ✅ |
| 主对话 LLM(任意 OpenAI 协议供应商) | 例如 Kimi:https://platform.moonshot.cn/console/api-keys | ✅(选一家即可) |
不想用云端 embedding?装 Ollama +
ollama pull bge-m3,然后.env里EMBED_PROVIDER=ollama。两家都是 bge-m3、1024 维,切换不用重建索引。
cp .env.example .env
# 编辑 .env,填入 keys + 你的笔记目录最小可用配置:
CHAT_BASE_URL=https://api.moonshot.cn/v1 # 主对话 LLM(任意 OpenAI 协议)
CHAT_MODEL=moonshot-v1-32k
CHAT_API_KEY=sk-...
DEEPSEEK_API_KEY=sk-...
SILICONFLOW_API_KEY=sk-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com
KB_ROOT=/path/to/your/markdown/notes # 你的笔记目录在 Langfuse → Prompts → New Prompt:
- Name:
kb-agent-system(agent 按这个名字拉取) - Type:
text - Content: 你的 agent 人设 + 回答规则(例:「你是用户的个人知识库助手,基于检索到的笔记内容综合回答,标注来源,检索不到就明说」)
- Labels: 加
production
Prompt 放 Langfuse 而不是硬编码,是为了体验 production 标配的 Prompt Management:UI 改 prompt、切版本,不用动代码。
npm run index # 首次全量(耗时/成本与笔记量成正比);之后增量,只处理改动
npm run build-fts # 建 FTS 全文索引(hybrid 检索用,建一次)npm run chat
> 我学过哪些 RAG 优化?
🤖 (流式输出综合回答...)
> /exit 退出
> /reset 清空对话历史cd desktop
npm install
npm run dev三栏布局:文件树 | 文档(读/写,自动保存 + 自动增量索引)| AI 助手(对话/搜索)。检索过程可视化:每条答案可展开 4 阶段检索详情(向量/FTS/RRF/rerank 双分数),引用点击跳转原文高亮。详见 desktop/README.md。
npm run search "RRF" # 三路对比:纯向量 / FTS / RRF+rerank
npm run sample 10 # 抽 10 个 chunk 审查 Contextual 摘要质量
npm run intent "你好" # 看意图分类走规则层还是 LLM 层
npm run gen-eval 15 semantic # LLM 反向生成 eval 考卷(语义组:换说法)
npm run gen-eval 15 term # 术语组:保留精确术语
npm run eval # 分层 eval:纯向量 vs hybrid(recall@5 + MRR)
npm run eval-langfuse # eval 上报 Langfuse(Datasets + Experiments)
npm run eval-quality # 答案质量 eval(需先跑 gen-eval term + eval-langfuse)
npm run retry-failed # 修复索引失败的 chunks每个 chunk 在 embedding 前先让 LLM 生成 50-100 字"上下文摘要",拼接后再 embed(来源:Anthropic 2024):
// indexer.ts
const augmented = `[上下文]\n${context}\n\n[原文]\n${chunk}`;
const vec = await embed(augmented);向量检索对"换说法"的语义查询强,但对精确术语(函数名、缩写)有盲区;BM25 恰好相反。RRF 融合只看排名不看分数,天然绕开两路分数量纲不可比的问题。分层 eval 实测:术语组 recall@5 从 0.2 → 0.6。
generateContext()
→ Pass 1: 正常调用(自动重试 rate_limit / 5xx)
→ Pass 2: encoding 错 → sanitize 后重试
→ Pass 3: context 超长 → 截短后重试
→ fail-fast: auth / balance 立刻退出
→ 其他失败:记录进 failed_chunks.json 降级,继续跑,事后 retry-failed
文件级 SHA256 判"哪些文件动了" → chunk 集合 diff 判"哪些段动了" → 按稳定 chunk_id 精准增删:
| 场景 | 代价 |
|---|---|
| 什么都没改再跑 | 秒级,0 成本 |
| 改 3 个文件里各一段 | 6 chunk / 4 秒 |
| 换 chunking 策略 | 自动判定全量重建(configHash 守卫) |
- 交互链路(对话/检索/意图)全量 trace,自动嵌套;离线批处理(索引/eval 生成)刻意不 trace,避免淹没面板
observeOpenAI()自动捕获 model / tokens / latency- System prompt 在 Langfuse UI 动态改,不用发版
用 LLM 反向生成两组考卷(语义组=换说法问、术语组=保留精确术语),分别测纯向量 vs hybrid+rerank 的 recall@5 / MRR。不分层的平均数会掩盖真相:hybrid 对语义组几乎没提升甚至略降,对术语组是质变 —— 这正是它存在的理由。
- 意图分类 / Hybrid+RRF / 分层 Eval / Stable chunking / chunk 级增量 / 答案质量 eval
- Electron 桌面 app:三栏布局、chat streaming + 工具可视化、检索详情 fold、引用跳转高亮、markdown 编辑 + 自动保存 + 自动增量、多会话持久化、wikilink/本地图片渲染、上下文压缩
- chunking 边界:表格 / 数学公式 / 图片(难点清单见 CLAUDE.md)
- 桌面 app 打包分发(electron-builder + LanceDB asarUnpack,见 APP_PLAN_v2.md §11)
- MCP Server:把检索后端暴露给 Claude Code / 其他 agent 消费
MIT