智能开发者文档搜索:RAG 在小团队文档里的落地

发布时间:2026/7/10 5:57:19
智能开发者文档搜索:RAG 在小团队文档里的落地 智能开发者文档搜索RAG 在小团队文档里的落地一、小团队文档搜索的痛点与技术机遇小团队通常缺乏专门的文档管理平台。技术文档分散在多个地方代码注释、Markdown 文件、Wiki 页面、Issue 讨论。开发者需要查找某个 API 用法或配置说明时不得不在多个工具间切换搜索。传统关键词搜索无法理解语义。搜索如何配置数据库时找不到标题为Database Configuration Guide的文档。RAGRetrieval-Augmented Generation技术能够理解查询意图从文档库中检索相关内容并生成精准回答。// 传统文档搜索的问题 // 开发者想查找如何更新用户资料 // 但文档标题是User Profile Update API // 使用 grep 或简单搜索 // $ grep -r 更新用户 docs/ -- 无结果 // $ grep -r update user docs/ -- 可能找到但漏掉中文内容 // RAG 方案的搜索流程 // 1. 将文档分块并转换为向量嵌入 // 2. 将用户查询转换为向量嵌入 // 3. 计算相似度检索最相关的文档块 // 4. 将检索到的内容喂给 LLM 生成回答小团队落地 RAG 的优势成本低可以使用开源嵌入模型如 all-MiniLM-L6-v2无需付费 API部署简单可以使用 Docker Compose 一键部署数据安全文档留在本地不发送到第三方服务定制化强针对团队特定的技术栈优化二、RAG 系统的技术架构与核心组件RAG 系统包含以下核心组件文档加载器从多种来源Markdown、代码注释、Wiki加载文档文本分块器将长文档拆分为适当大小的块Chunk嵌入模型将文本块转换为向量表示向量数据库存储向量和原始文本支持相似度搜索检索器根据用户查询检索相关文档块生成器使用 LLM 基于检索内容生成回答graph TD A[文档源] -- B[文档加载器] B -- C[文本分块] C -- D[嵌入模型] D -- E[向量数据库] F[用户查询] -- G[查询嵌入] G -- H[向量相似度搜索] E -- H H -- I[检索相关文档块] I -- J[提示词构建] J -- K[LLM 生成回答] K -- L[返回回答给用户] style A fill:#e1f5fe style E fill:#fff3e0 style H fill:#f3e5f5 style K fill:#e8f5e9技术选型文档加载LangChain Document Loaders 或自研解析器文本分块递归字符分块、语义分块嵌入模型Sentence Transformers本地、OpenAI Embeddings云端向量数据库Chroma轻量级、Qdrant高性能、PostgreSQL pgvector现有基础设施复用LLMOllama本地、OpenAI API云端场景描述在一开始搭建 RAG 系统时我们直接用 Chroma 的addDocuments方法索引了团队所有 Markdown 文档。看起来一切正常直到有人问如何部署到生产环境——系统返回了包含secretKey: sk-xxx的配置文档片段。排查发现我们的文档目录中有一份带着真实 API Key 的内部操作手册被一起索引了没有任何敏感信息过滤机制。# 索引前必须做的安全清洗 import re def sanitize_before_index(text: str) - str: # 替换常见的敏感模式 text re.sub(r(sk-\w{20,}), [API_KEY_REDACTED], text) text re.sub(r(password\s*[:]\s*[\]?)\S, r\1[REDACTED], text) text re.sub(r(secret\s*[:]\s*[\]?)\S, r\1[REDACTED], text) return text这个教训让我们建立了文档索引的安全审查清单索引前扫描敏感信息、支持文档级别的不索引标记如 frontmatter 中的rag_index: false、提供索引回滚能力。// 使用 LangChain 构建基础 RAG 管道 import { RecursiveCharacterTextSplitter } from langchain/text_splitter; import { HNSWLib } from langchain/vectorstores/hnswlib; import { OpenAIEmbeddings } from langchain/embeddings/openai; import { ChatOpenAI } from langchain/chat_models/openai; import { RetrievalQAChain } from langchain/chains; import { MarkdownTextSplitter } from langchain/text_splitter; export class DocSearchService { private vectorStore: HNSWLib; private qaChain: RetrievalQAChain; private embeddings: OpenAIEmbeddings; private llm: ChatOpenAI; constructor(openaiApiKey: string) { this.embeddings new OpenAIEmbeddings({ openaiApiKey, modelName: text-embedding-3-small // 成本较低的嵌入模型 }); this.llm new ChatOpenAI({ openaiApiKey, modelName: gpt-3.5-turbo, temperature: 0.1 // 低温度确保回答准确性 }); } /** * 初始化向量数据库从文档目录 */ async initialize(documentsDir: string): Promisevoid { // 1. 加载 Markdown 文档 const docs await this.loadMarkdownDocs(documentsDir); // 2. 分块 const splitter new MarkdownTextSplitter({ chunkSize: 500, // 每个块 500 字符 chunkOverlap: 50 // 重叠 50 字符保持上下文连贯 }); const splitDocs await splitter.splitDocuments(docs); console.log(Loaded ${docs.length} documents, split into ${splitDocs.length} chunks); // 3. 创建向量存储 this.vectorStore await HNSWLib.fromDocuments( splitDocs, this.embeddings ); // 4. 创建检索器 const retriever this.vectorStore.asRetriever({ k: 3 // 检索最相关的 3 个文档块 }); // 5. 创建 QA 链 this.qaChain RetrievalQAChain.fromLLM( this.llm, retriever, { returnSourceDocuments: true // 返回源文档便于验证 } ); console.log(RAG system initialized successfully); } /** * 搜索文档问答模式 */ async search(query: string): Promise{ answer: string; sourceDocuments: Array{ pageContent: string; metadata: any }; } { if (!this.qaChain) { throw new Error(RAG system not initialized. Call initialize() first.); } try { const result await this.qaChain.call({ query }); return { answer: result.text, sourceDocuments: result.sourceDocuments }; } catch (error) { console.error(Search failed:, error); throw new Error(Failed to search documents: ${error instanceof Error ? error.message : Unknown error}); } } /** * 仅检索相关文档不使用 LLM 生成 */ async retrieveOnly(query: string, k: number 3): PromiseArray{ content: string; metadata: any; score: number; } { if (!this.vectorStore) { throw new Error(Vector store not initialized); } const results await this.vectorStore.similaritySearchWithScore(query, k); return results.map(([doc, score]) ({ content: doc.pageContent, metadata: doc.metadata, score })); } /** * 加载 Markdown 文档简化实现 */ private async loadMarkdownDocs(dirPath: string): Promiseany[] { const fs require(fs).promises; const path require(path); const files await this.getMarkdownFiles(dirPath); const docs: any[] []; for (const file of files) { try { const content await fs.readFile(file, utf-8); docs.push({ pageContent: content, metadata: { source: file, fileName: path.basename(file), lastModified: (await fs.stat(file)).mtime } }); } catch (error) { console.error(Failed to load document ${file}:, error); } } return docs; } private async getMarkdownFiles(dirPath: string): Promisestring[] { const fs require(fs).promises; const path require(path); let results: string[] []; const items await fs.readdir(dirPath, { withFileTypes: true }); for (const item of items) { const itemPath path.join(dirPath, item.name); if (item.isDirectory()) { const subFiles await this.getMarkdownFiles(itemPath); results results.concat(subFiles); } else if (item.isFile() item.name.endsWith(.md)) { results.push(itemPath); } } return results; } }三、实战本地化 RAG 文档搜索系统以下实现一个完整的本地化 RAG 系统支持文档索引、语义搜索、问答功能并且可以部署在小团队的服务器上。1. 使用 Chroma 向量数据库的完整实现// services/rag/documentIndexer.ts import { Chroma } from langchain/vectorstores/chroma; import { SentenceTransformerEmbeddings } from langchain/embeddings/sentence_transformer; import { TextLoader } from langchain/document_loaders/fs/text; import { MarkdownLoader } from langchain/document_loaders/fs/markdown; import { RecursiveCharacterTextSplitter } from langchain/text_splitter; import * as fs from fs/promises; import * as path from path; export interface DocumentMetadata { source: string; fileName: string; fileType: string; lastModified: Date; tags?: string[]; } export class DocumentIndexer { private embeddings: SentenceTransformerEmbeddings; private vectorStore: Chroma | null null; private collectionName: string; constructor( embeddingsModel: string Xenova/all-MiniLM-L6-v2, collectionName: string team-docs ) { // 使用本地 Sentence Transformers 模型无需 API Key this.embeddings new SentenceTransformerEmbeddings({ modelName: embeddingsModel }); this.collectionName collectionName; } /** * 初始化 Chroma 向量数据库 */ async initVectorStore(): Promisevoid { try { this.vectorStore await Chroma.fromExistingCollection( this.embeddings, { collectionName: this.collectionName } ); console.log(Connected to existing collection: ${this.collectionName}); } catch (error) { // 集合不存在创建新集合 console.log(Creating new collection: ${this.collectionName}); this.vectorStore await Chroma.fromTexts( [], // 初始无文档 [], // 初始无元数据 this.embeddings, { collectionName: this.collectionName } ); } } /** * 索引文档目录 */ async indexDocuments(docsDir: string, options?: { filePatterns?: string[]; ignorePatterns?: string[]; }): Promise{ indexed: number; failed: number } { if (!this.vectorStore) { await this.initVectorStore(); } const filePatterns options?.filePatterns || [.md, .txt, .js, .ts, .tsx]; const ignorePatterns options?.ignorePatterns || [node_modules, dist, .git]; const files await this.scanDirectory(docsDir, filePatterns, ignorePatterns); let indexed 0; let failed 0; for (const file of files) { try { await this.indexFile(file); indexed; console.log(Indexed: ${file}); } catch (error) { console.error(Failed to index ${file}:, error); failed; } } return { indexed, failed }; } /** * 索引单个文件 */ async indexFile(filePath: string): Promisevoid { if (!this.vectorStore) { throw new Error(Vector store not initialized); } // 根据文件类型选择加载器 let loader; if (filePath.endsWith(.md)) { loader new MarkdownLoader(filePath); } else if (filePath.match(/\.(js|ts|tsx|jsx)$/)) { // 使用自定义代码文档加载器 loader new CodeDocumentLoader(filePath); } else { loader new TextLoader(filePath); } const docs await loader.load(); // 分块 const splitter new RecursiveCharacterTextSplitter({ chunkSize: 800, chunkOverlap: 100, separators: [\n## , \n### , \n, \n, , ] }); const splitDocs await splitter.splitDocuments(docs); // 添加元数据 const docsWithMetadata splitDocs.map(doc ({ ...doc, metadata: { ...doc.metadata, indexedAt: new Date().toISOString(), source: filePath, fileName: path.basename(filePath), fileType: path.extname(filePath) } })); // 添加到向量数据库 await this.vectorStore.addDocuments(docsWithMetadata); console.log(Indexed ${docsWithMetadata.length} chunks from ${filePath}); } /** * 扫描目录获取需要索引的文件 */ private async scanDirectory( dirPath: string, filePatterns: string[], ignorePatterns: string[] ): Promisestring[] { let results: string[] []; const items await fs.readdir(dirPath, { withFileTypes: true }); for (const item of items) { const itemPath path.join(dirPath, item.name); // 检查是否应该忽略 if (ignorePatterns.some(pattern itemPath.includes(pattern))) { continue; } if (item.isDirectory()) { const subFiles await this.scanDirectory(itemPath, filePatterns, ignorePatterns); results results.concat(subFiles); } else if (item.isFile()) { const ext path.extname(item.name); if (filePatterns.some(pattern pattern ext || item.name.endsWith(pattern))) { results.push(itemPath); } } } return results; } } /** * 自定义代码文档加载器提取代码注释和文档字符串 */ class CodeDocumentLoader { private filePath: string; constructor(filePath: string) { this.filePath filePath; } async load(): Promiseany[] { const content await fs.readFile(this.filePath, utf-8); const ext path.extname(this.filePath); let docs: any[] []; if ([.js, .ts, .tsx, .jsx].includes(ext)) { // 提取 JSDoc 注释和函数签名 docs this.extractCodeComments(content); } else { // 作为纯文本处理 docs [{ pageContent: content, metadata: { source: this.filePath } }]; } return docs; } private extractCodeComments(code: string): any[] { const docs: any[] []; // 匹配 JSDoc 注释/* ... / const jsdocRegex /\/\*\*([\s\S]*?)\*\//g; let match; while ((match jsdocRegex.exec(code)) ! null) { const comment match[1] .split(\n) .map(line line.replace(/^\s*\*/, ).trim()) .filter(line line.length 0) .join(\n); if (comment.length 0) { docs.push({ pageContent: comment, metadata: { source: this.filePath, type: jsdoc } }); } } // 匹配单行注释// ...-- 仅当后面跟着函数声明时 const singleLineCommentRegex /\/\/\s*(.)/g; while ((match singleLineCommentRegex.exec(code)) ! null) { const nextLine code.split(\n)[match.index]; if (nextLine nextLine.match(/(function|const|class|interface|type)\s/)) { docs.push({ pageContent: match[1].trim(), metadata: { source: this.filePath, type: single-line-comment } }); } } return docs; } }2. 问答服务使用本地 LLM场景描述使用本地 LLM如 Ollama llama2做 RAG 问答时最大的问题不是速度而是输出格式。LLM 有时会忽略 Prompt 中的指令生成 markdown 表格、多轮对话、甚至追加一些这是我找到的相关信息…的元描述文字。如果问答 API 的调用方是一个 Slack Bot这些格式化的输出会让 Bot 的体验很糟糕。# 踩坑LLM 输出追加了不需要的元信息 answer: 你需要修改 settings.py 中的 DATABASE_URL... \n\n*来源: docs/deployment.md* # 解决办法在 Prompt 中明确禁止追加信息 prompt_template 仅回答用户的提问不要追加任何引用来源、元描述或额外评论。 如果文档中没有相关信息直接回答未找到相关文档。 另一个需要注意的细节是 Ollama 模型的首次响应延迟。每个新的查询都可能触发模型加载到内存7B 模型首次推理可能需要 10-15 秒。通过在服务启动时 warm-up发送一条空查询可以显著减少首次用户请求的等待时间。// services/rag/qaService.ts import { Chroma } from langchain/vectorstores/chroma; import { Ollama } from langchain/llms/ollama; import { PromptTemplate } from langchain/prompts; import { RunnableSequence } from langchain/schema/runnable; export class DocQAService { private vectorStore: Chroma; private llm: Ollama; private qaChain: RunnableSequence; constructor(vectorStore: Chroma, ollamaModel: string llama2) { this.vectorStore vectorStore; // 使用 Ollama 运行本地 LLM无需 API Key this.llm new Ollama({ model: ollamaModel, temperature: 0.1 }); this.initQASystem(); } /** * 初始化问答系统 */ private async initQASystem(): Promisevoid { // 定义提示词模板 const promptTemplate PromptTemplate.fromTemplate( You are a technical documentation assistant for a development team. Use the following pieces of context to answer the question at the end. If you dont know the answer, just say that you dont know. Dont try to make up an answer. Context: {context} Question: {question} Helpful Answer (in the same language as the question): ); // 创建检索器 const retriever this.vectorStore.asRetriever({ k: 4, // 检索 4 个相关文档块 searchType: similarity // 相似度搜索 }); // 构建 RAG 链 this.qaChain RunnableSequence.from([ { context: async (input: { question: string }) { const docs await retriever.getRelevantDocuments(input.question); return docs.map(doc doc.pageContent).join(\n\n); }, question: (input: { question: string }) input.question }, promptTemplate, this.llm ]); } /** * 问答接口 */ async ask(question: string): Promise{ answer: string; sources: Array{ content: string; source: string }; } { try { // 检索相关文档 const relevantDocs await this.vectorStore.similaritySearch(question, 4); // 生成回答 const answer await this.qaChain.invoke({ question }); return { answer: typeof answer string ? answer : answer.content, sources: relevantDocs.map(doc ({ content: doc.pageContent.substring(0, 200) ..., // 截断显示 source: doc.metadata.source || Unknown })) }; } catch (error) { console.error(QA failed:, error); throw new Error(Failed to answer question: ${error instanceof Error ? error.message : Unknown error}); } } /** * 搜索相关文档不使用 LLM */ async searchDocuments(query: string, k: number 5): PromiseArray{ content: string; source: string; score: number; } { try { const results await this.vectorStore.similaritySearchWithScore(query, k); return results.map(([doc, score]) ({ content: doc.pageContent, source: doc.metadata.source || Unknown, score })); } catch (error) { console.error(Document search failed:, error); throw error; } } }3. REST API 服务供团队使用// api/ragServer.ts import express from express; import { DocumentIndexer } from ../services/rag/documentIndexer; import { DocQAService } from ../services/rag/qaService; import { Chroma } from langchain/vectorstores/chroma; export function createRAGServer() { const app express(); app.use(express.json()); let qaService: DocQAService | null null; // 初始化端点 app.post(/api/rag/init, async (req, res) { try { const { docsDir, collectionName } req.body; // 索引文档 const indexer new DocumentIndexer(Xenova/all-MiniLM-L6-v2, collectionName); const result await indexer.indexDocuments(docsDir); // 初始化 QA 服务 const vectorStore await Chroma.fromExistingCollection( indexer[embeddings], { collectionName } ); qaService new DocQAService(vectorStore, llama2); res.json({ success: true, message: RAG system initialized, result }); } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : Unknown error }); } }); // 问答端点 app.post(/api/rag/ask, async (req, res) { if (!qaService) { return res.status(400).json({ success: false, error: RAG system not initialized. Call /api/rag/init first. }); } try { const { question } req.body; if (!question || typeof question ! string) { return res.status(400).json({ success: false, error: Invalid question parameter }); } const result await qaService.ask(question); res.json({ success: true, question, answer: result.answer, sources: result.sources }); } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : Unknown error }); } }); // 文档搜索端点不使用 LLM app.post(/api/rag/search, async (req, res) { if (!qaService) { return res.status(400).json({ success: false, error: RAG system not initialized }); } try { const { query, k 5 } req.body; const results await qaService.searchDocuments(query, k); res.json({ success: true, query, results }); } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : Unknown error }); } }); // 索引状态端点 app.get(/api/rag/status, async (req, res) { try { // 这里可以添加获取索引统计信息的逻辑 res.json({ success: true, initialized: qaService ! null }); } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : Unknown error }); } }); const PORT process.env.PORT || 3003; app.listen(PORT, () { console.log(RAG server running on port ${PORT}); }); return app; } // 启动服务器 if (require.main module) { createRAGServer(); }四、RAG 系统的优化与工程化1. 文档分块策略优化不同的分块策略对检索效果影响显著。// 多种分块策略对比 export class ChunkingStrategy { /** * 策略 1固定大小分块简单但可能切断语义 */ static fixedSizeChunking(text: string, chunkSize: number 500, overlap: number 50): string[] { const chunks: string[] []; for (let i 0; i text.length; i chunkSize - overlap) { chunks.push(text.substring(i, i chunkSize)); } return chunks; } /** * 策略 2语义分块按段落或章节分块 */ static semanticChunking(markdown: string): string[] { const chunks: string[] []; // 按 Markdown 标题分块 const sections markdown.split(/\n(?#{1,3}\s)/); let currentChunk ; for (const section of sections) { if (currentChunk.length section.length 800) { // 当前块已满保存并开始新块 chunks.push(currentChunk); currentChunk section; } else { currentChunk section; } } if (currentChunk) { chunks.push(currentChunk); } return chunks; } /** * 策略 3递归分块LangChain 默认策略推荐 */ static async recursiveChunking(text: string): Promiseany[] { const splitter new RecursiveCharacterTextSplitter({ chunkSize: 800, chunkOverlap: 100, separators: [ \n## , // Markdown 二级标题 \n### , // Markdown 三级标题 \n, // 代码块 \n\n, // 段落 \n, // 换行 。, // 中文句号 . , // 英文句号 , // 空格 // 字符级 ] }); return splitter.splitText(text); } }2. 混合检索向量搜索 关键词搜索提升检索准确率。场景描述纯向量搜索在面对API 接口版本变更这类查询时有时会返回不相关但语义相近的文档——比如把版本变更的查询匹配到了代码审查流程变更的文档因为它们都包含变更这个词的语义向量。这就是向量搜索会匹配语义但不够精确的典型问题。混合检索的 RRFReciprocal Rank Fusion方案可以在一定程度上缓解这个问题关键词搜索对API和版本做精确匹配向量搜索提供语义补充。但 RRF 的权重调优是个经验活——如果keywordWeight0.7又回到了关键词匹配即搜索结果的原始问题。通常在文档量不大1000 篇时使用keywordWeight0.3, vectorWeight0.7当文档量增长到数千篇后调整为keywordWeight0.5, vectorWeight0.5——因为语义搜索在大规模文档中更容易产生不相关匹配。// 混合检索实现 export class HybridRetriever { private vectorStore: Chroma; private keywordWeight: number 0.3; private vectorWeight: number 0.7; constructor(vectorStore: Chroma, options?: { keywordWeight?: number; vectorWeight?: number }) { this.vectorStore vectorStore; if (options) { this.keywordWeight options.keywordWeight ?? 0.3; this.vectorWeight options.vectorWeight ?? 0.7; } } /** * 混合检索向量相似度 BM25 关键词匹配 */ async retrieve(query: string, k: number 5): PromiseArray{ document: any; score: number; vectorScore?: number; keywordScore?: number; } { // 1. 向量检索 const vectorResults await this.vectorStore.similaritySearchWithScore(query, k * 2); // 2. 关键词检索简化版 BM25 const keywordResults await this.keywordSearch(query, k * 2); // 3. 合并结果RRF: Reciprocal Rank Fusion const merged this.reciprocalRankFusion(vectorResults, keywordResults, k); return merged; } /** * 简化的关键词搜索实际应使用 Elasticsearch 或 BM25 库 */ private async keywordSearch(query: string, k: number): PromiseArray{ document: any; score: number } { // 这里简化为从向量数据库获取所有文档然后进行关键词匹配 // 实际生产中应使用专门的关键词搜索引擎 const allDocs await this.vectorStore.similaritySearch(query, 100); // 获取较多文档 const queryTerms query.toLowerCase().split(/\s/); const scored allDocs.map(doc { const content doc.pageContent.toLowerCase(); let score 0; for (const term of queryTerms) { const matches (content.match(new RegExp(term, g)) || []).length; score matches; } return { document: doc, score: score / queryTerms.length }; }); return scored .sort((a, b) b.score - a.score) .slice(0, k); } /** * 倒数排名融合RRF */ private reciprocalRankFusion( vectorResults: Array[any, number], keywordResults: Array{ document: any; score: number }, k: number ): Array{ document: any; score: number; vectorScore?: number; keywordScore?: number } { const scores: Mapstring, { document: any; score: number; vectorScore?: number; keywordScore?: number } new Map(); // 处理向量检索结果 vectorResults.forEach(([doc, score], rank) { const key doc.pageContent; // 使用内容作为键简化处理 const rrfScore 1 / (rank 60); // k60 是标准 RRF 常数 scores.set(key, { document: doc, score: rrfScore * this.vectorWeight, vectorScore: score }); }); // 处理关键词检索结果 keywordResults.forEach((result, rank) { const key result.document.pageContent; const rrfScore 1 / (rank 60); const existing scores.get(key); if (existing) { existing.score rrfScore * this.keywordWeight; existing.keywordScore result.score; } else { scores.set(key, { document: result.document, score: rrfScore * this.keywordWeight, keywordScore: result.score }); } }); return Array.from(scores.values()) .sort((a, b) b.score - a.score) .slice(0, k); } }3. 自动化评估与持续优化建立评估流程量化 RAG 系统性能。// RAG 系统评估 export class RAGEvaluator { /** * 评估检索准确率需要标注数据集 */ async evaluateRetrieval( testQueries: Array{ query: string; relevantDocs: string[] } ): Promise{ precision: number; recall: number; mrr: number } { let totalPrecision 0; let totalRecall 0; let totalMRR 0; // Mean Reciprocal Rank for (const test of testQueries) { const retrieved await this.retrieveDocuments(test.query, 5); const retrievedIds retrieved.map(doc doc.metadata.source); // 计算 PrecisionK const relevantRetrieved retrievedIds.filter(id test.relevantDocs.includes(id)); const precision relevantRetrieved.length / retrievedIds.length; // 计算 RecallK const recall relevantRetrieved.length / test.relevantDocs.length; // 计算 MRR let mrr 0; for (let i 0; i retrievedIds.length; i) { if (test.relevantDocs.includes(retrievedIds[i])) { mrr 1 / (i 1); break; } } totalPrecision precision; totalRecall recall; totalMRR mrr; } return { precision: totalPrecision / testQueries.length, recall: totalRecall / testQueries.length, mrr: totalMRR / testQueries.length }; } private async retrieveDocuments(query: string, k: number): Promiseany[] { // 调用实际的检索函数 return []; } }五、总结RAG 技术在小团队文档搜索场景中具有显著优势。通过本地化部署可以在保护数据安全的同时提供语义搜索和智能问答能力。关键实践包括选择合适的嵌入模型和向量数据库、优化文档分块策略、实现混合检索、建立评估体系。对于资源有限的团队可以从开源方案起步逐步优化效果。RAG 系统的落地是一个持续迭代的过程。建议从小规模试点开始收集团队反馈逐步扩展文档覆盖范围和优化检索质量。);