RAG 简述
RAG (Retrieval Augmented Generation,检索增强生成)是与大语言模型 LLM 和智能体 Agent 等技术密不可分的一项技术。当你有一批已经整理好的内容,比如十几万字甚至百万字的规范描述,知识内容,内部文档等等,单单堆积提示词已经难以做到目标效果。
即便你有 50k-1M token context,将几十万字内容作为输入让大模型全量处理已经不太适合,后续导致的注意力缺失问题和计算成本是难以招架的。但通过符合场景的 RAG 技术可以大幅缓解这种场景的痛苦。
通过对海量文本的向量化存储和优化查询,在最终发给大模型 Prompt 输入之前对 Prompt 提示词做内容补全。这样可以既保持一个合理的输入 size,同时让大模型拿到基于用户原 Prompt 有关的特定内容,让大模型有机会做准确回复。
说到这里,你可能会有一个直觉:不去纠结 RAG 技术中的各种细节和花哨方法,只专注于它解决的核心问题 ——RAG 是不是本质上就是一种 "高级的 Prompt 拼接",属于 Prompt Engineering 的一部分?
这个问题我思考了很久。在做 fitneheal 这个项目的过程中,我越来越觉得:表层看是 Prompt 拼接,深层看是一整套工程体系。接下来我会拆解 RAG 的具体原理,再结合我在 fitneheal 中的实战经验,看看这个说法到底站不站得住脚。
https://fitnuhealth.supwil.com/zh/assistant
RAG 的两个阶段:索引阶段(Indexing)与 查询阶段(Querying)
RAG 可以清晰地拆成两个阶段:建索引和查索引。前者是离线、一次性的(或增量的),后者是在线的、每次查询都要走的。
索引阶段:把知识变成可检索的向量
原始数据 -> 清洗与解析
原始数据可能是 Markdown、PDF、Word、HTML..... 第一步是把它们统一解析成干净的文本结构。这一步看似简单,实则是很多 RAG 系统翻车的地方,PDF 表格解析错乱、HTML 标签残留、中英文混排格式混乱,都会直接影响后续 embedding 的质量。
在 fitneheal 中,我们的内容是结构化的 TypeScript 数据(@fitnuhealth/content 包),所以这一步几乎是免费的。但对于大多数项目来说,这是一个需要认真对待的工程问题。
文本分割(Chunking)
这是 RAG 里最被低估、也最影响效果的一步。把长文本切成多大的块?按什么规则切?
常见的几种切法:固定字符窗口(每 500 字一刀切,简单但经常切断语义)、语义边界切分(按段落 / 章节切,语义完整但大小不均)、重叠窗口(块之间留一部分重叠,防止关键信息掉在切割线上)。
我个人的经验是:最好的 chunking 是让内容作者来帮你做。 如果你的内容本身就是结构化的(比如一篇篇文章、一个个知识点),直接用作者定义的边界来切,效果远好于任何自动切分算法。fitneheal 就是这么干的 —— 一个 scene 就是一个 chunk,天然语义完整。
向量化(Embedding)
把每个 chunk 的文本送入 embedding 模型,转换成一个固定维度的向量。这个向量就是这段文本的 "语义指纹"—— 意思相近的文本,在向量空间里的距离也近。
选模型的时候注意两点:一是语言适配性,中文语料就选中⽂强的模型(bge-m3、Qwen3-Embedding 这类),别默认上 OpenAI 的英文模型;二是维度和成本的 trade-off,维度越高表达力越强,但存储和计算也越贵。
存储
向量 + 原始文本 + 元数据一起存进向量数据库。常见选择:pgvector(PG 扩展,适合已经在用 PG 的团队)、Pinecone / Weaviate(专门的向量库)、Chroma / FAISS(轻量本地方案)。
fitneheal 用的是 pgvector on Neon,索引类型 HNSW,cosine 相似度。选它的原因很实际:我们已经在用 Neon 做主库,没必要为了向量再引入一套新基础设施。
查询阶段:从向量库中捞出最相关的内容
输入向量化
把用户的问题也用同一个 embedding 模型转成向量。很快,因为只有一句话。
检索
拿查询向量去向量库里做相似度搜索,找出最接近的 top-K 个 chunk。这一步叫粗排—— 速度快,但精度有限,毕竟 embedding 是对整段文本的压缩表示,细粒度信息丢了不少。
(重排)
这步很多人跳过,但我觉得只要预算允许就一定要加。粗排回来的 20-50 个候选,再用一个交叉编码器(Cross-Encoder)重新打分。
为什么需要重排?因为 embedding 模型是双编码器(query 和 document 分别编码再算距离),快但精度有限;交叉编码器是把 query 和 document 拼在一起直接算相关性,精度高得多,但慢也贵得多。所以策略是:双编码器粗排召回大量候选,交叉编码器精排选 top-N。
fitneheal 的配置:pgvector 先召回 20 个,取前 12 个送重排器,最后取 5 个进 Prompt。重排器的分数还兼任了可回答性门槛—— 最高分都低于阈值,说明库里真没有,直接告诉用户没找到,别让 LLM 硬编。
提示词增强
把检索到的 chunk 格式化后塞进 Prompt,和用户问题一起发给大模型。这就是大家最熟悉的 "拼接" 环节。但怎么塞、用什么格式、怎么告诉模型哪些能用哪些不能用,这里面的门道也不少。后面讲 fitneheal 的时候细说。
模型生成
大模型基于增强后的 Prompt 生成回答。理想状态下,模型只用检索到的内容回答,每个事实都能追溯到具体来源。
一些高级的 RAG 技术
基础的 "索引、检索、生成" 只是入门。真正到了生产环境,你会遇到各种问题,于是有了各种高级技术。
混合检索(Hybrid Search)
纯向量检索有个盲点:专有名词、缩写、罕见术语。比如用户搜 "MTHFR 基因",向量检索可能召回不好,但关键词匹配(BM25)反而能精准命中。混合检索就是同时跑语义检索和关键词检索两路,再用 RRF 之类的算法融合结果。
但说句实话 ——不要上来就加混合检索。fitneheal 曾经实现过 dense + sparse 双臂 + RRF 融合,然后在专有名词 query 上做了 A/B,结果是稀疏臂零增益。dense embedding 已经把正确 chunk 放进了 top-12,交叉编码器也已经把它提上来了。
所以我们后来把稀疏臂整个删了。不是混合检索不好,是当你的 embedder 足够强、语料足够规整时,第二臂的边际收益可能是零。别为了 "高级" 而高级,测了再说。
查询改写(Query Rewriting)
用户的提问往往不是最优检索 query—— 太口语化、太宽泛、指代不清。查询改写就是用 LLM 把问题改写成更适合检索的形式。常见的有 HyDE(先幻想一篇答案再用答案去检索)、多查询生成(一个问题拆成几个子问题分别检索)。
fitneheal 有个 broaden 机制:对特别宽泛的问题(比如 "维生素 B 族都有什么用"),自动拆成几个子问题分别检索再合并,本质就是多查询生成。
重排之后的重排
重排不是终点。fitneheal 重排之后还有几步纯逻辑后处理,不花一分钱 API 费用:
去冗余:如果一个 story 摘要排在了它自己某个 scene 的后面,说明摘要是多余的,删掉
同场景限制:同一个 scene 最多 2 个 page chunk 进 Prompt,防止一个超长场景占满整个上下文
个性化加权:用户最近读过的内容、当前正在看的营养素,相关 chunk 会得到额外 boost
这些都是 "重排之后的重排"—— 纯逻辑,零成本,效果还不错。
内容更新要如何应对?
这是 RAG 从 demo 走向生产绕不开的问题。知识库不是一成不变的 —— 内容会加、会改、会删,向量库怎么跟内容保持同步?
几种常见方案:
全量重建—— 最简单但最笨。每次内容变了就删掉重建。小数据量没问题,内容多了之后又慢又贵。
增量更新—— 给每个 chunk 算个内容哈希,更新时对比哈希,没变的跳过,变了的重新 embedding。fitneheal 就是这么做的,能省大量 embedding API 费用。
还有一个细节:纯元数据变更不需要 re-embed。比如只是修正了一个 citation 的 id,正文没变,哈希也没变 —— 不需要重新调用 embedding,只更新元数据就行。fitneheal 有个 --reconcile 模式专门干这个,快得多也便宜得多。
实时更新—— 内容频繁变化的场景(新闻、文档协作)需要实时索引,通常用 webhook 事件驱动或 CDC 监听数据库变更。
fitneheal 的策略总结下来就是三句话:增量构建哈希驱动、元数据独立更新、CI 无 key 自动 dry-run(保证 CI 不会意外产生费用或写入生产库)。
Fitnuhealth 的 RAG 技术
前面讲了很多通用原理,来看看 fitneheal 里 RAG 是怎么落地的。
整体流程长这样:
plaintext
用户提问
↓
红旗预拦截 / 拒答检查(Guard)
↓
Query Embedding (Qwen3-Embedding-8B, 1024-dim)
↓
Dense Search (pgvector HNSW, cosine, top-20)
↓
Rerank (Qwen3-Reranker-8B, top-8)
↓
后处理:去冗余 roll-up + 同场景页数限制 + 用户上下文加权
↓
取 top-5 进 Prompt
↓
LLM 生成 (DeepSeek-V4-Flash)
↓
机械忠实度检查 (Faithfulness Check)
↓
返回给用户挑几个我觉得有意思的设计决策聊聊。
机械忠实度检查 —— 这是我最满意的设计
大多数 RAG 系统的 "忠实度" 靠另一个 LLM 来评判,这本身就很讽刺 ——LLM 评判 LLM,你信谁?
fitneheal 的做法是机械的、可验证的:每个 chunk 都带着它的 citationIds 列表,检索回来的所有 chunk 的 citationIds 并集,就是本次回答的引用白名单。模型生成的每个 <Cite id="..."> 都必须在这个白名单里,不在的要么是越界引用要么是幻觉引用。
这是一个纯函数就能完成的检查,不需要再调一次 LLM,快、确定、可写单元测试。
当然也发现了漏洞:如果模型干脆不引用任何东西,忠实度分数就是满分(0 个无效 / 0 个总数 = 1)。所以我们加了个检测 —— 回答很长但一个引用都没有,标记出来,这通常意味着模型在自由发挥。
内容作者即 chunk 作者
前面提过,fitneheal 的 chunk 边界就是内容作者定义的内容单元。一个 scene 一个 chunk,一个 story 一个 chunk,一个 rule 一个 chunk。好处是每个 chunk 天然语义完整,chunk 有明确类型可以做过滤,chunk 带着丰富的元数据可以做更精细的检索策略。
代价是你得有结构化的内容。如果你的内容是一堆散乱的 PDF,那就享受不到这个红利了。
用户上下文增强检索
fitneheal 的检索不是千人一面的。同一个问题,不同用户问,结果可能不一样 —— 我们会根据用户的个人上下文调整排序:报告触发了某个规则 → 对应 rule chunk 1.5 倍 boost;最近读过某个 scene → 1.2 倍 boost(回调式复习,不从头讲基础);当前正在浏览某个营养素 → 1.6 倍 boost(最强,因为用户明确在关注这个话题)。
RAG 从 "通用知识库检索" 变成了 "个性化知识检索"。
构建个人 RAG 知识库
最后,聊聊如果你想自己搭一个 RAG 知识库,应该怎么入手。
先跑通,再优化。 别一开始就追求 "最先进的 RAG"。先把最基础的流程跑通:选一个 embedding 模型、选一个向量库、选一个 LLM、写最简单的 "切分→向量化→检索→生成"。跑通之后用你的真实问题去测,哪里不行再针对性地优化。
最影响效果的三个因素,按优先级排:
内容质量 > 一切。知识库本身是混乱的、过时的、不准确的,再牛的 RAG 技术也救不了。RAG 只能检索你已有的知识,不能创造知识。
Chunking 策略 > 模型选择。很多人花大量时间选 embedding 模型、调向量库参数,但效果提升最大的往往是改 chunking 策略。
重排 > 混合检索。只能加一个高级功能的话,加重排,不要加混合检索。交叉编码器带来的精度提升通常比加一个稀疏臂大得多。
别忽视工程问题:成本监控(embedding + 重排 + LLM,每一项都在烧钱,全量重建时账单可能让你吓一跳)、延迟预算(设个 p95 目标,倒推每一步的预算)、失败降级(API 挂了怎么办?别直接 500)、内容同步(知识库更新了向量库怎么同步?第一天就要想清楚)。
回到最开始的问题:RAG 是高级 Prompt 拼接吗?
我的答案是:从结果上看是,从工程上看不是。
用户看到的最终效果,确实就是 "把相关内容拼进 Prompt 里"。但为了让这个拼接拼得准、拼得对、拼得快、拼得便宜、拼得可验证,背后是一整套工程体系 ——chunking、embedding、向量检索、重排、后处理、忠实度检查、增量更新、评测体系……
Prompt 拼接是冰山露出水面的那一角。水面之下,是 90% 的工程工作。
而这,也正是 RAG 真正有意思的地方。