题目 1:为什么直接在 Agent Engine 中 import OpenAI SDK 是一种强耦合?
假设 AgentEngine.run() 内部直接创建 OpenAI Client、发请求、读取原始 Response 字段。请说明未来切 Claude 或本地模型时,为什么核心 Agent 逻辑会被迫修改,并指出 Provider 抽象可以把哪些变化挡在外面。查看参考答案
因为 Agent Engine 已经依赖具体 SDK 的客户端创建方式、请求参数、返回对象结构和异常类型。换 Provider 时,这些细节都会变化,从而迫使 Engine 修改。Provider 抽象把“怎么请求具体模型、怎么解析具体响应、怎么处理具体异常”放到适配层,上层只依赖统一的
chat()和ProviderResponse。这样模型变化只影响 Provider,不影响 Main Loop、State 和 Agent 业务逻辑。
题目 2:判断题——为了统一接口,应该把所有厂商的所有参数都塞进 chat(),对吗?
请判断,并解释什么叫“统一公共能力,而不是强行统一所有差异”。
不对。如果把 temperature、top_p、thinking、cache、response_format、供应商专属开关全部塞进一个方法,接口会越来越臃肿,而且很多参数只对部分 Provider 有意义。更合理的是统一 Agent 真正依赖的公共能力,比如 messages、tools、标准响应和标准异常;Provider 专属配置放在初始化参数、Config 或内部实现中。抽象不是把所有差异消灭,而是让不相关差异不要污染上层。
题目 3:代码阅读——下面代码为什么说明抽象已经泄漏?
response = provider.chat(messages)
if provider.name == "openai":
text = response.output_text
elif provider.name == "anthropic":
text = response.content[0].text
请说明问题,并给出正确方向。
这段代码虽然表面上用了
provider.chat(),但 Agent 上层仍然知道不同 Provider 的原始响应格式,因此并没有真正解耦。正确方向是由每个 Provider 自己把原始响应转换成统一ProviderResponse,例如统一提供response.text。Agent Engine 永远只读取标准字段,不判断 Provider 类型。否则以后每增加一家模型,所有上层调用点都要继续增加分支。
题目 4:编程实现——手写一个可切换 Provider 的最小程序
请用 ABC 定义 LLMProvider,实现两个 Fake Provider,并写一个 AgentEngine。要求切换 Provider 时不能修改 AgentEngine 的代码。
#下面是一种完整实现:
from abc import ABC, abstractmethod
class LLMProvider(ABC):
@abstractmethod
def chat(self, messages):
raise NotImplementedError
class ProviderA(LLMProvider):
def chat(self, messages):
return "Provider A"
class ProviderB(LLMProvider):
def chat(self, messages):
return "Provider B"
class AgentEngine:
def __init__(self, provider: LLMProvider):
self.provider = provider
def run(self, message: str) -> str:
messages = [
{
"role": "user",
"content": message,
}
]
return self.provider.chat(messages)
if __name__ == "__main__":
engine_a = AgentEngine(
ProviderA()
)
engine_b = AgentEngine(
ProviderB()
)
print(
engine_a.run("hello")
)
print(
engine_b.run("hello")
)
这里
AgentEngine只依赖LLMProvider,所以新增 Provider 时不需要改 Engine。真正项目会再统一 Response、Error、Tool Call 等结构。
题目 5:综合应用——为什么测试环境特别需要 FakeProvider?
如果每次单元测试 Agent State、Main Loop、超时逻辑都调用真实云模型,会有哪些问题?FakeProvider 可以解决什么?
真实模型调用存在成本、网络波动、限流、响应概率性和执行速度问题,测试结果可能不稳定。FakeProvider 可以固定返回某个响应或 Tool Call,让我们精确测试 Main Loop、State 流转、错误恢复等逻辑。例如第一轮固定返回 Tool Call,第二轮固定返回最终答案。这样测试不依赖外部服务,也不会烧 API 成本。Provider 抽象使 FakeProvider 和真实 Provider 使用同一个接口,因此测试替换非常自然。
题目 6:对比分析——Provider 和 Agent Engine 的职责分别是什么?
请至少从“模型通信、任务决策、状态管理、工具执行、异常适配”五个方面对比。
Provider 主要负责模型通信,包括把内部 messages 转成具体厂商请求、调用 API、把原始响应转换成统一 ProviderResponse,并把底层异常映射成统一 ProviderError。Agent Engine 则负责任务级控制:推进 Main Loop、读取和更新 Agent State、判断是否继续、是否完成。工具真正执行属于 Tool Executor / Harness,不应该塞进 Provider。Provider 是模型适配层,Engine 是 Agent 控制层,两者职责清楚后才容易扩展。
题目 7:概念理解——为什么 Provider 抽象是 Fallback 和 Model Router 的前提?
请解释如果没有统一 Provider Interface,做“OpenAI 失败切 Claude,Claude 失败切本地”会出现什么问题;统一后为什么简单很多。
如果没有统一接口,每个 Provider 的调用方式、异常类型、响应结构都不同,Fallback 逻辑会充满多层 if/else 和 try/except,而且 Agent Engine 需要理解所有厂商细节。统一 Provider Interface 后,FallbackProvider 只需要按顺序调用每个
provider.chat(),捕获统一 ProviderError,并返回统一 ProviderResponse。Agent Engine 甚至不知道发生过切换。这说明抽象真正的价值,是把变化集中到一个明确边界内。
进一步阅读
- OpenAI Responses API / Python SDK 文档:观察模型调用、Tool 与 Response 的官方接口形式,重点理解它和我们内部 ProviderResponse 的边界。
- Anthropic Messages API / Python SDK 文档:对比不同厂商的消息与内容块设计,理解为什么需要适配层。
- Ollama API 文档:适合理解本地模型如何通过 HTTP 服务接入 Provider。
- Python
abc模块官方文档:学习如何通过 ABC 和 abstractmethod 定义明确接口。 - Dependency Inversion Principle 相关资料:理解“高层模块依赖抽象,不依赖具体实现”的设计思想。
- Adapter Pattern 相关资料:Provider 本质上就是一种适配器,把外部模型 API 适配成内部统一协议。
- Strategy Pattern 相关资料:切换不同 Provider 也可以从策略模式角度理解。
- Resilience / Fallback 设计资料:为后续 Provider Retry、Fallback、Model Router 做准备。
术语表
- 模型提供器(LLM Provider):Agent 系统中负责和具体模型服务通信的适配层。它把内部统一消息转换成厂商请求,再把厂商响应转换成 Agent 能理解的统一结构。
- 提供器接口(Provider Interface):Agent Engine 面向的抽象协议,例如统一的
chat(messages, tools=None)。Engine 依赖接口而不是具体 SDK,从而降低模型切换成本。 - 适配器(Adapter):把一个已有接口转换成系统期望接口的设计模式。OpenAIProvider、ClaudeProvider、LocalModelProvider 本质上都承担 Adapter 的职责。
- 依赖反转(Dependency Inversion):高层业务模块不直接依赖底层具体实现,而是双方都依赖抽象。在本章中表现为 Agent Engine 依赖 LLMProvider,而不是直接依赖 OpenAI SDK。
- 统一响应(Provider Response):系统内部定义的标准模型响应结构,例如统一提供 text、tool_calls、usage、finish_reason。它避免具体厂商 Response 泄漏到 Agent Engine。
- 能力描述(Capability):表示某个 Provider 或模型是否支持 Tool Calling、Vision、Structured Output 等能力。公共接口可以统一,但实际能力差异仍需要被显式管理。
- 回退(Fallback):某个 Provider 调用失败时切换到另一个 Provider 的容错策略。统一 Provider Interface 能让 Fallback 不污染 Agent Engine。
- 模型路由(Model Routing):根据任务复杂度、成本、安全要求或延迟,把请求分配到不同模型的策略。Provider 抽象是实现路由的重要基础。
本章总结
这一节我们把 Agent 最重要的一条工程边界建立起来了,Agent Engine 不应该知道底层具体模型是谁。
前面我们已经有 Main Loop 和 State。
如果这时直接把具体模型 SDK 写进 Engine,整个 Agent 很快会被厂商细节绑住。模型一换,请求参数要改;响应格式一换,解析逻辑要改;异常类型一换,错误处理也要改。
所以我们引入:
class LLMProvider:
def chat(
self,
messages,
tools=None,
):
...
然后让具体实现去适配:
OpenAIProvider
ClaudeProvider
LocalModelProvider
最终依赖关系变成:
Agent Engine
↓
Provider Interface
↓
不同 LLM
这带来的价值不仅是“切模型方便”。
更重要的是,Agent Engine 可以始终专注于自己的职责:
推进 Main Loop
管理 State
决定什么时候继续
决定什么时候完成
而 Provider 专注于:
翻译请求
调用模型
翻译响应
统一异常
我们还专门强调了一个容易踩的坑,抽象不是把所有厂商差异硬揉成一个巨型接口。
真正值得统一的是 Agent 依赖的公共能力,例如:
messages
tools
ProviderResponse
ProviderError
至于某个模型特有的配置,应该留在 Provider 内部或配置对象里。
如果你想验证自己是否真正掌握这一节,可以做一个非常直接的实验。
写一个:
AgentEngine
再写三个:
FakeOpenAIProvider
FakeClaudeProvider
FakeLocalModelProvider
然后依次切换。
如果:
AgentEngine
一行都不需要改,而且最终都能通过统一:
provider.chat(...)
工作,那么最核心的 Provider 解耦已经实现了。
到这里,第1部分“真正理解 Agent”就完成了最基础的四块:
为什么需要 Agent
↓
Main Loop
↓
Agent State
↓
LLM Provider
下一部分我们要正式让 Agent 长出“手脚”。
第一节就是,Tool Calling 到底是怎么回事?
从下一节开始,模型不再只是输出文字,而会第一次真正产生可执行动作。我们会把“模型想调用工具”和“Python 真正执行函数”之间的完整链路彻底拆开。
















请登录后查看评论内容