第 7 章 · 01 Model 基类与 10 个实现


第 7 章 · 01 Model 基类与 10 个实现

本节摘要:本节精读 src/smolagents/models.py——2102 行,smolagents 最大的单文件。它回答一个工程问题:agent 逻辑(第 2、3 章的 ReAct 循环与代码解释器)如何与具体的 LLM 服务解耦?答案是 Model 基类定义 generate/generate_stream 统一接口,ChatMessage 统一消息封装,下面再挂 10 个实现类:本地三兄弟(TransformersModel/vLLMModel/MLXModel)、云端六家(InferenceClientModel/LiteLLMModel/OpenAIModel/AzureOpenAIModel/AmazonBedrockModel/LiteLLMRouterModel),中间还有一层 ApiModel 抽象。换模型=换一个类,agent 代码零改动。读完本节,你能按场景(本地隐私/高吞吐/多云)选对 Model 类。

内容来源:原项目源码 src/smolagents/models.py(2102 行)与 examples/agent_from_any_llm.py,精读并套用体系化模板。

⚠️ 注意:Model 是抽象基类,直接实例化后调用 generate 会抛 NotImplementedError;各实现类的重依赖(vllm/mlx_lm/openai/litellm/boto3)都是可选安装,未安装时报 ModuleNotFoundError 并提示对应的 pip install 'smolagents[...]' 命令。

学习目标

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

  1. 讲清 Model 基类的 generate 统一接口签名与 _prepare_completion_kwargs 的参数优先级。
  2. 说出 ChatMessage 的五个字段(role/content/tool_calls/raw/token_usage)各自的作用。
  3. 默写 10 个 Model 类的分工与继承结构(哪些走 ApiModel 中间层)。
  4. 解释"换模型=换一个类,agent 代码零改动"为何成立。
  5. 按本地隐私/vLLM 吞吐/LiteLLM 多云三个场景给出选型建议。

一、models.py 全景:最大的单文件在干什么

先看这 2102 行的版图:

70-200 ChatMessage 家族:工具调用/消息角色/文本工具调用解析 203-280 流式 delta 家族与 agglomerate_stream_deltas(下节精读) 282-438 消息清洗 get_clean_message_list、工具调用解析、stop 参数探测 441-631 Model 基类:统一接口的核心 633-1136 本地三兄弟:VLLMModel / MLXModel / TransformersModel 1138-1202 ApiModel 中间基类:限流 + 重试的公共底座 1205-2063 云端六家:LiteLLMModel/LiteLLMRouterModel/InferenceClientModel/ OpenAIModel/AzureOpenAIModel/AmazonBedrockModel 2066-2102 MODEL_REGISTRY 反序列化白名单

为什么这个文件最大?因为每接入一家 LLM 供应商,就要处理一套方言:消息角色叫法不同(tool-call vs function)、工具 schema 格式不同、token 用量字段不同、甚至 Bedrock 连消息里的 type 键都要删掉(models.py:2000-2003)。smolagents 把所有方言差异压平到一个文件里,换来的是 agent 侧的绝对简洁——agents.py 里只认 ChatMessage

二、Model 基类:generate 统一接口

models.py:452-596,核心只有三段。先是构造参数:

452 class Model: 484 def __init__( 485 self, 486 flatten_messages_as_text: bool = False, 487 tool_name_key: str = "name", 488 tool_arguments_key: str = "arguments", 489 model_id: str | None = None, 490 **kwargs, 491 ): 492 self.flatten_messages_as_text = flatten_messages_as_text 493 self.tool_name_key = tool_name_key 494 self.tool_arguments_key = tool_arguments_key 495 self.kwargs = kwargs 496 self.model_id: str | None = model_id

flatten_messages_as_text 决定多模态消息是拍平成纯文本(适配不支持图片列表的本地后端)还是保留结构化 content;**kwargs 原样转存,最终会转发给底层 completion 调用——这是"构造时配 temperature/max_tokens"的实现基础。

然后是必须由子类实现的 generate(models.py:553-578,签名节选):

553 def generate( 554 self, 555 messages: list[ChatMessage], 556 stop_sequences: list[str] | None = None, 557 response_format: dict[str, str] | None = None, 558 tools_to_call_from: list[Tool] | None = None, 559 **kwargs, 560 ) -> ChatMessage: ... 578 raise NotImplementedError("This method must be implemented in child classes")

四个参数覆盖 agent 的全部需要:messages 是记忆转出的消息列表;stop_sequences 让 ToolCallingAgent 在 </tool_call> 处截断;response_format 传结构化输出 schema;tools_to_call_from 传工具列表。返回值恒为 ChatMessage。还有 __call__ 直接委托 generate(models.py:580-581),所以 agent 里可以写 model(messages)

接口的"胶水"是 _prepare_completion_kwargs(models.py:502-551),它把 ChatMessage 清洗成各家 API 要的字典,并实现三级参数优先级:

521 # Parameter priority (highest to lowest): 522 # 1. self.kwargs (model defaults) 523 # 2. Explicitly passed kwargs 524 # 3. Specific parameters (stop_sequences, response_format, etc.) ... 543 completion_kwargs.update(kwargs) 544 # Override with self.kwargs 545 for kwarg_name, kwarg_value in self.kwargs.items(): 546 if kwarg_value is REMOVE_PARAMETER: 547 completion_kwargs.pop(kwarg_name, None) 548 else: 549 completion_kwargs[kwarg_name] = kwarg_value

构造时的 self.kwargs 优先级最高——你在 TransformersModel(max_new_tokens=5000) 里写死的值,任何调用处都覆盖不了;配合哨兵值 REMOVE_PARAMETER(models.py:441-449)还能反向删除某个参数(比如给不支持 tool_choice 的后端拆掉它)。

💡 阶梯要点:统一接口的价值在第 2、3 章已埋下伏笔——MultiStepAgent.step 里只写 self.model.generate(...),从不知道背后是 GPU 上的 1.7B 小模型还是云端 80B 大模型。接口收敛在一处,变化被隔离在十个子类里,这是"barebones"哲学在模型层的落地。

三、ChatMessage:消息的统一封装

models.py:123-137,五个字段(MessageRole 枚举定义在 models.py:111-120,含 USER/ASSISTANT/SYSTEM/TOOL_CALL/TOOL_RESPONSE 五种角色):

123 @dataclass 124 class ChatMessage: 125 role: MessageRole 126 content: str | list[dict[str, Any]] | None = None 127 tool_calls: list[ChatMessageToolCall] | None = None 128 raw: Any | None = None # Stores the raw output from the API 129 token_usage: TokenUsage | None = None
  • role:五种角色,内部多出的 TOOL_CALL/TOOL_RESPONSE 会在 tool_role_conversions(models.py:282-285)里折算回 assistant/user,因为多数 API 只认三种角色;
  • content:纯文本多模态块列表([{"type": "image", ...}, {"type": "text", ...}]);
  • tool_calls:结构化工具调用,__post_init__ 里用 _coerce_tool_call 把 OpenAI 的 pydantic 对象、裸 dict 统一拍成内部 dataclass——又一处方言压平;
  • raw:保留 API 原始响应,调试时有用,model_dump_json 序列化时会忽略;
  • token_usage:挂第 2 节精读的 TokenUsage

四、10 个 Model 类总览

继承结构与分工一表看全(行号为 __init__ 位置):

继承 底层 一句话定位
TransformersModel Model transformers + torch 本地推理,auto 探测 VLM(models.py:906)
VLLMModel Model vLLM LLM 高吞吐本地服务,支持结构化输出(models.py:648)
MLXModel Model mlx-lm Apple 芯片优化,Mac 本地跑(models.py:791)
ApiModel Model (抽象) 云端公共底座:限流+重试(models.py:1161)
LiteLLMModel ApiModel litellm SDK 一个接口调 100+ 云模型(models.py:1224)
LiteLLMRouterModel LiteLLMModel litellm Router 多模型编组智能路由/负载均衡(models.py:1426)
InferenceClientModel ApiModel HF InferenceClient HF Inference Providers,默认后端(models.py:1514)
OpenAIModel ApiModel openai SDK OpenAI 及一切兼容 API 服务(models.py:1671)
AzureOpenAIModel OpenAIModel openai AzureOpenAI 企业 Azure 部署(models.py:1820)
AmazonBedrockModel ApiModel boto3 converse AWS Bedrock,角色全转 user(models.py:1940)

几个值得点名的细节:

TransformersModel 的 VLM 探测(models.py:950-973):先尝试 AutoModelForImageTextToText 加载,失败且报 "Unrecognized configuration class" 就回退 AutoModelForCausalLM——同一份代码既能跑 Qwen3 文本模型也能跑视觉模型,靠异常类型分支而非配置开关。

ApiModel 的公共底座(models.py:1161-1191):云端模型的 __init__ 统一创建 RateLimiterRetrying(详见下节),generate 里统一走 self._apply_rate_limit() + self.retryer(self.client...)。子类只需实现 create_clientgenerate,限流重试白送。

AmazonBedrockModel 的角色折叠(models.py:1953-1958):Bedrock 只支持 assistant/user 两种角色且不允许对话以 assistant 开头,于是默认把 SYSTEM/TOOL_CALL/TOOL_RESPONSE 全部转成 user,再拍平消息——方言差异的极端案例。

LiteLLMRouterModel:把 gpt-4o-mini 和 bedrock Claude 塞进同一个 model-group-1,routing_strategy: "simple-shuffle" 随机分发,一个逻辑模型组背后是多云负载均衡(完整示例见 examples/multi_llm_agent.py)。

文件末尾的 MODEL_REGISTRY(models.py:2070-2080)是 9 个具体类的白名单,反序列化 from_dict 只允许实例化注册过的类,防止通过恶意序列化数据动态 import 任意代码——安全意识贯穿到模型层。

五、换模型=换一个类:agent 代码零改动

官方示例 examples/agent_from_any_llm.py(即文档 using_different_models)把这一点演示到极致——改一个字符串,后端全换:

14 available_inferences = ["inference_client", "transformers", "ollama", "litellm", "openai"] 15 chosen_inference = "inference_client" ... 19 if chosen_inference == "inference_client": 20 model = InferenceClientModel(model_id="meta-llama/Llama-3.3-70B-Instruct", provider="nebius") 22 elif chosen_inference == "transformers": 23 model = TransformersModel(model_id="HuggingFaceTB/SmolLM2-1.7B-Instruct", device_map="auto", max_new_tokens=1000) 25 elif chosen_inference == "ollama": 26 model = LiteLLMModel(model_id="ollama_chat/llama3.2", api_base="http://localhost:11434", ...) 37 elif chosen_inference == "openai": 39 model = OpenAIModel(model_id="gpt-4o") ... 55 agent = ToolCallingAgent(tools=[get_weather], model=model, verbosity_level=2) 57 print("ToolCallingAgent:", agent.run("What's the weather like in Paris?")) 59 agent = CodeAgent(tools=[get_weather], model=model, verbosity_level=2, stream_outputs=True)

注意 L55-59:agent 的构造与 run 调用对五种后端完全一致。差异全部被压缩在 model = XxxModel(...) 这一行。这正是第 3 章"代码即 action"能跨模型验证的前提——论文里"少 30% 步"的实验,就是在同一套 CodeAgent 代码下换不同后端模型跑出来的。

六、模型选型建议

场景 推荐 理由
数据隐私/离线/零 API 成本 TransformersModel 权重本地加载,数据不出机;代价是速度与显存
本地高并发/批量任务 VLLMModel vLLM 的 PagedAttention 连续批处理,吞吐远超裸 transformers,还支持结构化输出
Mac 笔记本开发调试 MLXModel MLX 针对 Apple 芯片统一内存优化,4bit 量化跑 32B 模型
不想选型/快速起跑 InferenceClientModel 一个 HF token 通吃几百个开源模型,默认后端
多云/避免供应商锁定 LiteLLMModel anthropic/claude-...gpt-4oollama_chat/... 统一前缀寻址
生产高可用/成本分流 LiteLLMRouterModel 小任务路由便宜模型,大任务路由旗舰,还能故障转移
企业合规(Azure/AWS) AzureOpenAIModel / AmazonBedrockModel 走企业账号、私有端点与合规审计

⚠️ 注意:本地三兄弟对多模态的支持参差——vLLMModel 与 MLXModel 明确 _is_vlm = False(models.py:668、814),图片会被拍平成文本;只有 TransformersModel 探测到 VLM 架构时才保留图像输入。视觉任务要么用云端模型,要么用第 8 章的 vision_web_browser 思路(截图走工具回调)。

本节要点回顾

  1. models.py 2102 行是最大单文件,本质是把 10 家 LLM 供应商的方言(角色/schema/token 计数)压平成一套内部协议。
  2. Model 基类:generate(messages, stop_sequences, response_format, tools_to_call_from) -> ChatMessage,__call__ 委托 generate;_prepare_completion_kwargs 实现 self.kwargs > 显式 kwargs > 专用参数的三级优先级,REMOVE_PARAMETER 哨兵可删参数。
  3. ChatMessage 五字段:role/content/tool_calls/raw/token_usage;tool_role_conversions 把内部五角色折算回 API 三角色;_coerce_tool_call 兼容各家工具调用对象。
  4. 10 个类三层结构:Model →(本地)TransformersModel/vLLMModel/MLXModel、(云端)ApiModel → LiteLLMModel→LiteLLMRouterModel、InferenceClientModel、OpenAIModel→AzureOpenAIModel、AmazonBedrockModel。
  5. ApiModel 白送限流重试,子类只需 create_client + generate
  6. 换模型=换一行构造代码,agent 与 run 调用零改动(agent_from_any_llm.py 演示五种后端同一套 agent)。
  7. 选型口诀:隐私选 Transformers、吞吐选 vLLM、Mac 选 MLX、省心选 InferenceClient、多云选 LiteLLM、分流选 Router、合规选 Azure/Bedrock。

下一节:深入 generate_stream 流式输出与 delta 聚合、工具 JSON schema 的自动生成,以及 TokenUsage 统计与 AgentLogger 遥测——把 Model 层的"毛细血管"走完。


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