RAG+Milvus构建汽修智能问答与工单闭环系统实战

RAG+Milvus构建汽修智能问答与工单闭环系统实战 1. 项目背景与系统整体设计思路先把我踩过的坑和这次项目的来龙去脉交代清楚。这个项目最初的需求很朴素汽修厂老板发现客户电话咨询太占人力而且同一个故障码比如P0301气缸1失火几乎每周都会被问一遍但技师给出的回答口径却不统一。更麻烦的是有些典型故障处理完之后没有沉淀下来下次遇到类似问题还得重新查手册、翻聊天记录。所以想做一个“能回答问题、还能把维修过程自动记录下来”的系统这就是“汽修领域智能问答与工单闭环系统”的由来。技术选型的时候我在几个方案之间犹豫过。第一版方案是直接用开源大模型 API 关键词检索建一个简单问答机器人后来发现汽修问题对“车型适配”和“故障码精度”要求非常高比如同一个“怠速抖动”故障丰田和大众的诊断路径差异很大纯靠大模型记忆很容易产生幻觉。所以最终决定引入 RAG 架构把维修手册、故障码表、案例记录全部向量化用 Milvus 做向量检索再用 FastAPI 把检索结果和大模型回答串成完整链路。这个组合解决的三个核心问题分别是知识库统一管理、故障诊断依据可追溯、工单数据自动回流到知识库形成闭环。整个系统跑通的业务闭环是这样的用户在微信小程序或者网页端提交故障描述FastAPI 接收请求后先做意图识别是“咨询类”还是“报修类”。咨询类直接走 RAG 检索 大模型生成回答报修类除了生成初步诊断建议还会自动创建一张维修工单工单进入维修流程后技师补充的处理结果会被系统抓取清洗后回写进向量知识库。也就是说每次维修都在让这个系统变聪明而不是问完就结束了。这套设计对刚接触 RAG 的开发者来说最大的参考价值在于它不仅仅是个 Demo而是把“检索 - 生成 - 反馈入库”三个环节真正串成了一个可运行的生产级系统。如果你正在做类似的知识库问答项目不管是法律、医疗还是金融领域整体架构是可以直接平移的只是需要把汽修领域的实体故障码、车型、维修工序换成对应行业的实体而已。2. 汽修故障知识库构建与 RAG 数据链路详解2.1 故障知识库的数据来源与实体关系设计做 RAG 项目的人常犯的一个错误是一上来就想着“怎么用大模型”。说实话大模型只是最后一步的生成器真正决定问答效果的是知识库的“底子”。汽修领域要建知识库数据来源我梳理下来主要有四类第一类是车型维修手册这类 PDF 文档的特点是结构规整但格式多样有 s3 的也有 u 盘的扫描版图片占了不少第二类是故障码表DTC字段包括故障码、含义、可能的故障部件、维修建议、适用车型范围第三类是历史工单数据字段包括用户描述、技师诊断、更换配件、维修时长、费用第四类是技师经验笔记这类数据最乱可能是微信聊天记录、Excel 表格甚至是一段录音转文字。数据清洗阶段有几个细节特别值得提。PDF 里的表格转 text 的时候传统的 pdfplumber 对简单表格效果尚可但遇到跨页表格经常丢行。我在这个项目里用了“表格识别 版面分析”的组合先识别出表格边界再用规则修正把同一行但被分页拆开的记录合并回来。故障码表这块一定要做“规范化”处理比如“P0301”有人写作“P0301”有人写作“p0301”还有人写成“P0301 cylinder 1 misfire”如果不做统一格式化后面检索的时候会因为字符大小写不一致导致召回失败这是我实际踩过的坑。实体关系设计也是不能跳过的一步。简单把知识库看成“文本 向量”是很多人忽略的盲区。我设计了三个核心实体一个是车型包含品牌、车系、年款、发动机型号一个是故障码包含代号、系统分类、触发条件、影响表现一个是维修案例包含问题描述、诊断过程、解决方案、备件清单。它们之间的关系是一个车型可以关联多个故障码一个故障码对应多个历史维修案例一个维修案例又反过来可以修正车型的常见故障结论。这套关系在工单闭环里非常重要后面会详细讲。2.2 向量化流程切分策略、Embedding 选型与索引入库向量化是整个 RAG 链路里最影响效果也最容易被低估的环节。切分策略我调了大概两周最终的结论是不要用一个固定的 chunk_size 走天下。汽修手册里的“一句诊断结论”和“一段维修步骤”对粒度的要求完全不一样。我采用的策略是“结构化段落切分 语义边界检测”优先按标题层级切再按表格行切最后按句子长度合并。比如故障码表每个故障码单独成一个 chunk但该故障码下的“触发条件”“维修建议”等字段要拼接在一起维修手册则按“章 - 节 - 小节”逐级切如果某个小节内容过长超过 500 字再按语义完整句子的边界做二次切分。Embedding 选型也值得说说。我对比过 BGE-large-zh、m3e-base、text2vec-large-chinese以及几个闭源模型的 API。结论是如果想省事且中文效果均衡BGE-large-zh 是很稳的选择而且这个模型对“短文本 长文本”的匹配做了专门优化检索效果比 m3e 高出一截。不过要注意的是BGE 系列模型在计算相似度时需要加上指令前缀比如“为这个句子生成表示以用于检索相关文章”不加前缀和加了前缀的召回效果差距挺明显这个细节在官方文档里写得不醒目很多人会漏掉。入库流程我用了一个数据管道脚本分三步走第一步读源数据按上面说的切分策略生成结构化段落第二步调用 embedding 服务把段落文本转换成 768 维向量第三步连接 Milvus创建 Collection 并且批量写入向量和原文元数据。这里有一个性能优化点批量写入时不要一条一条 insert而是攒够 100~200 条之后走批量接口实测吞吐量能提升五六倍。2.3 混合检索策略为什么不能只靠向量相似度这个项目做到一半的时候我发现纯向量检索有一个致命问题故障码“P0301”这种短代码经过 BGE 向量化之后相似度计算结果往往不如语义完整的长文本好。也就是说用户搜索“P0301”有时候返回的前几个结果是包含“P0301”的长段落还好如果知识库里只有单独的“P0301”故障码条目向量相似度反而排很后。这就是稠密向量检索在精确短码匹配上的天然短板。所以最终采用的是“BM25 稀疏检索 向量稠密检索”的混合检索策略。BM25 负责处理精确词匹配故障码、车型名、配件编号向量检索负责处理语义表达比如“起步加油顿挫感很明显”和“加速时车一冲一冲”这段话的语义相似。在 Milvus 里实现混合检索早期版本是分两次查询再在应用层做结果融合后来我直接用 Milvus 的 HybridSearch 接口一次性把两类检索结果做 rank fusion。融合权重初步设成 BM25:向量 3:7实测下来故障码精确匹配场景的准确率提升非常明显而语义泛化场景也没有明显回退。还有一种情况值得注意当用户输入包含“怎么修”“什么原因”这类问题词的时候BM25 和向量检索的结果往往都不理想因为知识库里的原文是陈述句式不是疑问句。我专门加了一层查询改写逻辑在 FastAPI 里把用户的疑问句先转成陈述短语比如“怠速抖动怎么处理”改写成“怠速抖动 处理方案”再送进检索模块召回效果会好很多。这个改写可以先用自己的知识库跑一遍看哪些句式转换收益最明显。3. Milvus 向量数据库安装、配置与生产环境调优3.1 Milvus 安装方式对比Docker Compose、Helm 与 Windows 本地部署Milvus 的部署方式网上一搜一大把但真正决定你选哪种方式的是你的使用阶段和环境。我第一次搭环境的时候图省事直接在一台 8G 内存的开发机上用了 Docker Compose 方案一键装完 Milvus 2.3.3花了大概十分钟。后来发现这方案虽然省心但 etcd 和 MinIO 这两个依赖组件会把内存吃掉不少开发机一跑起来风扇呼呼转所以后来我专门把高负载环境分开了。如果是本地 Windows 开发不想装 Docker 的话Milvus 官方其实提供了不带 Docker 的编译运行方式也就是从源码构建或者下载预编译的二进制包然后手动启动 etcd、MinIO 和 Milvus 三个进程。这个方案最大的坑是版本匹配Milvus 2.x 会对 etcd 版本有严格要求etcd 版本连错会出现大量连接超时。我踩过之后强烈建议 Windows 本地调试就用 Milvus Lite也就是嵌入式版本它不需要单独装 etcd一个进程就能跑起来做开发验证等上生产再切到分布式部署更合理。不少人在新手阶段就想直接上分布式集群结果反而被依赖组件的问题劝退完全没必要。生产环境我后来用的是 Kubernetes Helm 安装不是因为分布式一定比单机强而是因为运维升级方便。Milvus 的 etcd、MinIO 和 queryservice、datacoord 等组件非常多单纯用 Docker Compose 管理发布版本很麻烦。Helm 方式的好处是可以通过 values.yaml 统一配置资源配额和副本数升级 Milvus 版本时也能一键替换镜像。不过这里有个经验之谈上线初期数据量没超过百万向量之前单机部署完全够用分布式带来的收益很有限反而增加排查问题的心智负担。3.2 Milvus 集合设计、索引参数与数据写入性能优化Milvus 里的 Collection 相当于关系型数据库的表设计得好不好直接影响查询性能和召回质量。我设计的“故障知识”集合包含以下字段id主键、text原文段落、embedding768 维向量、source_type来源类型手册/故障码/工单/笔记、car_model车型、fault_code故障码、create_time。其中 car_model 和 fault_code 这两个字段我加了标量索引因为业务上经常需要按车型过滤再检索比如用户问“卡罗拉顿挫怎么解决”如果不带车型过滤系统可能返回一堆凯美瑞的案例相关性会被稀释。索引类型的选择上对于一个大约十万行的知识库用的是一致性要求不怎么高的业务场景所以我选了 HNSW 索引参数设置是 M16、efConstruction128查询时 ef64。这个参数组合是我做过几组对比测出来的。M 太小召回率会下降M 太大索引体积膨胀且构建时间变长efConstruction 控制在 128 够用了再往上提对准确率的提升非常有限但构建时间翻倍查询时的 ef 我建议动态调整召回率不够时调大延迟敏感时调小不要一把尺子量到底。数据写入性能这个坑也值得单独拿出来讲。第一次全量灌知识库的时候我用的是单线程循环插入几万条数据写了大半天一度以为系统坏了。后来改成批量插入每次 200 条数据用 Milvus 的 Client 接口 write总耗时降到了几分钟以内。另一个很关键的操作是批量写入之前先创建索引写入完成后再创建索引。很多人不知道的是如果你边写边建索引Milvus 后台会不断对增量数据做建索引操作磁盘 IO 和 CPU 都被耗掉了数据导入速度会慢很多。建议先把索引字段设为空数据全部写完后再一次性 Build Index效率差距是数量级的。3.3 向量数据双写与运维监控的附加建议做生产级系统还要考虑一个问题Milvus 里的数据不能是唯一副本因为 Milvus 本身是主推“以对象存储为底座”的架构一旦对象存储MinIO 或 S3出了问题向量数据很难快速恢复。我的做法是旁路把每一段文本的原文和向量存一份到 PostgreSQL 或者本地 JSON 文件里并记录一个全局文档 IDMilvus 里的主键用同一套 ID。这样就算 Milvus 崩了要重建 collection也能直接从备份数据里逐条恢复而不是重新做一遍 embedding成本低很多。运维端还有一个细节值得提醒Milvus 的日志默认级别比较verbose生产环境建议把日志级别调为 WARN不然一天能产生好几个 GB 的日志。后来我特意关注了官方社区里关于“Milvus 2.4 版本 Attu 支持情况”的话题。在我用的 Milvus 2.4.x 上Attu 的兼容版本需要选 2.4 对应的 Release否则连接时会报版本不匹配的错这个也顺手记录一下避免大家重走弯路。4. FastAPI 服务层实现与工单闭环业务逻辑4.1 FastAPI 项目结构与依赖生命周期管理FastAPI 在这一整套系统里扮演的是“胶水层”的角色对外暴露 HTTP 接口对内协调 Milvus 检索、大模型调用和工单数据库的读写。项目目录结构我按照业务模块做了拆分避免把所有逻辑堆在一个 main.py 里面。app/ ├─ main.py # FastAPI 实例、路由注册 ├─ config.py # 配置读取 ├─ models/ # Pydantic 请求/响应模型 ├─ routers/ │ ├─ chat.py # 问答接口 │ ├─ ticket.py # 工单接口 │ └─ feedback.py # 工单回写与知识沉淀接口 ├─ services/ │ ├─ rag_service.py # 检索 生成主流程 │ ├─ milvus_client.py # Milvus 操作封装 │ ├─ llm_client.py # 大模型调用封装 │ ├─ ticket_service.py # 工单状态流转 │ └─ indexer.py # 知识库增量导入 └─ utils/ ├─ logger.py └─ text_process.py启动时的依赖绑定我用了 FastAPI 的 lifespan 机制在启动事件里初始化 Milvus 连接、Embedding 模型和大模型客户端关闭时统一释放资源。需要特别提醒的是现在 FastAPI 主推 lifespan 写法替代过时的 startup/shutdown 装饰器如果你在别人的项目里看到 on_event 写法那是旧版本风格虽然还能用但官方已经不推荐了。4.2 问答接口的完整调用链参数设计、检索重排与流式输出问答接口是系统的门面核心调用链是这样的接收用户问题 - 意图识别 - 查询改写 - 混合检索 - 重排序 - 拼装 Prompt - 大模型生成 - 返回答案和引用来源。每一步都有可以优化的点我这里挑几个最关键的说。重排序环节我一开始没做直接拿检索结果让大模型回答结果发现经常被不相关的车载电子类文档干扰。后来加了一个 Cross-Encoder 做重排序把检索返回的前 20 条精排成前 5 条效果提升非常明显。推荐用 bge-reranker-large中文场景效果好且耗时可控。重排之后的 top5 再拼装 Prompt整个响应的质量和上限就完全不一样了。参数设计上大模型生成可以暴露 temperature、max_tokens 参数但默认值千万不要调太高。汽修领域追求的是“准确”而不是“创意”temperature 在 0.1~0.3 之间比较合适。Prompt 模板里强调两件事第一只根据给定的知识库内容回答知识库没有的内容要明确说“知识库暂未收录”第二如果问题涉及故障码必须输出对应故障码以及可能的故障原因排序。这样回答质量的稳定性会高很多。接口支持流式输出SSE这件事我认为应该是标配。用户填了一段故障描述如果等大模型把全部内容生成完再一次性返回等待时间往往超过 15 秒体验很糟糕。用 FastAPI 的 StreamingResponse 结合大模型 SDK 的流式接口把文本一段一段推给前端用户第一句话通常在 1~2 秒内就能看到体感好非常多。前端实现的时候记得用 fetch 流的模式读接口而不是普通的 axios.get不然 SSE 会被吞掉。问答响应的数据结构我也设计成了带引用来源的格式用户回答下面附带“参考依据”列表展示匹配到的故障码或维修案例编号。这个设计不仅让回答具有可解释性也为后面工单闭环提供了入口。4.3 工单闭环设计从诊断建议到知识沉淀的完整路径工单闭环是系统的点睛之笔也是汽修领域和其他通用 RAG 项目最不一样的地方。当用户提交的故障描述被判定为“报修类”后FastAPI 会把 RAG 生成的初步诊断建议和用户提交的信息组装成一张工单工单状态流设计为待处理 - 诊断中 - 维修中 - 已完成 - 已归档。每个状态变化都触发事件供后续数据回流使用。工单表的核心字段包括ticket_id、用户描述、初判故障码、RAG生成、技师确认故障码、处理结果、更换配件、耗时、费用、满意度评分。这里特别设计了“初判故障码”和“确认故障码”两个字段分开存储因为 RAG 生成的判断并不一定正确技师维修时的确认才是真实答案。这个设计很关键稍后知识库回流时我们只拿“确认故障码”作为学习信号而不是拿初判结果去污染知识库。工单闭环最关键的一步是“获取维修结果并回流知识库”。实现上我用定时任务扫描状态为“已完成”的工单调用工单回写接口把“用户描述 技师诊断 处理结果 更换配件”组装成一条新的文本记录做向量化之后写入 Milvus同时在工单记录里标记“已入库”。这样一来每一次真实维修都在给知识库“施肥”系统越用越精准。还有一个细节回流的数据要在 source_type 字段里标记为“工单案例”并关联车型和确认故障码这样后续检索时可以进行来源过滤优先展示用户真实维修案例而非手册原文。5. 效果调优、评测方法与实践中的常见问题排查5.1 RAG 效果评测不能光看“感觉”要建立指标体系很多做 RAG 项目的朋友都深受“感觉效果还行”之苦因为没有数据支撑调整了 Prompt 也不知道是好是坏。我在这个项目里建了一套轻量级的评测流程主要跟踪三个指标召回率、命中率和最终回答准确率。召回率定义为对于一组测试问题检索结果中是否包含能回答该问题的参考文档命中率定义为重排序之后 top5 中是否包含正确答案回答准确率则靠人工抽样标注或者 LLM-as-Judge 来做。具体操作上我会从知识库里人工挑 30~50 条“问答对”每条对应一段标准答案文本然后跑一遍完整的“问题 - 检索 - 重排 - 生成”链路自动统计召回率和命中率回答准确率抽 10~20 条人工打分。这套评测最初跑下来召回率只有 61%后来把混合检索权重从纯向量改成 BM25:向量 3:7召回率升到了 78%再加查询改写之后到了 84% 左右。这些数字看起来枯燥但调参时如果没有它们你根本不知道改动是正向还是负向。5.2 Milvus 连接篇attu、版本兼容和 etcd 依赖问题排查Milvus 使用过程中经常会在连接环节卡住。我最常遇到的是 AttuMilvus 的可视化管理工具连不上本地 Milvus。这个问题 90% 是版本不匹配比如 Attu 2.3 版本尝试连 Milvus 2.4.x或者反过来都会出现握手失败或者连接中断。我的建议是先确认你的 Milvus 版本再到 Attu 的 GitHub Release 页面选对应版本号的安装包不要随手下载 latestlatest 经常会和你的 Milvus 版本错位。另一个高频问题是 etcd 连接失败。Milvus 写入数据时依赖 etcd 存储元数据开发机上如果 etcd 版本和 Milvus 要求的版本不匹配最常见的报错是“etcdserver: request timed out”或“connection refused”。排查思路是第一检查 etcd 进程是否存活第二检查 etcd 端口默认 2379是否能连通第三检查 etcd 版本Milvus 2.3.x 通常对应 etcd 3.5.x第四检查 ETCD 的日志有没有大量 leader 选举失败。实测发现大部分情况就是 etcd 数据目录里的数据损坏或权限不对清理掉 etcd 数据目录重新启动就能恢复但这种操作会丢失元数据所以建议统一重建 collection别想着恢复部分数据。5.3 FastAPI 接口层容易踩的坑超时、并发和上下文管理FastAPI 层最常见的问题有三个。第一个是异步客户端的超时设置如果调用大模型或 Milvus 时不显式设置 timeout默认等待时间可能非常长大模型 SDK 默认可能 60 秒甚至更久前端体验就很差。建议在调用外部服务时统一设置 read_timeout30、write_timeout30RAG 全链路的时间预算控制在 2~5 秒检索 5~15 秒生成。第二个坑是 Milvus 连接的并发安全。Milvus 客户端不是绝对意义上的无状态高并发下如果共享同一个连接对象偶尔会出现“connection closed”之类的异常。我用的是每个请求从连接池获取一个独立连接或者至少给 Milvus 操作加一个实例锁/异步锁能很大程度避免并发问题。第三个坑是 FastAPI 里的“同步耗时操作”。如果你直接在 async 函数里调用 Milvus 的同步接口或者 is 阻塞型的 embedding 模型推理那么整个事件循环会被卡住并发一高系统直接假死。正确的做法是把耗时的同步操作放到线程池里执行用 run_in_executor或者直接使用 httpx.AsyncClient / Milvus 的异步接口来保持全链路异步。我自己经验上Embedding 模型的推理放线程池就够了大模型调用用异步 HTTP 客户端Milvus 查询用异步或线程池皆可关键是不能让同步阻塞进入事件循环。5.4 RAG 调优经验查询改写、上下文窗口和 Prompt 细节最后分享几个反复调试之后最有价值的经验。查询改写不要只做“疑问句转陈述句”。很多用户在汽修场景的提问是“我的车昨天开着开着突然抖动排气管还冒黑烟”这种描述天然适合检索但问题里既有现象又有时间信息时间信息其实是噪音。我的处理是把问题分词后提取“现象词 部件词 故障码”组装成多个候选查询分别去检索最后汇总结果再重排。这样做虽然会增加一点检索耗时但能明显提升召回效果。上下文窗口的利用上不建议把知识库所有检索到的内容都塞进 Prompt。我给先生的答案是 top5但如果某一条结果本身超过 800 字会被截断或者整条丢弃因为大模型注意力集中在前中段太长的上下文会稀释关键信息。汽修知识的回答通常讲究“先说结论、再列依据”所以我会在 Prompt 模板里要求模型先输出“可能故障码 原因排序”再展开解释这样回答结构清晰且准确率高。Prompt 细节上还有一条挺重要的在系统 Prompt 里明确“你是汽修领域资深技师回答需要严谨不确定时不要猜测”。这句话看起来简单但确实能明显减少大模型一本正经胡说八道的情况。尤其是故障码判读这类场景模型很容易编一个不存在的故障码加了严谨约束之后模型的“承认不知道”比例高了很多这在工程上实际是加分项而不是减分项。6. 个人实操总结与后续扩展方向写到这我把自己在原项目里最深刻的两点体会分享出来。第一点RAG 项目里 60% 的精力应该花在知识库的质量上而不是大模型的选型上。Milvus 和 FastAPI 都只是管道管道再顺知识库里的原文是垃圾检索出来的还是垃圾。为了提升知识库质量我花了大量时间清洗维修手册、规范故障码、去重历史工单最终效果远超换一个大模型带来的提升。第二点工单闭环不是功能而是一种数据飞轮的思维每个业务系统里产生的真实数据都应该被设计成能自动回流到知识体系里系统价值才会随着使用时间增长而增值。后续想扩展的方向有两个。一是接入 GraphRAG 做故障关联分析汽修场景里一个故障往往涉及多个系统发动机、变速箱、电控现在向量检索只能找到相似的文本但没法展示“P0301 可能由点火线圈、火花塞、喷油嘴等多个组件导致”这种多层依赖关系GraphRAG 在表达这种实体关联上有天然优势。二是结合 OCR 技术识别维修工单里的手写内容把非结构化的纸质单据也纳入知识库让闭环飞轮转得更完整。这些方向目前还在原型验证阶段后续有结论了再来和大家分享。