章节导航 第 6 章 / 共 21 章
模块管理与扩展开发
nexus modules CLI、TUI、写自定义 Task/Agent/Tool 的完整流程、pyproject.toml 元数据规范
nexus modules CLI
Nexus 的模块管理是框架的一个亮点——你不需要修改任何核心代码就能添加新功能。
列出所有模块
nexus modules
输出按类别分组(Agent / Task / Tool / LLM Client / Executor / Provider / Processor),每个模块显示 Host 和 Runtime 依赖的安装状态:
Agent (16)
base 3/3 ✓ 0 ✓ 1 agent
browser_gym 4/4 ✓ 0 ✓ 1 agent
claude_code 0 ✓ 0 ✓ 1 agent
...
Task (28)
swe 5/5 ✓ 2/2 ✓ 5 tasks
webarena 0 ✓ 0 ✓ 1 task
...
56 modules, all OK
安装缺失依赖
# 安装指定模块的 Host 依赖
nexus modules install swe_agent
# 安装指定模块的 Host + Runtime 依赖
nexus modules install swe_agent --runtime
# 安装所有模块的依赖
nexus modules install --all
交互式 TUI
nexus modules --tui
启动一个基于 Textual 的终端 UI,可以浏览模块、查看依赖、一键安装。在 Terminal 里操作比 CLI 更直观。
模块的组织方式
Nexus 的模块分布在 8 个扫描根目录下:
nexus/
├── extensions/
│ ├── agents/ # Agent 模块
│ ├── tasks/ # Task 模块
│ ├── tools/ # Tool 模块
│ ├── projects/ # Project 模块(自包含的完整项目)
│ └── openai_clients/# LLM Client 模块
├── runtime/
│ ├── providers/ # Runtime Provider 模块
│ └── executors/ # Executor 模块
└── trace/
└── processors/ # Trace Processor 模块
每个模块是一个目录(含 pyproject.toml)或单文件(含 PEP 723 inline metadata)。
写一个自定义 Tool
从最简单的开始。假设你要写一个计算器工具。
步骤 1:创建目录
nexus/extensions/tools/calculator/
├── pyproject.toml
├── __init__.py
└── tool.py
步骤 2:写 pyproject.toml
[project]
name = "calculator"
dependencies = []
[tool.nexus]
tools = { calculator = "nexus.extensions.tools.calculator.tool::calculate" }
[tool.nexus] 是关键——它告诉模块发现系统:这个模块注册了一个名为 calculator 的 Tool,import path 是 nexus.extensions.tools.calculator.tool::calculate。
步骤 3:写工具函数
# tool.py
from pydantic import BaseModel
from nexus.core import ToolContext, ToolResponse, tool_registry
class CalculateInput(BaseModel):
"""计算数学表达式"""
expression: str
@tool_registry.register('calculator', meta={
'description': 'Evaluate a mathematical expression',
'tool_type': 'local',
})
async def calculate(params: CalculateInput, context: ToolContext) -> ToolResponse:
try:
result = eval(params.expression) # 生产环境请用 safe eval
return ToolResponse(output=[{'type': 'text', 'text': str(result)}])
except Exception as e:
return ToolResponse(output=[{'type': 'text', 'text': f'Error: {e}'}])
Tool 函数必须满足:
- 是 async 函数
- 第一个参数是 Pydantic BaseModel 子类(自动生成 OpenAI function schema)
- 第二个参数是
ToolContext - 返回
ToolResponse
步骤 4:验证
nexus modules # 应该能看到新的 calculator Tool
然后在 Agent 配置中使用:
agent_config:
tools:
- "calculator"
写一个自定义 Agent
Agent 的骨架更简单——就是一个 async 函数。
# my_agent/agent.py
from nexus.core import (
AgentRunInput, AgentRunOutput, WithUserPrompt, WithMainTrajectory,
agent_registry, require_context,
)
class MyAgentInput(AgentRunInput, WithUserPrompt):
max_iterations: int = 10
class MyAgentOutput(AgentRunOutput, WithMainTrajectory):
final_answer: str
@agent_registry.register('my_agent')
async def run_my_agent(agent_input: MyAgentInput) -> MyAgentOutput:
ctx = require_context()
client = ctx.openai_client
runtime = await ctx.get_runtime()
messages = [
{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': agent_input.user_prompt},
]
for i in range(agent_input.max_iterations):
completion = await client.create_completion(messages=messages)
assistant_msg = completion.choices[0].message
if not assistant_msg.tool_calls:
# 没有工具调用,Agent 认为任务完成
break
# 处理工具调用...
messages.append(assistant_msg.model_dump())
return MyAgentOutput(
final_answer=assistant_msg.content or '',
main_trajectory=messages,
)
对应的 pyproject.toml:
[project]
name = "my_agent"
[tool.nexus]
agents = { my_agent = "nexus.extensions.agents.my_agent.agent::run_my_agent" }
Agent 函数的约束:
- 是 async 函数
- 恰好一个参数,类型是
AgentRunInput子类 - 返回类型是
AgentRunOutput子类 - 通过
require_context()访问 Runtime 和 LLM Client
写一个自定义 Task
Task 是最上层的编排单元。
# my_bench/task.py
from nexus.core import (
TaskRunInput, TaskRunOutput, WithAgentRunner, WithScore,
task_registry, require_context,
)
class MyBenchInput(TaskRunInput, WithAgentRunner):
question: str
expected_answer: str
class MyBenchOutput(TaskRunOutput, WithScore):
agent_answer: str
correct: bool
@task_registry.register('my_bench')
async def run_my_bench(task_input: MyBenchInput) -> MyBenchOutput:
# 1. 运行 Agent
agent_output = await task_input.run_agent(
user_prompt=task_input.question,
)
# 2. 从 Agent 输出中提取答案
answer = agent_output.final_answer if hasattr(agent_output, 'final_answer') else ''
# 3. 评分
correct = answer.strip().lower() == task_input.expected_answer.strip().lower()
return MyBenchOutput(
agent_answer=answer,
correct=correct,
score=1.0 if correct else 0.0,
)
Task 函数的约束:
- 是 async 函数
- 恰好一个参数,类型是
TaskRunInput子类 - 返回类型是
TaskRunOutput子类 - 如果需要 Agent,input 继承
WithAgentRunner,通过task_input.run_agent()调用
pyproject.toml 元数据规范
[tool.nexus] section 的完整格式:
[project]
name = "my_module"
description = "My awesome module"
version = "0.1.0"
dependencies = [ # Host + Runtime 共享依赖
"requests>=2.28.0",
]
[project.optional-dependencies]
host = [ # 仅 Host 侧需要的依赖
"rich>=13.0",
]
runtime = [ # 仅 Runtime 侧需要的依赖
"playwright>=1.40.0",
]
[tool.nexus]
# 注册对象:{ 注册名 = "import_path" }
agents = { my_agent = "nexus.extensions.agents.my_agent.agent::run_my_agent" }
tasks = { my_task = "nexus.extensions.tasks.my_task.task::run_my_task" }
tools = { my_tool = "nexus.extensions.tools.my_tool.tool::my_tool_func" }
executors = { my_exec = "nexus.runtime.executors.my_exec::my_executor" }
openai_clients = { my_client = "nexus.extensions.openai_clients.my_client" }
runtime_providers = { my_provider = "nexus.runtime.providers.my_provider" }
span_processors = { my_processor = "nexus.trace.processors.my_processor" }
三层依赖
Nexus 将依赖分为三层:
| 层级 | 位置 | 说明 |
|---|---|---|
dependencies | Host + Runtime | 两端都需要的包 |
optional-dependencies.host | 仅 Host | Host 侧额外需要的包(如 rich、textual) |
optional-dependencies.runtime | 仅 Runtime | Runtime 侧额外需要的包(如 playwright) |
这种分层的原因:Runtime 镜像通过 Nix volume 挂载,不能随意 pip install。Host 侧的重依赖不应该污染 Runtime 环境。nexus modules install --runtime 会安装 dependencies + runtime 的合集。
单文件模块(PEP 723)
对于简单模块,不需要创建目录,可以用 PEP 723 inline metadata 直接在 .py 文件中声明:
# nexus/runtime/providers/local.py
# /// script
# dependencies = []
#
# [tool.nexus]
# runtime_providers = { local = "nexus.runtime.providers.local" }
# ///
from nexus.runtime.providers.base import RuntimeProvider
# ...
# /// script 和 # /// 之间的注释会被解析为 TOML 格式的元数据。
模块注册范式对比
Python 生态有三种主流的模块注册范式:
| 范式 | 代表 | 注册时机 | 适用规模 | 局限 |
|---|---|---|---|---|
| 元类自动注册 | metaclass-registry | 子类定义即注册,零样板 | 小型项目 | 与继承深度绑定,不适合跨包发现 |
| import string 懒加载 | lazyregistry | 显式 registry["name"] = "mod::func" | 库内使用 | 无框架级发现,无验证管道 |
| TOML 元数据驱动 | Nexus [tool.nexus] | discover_and_register() 读 TOML | 框架级(8 根目录 56+ 模块) | 需要约定的目录结构 |
| Agent 框架级插件 | Genkit Agents API + Qwen Agent Plugins v1 | 统一 chat() 接口 / plugin.json+mcp.json+SKILL.md 便携包 | npm/pip 包 + 配置文件 | 生态级(开源社区驱动);v1 包格式不支持 commands / agents / hooks(见下) |
Nexus 选择 TOML 驱动是因为需要跨 8 个扫描根目录发现 56+ 模块——元类和 import string 范式不适合这种规模。TOML 元数据让发现过程完全不执行 Python 代码,从根本上解耦了”声明”和”实现”。Agent 框架级插件范式(qwen-code 25k★ / Genkit Agents API)则代表了另一个维度——不是代码级的模块注册,而是 Agent 生态级的可替换组件——对评测框架而言,两种粒度可能需要同时兼容。
Agent 框架级插件范式的两个最新进展值得补充:(1) Genkit Agents API(2026-07-16~08-01 发布)将 Google Genkit 从”插件”升级为”统一 Agent 接口”——统一 chat() 接口(preview)+ middleware 体系在 TS/Go/Dart 达到 GA,Agent Skills 以 SKILL.md + progressive disclosure 跨 TS/Go/Dart/Python 分发。
⚠️ 更正(2026-10-03):上面这段里”统一
chat()接口(preview)“的说法已经过时,而且方向说反了。Genkit 1.39.0(npm 实测发布 2026-06-26)是一次破坏性变更,官方发布说明原文是「Removal of the beta Chat API:Chatclass,ai.chat(...), andsession.chat(...)are deleted. Migrate toai.defineAgent(...)and useagent.chat()instead.」也就是说:chat()不是”统一后的新接口”,而是已被替换掉的旧接口;“preview”这个限定词既过时(它不再是预览特性,因为它已经不存在了)又误导(它暗示旧路径还留着)。正确的现状是——Agents API 是唯一路径,状态化多轮对话、Zod 类型化的自定义 state、可插拔持久化、流式 patch、interrupts/resume、abort/detach 都由它提供,并配套agents()/artifacts()中间件。后续版本(npm
genkit当前 latest = 1.42.0,发布于 2026-09-01,GitHub release 记为 09-02):1.42 新增 A2UI 插件(@genkit-ai/a2ui,Agent 以增量方式向客户端流式输出可交互 UI 表面,中间件注入 catalog 能力并校验 a2ui block),并把 OpenTelemetry logs 改为默认开启(GENKIT_OTEL_ENABLE_LOGSflag 被移除)。官方路线图的首要目标是让 Agents API 跨 TS/Go/Dart/Python 达到 stable。⚠️ 时间戳提醒:本轮核验发现,二手发布聚合站对 Genkit 1.42.0 的标注是”2026-10-01~02”,与 npm registry 的 2026-09-01 差整整一个月;1.39.0 也被标成”约 2 个月前”,实际是 2026-06-26(约三个多月前)。版本号与日期一律以 npm registry 的
time字段为准。
这套 SKILL.md 约定的实际覆盖面已有公开统计:截至 2026-09-14,vercel-labs/skills(npm 包 skills,当前 1.5.26)的 README 列出 68 个 AI coding agent 的 skills 路径约定;其特性对比矩阵覆盖 18 个 agent,其中 context: fork 目前仅 Claude Code 支持。⚠️ 该数字增长很快(早期版本的公开统计约 39,本轮未回源核实其出处与日期 ),引用时必须带快照日期。路径约定本身也高度不统一——Amp / Replit / Universal 与 Antigravity 走 .agents/skills/,AstrBot 走 data/skills/,另有若干 agent 使用自家目录——这正是”通用 agent”与”需 symlink 适配”之分。
🔄 锚点刷新(2026-10-03):这一段自带的警示在 19 天后应验了。截至 2026-10-03,npm
skills的 latest 已是 1.7.0(1.6.0与1.7.0同日发布于 2026-09-17,前一个是 09-11 的 1.5.26);README 顶部自述句改成「Supports OpenCode, Claude Code, Codex, Cursor, and 75 more」,即合计 79 个 agent。⚠️ 口径差:79 只能引自述句,不能自己数表。 本轮用正则解析 README 的 agent 表格只得到 67 行(含表头),与顶部自述的 79 对不上——两个数字无法互相印证。引用时只取顶部自述数并带快照日期。
复核后仍成立的两条:特性矩阵仍是 18 个 agent 列,
context: fork仍然只有 Claude Code 支持(其余 17 个全为 No)。矩阵本轮新增了 Hooks 行(Claude Code / Cline / Command Code 为 Yes)。另有三个新出现的技术细节值得记:① 技能发现有深度上限——README 原文是每个技能容器目录「walked up to three levels deep」,超出部分要用
--full-depth才扫得到(例如examples/、tests/下的);② 支持从 manifest(.claude-plugin/marketplace.json或plugin.json里的source/skills字段)声明技能,声明的路径按其声明深度搜索、不受 depth-3 约束,README 明说这是为兼容 Claude Code plugin marketplace;③ 这套约定现在有了正式规范出口——Agent Skills specification,规范站为 https://agentskills.io ,README 称各 agent 的 skills「follow a shared Agent Skills specification」。第 ③ 条要加一个限定:本轮实测该站可访问(HTTP 200),其 GitHub 仓库是独立组织
agentskills/agentskills(2025-12-16 创建,约 2.6 万 star,自述”Specification and documentation for Agent Skills”),不是 vercel-labs 的单方站点;但本站未逐页读规范正文,也未核实其是否为多厂商共同发布,因此不把它写成”行业标准”,只写成”已有正式规范出口”。顺带一句对本章论点的支撑:这些细节恰好说明统一的是规范,不统一的是落地目录——规范站只有一份,而扫描根目录仍在不断增加(本轮 README 的发现目录列表已含
.codebuddy/skills/、.commandcode/skills/等)。
(2) Qwen Code Agent Plugins v1(Qwen Code 0.24.7;npm registry 实测,2026-10-03,发布于 2026-09-29 )原生加载可移植的 v1 包,保留标准 plugin.json / mcp.json / SKILL.md,不生成 qwen-extension.json、不重写可移植文件;根清单须指向规范 schema https://agent-plugins.org/schemas/1.0.0/plugin.schema.json。
要分清”支持什么 / 不支持什么 / 不是什么”三层:
-
支持:直接子级
skills/*/SKILL.md、stdio MCP、Streamable HTTP MCP。 -
不支持——做成 v1 包后这些不生效:
能力 行为 Commands、agents、hooks 目录被忽略 旧版 HTTP+SSE MCP 条目被跳过(无静默的传输层替换) Qwen context / settings / channels / apps 忽略 extensions.*客户端命名空间未实现的命名空间被忽略 -
不是什么:官方文档结尾原话是「Agent Plugins v1 is a package format, not a marketplace integration」(Agent Plugins v1 是一种包格式,而非市场集成)。Qwen Code 确实能从 Gemini CLI Extensions Gallery / Claude Code Marketplace / Qoder 安装兼容包,但那是 Qwen Code 自身扩展体系的另一条能力,与 v1 包格式是两条独立路径——不要因为装上了 v1 包,就假定 commands / hooks 也会跟着生效。
三个实现细节值得留意:① 实验性的 allowed-tools 字段「被识别为字符串,但不会授予预批准的 Qwen tools」,权限仍走 Qwen 自身的权限策略(与 Ch04 讨论的 Installed Agent 权限边界直接相关);② 对 stdio MCP,${PLUGIN_ROOT} / ${PLUGIN_DATA} 在 args / env / cwd 中只展开一次,PLUGIN_DATA 是可写的、每安装独立的目录,内容在更新与重装后保留;③ 远程 MCP 端点必须使用 HTTPS(环回 HTTP 端点除外)。
前者代表”从插件到统一接口”的定位升级,后者代表”跨平台、声明式开放打包标准”——两者与 Nexus 的 [tool.nexus] TOML 元数据方案形成对照:TOML 元数据面向框架内模块发现,Agent Plugins v1 面向跨平台扩展分发。
Project 模块
extensions/projects/ 是一种特殊的模块类型——自包含的完整项目,同时定义 Task + Agent + Tool。
内置的 7 个 Project:
| Project | 说明 |
|---|---|
calculator | 计算器(教学示例,常用于 RL 训练) |
maze | 迷宫寻宝(RL 训练环境) |
agts | AGTS Agent 训练系统 |
deepeyes | DeepEyes 视觉 Agent |
search_r1_demo | Search R1 Demo |
videocut | 视频剪辑 Agent |
android_demo | Android 操作示例 |
Project 的 pyproject.toml 会同时注册 agents、tasks、tools:
[tool.nexus]
agents = { calculator_agent = "...agent::run_calculator_agent" }
tasks = { calculator_task = "...task::calculator_task" }
tools = { calculator_add = "...tools::add", calculator_multiply = "...tools::multiply" }
使用时需要显式导入 Project 模块:
imports:
- "nexus.extensions.projects.calculator"
task_name: "calculator_task"
agent_name: "calculator_agent"
这是因为 discover_and_register() 默认会注册所有模块,但 Project 的工具可能与通用工具冲突,所以设计为按需导入。
模块规模扩展的挑战
当前 56 个模块的规模下,nexus modules TUI 浏览体验尚可,依赖冲突概率可控。但当模块数量扩展到 500+ 时,会面临三个挑战:
- 浏览体验下降:TUI 的分类列表在数百条目下变得冗长,需要搜索和筛选能力
- 依赖冲突增加:不同模块可能依赖同一包的不同版本,目前的三层依赖模型无法处理版本冲突
- 全量扫描低效:启动时扫描 500+ 模块的 TOML 虽然不执行代码,但 I/O 开销不可忽略
S1-NexusAgent(arXiv:2602.01550)提出的意图感知动态工具检索提供了一种解法——用子任务级意图嵌入与 Faiss 索引匹配,按需选择和热插拔工具,而非全量扫描。可能的演进方向是:模块市场 + 语义搜索 + 按需安装,从”发现所有”转向”按需检索”。
这个方向已经不是设想了——对照产品里已经有了。 qwen-code 的 0.24 线(2026-09-16 的 0.24.0 到 2026-09-29 的 0.24.7,19 天内跨完)连续合入两条正对着上面三个挑战的改动:
align Code Mode text with lazy tool discovery(#12990)——工具按需要才进入上下文,对应挑战 1 与 3;share a per-turn token budget from a +Nk directive(#11917)——每轮有显式 token 预算,对应挑战 3 的 I/O 与上下文开销。
同一批还合入了 add budget-based ACP child admission(#11911)与 cap web_search calls per session(#11852),都是把”每轮/每会话能用多少”变成显式配额。这把本章的演进方向从”可能的发展路径”降格为”同行业已经在做的事”,读者评估 Nexus 的 500+ 模块方案时可以直接照这个尺子量。
⚠️ 上面四条均取自 release notes 的条目标题;
lazy tool discovery只在 nightly 的 fix 标题里反复出现,其具体语义(工具是否真的延迟加载、还是仅文案对齐)本轮未读 PR 正文,“已实现”应限定为”标题层面可见”。
另有一条与本章”三层依赖 / 沙箱边界”同族的对照:qwen-code 0.24.0 新增 add bwrap kernel sandbox backend for Linux(#11614)。沙箱内的执行边界是同一族问题——本章 Runtime 镜像经 Nix volume 挂载后不能随意 pip install,与”沙箱里能跑什么”是同一条约束的两种表述。
一个值得关注的工业对照是 Google Genkit 的插件架构(2026-07 Agents API 预览)——它选择了更粗粒度的”模型 Provider + 中间件”二维分类而非三维,避免了 Nexus 面临的 Task/Agent/Tool 交叉注册复杂度。这种”减法”策略对评测框架的模块管理有参考价值。
至此,使用篇结束。如果你已经能跑通任务、切换 Agent、产出训练数据、写自定义模块,那”赶时间”路径的目标已经达成。
从第 7 章开始,我们进入原理篇——拆解 Nexus 的核心设计。