构建统一LLM API调用层:适配OpenAI、Claude、Gemini与开源模型
1. 项目概述:一次代码,四家模型,我的统一调用实践
最近在做一个需要集成多种大语言模型(LLM)的智能应用原型,需求很简单:后端服务需要能灵活切换调用不同厂商的模型,比如 OpenAI 的 GPT-4、Anthropic 的 Claude、Google 的 Gemini,以及开源的 Llama 系列。我不想为每个模型写一套独立的、紧耦合的调用代码,那会让后续的维护、测试和模型切换变成一场噩梦。于是,我决定构建一个统一的代码库,用同一套接口和逻辑去调用这四家风格迥异的 API。
这个项目听起来像是简单的“封装”,但实际做下来,我发现远不止是写几个适配器那么简单。它涉及到对各家 API 设计哲学、计费模式、性能特性和“怪癖”的深度理解。通过这次实践,我不仅成功实现了目标,更收获了一套关于如何设计健壮、可扩展的 LLM 集成架构的宝贵经验。无论你是正在构建多模型应用的开发者,还是单纯想了解主流 LLM API 的异同,我相信我的这些踩坑记录和解决方案都能给你带来直接的参考价值。
2. 核心架构设计与抽象层定义
2.1 为什么需要抽象,而不仅仅是封装
最开始,我的想法很朴素:为每个模型写一个函数,比如call_openai(),call_claude(),然后在业务逻辑里用if-else判断该用哪个。这个方案在只有两个模型时还能忍受,但很快问题就暴露了。
首先,参数差异巨大。OpenAI 的temperature和top_p通常只用一个,而 Anthropic 早期版本对两者都有严格限制;各家对max_tokens(输出长度)的定义和默认值也不同。其次,响应格式不统一。有的返回choices[0].message.content,有的返回content[0].text,错误码和重试逻辑更是千差万别。最后,扩展成本高。每增加一个新模型,我就要在所有调用处添加新的判断分支,测试用例也要成倍增加。
因此,真正的解决方案不是“封装”,而是“抽象”。我需要定义一套与具体厂商无关的、属于我自己的“LLM 领域模型”。这个模型包括:
- 统一的请求对象:包含所有模型都需要的核心参数(如消息列表、温度、最大输出长度),并将厂商特定参数作为可选的“扩展字段”。
- 统一的响应对象:标准化成功时的文本内容、token 使用量,以及错误时的异常信息。
- 统一的客户端接口:一个
complete方法,接收统一请求,返回统一响应,内部处理所有厂商适配细节。
这样,我的业务代码只需要和这套统一的接口打交道,完全不知道底层调用的是 GPT 还是 Claude。模型的切换可以通过配置(如一个环境变量LLM_PROVIDER=openai)来实现,实现了“控制反转”。
2.2 统一数据模型的设计细节
设计统一数据模型是平衡通用性与灵活性的艺术。以下是我定义的核心类:
from typing import List, Optional, Dict, Any from pydantic import BaseModel class UnifiedMessage(BaseModel): """统一的消息格式,兼容 OpenAI 的 role/content 格式。""" role: str # “system”, “user”, “assistant” content: str class UnifiedLLMRequest(BaseModel): """发送给 LLM 的统一请求。""" messages: List[UnifiedMessage] model: str # 如 “gpt-4-turbo”, “claude-3-opus-20240229” temperature: Optional[float] = 0.7 max_tokens: Optional[int] = 2048 stream: bool = False # 厂商特定参数,用于传递不通用但必要的选项 provider_kwargs: Dict[str, Any] = {} class UnifiedLLMResponse(BaseModel): """从 LLM 接收的统一响应。""" success: bool content: Optional[str] = None # 成功时的回复文本 error_message: Optional[str] = None # 失败时的错误信息 usage: Optional[Dict[str, int]] = None # 如 {“prompt_tokens”: 100, “completion_tokens”: 50} raw_response: Optional[Any] = None # 保留原始响应,用于调试关键设计决策与理由:
model字段包含提供商信息:我最初考虑过拆分成provider和model_name两个字段,但后来发现像“claude-3-sonnet-20240229”这样的字符串本身就具有唯一标识性。业务配置时直接写这个字符串更直观。内部适配器可以通过字符串前缀(如gpt-、claude-)或一个映射表来识别提供商。provider_kwargs这个“逃生舱”:这是最重要的设计之一。无论抽象层设计得多好,总会遇到某个厂商独有的、必须传递的参数(例如,OpenAI 的response_format用于 JSON 模式,Anthropic 的stop_sequences)。provider_kwargs允许业务层在知晓特定厂商细节时,传入这些参数,由对应的适配器负责处理。这避免了为了一两个特殊参数而污染通用接口。- 保留
raw_response:在调试阶段,能够看到 API 返回的原始数据至关重要。它帮助我快速定位是抽象层转换出错,还是 API 本身返回了异常结构。
3. 四大主流 LLM API 适配器实现详解
有了统一接口,接下来就是为每个厂商实现适配器(Adapter)。每个适配器继承自一个抽象的BaseLLMAdapter类,实现complete方法。以下是适配四大 API 的核心要点和踩坑记录。
3.1 OpenAI API 适配:稳定但需注意细节
OpenAI 的 API 是目前事实上的标准,文档清晰,社区支持最好。适配它相对直接。
核心实现步骤:
- 将
UnifiedMessage列表直接转换为 OpenAI 格式的messages列表(格式几乎一致)。 - 处理参数映射:
temperature,max_tokens直接对应。stream模式需要特殊处理回调。 - 调用
openai.ChatCompletion.create(或较新版本的openai.resources.chat.completions.create)。 - 从响应中提取内容:
response.choices[0].message.content。 - 提取用量:
response.usage字典。
注意事项与实操心得:
- API Key 与 Base URL:务必通过环境变量管理 API Key。对于使用 Azure OpenAI 服务的用户,
base_url和api_version是必须正确配置的关键参数,与标准的 OpenAI 端点不同。 - Token 计算与
max_tokens:OpenAI 的max_tokens指的是生成令牌的上限。如果你的提示(Prompt)非常长,需要预留足够的max_tokens给回复。一个常见的坑是忘记了系统提示(System Prompt)也消耗 Token。在关键业务中,最好在发送前用tiktoken库估算一下总 Token 数,避免因超出上下文长度而请求失败。 - 流式响应(Streaming)处理:如果开启了
stream=True,响应是一个异步生成器。适配器需要将这些 chunk 拼接起来,并在流结束时(收到[DONE]标记或特定字段)构造统一的响应对象。处理流式响应时,错误处理会更复杂,因为网络中断可能发生在流中间。
# 简化的 OpenAI 适配器核心片段 class OpenAIAdapter(BaseLLMAdapter): async def complete(self, request: UnifiedLLMRequest) -> UnifiedLLMResponse: try: client = openai.AsyncOpenAI(api_key=self.api_key) openai_messages = [{"role": msg.role, "content": msg.content} for msg in request.messages] extra_args = request.provider_kwargs.copy() # 处理可能冲突的通用参数 if "temperature" not in extra_args: extra_args["temperature"] = request.temperature # ... 类似处理 max_tokens, stream response = await client.chat.completions.create( model=request.model, messages=openai_messages, **extra_args ) content = response.choices[0].message.content usage = response.usage.dict() if response.usage else None return UnifiedLLMResponse(success=True, content=content, usage=usage, raw_response=response) except openai.APIError as e: # 统一转换为自定义异常或错误响应 return UnifiedLLMResponse(success=False, error_message=f"OpenAI API Error: {e}")3.2 Anthropic Claude API 适配:消息格式与思维链
Anthropic 的 Claude API 设计上有其独特之处,需要特别注意。
核心差异与适配:
- 消息格式:Claude 使用
messages数组,但每个消息是一个字典,包含role和content。content在最新 API 中是一个由text或image块组成的数组。对于纯文本,我们需要将UnifiedMessage.content包装成{"type": "text", "text": content}。这是第一个关键转换点。 - 系统提示(System Prompt):Claude 将系统提示作为独立的
system参数传递,而不是放在messages数组的开头。适配器需要从messages中找出role为“system”的消息,将其内容提取出来作为system参数,并从messages列表中移除,剩下的作为对话历史。 max_tokens是必填项:与 OpenAI 不同,Claude 的max_tokens是请求的必填参数,没有默认值。适配器必须提供一个合理的默认值(如 1024)或强制业务层指定。- 思维链(Chain of Thought)与工具使用:Claude 支持在消息中要求模型展示思维过程(通过
thinking类型的 content 块),也支持复杂的工具调用(Function Calling/Tool Use)。这些高级功能需要通过provider_kwargs来传递复杂结构。
实操心得:
- 参数严格性:Anthropic 的 API 对参数值范围检查更严格。例如,早期版本对
temperature和top_p有互斥要求。务必仔细阅读你所用 API 版本的最新文档。 - 错误处理:Claude API 的错误响应结构可能与 OpenAI 不同。适配器需要捕获
anthropic.APIError并从中解析出有用的错误信息,统一到我们的error_message字段中。 - 流式响应:Claude 的流式响应格式(Server-Sent Events)与 OpenAI 不同,解析逻辑需要单独实现。特别是,思维链的流式输出是分块的,需要正确拼接。
3.3 Google Gemini API 适配:面向多模态的设计
Google 的 Gemini API 在设计上更强调多模态能力,其 Python SDK 的使用方式也与前两者有区别。
核心差异与适配:
- 消息历史结构:Gemini 的
ChatSession概念更重。虽然单次调用也可以,但为了利用多轮对话历史,最好维护一个chat会话对象。在我们的抽象中,每次complete调用可能对应一次独立的会话(Stateless),因此我们需要在适配器内部,根据messages历史动态构造本次请求的上下文。Gemini 的消息内容也是parts列表,每个part可以是text或file_data。 - 安全设置:Gemini API 明确要求配置安全设置(
safety_settings),以过滤不同危险等级的回复。这通常是一个全局配置,可以在适配器初始化时设置,并通过provider_kwargs允许每次请求覆盖。 - 模型名称:Gemini 模型名称如
“gemini-1.5-pro”,适配器需要正确识别。 - 响应格式:成功响应的文本内容在
response.text中。需要特别注意,Gemini 的response.prompt_feedback可能包含因安全设置而被阻止的提示,这应被视为一种特定类型的错误。
注意事项:
- 初始化开销:
google.generativeai.configure和生成模型对象有一定开销。适配器应实现连接池或缓存模型对象,避免每次调用都重复初始化。 - 多模态输入:如果未来需要支持图像输入,
UnifiedMessage可能需要扩展以支持多模态content。目前可以通过provider_kwargs传递复杂的parts列表来临时实现。 - 速率限制与配额:Google Cloud 项目的配额管理方式与 OpenAI 的账户额度不同,错误信息也可能体现在 Google Cloud 的 API 错误中,需要单独处理。
3.4 开源模型(如 Llama)API 适配:与 OpenAI 协议兼容
这里指的是通过像vLLM、Ollama或Llama.cpp的server模式等部署方式提供的、通常兼容OpenAI API 格式的本地或自托管模型端点。
适配策略:这是最简单的适配情况。因为这些项目的目标之一就是提供与 OpenAI ChatCompletion API 兼容的端点,所以我们的OpenAIAdapter几乎可以复用。
需要调整的关键点:
- Base URL:将客户端指向本地或内网端点,例如
http://localhost:8000/v1。 - API Key:这类服务可能不需要 API Key,或使用一个固定的假 Key(如
“no-key”)。适配器需要处理空 Key 或模拟 Key 的情况。 - 模型名称:请求中的
model参数可能需要与服务器端配置的模型名称对应。有时服务器会忽略这个参数,只使用其加载的唯一模型。 - 细微差异:尽管协议兼容,但实现上可能有细微差别。例如,某些端点可能不支持
stream_options参数,或者错误响应的格式略有不同。必须进行充分的兼容性测试。
实操心得:
- 使用
openai库的灵活性:openai.OpenAI或openai.AsyncOpenAI客户端可以接受自定义的base_url。这使得我们可以用同一套代码与兼容 OpenAI 协议的任意端点通信,极大地简化了集成工作。 - 超时设置:自托管模型的性能可能不稳定,需要适当增加客户端的超时(
timeout)参数。 - 上下文长度:不同开源模型的上下文窗口(Context Window)差异很大(如 4K, 8K, 32K, 128K)。适配器或配置层需要知晓这个限制,并在构造请求时进行提示词裁剪或给出明确错误。
4. 统一调用层的进阶实现与优化
当各个适配器就位后,我们需要一个“调度器”或“工厂”来管理它们,这就是LLMClient类。
4.1 客户端工厂与动态适配器加载
LLMClient的核心职责是根据配置或请求,选择正确的适配器实例。
class LLMClient: def __init__(self): self._adapters: Dict[str, BaseLLMAdapter] = {} self._default_provider = os.getenv("DEFAULT_LLM_PROVIDER", "openai") def register_adapter(self, provider: str, adapter: BaseLLMAdapter): self._adapters[provider] = adapter def get_adapter(self, request: UnifiedLLMRequest) -> BaseLLMAdapter: # 策略1: 从 model 字符串推断 provider (如 “gpt-” -> “openai”) provider = self._infer_provider_from_model(request.model) # 策略2: 如果推断不出,使用默认 provider if not provider: provider = self._default_provider # 策略3: 允许请求通过 provider_kwargs 强制指定 force_provider = request.provider_kwargs.pop("force_provider", None) if force_provider and force_provider in self._adapters: provider = force_provider adapter = self._adapters.get(provider) if not adapter: raise ValueError(f"No adapter registered for provider: {provider}") return adapter async def complete(self, request: UnifiedLLMRequest) -> UnifiedLLMResponse: adapter = self.get_adapter(request) return await adapter.complete(request) def _infer_provider_from_model(self, model: str) -> Optional[str]: model_lower = model.lower() if model_lower.startswith("gpt-") or model_lower.startswith("ft:"): return "openai" elif model_lower.startswith("claude-"): return "anthropic" elif model_lower.startswith("gemini-"): return "google" elif "llama" in model_lower or "mistral" in model_lower: # 示例规则 return "openai_compatible" # 指向一个兼容 OpenAI 协议的适配器 return None设计优势:
- 松耦合:业务代码只依赖
LLMClient和UnifiedLLMRequest。 - 灵活的策略:提供商推断逻辑可配置、可扩展。新增一个模型系列,只需更新
_infer_provider_from_model方法或配置映射表。 - 适配器注册机制:方便进行单元测试时注入 Mock 适配器。
4.2 关键共性功能的抽象:重试、限流与日志
不同的 API 提供商都可能遇到网络抖动、速率限制(Rate Limit)等问题。我们应该在抽象层之上,实现一套通用的中间件机制来处理这些横切关注点。
1. 重试机制(Retry with Backoff)所有网络调用都可能失败。一个健壮的重试策略应包括:
- 指数退避:每次重试等待时间递增(如 1s, 2s, 4s, 8s),避免加重服务器压力。
- 抖动(Jitter):在退避时间上加一个随机值,防止大量客户端同时重试形成“惊群效应”。
- 选择性重试:只对特定错误重试(如网络超时、5xx 服务器错误、429 速率限制),而不对 4xx 客户端错误(如无效 API Key)重试。
我们可以使用tenacity或backoff库,或者自己实现一个装饰器,包装adapter.complete方法。
2. 速率限制(Rate Limiting)即使每个适配器独立处理其提供商的限流,一个全局的客户端级限流也有价值,防止应用自身线程或进程过多导致本地超限。可以使用asyncio.Semaphore或redis配合令牌桶算法实现分布式限流。
3. 结构化日志与监控每次调用都应记录结构化日志,至少包括:时间戳、提供商、模型、请求 Token 数(估算)、响应 Token 数、耗时、成功/失败状态。这有助于:
- 成本分析:计算各模型的使用成本和性价比。
- 性能监控:发现响应时间变慢的模型或提供商。
- 故障排查:快速定位错误是普遍性的还是针对特定模型的。
# 一个集成了重试、日志的装饰器示例 def with_retry_and_log(original_func): @wraps(original_func) async def wrapper(adapter, request: UnifiedLLMRequest, *args, **kwargs): start_time = time.time() provider = adapter.provider_name model = request.model @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10)) async def _call_with_retry(): return await original_func(adapter, request, *args, **kwargs) try: logger.info(f"LLM调用开始", provider=provider, model=model, req_id=request.id) response = await _call_with_retry() elapsed = time.time() - start_time status = "success" if response.success else "failure" logger.info(f"LLM调用结束", provider=provider, model=model, status=status, duration_ms=round(elapsed*1000, 2), req_id=request.id) return response except Exception as e: elapsed = time.time() - start_time logger.error(f"LLM调用异常", provider=provider, model=model, error=str(e), duration_ms=round(elapsed*1000, 2), req_id=request.id, exc_info=True) return UnifiedLLMResponse(success=False, error_message=f"调用异常: {e}") return wrapper5. 实践中遇到的典型问题与解决方案
在开发和测试这套统一调用库的过程中,我遇到了不少“坑”。这里记录下最典型的几个问题及其解决方法。
5.1 问题一:上下文长度(Context Window)管理混乱
现象:同一个应用,调用 GPT-4(128K 上下文)正常,切换到 Claude 3 Sonnet(200K 上下文)也正常,但切换到某个仅支持 4K 上下文的开源模型时,频繁报错“上下文长度超限”。
根因分析:抽象层只做了参数格式转换,但没有对输入内容的长度进行统一检查和适配。不同模型的最大上下文长度(Max Context Window)和有效提示长度(Effective Prompt Length,即总长度减去为回复预留的长度)差异巨大。
解决方案:
- 建立模型规格元数据:创建一个配置文件或数据库表,记录每个
model字符串对应的最大上下文长度(max_context_tokens)。这个数据可以从厂商文档获取,或通过简单的探测请求(如发送一个长提示看是否报错)来验证。 - 在适配器或统一层进行长度检查:在发送请求前,估算本次请求 messages 的 Token 总数(使用各厂商推荐的 Tokenizer,如
tiktokenfor OpenAI,anthropic库自带的 for Claude)。如果估算值超过max_context_tokens - safety_margin(安全边际,如预留 500 Token 给回复),则提前失败或触发处理策略。 - 实现智能裁剪策略:对于超长的对话历史,实现一个“摘要”或“滑动窗口”策略。例如,保留最新的 N 轮对话,或将最早的消息进行摘要压缩。这个功能可以作为一个可插拔的“预处理中间件”集成到
LLMClient中。
注意:Token 估算本身有开销。在生产环境中,可以考虑缓存估算结果,或对非常长的文本进行采样估算。
5.2 问题二:流式响应(Streaming)处理不一致
现象:在实现一个实时聊天功能时,OpenAI 的流式响应能正常逐字输出,但切换到 Claude 后,前端接收到的数据块格式解析失败。
根因分析:虽然抽象层定义了stream: bool参数,但各个适配器返回的流式数据格式(Server-Sent Events 的分块结构、JSON 字段名)完全不同。前端或流式处理逻辑如果依赖了某个厂商的特定格式,就会出错。
解决方案:
- 定义统一的流式响应协议:不在适配器层返回原始的、厂商特定的流对象。而是让每个适配器在内部处理流,并将其转换为一个统一的、简单的数据格式。例如,定义一个异步生成器,每次
yield一个包含text_delta(本次新增文本)和is_finished(是否结束)的字典。 - 客户端处理统一格式:业务代码或前端只处理这种统一的流格式。这样,切换模型时,流式处理逻辑完全无需改动。
- 错误流的统一:流式传输中也可能发生错误。统一协议中也需要包含错误信息字段,以便在流中途能通知客户端。
# 统一的流式响应协议示例 (在适配器内部转换) async def complete_stream(self, request: UnifiedLLMRequest) -> AsyncGenerator[Dict[str, Any], None]: """返回统一的流式响应字典。""" raw_stream = await self._get_raw_stream(request) # 获取厂商原生流 async for chunk in raw_stream: # 将 chunk 解析为统一的格式 delta, finished, error = self._parse_chunk(chunk) if error: yield {"type": "error", "message": error} break if delta: yield {"type": "delta", "content": delta} if finished: yield {"type": "finished"} break5.3 问题三:成本与延迟的监控盲区
现象:某天发现账单异常增高,排查很久才发现是某个非关键后台任务错误地调用了最昂贵的模型(如 GPT-4 Turbo),而不是预设的廉价模型(如 GPT-3.5 Turbo)。
根因分析:抽象层隐藏了模型细节,但也让成本监控变得困难。如果没有在每次调用时记录详细的元数据(提供商、模型、Token 用量),就无法进行细粒度的成本分析和审计。
解决方案:
- 强制日志记录:如前文所述,在统一调用层强制记录包含
provider,model,prompt_tokens,completion_tokens,total_tokens,duration_ms的结构化日志。 - 实时成本估算:根据日志中的 Token 数量和已知的模型单价(可配置),实时估算每次调用的成本,并累计到应用或用户维度。这能帮助快速发现异常调用模式。
- 集成监控告警:将上述日志和指标发送到监控系统(如 Prometheus + Grafana),并设置告警规则。例如:“过去5分钟内,模型
claude-3-opus的调用成本超过100元”或“平均响应时间超过10秒”。 - 在适配器内部实现成本控制:可以为适配器设置预算上限,当某个模型或用户的累计成本超过阈值时,自动降级到更便宜的模型或直接拒绝请求。
5.4 问题四:模型能力差异导致的输出质量波动
现象:针对同一个精心设计的提示词(Prompt),不同模型的输出质量、风格和遵循指令的程度差异很大。切换模型后,应用的整体效果可能下降。
根因分析:这是本质问题,无法通过技术抽象完全解决。不同的模型在逻辑推理、创造性、指令遵循、格式输出等方面能力不同。
缓解策略:
- 提示词工程(Prompt Engineering)适配:虽然我们追求统一的请求格式,但针对不同模型微调提示词是必要的。可以通过
provider_kwargs传递一些模型特定的提示词片段,或者在适配器内部根据模型类型对基础提示词进行微调(例如,为 Claude 添加更详细的思考步骤要求,为 Gemini 明确结构化输出格式)。 - 模型能力矩阵:建立内部文档,记录各模型在“代码生成”、“逻辑推理”、“创意写作”、“结构化输出”等维度上的表现评级。在业务逻辑选择模型时,可以参考这个矩阵。
- A/B 测试与自动路由:对于关键任务,可以实现一个“路由层”。该层同时向多个模型(或同一模型的不同版本)发送请求,根据响应时间、成本、以及通过一些简单校验器(Validator)评估的输出质量,选择最佳结果返回,或用于收集数据以持续优化模型选择策略。
6. 项目总结与未来演进思考
构建这个统一 LLM API 调用库的过程,是一个从“简单封装”到“深度抽象”的认识升级。它不仅仅是为了少写几行if-else,更是为了在快速变化的 LLM 生态中,为应用程序建立一个稳定、可观测、可扩展的基石。
回顾整个过程,我认为以下几个决策至关重要:
- 定义了稳定、可扩展的统一数据模型:
UnifiedLLMRequest和UnifiedLLMResponse是系统的核心契约。provider_kwargs这个“后门”设计在保持接口简洁的同时,提供了应对厂商差异的灵活性。 - 适配器模式(Adapter Pattern)的彻底应用:每个适配器专心处理与单一厂商 API 的对话细节,职责单一,易于测试和维护。新增一个模型提供商,只需要增加一个新的适配器类,并通过工厂注册即可,符合开闭原则。
- 在抽象层之上实现共性功能:重试、限流、日志、监控、Token 估算、提示词裁剪等,这些是所有 LLM 调用都需要的功能。将它们实现在适配器之上的统一层,避免了代码重复,也确保了行为的一致性。
- 重视可观测性(Observability):从第一天就加入详细的、结构化的日志和指标收集,这对后续的问题排查、成本优化和性能调优产生了巨大价值。
这个项目目前已经稳定支撑了多个内部应用。随着 LLM 技术的持续演进,我计划在以下几个方面进行扩展:
- 工具调用(Function Calling/Tool Use)的统一抽象:目前各家的工具调用格式正在趋同(类似 OpenAI 的格式),但仍需一个统一的抽象来定义工具、解析模型返回的工具调用请求、执行工具并返回结果。
- 异步批处理优化:对于非实时任务,将多个独立请求批量发送给支持批处理的 API(如 OpenAI 的 Batch API),可以显著降低成本和提高吞吐量。需要在客户端层面实现请求队列和批量发送逻辑。
- 向量数据库与上下文管理的集成:对于需要超长上下文或知识库检索的应用(RAG),可以将向量数据库的检索、上下文组装等逻辑也封装成可插拔的模块,与 LLM 调用层无缝集成。
最后,一个最实用的建议是:不要过度设计。初期可以只抽象最核心的聊天补全(Chat Completion)功能,快速跑通流程。在遇到具体的、重复的痛点时(比如第二个模型接入时的参数转换麻烦),再着手进行抽象和重构。让代码的演进驱动架构的完善,这样构建出来的系统才是最贴合实际需求的。
