RAG 接入存量应用:影子双跑、比例灰度与自动回退

RAG 接入存量应用:影子双跑、比例灰度与自动回退

RAG 接入旧系统时,最重要的不是尽快切换,而是让新旧结果可比、异常能退。本文按影子双跑、灰度和回退梳理迁移路径;比例与阈值需要用实际数据确认。

存量系统迁移的四阶段演进模型

直接硬切会把检索差异、延迟和数据兼容问题同时暴露出来。更稳妥的做法是渐进迁移,并为每一步保留旧路径。

graph TD
    UserReq[用户请求入口] --> RouterProxy{智能路由代理}
    RouterProxy -- 阶段一: 影子流量 (0%) --> LegacySystem[存量检索系统]
    RouterProxy -- 异步复制请求 --> ShadowChecker[影子校验器]
    ShadowChecker --> NewRAG[新 RAG API 架构]
    ShadowChecker -- 差异对比/性能埋点 --> LogStorage[(校验日志库)]
    
    RouterProxy -- 阶段二: 小流量灰度 (10%-30%) --> WeightSplitter{按权重/用户标分流}
    WeightSplitter -- 主流量 --> LegacySystem
    WeightSplitter -- 灰度流量 --> NewRAG
    NewRAG -- 异常/超时触发 --> FallbackBreaker[熔断降级网关]
    FallbackBreaker -- 降级回退 --> LegacySystem
    
    WeightSplitter -- 阶段三&四: 全量上线 (100%) --> NewRAG

阶段一:影子流量与结果双重校验(Shadow Traffic & Dual-run)

线上请求依然由旧系统百分之百响应。路由代理在后台异步复制一份请求体发往新的 RAG 编排链,对比两者的召回准确度、时延差距与 Token 消耗,同时积累向量数据库的缓存热度。

阶段二:带自动回退的比例灰度(Canary with Circuit Breaker)

根据 User ID Hash 或自定义 Header 划分 5% - 20% 的真实流量至 RAG 系统。一旦新系统连续报出 HTTP 429(超频限制)或响应耗时超过 2.5 秒,熔断网关自动把该请求瞬间回退至旧系统处理。

阶段三:主次倒置与全量平滑过载

RAG 架构承载 90% 以上主流量,旧系统转为底线保底服务(Fallback Layer),直到确定向量库召回与模型吞吐完全符合预期。

流量代理与双跑对比中间件实现

下面使用 Python 和 asyncio 实现一个生产级的双跑校验与灰度切流代理。代码内建异步并发、超时控制、语义差异记录与自动熔断回退机制。

import asyncio
import hashlib
import json
import logging
import time
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple

logging.basicConfig(level=logging.INFO, format="%(asctime)s - [%(levelname)s] - %(message)s")
logger = logging.getLogger("RAGMigrationProxy")


@dataclass
class MigrationConfig:
    gray_ratio: float = 0.1  # 灰度比例 (0.0 到 1.0)
    timeout_seconds: float = 2.0  # RAG 允许的最大超时耗时
    circuit_breaker_threshold: int = 5  # 连续异常触发熔断阈值
    shadow_mode: bool = True  # 是否开启影子流量模式


class LegacyEngineService:
    """模拟存量传统检索系统"""
    async def query(self, prompt: str) -> Dict[str, Any]:
        await asyncio.sleep(0.1)  # 旧系统极快
        return {
            "source": "legacy_keyword_search",
            "content": f"【旧系统匹配结果】针对问题 '{prompt}' 的标准文档回答。",
            "status": "success",
        }


class NewRAGEngineService:
    """模拟新 LLM API + 向量 RAG 编排系统"""
    def __init__(self):
        self.should_fail_mock = False

    async def query(self, prompt: str) -> Dict[str, Any]:
        start_time = time.time()
        if self.should_fail_mock:
            await asyncio.sleep(0.5)
            raise RuntimeError("Upstream LLM Rate Limit Exceeded (HTTP 429)")

        # 模拟 RAG 向量检索与流式拼接
        await asyncio.sleep(0.4)
        duration = time.time() - start_time
        return {
            "source": "new_rag_pipeline",
            "content": f"【RAG 生成回复】基于知识库检索,针对 '{prompt}' 给出温情详尽的解答。",
            "latency_ms": round(duration * 1000, 2),
            "status": "success",
        }


class TrafficMigrationGateway:
    def __init__(self, config: MigrationConfig, legacy_sys: LegacyEngineService, rag_sys: NewRAGEngineService):
        self.config = config
        self.legacy_sys = legacy_sys
        self.rag_sys = rag_sys
        self.consecutive_failures = 0
        self.is_circuit_open = False

    def _should_route_to_rag(self, user_id: str) -> bool:
        """根据 user_id 进行 Hash 分流,保证同一用户的体验一致性"""
        if self.is_circuit_open:
            return False
        hash_val = int(hashlib.md5(user_id.encode("utf-8")).hexdigest(), 16)
        percentile = (hash_val % 100) / 100.0
        return percentile < self.config.gray_ratio

    async def _execute_shadow_check(self, prompt: str, legacy_res: Dict[str, Any]) -> None:
        """后台影子任务:对比新旧系统输出差异,不影响主流程响应时间"""
        try:
            rag_res = await asyncio.wait_for(
                self.rag_sys.query(prompt), timeout=self.config.timeout_seconds
            )
            # 记录差异分析日志
            logger.info(
                f"[影子校验对比] 用户问题: '{prompt}' | 旧系统长度: {len(legacy_res['content'])} | RAG耗时: {rag_res.get('latency_ms')}ms"
            )
        except Exception as err:
            logger.warning(f"[影子校验失败] RAG 系统输出异常: {str(err)}")

    async def dispatch_request(self, user_id: str, prompt: str) -> Dict[str, Any]:
        use_rag = self._should_route_to_rag(user_id)

        # 1. 影子模式或走旧系统
        if not use_rag:
            legacy_result = await self.legacy_sys.query(prompt)
            if self.config.shadow_mode:
                # 异步并发触发影子校验,不 await 阻塞主链路
                asyncio.create_task(self._execute_shadow_check(prompt, legacy_result))
            return legacy_result

        # 2. 灰度模式走新 RAG 架构(带熔断保护与降级回退)
        try:
            logger.info(f"用户 [{user_id}] 命中灰度,路由至 RAG 新系统")
            rag_result = await asyncio.wait_for(
                self.rag_sys.query(prompt), timeout=self.config.timeout_seconds
            )
            # 成功后清空连续失败计数器
            self.consecutive_failures = 0
            return rag_result

        except (asyncio.TimeoutError, Exception) as err:
            self.consecutive_failures += 1
            logger.error(
                f"RAG 系统服务响应失败 (连续失败 {self.consecutive_failures} 次): {str(err)}。触发降级逻辑!"
            )

            if self.consecutive_failures >= self.config.circuit_breaker_threshold:
                self.is_circuit_open = True
                logger.critical("【警告】RAG 系统连续失败达到阈值,熔断器开启,全量退回旧系统!")

            # 无缝自动回退到旧系统保底
            fallback_res = await self.legacy_sys.query(prompt)
            fallback_res["fallback_notice"] = "Returned from legacy engine due to RAG anomaly."
            return fallback_res


# 模拟流转验证
async def main():
    config = MigrationConfig(gray_ratio=0.5, timeout_seconds=1.0, shadow_mode=True)
    legacy = LegacyEngineService()
    rag = NewRAGEngineService()
    gateway = TrafficMigrationGateway(config, legacy, rag)

    print("=== 第一阶段:正常灰度与影子流量测试 ===")
    for i in range(5):
        user_id = f"user_{i*10 + 3}"
        res = await gateway.dispatch_request(user_id, f"如何设置智能家居的温湿度提醒?")
        print(f"请求 {i+1} 响应来源: {res['source']}")

    print("\n=== 第二阶段:模拟 RAG 上游故障触发熔断降级 ===")
    rag.should_fail_mock = True
    for i in range(6):
        user_id = f"user_{i*10 + 3}"  # 使用同样会命中灰度的 user_id
        res = await gateway.dispatch_request(user_id, f"早晨的语音闹钟语音包如何自定义?")
        print(f"故障模拟请求 {i+1} 最终响应来源: {res['source']} (降级标记: {'fallback_notice' in res})")


if __name__ == "__main__":
    asyncio.run(main())

迁移工程落地里的注意细节

在将线上架构逐步替换为大模型 API + RAG 的过程中,有几个坑点需要特别留意:

  1. 向量索引增量构建的原子性:旧数据库发生 UPDATE / DELETE 时,向量数据库中的嵌入向量(Embeddings)必须有对应的同步机制。否则用户删除了个人偏好,RAG 依然从向量库召回旧记忆。
  2. Key 轮询与限流配额控制:大模型厂商的 API Rate Limit 通常针对 Organization 或 IP。在编排中间件中必须实现多 Key 权重轮流与令牌桶(Token Bucket)限流,防止单点 API 暴毙导致灰度失败。
  3. 确定性指标校验:对于涉及数值、日期、计算等确定性任务,切忌全盘交付给大模型自流发挥。采用“规则引擎过滤 + 大模型润色表达”的混合架构,才是从旧流程迁过来的最稳做法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值