跳到主要内容

LLM 抽象层:provider 无关、BYOM、跨模型回退与结构化输出

30 秒导读: DocsGPT 的 agent(见 01)和工具循环(见 02) 从头到尾不知道自己在跟 OpenAI、Anthropic 还是 Google 说话。中间隔着一层 BaseLLM:统一的 gen / gen_stream 契约在上,十几个 provider 子类在下。本章讲清这层怎么把"provider 差异"藏起来—— 包括用户自带模型(BYOM)怎么解析、主模型挂了怎么在同一次调用里换一家、以及不同 provider 的流式 chunk 怎么被归一成同一个 LLMResponse


1. 这是什么(零基础也能懂)

一句话定义: LLM 抽象层是一层"翻译 + 适配"代码,让上层业务只用一套 API 就能调用任意大模型 provider。

它要解决的问题。 每家大模型的 SDK 都不一样:

providerPython SDK调用方式流式 chunk 长什么样
OpenAIopenaiclient.chat.completions.createclient.responses.createchoice.delta.content
Anthropicanthropicanthropic.completions.createcompletion.completion
Googlegoogle.genaiclient.models.generate_content_streamcandidate.content.parts[].text

如果 agent 直接写死其中一家,换 provider 就要重写一遍。抽象层的价值就是:agent 只调 llm.gen_stream(...), 底下是谁、怎么拼参数、怎么解流,它一概不管。

给谁用 / 典型场景:

  • 部署方想把默认模型从 GPT 换成 Gemini —— 改一个配置,agent 代码不动。
  • 终端用户想用自己的 API key 接一个自建的 OpenAI 兼容端点(BYOM,Bring Your Own Model)—— 注册一条记录即可。
  • 主模型被限流(429)—— 系统在同一次请求内自动换到备份模型,用户几乎无感。

一句话直觉:BaseLLM 想成电源插座标准。你的电器(agent)只认插座的形状;背后是水电、火电还是风电 (OpenAI / Anthropic / Google),电器不需要知道。BYOM 就是允许你自己接一根电线进来,只要插头符合标准。


2. 顶层全景(它大概怎么转)

这一层由四类角色组成,职责分明:

角色干什么在哪
工厂 LLMCreator按 provider 名选实现类;为 BYOM 把注册表 UUID 解析成真实端点application/llm/llm_creator.py:LLMCreator
基类 BaseLLM定义 gen/gen_stream 契约、能力协商、跨 provider 回退application/llm/base.py:BaseLLM
provider 子类每家一个,实现 _raw_gen/_raw_gen_stream,做 SDK 适配application/llm/openai.pyanthropic.pygoogle_ai.py
handler把不同 provider 的流式 chunk 归一LLMResponse/ToolCallapplication/llm/handlers/

主线走一遍(高层,不进代码):

agent._llm_gen(messages) (第 2 章:agent 侧)
│ 拼 model / tools / 结构化输出格式

llm.gen_stream(...) ── BaseLLM:发日志、挂装饰器(计费/缓存)


_execute_with_fallback ───── 主模型失败?→ 悄悄切到 fallback_llm
│ 并标记 _responding_provider

_raw_gen_stream(...) ── provider 子类:拼这家 SDK 的参数、发请求、吐 chunk


handler.parse_response(chunk) ── 按"真正响应的 provider"选 handler
│ 归一成 LLMResponse(content, tool_calls, ...)

工具循环 / 文本流 (第 2 章:LLMHandler 编排)

怎么读这张图: 从上到下是一次生成调用的路径。左边是"上层不变的接口",越往下越贴近某家 provider 的真实 SDK;_execute_with_fallback 是那道"provider 可以中途被换掉"的暗门,本章 §5 专门讲它。

工厂在创建时决定用哪家;回退在调用时可能再换一家。记住这两个时刻,后面就不会绕晕。


3. 工厂:LLMCreator.create_llm —— 谁来接这次请求

本节讲清"给一个 llm_name 和一个 model_id,怎么造出正确的 LLM 实例"。

3.1 按名选实现类

第一步很朴素:拿 provider 名去插件注册表里查对应的实现类。

真实实现 application/llm/llm_creator.py:39-41(create_llm):

plugin = PROVIDERS_BY_NAME.get(type.lower())
if plugin is None or plugin.llm_class is None:
raise ValueError(f"No LLM class found for type {type}")

PROVIDERS_BY_NAME{provider 名: Provider 插件} 的字典,由 application/llm/providers/__init__.py:32 (ALL_PROVIDERS)按固定顺序构建。每个插件(application/llm/providers/base.py:13,Provider)声明两样东西: 自己的 name 和要实例化的 llm_class

内置 provider 与它们的实现类:

provider 名实现类说明
openaiOpenAILLM云端 OpenAI
openai_compatibleOpenAILLMMistral / Together / Ollama / LM Studio 等 OpenAI 兼容端点
anthropicAnthropicLLMClaude
googleGoogleLLMGemini
groq / novita / openrouter / premai / sagemaker / llama_cpp各自子类其余 provider,插件同构,不逐一展开
huggingfacellm_class = None出现在目录里但不可派发(工厂会直接报错)

注意 openaiopenai_compatible 共用同一个 OpenAILLM 类——差异不在代码,而在"端点配置从哪来", 这正是下一节 BYOM 要解决的。

3.2 BYOM:把注册表 UUID 解析成真实端点

它要解决的小问题。 内置模型的 model_id 就是上游 API 认得的名字(如 gpt-4o)。但用户自带模型时, DocsGPT 给这条记录发的是一个内部 UUID——上游 API 根本不认。所以工厂必须把 UUID 翻译回三样东西: 真实的 upstream model 名base_urlapi_key

思路。 去模型注册表按 model_id 查出那条 AvailableModel 记录,记录上带着这三样,谁有值谁就覆盖调用方传入的默认值。

真实实现 application/llm/llm_creator.py:61-84(create_llm):

model = ModelRegistry.get_instance().get_model(model_id, user_id=user_id)
if model is not None:
capabilities = getattr(model, "capabilities", None) # 见 §4.3
...
if model.api_key:
api_key = model.api_key
if model.base_url:
base_url = model.base_url
# BYOM 的 registry id 是 UUID;上游 API 需要用户填的真实模型名
if model.upstream_model_id:
upstream_model_id = model.upstream_model_id

这里有几个关键点,拆开讲:

(a) 按 user_id 分层查找。 BYOM 模型属于某个用户,查找必须带上 user_id 才能命中"该用户的私有模型层"; 内置模型不带 user_id 也能查到,所以保持了向后兼容。get_model 的分层逻辑见 application/core/model_registry.py:357-364:先查用户层,miss 再查全局层。

(b) model_user_id —— 替谁解析。 user_id 默认取 decoded_token['sub'](调用者),但共享 agent 场景里,agent 存的 default_model_id属主的 BYOM UUID,而 decoded_token 代表的是调用者。这时要显式 传 model_user_id 指定"用属主的层去解析"(create_llm docstring,llm_creator.py:23-31)。

(c) 这就是 base.pyself.upstream_model_id 的来历。 工厂把解析出的真实名字塞进构造参数 model_id=upstream_model_id(llm_creator.py:118);同时把原始 UUID 单独盖在 _canonical_model_id 上给计费用:

llm._canonical_model_id = model_id # llm_creator.py:129,UUID 归 UUID,给 token_usage

于是 llm.model_id 永远是上游认得的名字,agent 侧 _llm_gen 用它发请求(agents/base.py:707), 而计费仍能按 canonical UUID 归账。

3.3 派发前的安全闸(BYOM 特有)

用户能自填 base_url 就意味着"服务端可能被诱导去访问内网地址"(SSRF)。工厂在派发前加了两道闸:

做什么代码
拒绝无 key 的用户模型user-source 记录若没有自己的 api_key,直接报错——否则会把服务端 settings.API_KEY 泄漏给用户的 base_urlllm_creator.py:68-76
重新校验 base_url + 钉住 IP对 user-source 的 base_url 再跑一次 validate_user_base_url,并给 SDK 注入一个"解析一次、绑定已验证 IP"的 pinned_httpx_client,关掉 DNS-rebinding 的 TOCTOU 窗口llm_creator.py:90-110

第二道闸目前只对 openai_compatible 生效(llm_creator.py:102):未来的 BYOM provider 必须显式接入 http_client 才享受这层保护。这是一处"安全默认关闭、显式开启"的谨慎设计。


4. 基类 BaseLLM —— 所有 provider 都要遵守的契约

本节讲 application/llm/base.py:13(BaseLLM)定义的"最小公约数":上层只依赖这几个方法,provider 想接入就得实现它们。

4.1 gen / gen_stream:对外的两个入口

上层只调这两个方法:gen(一次性)和 gen_stream(流式)。它们本身不发请求,而是做三件公共事,再委托给子类的 _raw_*:

  1. 发一条 start 日志(_emit_stream_start_log,给计费面板按 provider 分组)。
  2. 挂上装饰器:stream_cache(缓存)+ stream_token_usage(计费)。
  3. 交给 _execute_with_fallback 执行(见 §5)。

真实实现 application/llm/base.py:384-402(gen_stream):

def gen_stream(self, model, messages, stream=True, tools=None, *args, **kwargs):
has_attachments = bool(kwargs.get("_usage_attachments") or kwargs.get("attachments"))
self._emit_stream_start_log(model, messages, tools, has_attachments)
decorators = [stream_cache, stream_token_usage]
return self._execute_with_fallback(
"_raw_gen_stream", decorators, model=model, messages=messages,
stream=stream, tools=tools, *args, **kwargs,
)

子类真正干活的是两个抽象方法,不实现就没法实例化(base.py:404-410):

  • _raw_gen(self, model, messages, stream, tools, ...)
  • _raw_gen_stream(self, model, messages, stream, ...)

注意一处不寻常的签名。 子类的 _raw_gen_stream(self, baseself, model, ...) 第二个参数叫 baseself ——因为装饰器把方法当普通函数调用时会再传一次实例(base.py:172 method(self, ...))。看 provider 代码时 别把 baseself 当成笔误。

4.2 一次调用的完整数据流

把装饰器和抽象方法串起来:

gen_stream(model, messages, tools)
│ 发 start 日志

_execute_with_fallback("_raw_gen_stream", [stream_cache, stream_token_usage])
│ 给 _raw_gen_stream 套上缓存 + 计费装饰器

_raw_gen_stream(baseself, model, messages, ...) ← 子类实现,真正发 SDK 请求
│ 逐块 yield:纯文本 str / {"type":"thought"} / 原始 choice(带 tool_calls)

(回到第 2 章 LLMHandler.handle_streaming 消费这些 chunk)

子类 yield 的东西有三种形态,是上下层之间的非正式约定:

  • str —— 普通正文增量。
  • {"type": "thought", "thought": "..."} —— 思考/reasoning 增量(DeepSeek、Gemini thinking)。
  • 一个"choice/part 对象" —— 里面带 tool_calls,交给 handler 解析。

4.3 能力协商:这个模型到底支不支持 X

不是每个模型都支持工具调用、结构化输出、或图片附件。BaseLLM 用三个方法做"能力协商",让上层在派发前先问一句:

方法问什么默认代码
_supports_tools()支持 function calling 吗子类实现;OpenAI/Google 返回 Truebase.py:412-418
_supports_structured_output()支持 JSON schema 强约束吗基类默认 Falsebase.py:420-427
get_supported_attachment_types()能吃哪些 MIME 附件基类默认 []base.py:434-441

关键设计:注册表能力覆盖硬编码默认。 内置 provider 类里这几个方法常常硬编码返回 True,但 BYOM 用户 可能接了一个不支持工具的端点。所以工厂把注册表记录里的 capabilities(application/core/model_settings.py:31, ModelCapabilities)一路 forward 进构造参数(llm_creator.py:65,123),provider 在派发时优先读它。

以 OpenAI 为例(application/llm/openai.py:1120-1127,_supports_tools):

def _supports_tools(self):
if self.capabilities is not None: # 注册表带来的 per-model 能力
return bool(self.capabilities.supports_tools)
return True # 否则 OpenAI 默认支持

这条能力还会在 _raw_gen 里做纵深防御:即使上层传了 tools,若能力标志说不支持,就地丢弃 (openai.py:413-416)——防止把不被端点接受的字段发出去导致 400。


5. 跨 provider 回退 —— 主模型挂了,悄悄换一家

这是本层最巧妙的机制:主模型失败时,在同一次 gen_stream 调用内部切换到备份模型,上层几乎无感。

5.1 fallback_llm:懒加载的备胎

application/llm/base.py:48(fallback_llm,property)在第一次被读时才构造备份 LLM,顺序是:

  1. 先试每个 agent 的 backup_models(用户为这个 agent 配的备份列表)。
  2. 都失败,再退回全局 FALLBACK_* 设置(部署级兜底)。

构造备胎时它递归调用 LLMCreator.create_llm(base.py:80114),并把 model_user_id (BYOM 解析范围)一路传下去——这样共享 agent 的备份也在属主的层里解析(base.py:68121)。

备胎还被打上两个标记:_token_usage_source = "fallback"(计费面板据此归到 fallback 来源)和继承父级的 _request_id(同一次用户请求即使跑了回退,仍归在同一个 id 下)——见 base.py:93-96

5.2 _execute_with_fallback:非流式的一次性重试

非流式路径直接 try/except:主模型抛异常 → 若配了备胎,换备胎重试一次。

真实实现 application/llm/base.py:181-215(_execute_with_fallback)的骨架:

self._responding_provider = self.provider_name # 先记:现在是主 provider 在应答
try:
return decorated_method() # 主模型
except Exception as e:
if not self.fallback_llm:
raise
fallback = self.fallback_llm
self._responding_provider = fallback.provider_name # ★ 应答方换人了
fallback._emit_gen_start_log(...) # 让面板把这次归到备份 provider
# 直接对备胎的 _raw 方法套装饰器,而不是调 fallback.gen()——否则会
# 再进一次编排、经 fallback.fallback_llm 递归下去
...
return fallback_method(fallback, *args, **fallback_kwargs)

没有错误分类,这是刻意的。 无论主模型抛的是 5xx、429 还是 4xx 载荷被拒,都一律触发一次备胎尝试—— 因为"换一家 provider 往往能接受另一家拒绝的东西",这一次额外尝试是廉价保险(base.py:148-160 的 docstring)。 唯一例外:GeneratorExit/取消是 BaseException,不走这个 handler(客户端断开不触发回退)。

5.3 _stream_with_fallback:流式的中途接管

流式更棘手:异常不是在调用时抛,而是在迭代生成器时才抛。所以回退包了一层生成器 (application/llm/base.py:220-270,_stream_with_fallback):

self._responding_provider = self.provider_name
try:
yield from decorated_method() # 边流边可能炸
except Exception as e:
fallback = self.fallback_llm
self._responding_provider = fallback.provider_name # ★ 中途换人
fallback._emit_stream_start_log(...)
...
yield from fallback_method(fallback, *args, **fallback_kwargs)

哪怕主模型已经吐了半截又炸了,后半截也会从备胎继续流出来。

5.4 _responding_provider:与第 2 章的呼应

上面两处都在换人时更新 self._responding_provider(初值等于 provider_name,base.py:46)。这个字段是给 handler 层读的。回想第 2 章:handler 是按"主 provider"建的,它的 parse_response 只认主 provider 的 chunk 形状。 可回退可能把一个 Google-主的 agent 换成了 OpenAI 兼容的备胎——chunk 形状变了。

application/llm/handlers/base.py:78(_parse_for_response)就靠读 _responding_provider动态选对 handler:

provider = getattr(getattr(agent, "llm", None), "_responding_provider", None)
if not isinstance(provider, str):
return self.parse_response(response) # 无回退:老路径不变
return self._handler_for_provider(provider).parse_response(response) # 有回退:换 handler 解

一句话: _stream_with_fallback 在下面换了应答 provider 并盖章 _responding_provider, _parse_for_response 在上面读这个章、换用对应 provider 的 handler 去解 chunk——两处一发一收,tool_call 才不会被 "用错 handler"悄悄丢掉。这是"provider 无关"能扛住中途换 provider 的关键一环。


6. 具体 provider:各家挑关键差异点

三家主力 provider 各有一处"最不一样"的地方,值得单独看。其余 provider 结构同构,不展开。

6.1 OpenAI:Chat Completions vs Responses API 双通道

OpenAILLM(application/llm/openai.py:111)最特别的是它同时支持两套 OpenAI 端点:老的 /v1/chat/completions 和新的 /v1/responses。走哪套由注册表能力里的 api_flavor 决定 (application/core/model_settings.py:47,默认 "chat_completions"):

def _uses_responses_api(self): # openai.py:550
return (self.capabilities is not None
and getattr(self.capabilities, "api_flavor", "chat_completions") == "responses")

Responses API 带来两个 Chat Completions 没有的能力:

(a) previous_response_id 续接。OPENAI_RESPONSES_STORE 开启,服务端替你保存了上一轮, 下一轮只需带上 previous_response_id,并把已在服务端的历史裁掉、只发增量 (openai.py:912-928,_responses_gen + _trim_for_previous_response)。这个 id 的生成还带凭证/端点指纹 (responses_chain_key,openai.py:165-187)——换了模型/端点/key 就自动作废,避免串号。

(b) reasoning 跨工具往返的连续性。 Responses 会返回加密的 reasoning item;OpenAI 把它们按 function-call id 存起来(_remember_reasoning,openai.py:879-885),下一轮工具请求前再按 id 塞回去 (_to_responses_input,openai.py:599-654),让模型的思维链在一次工具循环里不断档。

因为两套端点的返回结构不同,OpenAILLM 造了一批伪 Chat 对象(_RespToolCall/_RespDelta/_RespChoice, openai.py:64-108),把 Responses 的 function_call item 伪装成 Chat 形状,这样同一个 OpenAI handler 和流式累加器 不用改就能消费。这是一处漂亮的适配器手法。

6.2 Anthropic:仍是老式 completions 端点

AnthropicLLM(application/llm/anthropic.py:13)在此提交里接的是 Anthropic 的旧版文本补全 API, 不是 Messages API:它手工拼 HUMAN_PROMPT/AI_PROMPT,调 self.anthropic.completions.create (anthropic.py:47-5368-73)。

由此带来几个诚实要说明的局限:

  • _raw_gen 只取 messages[0](context)和 messages[-1](question)拼成一个 prompt,不处理多轮/工具调用 (anthropic.py:42-44)。
  • 它没有覆盖 _supports_tools,tools 参数被直接忽略——tools 在这条通道上不生效。
  • 附件只支持图片、且靠 handler 侧 PDF→图片合成(get_supported_attachment_types,anthropic.py:82-97)。

换句话说,在这一层里 Anthropic 是"能出文本、但不是一等公民"的 provider。要跑完整工具循环,当前更成熟的路径是 OpenAI 家族和 Google。

6.3 Google:thought_signature 与内联图片

GoogleLLM(application/llm/google_ai.py:13)有几处 Gemini 独有的适配:

(a) thought_signature(思维签名)。 Gemini 3 的 function call 附带一段签名,回放这次工具调用时必须原样带回, 否则模型拒绝。麻烦在于 SDK 给的是 bytes,而 DocsGPT 要把它 json.dumps 进 SSE 事件(要 str)。于是 handler 在入口 base64 编码、回放时解码(application/llm/handlers/google.py:9-30,_encode/_decode_thought_signature), _clean_messages_google 再把解码后的签名塞回 types.Part(google_ai.py:262-272)。

(b) system 指令要单拎出来。 Gemini 的 contents 只收 user/model,system 消息得抽出来单独作 system_instruction(_clean_messages_google,google_ai.py:239-243373-375)。

(c) 图片走内联 bytes,文件走 Files API。 20MB 以下的图片直接内联字节(Files API 可能在上传未 ACTIVE 时返回空 URI), 其余走上传(prepare_messages_with_attachments,google_ai.py:91-106)。

其余 provider —— Groq / Novita / OpenRouter / PremAI / SageMaker / llama.cpp —— 大多复用 OpenAI 兼容形状或做薄适配, 差异点不如上面三家关键,读源码时按同样的"_raw_gen* + _clean_messages*"骨架看即可。


7. handler 侧:把各家 chunk 归一成 LLMResponse

provider 子类吐出的是"各家形状的 chunk",工具循环却只想吃统一结构。这道归一化由 handler 完成。

7.1 选 handler:LLMHandlerCreator

application/llm/handlers/handler_creator.py:6(LLMHandlerCreator)按 provider 名选 handler,只有两种实现, 其余全部落到 OpenAI handler:

handlers = {
"openai": OpenAILLMHandler,
"google": GoogleLLMHandler,
"novita": OpenAILLMHandler, # Novita 用 OpenAI 兼容 API
"default": OpenAILLMHandler,
}

也就是说:只有 Google 需要一套自己的解析,其余 provider 都能被 OpenAI handler 的解析吃下——因为它们的 chunk 要么本就是 OpenAI 形状,要么被 provider 子类伪装成了 OpenAI 形状(见 §6.1 的 _Resp* 伪对象)。

7.2 parse_response:两家的归一化目标一致

两个 handler 的 parse_response 输出同一个 dataclass:LLMResponse(content, tool_calls, finish_reason, ...) (application/llm/handlers/base.py:46),里面的工具调用统一成 ToolCall(handlers/base.py:26)。

它们读的地方不同:

handler从哪读文本从哪读 tool_calls代码
OpenAImessage.contentdelta.contentmessage.tool_calls[].functionhandlers/openai.py:10-43
Googlecandidates[0].content.parts[].textparts[].function_callhandlers/google.py:36-93

OpenAI handler 还顺手抽 reasoning 文本(OpenAILLM._extract_reasoning_text,handlers/openai.py:36), 供 DeepSeek thinking 模式回传;Google handler 则在这里编码 thought_signature(handlers/google.py:52)。 归一之后,第 2 章的工具循环就只跟 LLMResponse/ToolCall 打交道,再不碰任何 provider 细节

编排(工具循环怎么驱动多轮、暂停续跑)属于第 2 章,这里不重复;本节只讲"chunk → 统一结构"这一步。


8. 结构化输出:同一个方法,两种方言

最后一块拼图是"让模型只吐符合 JSON schema 的结果"。抽象方式和前面一致:基类定义方法,provider 各自实现方言。

8.1 契约:prepare_structured_output_format

基类留了空实现(application/llm/base.py:429-432,默认返回 None),provider 覆盖它,把一份通用 JSON schema 翻译成自家 API 认得的格式

provider目标格式特殊处理代码
OpenAI{"type":"json_schema","json_schema":{...,"strict":true}}strict 模式下递归给每个 object 补 additionalProperties:false 且把全部字段列进 requiredopenai.py:1150-1207
GoogleGemini 的 response_schema(类型名大写:OBJECT/STRING…)剔除不支持字段、映射类型、补 propertyOrderinggoogle_ai.py:671-735

两家方言差得很远(OpenAI 要 strict 三件套,Google 要大写类型名),正好说明抽象层的价值:agent 只管给一份普通 schema, 翻译由 provider 各自消化。

8.2 在 agent 侧怎么被接上

application/agents/base.py:717-729(_llm_gen)决定要不要走结构化输出,并把翻译结果塞进对应 provider 的参数名:

if (self.json_schema
and hasattr(self.llm, "_supports_structured_output")
and self.llm._supports_structured_output()): # 先能力协商(§4.3)
structured_format = self.llm.prepare_structured_output_format(
self.json_schema, strict=getattr(self, "json_schema_strict", True))
if structured_format:
if self.llm_name == "openai":
gen_kwargs["response_format"] = structured_format # OpenAI 用 response_format
elif self.llm_name == "google":
gen_kwargs["response_schema"] = structured_format # Google 用 response_schema

注意先做 _supports_structured_output() 协商——BYOM 端点若不支持,这段就整体跳过;_raw_gen 里还有一层 纵深防御,能力不允许就丢掉 response_format(openai.py:415-416)。这与 §4.3 的能力协商首尾呼应: 能不能用先问,怎么翻译各家管,发出去前再兜一次底。


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

主题文件关键符号
工厂:选实现类 + BYOM 解析 + 安全闸application/llm/llm_creator.pyLLMCreator.create_llm
provider 插件注册表application/llm/providers/__init__.pyALL_PROVIDERSPROVIDERS_BY_NAME
provider 插件基类application/llm/providers/base.pyProviderget_models
OpenAI 兼容/BYOM 端点插件application/llm/providers/openai_compatible.pyOpenAICompatibleProvider
模型注册表分层查找application/core/model_registry.pyModelRegistry.get_model
能力记录(caps forward 的源头)application/core/model_settings.pyModelCapabilitiesAvailableModel.upstream_model_idapi_flavor
基类契约:gen/gen_stream/抽象方法application/llm/base.pyBaseLLM.gen_stream_raw_gen_stream
能力协商application/llm/base.py_supports_tools_supports_structured_outputget_supported_attachment_types
跨 provider 回退 + 应答方标记application/llm/base.pyfallback_llm_execute_with_fallback_stream_with_fallback_responding_provider
OpenAI provider(含 Responses API)application/llm/openai.pyOpenAILLM_uses_responses_api_responses_gen_RespToolCall
Anthropic provider(旧 completions)application/llm/anthropic.pyAnthropicLLM._raw_gen
Google provider(thought_signature)application/llm/google_ai.pyGoogleLLM._clean_messages_googleprepare_structured_output_format
handler 选择application/llm/handlers/handler_creator.pyLLMHandlerCreator.create_handler
chunk 归一 + 回退感知解析application/llm/handlers/base.pyLLMResponseToolCall_parse_for_response_handler_for_provider
OpenAI / Google 解析实现application/llm/handlers/openai.pyhandlers/google.pyparse_response_encode_thought_signature
结构化输出接线(agent 侧)application/agents/base.pyAgent._llm_gen

相邻章节: 入口与 agent 见 01;工具循环编排见 02; 工具体系见 03;检索层见 04;摄取与高级 agent 见 06; 全景与阅读地图见 index