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

376

被折叠的 条评论
为什么被折叠?



