硬编码默认值逐个看:DeepTutor 里哪些值你改不到
读一个自带一大堆 provider 接入的项目,最先想搞清楚的往往不是它支持多少家,而是这一句:我在设置里改的那个值,到底有没有走到请求里去。
这篇只回答这一个问题。所有行号与数字都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 之后行号会漂,但文件名、常量名、字段名是稳的,照着 grep 就能找到。
一、先把「默认值」分成三层
在 deeptutor/services/ 里,同样叫「默认值」的东西至少分三层,它们的可改动程度完全不同:
- 走 pydantic settings 的。例如全局重试的
max_retries默认 8、base_delay默认 5.0 秒,出处是deeptutor/config/settings.py:20-22,然后在deeptutor/services/llm/factory.py:27-29被导出成DEFAULT_MAX_RETRIES/DEFAULT_RETRY_DELAY/DEFAULT_EXPONENTIAL_BACKOFF。这一层的语义摆明了是「配置项」。(重试与超时那一整套常量另有一篇专门讲,这里不展开。) - 写成模块级常量的。例如 provider 实例池上限
_PROVIDER_POOL_MAXSIZE = 2(llm/provider_factory.py:17)、流式合并参数DEFAULT_STREAM_COALESCE_CHARS = 64与DEFAULT_STREAM_COALESCE_SECONDS = 0.04(llm/factory.py:30-32)。这类值在我们核对的范围内没有看到对应的配置入口。 - 表驱动的覆盖值。这一层最容易被忽略:它不是「你没设时用什么」,而是你设了之后按规则改写成什么。
第三层就是本文标题里的「改不到」。前两层至少是「你不改就用它」,第三层是「你改了它也可能不算」。
顺带说一句可改性的对照:cli_apps/installer.py:60 那个安装超时 900 秒,源码里明写可以被环境变量 DEEPTUTOR_CLI_APP_INSTALL_TIMEOUT_S 覆盖。同一个 services 层里,有的常量自带环境变量开关,有的没有——判断某个值能不能改,只能一个个回到定义处看,没有统一规律可套。
二、温度和 max_tokens:同一组默认值散在好几个文件里
生成参数的基准值在 llm/provider_core/base.py:62-68:
@dataclass(frozen=True)
class GenerationSettings:
temperature: float = 0.7
max_tokens: int = 4096
reasoning_effort: str | None = None
有意思的是,同样的 max_tokens: int = 4096 与 temperature: float = 0.7 并不是只有这一处。至少在这四个后端实现的方法签名里各写了一遍(这是我们逐个核到的范围,实际 grep 出来的处数可能更多):anthropic(:492-493、:521-522)、azure_openai(:113-114、:139-140)、github_copilot(:179-180、:202-203)、openai_codex(:140)。
对读代码的人来说,这件事的实际影响是:你在某一处把 4096 改掉,不代表所有路径都跟着变。要确认某条调用路径最终带的是哪个值,得看它走的是哪个后端类的哪个方法,而不是只看 GenerationSettings。核对动作很直接,在 clone 根目录跑:
grep -rn "max_tokens: int = 4096" deeptutor/services/llm/provider_core/
grep -rn "temperature: float = 0.7" deeptutor/services/llm/provider_core/
数出来是几处,就是几处默认值各自独立。
三、★ 反直觉的那一处:你设的 temperature 会被两张表改写
现在说第三层。
llm/capabilities.py 里有一张 MODEL_OVERRIDES,按 ast 计数是 34 个模型前缀键(llm/capabilities.py:248)。其中三个前缀 gpt-5、o1、o3 带的字段是 forced_temperature: 1.0,上方注释直接引用了项目自己的 issue #141(llm/capabilities.py:291-301)。而 DEFAULT_CAPABILITIES 里这个字段的默认是 forced_temperature=None(llm/capabilities.py:234-243)。
翻译成使用者视角:默认情况下没有强制温度;一旦你用的模型 id 命中那三个前缀,就有一个 1.0 的强制值等在那里。 你在 agent 参数里写的 0.3 还是 0.9,在这条路径上不是最终决定项。
这不是唯一一张会动温度的表。另一张挂在 provider 上:Moonshot 的 spec 里写着 model_overrides=(("kimi", {"temperature": None}),),注释说明 Kimi 系列在服务端锁死温度、传非固定值会返回 HTTP 400(provider_registry.py:335-342)。
所以温度这个「最常改的参数」,在这个仓库里同时被两个文件按前缀干预:一个按模型 id 前缀(llm/capabilities.py),一个挂在 provider spec 上(provider_registry.py)。排查「我明明调了温度但输出没变化」时,正确的顺序是先确认你的模型 id 前缀有没有命中这两张表,再去怀疑别的地方。
同一张 MODEL_OVERRIDES 里还有一条值得单独记:claude- 前缀被整体标为 supports_vision: True,注释解释了原因——Claude 3 之后模型 id 不再是 claude-4 这种形式,所以匹配的是厂商前缀而不是逐个版本枚举(llm/capabilities.py:306-311)。这是同一套前缀机制的另一种用法:不是改写你的参数,而是补一个能力位。
四、没命中任何前缀时,兜底的那个值是 None
第三节两张表讲的都是「命中前缀就改写」。为了看清这件事的边界,还得看一眼另一条路径:什么都没命中的时候用什么。
PROVIDER_CAPABILITIES 的覆盖面小于 provider_registry.py 里注册的 provider 数量,没有专属能力条目的那些会落到 DEFAULT_CAPABILITIES 上,而这张兜底表里 forced_temperature 的取值是 None(llm/capabilities.py:234-243)。
跟第三节对照着读,这一行的用处是给出基线:默认状态下没有强制温度,出现 1.0 这种强制值,一定是命中了某张覆盖表的前缀,而不是「兜底就这样」。至于两条路径合并时谁先谁后、覆盖表的字段怎么盖住兜底表,那是 capabilities 的实现细节,我们没有逐行读,不替它下结论。
具体是哪些 provider 会落到这张兜底表上、这张表的另外七个能力字段各是什么取值、以及能力表里为什么还留着对不上 ProviderSpec 的条目,都属于 provider 接入层的话题,我们另有一篇专门讲,这里只借用 forced_temperature 这一行做对照。
五、写死了、但注释自己说「这不是权威」
有一类硬编码值很特别:它写死了,同时明确告诉你别拿它当准。
最典型的是 Codex 的默认模型 CODEX_DEFAULT_MODEL = "gpt-5.6-sol"(codex_auth/constants.py:20-23),注释原文的意思是:这只是 fallback,仅在调用方没指定模型时用,真实的模型列表始终来自已登录账号的实时 catalog。
codex_auth/constants.py 里同一批常量还有:客户端版本 0.145.0(:4)、上游 commit 记录(:3)、OAuth issuer https://auth.openai.com(:5)、回调端口 (1455, 1457)(:9)、模型缓存的 fresh 300 秒 / stale 86400 秒(:16-17)、最多 512 个模型(:18)、catalog 上限 8MB(:19)。
这些值的性质各不相同:回调端口是写死的本机端口对,模型数与体积是上限保护,缓存秒数是刷新节奏。放在一个文件里,读起来省事,但也意味着它们没有分层——想知道某一个能不能配,还是只能看有没有对应的读取入口。顺便记一句仓库自己的标注:README.md:656 关于 Codex 兼容路径写着 “This compatibility path is experimental: the upstream interface may change.”,这是原文,照实抄。
六、默认模型名与那个改了语义的 default_api_base
另一批「改不到」其实是「你没意识到要改」。config/provider_runtime.py 里给各个 provider 都钉了默认模型名,换 provider 就等于换了一整套默认:embedding 侧 openai 是 text-embedding-3-large、维度 3072(:89-90),ollama 是 nomic-embed-text(:134);TTS 侧 azure_openai 是 tts-1(:254);STT 侧 groq 是 whisper-large-v3-turbo(:287)。整张表这里不铺开,需要哪一项就去这个文件按行号找。
这一节里真正会绊人的是另一条注释:config/provider_runtime.py:60-65 写明,自 v1.3.0 起 default_api_base 是完整的 embedding 端点 URL,而不是 base;适配器「逐字使用配置的 URL,不追加路径」。
字段名还叫 ..._api_base,语义已经不是 base 了。你如果按名字的直觉只填到域名或 /v1,请求打到哪里就跟你预期的不一样。判定动作:把你填进去的那个 URL 和这个文件里同类 provider 的 default_api_base 原值放在一起比对——比的不是域名对不对,是路径深度一不一样。
EMBEDDING_PROVIDERS 一共 12 个键(config/provider_runtime.py:83-196,ast 计数),这些默认值就散在这 12 个 spec 里。
七、识别规则也是硬编码的
provider_registry.py 里还有一类值不是「参数默认」,而是「识别关键字」:本地服务按端口关键字识别,ollama 11434(:407)、lm_studio 1234(:417)、llama.cpp 8080(:427)、lemonade 13305(:437);另有按 key 前缀识别的,openrouter sk-or-(:152)、nvidia_nim nvapi-(:458)。
ProviderSpec 是 frozen dataclass,detect_by_key_prefix 与 detect_by_base_keyword 是它的字段(provider_registry.py:18-54)。文件头注释还写了一句要紧的:「Order matters — it controls match priority and fallback. Gateways first.」(provider_registry.py:3-7)——顺序本身也是硬编码的语义。
所以如果你把本地推理服务挪到了非默认端口,命中的就不是这条识别规则里的关键字了。至于换端口之后整体会怎样,取决于匹配函数的具体实现,那部分我们没有逐行读,不替它下结论。
八、改一个值,会牵动什么
这个仓库里有一处能明确说出关联的地方:provider 实例池。
llm/provider_factory.py:17 把池上限钉在 _PROVIDER_POOL_MAXSIZE = 2;:28-42 说明缓存键包含事件循环、provider 名、模式、模型、api_key 的 sha256 前 16 位指纹、URL、api_version、headers、temperature、max_tokens、reasoning_effort。
temperature、max_tokens、reasoning_effort 是缓存键的一部分。 按字面语义,改这三个参数会得到不同的缓存键;而池只有 2 个位置。至于具体的换入换出行为,我们没有逐行读它的淘汰逻辑,这里只陈述缓存键包含哪些字段。
另外注意 api_key 进缓存键时用的是 sha256 前 16 位指纹而不是原文——这是源码里的写法,照实记录,不引申任何安全评价。
九、自己核一遍:做什么、以及什么情况下不是这个原因
在你的 clone 根目录,这几条足够把本文的主要论断都验一遍:
grep -n "forced_temperature" deeptutor/services/llm/capabilities.py
grep -n "model_overrides" deeptutor/services/provider_registry.py
grep -c '^ name="' deeptutor/services/provider_registry.py
grep -n "default_api_base" deeptutor/services/config/provider_runtime.py
grep -rn "_PROVIDER_POOL_MAXSIZE" deeptutor/services/llm/
数键的数量时别用目测,用 Python 的 ast 解析后取 len(node.value.keys),我们统计 PROVIDER_CAPABILITIES(23)、MODEL_OVERRIDES(34)、EMBEDDING_PROVIDERS(12)用的都是这个口径。以上为按仓库中的文件与常量语义组合的检索示例,未经实测,以官方文档与实际源码为准。
什么情况说明不是本文讲的这些原因:
- 如果你的模型 id 前缀不在那三个
forced_temperature键上,也不是 kimi 系列,那温度没生效就跟本文这两张表无关,得往调用链上游找; - 如果你的 provider 在
PROVIDER_CAPABILITIES的 23 个键里有专属条目,那它读到的能力值就不是DEFAULT_CAPABILITIES兜底来的,得回到那条专属条目本身去看; - 如果问题只出现在重试之后,那是重试与超时那一套常量的范畴,跟这些生成参数的默认值是两码事。
最后把边界说清楚。我们没有核实运行期的 data/user/settings/*.json 会不会覆盖这些默认值——本文说的「改不到」,准确含义是:在我们读到的这些定义处,没有看到对应的配置读取入口,而不是断言项目一定没提供改法。同理,session/turn_runtime.py、config/provider_runtime.py、config/runtime_settings.py 这几个千行以上的文件,我们只读了 docstring、签名与常量段,没有逐行读完。
还有一点必须说明:这一层里有些常量对应的行为不止是发个 HTTP 请求。services/cli_apps/ 的职责是「管理员安装、chat agent 调用的命令行工具」(cli_apps/__init__.py:1),services/subagent/ 的职责是「驱动用户本机 agent CLI 作为子代理」(subagent/__init__.py:1-7)——也就是说,这两块涉及在你本机安装与执行外部程序。上面提到的那个 900 秒安装超时,管的正是这类动作。是否启用请结合自身环境评估。
本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.md、
pyproject.toml 与 deeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。
本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API,
因此不涉及生成质量、响应速度与教学效果的任何描述。
参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。