DeerFlow 模型工厂与 vLLM Provider 深度解析:从配置反射实例化到跨轮次推理字段保持
本文基于 DeerFlow 仓库中 models 模块的开发者文档 models/AGENTS.md 展开,系统讲解两个核心组件:通过反射从 config.yaml 实例化任意 LangChain 聊天模型的 Model Factory,以及专为 vLLM 0.19.0 推理模型定制的 VllmChatModel。读完后,你将能够独立配置带 thinking 开关、环境变量密钥与视觉能力的多模型体系,并理解 vLLM 场景下 reasoning 字段如何在非流式响应、流式增量与多轮工具调用请求之间被完整保留。
一、Model Factory:基于反射的模型实例化入口
DeerFlow 的模型装配统一收敛在 factory.py 的 create_chat_model 函数中(见 factory.py#L174)。文档对它的核心描述是:
create_chat_model(name, thinking_enabled)instantiates LLM from config via reflection
即通过“反射”方式,把 config.yaml 中 models 列表里每个条目的 use 字段(类路径字符串)解析为真实的 Python 类并构造实例,从而无需修改代码即可切换任意 LangChain 聊天模型。
1.1 函数签名与关键参数
当前实现的完整签名为:
def create_chat_model(
name: str | None = None,
thinking_enabled: bool = False,
*,
app_config: AppConfig | None = None,
attach_tracing: bool = True,
model_overrides: dict | None = None,
**kwargs,
) -> BaseChatModel:
各参数的实际行为(依据 factory.py#L174-L201 的 docstring 与实现):
name:要创建的模型名;传None时取config.models[0].name,即配置中的第一个模型作为默认模型。thinking_enabled:布尔开关,控制是否启用该模型的扩展思考模式(前提见下文 1.2 的supports_thinking校验)。app_config:显式注入应用配置;省略时回落到全局缓存的get_app_config()。model_overrides:按调用方粒度叠加的采样参数覆盖(例如某个自定义 agent 想用自己的temperature/max_tokens)。实现上None值会被忽略,保证“未设置的覆盖项不会盖掉 profile 中的值”,且覆盖发生在 thinking / Codex 变换之前,使 provider 专属归一化逻辑依然有效。attach_tracing:默认True,把 Langfuse / LangSmith 等 tracing 回调直接挂到模型实例上。文档特别指出:已经在图根(make_lead_agent、图内TitleMiddleware等)挂过 tracing 的调用方必须传attach_tracing=False,否则同一次 LLM 调用会产生重复 span,且session_id/user_id元数据因模型变成嵌套 observation 而丢失。
1.2 配置元数据与 thinking 字段的分层处理
模型 profile 定义在 model_config.py 的 ModelConfig 类中(model_config.py#L4-L63),它是 extra="allow" 的 pydantic 模型,因此任意 provider 专属参数(max_tokens、base_url、request_timeout 等)都能直接写进配置项。声明字段包括:name、display_name、description、use、model、supports_thinking、supports_reasoning_effort、when_thinking_enabled、when_thinking_disabled、supports_vision、context_window、stream_chunk_timeout、thinking 等。
工厂在 model_dump 时会显式排除一批运行时/展示元数据,防止它们流入 provider 构造函数(factory.py#L209-L230):
| 排除字段 | 用途(不进 provider 客户端的原因) |
|---|---|
use / name / display_name / description |
装配元数据 |
supports_thinking / supports_reasoning_effort / when_thinking_enabled / when_thinking_disabled / thinking |
由工厂在装配期消费 |
supports_vision |
供上层判断是否把图片交给该模型 |
context_window |
UI 上下文指示器与 langchain profile 使用,构造函数不接受 |
pricing |
仅用于控制台成本展示,若泄漏会被转发进 completion 请求体 |
thinking_enabled 的生效路径在源码中体现为严格的分支逻辑(factory.py#L246-L273):
- 开启时:若模型未声明
supports_thinking: true,直接抛出带配置指引的ValueError(提示在config.yaml中置 true);否则把生效的when_thinking_enabled合并进构造参数。其中thinking字段是when_thinking_enabled["thinking"]的语法糖,两者同时存在时按“先底后盖”顺序深度合并。 - 关闭时(
thinking_enabled=False)按优先级依次尝试:- 用户显式提供的
when_thinking_disabled拥有最高优先级; - OpenAI 兼容网关路径:把
extra_body.thinking.type置为disabled并设reasoning_effort: "minimal"; - vLLM 路径:调用
_vllm_disable_chat_template_kwargs(factory.py#L25-L32),把thinking/enable_thinking对应的开关翻转为False写回chat_template_kwargs; - 原生
langchain_anthropic路径:直接把thinking构造参数置为{"type": "disabled"}。
- 用户显式提供的
这一分层保证了同一套工厂可以驱动 Anthropic、OpenAI 网关与 vLLM 三类思考开关语义完全不同的 provider。
1.3 环境变量解析与缺失 provider 的安装提示
文档中另外两条工厂特性,在仓库中都有直接实现证据:
$ 前缀配置值解析为环境变量。应用配置加载时(app_config.py#L568)检测到以 $ 开头的字符串值即做环境变量替换,因此 API 密钥普遍以 api_key: $VLLM_API_KEY 形式书写,避免明文入库。
缺失 provider 模块时给出可操作的安装提示。反射解析器 resolvers.py#L22 会在 import 失败时抛出形如:
Missing dependency '<missing_module>'. Install it with `uv add <package_name>` (or `pip install <package_name>`), then restart DeerFlow.
的错误——例如配置了 Google 模型却未装 langchain-google-genai 时,报错会直接给出 uv add langchain-google-genai,把“找不到类”这类模糊故障变成一行可执行的修复命令。
1.4 工厂内建的其他归一化(源码佐证)
围绕同一个 create_chat_model,源码还承担了几项让 OpenAI 兼容模型“开箱即用”的兜底,可作为配置排障的参考(均有源码位置可查):
api_base→base_url别名归一化(factory.py#L45-L73):ModelConfig是extra="allow",误写的api_base不会在配置加载期报错,而是被 LangChain OpenAI 客户端静默转进model_kwargs,最终在请求期被 OpenAI SDK 以晦涩的unexpected keyword argument 'api_base'拒绝、且端点覆盖悄悄丢失。工厂在建模前主动改名为base_url;对自行声明api_base字段的类(如ChatDeepSeek)则跳过,因为那里该键是合法语义。stream_chunk_timeout默认 240 秒(factory.py#L126-L171):langchain-openai 内建默认 120s 对 DeepSeek-R1、GPT-5 等推理模型过激(首个 chunk 可能合法地等待 90~150s),工厂对BaseChatOpenAI全体子类注入 240s 默认值,尊重用户显式覆盖;非 OpenAI 兼容客户端则主动丢弃该键。stream_usage默认开启(factory.py#L302-L309):LangChain 只在未设置自定义 base_url 时才默认stream_usage=True,第三方端点(doubao、deepseek 等)会因此丢失 usage 数据,工厂默认补齐。context_window翻译进 langchain profile(factory.py#L318-L332):把配置声明的上下文窗口合并进模型profile的max_input_tokens,使SummarizationMiddleware的分数型触发阈值在第三方 OpenAI 兼容模型上也能解析;构造后合并而非构造时传入,是为了避免整体替换掉 provider 推断出的其余 profile 元数据。
二、vLLM Provider:跨轮次保留非标准 reasoning 字段
vLLM 0.19.0 通过 OpenAI 兼容 API 暴露推理模型,但其 reasoning 字段是非标准的:LangChain 默认 OpenAI 适配器会在 assistant 消息与流式增量上丢弃它。这恰恰破坏了 interleaved thinking / tool-call 流程——vLLM 期望在后续轮次中把 assistant 此前的推理内容回显回去。vllm_provider.py 的模块 docstring(vllm_provider.py#L1-L13)开宗明义:
vLLM 0.19.0 exposes reasoning models through an OpenAI-compatible API, but LangChain's default OpenAI adapter drops the non-standard
reasoningfield from assistant messages and streaming deltas.
VllmChatModel(vllm_provider.py#L180)继承 langchain_openai:ChatOpenAI,在三个层面补齐这一缺口:
2.1 非流式响应:_create_chat_result
重写 vllm_provider.py#L245-L264:在父类生成结果后,逐 choice 从原始响应中取 message.reasoning,写回 AIMessage.additional_kwargs["reasoning"],并用 _reasoning_to_text(vllm_provider.py#L73-L99)做尽力而为的文本化(支持 str / list / dict,dict 依次尝试 text、content、reasoning 键),得到可读的 reasoning_content。回归测试 test_vllm_provider.py#L108 的 test_vllm_provider_preserves_reasoning_in_chat_result 验证了该路径。
2.2 流式增量:_convert_chunk_to_generation_chunk
重写 vllm_provider.py#L266-L317。核心是把 delta 转成消息块时委托给 _convert_delta_to_message_chunk_with_reasoning(vllm_provider.py#L102-L155):只要 delta 携带 reasoning,就同时写入 additional_kwargs["reasoning"](原始值)与 additional_kwargs["reasoning_content"](文本化结果),保证前端可以逐 token 呈现思考过程。
2.3 多轮请求:_get_request_payload 回注历史推理
重写 vllm_provider.py#L220-L243:先调用父类生成 payload,再对 payload 中的 assistant 消息逐条执行 _restore_reasoning_field(vllm_provider.py#L158-L164),把 AIMessage.additional_kwargs 里缓存的 reasoning(或 reasoning_content)重新注入出站请求体。测试 test_vllm_provider.py#L60-L76 的 test_vllm_provider_restores_reasoning_in_request_payload 构造了“带 tool_call 的历史 assistant 消息 + 新 Human 消息”的场景,断言请求体中 assistant_message["reasoning"] 被原样回显、tool_calls 结构完好。
2.4 thinking 开关的语义归一:enable_thinking 与旧 thinking 别名
vLLM 0.19.0 的 Qwen reasoning parser 读取的是 chat_template_kwargs.enable_thinking,而 DeerFlow 早期文档写的是 extra_body.chat_template_kwargs.thinking。_normalize_vllm_chat_template_kwargs(vllm_provider.py#L47-L70)在 payload 发出前做兼容转换:仅当存在 thinking 键时,以其值 setdefault 出 enable_thinking 再移除旧键——已有显式 enable_thinking 的配置不受影响。两条回归测试分别验证了旧键转换(test_vllm_provider.py#L79-L89)与显式新键保留(test_vllm_provider.py#L92-L105)。
注意归一化发生在请求 payload 层:配合工厂侧 _vllm_disable_chat_template_kwargs(见 1.2 节),thinking_enabled 开关的开启与关闭两个方向都能真正落到 vLLM 的 chat template 上,"flash 模式"可以确定性地关掉推理。
三、cumulative_stream_usage:把累计 usage 快照换算为逐 chunk 增量
这是文档着墨最多的 provider 特性。背景:部分 vLLM 端点在每个流式 chunk 上都重复携带累计 token 总量而非增量。若直接透传,usage_metadata 会随 chunk 数被重复累加、总量严重虚高。
cumulative_stream_usage 是 VllmChatModel 上的 opt-in 字段,默认 false(vllm_provider.py#L185-L188)。开启后的换算机制在 _usage_delta(vllm_provider.py#L196-L213)中实现,逐条对应文档承诺的行为:
| 文档描述 | 源码实现 |
|---|---|
| 仅在存在稳定 completion id 时换算 | _get_completion_id(vllm_provider.py#L167-L177)从 chunk 顶层或嵌套 chunk.id 取非空 id;completion_id 为空则直接返回原始 usage,"leaves the original usage untouched" |
| 按 id 隔离交错的并发流 | 以 OrderedDict[completion_id -> (UsageMetadata, monotonic_time)] 为键,多条并发 completion 各自独立追踪 |
| 锁保护 | _cumulative_usage_lock(threading.Lock)包裹全部读写(vllm_provider.py#L190) |
| 软上限 1024,只驱逐闲置满 1 小时的条目 | 常量 _CUMULATIVE_USAGE_TRACKER_CAPACITY = 1024、_CUMULATIVE_USAGE_TRACKER_IDLE_SECONDS = 60 * 60(vllm_provider.py#L43-L44);驱逐循环检查队首条目的 now - updated_at < idle,不满足即 break——因此活跃流可临时突破 1024,但驱逐永不触碰活跃 id,delta 不会被破坏 |
终端空 choices 帧必清状态,无论该帧是否带 usage |
_convert_chunk_to_generation_chunk 中 len(choices) == 0 分支:有 usage 走 _usage_delta(..., terminal=True) 弹出记录,无 usage 走 _clear_usage_snapshot(vllm_provider.py#L281-L291) |
换算本身用 langchain_core.messages.ai.subtract_usage 做“本次快照 − 上次快照”,所以每个 chunk 对外暴露的是真正的增量。回归测试位于 backend/tests/test_vllm_provider.py(509 行,覆盖请求体回注、thinking 归一化、非流式保留、累计 usage 换算等场景)。
四、完整配置示例:在 config.yaml 中接入 vLLM 推理模型
仓库根目录的 config.example.yaml#L656-L678 提供了开箱可复制的官方示例(原文为注释态,启用时去掉行首 # 即可):
# Example: vLLM 0.19.0 (OpenAI-compatible, with reasoning toggle)
# DeerFlow's vLLM provider preserves vLLM reasoning across tool-call turns and
# toggles Qwen-style reasoning by writing
# extra_body.chat_template_kwargs.enable_thinking=true/false.
# Some reasoning models also require the server to be started with
# `vllm serve ... --reasoning-parser <parser>`.
# - name: qwen3-32b-vllm
# display_name: Qwen3 32B (vLLM)
# use: deerflow.models.vllm_provider:VllmChatModel
# model: Qwen/Qwen3-32B
# api_key: $VLLM_API_KEY
# base_url: http://localhost:8000/v1
# # Enable only when the endpoint reports cumulative usage on every stream chunk.
# cumulative_stream_usage: true
# request_timeout: 600.0
# max_retries: 2
# max_tokens: 8192
# supports_thinking: true
# supports_vision: false
# when_thinking_enabled:
# extra_body:
# chat_template_kwargs:
# enable_thinking: true
要点解读:
use: deerflow.models.vllm_provider:VllmChatModel是反射装配的“类路径”约定(模块路径:类名),与 1.1 节的resolve_class直接对应;api_key: $VLLM_API_KEY演示了$前缀环境变量解析;supports_thinking: true是 1.2 节工厂校验的前置声明——缺了它,开启思考会直接抛错;when_thinking_enabled.extra_body.chat_template_kwargs.enable_thinking: true即文档所述的 vLLM 风格思考开关;关闭思考时工厂自动翻转对应值为False(2.4 节);cumulative_stream_usage: true应仅在端点确实“每个 chunk 都重复累计 usage”时开启(示例注释亦如此提醒),否则保持默认false;- 示例还提示:部分推理模型需要服务端以
vllm serve ... --reasoning-parser <parser>启动,reasoning 字段才会出现在响应中。
五、小结:两个组件的职责边界
从 models/AGENTS.md 与源码的分工可以看出清晰的分层:
- Model Factory(factory.py) 解决“配置 → 实例”的通用问题:反射解析
use、消费 thinking 开关、剥离展示元数据、兜底别名与超时/usage 默认值、把$配置解析为环境变量、把缺依赖转化为uv add ...级别的可执行提示; - VllmChatModel(vllm_provider.py) 解决“provider 协议差异”问题:在不修改 LangChain 的前提下,把 vLLM 非标准
reasoning字段在响应解析、流式增量与出站请求三个方向完整保留,并可选地把累计 usage 快照安全地换算为增量。
两者组合起来,使 DeerFlow 在多模型(Anthropic、OpenAI 网关、自托管 vLLM/MindIE 等)混配场景下,既保持配置驱动的扩展性,又不牺牲推理模型的语义完整性。相关行为均有回归测试覆盖:工厂与思考开关的测试见 backend/tests/test_model_factory.py,vLLM provider 的专项测试见 backend/tests/test_vllm_provider.py。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00