LLM 抽象:模型字符串、统一基类与多 provider
30 秒导读: 在 fast-agent 里,你选模型只写一个字符串——
"sonnet"、"gpt-5.1"、"claude-sonnet-4-6?web_search=on"、"generic.qwen2.5"。本章讲这一个字符串怎么一步步变成 对某个真实厂商(Anthropic / OpenAI / Google / Bedrock……)的一次 API 调用:先被解析成结构化 规格,再由工厂挑出对应的 provider 子类,最后所有子类都长在同一个泛型基类FastAgentLLM上。
本章只讲「一个模型串 → 一次 provider 调用」这条线。agent 怎么调用 LLM、工具循环怎么驱动一次 turn,见 02 / 03;MCP sampling 复用 LLM 那条路径见 05。
1. 这是什么(零基础也能懂)
一句话定义: 这是 fast-agent 的「模型层」——把用户写的一小段模型字符串翻译成一个可以真正 发请求的 LLM 对象。
它解决什么问题。 市面上的 LLM 厂商(provider)接口五花八门:Anthropic 的 Messages API、OpenAI 的 Chat Completions / Responses、Google 的 GenAI、AWS Bedrock……参数名、消息格式、推理开关全不一样。 fast-agent 想让上层的 agent 完全不关心这些差异,于是需要两样东西:
- 一个统一入口:无论用哪个厂商,agent 只调用同一组方法(
generate、structured)。 - 一套声明式选择法:用户不写代码,只写一个字符串就能选定厂商、模型、推理档位、附加能力。
用起来什么样。 下面这些都是合法的模型字符串,展示了它能表达多少信息:
| 模型字符串 | 表达的意思 |
|---|---|
sonnet | 别名(preset),展开成 claude-sonnet-4-6 |
gpt-5.1?reasoning=low | OpenAI 的 gpt-5.1,推理档位设为 low |
claude-sonnet-4-6?web_search=on | Anthropic Sonnet,打开服务端联网搜索 |
generic.qwen2.5 | 走 generic(本地 Ollama 等)provider,模型名 qwen2.5 |
hf.Qwen/Qwen3.5-397B:novita?temperature=0.6 | HuggingFace 路由到 novita,带采样参数 |
openai/gpt-4.1 | 用 / 显式指定 provider 前缀 |
一句话直觉/类比。 把模型字符串想成一个 URL:主体是「模型名」,? 后面是「查询参数」
(要不要联网、推理多深、温度多少),前缀 provider/ 或 provider. 像协议头(https://)指明
「走哪家」。fast-agent 的模型层就是这个 URL 的解析器 + 路由器。
本节不碰代码细节。你只要记住:一个字符串进去,一个能发请求的 LLM 对象出来。
2. 顶层全景(它大概怎么转)
整条链路分三段:解析 → 解析规格(hydrate)→ 造对象。先看图,再逐段说。
怎么读这张图: 从上到下是时间顺序;左边是数据形态,右边是负责的符号。一个模型字符串 依次被加工成 3 个越来越"熟"的对象,最后吐出一个 provider 子类实例。
"claude-sonnet-4-6?web_search=on" ← 用户写的一个字符串
│
▼ ModelFactory.parse_model_spec()
┌───────────────────┐
│ ParsedModelSpec │ ← 拆好:provider + model_name + 推理档 + 查询覆盖
└───────────────────┘ (还只是"字符串层面"的解析)
│
▼ ModelFactory.resolve_model_spec()
┌───────────────────┐
│ ResolvedModelSpec │ ← 补齐:查 ModelDatabase 拿能力/上下文窗口/wire 名字
└───────────────────┘ (加上 overlay、model_params,可以驱动真实请求)
│
▼ ModelFactory.create_factory()
┌───────────────────┐
│ factory(agent) │ ← 一个闭包:挑好 provider 子类,等 agent 来"附着"
└───────────────────┘
│ agent 调用 factory(self)
▼
┌───────────────────┐
│ AnthropicLLM(...) │ ← FastAgentLLM 的某个子类实例,真正会发 API
└───────────────────┘
三段各干什么:
| 阶段 | 入口符号 | 产物 | 职责一句话 |
|---|---|---|---|
| 解析 | ModelFactory.parse_model_spec | ParsedModelSpec | 纯字符串拆解:别名展开、provider 前缀、? 查询覆盖 |
| 解析规格 | ModelFactory.resolve_model_spec | ResolvedModelSpec | 查 ModelDatabase 补能力元数据、wire 模型名、overlay |
| 造对象 | ModelFactory.create_factory | factory 闭包 → LLM 实例 | 按 provider 懒加载子类,注入 request params 造实例 |
统一入口在哪。 所有 provider 子类都继承自 FastAgentLLM
(llm/fastagent_llm.py:101,class FastAgentLLM)。它是一个泛型基类,用两个类型参数
表示「输入消息类型」和「输出消息类型」,把 provider 差异关进子类里,对上只暴露 generate /
structured 两个统一方法。
主线走一遍(高层): 用户给字符串 → ModelFactory 解析并解析规格 → 挑出子类 → agent 附着生成
实例 → agent 调 generate() → 基类做通用的事(合并参数、重试、计费)→ 委托子类的
_apply_prompt_provider_specific() 去真正拼厂商格式、发请求 → 结果回到基类做统一收尾。
关键文件一览:
| 部件 | 干什么 | 文件 |
|---|---|---|
ModelFactory | 解析字符串、路由 provider、造工厂 | llm/model_factory.py |
ParsedModelSpec / ModelQueryOverrides | 解析结果的结构化载体 | llm/model_factory.py |
ResolvedModelSpec | 补齐能力元数据的解析规格 | llm/resolved_model.py |
FastAgentLLM | 所有 provider 的统一泛型基类 | llm/fastagent_llm.py |
| provider 子类 | Anthropic / OpenAI / Google / Bedrock 适配 | llm/provider/** |
| 内部测试 LLM | passthrough / playback / silent / slow | llm/internal/** |
| 结构化输出 | 模式解析 + JSON Schema 校验 | llm/structured_output_mode.py、llm/structured_schema.py |
3. 核心机制一:模型字符串怎么被解析
这是整条链路里最"绕"的一步,却也是精华所在。目标:把 claude-sonnet-4-6?web_search=on 这样
一个人写的串,无歧义地拆成「哪家 provider、哪个模型、推理多深、附带哪些开关」。
入口是 ModelFactory.parse_model_spec(llm/model_factory.py:896)。它按固定顺序做若干步拆解。
3.1 拆解的先后顺序
顺序很重要——先切 ? 查询、再切 : 后缀、再展开别名、最后定 provider。用一张流程图看清:
怎么读这张图: 从左到右是加工顺序,每一步只负责剥掉一层;命中 就把该层信息存下来,继续往右。
model_string
│
├─① 切 "?" ────────────► model_spec + 查询覆盖(ModelQueryOverrides)
│ _split_model_spec_and_query
│
├─② 切 ":" ────────────► base + user_suffix(如 :groq / :novita 传输/路由后缀)
│ _split_model_suffix
│
├─③ 展开别名/preset ─────► expanded_spec + preset 自带的查询默认值
│ _expand_model_preset (用户显式的 ? 覆盖 preset 默认)
│
├─④ 定 provider + 模型名 ► Provider + model_name
│ _resolve_provider_and_model_name
│ ├─ "openai/xxx" 斜杠前缀 _split_slash_provider_override
│ ├─ "generic.xxx" 点号前缀 _split_dotted_provider_prefix
│ └─ 都没有 → 按模型名猜默认 provider(ModelDatabase / Bedrock 模式)
│
└─⑤ 校验/派生 ──────────► 推理档、instant、transport、service_tier 合法性
reasoning / instant / _validate_transport_constraints ...
3.2 provider 前缀:两种写法
第 ④ 步是路由的关键。fast-agent 支持两种前缀语法,含义相同但形态不同:
- 斜杠前缀
openai/gpt-4.1:用/分隔,前半段必须是合法Provider值 (_split_slash_provider_override,llm/model_factory.py:628)。 - 点号前缀
generic.qwen2.5、hf.Qwen/...:用.分隔,支持取前 1 段或前 2 段做 provider (_split_dotted_provider_prefix,llm/model_factory.py:640;它按(2, 1)依次尝试, 所以anthropic-vertex这类带连字符的名字也能命中)。
如果两种前缀都没有,就按模型名反查默认 provider:先问 ModelDatabase.get_default_provider,
不行再看是否匹配 Bedrock 的模型名模式(_default_provider_for_model,:651)。都失败就抛
ModelConfigError。
3.3 查询覆盖:? 后面那串
第 ① 步把 ? 后面的部分用标准 parse_qsl 解析成键值对,再交给 _parse_query_overrides
(llm/model_factory.py:506)逐类解析,产出一个冻结的 ModelQueryOverrides 数据类。支持的键
被白名单 SUPPORTED_MODEL_QUERY_KEYS(:88)约束——写了不认识的键会直接报错,不会被悄悄忽略。
支持的查询键分几类:
| 类别 | 键 | 例子 | 说明 |
|---|---|---|---|
| 推理 | reasoning、instant | ?reasoning=low | 档位 low/medium/high…,或整数 budget |
| 输出 | verbosity、structured | ?verbosity=low | 文本详略;结构化输出模式 json/tool_use |
| 结构化工具 | structured_tools 等 | ?structured_tools=defer | 工具与结构化同请求的策略 |
| 联网 | web_search、x_search、web_fetch | ?web_search=on | 服务端网络工具开关(布尔) |
| 上下文 | context | ?context=1m | 仅支持 1m(Anthropic 百万上下文 beta) |
| 传输 | transport、service_tier | ?transport=ws | WebSocket/SSE;fast/flex 服务档 |
| 采样 | temperature/top_p/top_k/min_p/presence_penalty/repetition_penalty | ?temperature=0.6 | 各家采样参数,含驼峰别名 |
| 预算 | task_budget | ?task_budget=2000 | Anthropic 任务 token 预算 |
布尔类查询走 parse_boolean_alias,on/off/true/false/1/0 都认;不认的值会抛出带提示的
ModelConfigError(_parse_bool_query,:348)。
3.4 别名(preset)展开
第 ③ 步把短别名换成完整字符串。别名表是 ModelFactory.MODEL_PRESETS(:749),几十条,例如:
"sonnet" -> "claude-sonnet-4-6"
"opus" -> "claude-opus-4-8"
"codex" -> "responses.gpt-5.3-codex"
"grok" -> "xai.grok-4.3"
"minimax" -> "hf.MiniMaxAI/MiniMax-M3:together?temperature=1.0&top_p=0.95&top_k=40"
注意最后一条:preset 本身可以带 ? 查询默认值。_expand_model_preset(:588)会把 preset
里的查询解析成"默认",而用户显式写的 ?... 会覆盖这些默认(通过
ModelQueryOverrides.with_defaults,:189——用户值优先、缺省才回落到 preset)。
运行时的别名表比静态表更大:get_runtime_presets(:880)会把静态 MODEL_PRESETS、精选目录
(ModelSelectionCatalog)和 overlay 注册表合并进来。
3.5 一个派生小机制:instant
instant=on 不是一个独立开关,而是被翻译成"关掉推理"。解析逻辑在
parse_model_spec 尾段(llm/model_factory.py:931-945):它只对
moonshotai/kimi-k2.5 / kimi-k2.6 生效,且不能和 reasoning 同时给;命中后转成一个
ReasoningEffortSetting(kind="toggle", value=not instant)。这是"一个用户友好别名映射到底层
统一表示"的典型例子。
3.6 原理演示
把上面几步用示意代码串起来,建立直觉:
# 示意,非源码:模型串解析的骨架
def parse(model_string, presets):
spec, overrides = split_query(model_string) # ① 切 "?"
spec, suffix = split_suffix(spec) # ② 切 ":"
expanded, preset_defaults = expand_preset(spec, presets) # ③ 展开别名
if suffix:
expanded = f"{expanded}:{suffix}"
overrides = overrides.with_defaults(preset_defaults) # 用户覆盖优先
provider, model_name = resolve_provider(expanded) # ④ 定 provider
validate_transport(provider, model_name, overrides) # ⑤ 校验
return ParsedModelSpec(provider, model_name, overrides.reasoning, overrides)
重点看: 顺序(先查询、后后缀、再别名、最后 provider)和"用户显式值覆盖 preset 默认"这两点, 决定了同一个字符串在各种写法下都能被无歧义地解析。
真实产物是 ParsedModelSpec(llm/model_factory.py:226),一个冻结数据类,携带
provider / model_name / reasoning_effort / query_overrides,并能用 to_model_config() 转成
公开的 ModelConfig。
4. 核心机 制二:从解析结果到能发请求的实例
ParsedModelSpec 还只是"字符串层面"的解析。要真正发请求,还得知道这个模型的能力(上下文
窗口、是否支持推理、wire 上真正的模型名等)。这一步由 resolve_model_spec(:972)完成。
4.1 补齐能力:ResolvedModelSpec
resolve_model_spec 先看有没有 overlay(用户自定义模型覆盖),没有就走 preset/direct,然后:
- 查
ModelDatabase.resolve_wire_model_name得到发到线上的真实模型名(别名/展示名 → wire 名); - 用
resolve_base_model_params拿ModelParameters(能力元数据); - 打包成
ResolvedModelSpec(llm/resolved_model.py:23)。
ResolvedModelSpec 是个"能力查询门面":它把一堆 @property 挂在 model_params 上,例如
context_window、json_mode、reasoning_mode、response_transports、structured_tool_policy。
上层要判断"这个模型支不支持 X"时,问它即可,不必再碰数据库。
它还有两个关键方法:
build_llm_kwargs(:176):把ModelConfig里的选择(推理档、联网、长上下文、task_budget…) 翻 译成构造子类要用的 kwargs,并且按 provider 过滤——例如x_search只对Provider.XAI传入,web_fetch只对Provider.ANTHROPIC传入。apply_request_defaults(:143):把采样参数、service_tier、maxTokens等作为默认值填进RequestParams(只填用户没显式设过的字段)。
4.2 造对象:工厂闭包 + 懒加载
create_factory(llm/model_factory.py:1023)不直接造 LLM,而是返回一个闭包,等 agent 拿着
自己(name、instruction)来"附着":
# 真实结构简化自 create_factory / factory
def factory(agent, request_params=None, **kwargs):
effective = resolved_model.apply_request_defaults(request_params)
llm_args = {
"model": resolved_model.wire_model_name,
"resolved_model_spec": resolved_model,
"request_params": effective,
"name": getattr(agent, "name", "fast-agent"),
"instructions": getattr(agent, "instruction", None),
**resolved_model.build_llm_kwargs(), # 推理档/联网/长上下文…
**kwargs,
}
return llm_class(**llm_args) # llm_class 是解析出来的 provider 子类
provider 子类是懒加载的:_load_provider_class(:1078)按 _PROVIDER_CLASS_PATHS(:97)
里的 (模块路径, 类名) 用 import_module 动态导入,导入结果缓存在 PROVIDER_CLASSES。这样
CLI 启动时不必导入所有厂商 SDK——只在真正用到某家时才 import 它的重依赖。
provider → 类 的映射(节选):
| Provider 值 | LLM 类 | 模块 |
|---|---|---|
anthropic | AnthropicLLM | llm/provider/anthropic/llm_anthropic.py |
openai | OpenAILLM | llm/provider/openai/llm_openai.py |
google | GoogleNativeLLM | llm/provider/google/llm_google_native.py |
bedrock | BedrockLLM | llm/provider/bedrock/llm_bedrock.py |
deepseek | DeepSeekLLM | llm/provider/openai/llm_deepseek.py |
generic | GenericLLM | llm/provider/openai/llm_generic.py |
xai | XAIResponsesLLM | llm/provider/openai/xai_responses.py |
fast-agent | PassthroughLLM | llm/internal/passthrough.py |
playback / silent / slow 这三个不是 provider,而是模型名,走另一张
_MODEL_SPECIFIC_CLASS_PATHS(:133)。
5. 核心机制三:统一基类 FastAgentLLM
所有会发请求的 LLM 都长在 FastAgentLLM(llm/fastagent_llm.py:101)上。它的类型签名是
Generic[MessageParamT, MessageT]——两个类型参数分别是输入消息类型(发给厂商的)和
输出消息类型(厂商回来的),让每个子类都能带上自己厂商的强类型消息对象。
例如 Anthropic 子类声明为 AnthropicLLM(FastAgentLLM[BetaMessageParam, BetaMessage])
(llm/provider/anthropic/llm_anthropic.py:454),OpenAI 子类是
FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage](:170)。
5.1 模板方法:通用流程 vs provider 细节
基类用模板方法模式:公开的 generate / structured 负责所有 provider 通用的事,把厂商特有
的一步留成抽象方法交给子类。
子类必须实现的两个抽象方法:
| 抽象方法 | 位置 | 干什么 |
|---|---|---|
_apply_prompt_provider_specific | fastagent_llm.py:908 | 真正拼厂商请求、发出去、解析回复 |
_convert_extended_messages_to_provider | fastagent_llm.py:1409 | 把统一消息 PromptMessageExtended 转成厂商格式 |
generate(:766)一次调用里,基类做的通用步骤:
generate(messages, request_params, tools)
│
├─ 处理 ***SAVE_HISTORY 之类控制消息
├─ get_request_params(): 合并默认参数 + 本次覆盖
├─ _prepare_structured_request(): 结构化意图预处理(见 §7)
├─ 决定是否抑制 tools / schema(structured tool policy)
├─ _execute_with_retry( _apply_prompt_provider_specific ) ← 委托子类真正发请求
├─ 记录时延通道(TTFT / 总时长)
└─ 统计 usage、计工具数,返回 assistant 消息
注意重试是基类统一做的:_execute_with_retry(:642)对暂时性错误(429/503/超时/过载等,
词表见 _RETRYABLE_PROVIDER_KEY_ERROR_TERMS,:81)按线性退避重试,默认 2 次
(_resolve_retry_count,:743,可用 config 或 FAST_AGENT_RETRIES 覆盖)。子类完全不用管重试。
5.2 请求参数的合并
get_request_params(:1238)把三层参数叠起来:子类初始化时的默认 → 解析规格注入的默认 → 本次
调用的覆盖。真正把 RequestParams 拍平成厂商 API 参数字典的是 prepare_provider_arguments
(:1185):它按 BASE_EXCLUDE_FIELDS(:124,加上子类各自的排除集)剔掉纯内部字段
(如 tool_execution_handler、batch_context),再把剩下的合并进 base_args。
5.3 provider-中立的三个旋钮
基类把几个"跨厂商概念"抽象成中立设置,子类只需在自己 API 上落地:
- 推理努力(reasoning effort):
set_reasoning_effort(:456)。设置时会用模型的ReasoningEffortSpec(能力声明)校验——档位不合法、或把 effort 映射成 budget,都在validate_reasoning_setting(llm/reasoning_effort.py:237)里完成。DeepSeek 子类甚至重写了set_reasoning_effort把通用档位映射到自家的 high/max(llm/provider/openai/llm_deepseek.py:55)。 - 文本详略(text verbosity):
set_text_verbosity(:476),同样按 spec 校验。 - 服务端能力开关:
web_search/x_search/web_fetch/service_tier/task_budget都是"基类给默认 False/None,支持的子类重写*_supported属性并落地"。例如 Anthropic 子类 重写web_search_supported(llm/provider/anthropic/llm_anthropic.py:1169)。
5.4 usage 追踪
每个实例带一个 UsageAccumulator(_initialize_usage_tracking,:283)。每次 turn 结束,子类构造
一个 TurnUsage 交给累加器;基类还会 count_tools 记录工具调用数,并把 usage 挂到响应的通道里
(_append_usage_channel,:881)。上下文窗口大小也在这里从解析规格里取,供"还剩多少上下文"显示。
6. 各 provider 适配:一条继承链的分叉
provider 子类不是各写各的,而是共享一条继承链。核心分叉在 OpenAI 家族——很多"OpenAI 兼容"的 厂商(DeepSeek、Groq、Aliyun、OpenRouter、Ollama…)都能复用 OpenAI 的 Chat Completions 逻辑。
怎么读这张图: 箭头是"继承自";越往下越具体。左边独立的三支各自直接继承基类。
FastAgentLLM (泛型基类)
┌───────────────┬──────────────┬───────────────────────────┐
│ │ │ │
AnthropicLLM GoogleNativeLLM BedrockLLM OpenAILLM
(Messages API) (GenAI 原生) (AWS Bedrock) (Chat Completions)
│
OpenAICompatibleLLM
(共享 JSON 结构化提示词)
┌─────────┴─────────┐
DeepSeekLLM GroqLLM …
几点值得注意:
GenericLLM(llm/provider/openai/llm_generic.py:12)直接继承OpenAILLM,默认指向本地 Ollama(http://localhost:11434/v1),这就是generic.xxx模型串背后的东西。OpenAICompatibleLLM(llm/provider/openai/llm_openai_compatible.py:13)在 OpenAI 之上补了 "不支持原生结构化输出时,用提示词逼模型吐 JSON"的共享逻辑(见 §7)。DeepSeek、Groq 继承它。- 每个子类可用
config_section(如OpenAILLM.config_section,llm/provider/openai/llm_openai.py:177) 指定自己在配置文件里读哪一节的 api_key / base_url,回落到 provider 值。 - Bedrock 特殊:它还提供
matches_model_pattern(llm/provider/bedrock/llm_bedrock.py:462), 给 §3.2 的"按模型名猜默认 provider"当兜底判据。
7. 结构化输出:两种模式与工具共存策略
让模型返回严格符合 schema 的 JSON,是 agent 场景的刚需。fast-agent 在基类层统一了两条入口和 两种底层模式,还专门处理了"结构化 + 工具"能不能同请求的坑。
7.1 两条入口
structured(messages, model, ...)(fastagent_llm.py:933):传一个 Pydantic 模型类, 返回(解析好的实例 | None, 原始消息)。structured_schema(messages, schema, ...)(:992):传一份原始 JSON Schema,先用validate_json_schema_definition校验 schema 本身,再让模型产出、并用validate_json_instance校验结果(两者都在llm/structured_schema.py:20/:28,底层是jsonschema库)。
基类兜底的解析在 _structured_from_multipart(:1132):取最后一段文本,用 pydantic_core.from_json
(允许部分解析)转对象再 model_validate;失败就记 warning 返回 None,不会抛崩。
7.2 两种底层模式
structured 查询键或 StructuredOutputMode(llm/structured_output_mode.py:5)只有两个取值:
| 模式 | 含义 |
|---|---|
json | 走厂商的 JSON/response_format 通道,直接约束输出为 JSON |
tool_use | 把 schema 包成一个"工具",逼模型以工具调用形式返回结构化数据 |
对不支持原生结构化的 OpenAI 兼容模型,OpenAICompatibleLLM 会降级为提示词:把 schema 渲染成
一段"你必须严格按这个格式输出 JSON"的指令追加到用户消息后
(STRUCTURED_PROMPT_TEMPLATE 与 _build_structured_prompt_instruction_from_schema,
llm/provider/openai/llm_openai_compatible.py)。
7.3 结构化工具策略(structured tool policy)
有些模型/厂商不能在同一次请求里既给工具又要结构化输出。fast-agent 用一个三态策略应对
(_resolve_structured_tool_policy,fastagent_llm.py:335;auto 时回落到模型元数据或默认 always):
| 策略 | 行为 |
|---|---|
always | 工具和结构化 schema 可以同请求,不做特殊处理 |
no_tools | 有结构化意图时直接不带工具 |
defer | 两阶段:先带工具正常跑(此轮压掉 schema),等工具结果回来后,最后一轮压掉工具、只要结构化答案 |
defer 的两个判断分别在 _should_suppress_structured_schema_for_tools(:372,第一阶段压 schema)
和 _should_suppress_tools_for_structured_final(:385,末阶段压工具)。这是本章里最"隐"的一个
设计:它让"能用工具的 agent"也能可靠拿到结构化终答,即使底层模型不支持二者共存。
8. 内部测试型 LLM:不打真实 API 的四个替身
llm/internal/ 下有四个"假" LLM,专门用于测试、workflow 编排和调试。它们都继承自
PassthroughLLM,共享"不发请求、直接回显/回放"的骨架。
| 名字 | 类 | 触发方式 | 用途 |
|---|---|---|---|
| passthrough | PassthroughLLM | provider fast-agent | 原样回显输入;可用 ***CALL_TOOL / ***FIXED_RESPONSE 指令模拟工具调用与固定回复 |
| playback | PlaybackLLM | 模型名 playback | 把加载进来的历史消息按顺序回放 assistant 消息 |
| silent | SilentLLM | 模型名 silent | 像 passthrough,但压掉显示输出、usage 恒为零 |
| slow | SlowLLM | 模型名 slow | 回复前 sleep(3),模拟慢响应 |
passthrough 是核心(llm/internal/passthrough.py:23)。它的 _apply_prompt_provider_specific
(:73)会:若最后一条消息以 ***CALL_TOOL <name> <json> 开头,就解析成一个真实的
CallToolRequest 并把 stop reason 设为 TOOL_USE;否则把连续的 user 文本拼起来回显。这让你能在
不花 token 的情况下,测通整个工具循环(见 03)。
playback(llm/internal/playback.py:17)重写了 generate(:55):第一次调用把消息存下并回
"HISTORY LOADED",之后每次调用吐下一条 assistant 消息(_get_next_assistant_message,:36);
放完了就回 "MESSAGES EXHAUSTED"。适合把一段录制好的对话精确重放。
silent(llm/internal/silent.py:18)只做一件事:换上一个 ZeroUsageAccumulator(:10),
add_turn 什么都不做——所以并行 workflow 的 fan-in agent 可以聚合结果而不污染控制台/计费。
9. 巧妙之处(可借鉴的技术)
-
模型串 = 可解析的 URL。 把 provider、模型、能力开关全塞进一个字符串,用
?query表达覆盖、 用前缀表达路由——用户零代码即可精确选型。解析器parse_model_spec(llm/model_factory.py:896) 把"先切什么后切什么"的顺序固化,消除歧义。 -
未知查询键直接报错。
_raise_for_unsupported_query_keys(:358)用白名单挡住拼错的键, 避免?web_serch=on这种笔误被静默忽略——配置错误尽早暴露。 -
懒加载 provider 类。
_PROVIDER_CLASS_PATHS(:97)存的是字符串路径而非类对象,真正用到才import_module——CLI 启动不必背上所有厂商 SDK 的导入成本。 -
能力声明驱动校验。 推理档位不是硬编码,而是每个模型带一份
ReasoningEffortSpec,由validate_reasoning_setting(llm/reasoning_effort.py:237)统一校验/映射(effort→budget)。 加新模型只需补元数据,不用改解析逻辑。 -
结构化 + 工具的两阶段 defer。
defer策略(fastagent_llm.py:385)让不支持二者共存的模型 也能既用工具又拿结构化终答,对上层透明。
10. 边界与局限
-
模型能力靠
ModelDatabase元数据。 数据库里没有的新模型,resolve_base_model_params会拿不到ModelParameters,能力查询(上下文窗口、json_mode 等)会退化为None/默认值——不是崩,但会少了 精确的能力约束。 -
instant只服务两个 Kimi 模型。 硬编码白名单(llm/model_factory.py:937),别的模型写instant=on会报错。 -
传输/服务档有 provider 限制。 WebSocket 传输只对少数 Responses/XAI provider 开放 (
_validate_transport_constraints,:692);service_tier=flex对codexresponses直接拒绝 (_validate_service_tier_constraints,:720)。 -
结构化解析容错但不保证。 基类解析失败只记 warning 返回
None(:1143),要不要重试/报错由 上层决定;对没有原生结构化能力的模型,靠提示词"劝"模型守格式,并不能 100% 保证合法 JSON。
11. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 解析入口 | llm/model_factory.py | ModelFactory.parse_model_spec |
| 切查询 / 切后缀 | llm/model_factory.py | _split_model_spec_and_query、_split_model_suffix |
| 查询覆盖解析 | llm/model_factory.py | _parse_query_overrides、ModelQueryOverrides、SUPPORTED_MODEL_QUERY_KEYS |
| provider 前缀 | llm/model_factory.py | _split_slash_provider_override、_split_dotted_provider_prefix |
| provider 兜底 | llm/model_factory.py | _resolve_provider_and_model_name、_default_provider_for_model |
| 别名/preset | llm/model_factory.py | MODEL_PRESETS、get_runtime_presets、_expand_model_preset |
| 解析结果载体 | llm/model_factory.py | ParsedModelSpec、ModelConfig |
| 解析规格 | llm/resolved_model.py | ResolvedModelSpec、build_llm_kwargs、apply_request_defaults |
| 工厂 / 懒加载 | llm/model_factory.py | create_factory、_load_provider_class、_PROVIDER_CLASS_PATHS |
| 统一基类 | llm/fastagent_llm.py | FastAgentLLM、generate、structured |
| 抽象方法 | llm/fastagent_llm.py | _apply_prompt_provider_specific、_convert_extended_messages_to_provider |
| 参数合并/重试 | llm/fastagent_llm.py | get_request_params、prepare_provider_arguments、_execute_with_retry |
| 结构化工具策略 | llm/fastagent_llm.py | _resolve_structured_tool_policy、_should_suppress_tools_for_structured_final |
| 推理努力 | llm/reasoning_effort.py | ReasoningEffortSetting、ReasoningEffortSpec、validate_reasoning_setting |
| 结构化模式 | llm/structured_output_mode.py | StructuredOutputMode、parse_structured_output_mode |
| Schema 校验 | llm/structured_schema.py | validate_json_schema_definition、validate_json_instance |
| Anthropic 适配 | llm/provider/anthropic/llm_anthropic.py | AnthropicLLM |
| OpenAI 适配 / 兼容层 | llm/provider/openai/llm_openai.py、llm_openai_compatible.py | OpenAILLM、OpenAICompatibleLLM |
| Google / Bedrock | llm/provider/google/llm_google_native.py、llm/provider/bedrock/llm_bedrock.py | GoogleNativeLLM、BedrockLLM |
| 测试型 LLM | llm/internal/passthrough.py、playback.py、silent.py、slow.py | PassthroughLLM、PlaybackLLM、SilentLLM、SlowLLM |