200字
Agent学习-Phase 1:模型调用层
2026-04-24
2026-04-25

前置说明:这是一个我自己学习Agent Harness开发的一个笔记,上传到这里纯属方便,这个项目叫Odd, 是通过分多个Phase来完成一个能够支持Tools MCP Skill 子agent 任务清单和上下文压缩的Agent框架。我会分多个Phase让AI完成代码,并阅读代码学习

至于为什么用Ado当头图,那当然是因为Ado统治世界

仓库在:https://github.com/GC-SHIRO/Odd.git

模型调用层 = Agent 的"嘴巴和耳朵"。它负责把 Agent 想说的话发给 AI 模型,再把 AI 模型的回复传回来。


为什么要单独搞一层?

想象你要打电话,但手上有两部手机:一部是 iPhone,一部是 Android。你不可能每次打电话都换一套操作方式吧?

模型调用层做的就是这件事——不管后面接的是 OpenAI 的 GPT,还是 Anthropic 的 Claude,Agent 都只用同一套方式"打电话"。后面换模型?改一行配置就行,Agent 的核心逻辑完全不用动。

这叫抽象,是写代码很重要的思想。


核心零件

1. 数据类型(odd/types.py

聊天本质上就是一条条消息来回传。里面定义了定义了这些基本概念:

  • Message(消息):一条对话记录,包含"谁说的"(角色)和"说了什么"(内容)。

  • SYSTEM:系统指令,告诉模型"你现在是个 Agent"

  • USER:用户的问题

  • ASSISTANT:模型的回答

  • TOOL:工具执行后的结果

  • ToolCall(工具调用):模型说"我要用某个工具"时,会带上这个结构,包含工具名和参数。

  • ChatResponse(模型回复):模型返回的完整内容,可能包含文字 + 工具调用请求。

这几个Dataclass的具体内容

@dataclass 是 Python 的一个语法糖,让你不用写一大堆 __init__() 代码。

@dataclass
class ToolCall:
    """Represents a tool invocation requested by the model."""

    id: str
    name: str
    arguments: Dict[str, Any]


@dataclass
class Message:
    """A single message in the conversation."""

    role: MessageRole
    content: str
    tool_calls: Optional[List[ToolCall]] = None
    tool_call_id: Optional[str] = None
    name: Optional[str] = None  # tool name for tool role


@dataclass
class ChatResponse:
    """Structured response from a model provider."""

    content: str
    tool_calls: List[ToolCall] = field(default_factory=list)
    raw: Optional[Any] = None  # provider-specific raw response

平时你要写 msg = Message(role=MessageRole.USER, content="你好"),不用手动定义构造函数,Python 自动帮你生成。

  • Message:聊天历史里的每一行记录

  • ToolCall:模型说"我要调用某某工具"时的结构化信息

  • ChatResponse:模型一次性返回的所有内容(文字 + 可能的工具请求)

  • ToolSpec(阶段 2 出现):工具的"身份证",告诉模型这个工具叫什么、干嘛的、需要什么参数

2. 配置管理(odd/config.py

API Key、模型名称、接口地址这些敏感信息绝对不能写死在代码里。我们通过环境变量来读取:

环境变量

作用

OPENAI_API_KEY

OpenAI 的 API 密钥

OPENAI_BASE_URL

OpenAI 的接口地址(默认是官方地址)

OPENAI_MODEL

使用的模型名,如 gpt-4o-mini

ANTHROPIC_API_KEY

Anthropic 的 API 密钥

ANTHROPIC_BASE_URL

Anthropic 的接口地址

ANTHROPIC_MODEL

使用的 Claude 模型

如何用 .env 文件管理配置?

每次开终端都要敲 export OPENAI_API_KEY=... 很麻烦。可以在项目根目录建一个 .env 文件:

OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_BASE_URL=https://...

然后在代码开头加载它:

from dotenv import load_dotenv
load_dotenv()  # 自动读取当前目录的 .env 文件

这样 os.getenv("OPENAI_API_KEY") 就能读到值了,而且 .env 可以加入 .gitignore,不会泄露密钥。

3. 模型提供者(odd/models/

ModelProvider(抽象基类)

规定了一个接口:所有模型都必须实现 chat(messages) -> ChatResponse。就像 USB 接口规范——不管里面是什么芯片,插上去都能用。

OpenAIProvider

封装了 OpenAI 的 Python SDK。主要做两件事:

  1. 把我们的 Message 转成 OpenAI 要求的格式

  2. 把 OpenAI 的返回结果转成我们的 ChatResponse

AnthropicProvider

Claude 的 API 和 OpenAI 差别挺大,主要体现在消息格式上:

发送时的区别

  • OpenAI:系统消息也是一条普通消息,和其他消息一起塞进 messages 数组

  • Anthropic:系统消息单独放在 system 字段里,messages 数组里只能放用户和助手的对话

# Anthropic 的请求结构
{
    "model": "claude-3-5-haiku-latest",
    "max_tokens": 4096,
    "system": "你是一个有帮助的助手",  # ← 单独放
    "messages": [
        {"role": "user", "content": "你好"},
        {"role": "assistant", "content": "你好!"}
    ]
}

返回内容的区别

  • OpenAI:回复在 choices[0].message.content 里,工具调用在 choices[0].message.tool_calls

  • Anthropic:回复是一个 content 数组,里面可能有多个 "block":

  • type: "text" → 普通文字

  • type: "tool_use" → 模型想调用工具

# Anthropic 的返回结构
{
    "content": [
        {"type": "text", "text": "我来帮你查一下"},
        {"type": "tool_use", "id": "tool_01", "name": "shell", "input": {"command": "ls"}}
    ]
}

所以在 anthropic.py 里,_to_anthropic_msg() 这个转换器专门负责把我们的 Message 转成 Claude 能懂的格式,也把 Claude 的返回拆成 ChatResponse

学到的要点

  1. 接口先行:先定义统一的抽象,再写具体实现,以后加新模型很容易。

  2. 配置外置:密钥、地址、模型名都放环境变量,代码里不留敏感信息。

  3. 适配器模式:每个模型 API 格式不同,但对外暴露一样的接口。

评论