你见过 deep research 工作:提一个问题,它拆解、分头调查、汇总成报告。 但它内部到底发生了什么?
打开任何一个多智能体框架找答案,迎接你的是几万行代码、层层抽象和回调地狱。 nanoteam 反着来:一个 Leader、几个 Worker、174 行核心代码,一天读完, 从此你对"多智能体系统"这五个字不再心虚。
它不是又一个框架,而是一份可以运行的教材。
- 小到能读完 — 核心代码 ≤ 1000 行是写进 CI 的承诺,当前只用了 174 行
- 薄到能看穿 — 唯一依赖是 openai SDK,没有基类、没有装饰器、没有魔法,数据流向一眼可见
- 真到能跑通 — 不是伪代码,配好三个环境变量就能产出真实的研究报告
配套两级阅读:overview.md(入门:跑通并看懂大概)→ how-it-works.md(精读:拆、做、合、工具与失效边界)。
三条命令,从零到第一份研究报告:
pip install -e .
export NANOTEAM_API_KEY=<你的Key> NANOTEAM_BASE_URL=<服务地址> NANOTEAM_MODEL=<模型名>
nanoteam "调研新手学习多智能体系统的三条路线,对比后给出建议"然后换成你自己的问题,或者直接把资料喂给它:
nanoteam "综合这两份笔记,给出结论" a.md b.md附上文件,它就变成"基于给定材料的 deep research":Leader 把资料与角度分给各 Worker,再综合成报告。
"174 行"是怎么数的? 只统计
nanoteam/下除prompts.py、__init__.py外.py文件的非空、非注释行,与 nanoGPT 惯例一致。不信可以自己数:sh scripts/count_lines.sh——超过 1000 行,CI 直接挂掉。
没有黑箱。一次完整运行 = N + 2 次模型调用:1 次分解 + N 次执行 + 1 次汇总,
成本可以掰着手指头算(开启 web 搜索后每次"执行"变为至多 MAX_TOOL_ROUNDS
轮工具循环,见下文"配置"):
运行时,整个过程摊开在你眼前——过程透明是本项目的生命线:
[leader] 收到目标:调研新手学习多智能体系统的三条路线,对比后给出建议
[leader] 分解出 4 个任务:
1. 调研路线一:从主流框架入手(CrewAI/AutoGen)
2. 调研路线二:从极简实现入手(读源码型项目)
...
[worker-1] 开始执行:调研路线一 ...
[worker-1] 完成 ✓(产出 812 字)
...
[leader] 全部任务完成,开始汇总
[leader] 最终产出 ↓
三个环境变量,没有配置文件,没有 CLI 选项。不设默认值、不偏向任何供应商——
任选一家 OpenAI 兼容服务即可(嫌麻烦可 cp .env.example .env 填好后 source .env):
| 环境变量 | 含义 | 必填 |
|---|---|---|
NANOTEAM_API_KEY |
模型服务的 API Key | ✅ |
NANOTEAM_BASE_URL |
OpenAI 兼容服务地址 | ✅ |
NANOTEAM_MODEL |
模型名 | ✅ |
NANOTEAM_SEARCH_URL / NANOTEAM_SEARCH_KEY |
开启 web 搜索(v0.2) | 可选 |
NANOTEAM_CONFIRM |
执行前人工确认(v0.2) | 可选 |
两种供应商的配置示例(DeepSeek / Qwen)
# DeepSeek 写法
export NANOTEAM_API_KEY=<你的 API Key>
export NANOTEAM_BASE_URL=https://api.deepseek.com
export NANOTEAM_MODEL=deepseek-chat
# Qwen(百炼 / DashScope)写法
export NANOTEAM_API_KEY=<你的 API Key>
export NANOTEAM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
export NANOTEAM_MODEL=<模型名,如 qwen-plus>想让它真的上网搜? 再加两个环境变量,Worker 就会通过 function calling 自主搜索互联网——从"基于已有知识/材料的研究"升级为真正的 deep research:
export NANOTEAM_SEARCH_URL=https://api.tavily.com/search # Tavily:零改动直用
export NANOTEAM_SEARCH_KEY=<你的搜索 Key>接口形状是常见的"POST JSON {"query": ...} + Bearer 鉴权,响应含 results
列表(title/url/content)",Tavily 原生即此格式。想换国内的博查等服务?
按 search.py 顶部注释改 run() 一个函数即可——那是全项目唯一发 HTTP
请求的地方,约 20 行。
想在执行前把把关? 设置 NANOTEAM_CONFIRM=1,分解结果展示后会停下来问你
(回车继续,输入 n 取消)。这是 human-in-the-loop 的最小形态——只有
"继续/取消"两个选择,编辑任务留作读者练习:
export NANOTEAM_CONFIRM=1 # 不设置即保持全自动六个文件讲完整个系统,每个文件一句话说清职责:
| 文件 | 行数(实测) | 职责 |
|---|---|---|
nanoteam/task.py |
14 | 核心数据结构:Task 与 Plan,整个系统唯一的状态容器 |
nanoteam/llm.py |
37 | 模型访问薄封装,唯一接触第三方 SDK 的文件 |
nanoteam/search.py |
23 | 搜索薄封装,唯一发起 HTTP 请求的文件(标准库 urllib,零新依赖) |
nanoteam/worker.py |
28 | Worker:一次 run() 即一个全新实例,上下文隔离 + 最小工具循环 |
nanoteam/leader.py |
40 | Leader:decompose / dispatch / aggregate,教学核心 |
nanoteam/cli.py |
32 | 入口:读材料、可选的分解确认、跑流程、渲染过程 |
| 合计 | 174 / 1000 | |
nanoteam/prompts.py |
不计入 | 全部提示词与工具声明(一等公民,场景锚定只发生在这里) |
tests/ examples/ |
不计入 | 冒烟测试与三个示例 |
这几条不是实现细节,而是可以带走复用的 agent 工程心法。
模型做判断,代码做搬运。
decompose 时模型只看"目标 + 文件清单(路径与行数)",给出任务与文件分组;
把文件内容搬进 Task.materials 的是代码。让模型碰它不必碰的大块数据,
是很多 agent 系统又贵又脆的根源。
接缝是可数的。
v0.1 系统与外界的接缝只有 llm.chat 一处;开启搜索后变成两处(模型在
llm.py,搜索在 search.py)——"一个工具让接缝从一处变成两处"正是 v0.2
的第一课。引用形式统一 from nanoteam import llm + llm.chat(...)
(search 同理),禁止 from nanoteam.llm import chat:后者会让冒烟测试
的模块级替换静默失效。
fail fast,不兜底。
API 失败不重试;分解 JSON 无效时抛出并打印模型原始输出,让你亲眼观察模型行为;
材料超过 MAX_MATERIALS_CHARS(10 万字符,cli.py 中可改的常量)直接报错退出;
工具循环超过 MAX_TOOL_ROUNDS(5 轮,在 worker.py)同样抛错而非静默截断。
每一次失败都摊开给你看,这正是教材该有的样子。
定位锚定,代码中立。
deep research 只出现在提示词与示例中,核心代码是通用编排引擎——
换一套 prompts.py,nanoteam 就是另一个领域的团队。
失效边界写在明面上。 Leader 会累积所有 Worker 的结论,Worker 一多(经验值 4 个以上)就会逼近上下文 窗口上限。MVP 的 3–6 个任务不会触及,但你应该知道它会在哪里坏掉。另外,开启 搜索后模型调用次数从确定的 N + 2 变为一个区间,成本不再是精确可预估的常数。
零网络、零 key、零测试框架,毫秒级跑完:
python tests/test_smoke.py # 离线跑完整闭环
sh scripts/count_lines.sh # 行数检查,> 1000 即失败测试的手法只有一种:用赋值逐一替换系统与外界的接缝(llm.chat、
llm.chat_tools、search.run)。看懂这个测试,你就理解了"接缝是可数的"——
接缝每多一处,测试就多替换一处。
| 示例 | 讲什么 |
|---|---|
research_sources.md |
纵切:多份资料分头研读后综合(材料切分的典型形态) |
research_angles.md |
横切:同一材料多角度分析 |
analyze_repo/ |
进阶·自举:让 nanoteam 研究自己的代码库(文件遍历脚本不进核心代码) |
可讨论的临时决议
以下两点为不阻塞实现而采取的临时决议,欢迎在 issue 中讨论:
- 不设任何默认模型与默认 base_url:三个环境变量缺一即报错退出(报错信息附两种 供应商的配置示例)。这避免了项目隐含推荐某家供应商,代价是首次配置多一步。
- 最终报告仅终端打印,不落盘。"把报告写入 Markdown 文件"留作下面的读者练习。
读懂之后,亲手改一改才算真的懂:
- 把最终报告写入 Markdown 文件(提示:改
cli.py的最后三行); - 修改
MAX_MATERIALS_CHARS,体会材料超限时"报错 / 截断 / 摘要"三种方案的取舍; - 换掉
prompts.py的三段提示词,把研究团队改造成翻译团队或评审团队—— 验证"核心代码零场景专用逻辑"是真的; - 把确认模式(
NANOTEAM_CONFIRM)升级为"可编辑":确认时允许输入任务序号删掉 某个任务再执行——这是向"多轮人机协作"(社区扩展方向)迈出的第一步。