65456 行的 services 层:26 个子领域怎么分工

2026-08-10

翻一个十几万行的 Python 项目,最没用的读法是从 README 的功能清单往下对。功能清单是按”用户能干什么”排的,目录是按”代码放哪儿方便”排的,两者根本不同构。更快的办法是先把每个目录有多少行数出来,看重心压在哪儿——重心所在的那一层,才是这个项目真正花了力气的地方。

DeepTutor 的重心是 deeptutor/services/。以下所有数字都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 之后行数会漂,但目录名是稳的。

一、先把规模数出来

我们数的口径就是几条 find,你可以逐条复现:

# 文件总数与扩展名分布
find deeptutor/services -type f | sed 's/.*\.//' | sort | uniq -c
# services 的 Python 总行数
find deeptutor/services -name '*.py' -exec wc -l {} + | tail -1
# 含嵌套的目录总数
find deeptutor/services -mindepth 1 -type d | wc -l

数出来的结果:deeptutor/services/ 下共 323 个文件,其中 .py 304 个、.yaml 14 个、.md 3 个、.json 2 个。Python 代码合计 65456 行。同样口径下 deeptutor/ 全包是 151534 行,也就是说 services 一层占了约 43.2%。

目录结构上,一级子目录 26 个,含嵌套共 62 个目录。四成多的代码压在一层里,26 个平级子目录——这个形状本身就说明分工不是按调用层次切的,而是按子领域切的。

二、26 个子领域,按行数排开

下面这张表是本文的主表,其它篇不会重复它。左边是一级子目录的 .py 总行数与文件数,右边是一句话职责——绝大多数抄自各自 __init__.py 的模块 docstring,但 llmsessionsearch 三行是例外:这三个模块我们这次没有采到 docstring 原文,那三句是我们按行数排名时自己给的归纳标签,表里已逐行标出,别当成引文去仓库里搜。

子目录行数 / 文件数一句话职责(docstring 或我们的归纳)
llm9263 / 36LLM 统一调用与 provider 接入(非 docstring 原文)
rag7539 / 52RAG 管线导出
session6677 / 10会话与回合运行时(非 docstring 原文)
memory6332 / 28三层记忆:L1 trace / L2-L3 document / ops / paths / ids / store / consolidator
config4930 / 12读写 data/user/settings 下运行期文件的配置助手
subagent3638 / 17驱动用户本机 agent CLI 作为子代理
partners3159 / 8partner 的生命周期、runtime、workspace、sessions
parsing2863 / 29可插拔引擎的文档解析层,一个 IR ParsedDocument + 内容寻址缓存
mcp2774 / 12部署级 MCP 服务器注册表 + 延迟工具适配器
skill2487 / 5按需通过 read_skill 加载的能力 skill 包
search2431 / 15网络搜索(非 docstring 原文)
embedding2312 / 13统一 embedding 客户端与适配器
codex_auth2064 / 7独立的 OpenAI Codex OAuth 支持
cli_apps1798 / 9管理员安装、chat agent 调用的命令行工具
sandbox1195 / 9沙箱 shell 执行,可插拔隔离后端 + 按用户配额
voice715 / 5TTS 与 STT
cron633 / 3内置 cron,为 chat 与 partner 提供定时任务
notebook465 / 2
persona390 / 2chat 的行为/语气预设
prompt370 / 3
videogen355 / 5文生视频,配置走同一个模型 catalog
imagegen350 / 6文生图,配置走同一个模型 catalog
setup322 / 2
storage274 / 2会话工件的可插拔存储后端,目前只暴露 attachment_store
model_selection197 / 3
settings113 / 2

这张表怎么读,比表本身重要。有三点值得单独拎出来:

其一,行数与文件数的比值差得很远。 rag 是 7539 行摊在 52 个文件里,session 是 6677 行只有 10 个文件——其中 session/turn_runtime.py 一个文件就 2141 行,session/sqlite_store.py 1902 行。前者是”多引擎并列,每个都薄”,后者是”少数几个厚文件扛主要状态”。你要改哪一块,翻代码的姿势完全不同。

其二,排在前六的六个目录合计已经超过全层的一半。 llmragsessionmemoryconfigsubagent 六个加起来 38379 行。想快速摸清这个项目,从这六个入手即可,剩下二十个大多是几百行的适配层。

其三,有五个目录的 docstring 我们没有取到一句话职责——表里标 的正好是五行:notebookpromptsetupmodel_selectionsettings,你可以自己对着表数一遍。这不代表它们没写,只代表我们这次核对没有采到,别把空格当成”这个模块没有职责”。

三、根目录只有 7 个文件,但收口点在这儿

deeptutor/services/ 根目录下只有 7 个 .py,合计 1810 行:

文件行数
provider_registry.py551
path_service.py462
auth.py378
pocketbase_client.py196
__init__.py86
generation_http.py74
file_io.py63

这里有一处很容易看错的位置关系:LLM provider 元数据的单一真源不在 llm/ 目录里,而在根目录的 provider_registry.py。这个文件的头注释写的是 “Single source of truth for provider metadata”,还补了一句 “Order matters — it controls match priority and fallback. Gateways first.”(provider_registry.py:3-7)。

llm/provider_registry.py 只有 3 行,内容是对 deeptutor.services.provider_registry 的兼容再导出(llm/provider_registry.py:1-3)。也就是说,你按直觉在 llm/ 目录里找到的那个同名文件,是个转发壳子;真正要改的表在上一级。这类”同名文件一厚一薄”的布局,靠肉眼扫目录树是发现不了的,得 wc -l 一遍才看得出来。

另外两个根文件里,path_service.py 的 docstring 直接画了一棵目录树,说明 data/user 运行期存储布局由它集中管理(path_service.py:2-23)。要搞清楚”这个项目到底往我机器上写什么”,那 22 行 docstring 比翻 README 快得多。

四、这一层的反直觉:它不在依赖图最底下

按大多数人的心智模型,叫 services 的那一层应该是底座:上面的 api / agents 调它,它自己不回头依赖任何人。DeepTutor 这一层不是这样,而且它自己在代码里留了两处痕迹。

痕迹一:__init__.py__getattr__ 懒加载。 services/__init__.py:43-86 用模块级 __getattr__ 懒加载 llm / prompt / search / setup / session / config / rag / embedding 八个子模块,注释里明写目的是”避免循环导入”。一个真正处在底部的层,不需要为循环导入做这种规避。

痕迹二:跨层引用计数是双向的。 我们用 grep -rho 'from deeptutor\.services\.[a-z_]*' 这类计数做了两个方向的统计:

  • 谁在调 servicesapi 201 处、agents 73 处、tools 46 处、core 12 处;configcli 两个目录是 0 处
  • services 在调谁:内部自引用 503 处;向外则依次是 multi_user 45、runtime 18、core 17、partners 15、utils 10、agents 4,logging / knowledge / book 各 2,events / config / api 各 1。

core 这一行尤其能说明问题:core 引用 services 12 处,services 反过来引用 core 17 处,两个方向都不是零。

services 反向 import api / agents 的地方只有 5 处,而且全部是函数内的延迟导入partners/commands.py:9partners/runtime.py:472session/context_builder.py:10session/turn_runtime.py:792session/turn_runtime.py:1213

这里要说清核对的边界:这 5 处里,我们明确记到导入目标的只有 turn_runtime.py:792 一处,它导入的是 from deeptutor.api.routers.settings import get_enabled_optional_tools;其余四处我们只落到了文件与行号,没有逐一核对各自导入的到底是 api 还是 agents 下的哪个对象。你要用这几行做判断,请自己打开看,别照着本文的顺序当成”都是指向 api”。

即便如此,把 import 塞进函数体这个写法本身就有信息量——它是打破循环依赖的常见做法;这 5 处加上那个 __getattr__,构成了这一层的实际形状:它是横向的子领域集合,不是纵向的底座。

这件事对你有两个具体影响。一是别指望能把 services 单独摘出来复用:这 5 处反向引用是实打实存在的,再加上 __getattr__ 那八个模块的懒加载,说明这一层与 api / agents 之间存在双向牵连,不是把一个子目录拷出来就能独立跑起来的东西——具体牵连到什么程度,取决于你摘的是哪一块,得逐处去看。二是排查启动期报错时,循环导入是一个真实可能性:这层已经用懒加载压住了一批,你如果新加一个模块级 import,压住的东西可能会翻出来。

cli 目录 0 处引用 services 也值得记一笔:命令行那一侧不是直接调 services 的。它走的是哪条路,我们这次没有核,不做推断。

五、目录数不等于”在跑的分工数”

26 个一级子目录之下,还有一块嵌套子目录是我们能确认”在生产代码里没有被引用”的:deeptutor/services/llm/providers/。注意它的层级——它挂在 llm 里面,属于”含嵌套共 62 个目录”里的一个,并不在第二节那张 26 行表上,你按那张表去找是找不到的。这个子包 4 个文件、896 行(base_provider.py 204、routing.py 264、anthropic.py 258、open_ai.py 170)。

全仓 grep -rn 'services\.llm\.providers' . --include=*.py 只命中 2 处,都在 tests/ 下(tests/services/llm/test_base_provider.py:7tests/services/llm/test_routing_provider.py:7);deeptutor/services/llm/*.py 里也没有 from .providers 之类的相对导入。这个子包还自带一套独立的 register_provider 注册表(llm/registry.py),与根目录那份 provider_registry.py 并存。

它自己怎么说的?llm/providers/routing.py:1-9 的模块 docstring 写着:这个 provider 委托给已有的函数式 provider,存在的目的是在把调用点逐步迁移到 provider 对象的同时保持公开 API 稳定。相邻的 llm/__init__.py:106 也把 LLMClient / get_llm_client / reset_llm_client 标注为 “Client (legacy, prefer factory functions)”。

按纪律,我们只陈述差异、标明位置:一套并存的注册表实现,生产代码零引用、测试有引用,文档里自述为过渡性桥接。 以我们实读的仓库状态为准,说完就停,不推断原因,也不据此评价项目。

另一处对照口径也放在这儿:AGENTS.md:108 的 Key Files 表里,唯一列出的 services 文件是 deeptutor/services/config/runtime_settings.py,说明写的是 “JSON settings + process-env overrides”。一层 65456 行,文档指路只指了一个文件——这不是文档的错,只是提醒你:指路文档的粒度和这层的实际粒度差着两个量级,别拿它当地图用。

六、这层里有几块会动你本机的东西

这一点必须如实说明,不能藏在架构讨论里带过。services 的 26 个子领域中,至少三块会在你的机器上执行本项目之外的程序:

  • subagent(3638 行):模块 docstring 自述是”驱动用户本机 agent CLI 作为子代理”。后端注册表共 7 个:ClaudeCodeBackendCodexBackendGeminiBackendKimiBackendOpencodeBackendMimoBackendPartnerBackendsubagent/registry.py:24-35)。subagent/registry.py:47-57 记录只有 local_cli=True 的后端会被探测,partner 后端被排除在外。也就是说,它会去找你已经装在本机的那些 agent 命令行工具并调起来。
  • cli_apps(1798 行):docstring 写的是”管理员安装、chat agent 调用的命令行工具”。cli_apps/vendor/catalog.json 里有 101 个 appmeta 段标注 source: https://github.com/HKUDS/CLI-Anything、pinned commit bc536c9…commit_date: 2026-07-09,并写明 “Generated snapshot; do not hand-edit.”。安装动作有超时约束:cli_apps/installer.py:60 的安装超时是 900 秒,可被 DEEPTUTOR_CLI_APP_INSTALL_TIMEOUT_S 覆盖。
  • sandbox(1195 行):三种隔离后端并列——RunnerSidecarBackend(SYSTEM 级,走 runner 容器 HTTP)、BwrapBackend(SYSTEM 级,Linux bare-metal bwrap)、RestrictedSubprocessBackend(APPLICATION 级,降级 fallback)。第三种的注释原文写的是 “admin-opt-in only because it does not OS-isolate”(sandbox/backends.py:1-15:34-50)。这句话是仓库自己写的,我们照抄,不做加工。

把这三块放一起看:**这个项目不只是”调 API 生成内容”,它有明确的、写在源码里的本机执行能力。**要不要开、开哪几个后端,得结合你自己机器上的环境和数据敏感度判断。上面这些默认值都是源码里的默认配置,不是对实际行为的保证。

七、你可以照着核的五步

  1. 在仓库根跑第一节那三条 find,确认 323 个文件、65456 行、62 个目录这三个数——数字对不上说明你的 HEAD 已经不是 456f9c2 了,后面所有行号都要重新对。
  2. wc -l deeptutor/services/*.py,确认根目录只有 7 个文件、1810 行,并注意 provider_registry.py 是 551 行。
  3. 打开 deeptutor/services/llm/provider_registry.py,确认它只有 3 行、是再导出;再回根目录那份看 :3-7 的 “Single source of truth” 注释。
  4. 打开 services/__init__.py:43-86,确认 __getattr__ 懒加载的是哪八个子模块,以及注释里”避免循环导入”那句。
  5. grep -rn 'from deeptutor.services' deeptutor/api | wc -l... deeptutor/core | wc -l 各跑一次,再反过来在 services 里 grep from deeptutor.core,亲眼看一遍两个方向都不是零。

八、本文的边界

需要说清楚没覆盖到什么。我们没有逐行读完这一层任何一个千行以上的文件——session/turn_runtime.pysession/sqlite_store.pypartners/manager.pyconfig/provider_runtime.pyconfig/runtime_settings.pyskill/service.pyskill/hub.py 都只读了 docstring、类与函数签名和常量段。memory/consolidator/ 的四个 mode 与其下 YAML 提示词只看了文件名与目录结构,没有读内容。partnersvoiceimagegenvideogennotebookpersonasetupmodel_selectionsettings 以及根目录的 auth.pypocketbase_client.py 都只取了模块 docstring,内部实现未核实。各子领域内部的具体机制(LLM 接入、重试与超时、检索管线、解析引擎、三层记忆、subagent 细节)我们另有专门的篇目在讲,本文只到分工这一层为止。

还有一条通用的:本文中所有”支持 X”的说法,一律指源码里存在对应的代码路径与常量,不代表这项能力的实际可用性已被验证


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

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