news 2026/10/19 1:53:08

RAG最小闭环实战:从文档切分到知识库问答的完整记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RAG最小闭环实战:从文档切分到知识库问答的完整记录

说是学习日记,但今天一整天基本都在跟"检索"这个词较劲。起因是上周接到一个需求:对方公司想把几百份内部文档变成能直接问答的知识库,而不是让人翻文件夹。我调研了一圈,决定趁周末把检索增强生成(RAG)从原理到最小落地完整过一遍。今天这篇学习日记就是当天学习过程的流水账加复盘:从为什么选RAG、上午怎么补概念,到下午怎么把代码跑通,再到三个让我卡了一下午的坑,全部记录下来。

这篇日记想写给和我一样的人:知道RAG这个词、看过概念图,但没真正在一个干净环境里从零跑通最小闭环。我会尽量把参数、代码、报错信息都留下来,方便你直接复现。整个过程没有用特别复杂的框架,主要是Python、一个开源向量索引、以及一个兼容多数厂商的模型接口,所以基础要求不高,有Python环境和一点点命令行经验就能跟上。

1. 为什么跨过Demo直接写最小闭环

1.1 起因:一个"内部知识库问答"的诉求

前两天和人聊需求,对方说得很直白:公司文件夹里存了几百份产品文档、项目总结和售后记录,新员工入职要翻三天才能找到想要的信息。他们想要一个对话框,员工直接问"XX功能在哪个模块里,配置项叫什么",它能从文档里找到答案并给出来源。

听完需求我第一反应不是写代码,而是想到两个方案。第一个是微调一个私有模型,让它记住这些文档内容;第二个是走RAG,不改变模型权重,只在问答前先检索相关资料,拼到提示词里再让模型回答。我选了RAG,原因有三:文档会持续更新,RAG改一下索引库就行,微调却要重新训练;几百份文档的规模对检索完全够用,但对微调来说数据量偏少;最后是成本,微调需要算力和更长的调试周期,RAG今天就能看到效果。

1.2 目标拆解:今天不碰的部分

确定方向后,我把目标压缩成一个最小闭环:加载文档、把长文本切成小块、对每一块做向量化、把向量存进索引、用户提问时先检索最相关的块、把检索结果拼进提示词、调用大模型接口生成答案。这个闭环能跑通,就意味着"问答知识库"这件事已经成立,后面只是工程优化。

我同时给自己列了一个"今天不碰"清单:不做多轮对话、不做权限隔离、不做流式输出、不做向量库高可用。原因是这些都属于锦上添花,尤其在第一天,纠结任何一个都会把主链路拖垮。我的经验是,任何学习型项目第一天最重要的不是功能全,而是让数据从一头流到另一头。

1.3 环境准备:一台干净机器就够了

动手前先确认环境。我用的是一台普通的开发机,没有独立显卡,因为RAG的主链路里最吃算力的向量化和生成都走远程接口,本地只负责文本处理和检索。Python版本直接用了系统里现成的3.10,装了三个基础库:一个用来解析常见文档格式,一个用来做向量索引,还有一个用来发HTTP请求。

这里有个容易被忽略的点:很多人在环境准备阶段喜欢把能装的全装上,结果依赖冲突浪费两小时。我今天的做法是,先装运行主闭环必须的库,遇到缺什么再补什么。第一次跑通前,少装一个库比多装一个库更好排错。

2. 上午:先啃概念,免得下午瞎敲代码

2.1 资料筛选:我最后留下哪三类

上午没有直接开代码,而是先花两个半小时把概念捋清楚。说真的,RAG这个名字看着唬人,拆开就三件事:查、拼、生成。我主要看了三类资料。第一类是官方概念文档,重点看"检索"和"增强"两个词分别指什么;第二类是别人跑通的完整示例,特别关注他们用了什么切分长度和检索条数;第三类是报错合集,提前了解最常见的坑在哪里。

筛选资料有个很粗暴的标准:凡是超过三屏才进入正题的文章,直接跳过。学习期最怕的是用战术上的勤奋掩盖战略上的懒惰,材料收藏了一大堆,真正动手时却不知道从哪一步开始。今天最后沉淀下来的核心笔记也就三页纸:embedding是什么、切分怎么选、检索条数怎么定。

2.2 用外卖菜单打个比方

我习惯用一个外卖菜单的例子来理解embedding。假设你面前有几百道菜,每道菜单独抽出来描述它的特征:辣度、咸度、价格、主料、烹饪方式。如果把这些特征组合成一串数字,这道菜就能对应到多维空间里的一个点。口味相近的菜,点的位置也靠近。

文本embedding做的事情类似:它把"XX功能的配置项名称"这样一句话,映射成几百维的向量,语义接近的话,向量方向也接近。检索的本质就是在这个向量空间里找"离得最近"的点。这个比方后来帮了大忙,因为下午遇到检索不准的问题时,我会习惯性问自己:是我的向量空间映射有问题,还是在空间里找最近邻的时候策略有问题。

2.3 切分策略:不是越短越好

切分是RAG里最容易被低估的环节。所谓切分,就是把一篇完整文档切成若干个小块,每一块单独向量化。切分长度直接决定检索的粒度。我一开始的想法很朴素,切短一点,每个块的主题更纯粹,检索应该更准。但实际推演下来发现不是这么回事。

假如把一份操作手册按每100字切一块,一个完整的功能说明很可能被拦腰截断,变成两块互不相干的话。用户提问时,检索系统可能只命中了后半块,而前半块里的关键配置项名称反而丢了。切分的关键不是长度,而是"保持语义完整"。所以我上午的结论是:按段落切,段落太长再按句子边界补一刀,宁可块的长度不一致,也别把话说一半。

3. 下午:从零搭一个能跑通的最小RAG

3.1 最简架构设计

下午两点开始写代码。架构就五层:文档加载、文本切分、向量化入库、检索、拼装提示词调用模型。我没有用重量级框架,因为这些环节用百来行Python就能串起来,框架反而会遮住数据流动的过程。

数据流动是这样的:原始文档先被读成纯文本,按我上午定的策略切分成块;每一块调embedding接口转成向量,连同原文一起存进向量索引;用户提问时,把问题也转成向量,在索引里找距离最近的几个块;最后把问题和这些块拼成一个带上下文的提示词,发给大模型接口,拿到答案。这个流程里最需要关注的是"文本块"这个单位,因为它既承载检索,又承载上下文。

3.2 核心代码逐段拆解

加载文档我用了一个通用的解析函数,支持PDF和Word两种格式。代码并不复杂,核心是把文件读成文本,然后按段落粗切:

def load_document(path): text = extract_text_from_file(path) # 根据扩展名选解析器 return text def split_into_blocks(text, max_chars=800): blocks = [] current = [] for line in text.splitlines(): if sum(len(x) for x in current) + len(line) > max_chars and current: blocks.append("\n".join(current)) current = [line] else: current.append(line) if current: blocks.append("\n".join(current)) return blocks

这里的max_chars我选了800,理由很简单:按中文估算,800字大约是一页半文档的量,既能保住一个完整小节,又不会太碎。切分时我会先把文档按一级标题标记出来,让同一小节的段落尽量进同一个块。

向量化入库的部分更直接。每拿到一个文本块,就调一次embedding接口,拿回一个向量列表,然后把向量和原文一起写进索引:

def build_index(blocks): index = [] for block in blocks: vec = embedding_client.encode(block) # 返回一串浮点数 index.append({"vector": vec, "text": block}) return index

检索的时候,把用户问题也编码成向量,然后和索引里所有向量算余弦相似度,取top_k。我用的是最朴素的线性扫描,因为文档量小,几百个向量全量算一遍也只有几毫秒。

def search(query_vec, index, top_k=4): scored = [] for item in index: score = cosine_similarity(query_vec, item["vector"]) scored.append((score, item["text"])) scored.sort(reverse=True, key=lambda x: x[0]) return [text for _, text in scored[:top_k]]

最后拼提示词时,我把检索到的文本块按顺序放进上下文字段,让它只依据这些内容回答,并且要求答案里带上来源编号。这样一个最小RAG就通了。

3.3 三个测试问题检验效果

索引建好后,我准备了三个测试问题。第一个是"XX功能的配置项叫什么",用来验证能不能从文档里找到明确答案;第二个是"售后反馈里最常见的问题是什么",验证跨文档聚合;第三个是"新员工应该先看哪些文档",验证推荐类问题。

第一次跑的结论比较微妙。第一个问题答得很好,模型从两个文本块里抽出了配置项名称,来源也对。第二个问题勉强可以,但有一半信息来自错误文档。第三个问题基本失败,模型给了通用建议,没有任何文档依据。这个结果提醒我:检索环节的召回质量,直接决定生成环节的回答质量,问题二和三暴露的其实是检索策略的问题。

4. 卡了一下午的三个坑

4.1 第一个坑:切分粒度不匹配导致答非所问

问题二为什么答错?我先去看检索命中的是哪些文本块。结果发现,售后反馈这个主题散落在十几份文档里,而我的切分策略把每份文档按小节切块,一个块只包含一次反馈记录,信息太碎。用户问的是"最常见的问题",模型看到的是几块孤立的反馈,当然归纳不出来。

排查链路是这样走的:先打印检索top_k的原文,确认数据来源;再检查每一块的切分边界,发现大量块都只覆盖了局部;最后把切分策略改成"按主题聚合"——先按文档里的标题归类,把同一主题下的多个段落放进一个块。改完后,同一个块里能同时包含问题描述、原因分析和解决建议,检索效果立刻好转。

这个坑的本质是:切分粒度要匹配问题粒度。问答是细粒度,可以依赖小块;归纳汇总类的问题是粗粒度,必须保证块里包含足够上下文。

4.2 第二个坑:上下文超长被静默截断

问题三给了一个特别不像话的答案,而且没有任何报错。我一开始以为是大模型接口的问题,反复换参数都没用。后来把发给模型的完整提示词打印出来才发现,上下文长度限制把最后两段检索内容截断了,而最后一段恰恰包含关键的文档标题信息。

这个坑有点隐蔽,因为接口在很多情况下不会显式报错,而是直接截断超长内容。排查时最有效的办法就是"看完整请求":把提示词原样打印出来,数一数字符数,再对照模型接口的上下文上限。我后来在代码里加了一个保护逻辑,预估超过上限就先压缩检索条数和文本块长度,而不是硬发。

4.3 第三个坑:检索结果看着相关,实际没用

第三个坑发生在调整完切分之后。检索返回的top_k里出现了很多"表面相关"的块:它们都提到了关键词,但聊的内容根本不是用户想问的。比如用户问"配置项在哪里修改",检索到的块里全是"配置项的默认值是什么",字面上像,语义上差很远。

我后来意识到,这是向量检索的天然局限,它抓的是语义相似,不是问题意图匹配。我用了一个笨但有效的修正方案:把top_k从4提升到8,然后在拼提示词时加入一个"要求模型只使用与问题真正相关的块"的指令,并让模型在无法回答时明确说"文档中没有找到"。这样虽然偶尔还是会召回无关块,但模型能够自己过滤掉一部分。

5. 晚上复盘:今天到底收获了什么

5.1 一条值得坚持的调试路径

今天最大的收获不是把RAG跑通了,而是总结出一条调试路径。所有问题都从"数据流"入手看:问题进来之后,先看检索返回了什么,再看提示词拼成了什么样,最后才看模型答了什么。RAG链路的每一环都会掩盖上一环的问题,如果跳过中间检查直接看最终答案,很容易误判成"模型太笨"或"接口不稳"。

我把这条路径写成了四句备忘:检索引擎返回的必须打印原文;提示词必须完整可回溯;每个参数改动只动一个变量;跑不通时回到最小用例,而不是继续堆功能。

5.2 明天准备优化的两个方向

下一步我准备做两件事。第一是把检索条的来源标注做扎实,在提示词里让模型引用文档编号,这样用户能看到答案出处,也更方便校验正确性。第二是把切分策略从"按标题聚合"升级成"递归切分加重叠区",让上下文衔接更平滑。另外我还在考虑加一层重排,把向量检索召回的候选再做一次深度过滤,专治今天遇到的"表面相关"问题。

说实话,一天的实践下来,最大的感受是:RAG本身不难,难的是把每一个环节的细节调到适配你的文档和问题。同样是切分,换一批文档可能就要换一套策略;同样是检索,换一种提问方式可能就要调top_k。这也提醒我,别指望一次性搭好就万事大吉,索引和策略都需要跟着语料一起迭代。今天的日记写到这里正好,明天继续填这两个坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 20:46:16

Spring Boot校园一卡通系统实战:从状态管理到并发控制

校园一卡通管理系统,可以说是校园信息化系统里最“接地气”的一类项目了。只要在学校待过,食堂刷卡、超市消费、门禁通行、图书馆借阅,背后都是这套系统在支撑。我做过的这套Spring Boot校园一卡通管理系统,技术上不算花哨&#x…

作者头像 李华
网站建设 2026/10/16 19:13:42

AI自动生成断言:测试预言革命如何重构自动化测试的判断标准

先聊一个我印象很深的夜晚。凌晨一点,整套回归测试突然爆红,我扒开日志一看,接口返回的状态值从“OK”变成了“SUCCESS”。测试直接报断言失败,看起来是逮住了一个严重回归,但查了一整晚才发现,是产品经理当…

作者头像 李华
网站建设 2026/10/16 11:35:08

Ubuntu 18.04服务器凌晨自动重启?彻底关闭unattended-upgrade实战指南

最近被拉去一起看一台反复在凌晨自动重启的 Ubuntu 18.04 服务器,现象很典型:凌晨三点左右机器开始刷 apt 源、下载安全补丁,如果涉及内核更新,安装完成后就自动重启。业务日志断了几分钟,批处理任务全部被打散。查了一…

作者头像 李华
网站建设 2026/10/16 22:07:01

嵌入式开发必学数据结构:线性表、顺序表与链表实战指南

做嵌入式开发的人,十有八九都经历过这种场面:串口数据一多,缓冲区就不够用了;用数组存设备节点状态,增删一个设备要把整个表重新排一遍;中断里想往等待队列里塞数据,写着写着就不知道指针飘到哪…

作者头像 李华
网站建设 2026/10/17 23:41:04

麒麟V10救援模式显示异常排查与修复指南

我去年底接手过一台启动黑屏的麒麟V10-SP1机器,当时第一个判断就是救援模式,等真正进到救援模式,才发现显卡驱动加载那段的显示问题比系统本身的故障还麻烦。这篇文章就结合麒麟桌面系统V10-SP1 2503这个版本,把救援模式下显示异常…

作者头像 李华
网站建设 2026/10/18 8:26:41

百万级并发网络编程:Reactor模型原理与epoll实战调优

搞 Linux 网络编程的人,早晚都会撞上“百万级并发”这个词。面试要聊、压测要跑、真正的线上流量高峰也在考人。我早年在某内部系统上做长连接服务,一开始很天真,一个连接开一个线程,数据量稍微上来,整个进程直接卡到没…

作者头像 李华