解码 exo 的 AGENTS.md:面向 AI 编码代理的构建、测试与分布式架构指南
AGENTS.md 是 exo 仓库为 AI 编码代理准备的“工作手册”:它定义了项目概览、构建与运行命令、提交前必过的四项检查、节点级架构与事件溯源模型,以及 Dashboard UI 的无头截图流程。读完本文,你将掌握 exo 的完整开发工作流——从 uv run exo 启动一个本地集群,到理解 Router/Worker/Master/Election/API 五大组件如何通过 zenoh 类型化主题协同,并能按仓库规范完成类型检查、Lint、格式化与测试。
项目概览:exo 是什么
AGENTS.md 开篇给出的一句话定义是整个文档的基石:
exo is a distributed AI inference system that connects multiple devices into a cluster. It enables running large language models across multiple machines using MLX as the inference backend and zenoh for peer-to-peer networking.
即 exo 是一个分布式 AI 推理系统,把多台设备连接成集群,使用 MLX 作为推理后端、zenoh 作为点对点网络层。仓库 pyproject.toml 可以印证这一技术栈:核心依赖包含 pydantic、fastapi、exo-rs(Rust 绑定)、huggingface-hub 等,而 [tool.uv.sources] 将 exo-rs 声明为 workspace 成员;Rust 侧 Cargo.toml 将 zenoh 精确钉死在 =1.9.0,并通过 [patch.crates-io] 指向 exo 维护的 zenoh 分支,同时引入 pyo3/pyo3-async-runtimes 0.28 作为 Python 桥接层。项目固定运行于 Python ==3.13.*。
构建与运行命令
AGENTS.md 给出的命令清单是代理动手前的第一张地图,完整保留如下:
# Build the dashboard (required before running exo)
cd dashboard && npm install && npm run build && cd ..
# Run exo (starts both master and worker with API at http://localhost:52415)
uv run exo
# Run with verbose logging
uv run exo -v # or -vv for more verbose
# Run tests (excludes slow tests by default)
uv run pytest
# Run all tests including slow tests
uv run pytest -m ""
# Run a specific test file
uv run pytest src/exo/shared/tests/test_election.py
# Run a specific test function
uv run pytest src/exo/shared/tests/test_election.py::test_function_name
# Type checking (strict mode)
uv run basedpyright
# Linting
uv run ruff check
# Format code (using nix)
nix fmt
这些命令与仓库中的配置一一对应,值得逐条核实:
- Dashboard 必须先构建。src/exo/main.py 的入口
main()只负责组装Node并启动事件循环,Dashboard 静态产物由 API 服务端直接托管(AGENTS.md 注明输出位于dashboard/build/)。仓库的 justfile 也提供了等价的build-dashboard目标(npm install && npm run build)。 uv run exo的默认端口。从 src/exo/main.py 的Args定义可见默认值:api_port: PositiveInt = 52415(即 AGENTS.md 所说的 API 地址http://localhost:52415)、zenoh_port默认 52414(zenoh 固定 TCP 监听口)、discovery_port默认 52413(UDP 发现服务端口)。此外还支持--no-worker、--no-downloads、--offline、--no-batch、--force-master、--legacy-daemon、--fast-synch/--no-fast-synch等参数。-v/-vv的机制。Args.parse()用action="count"解析--verbose、用store_const(-1)解析--quiet,最终把 verbosity 传入logger_setup(EXO_LOG, args.verbosity),因此日志级别完全由这个计数驱动。- 测试默认排除 slow 用例。pyproject.toml 的
[tool.pytest.ini_options]配置了markers = ["slow: marks tests as slow (deselected by default)"]与addopts = "-m 'not slow' --ignore=tests --ignore=tmp",这解释了为什么uv run pytest默认跳过慢测试,而uv run pytest -m ""会用空 marker 表达式覆盖addopts中的筛选、跑全量测试。
提交前必过的四项检查(Pre-Commit Checks)
AGENTS.md 用加粗标注了 REQUIRED:提交前必须跑通全部检查,否则 CI 会失败:
# 1. Type checking - MUST pass with 0 errors
uv run basedpyright
# 2. Linting - MUST pass
uv run ruff check
# 3. Formatting - MUST be applied
nix fmt
# 4. Tests - MUST pass
uv run pytest
也可以一条命令串联执行:
uv run basedpyright && uv run ruff check && nix fmt && uv run pytest
文档还强调两点细节:若 nix fmt 改动了文件,提交前必须把它们加入暂存区;CI 会执行 nix flake check,同时校验格式化、Lint 并运行 Rust 测试。
这四项检查的“严苛程度”在配置文件里可以找到依据:
- basedpyright 是 strict 模式。pyproject.toml 中
typeCheckingMode = "strict"且failOnWarnings = true,并把reportAny、reportUnknownVariableType、reportMissingParameterType、reportMissingTypeStubs、reportInvalidCast、reportUnnecessaryCast等全部提升为 error——这正是 AGENTS.md 所说“MUST pass with 0 errors”的含义。 - ruff 启用了大量规则族。
[tool.ruff.lint]中extend-select = ["I", "N", "B", "A", "PIE", "SIM"],覆盖 import 排序、命名、Bugbear、aliased-import、可读性等类别。 - Rust 侧同样有严格 lint。Cargo.toml 的
[workspace.lints]对 clippycorrectness设为deny,并对panic、unwrap_used、indexing_slicing、as_conversions等restriction类检查设为warn。
架构:单节点内的组件与集群协作
节点构成(Node Composition)
AGENTS.md 指出:一个 exo 的 Node(位于 src/exo/main.py)同时运行多个组件:
- Router:基于 zenoh 的 pub/sub 消息层,经由 Rust 绑定(
exo_rs)实现; - Worker:处理推理任务、下载模型、管理 runner 进程;
- Master:协调集群状态,把模型实例放置到各个节点;
- Election:用 Bully 算法选举 Master;
- API:FastAPI 服务,提供 OpenAI 兼容的 chat completions。
从源码看,这一描述与 src/exo/main.py 的 Node 数据类字段完全吻合:router: Router、event_router: EventRouter、download_coordinator、worker、election、master、api。其中有几处实现细节值得注意:
Node.create()中每个节点都会创建Master实例(“We start every node with a master”),而 src/exo/main.py 的_elect_loop()负责根据选举结果动态提升(promoting)或降级(demoting)本节点的 master 角色,并在“新 master 产生”时重建 EventRouter、DownloadCoordinator 与 Worker;--force-master参数的实现是把选举优先级(seniority)直接设为 1,000,000,从而让本节点必然赢得 Bully 选举——源码注释里还留了一句自嘲:“If someone manages to assemble 1 MILLION devices into an exo cluster then. well done. good job champ.”- 启动时
main()会先做 PID 文件锁(Pidfile(EXO_PID_FILE, 0o0600),来自 Rust 绑定exo_rs),防止同一台机器重复拉起节点;--legacy-daemon则走双 fork 的经典 SysV 守护进程化路径。
消息流:类型化主题(Typed Topics)
AGENTS.md 列出五个组件间通信的主题(定义于 src/exo/routing/topics.py):
| 主题 | 方向与用途 |
|---|---|
GLOBAL_EVENTS |
Master 向所有 Worker 广播带索引的事件 |
LOCAL_EVENTS |
Worker 向 Master 发送事件以供索引 |
COMMANDS |
Worker/API 向 Master 发送命令 |
ELECTION_MESSAGES |
选举协议消息 |
CONNECTION_MESSAGES |
zenoh 连接状态更新 |
对照 src/exo/routing/topics.py 的实现,可以看到两个文档未明说但很关键的机制:
TypedTopic携带序列化契约。每个主题由(topic 名, publish 策略, 绑定的 Pydantic 模型类型)三元组构成,序列化统一走model_dump_json()编码为 UTF-8 字节,反序列化走model_validate_json()。消息类型安全由主题定义本身保证。PublishPolicy三档策略。Never(本地消息,不上网络)、Minimal(仅当本地无接收者时才发布)、Always(始终发布)。实际配置中CONNECTION_MESSAGES使用Never,其余均为Always;此外源码中还有一个文档未列出的DOWNLOAD_COMMANDS主题(承载ForwarderDownloadCommand),在 src/exo/main.py 中与其余主题一并注册。
事件溯源(Event Sourcing)
AGENTS.md 将状态管理概括为三点,均可在源码中逐条验证:
State是不可变状态对象:src/exo/shared/types/state.py 中的State(FrozenModel)使用strict=True、extra="forbid"的ConfigDict,字段包括instances、runners、downloads、tasks、topology、last_event_applied_idx,以及按不同频率更新的细粒度节点状态映射(内存、磁盘、网络、Thunderbolt、RDMA 状态等)。apply()是纯函数:src/exo/shared/apply.py 的event_apply(event, state) -> State用 Pythonmatch对Event联合类型做穷举匹配(ChunkGenerated、TaskCreated、TopologyEdgeDeleted等),逐分支调用apply_*纯函数派生新状态——这直接体现了仓库“用类型级纪律替代运行时分支”的编码信条。- Master 索引事件并广播,Worker 应用带索引的事件:事件从 Worker 出发经
LOCAL_EVENTS到 Master 打索引,再经GLOBAL_EVENTS广播回各节点,EventRouter(见 src/exo/routing/event_router.py)负责这两条链路的收发。
关键类型层级与 Rust 组件
AGENTS.md 指出共享类型全部位于 src/exo/shared/types/ 下的 Pydantic 模型:events.py(事件的判别联合)、commands.py(命令类型)、tasks.py(Worker 执行的任务类型)、state.py(集群状态模型)。所有节点通过这些类型在“消息”与“状态”之间形成单一事实来源。
Rust 组件方面,AGENTS.md 列出三项:networking(zenoh 网络层,gossipsub、节点发现)、exo_rs(PyO3 绑定,向 Python 暴露 Rust 能力)、system_custodian(系统级操作)。从当前 Cargo.toml 的 workspace 成员看,实际纳入编译的是 rust/exo_rs 与 rust/networking 两个 crate(rust/exo_rs 通过 PyO3 提供 Pidfile 等能力,rust/networking 包含 discovery.rs、swarm.rs 等),system_custodian 未出现在当前 workspace 中——可以推断该条目描述的是文档写作时的组件全景,阅读时以 Cargo.toml 为准。
Dashboard
AGENTS.md 的最后一句:Dashboard 是 dashboard/ 下的 Svelte 5 + TypeScript 前端,构建产物输出到 dashboard/build/ 并由 API 服务托管。仓库中 dashboard/src/routes/ 包含主聊天页、advanced、downloads、integrations、traces 等路由,dashboard/src/lib/components/ 下有 TopologyGraph.svelte、ModelPickerModal.svelte 等 20 余个组件,前端通过 HTTP 与同一进程内的 API 协作。
代码风格与测试约定
AGENTS.md 引用的代码风格要求(源自 .cursorrules)可归纳为:
- 严格且穷尽的类型标注,绝不绕过类型检查器;
- 枚举式集合用
Literal[...],原始类型包装用typing.NewType; - Pydantic 模型统一
frozen=True+strict=True; - 纯函数 + 可注入的 effect handler:副作用(如触发 saga)封装成函数传入纯函数,而不是让纯函数直接产生副作用;
- 命名描述化——禁止缩写与三字母缩写;
- 只在你能有意义地处理异常时才 catch;
- 在适用处使用
@final与不可变性。
这些要求与 RULES.md 的仓库规则互为表里(例如 UUID 需继承 Pydantic UUID4、幂等标签用加盐哈希生成以保证崩溃恢复后不重放、错误处理理由必须写进 docstring 等),也是 State 这类 FrozenModel 的由来。
测试方面,AGENTS.md 说明:测试使用 pytest-asyncio 的 asyncio_mode = "auto",测试代码放在被测代码同目录的 tests/ 子目录中,测试运行期间自动设置环境变量 EXO_TESTS=1。这三点在 pyproject.toml 中全部有对应配置:
[tool.pytest.ini_options]
asyncio_mode = "auto"
markers = ["slow: marks tests as slow (deselected by default)"]
env = ["EXO_TESTS=1"]
addopts = "-m 'not slow' --ignore=tests --ignore=tmp"
因此 uv run pytest src/exo/shared/tests/test_election.py 这类命令可以直接定位到 AGENTS.md 示例中提到的选举测试;仓库根目录下的 tests/ 则存放 1/2/4 节点集群级集成测试与 Dashboard 测试,被默认 --ignore 排除在单测之外。
Dashboard UI 测试与无头截图
AGENTS.md 的最后部分给出了完整的 Playwright 工作流,适合代理自动化完成 UI 验证:
构建并启动:
# Build the dashboard (must be done before running exo)
cd dashboard && npm install && npm run build && cd ..
# Start exo (serves the dashboard at http://localhost:52415)
uv run exo &
sleep 8 # Wait for server to start
一次性安装 Playwright:
npx --yes playwright install chromium
cd /tmp && npm init -y && npm install playwright
无头截图脚本(cd /tmp && node -e "..." 执行):
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto('http://localhost:52415', { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// Inject test data into localStorage if needed (e.g., recent models)
await page.evaluate(() => {
localStorage.setItem('exo-recent-models', JSON.stringify([
{ modelId: 'mlx-community/Qwen3-30B-A3B-4bit', launchedAt: Date.now() },
]));
});
await page.reload({ waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// Interact with UI elements
await page.locator('text=SELECT MODEL').click();
await page.waitForTimeout(1000);
// Take screenshot
await page.screenshot({ path: '/tmp/screenshot.png', fullPage: false });
await browser.close();
})();
两个实现细节值得留意:其一,exo-recent-models 直接注入 localStorage 后 reload,说明 Dashboard 的“最近使用模型”是纯前端持久化,这与 dashboard/src/lib/stores/recents.svelte.ts 的 store 实现路径一致;其二,AGENTS.md 随后给出向 PR 上传截图的变通方案(先把截图提交到分支、用永久 commit SHA 的 raw 地址发 PR 评论、再删除截图文件)——因为 GitHub API 不支持直接向 PR 评论上传图片,而引用已推送 commit 的原始 URL 可保证后续删除文件后评论中的图片仍可渲染。
关键路径速查
| 主题 | 路径 |
|---|---|
| AI 代理工作手册(本文主体) | AGENTS.md |
| 仓库工程规则 | RULES.md |
节点入口与 Args 参数定义 |
src/exo/main.py |
类型化主题与 PublishPolicy |
src/exo/routing/topics.py |
| 事件溯源纯函数 | src/exo/shared/apply.py |
| 不可变全局状态 | src/exo/shared/types/state.py |
| 共享类型(events/commands/tasks) | src/exo/shared/types/ |
| Rust workspace(exo_rs + networking) | Cargo.toml、rust/ |
| 工具链配置(pytest/basedpyright/ruff) | pyproject.toml |
| 常用任务(sync/check/test/build-dashboard) | justfile |
| Dashboard 前端 | dashboard/src/ |
小结
AGENTS.md 的价值在于把“如何正确地在这个仓库里干活”压缩成了五个可执行要点:先构建 Dashboard 再运行;提交前跑通 basedpyright → ruff → nix fmt → pytest 四道闸门;架构上以 Router/Worker/Master/Election/API 五组件 + 五个类型化主题为骨架;状态管理走“不可变 State + 纯函数 apply”的事件溯源路线;UI 验证用 Playwright 无头流程自动化。对照 src/exo/main.py、src/exo/routing/topics.py 与 pyproject.toml 的源码,文档中的每一条约定都能在仓库中找到落点,这正是它对人与 AI 代理都同样可靠的根本原因。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
