第 35 章:为什么 Formatter 独立于 Model

难度:中等
其他框架把消息格式转换和 API 调用放在同一个类里。AgentScope 把它们分成
Formatter和Model两个独立的类。为什么?
决策回顾
1 | Agent 调用流程: |
Formatter 负责 Msg ↔ dict 转换,Model 负责 HTTP 调用。它们是独立的对象,通过 Agent 的 reply 方法协调。
被否方案:合并为 Model
方案:格式转换逻辑内置在 Model 中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class OpenAIModel:
def __init__(self, model_name, api_key):
self.client = openai.OpenAI(api_key=api_key)
async def __call__(self, msgs: list[Msg], tools=None):
# 格式转换 + API 调用都在这里
messages = self._convert_msgs(msgs)
response = await self.client.chat.completions.create(
model=self.model_name,
messages=messages,
tools=tools,
)
return self._convert_response(response)
def _convert_msgs(self, msgs):
# OpenAI 格式转换
result = []
for msg in msgs:
if msg.role == "system":
result.append({"role": "system", "content": msg.content})
...
return result
LangChain 和很多框架就是这样做的——ChatOpenAI 类同时处理格式和调用。
为什么分离
理由一:Ollama 兼容 OpenAI 格式
Ollama 的 API 兼容 OpenAI 格式,但 HTTP 调用方式不同:1
2
3
4
5
6
7
8
9# 不分离时
class OllamaOpenAIModel: # 格式 = OpenAI, 调用 = Ollama
class OllamaModel: # 格式 = 自定义, 调用 = Ollama
class DeepSeekModel: # 格式 = OpenAI, 调用 = DeepSeek
# 分离后
OpenAIChatFormatter + OllamaChatModel # Ollama 用 OpenAI 格式
OllamaChatFormatter + OllamaChatModel # Ollama 用自定义格式
OpenAIChatFormatter + DeepSeekChatModel # DeepSeek 用 OpenAI 格式
分离前需要 N × M 个类(N 种格式 × M 种 API)。分离后只需 N + M 个类。
理由二:独立测试
1 | # 测试格式转换——不需要 mock HTTP |
理由三:独立替换
1 | # 运行时切换格式——不影响 Model |
1 | flowchart LR |
后果分析
好处
- 组合自由:N + M 个类替代 N × M 个
- 独立测试:格式转换和 API 调用分别测试
- 运行时替换:可以动态切换格式化策略
- 关注点分离:Formatter 只关心格式,Model 只关心 HTTP
麻烦
- 两处修改:添加新模型 API 可能需要同时写 Formatter 和 Model
- 协调复杂:Formatter 和 Model 的接口需要匹配(stream 参数、tool_schema 格式等)
- 额外概念:开发者需要理解”为什么要两个类”
横向对比
| 框架 | 格式与调用 | 组织方式 |
|---|---|---|
| AgentScope | 分离(Formatter + Model) | 正交分解 |
| LangChain | 合并(ChatOpenAI 等) | 按提供者分 |
| LiteLLM | 合并(统一接口) | 一个类适配所有 |
| AutoGen | 合并 | 按模型分 |
LiteLLM 的方案也有趣——用一个类适配所有 API,内部做格式转换。但扩展性不如 AgentScope 的分离方案。
AgentScope 官方文档的 Building Blocks > Models 页面展示了不同模型提供商的使用方法,包括 OpenAI、Anthropic、DashScope、Gemini、Ollama 等。每种模型可以搭配不同的 Formatter,实现格式与通信的分离。
AgentScope 1.0 论文对这一设计的说明是:
“we abstract foundational components essential for agentic applications and provide unified interfaces and extensible modules, enabling developers to easily leverage the latest progress, such as new models and MCPs”
— AgentScope 1.0: A Comprehensive Framework for Building Agentic Applications, arXiv:2508.16279, Section 2
Formatter 与 Model 的分离正是”可扩展模块”思想的体现——新增一个模型提供商只需要实现 ChatModelBase 和对应的 FormatterBase,两者独立演进。
验证性实验:测量 Formatter-Model 耦合度
目标:验证分离设计的组合自由度。
步骤:
列出
src/agentscope/formatter/下所有 Formatter 子类(5 个)。列出
src/agentscope/model/下所有 Model 子类(5 个)。理论上可能的组合数:5 × 5 = 25 种。实际可行的有多少种?
尝试用
OllamaChatModel+AnthropicChatFormatter组合——它能工作吗?为什么能/不能?
你的判断
- LiteLLM 的”统一接口”方案是否比 AgentScope 的”分离方案”更简单?在什么场景下?
- 如果未来的模型 API 全部兼容 OpenAI 格式,Formatter 还有存在的必要吗?
接口契约:Formatter ↔ Model 的匹配
分离的前提是两个类的接口必须匹配。实际契约:1
2
3
4
5Formatter.format(msgs, tools, stream) → (formatted_messages, tool_schemas, stream_flag)
↓
Model.__call__(messages, tools, stream) → ChatResponse
↓
Formatter.parse_response(response, msg) → Msg
关键匹配点:
- tool_schemas 格式:Formatter 的
format()输出的tools参数格式必须与 Model 的 API 预期一致 - stream 参数:Formatter 和 Model 必须同时支持流式/非流式,且行为对齐
- 消息角色:Formatter 把
Msg.role映射为 API 的角色字段(OpenAI 的 “user”/“assistant”/“system”,Anthropic 的 “user”/“assistant”)
不匹配时的典型问题:Formatter 用 OpenAI 格式输出 tool_result 角色为 "tool",但 Anthropic API 要求 "user" 角色。这正是 Formatter 存在的核心价值——抹平不同 API 的语义差异。
试一试:验证 Formatter 与 Model 的解耦
目标:确认可以独立替换 Formatter。
步骤:
- 搜索
Formatter和Model的匹配关系:
1 | grep -n "formatter" src/agentscope/agent/_react_agent.py | head -5 |
- 观察 Agent 的
reply方法中 Formatter 和 Model 是如何协调的——Formatter 在 Model 调用前后各出现一次。
下一章预告
我们看了 7 个具体的设计决策。最后一章,我们拉远视角,看整个架构的全景图和边界。
下一章:第 36 章 架构全景与边界