我们用三个多月把公司近万份文档接进了大模型
我们用三个多月把公司近万份文档接进了大模型
作者:海云日记
日期:2026年6月23日
标签:#agent面试 #agent简历
去年上半年我们团队内部在推一个项目,目标很简单——把散在各处的技术文档统一接入大模型,让技术同学问问题的时候不用再翻 Confluence 和老员工聊。
听起来不复杂对吧?我一开始也觉得一两周应该能搞定。结果项目做完我才意识到,当时低估了这件事的难度——这几乎是所有企业在做知识库时最容易犯的错。
从数据清洗到系统全面上线,四个人前后花了将近三个半月,中间踩的坑一把接一把。这篇想聊聊整个过程,特别是那些「以为搞定了结果没搞定」的环节。因为这类内容网上一搜一大堆 RAG 教程,但真的做过项目的人都知道,教程和生产环境之间隔了一个峡谷。
一、数据层:最脏也最关键
我们内部有一批技术文档,盘点完总共 8600 多份,来源很杂:
- 从 Confluence 导出的
- 各种 Word 文档
- 历史项目沉淀的 Markdown
- 内部技术论坛的精华帖
文档长度分布极不均匀,最短的百字,最长的接近 3 万字(一份完整的服务部署手册)。统计下来约 97% 的文档在 5000 字以内,只有不到 3% 属于超长文档。这个分布特点直接影响了后面的分块策略。
噪音清洗——占了 65% 的工时
文档里夹带的噪音类型特别多。因为很多文档是从内网系统导出来的,所以带着大量页面元信息:Confluence 的页面 ID 残留、上传时间戳、下载次数统计、编辑历史记录。有一类文档结尾是「上次编辑者:XXX」,还有些文档夹着「您的浏览器不支持 video 标签」这种多媒体兼容提示。论坛帖子里更杂——回复区的结构标记、附件下载提示、纯数字行,什么都有。这些东西如果直接进向量库,检索结果会非常迷幻。
我们做了好几轮多人抽样加人工审核:
- 第一轮随机抽 50 份,用
python-docx提取纯文本 - 把文本丢给大模型帮我们识别噪音模式(大模型在这一步确实有用,能快速发现人工不容易注意到的模式,比如论坛帖子里固定的回复区结构标记)
- 整理成正则规则之后跑全量数据,再抽样验证清洗效果
- 这个过程迭代了 五六轮 才基本收敛
光清洗脚本里的正则规则就写了 40 多条,脚本本身改了多个版本。
你可能觉得这不就是写几条正则?实际上这一步占了整个项目将近 65% 的工时,没有任何捷径。
双版本输出
清洗完成之后我们做了一个设计:对每份文档同时输出两个版本——一个 Markdown 给机器入库,一个干净的 Word 给人看。这样当系统给出回答引用来源时,用户点开文档能直接看到格式完整的版本,体验顺滑很多。
元数据增强
清洗完的文档元数据里只有文件名、路径、分类这些物理属性,对检索来说远远不够。
举个例子:用户问「MQTT 怎么配置」,但文档标题可能叫《数据采集通讯模块使用说明》,靠文件名匹配根本搜不到。
我们让大模型逐份阅读文档内容,生成三类语义信息:
- 一句话摘要
- 5~8 个关键技术术语
- 3~5 个「这份文档能回答的问题」
这几个 questions 字段配合 HyDE 检索策略,对问答场景的召回率提升非常明显——因为用户的问题和文档能回答的问题往往更接近。
批量处理用了多线程加断点续传,每 10 条存一次进度,8600 多份文档跑下来大概 一天半。
二、向量化阶段
向量库选型:从 Chroma 到 Milvus
POC 阶段我们用的是 Chroma,嵌入式模式,装个 pip 包就能跑,快速验证检索效果没问题。但进入 MVP 的时候,考虑到后面要支撑部门级使用、文档量还会持续增长,Chroma 在并发查询和 collection 管理上的短板比较明显,我们决定迁移到 Milvus。
这个迁移本身代价不小。Milvus 的 collection schema 需要预先定义字段类型,不像 Chroma 那样自动推断。我们重新设计了一版 schema:
id:INT64 自增主键filename和chunk_index:标量字段content:VARCHARembedding:FLOAT_VECTOR- 索引选 HNSW,M 值设 16,efConstruction 设 256,ef 参数查询时动态调——召回阶段开到 128,精排后缩到 64
这些参数的调优花了好几天,跑评测集对比不同配置下的 Recall,最后才定下来。向量库选型这件事,POC 阶段可以随便选,但一旦上了量再迁,成本是实打实的。
Embedding 模型的坑
最开始本地用 Ollama 跑 bge-m3,频繁崩溃,特别是文档长度超过 4000 字符的时候进程会不稳定,批量入库失败率能到 15% 以上。
后来切换成调云端 API,用的是 Qwen3-Embedding-8B,1024 维,支持 8k 以上上下文窗口,稳定性和速度都好很多。
中间还出过一次事故——切换 Embedding 模型之后向量维度从 768 变成 1024,Milvus 里的 collection 直接作废,得 drop 掉重建,8600 多份文档重新跑一遍 Embedding 入库,一天的活白干了。后来学乖了:Embedding 模型选型在数据清洗之前就锁定,不在入库过程中换。
三、分块策略
因为 97% 的文档都在 5000 字以内,我们决定短文档整篇入库不切分,保持语义完整性。超过 6000 字的长文档用 RecursiveCharacterTextSplitter 切,chunk_size 设 6000,overlap 设 500。
每个切片开头还会附带文档标题、摘要和关键词——这样即使检索到的是第 N 个切片,大模型也能通过附带的上下文理解这篇文档的整体背景。
四、检索层:看起来很好但其实不行的一环
我们遇到过一个很典型的问题:向量检索确实把正确的文档召回了,但最终大模型给的答案是 「未找到相关信息」。这个现象太反直觉了,花了快两天排查。
原因分析
原来我们给每个文档切片的开头都附加了全文摘要,结果摘要块的语义相似度太强,在 Top-K 里常年霸榜,而真正包含答案的正文切片排名太后被截断了。
比如用户问一个具体的配置参数,Chunk 0 里面包含的是摘要,跟「配置」这个词的语义距离最近,排名永远第一,但真正有参数表格的那个 Chunk 3 反而被挤出 Top-K 了。
解决方案:「连坐召回」
引入一个策略:把切片视为子文档,把原始文档视为父文档——只要任何一个切片被向量检索命中,就强制拉出这篇文档的全部切片,按 chunk_index 排序后重新拼接给大模型。
在 Milvus 里实现这个逻辑:
- 用
search查到 Top-K 切片 - 提取去重后的
filename列表 - 用
query接口配合expr表达式做标量过滤,把filename in 列表的全部切片拉出来
关键是第二步的 expr 过滤走的是标量索引,不走向量检索,速度很快,不会成为瓶颈。这个改动上线之后,「搜到了但答错」的问题基本消失了。
五、Rerank:数据驱动的调参
我们做了一套检索层评测:从线上真实问答记录里筛出 300 个有代表性的内部问题作为测试集,每个问题都标注了正确答案应该来自哪份文档,然后量化 Recall@K。
结果挺出乎意料的:
| 方案 | Recall@5 |
|---|---|
| 不加 Rerank(向量检索 + 连坐召回) | 94%(300 中 282 命中) |
| 引入 Rerank | 99.3%(300 中 298 命中) |
我们用的是 bge-reranker-v2-m3 做精排,部署在公司的一台 A10 GPU 服务器上,走 FP16 推理。
一开始对 Top-50 个候选做重排,单次请求耗时 12.6 秒,完全不可接受。后来回头看评测数据——既然 Top-5 已经 94%,说明绝大部分答案都在 Top-20 以内,继续扩大到 Top-50 的边际收益极低但计算成本线性增长。
于是把粗排候选集从 50 降到 20,重排耗时降到 2.8 秒,召回率保持在 99% 以上。这个调参的依据是评测数据,不是拍脑袋。
六、生成层
生成层用的是内部部署的 Qwen2.5-72B,通过公司统一的模型推理网关调用。
多轮对话这块做了 Query Rewrite,用 Qwen2.5-7B-Instruct 做轻量级改写——读对话历史,把指代性问题改写成完整的自包含问题。比如用户问「它的过滤规则怎么配」,结合上一轮关于 Nginx 的讨论,改写成「Nginx 如何配置过滤规则」。改写耗时 1 秒以内,前端会展示改写后的查询词,让用户感知到系统听懂了。
七、一个常被忽视的重要问题:图片
技术文档里有大量截图——界面操作步骤、报错截图、架构示意图。光我们从 Word 里提取出来的嵌入图片就有 3700 多张。如果系统只能返回文字,用户体验会非常差,图顶一千字。
POC 阶段我们用了 Base64 内嵌的方式,直接把图片转成字符串塞进响应里。能用,但太粗糙——一张 200KB 的截图转成 Base64 后体积膨胀三分之一,加载慢,也没法缓存。
MVP 阶段改成了 MinIO 对象存储,图片通过标准 HTTP URL 访问,浏览器可以缓存,后续还能接 CDN 加速。数据清洗阶段把图片上传到 MinIO 生成映射文件,LLM 回答里如果包含 Markdown 图片语法,后端在返回前端之前做一次 URL 替换,把本地路径换成 HTTP 地址。整套方案解耦得比较干净,后续换存储方案只需要重跑迁移脚本,业务代码不用动。
八、数据闭环与监控
系统上线之后,还有一件事很多团队没做:数据闭环。
数据层用 PostgreSQL + Redis——PostgreSQL 存会话、消息、反馈这些业务数据,Redis 做查询缓存和会话状态管理。
消息表里埋了几个字段:
- 改写后的查询
- 检索到的文档列表和相关度分数
- 各阶段耗时统计
用户有点赞/点踩的功能,点踩的时候会弹出输入框让他们填理由。这些数据会入库,用来做持续迭代的依据。
我一直觉得,知识库项目最有价值的资产不是模型本身,而是这些反馈数据。用户每次点踩的时候,其实就是在帮你标注 Bad Case。这些数据积累下来,是后续优化检索策略、迭代 Prompt 甚至做微调的基础。所以反馈机制一定要在 MVP 阶段就上,不能等到系统稳定了再说——因为用户一旦用顺了就很难再养成反馈习惯。
监控指标
整个系统跑在 K8s 上:
- FastAPI 后端 3 个副本
- Next.js 前端 2 个副本
- Milvus 三节点 standalone 集群
- GPU 节点单独调度跑 Rerank 模型
监控用的 Prometheus + Grafana,主要盯三个指标:
- Milvus 的查询延迟
- GPU 利用率
- API 的 P99 响应时间
上线第一周 Milvus 的内存占用涨得很快,排查发现是 HNSW 索引的 ef 参数开太大导致内存膨胀,调到 64 之后稳定了。
九、一些反思
回头看这个项目,最让我印象深刻的不是哪个技术细节,而是——很多拿来用就能跑的开源 RAG 框架,其实给了大家一种假象,以为知识库是一个低门槛的事。
这个假象的代价是:很多团队在一个框架上套了几周,发现效果不行之后归因到模型能力,然后换个模型继续试,结果还是不行。但根本原因往往不在模型,而在数据——数据清洗、分块策略、元数据增强,这些工作没做好,换再好的模型也救不了。
我们做这个项目用的是手搓方案,没有依赖 LangChain 或 LlamaIndex 这类框架,就是为了对数据处理的每个环节有足够的控制力。后端核心 RAG 引擎加上清洗管道大概 3000 多行代码,不算多,但灵活性高很多,出了问题也能直接定位到具体代码。
对链路的控制力比开发速度重要得多。
十、跟求职有关的事
最后说一个跟求职有关的事。
我最近给一些做 AI 方向的同学看简历,发现一个很普遍的问题——简历上「熟悉 RAG」或者「了解大模型应用」的人很多,但真的能说清楚自己做过什么、踩了什么坑、用数据说明了什么效果的,少之又少。
这类知识库项目,哪怕你是在公司内部做的、规模不大,只要你能说清楚:
- 数据清洗的迭代过程
- 检索策略的调参依据
- 评测指标的设计逻辑
面试官眼里你和「我用了 RAG 框架搭了个 Demo」的人,压根不是一个量级的。
那些坑、那些数据、那些决策依据——都是证据。
技术类岗位的面试,本质上是在验证你有没有真正做过这件事。你说的每一个细节,都是最好的证明。
Share Article
If this article helped you, please share it with others!