旗舰项目:从零搭一个 RAG 问答系统
一句话定位:把检索问答、调用、微调、部署那一堆知识拼成第一个完整系统——本文给出一条六个里程碑的搭建路线、一套「评估先行」的施工纪律,与一个可直接照做的评估脚本。前置:RAG 检索增强生成——五段管线在那篇拆过;调用篇——里程碑①的调用层直接落它的薄适配层与
chat()。
先从「不知道从哪下手」说起
知识备齐了:检索管线、混合检索、微调、本地部署,文章都读过,甚至跟着代码敲过。真要搭一个给同事用的文档问答系统,打开了编辑器,却卡住了——
先写检索还是先接模型?界面要不要先做?搭到一半想换个切块策略,怎么知道新的一版比旧的好?同事用了一周说「它答得变蠢了」,你连上一版好多少都说不出来。
第一行代码到底该写什么?
答案是反直觉的:先写评估集。功能可以一版一版加,但「变好」必须有裁判在场才能被证明——本篇沿六个里程碑把这座楼盖起来,评估的地基打在最前面。
全景图:六个里程碑的施工图
本篇采用一条业界常见的演进路线施工。六个里程碑层层依赖:每一层的地基都在下一层,每层都有明确的验收线——验收不过,不盖上一层:
图 1:六里程碑施工图——①统一调用服务 ②工具基础设施 ③Agent 能力 ④RAG 与记忆 ⑤评估体系 ⑥生产化;评估集先行,验收只认数字
渐进拆解:一层一层把楼盖起来
搭 RAG 系统像装修一栋房子:先通水电,对应统一调用服务——后面所有工序都要用电用水;再建管道与料场,对应工具基础设施,管文档进、知识库出;然后才轮到家具进场,也就是检索问答。评估体系不是某间屋子,是全屋的水电验收标准——从开工第一天就在量。很多人装修翻车,不是手艺差,是没装电表就开工:每件家具都说自己省电,没人能证明。
里程碑①:统一调用服务
第一个里程碑不是检索,是把「调模型」这件事封装成自家服务的一个端点:底下接哪家模型、什么参数,业务代码不感知——调用篇的薄适配层在这里落地。验收线:换一家供应商(或切到本地部署),业务代码零改动。先做它的理由很实际:后面每个里程碑的每个功能都要调模型,地基不稳,返工是全局的。
里程碑②:工具基础设施
文档进、知识库出的那条离线管线:解析、切块、向量化入库——RAG 篇讲过五段管线的前三段(入库是第三段的收尾),这里把它做成「一条命令可重跑」的管道。关键不是功能而是可重复:同样的文档进去,同样的块出来;换一个切块策略,重跑一遍就知道差多少。验收线:换一次切块参数,从重跑到重建知识库全流程一键完成、结果可复现。
里程碑③:Agent 能力,要按需
「查订单接口」「算个总数」这类需要模型动手的活儿,靠 Agent 模式(模型决定何时调哪个工具)。但注意顺序:Agent 是这座楼里的增强件,不是承重墙——问答系统的大多数价值靠检索就能兑现。先把工具调用的骨架搭好(函数注册、参数校验、错误回传),具体上几个工具按真实需求拉清单,别为了「像个 Agent」而堆工具。
里程碑④:RAG 与记忆,主楼封顶
在线链路接通:用户问题进来,检索命中块,填进提示词,模型作答——检索与生成两段的工程质量是这套系统体验的大头。多轮对话的「上文里那个它」靠记忆机制(会话内短期记忆起步)。验收线:评估集上的检索命中率与答案要点覆盖率达标(标准见⑤),多轮追问不丢上下文。
里程碑⑤:评估体系,贯穿全程的裁判
施工图右侧那条纪律值得单独一节讲——它是本篇与「能跑就行」的分水岭,下一节展开。
里程碑⑥:生产化
限流、监控、权限、灰度——这些是转型工程师的主场优势,下文「工程优势映射」一节专门讲怎么把旧本领映射过来。验收线:故障有告警、坏答案有样本回流、权限边界经得起测试。
大白话:这座楼的施工口诀就两句——先装电表再装修,验收只认数字。评估集是电表,里程碑是楼层,每上一层都要过表。
评估先行:第一个 golden set 怎么建
评估集(golden set)的概念与格式在RAG 进阶篇立过纲:每条 = 问题 + 应命中的块 + 标准答案要点,先当探针再当考卷。本篇只讲落地时最磨人的三个问题:题从哪来、格式怎么定、多少条才够。
题从哪来:真实问题,滚动入库
第一批题的正当来源是你自己与首批试用者的真实提问,而不是开发者对着文档编——自己编的题会顺着自己实现的路径问,考不出盲区。建一个回流管道:线上每个坏答案、每次「这都答不上来」的吐槽,脱敏后变成评估集新条目。评估集不是一次写完的试卷,是随系统一起生长的资产。
格式怎么定:四列各司其职
RAG 进阶篇立过三列纲目(问题、应命中块 id、答案要点),生产版补上第四列:失败归类(检索的锅还是生成的锅)。其中「答案要点」的粒度是关键:写成标准答案会逼你手工比对,写成若干条「答案里必须出现的事实」就能机器判——这正是后面脚本的判法。
多少条才够:一笔抽样波动的账
RAG 进阶篇说过 20 条起步能拦住大半回退——拦回退和「出统计信号」是两回事,这中间隔着抽样波动。粗算一笔账(命中率按六成算,波动取一个标准差口径):评估集 50 条时,每条题就是 2 个百分点,但抽样的天然波动约 ±7 个百分点——命中率从 60% 涨到 66%,你分不清是改动有效还是运气。波动按 √n 收敛:条数翻四倍,波动减半。把这笔账算成操作口径:
图 2:评估集条数账——20 条拦回退、50 条看方向、200 条验细节;条越短能分辨的差异越细
所以第一个 golden set 的建法收敛成三步:从真实问题凑 50 条起步(典型问题为主、边界问题两成),每条给全四列(问题、应命中块、答案要点、失败归类),配一个自动评估脚本让它 30 秒能跑一遍——跑得快的考卷才会被经常考。此后每次线上坏答案回流一条,评估集越用越厚,能分辨的差异越来越细。
里程碑④的「达标」就落在这份评估集上,判法全部可机器执行:
- 基线:首轮跑出的命中率写死为基线存档,此后任何改动不得低于基线——低于即回退,不许「感觉变好」。
- 覆盖率:答案要点覆盖率 = 答案命中的要点数 ÷ 要点总数,判法与命中率同款逐条比对。
- 多轮追问:不丢上下文的最小测法,是给评估集挂三条三轮连环追问的条目(代词指代、省略主语、改口径重问),全过才算过。
检索质量的迭代闭环
有了考卷,迭代就有章法:混合检索、重排序这些补丁(RAG 进阶篇的层级清单)按失效打——每轮只动一个变量,改完立刻跑评估集(下面动手段的示例是两条完整管线的 A/B 对比,给的是起点与终点的账;单变量纪律用在管线内逐个补丁的迭代上),把「检索没找到」与「找到答错」分开记账,改进归改进、退化归退化。一次动三个变量的「优化」,你永远不知道哪层在帮忙、哪层在捣乱。
更新、删除与撤权:上线前必须补的验收
静态评估集只能证明某个数据快照上的表现。例如报销制度从三十天改成十五天、原件被删除、用户调离部门之后,必须另测「数据变化是否传到读路径」。解析质量、增量版本和 ACL 继承的完整机制见RAG 数据生命周期,这里把它接到里程碑②、④与⑥,不重复实现整条管线。
里程碑②增加发布清单,记录五样:
- 文档稳定标识;
- 内容版本;
- 解析与嵌入版本;
- 权限版本;
- 每个派生块的来源位置。
新版完整验收后才切换活动版本,乱序的旧事件不得覆盖新版。里程碑④的每次查询不仅核对命中块,还要核对引用版本与调用者访问资格;权限检查必须发生在候选进入重排与模型之前,不能只在最终答案处过滤。
给 golden set 增加事件序列,而不是只改标准答案。每条保存初始状态、变更事件、执行身份、预期可见文档版本与预期拒绝结果。下面三项是本项目的新增硬门,命中率提高不能抵消任何一项失败。
| 变更验收 | 操作顺序 | 必须检查的事实 |
|---|---|---|
| 更新 | 查询填缓存,再发布十五天新版,再重放旧事件 | 只引用新版,旧块不复活 |
| 删除 | 填缓存,写墓碑,故意延迟索引清理 | 所有身份立即读不到;清理前后同版本、高版本迟到发布均不能覆盖墓碑,再核对清理回执 |
| 撤权 | 两人均可读,撤销其中一人资格 | 被撤者读不到,保留资格者仍可读,旧缓存不越权 |
下载生命周期离线验收脚本,保存后运行 python3 rag-lifecycle.py。它使用标准库,在内存里故意保留陈旧索引,检查更新、旧版本回流、撤权与删除,预期输出以 PASS update, stale-version, revoke, delete, physical-cleanup 开头。这是模拟数据的可见性测试,不是线上向量库、OCR 或实时鉴权的实测。
答案缓存的键也必须包含知识版本与权限版本,命中后不能绕过当前授权复核。三条机制边界:
- 脚本是机制断言,不是上述事件题的数据格式实现。
- 内容版本不代表删除、撤权等事件的全局顺序,高版本发布也可能在删除前排队。
- 本模拟不支持恢复或复用已删标识,普通 publish 永远不能覆盖墓碑,物理清理保留防复活标识、版本和删除状态。
里程碑⑥的线上复测还差三件事:
- 量出封读(先停止可见,物理删除另行进行)延迟和派生物清理延迟,记录权限服务不可用时的拒绝行为。
- 真实系统应在两种检索路径、答案缓存、会话上下文和引用下载入口复跑三组场景。
- 备份保留和已经发送的内容另列边界,不能承诺后台删除能收回用户已有副本——模拟全绿只是集成测试的起点,不代表这些部署条件已经核验。
工程优势映射:转型者的既有资产
有后端工程经验的人做这个项目,真正的捷径不在模型侧,在工程侧——把你已经练熟的本领对号入座:
- 稳定性 → 幂等与重试:知识库重建是批处理,天然要幂等(重跑不产生脏数据);调模型的容错(指数退避、降级切换)在模型调用与容错篇有成套机构,直接搬。
- 限流 → 并发预算:给问答端点做限流的不只是防刷——模型的并发能力就是本地部署篇算的那笔显存缓存账,限流阈值从算式来,不从拍脑袋来。
- 权限 → 检索前过滤:不同人能问出不同答案,靠的不是提示词叮嘱,是检索层的元数据过滤——权限体系怎么设计你有经验,只是过滤点从数据库查询挪到了向量检索。
- 监控 → 评估线上化:日志聚合、告警、仪表盘这套手艺原样适用,只是监控对象从 QPS 与错误率,多了一层「命中率与无据率」;把评估脚本挂到定时任务,这两条曲线就是系统的体温计。无据率(答案里定位不到引用原文的关键断言占比)的口径承 RAG 进阶篇的引用校验。
- 灰度 → 按评估集分数放量:新版本先对评估集跑分,不降基线才放出流量的一个百分比——放量比例跟着分数走,老手艺新判据。
这五条的共同点:模型侧的知识是新的,工程侧的判断力是现成的。转型期间别急着把旧本领清空——这座楼里最难学的那几间,你早就有钥匙。
动手:评估脚本实跑
按「评估先行」的纪律,动手节给的不是检索代码(RAG 篇已有),而是这座楼的电表——一个可以直接照抄的检索命中率评估脚本。先备一个 mini 评估集(四列格式的机器可判版):
golden = [ # (问题, 应命中块, 答案要点)——真实项目里每条再挂「失败归类」列
("过了七天还能退吗", "chunk_42", ["特殊商品除外"]),
("E-4021 怎么解决", "chunk_108", ["网络超时"]),
("发票抬头能改吗", "chunk_17", ["开票后 30 天内"]),
("年假可以顺延到明年吗", "chunk_63", ["经审批可顺延"]),
("报销周期是多久", "chunk_81", ["每月 25 日前"]),
("试用期有年假吗", "chunk_64", ["按比例折算"]),
]
retrieval_a = ["chunk_42", "chunk_07", "chunk_19", "chunk_108"] # 旧版管线:最终送入模型的检索结果
retrieval_b = ["chunk_42", "chunk_108", "chunk_17", "chunk_63",
"chunk_81", "chunk_64"] # 新版管线(混合检索 + 重排后)同口径结果
再写评估器——逐条问「应命中块进了最终送入模型的检索结果吗」(漏答先都记在检索的账上,按进阶篇的口径回去补是召回还是排序的问题):
def evaluate(golden, recalled):
hits = [g for g in golden if g[1] in recalled]
miss = [g[0] for g in golden if g[1] not in recalled]
return len(hits), miss # 命中数 + 漏掉的题(检索的锅先记在这里)
for name, recalled in [("旧版管线", retrieval_a), ("新版管线(混合检索+重排)", retrieval_b)]:
hits, miss = evaluate(golden, recalled)
print(f"{name}: 命中率 {hits}/{len(golden)},漏答 {miss}")
实际运行输出(已验证):
旧版管线: 命中率 2/6,漏答 ['发票抬头能改吗', '年假可以顺延到明年吗', '报销周期是多久', '试用期有年假吗']
新版管线(混合检索+重排): 命中率 6/6,漏答 []
这段运行只验证集合成员判断:retrieval_a/b 是人为构造的模拟 fixture,不是真实查询输出,2/6 与 6/6 不能证明混合检索或重排带来提升。接真实系统时必须对每道问题分别执行检索、分别保存候选列表,再逐题判断应命中块,不能让所有题共享一个召回集合。生成侧还要单独检查事实、否定条件与引用支持关系,关键词出现不等于事实正确。如何固定条件与解释差异的不确定性,见模型选型与对比实验。
边界与取舍
跳阶段的诱惑
最常见的翻车姿势是直奔④:检索和生成「跑通了」就上线,没有①的适配层,换模型是全局手术;没有⑤的评估集,「感觉变好了」和「真的变好了」永远分不清。施工图的次序不是仪式感,是返工成本的倒排——越基础的地基,返工越全局。
什么时候这个项目根本不该开工
开工前先过闸:微调篇立过「缺知识上 RAG、缺举止(格式、语气)做微调」的闸门,本场景再补第三档——两者都不缺,纯调用就够。给「就三十条内部文档」的系统上全套六里程碑,是给自行车装 ABS——先看知识规模与更新频率,再决定楼盖多大。
评估集自身的维护
评估集也会老化:文档更新后「应命中块」的块号会漂移,老题目可能不再代表真实问题分布。给评估集一个和代码同等的待遇——进版本管理、改文档时连带核对、每季度清一次不再典型的题。它不是一次性交付物,是和系统同寿命的资产。
别把评估做成一次性的上线验收
评估的价值在「每次改动都跑」,不在「上线前跑一次」。把脚本挂进改动流程(哪怕是手工触发),让每张提交的改进说明后面都跟着一行数字——这是这座楼和「能跑的 demo」之间真正的差别。
自测
合上文章先自己答,再展开对照:
第一个 golden set 怎么建?多少条才能出统计信号?
建法三步:从真实问题凑第一批题(自己与首批试用者的真实提问,不开发者自己编——自己编的题顺着自己实现的路径问,考不出盲区);每条给全四列(问题、应命中块、答案要点、失败归类),「答案要点」写成若干条机器可判的事实而非整段标准答案;配一个 30 秒能跑完的自动评估脚本。条数账:抽样波动按 √n 收敛,50 条波动约 ±7 个百分点,只够看大改动的方向;200 条波动 ±3.5 个百分点,才可信地分辨 5 个百分点级别的精细改进。所以 50 条起步、200 条验细节,之后随线上坏答案滚动入库——评估集是随系统生长的资产,不是一次写完的试卷。
检索质量迭代为什么要一次只动一个变量?失败为什么要分账?
一次只动一个变量,才能把分数变化归因到那一层——混合检索、重排序、切块参数同时上,分数涨了不知道谁的功劳,跌了不知道修哪里。失败分账同理:「该找到的没找到」是检索的锅,回去补召回与排序;「找到了答错」是生成的锅,回去补提示词约束与引用校验——混在一起算总账,优化就变成了盲改。这就是评估闭环:改动 → 跑评估集 → 分账 → 归因 → 下一轮。
文档更新、删除和用户撤权后,怎样证明旧索引与缓存不再泄露旧内容?
把静态题扩成事件序列:先查询填缓存,再分别发布新版、写删除墓碑或撤销一人权限。更新只允许活动版本且旧事件不可复活旧块;删除先验证所有身份不可见再验物理清理;撤权同时验证被撤者拒绝和保留资格者可读。查询进入重排与模型前核对当前版本和权限,并在真实部署测封读延迟、缓存及会话入口;离线模拟不能冒充跨服务保证。
参考与延伸
- RAGAS:RAG 自动化评估的开源框架(忠实度、回复相关性〔旧称答案相关性〕等指标) — docs.ragas.io/en/stable/
- LangSmith 评估概念文档:LLM 应用评估的通用框架与指标设计(原 LangChain 评估章节已迁至此) — docs.langchain.com/langsmith/evaluation-concepts
- LlamaIndex 评估模块:检索与生成两层的评估指标体系 — docs.llamaindex.ai/en/stable/module_guides/evaluating/
- 站内前置:RAG 检索增强生成(五段管线)、RAG 进阶(混合检索、重排序与评估集纲目)
下一站:Codebase Agent:让模型读懂整个代码库——把这套「检索 + 工具 + 评估」的本事,搬到最难的语料上:代码本身。