—— 检索证据、生成回答、展示来源
大模型能回答 JavaScript、数据库和架构问题,却不知道你的项目怎么启动、接口放在哪里、提交代码前要执行什么命令。不是模型不够聪明,而是这些知识从来没有进入它的上下文。
前段时间,我们从一次模型调用出发,做了一个 AI 需求分析助手。
它已经能根据用户输入整理目标、边界、待确认项和验收标准,但只要问题涉及项目内部知识,回答就开始变得不可靠:
这些答案可能写在 README.md、接口文档和开发规范里,却不在大模型本身的知识中。
最直接的想法,是把所有文档都塞进 Prompt。
文档只有两三页时可以这样做;但项目不断迭代后,Prompt 会越来越长,成本和延迟持续增加,真正相关的内容反而容易被淹没。
所以这次,我们给应用增加一套最小 RAG:
最终做出的不是一个泛泛聊天机器人,而是一个能回答项目问题、展示引用来源,并在文档没有答案时明确说“不知道”的知识助手。
先看结果:它到底能做什么?
我们准备三份通用项目文档:
其中分别记录:
✓项目的安装与启动命令;
◆接口路径、参数和错误码约定;
•分支、提交和代码检查规范。
完成索引后,用户可以直接提问。下面先展示这套 Demo 的目标效果:
如果用户问了文档中不存在的内容:
上面两段用于说明预期交互,并不是伪装成在线模型的实测记录。文章后面会单独说明已经完成的验证,以及仍需读者使用自己 API Key 验证的部分。
Web 端的演示界面如下。截图中的问题、回答和引用来自仓库内置的 DEVELOPMENT.md;它展示的是页面与证据绑定方式,不代表已经完成真实在线模型调用。

这两个结果同样重要。
RAG 不只是让模型“知道更多”,还要让它在没有证据时停止猜测。
RAG 到底做了哪几步?
RAG 的全称是 Retrieval-Augmented Generation,通常翻译为“检索增强生成”。
名字看起来复杂,实际链路只有两部分。
第一部分在文档更新时执行:
第二部分在用户提问时执行:
Embedding 可以理解成一组描述文本语义的数字。
“项目怎么启动”和“本地运行需要什么命令”虽然用词不同,但意思接近,生成的向量也会更接近。这样就能找到关键词没有完全重合、语义却相关的内容。
OpenAI 官方文档也把 Embeddings 用于搜索、聚类和推荐等场景,并提供 text-embedding-3-small 与 text-embedding-3-large 两个模型:
https://developers.openai.com/api/docs/guides/embeddings
这篇先使用 text-embedding-3-small。我们的文档规模很小,目标是把完整链路跑通,而不是一开始就搭建复杂基础设施。
项目结构和技术选择
这次仍然使用前端开发者比较熟悉的 Node.js:
完整 Demo 已开源到 GitHub,包含命令行问答、Web 页面、三份通用项目文档和自动测试:
安装依赖:
准备环境变量:
不要把真实 Key 写进前端代码,也不要提交到 GitHub。
.gitignore 至少加入:
官方 JavaScript SDK 会从环境变量中读取 OPENAI_API_KEY。文本生成部分使用 Responses API:
https://developers.openai.com/api/docs/guides/text
第一步:把文档切成合适的片段
为什么不能直接给每份文档生成一个向量?
假设 DEVELOPMENT.md 同时包含分支规范、代码检查、发布流程和回滚方案。用户只问“提交前执行什么命令”,整份文档只有一小段真正相关。
片段太大:
✓检索结果不够精确;
◆无关内容占用模型上下文;
•大段内容的主题容易混杂。
片段太小:
✓一句话可能脱离上下文;
◆标题、条件和结论可能被拆开;
•模型拿到片段后仍然无法理解。
最小 Demo 可以先按标题和段落切分,并控制每个片段的最大长度:
这里用字符数只是为了让流程容易理解,不代表生产环境的最佳切片方案。GitHub 仓库中的实际实现还加入了段落拼接和相邻片段重叠,读者运行时应以仓库代码为准。
正式项目还要根据文档类型处理:
✓Markdown 尽量保留标题层级;
◆API 文档不要把接口路径和参数表拆开;
•FAQ 尽量让一个问题和答案位于同一片段;
✓代码示例不要在函数中间截断;
◆相邻片段可保留少量重叠内容。
切片不是一个无关紧要的预处理步骤,它直接决定后面能不能检索到正确证据。
第二步:给每个片段生成 Embedding
建立 OpenAI 客户端:
然后封装 Embedding 调用:
向量本质上是一个浮点数数组:
我们不需要理解每个数字代表什么,只需要知道:语义更接近的文本,向量之间通常也更接近。
建立索引时,把每个片段及其向量保存在一起:
这里选择本地 index.json,是为了让读者能直接打开文件,看清索引里到底存了什么。
它适合几十到几百个小片段的教学 Demo,不适合大型生产知识库。数据增大后,应换成 Vector Store、pgvector、Milvus 等具备向量检索能力的存储方案。
第三步:生成本地知识索引
完整索引脚本主要做四件事:
在 package.json 中增加命令:
执行:
第一次建立索引会调用 Embeddings API。示例把多个片段作为数组批量提交,避免为每个小片段单独发起请求。之后只有文档发生变化,才需要重新生成对应索引,不应该在每次提问时重复计算全部文档向量。
真实项目还可以为每个片段保存内容哈希:内容没有变化就复用旧向量,避免不必要的重复调用。
第四步:用余弦相似度找到 TopK
用户提问时,先给问题生成一个向量,然后计算它与每个文档片段的相似度。
最小 Demo 使用余弦相似度:
再取相似度最高的三个片段:
TopK 不是越大越好。
取得太少,可能漏掉必要条件;取得太多,又会把不相关内容一起交给模型。
对于这个小 Demo,可以从 3 开始,再准备一组真实问题观察:
✓正确证据是否排在前面;
◆关键规则是否被分散在多个片段;
•无关片段是否频繁混入;
✓同义表达能否正确检索。
RAG 的第一项评测,不应该是“最终回答读起来像不像”,而应该是“正确证据有没有被检索出来”。
第五步:只允许模型根据证据回答
检索到片段后,把它们整理成上下文:
再调用 Responses API:
这里有三个关键约束:
1明确要求只能使用给定片段;
2文档没有答案时必须拒绝猜测;
3来源由程序返回,不能只让模型凭空生成文件名。
提示词可以降低胡编,但不能替代检索质量。
如果正确文档根本没有进入 TopK,模型再听话也没有证据可以回答。
把一次问答完整跑起来
准备命令行入口:
先建立索引:
再提问:
界面版也不复杂:前端提交问题,Node.js 接口调用 answerQuestion,然后把 answer 和 sources 分开渲染。
来源区域建议至少展示:
不要只展示一个看起来很权威的回答,却把检索证据藏起来。
这套 Demo 实际验证了什么?
我没有把“代码已经写完”等同于“RAG 已经跑通”。
Demo 使用 Node.js 自带测试框架,为核心链路准备了确定性模拟向量与模拟回答。执行:
当前真实结果为:
结构审计和语法检查同样通过:
Web 服务也完成了最小 HTTP 验证:首页可以访问,空问题会返回 400 和“请输入问题”。
这里必须说清楚:这些测试证明切片、索引、相似度排序、无答案门槛、来源绑定和 Web 服务能够工作,但没有冒充真实在线模型效果。
这套 Demo 的验证边界也很明确:仓库已经验证离线核心链路和 Web 服务,但发布稿不把模拟向量、模拟回答写成真实在线模型效果。
要验证完整在线链路,需要准备自己的 OpenAI API Key,配置 .env 后执行 npm run index 生成真实向量索引,再运行命令行或 Web 问答。代码已经接入 Embeddings 与 Responses API,实际回答仍应以本地运行结果为准。
不要只测“它答对了没有”
一个 RAG 应用至少要分别测试检索和回答。
为每个问题提前标记正确来源:
检查正确片段是否进入 TopK。如果没有,优先调整切片、标题信息和检索参数,不要急着修改生成提示词。
再检查:
✓关键结论是否来自检索片段;
◆是否遗漏条件和例外;
•引用编号能否对应真实来源;
✓文档没有答案时是否拒绝猜测;
◆多个文档冲突时是否暴露冲突,而不是自行选择。
修改一条项目规范后重新索引,确认旧答案不再出现。
很多 RAG Demo 只验证“能回答”,却没有验证“答案是否来自最新、正确的证据”。真正进入项目后,这两者差别很大。
最容易踩的几个坑
结果不是不能检索,而是命中范围太大,大量无关内容进入上下文。
回答生成后无法解释证据来自哪个文件,也无法追踪过期文档。
既慢又浪费调用成本。文档索引和在线问答应该是两条不同链路。
TopK 设置得越大越安心过多无关片段会增加干扰。应该通过测试集选择,而不是凭感觉不断调大。
模型可以在错误证据上生成非常流畅的答案。必须同时查看检索结果和引用片段。
如果知识库包含不同项目、部门或用户的数据,必须在检索前做权限过滤。不能先检索全部内容,再要求模型“不要泄露”。
RAG 和微调有什么区别?
这也是刚接触 AI 应用时很容易混淆的问题。
如果目标是让 AI 知道“项目当前怎么启动、接口规则是什么”,优先考虑 RAG。
如果目标是让模型始终按某种稳定方式分类或表达,再评估是否需要微调。
两者并不互斥,但不要把频繁变化的项目知识硬塞进模型参数。
让模型回答之前,先让证据进入上下文
做到这里,我们已经完成了一条最小但完整的 RAG 链路:
它已经具备命令行问答和最小 Web 页面,但还不是一套生产级知识平台:
✓没有增量索引;
◆没有专门的向量数据库;
•没有用户权限隔离;
✓没有完整的检索评测集;
◆只处理 Markdown 和 TXT。
但它把 RAG 最重要的部分完整暴露了出来。
你可以清楚看到:文档怎样被切分,向量怎样保存,问题怎样检索,以及最终答案究竟引用了什么。
对于第一个 RAG 项目,这比一开始搭建复杂平台更重要。
因为 RAG 真正难的,从来不是调用一次大模型,而是:
下一步可以继续增加 PDF/Word 解析、增量索引、文档权限、来源定位和流式回答,把这个最小 Demo 逐步升级成真正可用的项目知识助手。
RAG 的价值,不只是让模型知道更多
更重要的是:答案有来源,没有证据时能够停止猜测。
夜雨聆风