`init` 向导每一步在问什么,答错了会怎样

2026-08-10

一个交互式初始化向导最容易被当成「一路回车」的东西:反正问什么答什么,最后能跑起来就行。但向导恰恰是整个项目里唯一一处「你的每一次按键都直接变成磁盘上的一行配置」的地方——按错一个键,后面排查的成本会散落到十几个不相干的现象里。

所以这篇不讲怎么用,只做一件事:把 DeepTutor 的 deeptutor init 按源码行号拆开,逐步说清这一步在采集什么、默认值是什么、你答成另一个样子会落到哪个结果上

所有行号与数字对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 之后行号可能已经漂了,文件名与常量名是稳的。

需要先说明的边界:向导里那些提示文案(代码里写成 strings["init.*"])的实际中英文内容,来自 deeptutor/runtime/banner.pylabels_for()我们这次没有打开那个文件,所以本文一句提示原文都不引,只讲代码分支。我们也没有安装或运行过这个项目,下面全部是源码语义。

零、在第一步之前,先确认它要写到哪

init 的命令签名只有两个选项:--cli(bool,默认 False,help 是 Initialize for CLI-only use.)和 --home(Path,默认 None),docstring 是 Create or update data/user/settings for this workspace.deeptutor_cli/init_cmd.py:542-549)。

--home 看着像个可有可无的路径参数,实际上它决定了后面所有回答落到哪个目录。运行时根目录的取值优先级写在 deeptutor/runtime/home.py:12-27显式参数 → DEEPTUTOR_HOME 环境变量 → 当前工作目录。也就是说,你不传 --home、环境变量又没设,向导就把配置写到你当时所在的那个目录下。

run_init 会把 DEEPTUTOR_HOME 环境变量设为解析出来的 runtime home,并重置 PathService、RuntimeSettingsService、ModelCatalogService 三个单例(init_cmd.py:24-48:412-418)。这段代码的注释自己写明了不清理的后果:会静默写错地方

对使用者的含义很直接:这一步答错不会报错。你会得到一份完整跑完的向导和一句保存成功,只是文件不在你以为的位置。所以进向导之前先把「我现在在哪个目录、DEEPTUTOR_HOME 有没有值」这两件事确认掉,比事后翻文件系统省事。至于 data/user/settings 下具体生成哪些文件,我们另有一篇专门讲配置落地,这里不展开。

一、到底几步:两个「四步」不是同一个四步

这是本篇最容易被绕进去的一处,值得单拎出来。

init_cmd.py:1-9 的模块 docstring 写的是:deeptutor init 走一个 four-step wizard (ports → LLM → embedding → review)

而代码里步数是这么定的(init_cmd.py:429-431):

total_steps = 4 if cli_only else 5

紧挨着这一行的注释写明了两种模式各包含哪几步:CLI-only 是 LLM、Embedding、Search、Review 四步,完整模式是 Ports、LLM、Embedding、Search、Review 五步。

两处不一致:docstring 那句列举的四步里没有 Search,而代码里 Search 是实打实的一步(_search_stepinit_cmd.py:320-343,在主流程里的位置见 :501-505)。同时,代码里确实存在一个「四步」,但那是 cli_only 模式的四步——它去掉的是 Ports、保留了 Search,与 docstring 列的那四步不是同一组。以我们实读的仓库状态为准,说完就停。

实用的读法是:别按步数编号去对文档。你看到第 3 步时它到底是 Embedding 还是别的,取决于你有没有加 --cli。带 --cli 时 LLM 是第 1 步,不带时 LLM 是第 2 步。

二、Step 1 端口:只有完整模式才问

cli_only 时才有这一步,两个默认值写在 init_cmd.py:443-460backend_port 默认 8001frontend_port 默认 3782

这两个值是代码里的默认配置,不是「你必须用这两个端口」。答成别的值本身没有对错,只是要记住它们会被写进 system 那一侧的配置里——而这一侧在 --cli 模式下压根不会保存(见第六节)。

三、Step 2 LLM:唯一一步不能跳

LLM 这一步的流程是:选 provider → 是否改 base_url → 采集 api_key → 拉模型列表 → 选模型 → 可选连通性探测(init_cmd.py:462-470_llm_step:88-159)。

菜单键位是有约定的,写在 init_wizard.py:376-381 的注释里:[a] 是 Show all、[c] 是 Custom,而 [s] 保留给可选步骤的 Skip——注释把 [s] 明确留给 embedding、search 这类可选步骤,LLM 这一步没有 [s]。这意味着一旦进了向导,LLM 这一层你绕不过去。

几个会直接改变结果的细节:

其一,默认列表只有 10 个。 FEATURED_LLM_PROVIDERS 共 10 项,按显示顺序是 openai、anthropic、deepseek、dashscope、zhipu、moonshot、gemini、siliconflow、openrouter、ollama(init_wizard.py:40-51)。不在这 10 个里的,得按 [a] 展开或 [c] 自填。

其二,按 [a] 展开也未必能看到你要的那个。 「显示全部」时会排除 spec.is_oauth 的 provider,代码注释写的是 OAuth flows use 'deeptutor login'init_wizard.py:351-357)。所以在列表里翻不到某个 provider,先确认它是不是走 OAuth 那条路,而不是继续往下翻。

其三,模型列表是真的去拉的。 fetch_models() 请求 base_url.rstrip("/") + "/models",httpx 超时 5.0 秒;anthropic 用 x-api-keyanthropic-version: 2023-06-01,其他用 Authorization: Bearerinit_wizard.py:571-587)。它能解析三种响应形状:{"data": [...]}{"models": [...]}(注释标了 Ollama 的 GET /api/tags)、以及裸 list(:593-604)。

这里要如实说明一件事:向导会拿你刚填进去的 key 向你填的 base_url 发真实的 HTTP 请求,不是本地校验。这是它的工作方式,涉及密钥离开本机,值得你在填之前想清楚这个 base_url 是谁的。

其四,拉不到会退回一份内置清单。 LLM_FALLBACK_MODELS 共 10 个键,注释写明它只在 GET {base_url}/models 失败或 provider 为 custom 时使用init_wizard.py:53-78)。也就是说,拉取失败或 provider 为 custom 时向导不会中断,会退回 LLM_FALLBACK_MODELS 这份内置清单继续往下走,所以你照样能看到一个模型列表并选一个。这条对使用者的含义是:「我明明选到模型了」不等于「这个 key 是通的」。

其五,探测是可选的,失败可以重填。 probe_llm() 超时 15.0 秒,anthropic 走 /messagesmax_tokens: 1,其他走 /chat/completions,没有 key 时用占位串 sk-no-key-required,返回 (ok, elapsed_ms, error)init_wizard.py:797-841)。失败时 _probe_llm_with_retry 会给一个「重填 API key 再试一次」的循环(init_cmd.py:162-188)。

把四、五两条并起来看就是这一步真正的判定动作:如果你跳过了探测,那么这一步的「成功」只能说明你选了一个模型名,不能说明这条链是通的。 想当场确认,就把探测跑掉。

四、Step 3 Embedding:两个默认值会替你做决定

这一步可以按 [s] 跳过,跳过时返回 None(init_cmd.py:207-290SKIP_SENTINEL = "__skip__"init_wizard.py:428:472-473)。FEATURED_EMBEDDING_PROVIDERS 也是 10 项:openai、gemini、aliyun、siliconflow、jina、cohere、openrouter、azure_openai、vllm、ollama,注释里说 vllm 同时覆盖 LM Studio 与 llama.cpp(init_wizard.py:84-95);对应的 EMBEDDING_FALLBACK_MODELS 是 9 个键(:100-114)。

两个默认值需要留意:

默认复用 LLM 的 api_key,而且这个确认默认是 Trueinit_cmd.py:207-290)。如果你的 embedding 和 LLM 不是同一家,这一路顺着回车下去就会把 LLM 那家的 key 带过来。想核对填进去的是不是同一串,只能看掩码——_mask_secret() 只展示前 4 位加后 4 位,长度小于等于 8 时全掩码,空值显示 (empty)init_wizard.py:264-270)。首尾四位相同的两串 key 在这里看不出差别。

模型过滤用的是严格子串 "embed"init_wizard.py:670-675)。代码注释解释了为什么不用更宽的启发式(e5-nomicvoyage 这类):会把太多 LLM 拖进来。对你的直接影响是,模型名里不含 embed 的嵌入模型不会出现在候选里,得自己填。另外 _derive_embedding_models_url() 对 gemini 和 ollama 做了特判,ollama 转成 /api/tags:627-667)。

还有一个 dimension 会被问到,默认是空串init_cmd.py:207-290)。

顺带记一条排查前提:probe_embedding() 的错误串会把 base_url 脱敏、把 api_key 与敏感 query 值替换成 [REDACTED],并截断到 200 字符init_wizard.py:856-870)。所以嵌入探测失败时拿到的错误信息是被裁过的,别指望从里面读出完整的上游响应。

五、Step 4 Search:跳过和选 none 不是一回事

如果说前面几步都是「填错了值」,这一步是「按对了键但意思不对」——本篇最反直觉的一处在这里。

这一步同样可以按 [s] 跳过并返回 None。但候选列表里还有一项叫 none,代码注释把两者的区别写得很明白(init_cmd.py:320-343):provider == "none" 不是 skip,它是「显式禁用 web search」,仍然会被写进 catalog

换句话说,[s] 是「这一项我这次不动」,none 是「我明确要求关掉」。两条路径在向导里只差一个按键,落到磁盘上却是「没写」与「写了一个禁用值」两种状态。日后你去翻配置想搞清楚搜索为什么不工作,这两种状态给出的线索完全不同。

SEARCH_PROVIDERS 共 8 条 SearchProviderSpecinit_wizard.py:136-192):brave、tavily、jina、serper、perplexity、duckduckgo、searxng、none。其中两条有特殊约束:duckduckgo 的 requires_api_key=False,hint 是 no API key needed;searxng 的 requires_base_url=Truedefault_base_urlhttp://localhost:8888init_wizard.py:172-185)。选了 searxng 却没有一个在跑的实例,那个默认地址是指向本机的。

还有一处会「替你答」的地方:每个 search provider 带一个 env_keys 元组,按顺序取第一个非空的环境变量,例如 brave 是 ("BRAVE_API_KEY", "SEARCH_API_KEY")init_wizard.py:130:141,取值函数 search_api_key_from_env:505-511)。注意这个元组里第二位是一个通用变量名,不是 provider 专属的那一个。如果你的 shell 里早就有这些变量,向导取到的就是它们,而不是你以为的「这次填的那个」。进向导前把你要用的那个 provider 的 env_keys 里各个变量逐个 echo 一遍,是这一步最省事的自查动作。

六、Step 5 Review & save:不确认就是直接退出

最后一步渲染 review 面板,然后调 typer.confirm(strings["init.confirm_save"], default=True)不确认则 raise typer.Exit(code=1)init_cmd.py:512-526)。

这句的含义要说清楚:不确认不是「回到上一步改一改」,而是整个 init 以退出码 1 结束,前面几步填的东西一起作废。想改,重跑一遍。

保存动作分成两处,而且行为不对称(init_cmd.py:528-530):

动作--cli 模式完整模式
runtime.save_system(system)跳过执行
catalog_service.save(catalog)执行执行

也就是说,模型目录那一份两种模式下都会写,而 system 那一侧只有完整模式才写——这正好解释了为什么 --cli 模式下端口那一步整个不问。模型目录文件的路径常量是 CATALOG_PATH = get_path_service().get_settings_file("model_catalog"),落到 <data>/user/settings/model_catalog.jsondeeptutor/services/config/model_catalog.py:20)。

中途按 Ctrl-C 呢?KeyboardInterrupttyper.Abort 都会被捕获,打印 cancelled 后 raise typer.Exit(code=130)init_cmd.py:535-538)。所以脚本里判断 init 结果时,1 和 130 是两个不同的含义:前者是走到了最后一步没确认,后者是中途被打断。

七、README 与代码对不上的一处

deeptutor_cli/README.md 第 34 行说 deeptutor init --cli 仍会创建 system.jsonauth.jsonintegrations.jsonmodel_catalog.jsonmain.yamlagents.yaml,这份清单里没有 interface.json;而 deeptutor/services/setup/init.py:151-152_ensure_essential_settings() 是显式写 interface.json 的。

同一处还有第二个差异:--cli 模式下 runtime.save_system(system) 被显式跳过,只有 catalog_service.save(catalog) 一定执行(init_cmd.py:528-530)。

两处都以我们实读的仓库状态为准。不推断原因,也不据此评价项目,说完就停。

八、你可以照着核的六步

  1. 打开 deeptutor_cli/init_cmd.py:429-431,把那两行注释和 total_steps 抄下来,确认 --cli 的四步与模块 docstring 里那四步不是同一组。
  2. 打开 init_cmd.py:320-343,读 _search_step 的 docstring,确认 none[s] 的语义差。
  3. 打开 init_wizard.py:376-381,确认 [s] 只保留给可选步骤,LLM 步骤没有它。
  4. 打开 init_wizard.py:53-78,确认 fallback 模型清单的触发条件是 /models 拉取失败或 custom——这决定了「能选到模型」说明不了 key 是通的。
  5. 打开 init_wizard.py:130:141,把你要用的 search provider 的 env_keys 抄出来,回 shell 里逐个确认这些变量当前有没有值。
  6. 打开 init_cmd.py:528-530,对照上面那张两行的表,确认你这次跑的模式到底写了哪一侧。

最后交代范围。本文主要读了 init_cmd.py(549 行)与 init_wizard.py(996 行)中与向导主流程直接相关的函数,另外引用了 deeptutor/runtime/home.pydeeptutor/services/config/model_catalog.pydeeptutor/services/setup/init.py 的少数几行,均未逐行读完;init_wizard.pyselect_from_options 之外的渲染细节、以及所有 strings["init.*"] 的实际文案都没有核实。上面出现的端口、超时秒数、条目数全是源码里的默认配置,不是运行结果的保证——它会不会成功、要多久,取决于你的 key、网络与上游服务,我们没有任何依据下这类结论。


本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.mdpyproject.tomldeeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。 本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API, 因此不涉及生成质量、响应速度与教学效果的任何描述。 参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。