三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

RAG 知识库问答实战(1):RAG 是什么与整体架构拆解

RAG 知识库问答实战(1):RAG 是什么与整体架构拆解

很多团队第一次做知识库问答,会把任务理解成“把公司文档塞给大模型”。真正的难点却是:模型参数里没有刚发布的制度,提示窗口也装不下全部资料;即便回答碰巧正确,用户仍会追问依据在哪里。RAG(Retrieval-Augmented Generation,检索增强生成)解决的不是模型训练问题,而是在回答前动态寻找证据,把外部知识与问题一起交给模型,并保留可核验的来源。

一、痛点:先把“会聊天”和“有依据地回答”分开

纯大模型依靠训练时见过的统计模式生成文本,知识有截止日期,企业内部资料通常从未进入训练集。微调可以改变语气、格式和行为偏好,却不适合每天同步几十份制度:训练成本高,更新慢,也难删除某一条过期知识。RAG 将“知道什么”放在可更新的索引里,将“怎样回答”留给生成模型,因此文档变动时通常只需重建受影响的索引。

一条完整链路分离为离线和在线两部分。离线侧加载 PDF、网页或数据库记录,清洗噪声,切成可检索片段,为片段计算嵌入并写入向量库,同时保存文档 ID、页码、版本和权限。在线侧接收问题,改写查询,召回候选片段,按相关性重排,拼装上下文,调用大模型生成答案,最后返回引用。观测系统贯穿两侧,记录召回、延迟、成本和失败阶段。

二、原理:检索器负责找证据,生成器负责组织答案

嵌入模型把文本映射为向量,语义接近的文本通常距离更近。向量检索擅长同义表达,但对产品编号、错误码等精确词未必可靠;BM25 关键词检索正好互补。生产系统常用混合检索,再用重排模型逐对判断“问题—片段”相关性。大模型不应看到整个库,而只看到少量高质量候选以及明确的回答约束。

下面以词项集合模拟最小检索器。它不是生产嵌入模型,却完整展示索引、打分、Top-K 和带来源回答的数据流;换成向量库时,接口仍可保持一致。

fromdataclassesimportdataclassimportre@dataclass(frozen=True)classChunk:chunk_id:strsource:strtext:strdeftokens(text:str)->set[str]:returnset(re.findall(r"[A-Za-z0-9]+|[\u4e00-\u9fff]",text.lower()))defscore(query:str,chunk:Chunk)->float:q,d=tokens(query),tokens(chunk.text)returnlen(q&d)/len(q|d)ifq|delse0.0chunks=[Chunk("c1","报销制度.md","差旅报销应在出差结束后十个工作日内提交"),Chunk("c2","休假制度.md","年假申请需提前三个工作日提交"),Chunk("c3","采购制度.md","采购金额超过五万元需要财务复核"),]query="出差结束后多久提交报销"ranked=sorted(chunks,key=lambdax:(-score(query,x),x.chunk_id))foriteminranked[:2]:print(f"{item.chunk_id}\t{score(query,item):.3f}\t{item.source}")best=ranked[0]print(f"answer={best.text}[来源:{best.source}#{best.chunk_id}]")

运行输出:

c1 0.450 报销制度.md c2 0.091 休假制度.md answer=差旅报销应在出差结束后十个工作日内提交 [来源: 报销制度.md#c1]

这个例子揭示两个重要契约:检索结果必须携带稳定标识和元数据;排序相同时必须有确定的次级排序,否则离线评测会漂移。生成阶段使用的不是裸文本列表,而是带编号的证据包。回答中的引用编号再映射回原文 URL 或页码,用户才能复核。

三、实现:先建立可替换组件和失败边界

最小系统可定义五个接口:Loader产出文档,Splitter产出片段,Retriever返回候选,Reranker调整顺序,Generator基于证据回答。不要一开始把它们写进一个函数。组件边界允许分别测试“没找到”和“找到了却答错”:前者优化索引与查询,后者优化上下文和提示。

容量规划也应从数据流开始。假设每个片段平均 400 字、保留 80 字重叠,十万页可能产生数十万向量;在线一次检索只取 20 条,重排后送 5 条。下列脚本用漏斗方式估算上下文预算,并在证据超限时按排序截断。

question_tokens=24system_tokens=180answer_reserve=500context_window=2048chunks=[("c1",310),("c2",280),("c3",460),("c4",390)]budget=context_window-question_tokens-system_tokens-answer_reserve selected=[]used=0forchunk_id,sizeinchunks:ifused+size<=budget:selected.append(chunk_id)used+=sizeprint(f"evidence_budget={budget}")print(f"selected={','.join(selected)}")print(f"used={used}, remaining={budget-used}")

运行输出:

evidence_budget=1344 selected=c1,c2,c3 used=1050, remaining=294

预算必须预留系统提示、问题、回答和工具协议空间。按字符粗估只适合原型,接入具体模型后应使用对应 tokenizer。截断也不能从字符串中间硬切,应以片段为单位,并尽量保留标题路径,让一段话脱离原页后仍有语义。

四、踩坑:不要用一次漂亮演示替代系统验收

常见误区是只观察最终答案。答案错可能因为源文档缺失、解析乱码、切分破坏语义、检索漏召回、重排错序、上下文被截断或模型无视证据。每阶段都要输出可观察事件:索引版本、查询、候选 ID 与分数、最终证据、提示版本、模型版本和引用映射。日志应脱敏,权限过滤必须发生在返回候选之前,不能依赖提示词要求模型“别泄露”。

另一个误区是把相似度当可信度。向量库总能返回 Top-K,即便所有结果都不相关。因此要设置拒答机制:可以用校准过的分数阈值,也可以要求重排器和生成器共同判断证据是否充分。阈值不能凭感觉确定,要用包含“库内可答”和“库外不可答”的验证集选择。

架构设计还要明确数据新鲜度。高频变动的公告可以分钟级增量同步,稳定手册则按日扫描;索引响应应返回语料版本,使用户知道答案依据哪个时间点。原文被删除或权限收紧后,片段、向量、关键词索引和缓存都必须同步失效,不能只在源系统隐藏文件。

五、验证:用分层指标建立第一条基线

离线验收至少包含检索 Recall@K、引用命中率、答案正确性和拒答准确率;在线还要看 p50/p95 延迟、空结果率、每次调用 token 与用户追问率。准备二十个真实问题作为起点,每题标注可接受答案、证据文档和是否应该拒答。先手工跑通并保存各阶段产物,再引入框架,能够显著降低“框架能跑但不知道为何错”的排障成本。

本篇建立了 RAG 的双流水线和组件边界。下一篇将进入离线链路的第一站:把杂乱文档稳定加载、清洗并切成既能召回又能读懂的片段。

参考来源

  • Lewis 等:Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
  • Microsoft:Advanced RAG Techniques
  • Pinecone:Retrieval-Augmented Generation

👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。

🚀 本文属于《RAG 知识库问答实战》系列,持续更新,关注不迷路。

📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。

← 返回列表