章末自测

题目 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 真正执行函数”之间的完整链路彻底拆开。

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像-大赛人网
欢迎您留下宝贵的见解!
提交
头像-大赛人网

昵称

取消
昵称

    请登录后查看评论内容