012、工程化篇构建可维护、可扩展的RAG系统架构与流水线从一次深夜告警说起线上RAG服务的响应延迟从平均200ms飙到了5秒以上错误率突破30%。打开监控面板一看向量数据库的CPU被打满检索请求全部超时。紧急扩容后暂时稳住但根本问题没解决为什么突然流量激增为什么向量库这么脆弱排查发现问题出在检索前的query改写模块。某个用户输入了一段长达500字的“问题描述”改写模型输出了一段更长的“优化查询”直接触发了向量库的慢查询。更糟糕的是这个长查询被缓存了后续类似请求全部命中这个“毒缓存”。这次事故暴露了我们早期架构的典型问题各组件硬耦合没有熔断降级监控埋点不足。今天我们就聊聊怎么把RAG从实验室原型变成能在线上扛流量的工程系统。核心架构拆解与隔离RAG系统天生是流水线作业但很多团队把它写成一个干行代码的Python脚本。一旦要加新功能比如query分析、重排序、多路召回代码就变成一坨意大利面。我的建议是按数据流划分阶段每个阶段独立部署。典型流水线可以拆成这样用户请求 → 路由层 → 查询处理 → 检索 → 后处理 → 生成 → 响应每个箭头都是一个接口最好用Protobuf或JSON Schema定义清楚。别小看这个“定义”我们团队曾因为两个组对“score”字段理解不一致一个用0-1一个用0-100导致排序完全错乱。查询处理模块别把脏数据丢给向量库查询改写、关键词提取、意图识别这些预处理步骤最容易出幺蛾子。分享几个实战经验# 坏例子一股脑全塞给模型defprocess_query(query):# 没有长度限制前面的事故就是这么来的rewrittenllm_rewrite(query)returnrewritten# 好例子加护栏defprocess_query_safe(query):# 先剪枝超长query直接截断或拒绝iflen(query)200:querysmart_truncate(query)# 按句子截断别切在单词中间# 敏感词过滤我们真遇到过用户输入恶意promptifcontains_sensitive(query):returndefault_query# 缓存层相同query直接返回减轻LLM负担cache_keyhash(query)ifcache_keyinlocal_cache:returnlocal_cache[cache_key]# 降级策略模型服务超时返回原querytry:rewrittenllm_rewrite_with_timeout(query,timeout0.5)exceptTimeout:rewrittenquery# 保底# 后处理确保输出格式合法cleanedremove_special_chars(rewritten)local_cache[cache_key]cleanedreturncleaned向量数据库很贵别让它处理垃圾查询。我们曾统计过经过清洗后检索准确率提升不明显但P99延迟下降了40%因为避免了那些“奇奇怪怪”的查询拖慢整个集群。检索层多路召回与融合只靠向量检索等着被业务方吐槽吧。关键词匹配、业务规则过滤、热点缓存这些传统手段依然有效。classRetriever:def__init__(self):# 多路召回器并行跑self.retrievers[VectorRetriever(top_k20),KeywordRetriever(top_k10),RuleBasedRetriever(rules业务规则),]asyncdefretrieve(self,query):# 异步并发谁先回来用谁的tasks[ret.retrieve(query)forretinself.retrievers]resultsawaitgather_with_timeout(tasks,timeout1.0)# 融合策略简单加权平均起步fusedself.fuse_results(results)# 重排序用轻量级模型再排一次rerankedself.rerank(query,fused[:50])returnreranked[:10]# 最终返回top10这里踩过坑早期我们让各路召回器返回top100再融合结果内存爆了。后来改成top20融合后再用重排序模型精排效果差不多但内存只有原来的三分之一。生成层模板与fallbackLLM生成不可控那是你没加约束。我们现在的做法defgenerate_answer(query,contexts):# 必填上下文检查ifnotcontexts:return抱歉暂时没有找到相关资料。# 系统提示词模板化promptbuild_prompt(template_namerag_qa,queryquery,contextscontexts,max_length500# 明确限制生成长度)# 调用LLM带重试foriinrange(3):try:responsellm_completion(prompt)# 后处理提取有效部分去掉“根据资料”“综上所述”等废话cleanedextract_answer(response)# 格式验证是否包含乱码、特殊字符ifis_valid_answer(cleaned):returncleanedexceptExceptionase:log_warning(f第{i1}次生成失败:{e})continue# 三次都失败返回兜底答案returnget_fallback_answer(query)生成模块最怕LLM服务不稳定。我们设置了本地缓存对相同querycontexts的请求缓存生成结果命中率大概15%大大减轻下游压力。可观测性埋点要舍得花功夫RAG系统黑盒那是埋点没到位。必须监控的几个黄金指标各阶段耗时查询处理、检索、生成各花多少时间检索质量召回率、精确率需要人工标注样本定期评估生成质量幻觉率、相关度同样需要采样评估资源用量向量库连接数、GPU显存、缓存命中率我们给每个请求分配唯一trace_id流水线每个阶段都打点。有一次发现生成阶段P99延迟很高但平均正常。一查原来某些特定query会触发LLM的“长篇大论模式”生成超长回答。后来在prompt里加了长度限制问题解决。配置化与热更新改个prompt模板就要重新部署太原始了。我们把所有可调参数抽到配置中心# config.yamlretriever:vector_top_k:20keyword_top_k:10fusion_weights:vector:0.7keyword:0.3generator:prompt_template:rag_qa_v2max_tokens:500temperature:0.1fallback_enabled:true系统启动时加载配置运行时监听变更。业务方想调整关键词权重自己改配置不需要我们介入。个人经验从坑里爬出来的建议向量数据库不是银弹它擅长语义匹配但过滤、排序、聚合能力弱。混合检索向量全文规则才是王道。缓存要分层内存缓存高频query、Redis缓存中间结果、磁盘缓存兜底数据。我们甚至缓存了“空结果”避免对无答案问题反复检索。超时设置要激进检索服务超时设500ms生成服务设2s。超时立刻降级别让用户干等。快速失败比慢速成功体验好。版本化一切模型版本、prompt模板版本、检索算法版本。线上问题回追查全靠版本标签。压测要做全链路单独压向量数据库没意义要模拟真实query分布全链路压测。我们曾发现瓶颈在query改写模型而不是向量检索。留个后门紧急情况下能切换降级模式比如关掉向量检索只用关键词或者返回预置问答对。有次机房网络故障我们就是靠降级模式扛了半小时。RAG工程化本质是把不确定性组件LLM、向量检索包装成确定性服务。别追求完美效果先追求稳定可用。效果可以慢慢调但系统要是隔三差五挂业务方可不会给你留情面。