Skip to content

RAG 工作流指南

RAG(Retrieval-Augmented Generation,检索增强生成)是将你的知识库与 AI 结合的最佳方式。本文介绍如何在 Obsidian 中构建 RAG 工作流。

什么是 RAG?

RAG 是一种 AI 应用模式,先从知识库中检索相关内容,再将检索结果作为上下文提供给大语言模型,生成更准确、更相关的回答。

text
用户提问

知识库检索 → 找到相关笔记

组装 Prompt(提问 + 相关笔记内容)

大语言模型生成回答

基于你的知识库的精准回答

RAG vs 直接提问

方式准确性上下文幻觉风险
直接问 AI通用
RAG 问答你的知识

RAG 架构

核心组件

text
┌─────────────────────────────────────────┐
│              RAG 工作流                   │
├─────────────────────────────────────────┤
│                                         │
│  1. 文档处理                             │
│     笔记 → 分块 → 向量化 → 存储          │
│                                         │
│  2. 检索                                │
│     问题 → 向量化 → 相似度搜索 → 结果     │
│                                         │
│  3. 生成                                │
│     问题 + 检索结果 → LLM → 回答         │
│                                         │
└─────────────────────────────────────────┘

向量数据库选择

数据库特点适合
ChromaDB开源、轻量本地部署
Pinecone云端、快速生产环境
Qdrant高性能、Rust大规模
LanceDB嵌入式、零配置个人项目

使用 Smart Connections

Smart Connections 是 Obsidian 最流行的 RAG 插件。

安装配置

  1. 安装 Smart Connections 社区插件
  2. 配置嵌入模型(推荐 OpenAI 或本地模型)
  3. 等待笔记索引完成

基本使用

  1. 打开 Smart Connections 面板
  2. 输入问题或选择笔记
  3. 查看相关笔记和 AI 回答

配置选项

设置说明推荐值
Embed Model嵌入模型text-embedding-3-small
Chat Model对话模型gpt-4o-mini
Chunk Size分块大小1000
Chunk Overlap分块重叠200
Top K返回结果数5

使用本地模型

如果不想使用云 API,可以配置本地模型:

  1. 安装 Ollama
  2. 拉取嵌入模型:ollama pull nomic-embed-text
  3. 拉取对话模型:ollama pull llama3
  4. 在 Smart Connections 中配置 Ollama 端点

使用 Copilot 插件

Obsidian Copilot 也支持 RAG 模式。

配置 RAG

  1. 安装 Copilot 插件
  2. 配置 API Key
  3. 启用「Vault QA」模式
  4. 等待索引完成

使用方式

text
# 在聊天中引用笔记
@笔记名 你的问题

# 搜索整个知识库
@vault 你的问题

# 引用特定文件夹
@folder:Projects 你的问题

自建 RAG 系统

架构设计

text
Obsidian 仓库
    ↓ (文件监听)
文档处理器
    ↓ (分块 + 嵌入)
向量数据库 (ChromaDB)
    ↑ (检索)
API 服务 (FastAPI)
    ↑ (HTTP)
Obsidian 插件 / 聊天界面

后端服务

python
# server.py — RAG 后端服务
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import chromadb
from openai import OpenAI
import os

app = FastAPI()

# 初始化 ChromaDB
chroma_client = chromadb.PersistentClient(path="./chroma_db")
collection = chroma_client.get_or_create_collection("obsidian_notes")

# 初始化 OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def get_embedding(text: str) -> list[float]:
    """获取文本嵌入向量"""
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text,
    )
    return response.data[0].embedding

def query_notes(question: str, top_k: int = 5) -> list[dict]:
    """查询相关笔记"""
    query_embedding = get_embedding(question)
    
    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=top_k,
    )
    
    notes = []
    for i in range(len(results["ids"][0])):
        notes.append({
            "id": results["ids"][0][i],
            "content": results["documents"][0][i],
            "metadata": results["metadatas"][0][i],
            "distance": results["distances"][0][i],
        })
    
    return notes

def generate_answer(question: str, context_notes: list[dict]) -> str:
    """基于上下文生成回答"""
    context = "\n\n---\n\n".join(
        f"来源: {note['metadata']['source']}\n{note['content']}"
        for note in context_notes
    )
    
    prompt = f"""基于以下笔记内容回答问题。如果无法从笔记中找到答案,请说明。

笔记内容:
{context}

问题:{question}

回答:"""

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "你是一个知识库助手,基于用户的笔记回答问题。"},
            {"role": "user", "content": prompt},
        ],
    )
    
    return response.choices[0].message.content

# API 端点
class QuestionRequest(BaseModel):
    question: str
    top_k: int = 5

class QuestionResponse(BaseModel):
    answer: str
    sources: list[dict]

@app.post("/ask", response_model=QuestionResponse)
async def ask_question(request: QuestionRequest):
    """问答接口"""
    notes = query_notes(request.question, request.top_k)
    answer = generate_answer(request.question, notes)
    
    return QuestionResponse(
        answer=answer,
        sources=[{"source": n["metadata"]["source"], "distance": n["distance"]} for n in notes],
    )

@app.post("/index")
async def index_note(path: str, content: str):
    """索引笔记"""
    # 简单分块
    chunk_size = 1000
    chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
    
    for i, chunk in enumerate(chunks):
        embedding = get_embedding(chunk)
        collection.upsert(
            ids=[f"{path}_{i}"],
            embeddings=[embedding],
            documents=[chunk],
            metadatas=[{"source": path, "chunk": i}],
        )
    
    return {"status": "ok", "chunks": len(chunks)}

文档处理脚本

python
# indexer.py — 索引 Obsidian 仓库
import os
import frontmatter
from pathlib import Path

def process_vault(vault_path: str):
    """处理整个仓库"""
    for root, dirs, files in os.walk(vault_path):
        # 跳过 .obsidian 目录
        if ".obsidian" in dirs:
            dirs.remove(".obsidian")
        
        for file in files:
            if not file.endswith(".md"):
                continue
            
            file_path = os.path.join(root, file)
            rel_path = os.path.relpath(file_path, vault_path)
            
            with open(file_path, "r", encoding="utf-8") as f:
                post = frontmatter.load(f)
                content = post.content
            
            # 调用 API 索引
            import requests
            requests.post("http://localhost:8000/index", params={
                "path": rel_path,
                "content": content,
            })
            
            print(f"已索引: {rel_path}")

if __name__ == "__main__":
    process_vault("/path/to/vault")

Obsidian 插件集成

typescript
// 简单的 RAG 插件
import { Plugin, Notice } from "obsidian";

interface RAGResponse {
  answer: string;
  sources: { source: string; distance: number }[];
}

export default class RAGPlugin extends Plugin {
  private apiUrl = "http://localhost:8000";

  async onload() {
    this.addCommand({
      id: "ask-rag",
      name: "Ask your knowledge base",
      callback: () => this.askQuestion(),
    });
  }

  async askQuestion() {
    // 获取用户输入
    const question = await this.getInput("输入你的问题:");
    if (!question) return;

    try {
      const response = await fetch(`${this.apiUrl}/ask`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ question, top_k: 5 }),
      });

      const data: RAGResponse = await response.json();

      // 显示回答
      const md = `## AI 回答\n\n${data.answer}\n\n### 来源\n${data.sources.map((s) => `- ${s.source}`).join("\n")}`;

      // 创建回答笔记或显示通知
      const noteName = `AI-Answer-${Date.now()}`;
      await this.app.vault.create(`${noteName}.md`, md);
      new Notice("回答已生成");
    } catch (error) {
      new Notice("请求失败: " + error.message);
    }
  }

  async getInput(prompt: string): Promise<string | null> {
    return new Promise((resolve) => {
      const modal = new InputModal(this.app, prompt, resolve);
      modal.open();
    });
  }
}

优化 RAG 效果

分块策略

策略说明适合
固定大小按字符数分割通用
段落分割按段落分割长文档
语义分割按语义变化分割精确检索
Markdown 标题按标题分割结构化笔记

检索优化

  1. 混合检索:结合向量搜索和关键词搜索
  2. 重排序:使用 Cross-Encoder 对结果重排序
  3. 查询扩展:将用户问题扩展为多个查询
  4. 过滤:按标签、时间、路径过滤结果

Prompt 优化

text
你是一个专业的知识库助手。请严格基于提供的笔记内容回答问题。

规则:
1. 只基于笔记内容回答,不要编造信息
2. 引用具体的笔记来源
3. 如果笔记中没有相关信息,明确说明
4. 保持回答简洁、准确
5. 如果有多个相关笔记,综合所有信息

笔记内容:
{context}

问题:{question}

常见问题

索引需要多长时间?

取决于笔记数量和嵌入模型速度:

  • 100 篇笔记:1-2 分钟
  • 1000 篇笔记:10-20 分钟
  • 10000 篇笔记:1-2 小时

RAG 回答不准确?

  1. 检查分块大小是否合适
  2. 增加检索结果数量(top_k)
  3. 使用更好的嵌入模型
  4. 优化 Prompt

成本如何控制?

  1. 使用本地模型(Ollama + 开源模型)
  2. 使用小模型生成(gpt-4o-mini
  3. 批量嵌入而非逐条处理
  4. 缓存嵌入结果避免重复计算

相关资源