012、工程化篇:构建可维护、可扩展的RAG系统架构与流水线


从一次深夜告警说起

线上RAG服务的响应延迟从平均200ms飙到了5秒以上,错误率突破30%。打开监控面板一看,向量数据库的CPU被打满,检索请求全部超时。紧急扩容后暂时稳住,但根本问题没解决:为什么突然流量激增?为什么向量库这么脆弱?

排查发现,问题出在检索前的query改写模块。某个用户输入了一段长达500字的“问题描述”,改写模型输出了一段更长的“优化查询”,直接触发了向量库的慢查询。更糟糕的是,这个长查询被缓存了,后续类似请求全部命中这个“毒缓存”。

这次事故暴露了我们早期架构的典型问题:各组件硬耦合,没有熔断降级,监控埋点不足。今天我们就聊聊,怎么把RAG从实验室原型变成能在线上扛流量的工程系统。


核心架构:拆解与隔离

RAG系统天生是流水线作业,但很多团队把它写成一个干行代码的Python脚本。一旦要加新功能,比如query分析、重排序、多路召回,代码就变成一坨意大利面。

我的建议是:按数据流划分阶段,每个阶段独立部署。典型流水线可以拆成这样:

用户请求 → 路由层 → 查询处理 → 检索 → 后处理 → 生成 → 响应

每个箭头都是一个接口,最好用Protobuf或JSON Schema定义清楚。别小看这个“定义”,我们团队曾因为两个组对“score”字段理解不一致(一个用0-1,一个用0-100),导致排序完全错乱。


查询处理模块:别把脏数据丢给向量库

查询改写、关键词提取、意图识别,这些预处理步骤最容易出幺蛾子。分享几个实战经验:

# 坏例子:一股脑全塞给模型
def process_query(query):
    # 没有长度限制,前面的事故就是这么来的
    rewritten = llm_rewrite(query)  
    return rewritten

# 好例子:加护栏
def process_query_safe(query):
    # 先剪枝,超长query直接截断或拒绝
    if len(query) > 200:
        query = smart_truncate(query)  # 按句子截断,别切在单词中间
    
    # 敏感词过滤(我们真遇到过用户输入恶意prompt)
    if contains_sensitive(query):
        return default_query
    
    # 缓存层:相同query直接返回,减轻LLM负担
    cache_key = hash(query)
    if cache_key in local_cache:
        return local_cache[cache_key]
    
    # 降级策略:模型服务超时,返回原query
    try:
        rewritten = llm_rewrite_with_timeout(query, timeout=0.5)
    except Timeout:
        rewritten = query  # 保底
        
    # 后处理:确保输出格式合法
    cleaned = remove_special_chars(rewritten)
    local_cache[cache_key] = cleaned
    return cleaned

向量数据库很贵,别让它处理垃圾查询。我们曾统计过,经过清洗后,检索准确率提升不明显,但P99延迟下降了40%,因为避免了那些“奇奇怪怪”的查询拖慢整个集群。


检索层:多路召回与融合

只靠向量检索?等着被业务方吐槽吧。关键词匹配、业务规则过滤、热点缓存,这些传统手段依然有效。

class Retriever:
    def __init__(self):
        # 多路召回器并行跑
        self.retrievers = [
            VectorRetriever(top_k=20),
            KeywordRetriever(top_k=10),
            RuleBasedRetriever(rules=业务规则),
        ]
    
    async def retrieve(self, query):
        # 异步并发,谁先回来用谁的
        tasks = [ret.retrieve(query) for ret in self.retrievers]
        results = await gather_with_timeout(tasks, timeout=1.0)
        
        # 融合策略:简单加权平均起步
        fused = self.fuse_results(results)
        
        # 重排序:用轻量级模型再排一次
        reranked = self.rerank(query, fused[:50])
        
        return reranked[:10]  # 最终返回top10

这里踩过坑:早期我们让各路召回器返回top100再融合,结果内存爆了。后来改成top20,融合后再用重排序模型精排,效果差不多,但内存只有原来的三分之一。


生成层:模板与fallback

LLM生成不可控?那是你没加约束。我们现在的做法:

def generate_answer(query, contexts):
    # 必填上下文检查
    if not contexts:
        return "抱歉,暂时没有找到相关资料。"
    
    # 系统提示词模板化
    prompt = build_prompt(
        template_name="rag_qa",
        query=query,
        contexts=contexts,
        max_length=500  # 明确限制生成长度
    )
    
    # 调用LLM带重试
    for i in range(3):
        try:
            response = llm_completion(prompt)
            # 后处理:提取有效部分,去掉“根据资料”“综上所述”等废话
            cleaned = extract_answer(response)
            # 格式验证:是否包含乱码、特殊字符
            if is_valid_answer(cleaned):
                return cleaned
        except Exception as e:
            log_warning(f"第{i+1}次生成失败: {e}")
            continue
    
    # 三次都失败,返回兜底答案
    return get_fallback_answer(query)

生成模块最怕LLM服务不稳定。我们设置了本地缓存,对相同query+contexts的请求,缓存生成结果,命中率大概15%,大大减轻下游压力。


可观测性:埋点要舍得花功夫

RAG系统黑盒?那是埋点没到位。必须监控的几个黄金指标:

  1. 各阶段耗时:查询处理、检索、生成各花多少时间
  2. 检索质量:召回率、精确率(需要人工标注样本定期评估)
  3. 生成质量:幻觉率、相关度(同样需要采样评估)
  4. 资源用量:向量库连接数、GPU显存、缓存命中率

我们给每个请求分配唯一trace_id,流水线每个阶段都打点。有一次发现生成阶段P99延迟很高,但平均正常。一查,原来某些特定query会触发LLM的“长篇大论模式”,生成超长回答。后来在prompt里加了长度限制,问题解决。


配置化与热更新

改个prompt模板就要重新部署?太原始了。我们把所有可调参数抽到配置中心:

# config.yaml
retriever:
  vector_top_k: 20
  keyword_top_k: 10
  fusion_weights:
    vector: 0.7
    keyword: 0.3

generator:
  prompt_template: "rag_qa_v2"
  max_tokens: 500
  temperature: 0.1
  fallback_enabled: true

系统启动时加载配置,运行时监听变更。业务方想调整关键词权重,自己改配置,不需要我们介入。


个人经验:从坑里爬出来的建议

  1. 向量数据库不是银弹:它擅长语义匹配,但过滤、排序、聚合能力弱。混合检索(向量+全文+规则)才是王道。

  2. 缓存要分层:内存缓存(高频query)、Redis缓存(中间结果)、磁盘缓存(兜底数据)。我们甚至缓存了“空结果”,避免对无答案问题反复检索。

  3. 超时设置要激进:检索服务超时设500ms,生成服务设2s。超时立刻降级,别让用户干等。快速失败比慢速成功体验好。

  4. 版本化一切:模型版本、prompt模板版本、检索算法版本。线上问题回追查,全靠版本标签。

  5. 压测要做全链路:单独压向量数据库没意义,要模拟真实query分布全链路压测。我们曾发现瓶颈在query改写模型,而不是向量检索。

  6. 留个后门:紧急情况下能切换降级模式,比如关掉向量检索只用关键词,或者返回预置问答对。有次机房网络故障,我们就是靠降级模式扛了半小时。


RAG工程化,本质是把不确定性组件(LLM、向量检索)包装成确定性服务。别追求完美效果,先追求稳定可用。效果可以慢慢调,但系统要是隔三差五挂,业务方可不会给你留情面。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐