第21讲|RAG 实战:给 AI 注入你的私有知识库

5 阅读1分钟

金句:LLM 知道全世界的知识,但不知道你公司的数据。RAG 是一座桥,把你的私有知识和 AI 的通用能力连接起来。


一、为什么需要 RAG?

场景:你让 AI 帮你写代码,但 AI 不知道:

  • 你们公司内部的 API 规范
  • 你们的业务领域模型(DDD 术语)
  • 你们的历史技术决策(ADR 文档)
  • 你们的代码注释约定和工程实践

这时候,AI 只能给出"通用"的答案,而不是"符合你们具体情况"的答案。

RAG(Retrieval-Augmented Generation,检索增强生成) 解决了这个问题。


二、RAG 工作原理

传统 LLM 调用:
用户问题 → LLM → 基于训练数据回答

RAG 流程:
用户问题 → [检索层] → 相关文档 ↓
                              → LLM → 基于文档+训练数据回答

具体步骤

1. 知识库构建(离线)
   文档 → 切分 → 向量化 → 存入向量数据库

2. 查询时(在线)
   用户问题 → 向量化 → 相似度检索 → 找到相关文档片段
   → 构建提示词(问题 + 相关文档) → LLM 生成答案

三、构建代码知识库 RAG 系统

架构设计

# 技术选型
向量数据库: ChromaDB(本地部署,免费)
Embedding 模型: text-embedding-3-small(OpenAI)
LLM: GPT-4o / Claude-3.5-sonnet
文档处理: LangChain

完整实现

# code_knowledge_rag.py
import os
from pathlib import Path
from langchain_community.document_loaders import (
    TextLoader, 
    DirectoryLoader,
    MarkdownLoader
)
from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,
    Language
)
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

class CodeKnowledgeRAG:
    def __init__(self, knowledge_base_path: str, persist_dir: str = "./chroma_db"):
        self.knowledge_base_path = knowledge_base_path
        self.persist_dir = persist_dir
        self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
        self.vectorstore = None
        
    def build_index(self):
        """构建知识库索引"""
        print(f"📚 正在索引知识库:{self.knowledge_base_path}")
        
        # 加载不同类型的文档
        documents = []
        
        # 加载 Markdown 文档
        md_loader = DirectoryLoader(
            self.knowledge_base_path,
            glob="**/*.md",
            loader_cls=TextLoader,
            loader_kwargs={'encoding': 'utf-8'}
        )
        documents.extend(md_loader.load())
        
        # 加载 TypeScript/JavaScript 代码
        ts_loader = DirectoryLoader(
            self.knowledge_base_path,
            glob="**/*.ts",
            loader_cls=TextLoader,
            loader_kwargs={'encoding': 'utf-8'}
        )
        documents.extend(ts_loader.load())
        
        # 加载 Python 代码
        py_loader = DirectoryLoader(
            self.knowledge_base_path,
            glob="**/*.py",
            loader_cls=TextLoader,
            loader_kwargs={'encoding': 'utf-8'}
        )
        documents.extend(py_loader.load())
        
        print(f"✅ 加载了 {len(documents)} 个文档")
        
        # 智能切分:代码文件用代码感知切分器
        code_splitter = RecursiveCharacterTextSplitter.from_language(
            language=Language.PYTHON,
            chunk_size=1000,
            chunk_overlap=200,
        )
        
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=1000,
            chunk_overlap=200,
            separators=["\n## ", "\n### ", "\n\n", "\n", " "]
        )
        
        chunks = []
        for doc in documents:
            if doc.metadata.get('source', '').endswith('.py'):
                chunks.extend(code_splitter.split_documents([doc]))
            else:
                chunks.extend(text_splitter.split_documents([doc]))
        
        print(f"✅ 切分为 {len(chunks)} 个文本块")
        
        # 创建向量索引
        self.vectorstore = Chroma.from_documents(
            documents=chunks,
            embedding=self.embeddings,
            persist_directory=self.persist_dir
        )
        
        print(f"✅ 向量索引已创建,保存至 {self.persist_dir}")
    
    def load_index(self):
        """加载已有索引"""
        if Path(self.persist_dir).exists():
            self.vectorstore = Chroma(
                persist_directory=self.persist_dir,
                embedding_function=self.embeddings
            )
            print(f"✅ 已加载向量索引")
        else:
            raise FileNotFoundError(f"索引目录不存在:{self.persist_dir}")
    
    def ask(self, question: str, k: int = 5) -> dict:
        """基于知识库回答问题"""
        if not self.vectorstore:
            raise RuntimeError("请先调用 build_index() 或 load_index()")
        
        # 构建自定义提示词模板
        template = """你是一个代码助手,专门回答关于项目代码库的问题。
请基于以下检索到的相关代码和文档来回答问题。

如果文档中没有相关信息,请明确说明"在提供的代码库中没有找到相关信息"。
不要编造代码或信息。

相关代码/文档:
{context}

问题:{question}

请用中文回答,并指出你引用的是哪个文件中的内容:"""

        PROMPT = PromptTemplate(
            template=template,
            input_variables=["context", "question"]
        )
        
        llm = ChatOpenAI(model="gpt-4o", temperature=0)
        
        qa_chain = RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff",
            retriever=self.vectorstore.as_retriever(
                search_type="mmr",  # 使用 MMR 算法提高多样性
                search_kwargs={"k": k, "fetch_k": 20}
            ),
            chain_type_kwargs={"prompt": PROMPT},
            return_source_documents=True
        )
        
        result = qa_chain({"query": question})
        
        # 整理来源文件
        sources = list(set([
            doc.metadata.get('source', 'unknown')
            for doc in result['source_documents']
        ]))
        
        return {
            "answer": result['result'],
            "sources": sources
        }


# 使用示例
if __name__ == '__main__':
    rag = CodeKnowledgeRAG(
        knowledge_base_path="./docs",  # 文档目录
        persist_dir="./chroma_db"
    )
    
    # 首次使用:构建索引
    rag.build_index()
    
    # 之后直接加载:
    # rag.load_index()
    
    # 提问
    questions = [
        "我们的用户认证是如何实现的?使用了什么技术?",
        "如何添加一个新的 API 端点?有什么规范要求?",
        "数据库查询应该遵循什么规范?",
        "如何处理异步错误?"
    ]
    
    for q in questions:
        print(f"\n❓ 问题:{q}")
        result = rag.ask(q)
        print(f"💡 回答:{result['answer']}")
        print(f"📄 来源:{', '.join(result['sources'])}")
        print("-" * 60)

四、在 Cursor 中集成私有知识库

方法一:@Docs 功能

Cursor 内置了 @Docs 功能,可以添加自定义文档:

1. 在 Cursor 设置中找到 Docs
2. 添加你的内部文档 URL 或本地路径
3. 在对话中使用 @YourDocs 引用

方法二:MCP + RAG 集成

// rag-mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';

// 将 RAG 系统封装为 MCP 工具
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'search_knowledge_base',
    description: '在公司内部知识库中搜索相关信息',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: '搜索查询' }
      },
      required: ['query']
    }
  }]
}));

五、RAG 质量优化技巧

技巧一:Chunk 策略优化

# 错误示例:按固定字符数切分代码
text_splitter = CharacterTextSplitter(chunk_size=500)

# 正确示例:代码感知切分,保持函数完整性
code_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=2000,  # 代码块允许更大
    chunk_overlap=400  # 更多重叠,保持上下文
)

技巧二:混合检索

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

# 结合向量检索(语义)和 BM25(关键词)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
bm25_retriever = BM25Retriever.from_documents(documents, k=5)

ensemble_retriever = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.7, 0.3]  # 语义搜索权重更高
)

技巧三:元数据过滤

# 只搜索后端相关文档
results = vectorstore.similarity_search(
    query,
    filter={"module": "backend"}
)

章节小结:RAG 是让 AI 理解你私有知识的核心技术。通过向量化存储代码文档、架构规范和历史决策,AI 在回答你的问题时不再是"泛泛而谈",而是基于你的具体项目给出精准建议。这是 Vibe Coding 从"工具辅助"升级为"智能协作"的关键技术。