RAG 检索增强生成从零实现:手把手教你构建企业级知识库问答系统

深入解析 RAG 检索增强生成架构原理,从向量嵌入、相似度检索到上下文注入,完整实现一个可落地的知识库问答系统,含性能优化与避坑指南。

前端开发 2026-06-09 15 分钟

RAG(Retrieval Augmented Generation,检索增强生成)已经成为 2026 年企业级 AI 应用的标配架构。据统计,超过 78% 的生产级 LLM 应用采用了某种形式的 RAG,因为它能有效解决大模型「幻觉」和知识过时两大核心痛点。如果你还在让大模型凭空回答专业问题,是时候认真了解 RAG 了。

本文将从零开始,用纯 JavaScript 实现一个完整的 RAG 管线,涵盖文档切分、向量嵌入、相似度检索、上下文注入四大核心环节。不仅讲原理,更注重实战——每一个代码示例都可以直接运行,每一个优化建议都来自真实踩坑经验。

🧠 一、RAG 核心原理与架构设计

1.1 为什么需要 RAG?

大语言模型(LLM)有三个致命缺陷:

  • 知识截断:训练数据有截止日期,无法回答最新问题
  • 幻觉问题:对不确定的事实会「一本正经地胡说八道」
  • 缺乏私域知识:不了解你的企业文档、内部 Wiki、产品手册

RAG 的核心思路非常简单:先检索,再生成。在用户提问时,先从知识库中检索相关文档片段,然后把这些片段作为上下文注入到 Prompt 中,让大模型基于真实资料回答。

📌 记住: RAG 的本质是把「让模型记知识」变成「让模型查资料」。这就像开卷考试和闭卷考试的区别——开卷考试的答案质量天然更高。

1.2 完整 RAG 架构

一个生产级 RAG 系统包含以下核心模块:

文档摄入 → 文本切分 → 向量嵌入 → 向量存储
                                      ↓
用户提问 → 查询嵌入 → 相似度检索 → 上下文组装 → LLM 生成回答
阶段 核心任务 关键技术 性能瓶颈
文档摄入 解析 PDF/Word/HTML 文本提取、格式清洗 文件格式兼容性
文本切分 将长文档拆分为语义段 滑动窗口、语义切分 切分粒度控制
向量嵌入 将文本转为数值向量 Embedding 模型 API 调用延迟
向量存储 高效存储和检索向量 ANN 索引算法 大规模检索速度
相似度检索 找到最相关的文档片段 余弦相似度、HNSW 检索召回率
上下文注入 组装 Prompt 提示词工程 上下文窗口限制

1.3 文本切分策略

文本切分是 RAG 中最容易被低估的环节。切分质量直接决定检索质量。

⚠️ 警告: 切分粒度太粗会导致检索不精确,太细会丢失上下文。经验法则是每个 chunk 200-500 token,overlap 10-20%。

以下是三种常见切分策略的实现:

// 文本切分器:支持三种策略
class TextSplitter {
  /**
   * 固定长度切分 — 最简单但最粗暴
   * 问题:可能在句子中间截断,破坏语义完整性
   */
  static fixedSize(text, chunkSize = 500, overlap = 50) {
    const chunks = []
    for (let i = 0; i < text.length; i += chunkSize - overlap) {
      chunks.push(text.slice(i, i + chunkSize))
    }
    return chunks
  }

  /**
   * 按段落切分 — 保留自然语义边界
   * 适合:Markdown、HTML 等结构化文档
   */
  static byParagraph(text, maxSize = 1000) {
    const paragraphs = text.split(/\n\n+/)
    const chunks = []
    let current = ''

    for (const para of paragraphs) {
      if ((current + '\n\n' + para).length > maxSize && current) {
        chunks.push(current.trim())
        current = para
      } else {
        current = current ? current + '\n\n' + para : para
      }
    }
    if (current.trim()) chunks.push(current.trim())
    return chunks
  }

  /**
   * 滑动窗口切分 — 最佳平衡方案
   * 保证每个 chunk 有前后 overlap,不丢失上下文
   */
  static slidingWindow(text, chunkSize = 500, overlap = 100) {
    const sentences = text.match(/[^。!?.!?\n]+[。!?.!?\n]*/g) || [text]
    const chunks = []
    let currentChunk = ''
    let currentLen = 0

    for (const sentence of sentences) {
      if (currentLen + sentence.length > chunkSize && currentChunk) {
        chunks.push(currentChunk.trim())
        // 保留最后 overlap 长度的内容作为下一个 chunk 的开头
        const overlapText = currentChunk.slice(-overlap)
        currentChunk = overlapText + sentence
        currentLen = overlapText.length + sentence.length
      } else {
        currentChunk += sentence
        currentLen += sentence.length
      }
    }
    if (currentChunk.trim()) chunks.push(currentChunk.trim())
    return chunks
  }
}

// 测试三种切分策略
const doc = 'RAG 是一种架构模式。它通过检索外部知识来增强 LLM 的回答质量。'.repeat(20)

console.log('固定长度:', TextSplitter.fixedSize(doc, 100, 20).length, '个 chunks')
console.log('段落切分:', TextSplitter.byParagraph(doc, 200).length, '个 chunks')
console.log('滑动窗口:', TextSplitter.slidingWindow(doc, 100, 20).length, '个 chunks')

💡 提示: 实际生产中推荐使用「递归字符切分」策略——先按段落切,段落太大再按句子切,句子太大再按字符切。LangChain 的 RecursiveCharacterTextSplitter 就是这个思路。

🔍 二、向量嵌入与相似度检索

2.1 Embedding 模型选择

向量嵌入是 RAG 的核心——把人类语言转为机器可以计算距离的数值向量。2026 年主流 Embedding 模型对比如下:

模型 维度 中文支持 价格(/M tokens) 延迟 推荐场景
OpenAI text-embedding-3-small 1536 ✅ 良好 $0.02 ~100ms 通用场景首选
OpenAI text-embedding-3-large 3072 ✅ 良好 $0.13 ~150ms 高精度检索
Cohere embed-v4 1024 ✅ 优秀 $0.10 ~80ms 多语言场景
BGE-M3 (开源) 1024 ✅ 优秀 免费(自部署) ~50ms 私有化部署
Jina embeddings-v3 1024 ✅ 优秀 $0.02 ~60ms 性价比之选

关键结论: 如果你是中文场景且注重性价比,Jina embeddings-v3 或 BGE-M3 是最佳选择。OpenAI 的模型虽然生态好,但中文效果不如专用中文模型。

2.2 向量相似度计算

检索的本质是计算「用户问题的向量」和「文档 chunk 的向量」之间的距离。三种主流算法:

// 向量相似度计算:三种主流算法实现
const VectorSimilarity = {
  /**
   * 余弦相似度 — RAG 首选算法
   * 衡量两个向量的方向相似性,值域 [-1, 1],1 为完全相同
   * 优势:不受向量长度影响,适合文本检索
   */
  cosine(a, b) {
    if (a.length !== b.length) throw new Error('向量维度不一致')
    let dotProduct = 0, normA = 0, normB = 0
    for (let i = 0; i < a.length; i++) {
      dotProduct += a[i] * b[i]
      normA += a[i] * a[i]
      normB += b[i] * b[i]
    }
    return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB))
  },

  /**
   * 欧氏距离 — 衡量空间中的绝对距离
   * 值越小越相似,需要归一化后使用
   */
  euclidean(a, b) {
    let sum = 0
    for (let i = 0; i < a.length; i++) {
      sum += (a[i] - b[i]) ** 2
    }
    return 1 / (1 + Math.sqrt(sum)) // 转换为相似度
  },

  /**
   * 点积 — 适用于已归一化的向量
   * 计算最快,但要求向量已做 L2 归一化
   */
  dotProduct(a, b) {
    let sum = 0
    for (let i = 0; i < a.length; i++) {
      sum += a[i] * b[i]
    }
    return sum
  }
}

// 性能对比测试
const dim = 1536
const vecA = Array.from({ length: dim }, () => Math.random())
const vecB = Array.from({ length: dim }, () => Math.random())

console.time('余弦相似度')
for (let i = 0; i < 10000; i++) VectorSimilarity.cosine(vecA, vecB)
console.timeEnd('余弦相似度') // ~15ms

console.time('欧氏距离')
for (let i = 0; i < 10000; i++) VectorSimilarity.euclidean(vecA, vecB)
console.timeEnd('欧氏距离') // ~12ms

console.time('点积')
for (let i = 0; i < 10000; i++) VectorSimilarity.dotProduct(vecA, vecB)
console.timeEnd('点积') // ~8ms(最快)

2.3 暴力检索 vs 近似检索

当知识库规模超过 10 万条 chunk 时,暴力遍历计算相似度会非常慢。这时候需要近似最近邻(ANN)算法:

方案 10 万条检索耗时 召回率 内存占用 适用场景
暴力遍历 ~500ms 100% < 1 万条
HNSW 索引 ~2ms 98% 生产环境首选
IVF-PQ ~5ms 95% 超大规模数据
ScaNN ~1ms 97% Google 生态

💡 提示: 对于大多数中小规模应用(< 100 万条),直接用暴力遍历就够了,不要过早优化。等真的遇到性能瓶颈再上 HNSW。

🚀 三、完整 RAG 管线实现

3.1 核心 RAG 类实现

下面是一个完整的、可直接运行的 RAG 管线实现:

// 完整 RAG 管线:从文档到问答的全流程
class RAGPipeline {
  constructor(options = {}) {
    this.chunks = []           // 文档切片
    this.embeddings = []       // 向量存储
    this.chunkSize = options.chunkSize || 500
    this.overlap = options.overlap || 100
    this.topK = options.topK || 3           // 返回最相关的 K 个 chunk
    this.embeddingFn = options.embeddingFn  // 外部传入的 Embedding 函数
    this.llmFn = options.llmFn              // 外部传入的 LLM 调用函数
  }

  /**
   * 第一步:摄入文档,切分并生成向量
   */
  async ingest(text, metadata = {}) {
    // 切分文本
    const newChunks = this.splitText(text)

    for (let i = 0; i < newChunks.length; i++) {
      const chunk = {
        id: `${Date.now()}-${i}`,
        text: newChunks[i],
        metadata: {
          ...metadata,
          chunkIndex: i,
          totalChunks: newChunks.length
        }
      }
      this.chunks.push(chunk)

      // 生成向量嵌入
      const embedding = await this.embeddingFn(newChunks[i])
      this.embeddings.push(embedding)
    }

    console.log(`✅ 已摄入 ${newChunks.length} 个文档片段,总计 ${this.chunks.length} 个片段`)
  }

  /**
   * 第二步:检索最相关的文档片段
   */
  async retrieve(query) {
    if (this.chunks.length === 0) {
      throw new Error('知识库为空,请先调用 ingest() 导入文档')
    }

    // 将查询转为向量
    const queryEmbedding = await this.embeddingFn(query)

    // 计算与所有 chunk 的相似度
    const scored = this.embeddings.map((emb, i) => ({
      index: i,
      score: this.cosineSimilarity(queryEmbedding, emb),
      chunk: this.chunks[i]
    }))

    // 按相似度降序排列,取 topK
    scored.sort((a, b) => b.score - a.score)
    const results = scored.slice(0, this.topK)

    console.log(`🔍 检索到 ${results.length} 个相关片段,最高相似度: ${results[0].score.toFixed(4)}`)
    return results
  }

  /**
   * 第三步:检索 + 生成回答
   */
  async query(question) {
    // 检索相关文档
    const results = await this.retrieve(question)

    // 组装上下文
    const context = results
      .map((r, i) => `[${i + 1}] ${r.chunk.text}`)
      .join('\n\n')

    // 构建 Prompt
    const prompt = `基于以下参考资料回答用户问题。如果参考资料中没有相关信息,请如实说明。

参考资料:
${context}

用户问题:${question}

回答:`

    // 调用 LLM 生成回答
    const answer = await this.llmFn(prompt)

    return {
      answer,
      sources: results.map(r => ({
        text: r.chunk.text.slice(0, 100) + '...',
        score: r.score,
        metadata: r.chunk.metadata
      }))
    }
  }

  // 文本切分(滑动窗口策略)
  splitText(text) {
    const sentences = text.match(/[^。!?.!?\n]+[。!?.!?\n]*/g) || [text]
    const chunks = []
    let current = ''

    for (const s of sentences) {
      if (current.length + s.length > this.chunkSize && current) {
        chunks.push(current.trim())
        current = current.slice(-this.overlap) + s
      } else {
        current += s
      }
    }
    if (current.trim()) chunks.push(current.trim())
    return chunks
  }

  // 余弦相似度
  cosineSimilarity(a, b) {
    let dot = 0, na = 0, nb = 0
    for (let i = 0; i < a.length; i++) {
      dot += a[i] * b[i]
      na += a[i] * a[i]
      nb += b[i] * b[i]
    }
    return dot / (Math.sqrt(na) * Math.sqrt(nb))
  }
}

3.2 端到端使用示例

// 端到端使用示例:构建一个技术文档问答系统
async function demo() {
  // 模拟 Embedding 函数(实际项目中替换为真实 API 调用)
  async function mockEmbedding(text) {
    // 简单的 hash-based 伪向量,仅用于演示
    const vec = new Array(128).fill(0)
    for (let i = 0; i < text.length; i++) {
      vec[i % 128] += text.charCodeAt(i) / 1000
    }
    // L2 归一化
    const norm = Math.sqrt(vec.reduce((s, v) => s + v * v, 0))
    return vec.map(v => v / norm)
  }

  // 模拟 LLM 调用(实际项目中替换为 OpenAI/Claude API)
  async function mockLLM(prompt) {
    return `基于参考资料分析,这是一个技术问题。以下是关键信息:${prompt.slice(0, 200)}...`
  }

  // 创建 RAG 实例
  const rag = new RAGPipeline({
    chunkSize: 300,
    overlap: 50,
    topK: 3,
    embeddingFn: mockEmbedding,
    llmFn: mockLLM
  })

  // 导入知识库文档
  await rag.ingest(
    'RAG 是 Retrieval Augmented Generation 的缩写。它通过检索外部知识库来增强大模型的回答质量。' +
    'RAG 的核心流程包括文档切分、向量嵌入、相似度检索和上下文注入。' +
    '向量嵌入使用 Embedding 模型将文本转为高维向量。' +
    '余弦相似度是衡量向量相似性的常用方法。' +
    'HNSW 是一种高效的近似最近邻索引算法。',
    { source: 'rag-intro.md', category: 'AI' }
  )

  // 提问
  const result = await rag.query('什么是 RAG?它的核心流程是什么?')
  console.log('回答:', result.answer)
  console.log('来源:', result.sources.map(s => s.text))
}

demo().catch(console.error)

⚠️ 警告: 上面的 mockEmbeddingmockLLM 仅用于演示。生产环境中必须使用真实的 Embedding 模型和 LLM API,否则检索质量和回答质量都会很差。

3.3 检索优化:混合检索策略

单纯的向量检索有一个弱点——它擅长语义匹配,但对精确关键词匹配表现不佳。例如用户搜索「HTTP 403 错误」,向量检索可能返回「权限问题」的相关内容,但找不到精确包含「403」的文档。

解决方案是混合检索——同时使用向量检索和关键词检索(BM25),然后融合排序:

// 混合检索:向量检索 + BM25 关键词检索
class HybridRetriever {
  constructor(ragPipeline) {
    this.rag = ragPipeline
  }

  /**
   * BM25 关键词检索 — 经典信息检索算法
   * 对精确关键词匹配非常有效
   */
  bm25Search(query, k1 = 1.5, b = 0.75) {
    const queryTerms = query.toLowerCase().split(/\s+/)
    const avgDl = this.rag.chunks.reduce((s, c) => s + c.text.length, 0) / this.rag.chunks.length

    const scores = this.rag.chunks.map((chunk, idx) => {
      const text = chunk.text.toLowerCase()
      let score = 0

      for (const term of queryTerms) {
        // 词频 (TF)
        const tf = (text.match(new RegExp(term, 'g')) || []).length
        if (tf === 0) continue

        // 逆文档频率 (IDF)
        const docsWithTerm = this.rag.chunks.filter(c =>
          c.text.toLowerCase().includes(term)
        ).length
        const idf = Math.log(
          (this.rag.chunks.length - docsWithTerm + 0.5) / (docsWithTerm + 0.5) + 1
        )

        // BM25 公式
        const normFactor = (1 - b + b * (chunk.text.length / avgDl))
        score += idf * ((tf * (k1 + 1)) / (tf + k1 * normFactor))
      }

      return { index: idx, score, chunk }
    })

    scores.sort((a, b) => b.score - a.score)
    return scores.slice(0, this.rag.topK)
  }

  /**
   * 混合检索 — RRF (Reciprocal Rank Fusion) 融合排序
   * 结合向量检索的语义能力和 BM25 的精确匹配能力
   */
  async hybridSearch(query, vectorWeight = 0.6, bm25Weight = 0.4) {
    // 并行执行两种检索
    const [vectorResults, bm25Results] = await Promise.all([
      this.rag.retrieve(query),
      Promise.resolve(this.bm25Search(query))
    ])

    // RRF 融合排序
    const scores = new Map()
    const k = 60 // RRF 常数

    vectorResults.forEach((r, rank) => {
      const id = r.chunk.id
      scores.set(id, (scores.get(id) || 0) + vectorWeight / (k + rank + 1))
    })

    bm25Results.forEach((r, rank) => {
      const id = r.chunk.id
      scores.set(id, (scores.get(id) || 0) + bm25Weight / (k + rank + 1))
    })

    // 按融合分数排序
    const ranked = [...scores.entries()]
      .sort((a, b) => b[1] - a[1])
      .slice(0, this.rag.topK)
      .map(([id, score]) => ({
        chunk: this.rag.chunks.find(c => c.id === id),
        score
      }))

    return ranked
  }
}

关键结论: 混合检索比纯向量检索的召回率提升 15-30%,尤其是对于包含专有名词、代码片段、错误码等精确信息的查询。这是生产级 RAG 必须具备的能力。

⚠️ 四、RAG 常见陷阱与优化策略

4.1 十大常见坑点

序号 坑点 表现 解决方案
1 切分粒度不当 检索到的内容不完整或太碎片化 使用滑动窗口 + 语义切分
2 缺少元数据过滤 检索到无关文档 为每个 chunk 添加来源、时间等元数据
3 Embedding 模型不匹配 语义检索效果差 中文场景用中文优化的模型
4 只用向量检索 精确关键词查不到 混合检索(向量 + BM25)
5 上下文窗口塞太满 LLM 回答质量下降 控制 context 在 3000 token 以内
6 缺少引用溯源 用户无法验证答案 在回答中标注来源编号
7 没有设置相关性阈值 检索到不相关内容 过滤掉相似度 < 0.7 的结果
8 忽略文档更新 知识库过时 建立增量更新机制
9 没有评测体系 无法衡量 RAG 质量 建立测试集 + 定期评估
10 单一检索策略 复杂查询效果差 Query 改写 + 多路召回

4.2 Prompt 工程优化

RAG 的 Prompt 设计直接影响回答质量。以下是经过实战验证的 Prompt 模板:

// 生产级 RAG Prompt 模板
function buildRAGPrompt(question, contexts, options = {}) {
  const {
    language = '中文',
    maxContextTokens = 3000,
    enableCitation = true
  } = options

  // 截断过长的上下文
  let totalLength = 0
  const trimmedContexts = []
  for (const ctx of contexts) {
    if (totalLength + ctx.text.length > maxContextTokens * 3) break // 粗略估算
    trimmedContexts.push(ctx)
    totalLength += ctx.text.length
  }

  const contextBlock = trimmedContexts
    .map((ctx, i) => `[${i + 1}] 来源: ${ctx.source || '未知'}\n${ctx.text}`)
    .join('\n\n---\n\n')

  return `你是一个专业的技术助手。请严格基于以下参考资料回答用户问题。

## 回答规则
1. 只使用参考资料中的信息回答,不要编造
2. 如果参考资料中没有答案,明确说"根据现有资料无法回答"
3. ${enableCitation ? '在回答中用 [1][2] 标注信息来源' : ''}
4. 回答使用${language},简洁专业
5. 如果涉及代码,给出完整可运行的示例

## 参考资料
${contextBlock}

## 用户问题
${question}

## 回答
`
}

💡 提示: Prompt 中要明确告诉模型「只基于参考资料回答」和「不知道就说不知道」,否则模型会倾向于用自身知识补充,导致引用溯源失效。

4.3 评估指标

RAG 系统需要量化评估,核心指标包括:

  • 检索召回率(Recall@K):K 个结果中包含正确答案的比例
  • 准确率(Precision@K):K 个结果中真正相关的比例
  • 忠实度(Faithfulness):回答是否忠实于检索到的文档
  • 相关度(Relevancy):回答是否真正回答了用户的问题

推荐使用 RAGAS 框架进行自动化评估。它通过 LLM 自动评分,不需要人工标注数据。

💡 总结

RAG 是 2026 年最实用的 AI 应用架构模式。核心要点回顾:

  • 文本切分:使用滑动窗口策略,chunk 200-500 token,overlap 10-20%
  • Embedding 选择:中文场景首选 BGE-M3 或 Jina embeddings-v3
  • 混合检索:向量检索 + BM25 关键词检索,RRF 融合排序
  • Prompt 设计:明确要求只基于参考资料回答,加上引用标注
  • 持续评估:建立 RAGAS 评估体系,定期优化各环节参数

关键结论: RAG 的质量瓶颈不在 LLM,而在检索。投入 80% 的精力优化检索环节(切分策略、Embedding 质量、混合检索),才能真正提升 RAG 的回答质量。

相关工具推荐:

  • 🔧 LangChain — 最流行的 RAG 框架
  • 🔧 LlamaIndex — 专注于数据索引和 RAG
  • 🔧 ChromaDB — 轻量级向量数据库,适合快速原型
  • 🔧 Qdrant — 高性能向量数据库,适合生产环境
  • 🔧 RAGAS — RAG 自动评估框架
  • 🔧 jsjson.com JSON 工具 — 处理知识库的 JSON 数据格式化

📚 相关文章