
1. 项目概述为什么我们需要“中转API”最近在折腾AI应用开发的朋友估计没少为调用各种大模型的文本嵌入Embedding模型头疼。文本嵌入简单说就是把一段文字比如“今天天气真好”转换成一串有意义的数字向量这是构建智能搜索、推荐系统、知识问答的核心第一步。市面上好用的嵌入模型不少比如OpenAI的text-embedding-ada-002智谱、百度、阿里云也都有自家的产品。但直接调用这些官方API你可能会遇到几个很现实的问题网络延迟高、请求不稳定、费用不透明或者某些服务在国内访问不畅。这时候“中转API”就成了一个非常实用的技术方案。它不是一个新概念你可以把它理解为你和官方API之间的一个“智能调度员”或“缓冲层”。你自己搭建一个服务这个服务对外提供统一的API接口。当你的应用需要调用嵌入模型时不再直接请求OpenAI或智谱而是请求你自己的这个中转服务。然后由这个中转服务去帮你调用最终的后端模型并把结果返回给你。这么做的好处显而易见。第一是稳定性你可以在中转层实现重试、熔断、降级等机制即使某个后端服务临时抽风你的主应用也能保持基本功能。第二是灵活性你可以轻松地在多个嵌入模型供应商之间切换或者做A/B测试而无需修改主应用的代码。今天用OpenAI明天觉得智谱性价比更高改个配置就行。第三是成本与管控你可以在这里统一做请求的审计、限流、计费甚至对敏感信息进行脱敏处理这对于企业级应用至关重要。我最近就完整实现了一套这样的系统核心目标就一个让团队内部的各种AI应用都能通过一个简单、稳定、统一的接口来获取文本向量而不用关心背后到底是哪家公司在提供服务。下面我就把这套方案的设计思路、核心实现、踩过的坑以及可以直接抄作业的配置毫无保留地分享出来。2. 核心架构设计与技术选型2.1 整体架构蓝图我们的中转API服务在架构上遵循了经典的分层设计目标是高内聚、低耦合便于后续扩展。整个系统的数据流是这样的客户端可能是你的Web前端、移动App或者另一个后端服务。它向我们的中转服务发起一个标准的HTTP POST请求携带待处理的文本和必要的参数。API网关/路由层这是请求的入口。我们使用一个轻量级的Web框架如FastAPI或Spring Boot来接收请求进行初步的验证如API Key鉴权、参数校验。代理与路由层这是核心的“调度中心”。它根据预设的规则比如配置、负载、模型类型决定将这个请求转发给哪个后端的嵌入模型服务Provider。例如可以配置规则“所有中文文本优先走智谱GLM-Embedding英文文本走OpenAI”。供应商适配器层每个后端供应商如OpenAI、智谱、百度的API调用方式、参数格式、认证方法都不同。这一层就是一系列“翻译器”将我们内部的统一请求格式转换成对应供应商API能识别的格式并处理其特有的响应格式。后端模型服务即各大厂商提供的真实嵌入模型API。响应处理与返回拿到供应商的响应后适配器将其转换回我们内部统一的格式经过可能的结果缓存Cache层最终返回给客户端。此外系统中还会贯穿监控告警记录耗时、成功率、缓存机制对相同文本的嵌入结果进行缓存大幅降低成本和延迟和配置中心动态调整路由策略、供应商密钥。2.2 关键技术选型解析选型没有绝对的好坏只有是否适合你的场景。我的选择基于以下几个原则开发效率高、社区生态好、性能足够、易于部署。Web框架FastAPI为什么是它对于这类IO密集型的API代理服务Python的异步特性非常有优势。FastAPI天生支持async/await性能非常好而且自动生成交互式API文档Swagger UI这对于内部团队协作和调试简直是神器。它的数据验证依赖Pydantic用起来非常顺手。相比于Flask或Django它在构建现代API服务上更专注、更高效。备选方案如果你团队主力是Java那么用Spring Boot是更自然的选择。Spring Cloud生态下的Gateway、Feign等组件能让你更快地搭建起企业级的中转服务但在开发速度和原型验证上可能不如FastAPI快捷。HTTP客户端httpx为什么是它我们需要异步地调用后端供应商的API。httpx库提供了完全兼容requests库的同步接口同时支持一流的异步接口。它的连接池管理、超时设置、重试机制都很完善是替代aiohttp的一个更现代、更友好的选择。pip install httpx缓存Redis为什么需要缓存文本嵌入模型是确定性的相同的输入文本在同一个模型版本下一定会产生相同的向量。很多应用场景中会有大量重复或相似的文本被频繁请求比如商品标题、标准问题。每次都对重复文本调用收费的API是巨大的浪费。引入缓存后首次请求会访问真实API并缓存结果后续相同请求直接返回缓存延迟可以从几百毫秒降到几毫秒成本也能大幅降低。为什么选Redis内存数据库速度极快支持设置过期时间TTL数据结构丰富。我们可以用文本内容的MD5或SHA256哈希值作为键向量值作为值进行存储。注意点缓存键的设计要考虑模型名称和参数因为不同模型或不同参数下的向量是不同的。例如openai:text-embedding-ada-002:md5(文本)。配置管理环境变量 YAML文件供应商的API Key、Base URL、请求超时时间等敏感或易变的配置绝不能硬编码在代码里。我采用pydantic-settings来管理配置它支持从.env文件和环境变量中优雅地加载配置并和Pydantic模型结合提供类型验证和自动补全。路由规则等更复杂的配置可以放在一个config.yaml文件里在服务启动时加载。监控与日志Prometheus Grafana 结构化日志在关键位置如请求入口、调用供应商前后打点记录耗时、状态码。使用prometheus-client暴露指标。日志使用structlog或json-logging输出为JSON格式便于后续用ELK或Loki进行收集和检索。一定要记录请求ID方便串联整个调用链。3. 核心实现细节与代码拆解3.1 定义统一的数据模型这是保证系统内部一致性的基石。我们定义几个核心的Pydantic模型。from pydantic import BaseModel, Field from typing import List, Optional, Literal class EmbeddingRequest(BaseModel): 统一的嵌入请求模型 texts: List[str] Field(..., description待编码的文本列表支持批量) model: Optional[str] Field(defaultopenai-ada-002, description指定的嵌入模型名称) # 可以扩展其他通用参数如 truncation, encoding_format 等 # 但注意不同供应商支持的参数不同需要在适配器层处理差异 class EmbeddingResponse(BaseModel): 统一的嵌入响应模型 data: List[List[float]] Field(..., description向量列表每个文本对应一个向量) model: str Field(..., description实际使用的模型名称) total_tokens: Optional[int] Field(None, description消耗的总tokens数) cached: bool Field(defaultFalse, description本次结果是否来自缓存) class ProviderConfig(BaseModel): 供应商配置模型 name: str # 如 openai, zhipu api_key: str base_url: Optional[str] None # 有的供应商需要自定义base_url enabled: bool True priority: int 0 # 优先级用于路由 # 供应商特定参数如请求超时、最大重试次数等 timeout: int 30 max_retries: int 23.2 实现供应商适配器模式适配器模式是关键。我们定义一个抽象基类然后为每个供应商实现一个具体的适配器。from abc import ABC, abstractmethod import httpx from .models import EmbeddingRequest, EmbeddingResponse class BaseEmbeddingProvider(ABC): 嵌入供应商适配器基类 def __init__(self, config: ProviderConfig): self.config config self.client httpx.AsyncClient( timeoutself.config.timeout, limitshttpx.Limits(max_keepalive_connections10, max_connections100) ) abstractmethod async def embed(self, request: EmbeddingRequest) - EmbeddingResponse: 核心方法将请求转发给具体供应商并返回统一格式的响应 pass abstractmethod def calculate_cost(self, tokens: int) - float: 计算本次请求的成本可选用于成本分析 pass async def close(self): 关闭HTTP客户端 await self.client.aclose() class OpenAIEmbeddingProvider(BaseEmbeddingProvider): OpenAI 适配器 async def embed(self, request: EmbeddingRequest) - EmbeddingResponse: # 1. 构建符合OpenAI API格式的请求体 openai_request { input: request.texts, model: request.model or text-embedding-ada-002, # encoding_format: float # 可选 } # 2. 发送异步请求 headers { Authorization: fBearer {self.config.api_key}, Content-Type: application/json } try: resp await self.client.post( f{self.config.base_url or https://api.openai.com}/v1/embeddings, jsonopenai_request, headersheaders ) resp.raise_for_status() result resp.json() except httpx.HTTPStatusError as e: # 这里可以细化处理不同的HTTP错误码如429限流、401密钥错误等 raise Exception(fOpenAI API error: {e.response.status_code} - {e.response.text}) # 3. 将OpenAI的响应转换为我们统一的格式 embeddings [item[embedding] for item in result[data]] return EmbeddingResponse( dataembeddings, modelresult[model], total_tokensresult.get(usage, {}).get(total_tokens), cachedFalse ) def calculate_cost(self, tokens: int) - float: # 假设是 ada-002 模型每1000个token $0.0001 # 实际价格需要查询OpenAI官网最新定价 return tokens * 0.0001 / 1000 # 类似地实现智谱、百度等适配器 class ZhipuEmbeddingProvider(BaseEmbeddingProvider): 智谱AI 适配器 async def embed(self, request: EmbeddingRequest) - EmbeddingResponse: # 智谱的API端点、参数格式、认证方式与OpenAI不同 # 认证可能是 API Key 放在 Authorization 头也可能是其他方式 # 请求体格式也可能是 {prompt: texts, model: embedding-2} 等 # 需要查阅智谱的官方API文档进行适配 pass注意每个供应商的API都在快速迭代上述代码示例中的端点、参数、认证方式可能需要根据最新的官方文档进行调整。适配器的核心价值就在于将这种变化封装在内部对外保持接口稳定。3.3 实现路由与缓存逻辑有了多个适配器我们需要一个“路由器”来管理它们并集成缓存。import hashlib import json from typing import Dict import redis.asyncio as redis from .models import EmbeddingRequest, EmbeddingResponse, ProviderConfig from .providers import OpenAIEmbeddingProvider, ZhipuEmbeddingProvider # 导入各种适配器 class EmbeddingRouter: def __init__(self, redis_client: redis.Redis None): self.providers: Dict[str, BaseEmbeddingProvider] {} self.redis redis_client # 可以从配置文件加载并初始化所有启用的provider self._init_providers() def _init_providers(self): # 示例从配置加载 configs load_provider_configs() # 假设这个函数从配置中心或文件读取 for config in configs: if config.name openai and config.enabled: self.providers[openai] OpenAIEmbeddingProvider(config) elif config.name zhipu and config.enabled: self.providers[zhipu] ZhipuEmbeddingProvider(config) # ... 其他供应商 def _generate_cache_key(self, request: EmbeddingRequest, provider_name: str) - str: 生成缓存键供应商:模型:文本哈希 # 对请求文本排序后计算哈希确保顺序不影响缓存 text_for_hash json.dumps(sorted(request.texts), ensure_asciiFalse) hash_obj hashlib.md5(text_for_hash.encode()) return fembed:{provider_name}:{request.model}:{hash_obj.hexdigest()} async def get_embedding(self, request: EmbeddingRequest, preferred_provider: str None) - EmbeddingResponse: 主路由方法。 1. 检查缓存 2. 选择供应商 3. 调用供应商 4. 写入缓存 # --- 1. 供应商选择策略这里是一个简单示例--- provider_name preferred_provider if not provider_name: # 更复杂的策略根据文本语言、长度、成本、供应商健康度等选择 # 这里简单选择优先级最高且启用的第一个 available [p for p in self.providers.values() if p.config.enabled] if not available: raise Exception(No available embedding provider) # 按优先级排序 available.sort(keylambda x: x.config.priority, reverseTrue) provider available[0] provider_name provider.config.name else: provider self.providers.get(provider_name) if not provider or not provider.config.enabled: raise Exception(fProvider {provider_name} is not available) # --- 2. 缓存查询 --- cache_key self._generate_cache_key(request, provider_name) cached_result None if self.redis: try: cached_data await self.redis.get(cache_key) if cached_data: cached_result EmbeddingResponse.parse_raw(cached_data) cached_result.cached True # 标记为缓存结果 return cached_result except Exception as e: # 缓存查询失败不应阻塞主流程记录日志后继续 logging.warning(fCache query failed: {e}) # --- 3. 调用真实供应商API --- try: live_result await provider.embed(request) live_result.cached False except Exception as e: # 调用失败可以尝试降级到其他供应商 logging.error(fProvider {provider_name} failed: {e}) # 这里可以实现故障转移逻辑例如移除当前provider重试其他provider raise e # --- 4. 缓存写入异步进行不阻塞返回--- if self.redis and live_result: # 使用异步任务避免阻塞响应 asyncio.create_task(self._set_cache(cache_key, live_result)) return live_result async def _set_cache(self, key: str, result: EmbeddingResponse, ttl: int 86400): 异步设置缓存默认过期时间24小时 try: await self.redis.setex(key, ttl, result.json()) except Exception as e: logging.warning(fCache set failed: {e})3.4 构建FastAPI主应用最后我们用FastAPI将上述组件串联起来暴露一个干净的API。from fastapi import FastAPI, Depends, HTTPException, Header from contextlib import asynccontextmanager import uvicorn from .router import EmbeddingRouter from .models import EmbeddingRequest, EmbeddingResponse import redis.asyncio as redis # 全局依赖生命周期管理 asynccontextmanager async def lifespan(app: FastAPI): # 启动时初始化Redis连接和路由器 app.state.redis await redis.from_url(redis://localhost:6379, decode_responsesFalse) app.state.router EmbeddingRouter(app.state.redis) yield # 关闭时清理资源 await app.state.redis.close() for provider in app.state.router.providers.values(): await provider.close() app FastAPI(titleEmbedding API Gateway, lifespanlifespan) # 简单的API Key认证示例 def verify_api_key(x_api_key: str Header(None)): # 这里应该从数据库或配置中验证Key valid_keys {your-secret-key-here} if x_api_key not in valid_keys: raise HTTPException(status_code403, detailInvalid API Key) return x_api_key app.post(/v1/embeddings, response_modelEmbeddingResponse) async def create_embedding( request: EmbeddingRequest, x_provider: str Header(None), # 可选客户端指定供应商 _: str Depends(verify_api_key) # 依赖认证 ): 统一的文本嵌入接口 try: result await app.state.router.get_embedding(request, x_provider) return result except Exception as e: # 记录详细日志 logging.exception(Embedding request failed) # 向客户端返回适当的错误信息避免泄露内部细节 raise HTTPException(status_code500, detailInternal server error) if __name__ __main__: uvicorn.run(app, host0.0.0.0, port8000)现在你的客户端只需要向http://你的服务地址:8000/v1/embeddings发送一个标准的请求就可以获得文本向量完全不用管背后是OpenAI还是智谱。4. 部署、优化与生产级考量4.1 部署方案一个简单的服务写好了但要稳定可靠地跑在生产环境还需要考虑部署。容器化使用Docker是标准做法。编写Dockerfile基于Python slim镜像复制代码安装依赖。这保证了环境一致性。FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000, --workers, 4]进程管理在Docker容器内使用--workers参数启动多个Uvicorn工作进程利用多核CPU。更专业的做法是使用Gunicorn作为进程管理器搭配Uvicorn工作线程类。gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000编排与扩缩容使用Docker Compose单机或Kubernetes集群来编排你的应用、Redis等依赖服务。在K8s中你可以配置Horizontal Pod Autoscaler (HPA)根据CPU/内存或自定义指标如请求队列长度自动增加或减少Pod副本数。4.2 性能与稳定性优化连接池与超时务必为httpx.AsyncClient配置连接池limits参数避免频繁建立TCP连接的开销。设置合理的超时连接超时、读取超时防止慢请求拖垮整个服务。异步缓存操作如代码所示缓存读写尤其是写应该使用asyncio.create_task异步执行绝不能阻塞请求响应的主路径。重试与熔断对于供应商API的临时性失败如网络抖动、服务端限流429应该在适配器层实现重试逻辑可以使用tenacity库。同时引入熔断器模式如aiobreaker当某个供应商失败率达到阈值时自动熔断一段时间避免雪崩。限流在你的FastAPI入口处实现限流防止被恶意刷接口或内部某个应用异常调用拖垮。可以使用slowapi或fastapi-limiter中间件。监控告警这是线上服务的眼睛。除了基础的CPU/内存监控一定要暴露业务指标embedding_requests_total总请求数embedding_request_duration_seconds请求耗时分布embedding_requests_by_provider按供应商分类的请求数和错误数cache_hit_rate缓存命中率 当错误率飙升、延迟增加或缓存命中率骤降时及时触发告警如发送到钉钉、企业微信。4.3 成本控制与运营中转层给了你一个绝佳的成本控制视角。按需路由可以将对质量要求不高的内部工具、测试环境的请求路由到更便宜的模型甚至本地开源模型将对质量要求高的生产请求路由到性能更好的付费模型。用量分析与审计在路由器中记录每一笔请求的详细信息谁调的API Key或用户ID、调了什么模型、消耗了多少Token、成本是多少。这些数据对于财务核算、资源优化和异常排查至关重要。预算与熔断可以为每个团队或每个API Key设置每日/每月的Token消耗预算。在路由层进行累计当接近预算时发出警告超出后直接拒绝请求或降级到免费模型。5. 常见问题与故障排查实录在实际搭建和运营过程中我遇到了不少坑这里分享几个典型的问题一调用供应商API超时导致整个客户端请求挂起。现象客户端调用中转API有时会等待几十秒才返回或直接超时。排查查看中转服务日志发现是调用某个供应商如OpenAI的httpx请求卡住了。根因没有设置合理的超时时间或者网络环境不稳定。解决必须设置超时在创建httpx.AsyncClient时明确设置timeout30.0连接、读写总超时。更好的做法是分别设置connect和read超时。配置重试对于网络错误ConnectTimeout、ReadTimeout和特定的服务端错误如429 502 503使用tenacity库进行有限次数的重试例如最多3次并加入指数退避exponential backoff策略避免加重服务器负担。引入熔断如果某个供应商在短时间内失败率过高如10次请求失败8次则熔断该供应商60秒期间所有请求直接失败或路由到备用供应商给故障服务恢复的时间。问题二缓存导致向量不一致业务逻辑出错。现象更新了供应商的模型版本比如从text-embedding-ada-002换到text-embedding-3-small但业务上感觉搜索结果没变化甚至变差了。排查检查代码和配置确认请求的模型参数已经改了。但查看Redis发现缓存键中包含了模型名理论上是不同的键。根因缓存键设计有遗漏。除了模型名可能还有一些隐含参数如供应商API版本、请求的encoding_format等没有包含在缓存键中导致新请求命中了旧参数下的错误缓存。解决精细化缓存键确保缓存键唯一标识一次请求的所有可变输入。至少应包括供应商名:模型名:文本哈希。如果请求中有其他影响结果的参数通过查阅官方文档确认也要加入。主动清理缓存在切换重要配置如模型版本时要有预案手动或自动清理相关的缓存。例如可以给缓存键加上版本前缀v2:embed:...或者用Redis的SCAN命令模糊删除所有包含旧模型名的键。设置合理的TTL即使是相同的输入和模型供应商也可能在后台更新模型权重虽然不常见。为缓存设置一个不是特别长的TTL比如7天或30天可以让数据定期刷新。问题三高并发下Redis成为瓶颈或单点故障。现象QPS每秒查询率升高后API响应时间变长监控显示Redis的CPU使用率很高。排查大量请求同时查询缓存导致Redis压力大。或者Redis实例宕机导致所有请求都穿透到后端API瞬间打爆供应商的限流。解决本地内存缓存作为二级缓存在应用进程内存中使用LRU缓存如functools.lru_cache缓存最热的一部分数据。这能极大减少对Redis的访问。注意设置合适的内存上限和过期策略。Redis集群与读写分离如果数据量或QPS非常大需要考虑使用Redis集群模式并可能将缓存读请求导向从节点。缓存降级与穿透保护当Redis不可用时服务应能降级直接访问供应商API虽然慢但可用。同时要做好缓存穿透查询不存在的数据和缓存击穿热点Key过期瞬间大量请求的保护例如使用互斥锁Mutex或布隆过滤器。问题四供应商API返回非标准错误难以处理。现象供应商API返回了400 Bad Request但错误信息五花八门如invalid parameter: model、context length exceeded直接抛给客户端不友好。排查不同供应商的错误码和消息格式不统一。解决在适配器层做错误归一化。捕获供应商特定的异常然后转换为内部定义的一套标准错误码和友好消息。class EmbeddingError(Exception): def __init__(self, code: str, message: str, provider_detail: str None): self.code code # 如 INVALID_PARAMETER, CONTEXT_TOO_LONG, RATE_LIMIT self.message message self.provider_detail provider_detail # 在OpenAI适配器中 except httpx.HTTPStatusError as e: if e.response.status_code 400: error_body e.response.json() # 解析OpenAI特定的错误信息 if context length in error_body.get(error, {}).get(message, ): raise EmbeddingError( codeCONTEXT_TOO_LONG, message输入文本过长请缩短文本或分段处理。, provider_detailstr(error_body) ) else: raise EmbeddingError( codeINVALID_PARAMETER, message请求参数有误。, provider_detailstr(error_body) ) elif e.response.status_code 429: raise EmbeddingError(codeRATE_LIMIT, message请求过于频繁请稍后再试。) else: raise # 其他错误原样抛出或封装这样在中转API的全局异常处理器中就可以将EmbeddingError转换为结构化的、对客户端友好的HTTP错误响应。搭建一个成熟可用的AI文本嵌入中转API远不止是写一个简单的代理。它涉及到架构设计、稳定性保障、成本控制和日常运维的方方面面。从我的经验来看前期在缓存设计、错误处理和监控上多花一点时间后期运维的幸福感会提升好几个数量级。这个服务一旦稳定运行就会成为你AI应用基础设施中非常可靠的一块基石让你能更从容地探索和利用不同的AI能力。