构建模型无关的 AI Agent
chat() + normalize_tools() + normalize_messages() 接口,内部统一使用 OpenAI 工具格式——其他都是实现细节。成本差距可达 20 倍。
大多数 AI Agent 框架在诞生之初就绑定了特定的模型供应商。LangChain 最初围绕 OpenAI 构建,Claude Code 自然是 Anthropic 专属。但实际使用中,你经常需要在不同模型之间切换——因为成本、延迟、能力匹配,或者单纯不想被锁定。
本文展示如何构建一个能与任何模型配合工作的 Agent——Claude、GPT、DeepSeek、Llama 或本地部署的开源模型——只需修改一行配置。
为什么模型无关很重要
| 场景 | 你需要 | 锁定问题 |
|---|---|---|
| 生产部署 | GPT-4o 处理复杂任务,Claude 写长文 | 代码中硬编码了 OpenAI SDK |
| 成本优化 | DeepSeek 处理简单查询(便宜 10 倍),GPT 处理难题 | 工具定义只兼容一种格式 |
| 隐私敏感数据 | 本地 Llama 3 处理内部文档,云端 API 处理公开任务 | 不同消息格式破坏 pipeline |
| 模型评估 | 在同一个 Agent 任务上 A/B 测试 3 个模型 | 不修改代码无法切换模型 |
什么是模型无关架构
模型无关(Model-Agnostic)意味着你的 Agent 核心逻辑不依赖任何特定模型的 API 格式。Agent 循环——观察 → 思考 → 行动 → 观察——无论哪个模型驱动「思考」步骤,都完全一样。
架构分为三层:
- Agent 核心——ReAct 循环、工具执行、记忆管理(与模型无关)
- 适配器层——将核心的统一格式转换为每个模型的具体 API
- 模型提供商——Claude、GPT、DeepSeek、Llama 等(可替换)
┌─────────────────────────────────┐
│ Agent 核心循环 │ ← 永不改变
│ 观察 → 思考 → 行动 → 观察 │
└──────────────┬──────────────────┘
│ 统一接口
┌──────────────▼──────────────────┐
│ 适配器层 │ ← 按模型切换
│ ┌────────┐ ┌──────┐ ┌───────┐ │
│ │Claude │ │ GPT │ │DeepSk │ │
│ │适配器 │ │适配器│ │适配器 │ │
│ └───┬────┘ └──┬───┘ └───┬───┘ │
└──────┼─────────┼─────────┼──────┘
│ │ │
Anthropic OpenAI DeepSeek API
适配器接口
每个适配器实现相同的接口。以下是契约:
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
@dataclass
class AgentResponse:
"""统一响应格式,来自任何模型。"""
content: str | None # 最终文本答案(is_final=True 时)
tool_call: dict | None # 工具调用请求(is_final=False 时)
is_final: bool # True=完成, False=需要调用工具
usage: dict # Token 用量: {"input": N, "output": M}
class ModelAdapter(ABC):
"""每个模型适配器必须实现此接口。"""
@abstractmethod
def chat(self, messages: list[dict],
tools: list[dict] | None = None,
temperature: float = 0.7,
max_tokens: int = 1000) -> AgentResponse:
"""发送消息 + 工具 → 接收响应或工具调用。"""
...
@abstractmethod
def normalize_tools(self, tools: list[dict]) -> list[dict]:
"""将统一工具 schema 转换为模型特定格式。"""
...
@abstractmethod
def normalize_messages(self, messages: list[dict]) -> list[dict]:
"""将统一消息格式转换为模型特定格式。"""
...
实现真正的适配器
以下是三种最常见模型家族的具体实现。注意每种模型处理工具调用的方式有何不同。
OpenAI 适配器(GPT-4o, GPT-4, GPT-3.5)
from openai import OpenAI
class OpenAIAdapter(ModelAdapter):
def __init__(self, model="gpt-4o", api_key=None, base_url=None):
self.client = OpenAI(api_key=api_key, base_url=base_url)
self.model = model
def normalize_tools(self, tools):
# OpenAI 使用标准 function-calling 格式——改动最小
return [{"type": "function", "function": t} for t in tools]
def normalize_messages(self, messages):
# OpenAI 格式是基线;工具结果使用 role "tool"
return messages # 已经是正确格式
def chat(self, messages, tools=None, temperature=0.7, max_tokens=1000):
kwargs = dict(
model=self.model,
messages=self.normalize_messages(messages),
temperature=temperature,
max_tokens=max_tokens
)
if tools:
kwargs["tools"] = self.normalize_tools(tools)
resp = self.client.chat.completions.create(**kwargs)
msg = resp.choices[0].message
return AgentResponse(
content=msg.content,
tool_call={
"name": msg.tool_calls[0].function.name,
"arguments": msg.tool_calls[0].function.arguments
} if msg.tool_calls else None,
is_final=msg.tool_calls is None,
usage={
"input": resp.usage.prompt_tokens,
"output": resp.usage.completion_tokens
}
)
Anthropic 适配器(Claude Sonnet, Claude Opus)
import anthropic
class AnthropicAdapter(ModelAdapter):
def __init__(self, model="claude-sonnet-4-20250514", api_key=None):
self.client = anthropic.Anthropic(api_key=api_key)
self.model = model
def normalize_tools(self, tools):
# Anthropic 使用不同的工具格式——没有 "type":"function" 包装
normalized = []
for tool in tools:
inner = tool.get("function", tool) # 如果嵌套则解包
normalized.append({
"name": inner["name"],
"description": inner.get("description", ""),
"input_schema": inner.get("parameters",
{"type": "object", "properties": {}})
})
return normalized
def normalize_messages(self, messages):
# Anthropic 需要将 system prompt 提取为单独参数
normalized = []
for msg in messages:
if msg["role"] == "system":
continue # 单独处理
if msg["role"] == "tool":
# Anthropic 在 user 消息中使用 "tool_result" 块
normalized.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": msg.get("tool_call_id", "unknown"),
"content": msg["content"]
}]
})
else:
normalized.append({"role": msg["role"],
"content": msg["content"]})
return normalized
def chat(self, messages, tools=None, temperature=0.7, max_tokens=1000):
system = next((m["content"] for m in messages
if m["role"] == "system"), None)
normalized_msgs = self.normalize_messages(messages)
kwargs = dict(
model=self.model,
messages=normalized_msgs,
max_tokens=max_tokens,
temperature=temperature
)
if system:
kwargs["system"] = system
if tools:
kwargs["tools"] = self.normalize_tools(tools)
resp = self.client.messages.create(**kwargs)
# 从响应中提取 tool use 块
tool_calls = [
block for block in resp.content
if block.type == "tool_use"
]
return AgentResponse(
content=resp.content[0].text if resp.content[0].type == "text"
else None,
tool_call={
"name": tool_calls[0].name,
"arguments": tool_calls[0].input,
"id": tool_calls[0].id
} if tool_calls else None,
is_final=len(tool_calls) == 0,
usage={
"input": resp.usage.input_tokens,
"output": resp.usage.output_tokens
}
)
OpenAI 兼容适配器(DeepSeek, vLLM, Ollama, 本地模型)
许多模型(DeepSeek、通过 vLLM/Ollama 运行的 Llama、Groq)使用 OpenAI 兼容 API。一个适配器覆盖所有——只需修改 base_url:
# DeepSeek——简单任务便宜 10 倍,中文表现优秀
agent = ModelAgnosticAgent(
OpenAIAdapter(
model="deepseek-chat",
api_key="sk-xxx",
base_url="https://api.deepseek.com/v1"
),
tools, prompt
)
# 本地 Llama 3 via Ollama——零成本,完全隐私
agent = ModelAgnosticAgent(
OpenAIAdapter(
model="llama3:70b",
api_key="ollama", # Ollama 忽略 key
base_url="http://localhost:11434/v1"
),
tools, prompt
)
# Groq——最快推理速度,适合实时场景
agent = ModelAgnosticAgent(
OpenAIAdapter(
model="llama-3.1-70b-versatile",
api_key="gsk_xxx",
base_url="https://api.groq.com/openai/v1"
),
tools, prompt
)
工具格式归一化
不同提供商的工具 schema 略有差异。核心洞察:以 OpenAI 的 function-calling 格式作为内部标准,让每个适配器转换为其原生格式。
| 特性 | OpenAI | Anthropic | Google Gemini |
|---|---|---|---|
| 工具包装 | {"type":"function","function":{...}} | 裸对象,无包装 | {"functionDeclarations":[...]} |
| Schema 字段 | parameters(JSON Schema) | input_schema(JSON Schema) | parameters(类 OpenAPI) |
| 工具结果 role | role: "tool" | tool_result 内容块 | role: "tool" |
| 并行调用 | 原生支持 | 原生支持 | 不支持 |
{"type":"object","properties":{...},"required":[...]} 定义工具参数。这是所有主流提供商支持且转换成本最低的格式。避免使用特定提供商的 schema 特性。模型选择策略
拥有模型无关架构后,你可以根据特征将任务路由到最优模型:
| 任务类型 | 推荐模型 | 原因 |
|---|---|---|
| 复杂推理、数学、代码 | Claude Opus / GPT-4o | 最高推理准确度 |
| 简单问答、摘要 | DeepSeek / Llama 3 70B | 便宜 5-10 倍,效果够用 |
| 长文写作 | Claude Sonnet | 文笔质量优秀 |
| 中文内容 | DeepSeek / Qwen | 中文原生性能 |
| 敏感内部数据 | 本地 Llama / Qwen | 数据不出你的基础设施 |
| 实时场景(< 500ms) | Groq / GPT-4o-mini | 超低延迟 |
def smart_route(task: str) -> ModelAdapter:
"""根据启发式规则将任务路由到最佳模型。"""
if any(kw in task.lower() for kw in ["code", "debug", "math", "logic"]):
return AnthropicAdapter(model="claude-opus-4-20250514")
if any(kw in task.lower() for kw in ["中文", "chinese", "翻译"]):
return OpenAIAdapter(model="deepseek-chat",
base_url="https://api.deepseek.com/v1")
if len(task) < 100: # 简单短查询
return OpenAIAdapter(model="gpt-4o-mini")
return AnthropicAdapter(model="claude-sonnet-4-20250514") # 默认
完整 Agent 实现
以下是完整的模型无关 Agent。核心循环永不改变——只有适配器会变:
class ModelAgnosticAgent:
def __init__(self, model: ModelAdapter, tools: list[dict],
system_prompt: str):
self.model = model
self.tools = tools
self.messages = [{"role": "system", "content": system_prompt}]
self.total_cost = 0.0
def run(self, user_input: str, max_turns: int = 20) -> str:
self.messages.append({"role": "user", "content": user_input})
turns = 0
while turns < max_turns:
response = self.model.chat(
self.messages, self.tools,
temperature=0.7 if turns == 0 else 0.4 # 逐步降温
)
turns += 1
if response.is_final:
return response.content
# 执行工具并将结果反馈
tool_name = response.tool_call["name"]
tool_args = response.tool_call.get("arguments", {})
result = self._execute_tool(tool_name, tool_args)
self.messages.append({
"role": "assistant",
"content": None,
"tool_calls": [{
"id": f"call_{turns}",
"type": "function",
"function": {
"name": tool_name,
"arguments": json.dumps(tool_args)
if isinstance(tool_args, dict)
else tool_args
}
}]
})
self.messages.append({
"role": "tool",
"tool_call_id": f"call_{turns}",
"content": json.dumps(result)
})
return "已达最大轮次,任务未完成。"
测试模型无关层
如何知道适配器工作正常?用同一个工具调用任务跨所有模型测试:
def test_adapter(adapter: ModelAdapter):
"""验证适配器是否正确处理工具调用。"""
tools = [{
"name": "get_weather",
"description": "获取指定城市的当前天气。",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"}
},
"required": ["city"]
}
}]
messages = [
{"role": "system", "content": "你是一个有用的助手。"},
{"role": "user", "content": "东京今天天气怎么样?"}
]
response = adapter.chat(messages, tools)
assert not response.is_final, "应该请求工具调用"
assert response.tool_call["name"] == "get_weather", \
f"调了错误的工具: {response.tool_call['name']}"
assert "东京" in str(response.tool_call.get("arguments", "")), \
"缺少城市参数"
# 对所有适配器运行测试
for name, adapter in [
("OpenAI", OpenAIAdapter()),
("Anthropic", AnthropicAdapter()),
("DeepSeek", OpenAIAdapter(base_url="https://api.deepseek.com/v1"))
]:
try:
test_adapter(adapter)
print(f"✅ {name}: 通过")
except Exception as e:
print(f"❌ {name}: 失败 — {e}")
现有框架对比(以及何时自己造轮子)
| 框架 | 模型支持 | 最适合 | 何时跳过 |
|---|---|---|---|
| smolagents(HuggingFace) | 任何 HF 模型 + 外部 API | 快速原型、HF 生态用户 | 需要对工具循环精细控制 |
| DSPy | 10+ 提供商,通过适配器 | 提示词优化、模型 A/B 测试 | 简单工具调用 Agent(杀鸡用牛刀) |
| LangChain | 广泛但历史上 OpenAI 优先 | 复杂 RAG pipeline、大量集成 | 追求简洁;LangChain 增加抽象开销 |
| 自定义适配器(本文) | 任何模型,完全控制 | 生产系统、特定需求 | 你只用一个模型 |
核心要点
- 定义统一的适配器接口——一个
chat()方法,一个normalize_tools(),一个normalize_messages()。其他都是实现细节。 - 内部统一使用 OpenAI 工具格式——这是事实标准,大多数提供商都实现或可以转换为此格式。
- 用同一个工具调用场景测试每个适配器——工具格式转换 bug 是沉默的,在生产环境难以调试。
- 智能路由省钱——简单查询用便宜模型(DeepSeek 约 $0.14/百万 token),复杂推理用高级模型(Claude 约 $3/百万 token)。成本差距可达 20 倍。
- 不要过度设计——如果你只用一个模型,跳过这一切。但如果你在构建能持久的东西,适配器模式是防止厂商锁定的廉价保险。
常见问题
- Q: 我真的需要模型无关架构吗?
- A: 如果你只用一个模型且没有切换计划——不需要。但如果你在构建产品、平台、或任何可能比当前模型选择更长寿的东西——适配器模式在第一次需要切换时就会回本。它是防止厂商锁定的廉价保险。
- Q: 不同模型的工具调用格式有什么区别?
- A: OpenAI 用
{"type":"function","function":{...}}包装,Anthropic 用裸对象 +input_schema字段,Gemini 用functionDeclarations数组。本文统一以 OpenAI 格式为内部标准,各适配器自行转换。 - Q: 为什么内部统一使用 OpenAI 格式?
- A: 因为 OpenAI function-calling 格式已成为事实标准。DeepSeek、Groq、vLLM、Ollama 等都使用 OpenAI 兼容 API。用这个格式作为内部表示,转换成本最低。
- Q: 智能路由怎么省钱?
- A: 简单查询(<100 字)用 DeepSeek(约 $0.14/百万 token),复杂推理用 Claude(约 $3/百万 token)。成本差距可达 20 倍。本文提供了
smart_route()函数的参考实现。 - Q: 适配器模式有什么坑?
- A: 工具格式转换的 bug 是沉默的——生产环境难以发现。本文建议用同一个工具调用场景(如「查询天气」)测试每个适配器。另外,Anthropic 需要把 system prompt 提取为单独参数,这个细节容易被忽略。
可引用定义
模型无关 Agent(Model-Agnostic Agent):一种 AI Agent 架构模式,其核心决策循环(观察→思考→行动→观察)与任何特定大语言模型提供商的 API 格式解耦。通过适配器模式(Adapter Pattern),定义一个统一的接口契约——通常包括 chat()(发送消息并获取响应)、normalize_tools()(工具定义格式归一化)和 normalize_messages()(消息格式归一化)——每个模型提供商(OpenAI、Anthropic、DeepSeek、本地 Llama 等)各自实现该接口。Agent 核心逻辑始终操作统一格式,切换模型只需替换适配器实例,无需修改任何业务代码。这是防止厂商锁定的低成本保险策略。