本节摘要:同一颗 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.py、agent/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的集合为准。
阅读完本节,你应当能够:
parameters→input_schema)。官方 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 驱动而非名字驱动。
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 生态的协议层地基。
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:2822 的 convert_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:80 的 normalize_response:遍历 response.content 块,text 块进 content,thinking 块进 reasoning,tool_use 块重组成 OpenAI 形态的 tool_calls,stop_reason 映射为 finish_reason(如 tool_use→tool_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 的答案是:唯一,且转换边界有测试当规格书。
chat_completions(OpenAI 兼容,多数 provider)/codex_responses(OpenAI Responses)/anthropic_messages(Anthropic 原生);另有 bedrock_converse 等变体。api_mode > provider 探测 > base URL 启发式 > 默认 chat_completions(agent_init.py:694-751)。/anthropic 后缀的第三方端点(MiniMax/DashScope)自动走原生 Messages;协议必须 URL 驱动而非 provider 名驱动。下一节补上循环的最后一块控制论:迭代预算(父子如何共享、耗尽如何优雅退出)与 prompt 组装流水线(系统提示+技能+记忆+历史+工具 schema 的拼装顺序)。