面向多平台 AI 搜索引擎的适配层架构设计

面向多平台 AI 搜索引擎的适配层架构设计

豆包、千问、Kimi、文心一言......每家 AI 搜索引擎的 API、返回格式、限流策略各不相同。本文深入讲解如何设计一个统一的多平台适配层架构,让上层业务无需关心平台差异。

一、问题背景

AI GEO 系统需要同时监测多个 AI 搜索平台。如果每个平台直接对接,会面临以下问题:

复制代码
┌─────────────────────────────────────────────┐
│              业务逻辑层                      │
│  if platform == "doubao":                  │
│      call_doubao_api()                      │
│  elif platform == "qianwen":               │
│      call_qianwen_api()      # 完全不同     │
│  elif platform == "kimi":                   │
│      call_kimi_api()         # 又不同       │
│  elif platform == "wenxin":                 │
│      call_wenxin_api()       # 还不同       │
└─────────────────────────────────────────────┘

问题

  • 平台增加时,业务代码必须修改(违反开闭原则)
  • 每个平台的限流、重试、错误处理逻辑重复编写
  • 测试覆盖困难,代码膨胀
  • 平台 API 变更影响面大

二、适配层架构总览

采用 适配器模式 + 策略模式 + 工厂模式 的组合设计:

复制代码
┌──────────────────────────────────────────────────────┐
│                    业务层                             │
│         CollectionService / MonitorService           │
├──────────────────────────────────────────────────────┤
│                  适配层管理器                         │
│   AdapterManager · AdapterFactory · AdapterRegistry │
├──────────┬──────────┬──────────┬──────────┬─────────┤
│  Doubao  │ QianWen  │   Kimi   │  WenXin  │  ...    │
│ Adapter  │ Adapter  │ Adapter  │ Adapter  │         │
├──────────┴──────────┴──────────┴──────────┴─────────┤
│               公共能力层                              │
│  RateLimiter · RetryPolicy · CircuitBreaker · Cache │
├──────────────────────────────────────────────────────┤
│               平台 API 层                             │
│   豆包 SDK · 千问 SDK · Kimi HTTP · 文心 SDK · ...   │
└──────────────────────────────────────────────────────┘

三、核心接口设计

3.1 统一适配器接口

python 复制代码
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any
from enum import Enum

class PlatformType(Enum):
    DOUBAO = "doubao"
    QIANWEN = "qianwen"
    KIMI = "kimi"
    WENXIN = "wenxin"
    DEEPSEEK = "deepseek"
    ZHIPU = "zhipu"

@dataclass
class SearchRequest:
    """统一的搜索请求"""
    keyword: str
    platform: PlatformType
    system_prompt: Optional[str] = None
    temperature: float = 0.7
    max_tokens: int = 2048
    extra_params: Dict[str, Any] = field(default_factory=dict)

@dataclass
class SearchResponse:
    """统一的搜索响应"""
    platform: PlatformType
    keyword: str
    answer_text: str
    citations: List[str] = field(default_factory=list)
    sources: List[Dict] = field(default_factory=list)
    raw_response: Dict[str, Any] = field(default_factory=dict)
    latency_ms: int = 0
    tokens_used: int = 0
    success: bool = True
    error: Optional[str] = None

class AISearchAdapter(ABC):
    """AI 搜索引擎适配器抽象接口"""
    
    @property
    @abstractmethod
    def platform(self) -> PlatformType:
        """平台标识"""
        pass
    
    @property
    @abstractmethod
    def rate_limit(self) -> dict:
        """限流配置: {max_requests, window_seconds}"""
        pass
    
    @abstractmethod
    async def search(self, request: SearchRequest) -> SearchResponse:
        """执行搜索"""
        pass
    
    @abstractmethod
    def parse_response(self, raw: Dict[str, Any], keyword: str) -> SearchResponse:
        """解析平台原始响应为统一格式"""
        pass
    
    @abstractmethod
    def is_brand_mentioned(self, answer: str, brand_names: List[str]) -> bool:
        """检测品牌是否被提及"""
        pass

3.2 豆包适配器实现

python 复制代码
import httpx
import time
from typing import List

class DoubaoAdapter(AISearchAdapter):
    """豆包 AI 搜索适配器"""
    
    def __init__(self, api_key: str, app_id: str):
        self.api_key = api_key
        self.app_id = app_id
        self.base_url = "https://ark.cn-beijing.volces.com/api/v3"
        self.client = httpx.AsyncClient(
            timeout=30.0,
            limits=httpx.Limits(max_connections=50, max_keepalive_connections=20)
        )
    
    @property
    def platform(self) -> PlatformType:
        return PlatformType.DOUBAO
    
    @property
    def rate_limit(self) -> dict:
        # 豆包 API: 60 次/分钟
        return {"max_requests": 60, "window_seconds": 60}
    
    async def search(self, request: SearchRequest) -> SearchResponse:
        start = time.time()
        try:
            payload = {
                "model": "doubao-pro-32k",
                "messages": [
                    {"role": "system", "content": request.system_prompt or "你是一个搜索助手"},
                    {"role": "user", "content": request.keyword}
                ],
                "temperature": request.temperature,
                "max_tokens": request.max_tokens,
            }
            
            response = await self.client.post(
                f"{self.base_url}/chat/completions",
                json=payload,
                headers={
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                }
            )
            response.raise_for_status()
            raw = response.json()
            
            latency = int((time.time() - start) * 1000)
            return self.parse_response(raw, request.keyword, latency)
            
        except httpx.HTTPStatusError as e:
            return SearchResponse(
                platform=self.platform,
                keyword=request.keyword,
                answer_text="",
                success=False,
                error=f"HTTP {e.response.status_code}: {e.response.text}"
            )
        except Exception as e:
            return SearchResponse(
                platform=self.platform,
                keyword=request.keyword,
                answer_text="",
                success=False,
                error=str(e)
            )
    
    def parse_response(self, raw: Dict, keyword: str, latency: int = 0) -> SearchResponse:
        answer = raw.get("choices", [{}])[0].get("message", {}).get("content", "")
        citations = self._extract_citations(raw)
        
        return SearchResponse(
            platform=self.platform,
            keyword=keyword,
            answer_text=answer,
            citations=citations,
            raw_response=raw,
            latency_ms=latency,
            tokens_used=raw.get("usage", {}).get("total_tokens", 0),
            success=True
        )
    
    def _extract_citations(self, raw: Dict) -> List[str]:
        """豆包特有的引用提取逻辑"""
        citations = []
        for choice in raw.get("choices", []):
            msg = choice.get("message", {})
            # 豆包在 annotations 字段返回引用
            for annotation in msg.get("annotations", []):
                if annotation.get("type") == "url_citation":
                    citations.append(annotation["url_citation"]["url"])
        return citations
    
    def is_brand_mentioned(self, answer: str, brand_names: List[str]) -> bool:
        answer_lower = answer.lower()
        return any(name.lower() in answer_lower for name in brand_names)

3.3 千问适配器实现

python 复制代码
class QianwenAdapter(AISearchAdapter):
    """通义千问适配器"""
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://dashscope.aliyuncs.com/api/v1"
        self.client = httpx.AsyncClient(timeout=30.0)
    
    @property
    def platform(self) -> PlatformType:
        return PlatformType.QIANWEN
    
    @property
    def rate_limit(self) -> dict:
        # 千问: 120 次/分钟
        return {"max_requests": 120, "window_seconds": 60}
    
    async def search(self, request: SearchRequest) -> SearchResponse:
        start = time.time()
        try:
            # 千问使用 enable_search 参数启用联网搜索
            payload = {
                "model": "qwen-plus",
                "input": {
                    "messages": [
                        {"role": "system", "content": request.system_prompt or "你是一个搜索助手"},
                        {"role": "user", "content": request.keyword}
                    ]
                },
                "parameters": {
                    "temperature": request.temperature,
                    "max_tokens": request.max_tokens,
                    "enable_search": True,  # 启用联网搜索
                    "search_options": {"enable_citation": True}
                }
            }
            
            response = await self.client.post(
                f"{self.base_url}/services/aigc/text-generation/generation",
                json=payload,
                headers={
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                }
            )
            response.raise_for_status()
            raw = response.json()
            
            latency = int((time.time() - start) * 1000)
            return self.parse_response(raw, request.keyword, latency)
            
        except Exception as e:
            return SearchResponse(
                platform=self.platform,
                keyword=request.keyword,
                answer_text="",
                success=False,
                error=str(e)
            )
    
    def parse_response(self, raw: Dict, keyword: str, latency: int = 0) -> SearchResponse:
        # 千问的响应结构不同
        output = raw.get("output", {})
        answer = output.get("text", "")
        
        # 千问在 search_results 中返回引用
        citations = []
        for sr in output.get("search_results", []):
            citations.append(sr.get("url", ""))
        
        return SearchResponse(
            platform=self.platform,
            keyword=keyword,
            answer_text=answer,
            citations=citations,
            raw_response=raw,
            latency_ms=latency,
            tokens_used=raw.get("usage", {}).get("total_tokens", 0),
            success=True
        )
    
    def is_brand_mentioned(self, answer: str, brand_names: List[str]) -> bool:
        answer_lower = answer.lower()
        return any(name.lower() in answer_lower for name in brand_names)

3.4 Kimi 适配器实现

python 复制代码
class KimiAdapter(AISearchAdapter):
    """Kimi (Moonshot) 适配器"""
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.moonshot.cn/v1"
        self.client = httpx.AsyncClient(timeout=60.0)  # Kimi 联网搜索较慢
    
    @property
    def platform(self) -> PlatformType:
        return PlatformType.KIMI
    
    @property
    def rate_limit(self) -> dict:
        # Kimi: 30 次/分钟(免费额度)
        return {"max_requests": 30, "window_seconds": 60}
    
    async def search(self, request: SearchRequest) -> SearchResponse:
        start = time.time()
        try:
            payload = {
                "model": "moonshot-v1-32k",
                "messages": [
                    {"role": "system", "content": request.system_prompt or "你是一个搜索助手"},
                    {"role": "user", "content": request.keyword}
                ],
                "temperature": request.temperature,
                "max_tokens": request.max_tokens,
            }
            
            response = await self.client.post(
                f"{self.base_url}/chat/completions",
                json=payload,
                headers={
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                }
            )
            response.raise_for_status()
            raw = response.json()
            
            latency = int((time.time() - start) * 1000)
            return self.parse_response(raw, request.keyword, latency)
            
        except Exception as e:
            return SearchResponse(
                platform=self.platform,
                keyword=request.keyword,
                answer_text="",
                success=False,
                error=str(e)
            )
    
    def parse_response(self, raw: Dict, keyword: str, latency: int = 0) -> SearchResponse:
        answer = raw.get("choices", [{}])[0].get("message", {}).get("content", "")
        
        return SearchResponse(
            platform=self.platform,
            keyword=keyword,
            answer_text=answer,
            citations=[],  # Kimi 免费版不返回引用
            raw_response=raw,
            latency_ms=latency,
            tokens_used=raw.get("usage", {}).get("total_tokens", 0),
            success=True
        )
    
    def is_brand_mentioned(self, answer: str, brand_names: List[str]) -> bool:
        answer_lower = answer.lower()
        return any(name.lower() in answer_lower for name in brand_names)

四、适配器工厂与注册表

python 复制代码
from typing import Type, Dict

class AdapterRegistry:
    """适配器注册表 - 支持动态注册"""
    
    _adapters: Dict[PlatformType, Type[AISearchAdapter]] = {}
    _instances: Dict[PlatformType, AISearchAdapter] = {}
    
    @classmethod
    def register(cls, platform: PlatformType, adapter_class: Type[AISearchAdapter]):
        """注册适配器类"""
        cls._adapters[platform] = adapter_class
    
    @classmethod
    def get_adapter(cls, platform: PlatformType, **config) -> AISearchAdapter:
        """获取适配器实例(单例)"""
        if platform not in cls._instances:
            if platform not in cls._adapters:
                raise ValueError(f"No adapter registered for {platform}")
            adapter_class = cls._adapters[platform]
            cls._instances[platform] = adapter_class(**config)
        return cls._instances[platform]
    
    @classmethod
    def get_all_adapters(cls) -> Dict[PlatformType, AISearchAdapter]:
        """获取所有已注册的适配器"""
        return cls._instances.copy()


class AdapterFactory:
    """适配器工厂 - 根据配置创建适配器"""
    
    @staticmethod
    def create_from_config(config: dict) -> Dict[PlatformType, AISearchAdapter]:
        adapters = {}
        
        if doubao_cfg := config.get("doubao"):
            adapters[PlatformType.DOUBAO] = DoubaoAdapter(
                api_key=doubao_cfg["api_key"],
                app_id=doubao_cfg["app_id"]
            )
        
        if qianwen_cfg := config.get("qianwen"):
            adapters[PlatformType.QIANWEN] = QianwenAdapter(
                api_key=qianwen_cfg["api_key"]
            )
        
        if kimi_cfg := config.get("kimi"):
            adapters[PlatformType.KIMI] = KimiAdapter(
                api_key=kimi_cfg["api_key"]
            )
        
        return adapters

五、公共能力层

5.1 统一限流器

python 复制代码
import asyncio
import time
from collections import defaultdict

class RateLimiter:
    """基于令牌桶的分布式限流器"""
    
    def __init__(self, redis_client):
        self.redis = redis_client
    
    async def acquire(self, key: str, max_requests: int, window: int) -> bool:
        """尝试获取一个令牌"""
        now = time.time()
        pipe = self.redis.pipeline()
        
        # 清除窗口外的记录
        pipe.zremrangebyscore(key, 0, now - window)
        # 添加当前请求
        pipe.zadd(key, {str(now): now})
        # 计算窗口内请求数
        pipe.zcard(key)
        # 设置 key 过期时间
        pipe.expire(key, window)
        
        _, _, count, _ = await pipe.execute()
        return count <= max_requests


class RateLimitedAdapter:
    """限流装饰器 - 包装任何适配器"""
    
    def __init__(self, adapter: AISearchAdapter, rate_limiter: RateLimiter):
        self.adapter = adapter
        self.limiter = rate_limiter
    
    async def search(self, request: SearchRequest) -> SearchResponse:
        limit = self.adapter.rate_limit
        key = f"rate_limit:{self.adapter.platform.value}"
        
        acquired = await self.limiter.acquire(
            key, limit["max_requests"], limit["window_seconds"]
        )
        
        if not acquired:
            return SearchResponse(
                platform=self.adapter.platform,
                keyword=request.keyword,
                answer_text="",
                success=False,
                error="Rate limit exceeded"
            )
        
        return await self.adapter.search(request)

5.2 熔断器

python 复制代码
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"      # 正常
    OPEN = "open"          # 熔断
    HALF_OPEN = "half_open"  # 半开

class CircuitBreaker:
    """熔断器 - 保护下游服务"""
    
    def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 60):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.state = CircuitState.CLOSED
        self.failure_count = 0
        self.last_failure_time = 0
    
    async def call(self, func, *args, **kwargs):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
            else:
                raise Exception("Circuit breaker is OPEN")
        
        try:
            result = await func(*args, **kwargs)
            if self.state == CircuitState.HALF_OPEN:
                self.state = CircuitState.CLOSED
                self.failure_count = 0
            return result
        except Exception as e:
            self.failure_count += 1
            self.last_failure_time = time.time()
            if self.failure_count >= self.failure_threshold:
                self.state = CircuitState.OPEN
            raise

六、多平台并行采集

python 复制代码
import asyncio
from typing import List, Dict

class MultiPlatformCollector:
    """多平台并行采集器"""
    
    def __init__(self, adapters: Dict[PlatformType, AISearchAdapter]):
        self.adapters = adapters
    
    async def collect_all(self, keyword: str, brand_names: List[str]) -> Dict[str, SearchResponse]:
        """同时在所有平台搜索同一关键词"""
        tasks = []
        platforms = []
        
        for platform, adapter in self.adapters.items():
            request = SearchRequest(
                keyword=keyword,
                platform=platform,
                system_prompt=f"请详细介绍关于'{keyword}'的信息"
            )
            tasks.append(adapter.search(request))
            platforms.append(platform)
        
        # 并行执行,设置超时
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        output = {}
        for platform, result in zip(platforms, results):
            if isinstance(result, Exception):
                output[platform.value] = SearchResponse(
                    platform=platform,
                    keyword=keyword,
                    answer_text="",
                    success=False,
                    error=str(result)
                )
            else:
                output[platform.value] = result
        
        return output
    
    async def collect_batch(
        self, 
        keywords: List[str], 
        platforms: List[PlatformType],
        concurrency: int = 10
    ) -> List[Dict[str, SearchResponse]]:
        """批量采集:多个关键词 × 多个平台"""
        semaphore = asyncio.Semaphore(concurrency)
        
        async def collect_one(keyword: str):
            async with semaphore:
                adapters = {p: self.adapters[p] for p in platforms if p in self.adapters}
                collector = MultiPlatformCollector(adapters)
                return await collector.collect_all(keyword, [])
        
        tasks = [collect_one(kw) for kw in keywords]
        return await asyncio.gather(*tasks)

七、架构权衡

7.1 适配器粒度

方案 优势 劣势
细粒度(每平台独立类) 平台逻辑隔离清晰 类数量多
粗粒度(配置驱动) 代码量少 if-else 膨胀

选择:细粒度方案。每个平台差异足够大(响应格式、引用机制、限流),独立类更可维护。

7.2 同步 vs 异步

方案 适用场景
同步 少量关键词、实时查询
异步(asyncio) 大批量、高并发采集

选择:异步方案。AI API 调用是 I/O 密集型,asyncio 可高效并发。

7.3 适配器实例管理

python 复制代码
# 方案 A: 单例(推荐)
# 所有请求共享一个 adapter 实例,复用 HTTP 连接池
AdapterRegistry.get_adapter(PlatformType.DOUBAO, api_key="xxx")

# 方案 B: 每请求创建
# 无连接复用,适合极端隔离场景
DoubaoAdapter(api_key="xxx")

选择:单例方案。HTTP 连接池复用可大幅减少 TCP 握手开销。

八、扩展性设计

新增一个平台只需三步:

python 复制代码
# 1. 实现适配器
class DeepSeekAdapter(AISearchAdapter):
    def __init__(self, api_key: str):
        # ...
    
    @property
    def platform(self) -> PlatformType:
        return PlatformType.DEEPSEEK
    
    async def search(self, request: SearchRequest) -> SearchResponse:
        # 实现 DeepSeek API 调用
        pass
    
    def parse_response(self, raw, keyword, latency=0) -> SearchResponse:
        # 实现 DeepSeek 响应解析
        pass
    
    def is_brand_mentioned(self, answer, brand_names) -> bool:
        pass

# 2. 注册
AdapterRegistry.register(PlatformType.DEEPSEEK, DeepSeekAdapter)

# 3. 配置
# config.yaml 中添加 deepseek 配置即可

九、总结

多平台适配层是 AI GEO 系统的基础设施,其设计要点:

  1. 统一接口:SearchRequest/SearchResponse 屏蔽平台差异
  2. 适配器模式:每平台一个适配器,独立演进
  3. 工厂+注册表:动态管理适配器实例
  4. 公共能力层:限流、熔断、重试统一处理
  5. 并行采集:asyncio 并发调用多平台
  6. 开闭原则:新增平台不改现有代码

相关推荐
林伽一1 小时前
林伽一 · AI科技日报 | 2026年08月15日
人工智能
2501_942389551 小时前
时钟组件支持自由拖拽缩放
人工智能·散列表·启发式算法·宽度优先·图搜索算法
小马过河R1 小时前
不只是又一个 Agent 框架:DeepSeek Harness 如何重新定义“可组合”
人工智能·机器学习·系统架构·agent·ai编程·harness
蓝速科技1 小时前
蓝速科技 3D 全息舱 AI 数字人一体机全尺寸选型实测指南
人工智能·科技·3d
嵌入式学习_force1 小时前
BES2810ZP深度解析
ai·蓝牙·bes2810
小妖6661 小时前
设置了 box-sizing: border-box; 不管用,下边框还是被挤压没了
前端·css·html
lifallen2 小时前
DeepSeek Harness:把 Agent 做成可替换的运行时插件树
人工智能·学习·ai·开源软件·ai编程
刀锋00012 小时前
从0到1手搓生产级 AI Agent:LangGraph 1.2 + LangChain 1.3 保姆级实战(全部代码已跑通)
人工智能·python·langchain·ai agent·langgraph
水如烟2 小时前
孤能子视角:因果论——方向、锁定与必然感:关系场中归因链的生成语法
人工智能
老兵发新帖2 小时前
OSD和视频流接口随机出现net::ERR_CONNECTION_RESET问题分析总结
人工智能