跳到主要内容

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 只调用同一组方法(generatestructured)。
  • 一套声明式选择法:用户不写代码,只写一个字符串就能选定厂商、模型、推理档位、附加能力。

用起来什么样。 下面这些都是合法的模型字符串,展示了它能表达多少信息:

模型字符串表达的意思
sonnet别名(preset),展开成 claude-sonnet-4-6
gpt-5.1?reasoning=lowOpenAI 的 gpt-5.1,推理档位设为 low
claude-sonnet-4-6?web_search=onAnthropic Sonnet,打开服务端联网搜索
generic.qwen2.5generic(本地 Ollama 等)provider,模型名 qwen2.5
hf.Qwen/Qwen3.5-397B:novita?temperature=0.6HuggingFace 路由到 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_specParsedModelSpec纯字符串拆解:别名展开、provider 前缀、? 查询覆盖
解析规格ModelFactory.resolve_model_specResolvedModelSpecModelDatabase 补能力元数据、wire 模型名、overlay
造对象ModelFactory.create_factoryfactory 闭包 → 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/**
内部测试 LLMpassthrough / playback / silent / slowllm/internal/**
结构化输出模式解析 + JSON Schema 校验llm/structured_output_mode.pyllm/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.5hf.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)约束——写了不认识的键会直接报错,不会被悄悄忽略。

支持的查询键分几类:

类别例子说明
推理reasoninginstant?reasoning=low档位 low/medium/high…,或整数 budget
输出verbositystructured?verbosity=low文本详略;结构化输出模式 json/tool_use
结构化工具structured_tools?structured_tools=defer工具与结构化同请求的策略
联网web_searchx_searchweb_fetch?web_search=on服务端网络工具开关(布尔)
上下文context?context=1m仅支持 1m(Anthropic 百万上下文 beta)
传输transportservice_tier?transport=wsWebSocket/SSE;fast/flex 服务档
采样temperature/top_p/top_k/min_p/presence_penalty/repetition_penalty?temperature=0.6各家采样参数,含驼峰别名
预算task_budget?task_budget=2000Anthropic 任务 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_paramsModelParameters(能力元数据);
  • 打包成 ResolvedModelSpec(llm/resolved_model.py:23)。

ResolvedModelSpec 是个"能力查询门面":它把一堆 @property 挂在 model_params 上,例如 context_windowjson_modereasoning_moderesponse_transportsstructured_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_tiermaxTokens 等作为默认值填进 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 类模块
anthropicAnthropicLLMllm/provider/anthropic/llm_anthropic.py
openaiOpenAILLMllm/provider/openai/llm_openai.py
googleGoogleNativeLLMllm/provider/google/llm_google_native.py
bedrockBedrockLLMllm/provider/bedrock/llm_bedrock.py
deepseekDeepSeekLLMllm/provider/openai/llm_deepseek.py
genericGenericLLMllm/provider/openai/llm_generic.py
xaiXAIResponsesLLMllm/provider/openai/xai_responses.py
fast-agentPassthroughLLMllm/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_specificfastagent_llm.py:908真正拼厂商请求、发出去、解析回复
_convert_extended_messages_to_providerfastagent_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_handlerbatch_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,共享"不发请求、直接回显/回放"的骨架。

名字触发方式用途
passthroughPassthroughLLMprovider fast-agent原样回显输入;可用 ***CALL_TOOL / ***FIXED_RESPONSE 指令模拟工具调用与固定回复
playbackPlaybackLLM模型名 playback把加载进来的历史消息按顺序回放 assistant 消息
silentSilentLLM模型名 silent像 passthrough,但压掉显示输出、usage 恒为零
slowSlowLLM模型名 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=flexcodexresponses 直接拒绝 (_validate_service_tier_constraints,:720)。

  • 结构化解析容错但不保证。 基类解析失败只记 warning 返回 None(:1143),要不要重试/报错由 上层决定;对没有原生结构化能力的模型,靠提示词"劝"模型守格式,并不能 100% 保证合法 JSON。


11. 代码地图(导航索引)

主题文件路径符号名
解析入口llm/model_factory.pyModelFactory.parse_model_spec
切查询 / 切后缀llm/model_factory.py_split_model_spec_and_query_split_model_suffix
查询覆盖解析llm/model_factory.py_parse_query_overridesModelQueryOverridesSUPPORTED_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
别名/presetllm/model_factory.pyMODEL_PRESETSget_runtime_presets_expand_model_preset
解析结果载体llm/model_factory.pyParsedModelSpecModelConfig
解析规格llm/resolved_model.pyResolvedModelSpecbuild_llm_kwargsapply_request_defaults
工厂 / 懒加载llm/model_factory.pycreate_factory_load_provider_class_PROVIDER_CLASS_PATHS
统一基类llm/fastagent_llm.pyFastAgentLLMgeneratestructured
抽象方法llm/fastagent_llm.py_apply_prompt_provider_specific_convert_extended_messages_to_provider
参数合并/重试llm/fastagent_llm.pyget_request_paramsprepare_provider_arguments_execute_with_retry
结构化工具策略llm/fastagent_llm.py_resolve_structured_tool_policy_should_suppress_tools_for_structured_final
推理努力llm/reasoning_effort.pyReasoningEffortSettingReasoningEffortSpecvalidate_reasoning_setting
结构化模式llm/structured_output_mode.pyStructuredOutputModeparse_structured_output_mode
Schema 校验llm/structured_schema.pyvalidate_json_schema_definitionvalidate_json_instance
Anthropic 适配llm/provider/anthropic/llm_anthropic.pyAnthropicLLM
OpenAI 适配 / 兼容层llm/provider/openai/llm_openai.pyllm_openai_compatible.pyOpenAILLMOpenAICompatibleLLM
Google / Bedrockllm/provider/google/llm_google_native.pyllm/provider/bedrock/llm_bedrock.pyGoogleNativeLLMBedrockLLM
测试型 LLMllm/internal/passthrough.pyplayback.pysilent.pyslow.pyPassthroughLLMPlaybackLLMSilentLLMSlowLLM