第 2 章 · 02 三种 API 模式收敛


第 2 章 · 02 三种 API 模式收敛

本节摘要:同一颗 AIAgent 的心脏,要 pumping 三种截然不同的 API 协议:OpenAI 兼容的 chat_completions、OpenAI Codex 的 codex_responses(Responses API)、Anthropic 原生的 anthropic_messages。Hermes 的解法是收敛:内部一切消息、工具 schema、响应都以 OpenAI 消息格式(role/content/tool_calls 字典)为规范,差异被压缩到调用边界的两侧——请求前由 transport 的 convert_messages/convert_tools 转成 provider 原生格式,响应后由 normalize_response 归一化回 OpenAI 形态。本节走读 agent/agent_init.py 的模式解析链、agent/transports/ 的 ProviderTransport 抽象与 agent/anthropic_adapter.py 的转换细节,并解释"为什么收敛"让工具层与循环层完全不感知后端差异。

内容来源:原项目源码 agent/agent_init.py:694-751(模式解析)、agent/transports/base.py(传输抽象)、agent/transports/anthropic.pyagent/anthropic_adapter.py:1826/2822(格式转换)、agent/transports/__init__.py(注册表)。

⚠️ 注意:实际代码里 api_mode 的合法值不止三种——还有 bedrock_converse(AWS Bedrock)与 codex_app_server(Codex 子进程模式)。本教程按官方 agent-loop.md 的"三种主模式"框架讲解,bedrock/vertex/gemini native 等作为适配器变体在本节末尾盘点。读码时以 agent_init.py:694 的集合为准。

学习目标

阅读完本节,你应当能够:

  1. 说出三种 API 模式各自服务哪类后端、用什么客户端。
  2. 按优先级复述模式解析链:显式参数→provider 探测→base URL 启发式→默认 chat_completions。
  3. 写出 ProviderTransport 抽象的四个核心方法与它"不管"的六件事。
  4. 手工完成一次工具 schema 的 OpenAI→Anthropic 转换(parametersinput_schema)。
  5. 描述消息转换的六步收尾(孤儿块清理/合并相邻角色/确保 user 开头等)与思考块签名的坑。
  6. 解释"为什么收敛"在工程上的三层收益。

一、三种模式与解析链

官方 agent-loop.md 的模式表:

API 模式 服务对象 客户端
chat_completions OpenAI 兼容端点(OpenRouter、自定义、多数 provider) openai.OpenAI
codex_responses OpenAI Codex / Responses API openai.OpenAI(Responses 格式)
anthropic_messages Anthropic 原生 Messages API anthropic.Anthropic 经适配器

模式在构造期解析,agent/agent_init.py:694-751 是一条精心排序的决策链(节选):

694 if api_mode in {"chat_completions", "codex_responses", "anthropic_messages", "bedrock_converse", "codex_app_server"}: 695 agent.api_mode = api_mode # ① 显式参数最高优先 696 elif agent.provider == "openai-codex": 697 agent.api_mode = "codex_responses" # ② provider 名推断 709 elif agent.provider == "anthropic" or (provider_name is None and agent._base_url_hostname == "api.anthropic.com"): 710 agent.api_mode = "anthropic_messages" 712 elif agent._base_url_lower.rstrip("/").endswith("/anthropic"): 713 # Third-party Anthropic-compatible endpoints (e.g. MiniMax, DashScope) 714 # use a URL convention ending in /anthropic. 716 agent.api_mode = "anthropic_messages" 717 elif agent.provider == "bedrock" or ( 718 agent._base_url_hostname.startswith("bedrock-runtime.") ...): 723 agent.api_mode = "bedrock_converse" 731 else: 751 agent.api_mode = "chat_completions" # ④ 兜底默认

解析顺序即文档总结的四条:显式 api_mode 构造参数 > provider 专属探测(anthropic→原生 Messages) > base URL 启发式(api.anthropic.com/anthropic 后缀、bedrock-runtime.*.amazonaws.com) > 默认 chat_completions。两个细节见微知著:其一,URL 以 /anthropic 结尾的第三方兼容端点(MiniMax、DashScope)自动切原生 Messages 协议——中文生态的 Anthropic 兼容端点靠这条约定零配置接入;其二,host 级强制(host_mandated_api_mode)被刻意排在链尾,源码注释解释:provider 名 meta 可能被用户指向任意 OpenAI 兼容端点,协议必须 URL 驱动而非名字驱动。

二、ProviderTransport:差异的边界在哪

provider 差异被封装在 agent/transports/ 的传输抽象里。base.py 的类契约:

class ProviderTransport(ABC): """Base class for provider-specific format conversion and normalization.""" @property @abstractmethod def api_mode(self) -> str: """The api_mode string this transport handles (e.g. 'anthropic_messages').""" @abstractmethod def convert_messages(self, messages, **kwargs) -> Any: """Convert OpenAI-format messages to provider-native format. ...""" @abstractmethod def convert_tools(self, tools) -> Any: """Convert OpenAI-format tool definitions to provider-native format. ...""" @abstractmethod def build_kwargs(self, model, messages, tools=None, **params) -> Dict[str, Any]: """Build the complete API call kwargs dict. ...""" @abstractmethod def normalize_response(self, response, **kwargs) -> NormalizedResponse: ...

文件开头的注释划定了边界——transport 只管数据通路(convert_messages→convert_tools→build_kwargs→normalize_response),不管客户端构建、流式、凭证刷新、prompt caching、中断处理、重试逻辑,"Those stay on AIAgent"。agent/transports/__init__.py 提供 register_transport(api_mode, transport_cls) 注册表,按 api_mode 字符串取实现——新增一种协议 = 实现一个 Transport + 注册,主循环零改动。目录下的实现:chat_completions.py(恒等转换)、anthropic.py、codex.py、codex_app_server.py、bedrock.py,加上 agent/gemini_native_adapter.py(Gemini 原生)与 agent/codex_responses_adapter.py,合计五种 provider 适配器——正是第 9 章 provider 生态的协议层地基。

三、转换细节:Anthropic 适配器精读

工具 schema:parameters→input_schema

agent/anthropic_adapter.py:1826:

1826 def convert_tools_to_anthropic(tools: List[Dict]) -> List[Dict]: 1827 """Convert OpenAI tool definitions to Anthropic format.""" 1831 seen_names: set = set() 1832 for t in tools: 1833 fn = t.get("function", {}) 1834 name = fn.get("name", "") 1835 # Defensive dedup: Anthropic rejects requests with duplicate tool names. 1838 if name and name in seen_names: 1844 continue 1847 anthropic_tool: Dict[str, Any] = { 1848 "name": name, 1849 "description": fn.get("description", ""), 1850 "input_schema": _normalize_tool_input_schema( 1851 fn.get("parameters", {"type": "object", "properties": {}}) 1852 ), 1853 } 1857 cache_control = t.get("cache_control") 1858 if isinstance(cache_control, dict): 1859 anthropic_tool["cache_control"] = dict(cache_control)

OpenAI 的嵌套 {"type": "function", "function": {..., "parameters": ...}} 摊平为 Anthropic 的 {"name", "description", "input_schema"};重名防御性去重把潜在 400 变成一条警告;cache_control 标记被前传——Anthropic 支持在最后一个工具上打断点,整个工具 schema 就能跨会话进缓存(第 6 章的主题)。

消息:六步收尾与思考块签名

agent/anthropic_adapter.py:2822convert_messages_to_anthropic 返回 (system, messages) 二元组——OpenAI 把 system 塞在消息列表首位,Anthropic 要它单独成参。核心循环按角色分派(system 提取/assistant 转换/tool 转为 user 角色的 tool_result 块),随后六步收尾:

2897 _strip_orphaned_tool_blocks(result) # 清孤儿 tool_use/tool_result 2898 result = _merge_consecutive_roles(result) # 合并相邻同角色 2899 _ensure_leading_user_turn(result) # 确保首条是 user 2900 _manage_thinking_signatures(result, base_url, model) # 思考块签名管理 2901 _evict_old_screenshots(result) # 驱逐旧截图省 token 2902 _scrub_blank_text_blocks(result) # 清空白文本块(Anthropic 400 拒绝)

这六步全是"协议洁癖"工程:Anthropic 对消息形态的校验比 OpenAI 苛刻得多,任何一条不满足就是 400。最险的是思考块签名:Anthropic 给 thinking 块做签名,重放顺序错乱即 400——当一轮里思考与工具调用交错(Claude 4.6+ 的自适应思考),必须逐块保序回放。agent/transports/anthropic.py:97-106 的注释直接点名:tests/agent/test_anthropic_thinking_block_order.py 是这条铁律的规格书。另一处:base_url 指向第三方 Anthropic 兼容端点时剥离全部签名(第三方无法验证,带着必 400);Kimi/Moonshot 家族则相反,要求 tool-call 消息上必须有 thinking 字段——哪怕为空。

响应归一化

回来的路在 transports/anthropic.py:80normalize_response:遍历 response.content 块,text 块进 content,thinking 块进 reasoning,tool_use 块重组成 OpenAI 形态的 tool_calls,stop_reason 映射为 finish_reason(如 tool_usetool_calls)。归一化产物 NormalizedResponse 就是主循环眼中唯一的响应形态——循环层根本不知道这趟请求背后是哪家协议。

四、为什么收敛:三层收益

循环层收益:上一节的主循环代码(while/_interruptible_api_call/_execute_tool_calls)通篇没有出现 provider 分支——收敛把 if/else 从业务代码里挤了出去,循环逻辑得以单态。工具层收益:133 个工具只维护一份 OpenAI 格式 schema,convert_tools 在边界上适配;工具作者(包括你在第 3 章将读到的每个 register 调用)对后端协议零感知。测试层收益:一套针对 OpenAI 消息形态的 86 万行测试可以同时覆盖三种模式,协议差异各自隔离在 transport 的单元测试里(如上面的 thinking_block_order)。这也解释了为什么 ephemeral_system_prompt 只在 API 调用时注入、为什么系统 prompt 会话期间字节级稳定——收敛格式之后,缓存策略才能统一在第 6 章的"铁律"之下。

💡 循环要点:"收敛到 OpenAI 消息格式"是内循环的普通话政策:内部只说一种方言,方言翻译全部外包给调用边界上的 transport(请求侧 convert、响应侧 normalize)。判断一个多 provider agent 框架的工程质量,先看它的消息规范是否唯一——Hermes 的答案是:唯一,且转换边界有测试当规格书。

本节要点回顾

  1. 三种主模式:chat_completions(OpenAI 兼容,多数 provider)/codex_responses(OpenAI Responses)/anthropic_messages(Anthropic 原生);另有 bedrock_converse 等变体。
  2. 解析链四步:显式 api_mode > provider 探测 > base URL 启发式 > 默认 chat_completions(agent_init.py:694-751)。
  3. /anthropic 后缀的第三方端点(MiniMax/DashScope)自动走原生 Messages;协议必须 URL 驱动而非 provider 名驱动。
  4. ProviderTransport 四方法:convert_messages/convert_tools/build_kwargs/normalize_response;不管客户端构建、流式、凭证、缓存、中断、重试。
  5. 工具转换:OpenAI 嵌套 function 结构摊平为 name/description/input_schema;重名去重;cache_control 前传实现 schema 级缓存。
  6. 消息转换六步收尾:清孤儿块、合并相邻角色、确保 user 开头、思考签名管理、驱逐旧截图、清空白块。
  7. 思考块签名三坑:交错必须保序回放;第三方端点剥离签名;Kimi 家族要求字段存在。
  8. 收敛的三层收益:循环单态(无 provider 分支)、工具只维护一份 schema、一套测试覆盖三模式。

下一节补上循环的最后一块控制论:迭代预算(父子如何共享、耗尽如何优雅退出)与 prompt 组装流水线(系统提示+技能+记忆+历史+工具 schema 的拼装顺序)。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U