subagent 与 cli_apps:它会去驱动你本机的 agent CLI

2026-08-10

读一个后端项目,大部分子目录你可以按「它在进程里算什么」来理解。DeepTutor 的 deeptutor/services/ 里有两个目录不吃这套:它们干的事是在跑 DeepTutor 的那台机器上启动别的程序。一个是 subagent,去驱动你已经装好的 agent CLI;另一个是 cli_apps,去安装并调用第三方命令行工具。

这件事本身要先说清楚,不做任何淡化:涉及这两个目录的功能会在本机执行外部进程。是否启用、给谁启用,得你自己按部署环境判断。

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

一、这两个目录在 services 里有多大

deeptutor/services/ 一共 26 个一级子目录,Python 代码合计 65456 行。这两个目录的体量排位是:

目录.py 行数文件数模块 docstring 的自述
subagent363817驱动用户本机 agent CLI 作为子代理(subagent/__init__.py:1-7
cli_apps17989管理员安装、chat agent 调用的命令行工具(cli_apps/__init__.py:1

放在 services 的排行里看,subagent 排在 mcp(2774 行)前面、partners(3159 行)前面,是中上体量;cli_appssandbox(1195 行)同一档。这两个加起来 5436 行,不算大,但它们是整层里少数几个「作用域越出本进程」的模块。

值得单独注意一处措辞差异:cli_apps 的 docstring 一上来就把角色分成两方——管理员安装、chat agent 调用。也就是说,装什么和用什么在设计上不是同一个人做的决定。这个分工在后面的权限段还会再出现一次。

二、subagent 的后端注册表:7 个条目,但探测只走其中一部分

deeptutor/services/subagent/registry.py:24-35 里注册了 7 个后端:ClaudeCodeBackendCodexBackendGeminiBackendKimiBackendOpencodeBackendMimoBackendPartnerBackend。它们各自对应一个 kind 字符串,我们数到的取值是 claude_code(:61)、codex(:61)、gemini(:75)、kimi(:70)、opencode(:370)、mimo(:378)。

本文最想让你记住的反直觉点在这里:注册表里是 7 个,但会被拿去本机探测的不是 7 个。

规则写在两处:subagent/base.py:33 有一个 local_cli 字段;subagent/registry.py:47-57 只把 local_cli=True 的后端拿去探测,注释明写非 CLI 后端(partner 后端)不参与本机探测——它们不是”装在这台机器上”的东西,是从各自的列表里连过来的,所以这条路径只会返回 connect-CLI modal 会提供的那些 CLI。

这个区分很容易在读代码时被跳过去,但它决定了两件很实际的事:

  1. 数量对不上不是 bug。 如果你按注册表条目数去核对可连接项,会发现少一个;差的那个就是 partner 后端,它走的是另一条链路(partner 相关的机制另有一篇专门讲)。
  2. “探测不到”分两种。 一种是 local_cli=True 的 CLI 后端探测了、判定这台机器上没装或不可用;另一种是这个后端压根没进探测集合。排查时先确认自己看的是哪一种,再去查 PATH、装没装。

再往上抽一层看:探测集合由 subagent/registry.py:47-57 的筛选逻辑与 subagent/base.py:33local_cli 共同决定,而这套筛选要回答的问题始终是”这台机器上有没有”。注意”这台机器”——判定的对象是宿主机的状态,不是任何远端服务的状态。

顺带一条跨层的事实:subagent 不只是个连接器,它在知识库那一侧也有身份。deeptutor/knowledge/kb_types.py:63 定义了 SUBAGENT_KB_TYPE = "subagent",这类 KB 在磁盘上没有路径、没有可索引可检索的内容,external_root_of() 对它直接返回 Nonekb_types.py:105-113)。chat 那一侧对应的工具只有一个:consult_subagentdeeptutor/capabilities/subagent/tools.py:34)。知识库类型体系与 capability 层各有专门篇目在讲,这里只点出”同一个概念在三层各有一份表示”这个事实。

三、超时常量:读超时那一处没有上限

驱动外部进程,绕不开超时。subagent 下我们数到的几处常量是:

  • subagent/opencode_family.py:60-65:探测 15 秒、attach 15 秒、httpx 连接 10 秒、读无限、写 60 秒
  • subagent/opencode_server.py:39:ready 30 秒
  • subagent/claude_models.py:36:35 秒

其中「读超时无限」这一处值得单独拎出来看。它的语义是:建立连接这一步有 10 秒的耐心,但连上之后等对端吐数据这一步,代码里没有给上限。对一个流式输出的 agent CLI 来说,这个取值有它的道理——但它同时意味着,这条链路上不会因为读超时而自动断开。你如果需要一个兜底的挂死保护,得自己在别处加,这个位置的默认配置不提供。

必须强调的边界:以上是源码里的默认配置,不是”你用起来会怎样”的保证。我们没有安装也没有运行过这个项目,不对任何一条链路的实际行为下结论,也不据此推算会不会卡住、会卡多久。

四、cli_apps 的两个超时是两件事

cli_apps 这边有两个数量级不同的超时,很容易被当成一个:

  • 安装超时 900 秒,在 cli_apps/installer.py:60,并且可以被环境变量 DEEPTUTOR_CLI_APP_INSTALL_TIMEOUT_S 覆盖。
  • 单次调用超时默认 120 秒、上限 600 秒,在 cli_apps/runner.py:30-31

把它们分开记的理由很简单:一个管的是”把这个工具装上”,另一个管的是”用这个工具跑一次任务”。这两件事在时间尺度上差着一个量级,改错了地方就是改了个不相干的参数。

三条可以确定的推论——注意,只在这两行给出的语义范围内:

  1. 装不上、卡在装的过程里,去调环境变量那一个;跑一次跑不完,去调 runner 那一个。
  2. runner 那侧有上限(600 秒),意味着单次调用的可配置空间是有天花板的;installer 那侧我们只读到默认值与覆盖方式,没有读到上限。
  3. 该把它们调成多少,取决于你装的是什么工具、跑的是什么任务,仓库没有给通用值,我们也不给。

五、那份 catalog 是生成出来的快照,文件自己写了”不要手改”

这是第二处容易踩空的地方。cli_apps 能装哪些工具,不是运行时去远端查的,而是仓库里带的一份文件:cli_apps/vendor/catalog.json。我们读到的这份里有 101 个 app

它的 meta 段里写了三件事:

  • source: https://github.com/HKUDS/CLI-Anything
  • pinned commit bc536c9…commit_date: 2026-07-09
  • 一句 "Generated snapshot; do not hand-edit."

三件事拼起来的含义很清楚:这份清单是从另一个仓库的某个固定提交生成出来的,带着 2026-07-09 这个时间点,并且文件自己声明不要手工编辑。所以当你发现某个工具”清单里没有”时,第一反应不该是去改这个 json,而是先确认这份快照对应的是哪个上游提交——上游后来有没有变化、变了什么,是另一个话题,不在这份文件的口径里。

我们采集的是 DeepTutor 侧的这份快照。上游那个仓库本身的注册表内容我们另有篇目在讲,这里不展开,也不拿两边的条目数互相印证——那是两个不同时间点的东西,混着比没有意义。

六、装归管理员,用归账号:权限侧的默认是拒绝

回到第一节那句 docstring:管理员安装、chat agent 调用。这个分工在多用户授权那一层有对应实现。

授权结构 grant v2 里有一个 cli_apps 字段(deeptutor/multi_user/grants.py:16-44),它对非 admin 的语义是 deny-by-default:值为 None 即无权限,需要管理员显式点名(grants.py:28-43)。执行点写在 deeptutor/multi_user/tool_access.py:14-29 的 docstring 里:allowed_cli_apps 会与账号自身的偏好求交

这里的顺序值得记一下:管理员授权在前,账号偏好在后,最终生效的是两者的交集。也就是说,账号侧把某个工具打开,并不等于它就能用;同理,管理员点名了某个工具,账号侧仍然可以不用它。多用户与授权的完整机制另有一篇专门讲,本文只取与 cli_apps 直接相关的这两行。

配套地,容器化部署那边也有一处能对上:runner 镜像装了 nodejs 而不装 npm,Dockerfile.runner:37-42 的注释解释原因是 CLI apps 由主容器安装、runner 只读挂载执行Dockerfile.runner:26-35 是 apt 包清单)。这条注释把”装”和”跑”在部署形态上也分了开来——装在一处,执行在另一处,且执行侧是只读挂载。

七、你可以照着核的五步

  1. 打开 deeptutor/services/subagent/registry.py:24-35,把 7 个后端类抄下来;再看 :47-57subagent/base.py:33,确认 local_cli 这个字段以及 partner 后端被排除在探测之外。
  2. 打开 subagent/opencode_family.py:60-65,确认那组超时里读超时是无限;再对照 opencode_server.py:39claude_models.py:36
  3. 打开 cli_apps/installer.py:60cli_apps/runner.py:30-31,把 900 / 120 / 600 三个数记成”安装 / 单次默认 / 单次上限”,别混。
  4. 打开 cli_apps/vendor/catalog.json,先看 meta 段的 source 与 pinned commit,再数一遍 app 条目数,看看你手上那份是不是也是 101 个——这个数字随快照更新会变。
  5. 打开 deeptutor/multi_user/grants.py:16-44,确认 cli_apps 对非 admin 的 deny-by-default 语义:None 即无权限,需要管理员显式点名。

八、本文没有覆盖的部分

说清边界比说得多重要:

  • cli_apps/ 的内部实现我们只取了模块 docstring 与上面点名的那几行常量,除 installer.pyrunner.pyvendor/catalog.json 之外的其余文件我们没有读。所以”某个工具装完之后具体怎么被调起来、参数怎么拼”,本文不下结论。
  • subagent/ 下 17 个文件同样没有逐行读完,我们只核了注册表、base.py 的字段与几处超时常量。各后端与具体 CLI 之间的协议细节没有核实。
  • 我们没有安装、部署或运行过这个项目,也没有在本机驱动过任何一个 agent CLI。凡是”驱动之后表现如何”的部分,本文一律不写。
  • 上面所有阈值都是源码里的默认配置,不是运行结果的保证;版本一动就可能变。

最后再回到那句最该记住的话:这两个目录的功能落地在你的机器上执行外部程序。安装第三方命令行工具、启动本机 agent CLI、按管理员授权把这些能力开给账号——每一步都涉及本机权限与本机数据。开不开、给谁开,请结合自身部署环境评估。


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

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