最近这两年企业级RAG系统几乎是技术圈默认要补的一门课。别误会我不是说大家都在用——至少问到身边的人真正把RAG从Demo推到生产、让公司内部业务跑起来的还是少数。多数团队卡在同一个地方文档切片切不好检索结果不稳定向量库的配置全靠网上copy跑起来很慢或者干脆搜不到。这篇文章想聊的就是我所在团队过去一年多的实践。我们把公司内部的制度、技术文档、产品FAQ逐步结构化构建了一套基于Milvus向量数据库的检索增强生成系统服务对象从最开始的一个小工具变成了几个内部业务系统。Milvus不是唯一的选择但对企业级场景来说它在海量向量检索、元数据过滤、水平扩展这些维度上是少有的开箱即用且能扛住生产压力的方案。这篇内容不打算讲教科书式的原理而是把从数据准备到线上调优的完整链路摊开适合正在技术选型或者已经跑通Demo但不知道怎么往生产走的工程师。1. 企业级RAG的整体架构思路1.1 先搞清楚RAG到底解决了什么企业内部做知识问答最痛的不是“模型不够聪明”而是“模型没见过公司自己的东西”。制度文件、技术方案、产品手册、历史故障报告这些内容不可能全塞进模型参数里也不应该靠频繁微调去更新。RAG的方案简单说就是把文档切块、向量化、存进向量数据库用户提问时先从库里召回最相关的片段拼进Prompt交给大模型让它基于这些素材作答。这套路最近两年被说烂了但企业落地时真正的难点是切片策略怎么定、向量化用什么模型、向量库怎么选型、召回结果怎么排序、并发压力怎么扛。任何一个环节做糙了整体效果都会塌。1.2 系统模块怎么划分我们的架构没有搞什么宏大的微服务拆分只拆成几个边界清晰的服务文档处理服务负责文件解析、清洗、切片输出统一的切片记录写入消息队列。向量化服务消费切片记录调用Embedding模型生成向量批量写入Milvus。检索服务统一对外提供检索接口内部做向量召回、过滤、混合检索、重排序。问答服务编排Prompt和上下文调用大模型接口同时承担查询改写和答案溯源。管理后台看数据量、看检索效果、看任务日志。这个划分最大的好处是每个环节可以独立压测和调参。比如向量化服务压力大了单独扩消费者就行不用动检索那边。实际上线之后也证明这个粒度刚好——再细就变成为了拆而拆运维成本反而高。1.3 为什么向量数据库选了Milvus选型的时候我们对比了pgvector、FAISS自建、Elasticsearch插件向量检索以及Milvus。结论直接放出来方案优势我们放弃的理由pgvector和业务库同源SQL顺手千万级向量后性能衰减明显维护索引和备份麻烦FAISS自建轻量、灵活需要自己写服务、自己管分片和容灾研发成本不低Elasticsearch向量检索文本检索生态成熟HNSW索引内存开销大混合检索参数调起来头大Milvus原生分布式、元数据过滤强、与主流Embedding模型兼容需要单独部署和维护但整体可控我们当时的数据量已经有小几百万向量而且权限过滤和标量查询的需求很明确Milvus的CollectionPartition标量索引这套机制正好对口。它还支持Sparse向量和Dense向量混合后面做混合检索省了很多事。2. 数据准备与切片——检索质量的地基2.1 企业文档的复杂程度超乎想象很多人拿几篇干净的Markdown做Demo觉得RAG不难。但企业内部文档真实样貌是PDF里有多栏排版、表格被图片化、页眉页脚混着正文、Excel里有合并单元格、PPT的文字散落在文本框里。这些情况不处理后面检索的质量一定崩。我们的文档处理管线分了四步格式识别按扩展名和文件头判断真实格式PDF优先用文本层提取没有文本层的走OCR。内容清洗去掉页眉页脚、重复的目录信息、多余的换行和空行表格单独抽取成结构化数据。结构归一化把不同格式的文档统一转成Markdown或JSON结构保留标题层级。切片输出基于清洗后的内容做切分并记录元数据。这里最容易翻车的点就是PDF。有的PDF看着是文字但其实是扫描图片必须OCR有的PDF文字层顺序是乱的提取出来段落颠倒。我们当时加了一个质检步骤抽取文本后检查字符密度和段落完整性异常文件单独标记让人工处理。2.2 切片策略没有银弹只有适配切片是RAG系统里争议最大、影响最直接的一环。切片太大一个块里包含太多信息向量之间互相稀释召回的精度低切片太小语义不完整大模型拿到的上下文碎片化回答也容易偏。我们最终采用了“按结构优先、长度兜底”的策略优先按Markdown标题层级切分一级标题下如果内容太长再按二级标题切。没有明确标题层级的长文本按固定Token数切chunk_size设为512overlap设为50。表格类内容单独作为一个切片不跟正文混在一起。代码片段、命令块尽量保持完整不从中截断。chunk_size和overlap不是拍脑袋定的。我们做过一组对照实验分别用256、512、768、1024跑同一个测试集最后发现512在“检索准确率”和“上下文充分性”之间最平衡。注意overlap不是为了好看是避免一个完整语义单元恰好被一刀切成两半。50个token的overlap相当于保留了上一段的尾部可以让下一段的向量更完整。2.3 元数据设计检索能不能精准就靠它光有切片文本和向量还不够企业检索一定要有元数据支撑。我们的切片元数据设计得很朴素但很实用{ doc_id: DOC-2024-00123, doc_type: 技术方案, title: 订单系统降级方案, department: 中台技术部, author: 架构组, create_time: 1710000000, page: 12, chunk_index: 3, total_chunks: 18 }这些字段全部在Milvus里建了标量索引检索时可以直接通过filter表达式缩小范围。比如只搜某个部门的文档、只搜近三个月的内容、排除某类文档都是标量过滤的活不需要动向量索引。元数据还有一个容易被忽略的价值权限控制。我们内部的文档分密级用户检索时先根据其权限生成过滤条件从源头上保证低权限用户不会召回高密级内容。这一层必须在Milvus层面做不能靠大模型遵守规则否则一定会漏。3. 向量化与Embedding模型选型3.1 Embedding模型不是越新越好很多团队选Embedding模型只看排行榜分数忽略了领域适配性和服务成本。我们测试过几款开源中文向量模型也试过通用API向量接口最后留在一款开源的中文Embedding模型上支持固定文本长度和最大512个token。选择标准大概这几条领域效果拿我们自己的1000条真实问答对做召回评测看5准确率和10召回率。向量维度维度太低信息密度不够维度太高存储和内存开销大。我们用的模型是1024维下面是768和1536维的对比情况。服务化难度模型能否用CPU推理batch推理对显存要求高不高。我们用CPU多线程做离线批量处理线上增量也用同一个服务省了GPU成本。长文档支持有些模型最长输入只有512token切片就得相应控制有些支持到8000token但向量质量在长文本上不一定更好。测试集一定要自己攒不要拿公开数据集的结果当真。公开榜单上的模型在通用领域确实好但到了我们这种夹杂大量内部术语的文档里效果排序直接就变了。3.2 批量向量化的工程细节向量化不是一个一个调API那么简单。我们一次性灌入几十万条切片如果串行处理几天都跑不完。我们的做法是从消息队列批量拉取切片每批128条。用一个独立的推理服务处理这批文本输出向量后批量写入Milvus。写入失败的重试三次三次还失败则回到队列尾部避免丢数据。每处理完100批次做一次进度上报管理后台可以看到实时状态。这里有个细节Embedding推理服务返回的向量要检查维度是否一致偶尔会因文本异常导致输出维度不对批次写入前要过滤掉否则整个批量写入会报错。3.3 向量的精度与存储权衡Milvus默认支持FP32和FP16存储也支持INT8量化。我们一开始用FP32几百万向量下来存储量确实可观。后来测了FP16:召回率和FP32几乎没区别P99延迟还低了一些内存也省了一半。INT8我们也测过召回率掉了不到1%但考虑到后续检索精度的余量最终没有上保留FP16。一个简单的存储估算公式单条向量存储 维度 × 字节数FP32为4FP16为2100万条1024维FP16向量 1000000 × 1024 × 2 ≈ 2GB这只是向量本身的裸大小没算索引和标量字段。所以规划机器的时候按2.5到3倍预留磁盘和内存。4. Milvus落地配置与索引调优4.1 Collection设计和Schema规划在Milvus里一张表叫Collection。我们的设计是这样的schema { fields: [ {name: id, type: DataType.INT64, is_primary: True}, {name: vector, type: DataType.FLOAT_VECTOR, dim: 1024}, {name: doc_id, type: DataType.VARCHAR, max_length: 64}, {name: doc_type, type: DataType.VARCHAR, max_length: 32}, {name: department, type: DataType.VARCHAR, max_length: 64}, {name: title, type: DataType.VARCHAR, max_length: 256}, {name: create_time, type: DataType.INT64}, {name: chunk_index, type: DataType.INT32} ], enable_dynamic_field: False }Collection的Shard数初期设为2随着数据增长后面扩到了4。Shard数影响写入吞吐和查询并发但并不是越多越好每个Shard有固定开销。我们的经验是写入并发高就多开Shard查询并发高则更依赖副本数。关于Consistency Level我们有明确的选择索引构建期间用Strong保证数据一致日常检索用Bounded允许一点延迟换取性能。如果全部用Strong写入放大很明显检索延迟会涨。4.2 索引类型怎么选Milvus支持FLAT、IVF_FLAT、IVF_PQ、HNSW等索引。我们测试下来最常用的就是两种索引类型适用场景参数要点FLAT数据量小百万以内、要求100%召回无需参数暴力计算HNSW大规模数据百万到千万级、高并发M16, efConstruction200, efSearch128IVF_FLAT内存紧张、可接受分段线性扫描nlist1024, nprobe16我们生产环境选了HNSW。M控制每个节点的邻居数越大召回率越高但内存也越大。efConstruction是建索引时的搜索范围调太高建索引慢调到200足够了。efSearch是查询时搜索的范围直接影响延迟和召回率。我们的做法是固定M和efConstruction把efSearch做成可配置参数根据业务场景在接口层动态调整。注意HNSW索引全部加载在内存里索引构建完成后要及时做compaction否则未合并的小Segment很分散查询会明显变慢。4.3 检索参数的工程调优Milvus查询时除了基础向量距离计算还有两个关键参数limit和search_params里的efSearch。我们的经验值是默认召回Top 200不直接返回给用户先经过重排序再截断。efSearch在128到256之间P99延迟稳定在80到120毫秒。距离度量用COSINE向量写入前先做归一化这样cosine距离和欧氏距离在数值上等价但便于跨模型对比。另外比较重要的一点查询里尽可能带过滤条件。Milvus的检索是“先过滤再在过滤结果里做向量搜索”过滤条件能大幅缩小搜索范围。比如用户只查技术文档filter里带上doc_type 技术方案检索速度明显提升。5. 检索增强与排序优化5.1 混合检索向量不是万能的纯向量检索有个典型问题关键词完全匹配但语义不相关的句子向量距离反而远而语义相关但关键词不同的句子向量距离近。比如用户搜“怎么申请年假”文档里写的是“休假制度”纯向量检索效果不错但用户搜“报销2024年发票”文档里写的是“2024年度发票报销流程”这里关键词重合度高的情况向量检索反而不如传统BM25。我们的方案是混合检索同一查询同时跑向量召回和关键词召回再用RRF算法融合两路结果。Milvus 2.4以后支持Sparse Vector我们是把BM25词频统计做成Sparse Vector存进去查询时同时计算Dense和Sparse的相似度两路结果合并。RRF融合公式很简单score sum(1 / (60 rank))这个rank是文档在两路召回中的排名。RRF的好处是不需要调权重对排名的稳定性要求比对分数的要求低。混合检索上线后我们的召回准确率大概提高了5到8个百分点尤其是FAQ类和关键词导向的查询提升尤其明显。5.2 用Cross-Encoder做重排序召回Top 200直接丢给大模型是不现实的上下文窗口有限而且不分主次的内容会稀释大模型的注意力。我们引入了一个重排序模型对召回的段落逐一打分取Top 5作为最终上下文。这里推荐用Cross-Encoder模型比如bge-reranker这类开源方案。它的特点是把query和文档拼成一个序列输入模型得到相关性分数精度远高于双塔结构的向量相似度。我们的重排序流程混合检索召回200条候选段落。Cross-Encoder对每条候选段计算得分得到相关度排序。取Top 5同时过滤掉分数低于阈值的内容。把Top 5按原始文档顺序排列作为上下文交给大模型。这个阈值的设定很关键太低了会把不相关内容塞进去太高了可能直接过滤掉正确答案。我们都是基于标注数据做ROC曲线确定阈值。5.3 缓存策略别让每一次提问都全链路跑如果每次提问都重新走“向量检索→重排序→大模型生成”成本高不说用户遇到相同问题反复问浪费资源。我们做了一个语义缓存层上线一个缓存服务核心逻辑是将query向量化后用余弦相似度在缓存池里找历史缓存。相似度超过0.93直接返回缓存答案不再走完整链路。超过0.93但不到0.98返回答案并附加一个“仅供参考可能不完全匹配”的提示。低于0.93正常走RAG链路并把新query和答案写入缓存。缓存池用Redis记录向量和答案的映射每隔一段时间基于相似度做一次清理避免缓存池无限膨胀。实践下来缓存命中率大约在20%到30%对于内部知识问答场景日间查询高峰期的延迟和成本都明显下降。6. 性能、高可用与监控6.1 压测指标和容量预估上线前不要凭感觉我们压过一轮主要看四个指标QPS检索接口支持多少并发P9999%的请求延迟召回率Top 10里有多少正确答案资源占用CPU、内存、磁盘拿数据算一笔账公司内部文档大概5万份切片后约80万条1024维FP16向量HNSW索引单机配置32核64G内存压测结果单索引调度下P99延迟约110msQPS约300。如果把索引副本从1扩展到2QPS能到500以上P99降到80ms左右。这个数据说明容器规模不用一开始开很大预留副本能力根据压测结果再横向扩就来得及。6.2 部署架构和高可用我们的Milvus部署在Kubernetes里用独立的一套不跟业务抢资源。架构大致是Milvus Standalone版本起步数据量超过千万级再切Cluster。配置了2个副本QueryNode可以水平扩展。每个索引构建任务单独跑防止主流程卡死。定期备份Collection数据到对象存储保留最近7天备份。生产环境的经验是尽量不要跟其他业务共享底层存储Milvus对磁盘I/O敏感共享存储出现抖动时查询延迟波动特别明显。扩容时有一个坑直接加副本会让所有副本同时开始加载索引加载期间查询性能会下降。我们的处理办法是先把新副本标记为“只服务写不服务读”等索引全部加载完成后再切换为服务读。6.3 观测告警怎么做Milvus自带Prometheus指标我们接进监控系统重点关注QueryNode内存用量HNSW索引常驻内存内存使用曲线如果持续走高说明Segment合并不及时。磁盘使用量数据量和索引量增长过快时提前预警。Segment状态计数大量小Segment堆积是查询变慢的前兆。检索延迟P99持续超过200ms可能是索引退化或者过滤条件失效。日志方面Milvus会输出Coordinator和DataNode的日志我们遇到问题先看DataNode写入日志再查QueryNode查询日志。我个人的习惯是压测期间把日志级别调到WARN减少大量INFO日志干扰排障。7. 常见问题与排查技巧实录7.1 检索结果为空或者完全不对这类问题出现频率最高排查顺序建议从下往上切片是否入库先查Milvus里这个doc_id的记录数确认切片没丢。过滤条件是否写错最常见的坑是把doc_type拼错或者过滤字段在Schema里没建索引导致过滤直接失败。向量是否写入检查向量维度是否一致、有没有全0向量。切片内容是否干净有时候入库同一段文本重复了几十遍检索时全是重复结果检查一下分组的chunk_index。重排序是否把正确答案过滤了调低阈值测试一下看是不是阈值卡太死。我们遇到过一次全站检索结果为空最后发现是Embedding模型升级后维度从768变成了1024而Milvus里的Collection维度还是768查询向量和库里的向量维度不匹配直接报错。所以模型升级前一定确认维度否则必须重建Collection。7.2 回答质量差答案和问题对不上检索没问题但大模型回答跑偏这种情况多是上下文的问题切片粒度太大上下文塞了太多无关段落大模型抓不住重点。调整chunk_size从512降到384试试。Top 5排序不对可能是重排序模型阈值不好也可能是RRF融合的排名权重不合理。Prompt里没引导模型对照原文回答在Prompt里明确让模型“基于给定材料回答材料中找不到时明确说明”能有效减少幻觉。我们做过一次回归测试把Prompt从“请回答用户的问题”改成“基于以下材料回答材料中没有的内容不要猜测”之后答案的准确性评分直接涨了10分。7.3 系统性能劣化和资源异常内存持续上涨优先看Segment数量如果segment多而小手动触发一次compaction。查询延迟时高时低多半是索引未完全加载或副本在滚动更新检查QueryNode的加载状态。写入吞吐上不去看DataNode的并发度还有消息队列的消费速率先看瓶颈在哪一环再动手。我之前踩过一个大坑Milvus版本升级之后没重新建索引老索引直接兼容不了查询走了FLAT暴力扫描延迟从100ms变成500ms。所以升级版本前必须做好索引兼容性评估该重建就重建。7.4 一套排查命令速查症状优先检查命令检索为空list_collections、query过滤条件、get_collection_stats延迟突增show_query_node_metrics、segment状态写入失败get_task_state、DataNode日志、磁盘空间内存暴涨show_segment_loading_progress、索引类型实际过程中最想留住的经验如果让我只挑一点分享我会说RAG系统的瓶颈往往不在模型而在数据质量。切片策略、元数据、Embedding模型、索引参数、重排序阈值这些环节每一个都可能成为拖垮整体效果的关键因素。我所在团队这段实践下来最大的体会是企业级RAG不是一个跑通的Demo而是一套需要持续调优和运维的工程系统。宁可第一天就把数据清洗和评测集搭建做扎实也不要贪快上了一个看起来能跑、实际效果不可控的系统。另外强烈建议每轮调整都留一套离线评测集次数多了你就会发现真正优化效果的不是某一个灵光一现的参数而是每一环都做到了可量化、可回溯。