目录

用 RAG 做项目文档问答:从零搭一个 Demo

之前写过一篇「文档问答助手」的早期尝试,这次想更系统地从 RAG 角度,把整个过程讲清楚。

RAG(Retrieval-Augmented Generation)= 检索增强生成
简单说就是:先从你自己的文档里检索相关内容,再让大模型基于这些内容生成答案。

这篇文章以「项目文档问答」为例,从零搭一个小 Demo。


1. 目标:让大模型「读得懂」我们的项目文档

设定目标:

  • 知识来源:代码仓库中的 README、设计文档、接口说明等;
  • 使用方式:在一个 Web 界面里提问,例如:
    • 「私募投资人小程序支持哪些登陆方式?」
    • 「电子合同模块的签署流程有哪些关键步骤?」
  • 期望回答:
    • 基本准确;
    • 回答中能指出「来自哪份文档」。

约束条件:

  • 不追求「端到端生产可用」,先搭一个可演示、可验证的 Demo;
  • 优先用云端 Embedding + 向量库,而不是自己训模型。

2. 文档准备与切分

2.1 文档来源

  • Git 仓库中的 Markdown:docs/ 目录、各项目 README;
  • 内部 Wiki 导出的 HTML/Markdown;
  • 部分 API 文档(Swagger/OpenAPI 转文本)。

统一先转成 Markdown/纯文本,方便处理。

2.2 切分策略

RAG 很关键的一步是「chunking」,即把长文档切成小段。
我们采用的是:

  • 按标题结构 + 字数混合切分
  • 每段控制在 300–600 字;
  • 保留上级标题用于提供上下文。

示例结构:

{
  "docId": "fund-investor-miniapp",
  "docTitle": "信e募投资人小程序技术方案",
  "sectionPath": ["2. 技术架构", "2.1 前端技术栈"],
  "content": "前端采用 Vue3 + uni-app...",
  "index": 12
}

经验:

  • 纯按字数硬切容易把一个完整概念拆散;
  • 适当利用 Markdown 标题结构,会让回答更连贯。

3. 向量化:Embedding 与向量库

3.1 生成 Embedding

选一个支持中文的 Embedding API:

const embedding = await embeddingApi.embed({
  input: chunk.content,
});

得到一串向量(例如 1536 维)。
我们把每个 chunk 存成:

{
  "id": "fund-investor-miniapp#12",
  "vector": [0.12, -0.03, ...],
  "metadata": {
    "docId": "fund-investor-miniapp",
    "docTitle": "信e募投资人小程序技术方案",
    "sectionPath": ["2. 技术架构", "2.1 前端技术栈"],
    "content": "前端采用 Vue3 + uni-app..."
  }
}

3.2 向量库选择

Demo 阶段可以很简单:

  • 用支持向量字段的 Postgres/SQLite 扩展;
  • 或者用专门的向量数据库(如 Milvus、Pinecone 等);
  • 实在简单一点也可以内存向量搜索(适合小数据集)。

关键是支持:

给定一个向量,按余弦相似度/点积,返回 Top K 相似文本。


4. 问答流程:从问题到答案

用户在前端问:

「电子合同的签署流程有哪些关键步骤?」

流程大致是:

  1. 把问题本身做 Embedding;
  2. 在向量库中检索 Top K 文档片段;
  3. 组合这些片段 + 原问题,构造大模型 Prompt;
  4. 得到回答后返回给前端,同时附上引用片段。

4.1 检索相关片段

const qEmbedding = await embeddingApi.embed({ input: question });

const chunks = await vectorStore.search(qEmbedding, {
  topK: 5,
  filter: { project: 'private-fund-platform' }, // 可选过滤条件
});

得到的 chunks 包含内容 + 相似度。

4.2 构造 Prompt

你是项目文档助手,只能根据给定的文档内容回答问题。  
如果文档中没有相关信息,请明确回答「文档中没有找到相关说明」。  

【文档片段】
[1] 来自《电子合同模块设计》 - 小节「签署流程概览」:
...
[2] 来自《电子合同模块设计》 - 小节「签署方与签署顺序」:
...

【问题】
电子合同的签署流程有哪些关键步骤?

请使用简洁的中文回答,并在合适时标注引用的文档片段编号。

4.3 渲染回答

例如模型返回:

电子合同签署流程主要包含以下步骤:

  1. 合同模版选择与变量渲染;
  2. 根据业务方配置生成合同实例;
  3. 按签署顺序依次通知各签署方完成签名/盖章;
  4. 所有签署方完成后生成最终版合同,并推送回业务系统。
    (参考文档片段 [1][2])

前端展示时:

  • 上方显示这段回答;
  • 下方列出 [1][2] 对应的原文片段,方便用户点击查看。

5. 前端界面:简单但好用就行

页面布局可以很简单:

  • 左侧:问答对话区;
  • 右侧:文档列表/知识库切换;
  • 回答下方:引用片段列表。

交互小细节:

  1. 支持「引用片段展开」,方便查看上下文;
  2. 显示当前检索匹配的项目/文档范围;
  3. 当模型明确「找不到答案」时,要如实展示。

6. 一些工程化与产品化思考

6.1 权限控制

项目文档往往有访问控制:

  • 某些项目只对特定团队开放;
  • 某些文档包含敏感信息。

在检索层就要做权限过滤:

  • 按用户 ID / 角色过滤可见的 docId;
  • 只在这些文档范围内做向量检索。

6.2 文档更新与向量同步

文档会变化,向量库要跟上:

  • 对接文档存储的变更 webhook(如 Git commit hook);
  • 文档更新时重新切分 → 重新 Embedding → 更新向量库;
  • 旧版本保留或删除视情况而定。

6.3 错误与「幻觉」处理

  • 对置信度较低(相似度较低)的检索结果,干脆返回「文档中未明确说明」;
  • 对某些敏感领域(合规/法律/风控)建议在回答中加免责声明。

7. 实战经验 & 坑

  1. 切分真的很重要

    • 一开始切得太粗,导致命中片段包含大量无关信息,回答容易跑偏;
    • 后来改成「标题 + 段落」混合切分,效果明显好很多。
  2. Embedding 模型的选择有影响,但不是唯一关键

    • 换更强的模型会有提升,但如果切分/检索逻辑不好,收益有限;
    • 可以先用中等水平 Embedding,重点打磨流程。
  3. 文档质量决定上限

    • 文档过时、内容模糊,RAG 也无能为力;
    • 反而可以用 RAG 的表现倒推:哪些地方需要补文档。

8. 小结

用 RAG 做项目文档问答,从整体上看并不复杂:

  1. 准备文档 → 清洗 & 切分;
  2. 用 Embedding 转成向量,存入向量库;
  3. 提问时先检索,再让大模型基于检索结果生成回答;
  4. 前端界面展示答案 + 引用片段;
  5. 在权限、文档更新、质量评估方面逐步打磨。

它的价值不在于「多高级的模型」,而在于:
让团队的现有文档真正「活起来」,被更多人、以更自然的方式使用。