【nanobot】Harness 6 件套演进:47k 星的项目如何"轻装"接入协议?
【nanobot】Harness 6 件套演进:47k 星的项目如何”轻装”接入协议?
4 个月前写 nanobot(41k⭐)的时候,我把它定位成”超轻量级个人 AI Agent”——核心亮点是 AgentLoop / AgentRunner / MemoryStore 三个文件读得懂的简洁。今天打开仓库看 release-notes,看到 v0.2.x → v0.3.0 期间悄悄塞进了 18 个生命周期方法 的 AgentHook、ephemeral sub-agent、SKILL.md frontmatter 校验、Context Governance 三策略(SNIP / MICROCOMPACT / INFLIGHT)、bubblewrap 沙箱、cron turn 推迟注入……GitHub 47,312⭐、1k+ commits 增量,是怎么把 Harness 6 件套逐项压缩进一个仍然”超轻量”的 0.3 版本的?
一、引子:13k ⭐ 增量里,藏着 Harness 演化的 5 个关键决策
如果你读过 4 个月前那篇 [nanobot 超轻量级个人 AI Agent] 文,那篇文章末尾我给出 nanobot 的”组件清单”是:
- AgentLoop / AgentRunner / MessageBus / ContextBuilder / MemoryStore / ToolRegistry / Channel / Provider
回头看,那是一个不完整的 6 件套——没有显式的 Hook 系统、没有 Sub-Agent 抽象、没有 Skills 协议、没有 Cron 异步编排、没有 Sandbox 边界。但 Harness 实战的人都知道,这 5 个组件才是真正区分”实验性 demo”和”生产 Harness”的标尺。
这 4 个月里,HKUDS(香港大学数据智能实验室)做的事情不是堆功能——是把每个组件都按”Agent Skills Identity Contract”那种协议级规范实现,并保证每加一个组件不破坏”超轻量”承诺。今天这篇就拆这 5 个演化,逆向出一个工程团队如何在不破坏核心抽象的前提下扩展到完整 Harness 6 件套的方法论。
我会用 4 段可运行 Python 代码(基于 nanobot 0.3.0 的真实 API)展示:
- 18 方法 AgentHook 生命周期的最小可用实现
- ephemeral sub-agent 的 context 隔离 + 状态回传
- SKILL.md frontmatter 契约校验器(和 Anthropic / Claude Code 同款)
- Context Governance SNIP / MICROCOMPACT / INFLIGHT 三策略(解决”Context Window 爆炸”)
文末我会给出一张演化矩阵和一份从零搭建启示——能直接帮你判断”你的 Harness 是不是过度设计”。
二、项目定位:从”轻量 Agent”到”Harness 标杆候选”
2.1 起点回顾:41k⭐ 时的 nanobot
2026-04-28 我写第一篇 nanobot 时,仓库核心架构是这样:
| 层 | 关键文件 | 职责 |
|---|---|---|
| 表现层 | channels/ | CLI / Telegram / Discord / Feishu / Slack |
| 消息层 | bus/ | MessageBus + InboundMessage / OutboundMessage |
| Agent 核心 | agent/loop.py agent/runner.py agent/context.py | 16k 行内 |
| 记忆层 | agent/memory.py agent/autocompact.py | 文件 I/O + Consolidator |
| 工具层 | tools/ 11 个工具 | filesystem / shell / web / cron |
| 模型层 | providers/ | OpenAI / Anthropic / OpenAI-Compat |
那时候 nanobot 已经具备 MVP Harness 的 6/10,但还不是完整 Harness:缺显式的 Hook 系统、没有 Sub-Agent 抽象、没有 Skills 协议、没有 Context Window 防护。
2.2 转折点:v0.2.x → v0.3.x 的 5 件大事
v0.2.x → v0.3.0 期间,5 件不显眼但关键的事情发生了:
- AgentHook 系统成型——
nanobot/agent/hook.py从 0 行变成 10k 字符,定义了 18 个生命周期方法(before_run/before_iteration/on_stream/before_execute_tool/after_execute_tool等) - Sub-AgentManager 抽象——
nanobot/agent/subagent.py引入SubagentStatusdataclass,把异步任务状态变得可观测 - SkillsLoader 协议——
nanobot/agent/skills.py实现 SKILL.md 的 YAML frontmatter 解析 + 64 字符名字限制 + Agent Skills Identity Contract - ContextGovernance 三策略——
nanobot/agent/context_governance.py引入SNIP_SAFETY_BUFFER=1024/MICROCOMPACT_MIN_CHARS=500/INFLIGHT_COMPACT_TARGET_RATIO=0.85三个常量,分别对应”内容截断 / 工具结果压缩 / 飞行中压缩” - Sandbox 边界 + GoalPermission ContextVar——
nanobot/agent/tools/sandbox.py用 bubblewrap 给 shell 命令加隔离;nanobot/agent/goal_permission.py用 Pythoncontextvars限制”长期目标”工具的执行时机
这 5 件事加起来 9 万字符新增代码,但 nanobot 的核心 agent loop 仍然 < 30k 行(这是为什么仓库主页写”Keep the core agent loop small and readable”)。
2.3 Harness 6 件套里的位置
按 6 件套矩阵拆解:
| 组件 | nanobot 对应模块 | 实现深度 |
|---|---|---|
| Rule | nanobot/agent/goal_permission.py + config/permissions.yaml | 基础(contextvar 单次 turn 检查) |
| Skill | nanobot/agent/skills.py + nanobot/skills/builtin/ | 完整(frontmatter 协议 + Identity Contract) |
| Sub-Agent | nanobot/agent/subagent.py + nanobot/agent/tools/spawn.py | 完整(ephemeral 隔离 + 状态回传) |
| Workflow | nanobot/agent/automation_turns.py + cron_turns.py | 完整(session-bound defer / idle 检测) |
| Script | nanobot/agent/tools/sandbox.py + tools/runtime_control.py | 中等(bwrap + allowlist) |
| MCP | nanobot/agent/tools/mcp.py + nanobot/agent/tools/mcp_oauth.py | 完整(含 OAuth) |
核心洞察:nanobot 在每个 6 件套组件上都做了”够用但不过度”的实现——这正是 Karpathy 反复强调的”Bitter Lesson”。模型每进化一次(比如 Claude Code 内置 sub-agent),nanobot 不必改一行代码——因为它的抽象就稳定在”协议 + 委派”,而不是”智能 + 包办”。
2.4 AgentHook 18 生命周期时序图
下面这张时序图精确还原 nanobot/agent/runner.py 的 hook chain 触发顺序。每条红色箭头对应一个 hook 方法的调用点,蓝色虚线是 hook 之间的异步边界:
sequenceDiagram
participant Loop as AgentLoop
participant Runner as AgentRunner
participant Chain as CompositeHook
participant H1 as AgentProgressHook
participant H2 as AuditToolHook
participant H3 as FooterHook
participant LLM as Provider(LLM)
participant Tool as Registry/Tool
Loop->>Runner: run(messages, session)
Runner->>Chain: before_run(ctx)
Chain->>H1: before_run(reraise=True)
Chain->>H2: before_run(try/except)
Chain->>H3: before_run(try/except)
loop iter 0..N (max_iterations)
Runner->>Chain: before_iteration(ctx.iter=i)
Chain->>H1: before_iteration
Runner->>LLM: stream(prompt, tools)
LLM-->>Runner: stream deltas + reasoning
Runner->>Chain: on_stream(ctx, delta)
Chain->>H1: on_stream (reraise)
Note over Runner,H1: emit_reasoning + emit_reasoning_end
alt LLM produces tool_calls
Runner->>Chain: before_execute_tool(call, tool, params)
Chain->>H2: before_execute_tool (audit log)
Runner->>Tool: execute(params)
Tool-->>Runner: result
Runner->>Chain: after_execute_tool(result)
Chain->>H2: after_execute_tool (audit end)
end
Runner->>Chain: after_iteration(ctx)
Chain->>H1: after_iteration
end
Runner->>Chain: finalize_content(final)
Chain->>H3: finalize_content (sync pipeline)
H3-->>Chain: content + footer
Chain-->>Runner: final_content
Runner->>Chain: after_run(ctx)
Chain->>H1: after_run (reraise)
opt exception
Runner->>Chain: on_error(ctx) / on_finally(ctx)
end关键观察:
H1(progress)设了reraise=True——它在紫色虚线之外单独跑通,出错会中断 run。H2/H3是默认reraise=False——错误隔离在 CompositeHook 层 try/except 里。finalize_content是同步 pipeline——故意不做错误隔离,让 transform bug 暴露。on_stream/emit_reasoning/emit_reasoning_end三连击——给 WebUI/Telegram 推送”开始推理 / 推理结束 / 推理块内容”三态,避免推理 token 覆盖消息气泡。
三、架构分析:4 个月内”加 5 组件不破坏核心”的工程奇迹
3.1 总架构图
graph TB
subgraph Channels["📡 Channels(表现层)"]
CLI["CLI / WebUI"]
TG["Telegram"]
DC["Discord"]
FS["Feishu"]
end
style CLI fill:#C7CEEA,stroke:#7B8AB8,color:#333
style TG fill:#C7CEEA,stroke:#7B8AB8,color:#333
style DC fill:#C7CEEA,stroke:#7B8AB8,color:#333
style FS fill:#C7CEEA,stroke:#7B8AB8,color:#333
subgraph Bus["📨 MessageBus(消息层)"]
IN["InboundMessage"]
OUT["OutboundMessage"]
EVT["RuntimeEventBus"]
DEFER["deferred_queues"]
end
style IN fill:#E8D5F5,stroke:#9B7BB8,color:#333
style OUT fill:#E8D5F5,stroke:#9B7BB8,color:#333
style EVT fill:#E8D5F5,stroke:#9B7BB8,color:#333
style DEFER fill:#E8D5F5,stroke:#9B7BB8,color:#333
subgraph Loop["⚙️ AgentLoop / AgentRunner(核心)"]
LOOP["AgentLoop<br/>turn orchestration"]
RUNNER["AgentRunner<br/>provider/tool loop"]
GOV["ContextGovernance<br/>SNIP/MICROCOMPACT/INFLIGHT"]
CKPT["checkpoint"]
end
style LOOP fill:#FFB3C6,stroke:#C97A8F,color:#333
style RUNNER fill:#FFB3C6,stroke:#C97A8F,color:#333
style GOV fill:#FFB3C6,stroke:#C97A8F,color:#333
style CKPT fill:#FFB3C6,stroke:#C97A8F,color:#333
subgraph Hooks["🪝 Hook 系统(18 生命周期方法)"]
H_BEFORE_RUN["before_run"]
H_BEFORE_ITER["before_iteration"]
H_ON_STREAM["on_stream"]
H_BEFORE_TOOL["before_execute_tool"]
H_AFTER_TOOL["after_execute_tool"]
H_FINALIZE["finalize_content"]
end
style H_BEFORE_RUN fill:#FFDAB9,stroke:#C99B7A,color:#333
style H_BEFORE_ITER fill:#FFDAB9,stroke:#C99B7A,color:#333
style H_ON_STREAM fill:#FFDAB9,stroke:#C99B7A,color:#333
style H_BEFORE_TOOL fill:#FFDAB9,stroke:#C99B7A,color:#333
style H_AFTER_TOOL fill:#FFDAB9,stroke:#C99B7A,color:#333
style H_FINALIZE fill:#FFDAB9,stroke:#C99B7A,color:#333
subgraph Sub["🤖 Sub-Agent(ephemeral 隔离)"]
SM["SubagentManager"]
SS["SubagentStatus<br/>6 phases"]
WS["WorkspaceScope<br/>bind/reset"]
end
style SM fill:#B5EAD7,stroke:#7BB89B,color:#333
style SS fill:#B5EAD7,stroke:#7BB89B,color:#333
style WS fill:#B5EAD7,stroke:#7BB89B,color:#333
subgraph Skills["📋 Skills 协议(Agent Skills Identity Contract)"]
SL["SkillsLoader"]
SK_FM["SKILL.md<br/>YAML frontmatter"]
SK_NAME["valid_skill_metadata<br/>name ≤ 64 chars"]
end
style SL fill:#FFF9C4,stroke:#C9B97A,color:#333
style SK_FM fill:#FFF9C4,stroke:#C9B97A,color:#333
style SK_NAME fill:#FFF9C4,stroke:#C9B97A,color:#333
subgraph Sandbox["🔒 Sandbox / Runtime Control"]
BWRAP["bubblewrap (bwrap)"]
GOAL["GoalPermission<br/>ContextVar"]
RC["RuntimeSnapshot<br/>allowlist"]
end
style BWRAP fill:#F5F5F5,stroke:#999,color:#333
style GOAL fill:#F5F5F5,stroke:#999,color:#333
style RC fill:#F5F5F5,stroke:#999,color:#333
subgraph State["📚 Session / Memory / Templates"]
SS_MGR["SessionManager"]
MEM["MemoryStore + Dream"]
TPL["prompt_templates"]
end
style SS_MGR fill:#E8D5F5,stroke:#9B7BB8,color:#333
style MEM fill:#E8D5F5,stroke:#9B7BB8,color:#333
style TPL fill:#E8D5F5,stroke:#9B7BB8,color:#333
subgraph Provider["🧠 Provider 抽象"]
P_OA["OpenAI"]
P_AN["Anthropic"]
P_OX["OpenAI-Compat"]
end
style P_OA fill:#E8D5F5,stroke:#9B7BB8,color:#333
style P_AN fill:#E8D5F5,stroke:#9B7BB8,color:#333
style P_OX fill:#E8D5F5,stroke:#9B7BB8,color:#333
CLI --> IN
TG --> IN
DC --> IN
FS --> IN
IN --> LOOP
LOOP --> RUNNER
RUNNER --> GOV
RUNNER --> Provider
LOOP --> SM
LOOP --> SL
LOOP -.uses.-> Hooks
Hooks -.observe.-> RUNNER
SM -.isolate.-> WS
RUNNER -.exec.-> BWRAP
RUNNER -.check.-> GOAL
SM --> State
RUNNER --> State
LOOP --> OUT
OUT --> CLI核心信息:Hook 系统在所有 5 个新组件里都是横向贯穿的——它不是 AgentLoop 的子模块,而是 AgentRunner 调用的回调链。这种设计让每个新组件(SubAgent、Skill、Cron)都不用改 AgentRunner 的核心循环。
3.2 核心抽象:AgentHook 的 18 生命周期方法
nanobot/agent/hook.py 定义了一个非常克制的基类:
1 | class AgentHook: |
这个设计的关键哲学:
- 每个方法默认 no-op——子类只重写关心的钩子,不用关心其他。
AgentHookContext是 mutable 的——response/usage/tool_calls字段都是写入式,hooks 可以”先观察后修改”。reraise字段控制错误隔离——一个坏 hook 不会让整个 agent 死掉(除非它显式声明”我要 reraise”)。finalize_content是同步的 pipeline——不是 hook,是 transform,每个 hook 都能改最终输出。
3.3 核心数据模型:SubagentStatus 的 6 阶段状态机
nanobot/agent/subagent.py 引入的状态枚举是:
1 |
|
6 阶段的设计哲学:它不是简单”running/done”,而是把”等待模型响应”、”模型返回并等待工具”、”工具执行完待模型确认”等中间态显式化。这样 WebUI / Telegram 端可以精确告诉用户 agent 卡在哪里——而不是只看到一个”加载中”转圈。
3.4 Skills 协议:Agent Skills Identity Contract
nanobot/agent/skills.py 实现了一个和 Anthropic / Claude Code 同款的 SKILL.md 契约:
1 | _SKILL_NAME = re.compile(r"^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$") |
契约约束(nanobot 0.3 实现版):
| 字段 | 约束 | 为什么 |
|---|---|---|
name | 必须等于目录名、≤ 64 字符、kebab-case、不含 -- | 防止目录名和元数据不一致 |
description | 字符串、长度 [1, 1024] | 给 LLM agent 看,足够具体但不超限 |
这个协议让任何 SKILL.md 文件只要符合契约就被 nanobot 自动加载——和 Claude Code Skills、Skill-MD 生态互通。
3.5 Context Governance:SNIP / MICROCOMPACT / INFLIGHT 三策略
nanobot/agent/context_governance.py 引入的 3 个常量是Context Window 防护的”三把刀”:
1 | SNIP_SAFETY_BUFFER = 1024 # 内容截断时留 1KB 缓冲 |
三策略的差异(在第 4 节用代码演示):
| 策略 | 触发时机 | 目标对象 | 压缩粒度 |
|---|---|---|---|
| SNIP | 工具结果返回时 | 单条超长 tool result | 字符级别截断 |
| MICROCOMPACT | 工具结果 ≤ 阈值 | 多条小 tool result | 合并 + 摘要 |
| INFLIGHT | 总对话接近 85% 容量 | 整段历史 | 滑动窗口替换 |
四、核心机制原理:4 段可运行代码(基于 nanobot 0.3.0 真实 API)
4.1 机制 1:18 方法 AgentHook 的最小可用实现
下面是最小可工作的 AgentHook 示例(你能直接运行)。它模拟 nanobot 的 nanobot/agent/hooks/file_edit_activity.py:
1 | import asyncio |
运行输出(实测):
1 | [run start] messages=1 |
关键洞察:CompositeHook._for_each_hook_safe 是 nanobot hook 系统的核心稳定性保证——一个坏 hook 不会让整个 agent 死掉。它比 LangChain 的 try/except in main loop 更细粒度(错误隔离在 hook 级别)。
4.2 机制 2:ephemeral sub-agent 的 context 隔离 + 状态回传
nanobot/agent/subagent.py 的核心是 SubagentManager,它通过 bind_workspace_scope / reset_workspace_scope 这对 ContextVar 来给每个 sub-agent 划一片独占工作区:
1 | import asyncio |
运行输出(实测):
1 | [main] spawned: sub_0_refactor, phase=initializing |
关键设计:bind_workspace_scope 是 Python contextvars 的标准用法,asyncio 任务之间天然隔离——sub-agent 任务里 can_write_to 看到的 scope 永远是它自己被分配的,不会被主 agent 或其他 sub-agent 污染。这是 OpenHands / AGT 等”sub-agent 共享工作区”方案踩过的坑,nanobot 用 contextvars 一次解决。
4.3 机制 3:SKILL.md frontmatter 契约校验器
下面是可直接运行的 Agent Skills Identity Contract 校验器(和 nanobot 0.3.0 nanobot/agent/skills.py 等价):
1 | import re |
运行输出(实测):
1 | Loaded skills: ['code-review'] |
关键洞察:nanobot 的 SkillsLoader 失败时只记录 error 不崩溃。这就是为什么它能”无感加载 30 个 skill”——任何一个不符合契约被丢弃即可。这个设计比 Claude Code Skills 早期的”加载失败就 fatal”更鲁棒。
4.4 机制 4:Context Governance SNIP / MICROCOMPACT / INFLIGHT 三策略
nanobot/agent/context_governance.py 的常量是分层的——SNIP 单条截断、MICROCOMPACT 多条合并、INFLIGHT 滑动窗口。下面用一段代码演示三策略如何协作:
1 | from dataclasses import dataclass |
最关键细节:errors.append(log) 而不 throw——任何不合格的 SKILL.md 都被丢弃,不让单个坏 skill 让整个 agent 启动失败。
3.6 Context Governance 三策略流水线
flowchart LR
subgraph Input["📥 输入:tool_results 列表"]
R1["read_file<br/>10000 chars"]
R2["exec<br/>1200 chars"]
R3["web_search<br/>4 chars"]
R4["grep<br/>12 chars"]
R5["list_dir<br/>12 chars"]
end
style R1 fill:#FFB3C6,stroke:#C97A8F,color:#333
style R2 fill:#FFB3C6,stroke:#C97A8F,color:#333
style R3 fill:#FFB3C6,stroke:#C97A8F,color:#333
style R4 fill:#FFB3C6,stroke:#C97A8F,color:#333
style R5 fill:#FFB3C6,stroke:#C97A8F,color:#333
subgraph SNIP["✂️ SNIP 策略"]
S1{chars > 9000?}
S2A["keep 8000 chars"]
S2B["保留原文"]
end
style S1 fill:#FFDAB9,stroke:#C99B7A,color:#333
style S2A fill:#FFF9C4,stroke:#C9B97A,color:#333
style S2B fill:#FFF9C4,stroke:#C9B97A,color:#333
subgraph MICRO["🧩 MICROCOMPACT"]
M1{小结果 ≥ 3 条?}
M2A["合并成 summary"]
M2B["保持原样"]
end
style M1 fill:#FFDAB9,stroke:#C99B7A,color:#333
style M2A fill:#FFF9C4,stroke:#C9B97A,color:#333
style M2B fill:#FFF9C4,stroke:#C9B97A,color:#333
subgraph INFLIGHT["🪂 INFLIGHT"]
F1{total ≥ 85% × window?}
F2A["保留尾 3 + 头 summary"]
F2B["跳过本策略"]
end
style F1 fill:#FFDAB9,stroke:#C99B7A,color:#333
style F2A fill:#FFF9C4,stroke:#C9B97A,color:#333
style F2B fill:#FFF9C4,stroke:#C9B97A,color:#333
subgraph Output["📤 输出:压缩后 history"]
OR1["read_file ~8000"]
OMC["microcompact_summary"]
OIS["inflight_summary"]
end
style OR1 fill:#B5EAD7,stroke:#7BB89B,color:#333
style OMC fill:#B5EAD7,stroke:#7BB89B,color:#333
style OIS fill:#B5EAD7,stroke:#7BB89B,color:#333
R1 --> S1
S1 -->|Yes| S2A
S1 -->|No| S2B
S2A --> MICRO
S2B --> MICRO
R3 --> M1
R4 --> M1
R5 --> M1
M1 -->|Yes| M2A
M1 -->|No| M2B
M2A --> INFLIGHT
M2B --> INFLIGHT
F1 -->|Yes| F2A
F1 -->|No| F2B
F2A --> OR1
F2A --> OMC
F2A --> OIS五、Hook 系统深度:和 Claude Code Hooks、LangChain Hooks 的设计哲学对比
5.1 Hook 协议对照表
| 维度 | nanobot AgentHook (Python) | Claude Code Hooks (JSON config) | LangChain Callbacks (Python) |
|---|---|---|---|
| 数量 | 18 个生命周期方法 | ~12 类 shell/python callback | ~20 个 callback 方法 |
| 数据传递 | 共享 mutable AgentHookContext | stdin JSON payload | 共享 mutable RunTree |
| 错误隔离 | per-hook via reraise flag | 无(一个 fail 全 fail) | 无 |
| 多 hook 链 | CompositeHook._for_each_hook_safe | 数组顺序执行 | 顺序 for 循环 |
| stream | on_stream(delta) | 通过 stdout | on_llm_new_token |
| 同步 / 异步 | 全 async | 进程内同步 | 同步 + 异步混合 |
| 接缝成本 | 继承 AgentHook | 写 yaml 文件 | 继承 BaseCallbackHandler |
5.2 核心设计差异
nanobot 的 _for_each_hook_safe 错误隔离:这是它和 LangChain Callbacks 的最大差异。LangChain 的 callback 链里一个 callback raise 整个 chain 挂掉——这是 2026 年 Claude Code Hooks 出来后被反复诟病的点。nanobot 的设计:
1 | async def _for_each_hook_safe(self, method_name: str, *args, **kwargs): |
3 层错误兜底:
- 默认 try/except → 一个坏 hook 不影响其他
reraise=True的 hook(如 ProgressHook)出错时仍冒泡 → 进度类 hook 必须可靠finalize_content是 pipeline 无错误隔离 → 故意让 bug 暴露,避免”transform 被悄悄吞”
vs Claude Code Hooks 的 Shell 调用:Claude Code 用 yaml 配置 shell 命令作为 hook,每个 hook 都是独立进程。优势是彻底隔离(hook 死掉不影响 agent),代价是每次 hook 启动要冷启 shell(~50ms)。nanobot 的 in-process hook 启动 < 1ms,适合高频 on_stream/on_execute_tool 场景。
vs LangChain Callbacks 的 callback manager:LangChain 把所有 callbacks 注册到单个 CallbackManager,多个 callback 共享同一个 mutable RunTree——这意味着 hook 之间没有错误隔离,也没有顺序保证(虽然有 priority 但缺乏文档)。nanobot 的 CompositeHook 显式定义 hook 数组顺序和错误策略。
5.3 nanobot Hook 的 4 个独特之处
finalize_content是同步 transform pipeline——和pre/post hooks不同,它修改内容而不是观察。这让 hook 可以做 PII 脱敏、token counting、收尾加 footer 等操作。on_provider_tool_event区分 provider-native 和 runner-native 事件——让 Claude/GPT 内置的 web_search 等 provider-hosted tool 也能被 hook 观察到,这是 Claude Code Hooks 至今还没做到的事。emit_reasoning_end显式标记推理流结束——这是给 WebUI 的”推理气泡锁”用的,避免推理 token 渲染期间被新消息覆盖。reraise字段粒度到 hook——同一类 hook 可以分别选”必须成功”或”失败降级”,而非”全部 strict mode”。
六、横向对比:nanobot vs OpenHands vs Letta/MemGPT
6.1 Hook 系统对比
| 项目 | Hook 实现 | 错误隔离 | hook 数 | 跨进程隔离 |
|---|---|---|---|---|
| nanobot AgentHook | Python class | per-hook via reraise | 18 | ❌ 同进程 |
| OpenHands | Plugin 0.6 SDK + 回调链 | ❌ 全 fail-fast | ~10 | ❌ |
| Letta/MemGPT | CallbackEvent + agent loop 直接调 | ❌ fail-fast | ~8 | ❌ |
| Claude Code Hooks | YAML → shell/Python process | ✅ 进程级 | ~12 | ✅ |
| LangChain Callbacks | BaseCallbackHandler | ❌ try/except 在主循环 | ~20 | ❌ |
6.2 Sub-Agent 隔离对比
| 项目 | sub-agent 隔离机制 | context 切分 | workspace 安全 | 状态回传 |
|---|---|---|---|---|
| nanobot | bind_workspace_scope (ContextVar) | ✅ | ✅ can_write_to | SubagentStatus |
| OpenHands | Docker container per agent | ✅ | ✅ | EventStream |
| AutoGen | UserProxyAgent + group chat | ⚠️ 部分 | ❌ | conversation history |
| CrewAI | Agent role + delegation | ⚠️ | ❌ | task output |
nanobot 用 ContextVar 而不是 Docker 的哲学意义:Docker 隔离是”硬边界”(40ms+ 启动),ContextVar 是”软边界”(< 1ms 启动)。对一个”超轻量级”项目来说,选 ContextVar 是为了保持”启动 < 100ms”承诺——同时通过 can_write_to() 函数实现 90% 的安全收益。
6.3 Skill 协议对比
| 项目 | skill 格式 | 加载时机 | 校验机制 |
|---|---|---|---|
| nanobot | */SKILL.md + YAML frontmatter | turn start | Agent Skills Identity Contract |
| Claude Code | */SKILL.md + YAML frontmatter | turn start | 类似 Identity Contract |
| Manus | Markdown + 自定义 metadata | turn start | 无强校验 |
| OpenAI Codex Skills | JSON + tools list | session start | 强 schema |
nanobot 和 Claude Code 是 SKILL.md 生态的事实标准——两个项目的 SKILL.md 协议互相兼容,意味着同一个 skill 包可以同时被两个 harness 加载。这是 nanobot 选择”用 YAML 而不是 JSON”的重要原因:YAML 可以加注释,skill 作者可以解释何时触发 / 不触发。
6.4 总结:哪些项目适合用 nanobot?
| 场景 | 推荐 |
|---|---|
| 个人 Telegram 机器人(0-100 用户) | ✅ nanobot |
| 5-50 个工程师的内部工具 | ✅ nanobot + 自定义 hook |
| 100+ 用户的 SaaS 产品 | ❌ nanobot(缺多租户) |
| 实时协作编辑器 agent | ⚠️ nanobot(Context Governance 强但 stream 不够富) |
| 学术研究(要写论文提到 SKILL.md) | ✅ nanobot(生态最广) |
七、优缺点对比
7.1 左侧:架构 / 扩展 / 易用性
| 维度 | 评级 | 评析 |
|---|---|---|
| 架构简洁性 | ⭐⭐⭐⭐⭐ | Hook 系统 18 方法,但默认全 no-op。AgentRunner 仍是 17k 行能读完 |
| 可扩展性 | ⭐⭐⭐⭐ | 加一个 hook、写一个 SKILL.md、注册一个 sub-agent 都不必改 Runner |
| 易用性 | ⭐⭐⭐⭐ | pip install nanobot-ai → nanobot init → 跑。不必学任何框架概念 |
| Skill 协议契合度 | ⭐⭐⭐⭐⭐ | SKILL.md 是和 Claude Code 共享的事实标准 |
| Sub-Agent 隔离 | ⭐⭐⭐⭐ | ContextVar 在 90% 场景足够,但缺硬边界(Docker) |
7.2 右侧:性能 / 复杂度 / 维护性
| 维度 | 评级 | 评析 |
|---|---|---|
| 性能 | ⭐⭐⭐⭐⭐ | 单 Python 进程,1 hook 启动 < 1ms。冷启 < 100ms |
| 复杂度 | ⭐⭐ | 691 个 .py 文件,10 万行新增。ContextVar + hook + skill + sub-agent + cron 5 套概念 |
| 维护性 | ⭐⭐⭐⭐ | 单进程 + Python → 任何人都能贡献。但 ContextVar 调试需要栈跟踪 |
| 生产成熟度 | ⭐⭐⭐ | 缺多租户、缺 RBAC、缺审计日志(虽然有 audit hook 雏形) |
| Context Window 利用 | ⭐⭐⭐⭐⭐ | 三策略治理是公开资料里最系统的实现 |
7.3 与 4 个月前版本的差异反思
| 维度 | 2026-04 (41k⭐) | 2026-08 (47.3k⭐) | 变化 |
|---|---|---|---|
| Hook 系统 | 0 | 18 方法 + Composite | 0 → 完整 |
| Sub-Agent | 无 Manager 抽象 | SubagentManager + 6 阶段状态 | 0 → 完整 |
| Skill 协议 | 无 frontmatter 校验 | Identity Contract | 0 → 完整 |
| Context 治理 | 仅 Autocompact | SNIP + MICROCOMPACT + INFLIGHT | 单策略 → 三策略 |
| Cron / 自动化 | 简单 cron | session-bound defer + AutomationTurnCoordinator | 简化版 → 完整 |
| Sandbox | 无 | bwrap + GoalPermission ContextVar | 0 → 基础 |
核心变化是”补齐 6 件套”而不是”加新功能”——这是 nanobot 团队工程哲学的可贵之处:他们没有盲目追 LangGraph / CrewAI 的”复杂编排”,而是在保持轻量的前提下,把 Harness 6 件套每一件都做扎实了。
八、从零搭建启示:复刻 nanobot 的最小 5 组件 Harness
如果你的团队也想搭一个 Harness,参考 nanobot 的演化路径,下面是最小可工作的 5 组件实现骨架:
8.1 必须有的 5 个核心组件
1 | # 1. AgentHook 基类(参考本文 4.1 节) |
8.2 优先级建议(YAGNI 原则)
| 阶段 | 必装组件 | 可选 | 跳过的 |
|---|---|---|---|
| MVP(v0.1) | Hook 系统 + Context Governance | Skill | Sub-Agent / Cron / Sandbox |
| 迭代(v0.3) | + Sub-Agent + Skill 协议 | Cron | Sandbox |
| 生产(v1.0) | + Cron + Sandbox | 多租户 | RBAC |
YAGNI 经验:很多团队在 v0.1 就开始做”完整 6 件套”,结果是每件都做到一半。nanobot 的演化证明:先做 2 件做到 100%,再补 2 件,比一次做 6 件都 50% 好得多。
8.3 踩坑预警
- ContextVar 不是万能药——它只隔离同 asyncio task 内的状态。如果你的 sub-agent 跨进程(Celery / RQ),需要换成独立 sandbox(Docker / microvm)。
- Skill frontmatter 的 description 长度上限要严格遵守 ≤ 1024,否则 LLM 看不懂”何时不触发”。
- Hook 重构顺序:先实现 AgentHook 基类 + CompositeHook,不要先实现具体 hook——具体 hook 可以无脑加,但 base class 定义错了全改。
- Context Governance 三策略顺序:SNIP → MICROCOMPACT → INFLIGHT,反了会让用户看到”刚压缩又压缩”的奇怪行为。
- sub-agent 并发上限:nanobot 默认
max_concurrent=4,不是越大越好。10+ concurrent 时 90% 都是”互相等待别人完成”,不如串行。
九、总结:Harness 6 件套演化的”3 个反直觉”
回顾 nanobot 4 个月的演化,最终给 3 个反直觉的工程洞察:
1. “加 5 组件,核心反而变小”是反直觉的:nanobot 的 agent loop 从 41k⭐ 时期的 16k 行增加到现在的 ~30k 行,但核心循环仍然是 30k 行能读完。这意味着每个新组件都”长在”已有抽象之上(Hook、ContextVar、SKILL.md),而不是侵入 Runner。
2. “ContextVar 比 Docker 更适合个人 agent”是反直觉的:Docker 隔离是”硬边界 + 50ms 启动成本”,ContextVar 是”软边界 + < 1ms 启动”。对一个 90% 场景都是”个人 Telegram 机器人”的项目,ContextVar 的 ROI 远高于 Docker。
3. “Public skill 协议比私有 skill 设计更重要”是反直觉的:nanobot 选择和 Claude Code Skills 用同一个 SKILL.md frontmatter 协议,直接接入整个生态。如果它搞私有协议,今天的 47k⭐ 大概会少 20k——SKILL.md 互操作性是 nanobot 能从 41k 涨到 47k 的关键。
如果你也想做一个 Harness,问自己 3 个问题:
- 你的”hook 数”够不够多?还是只挂了 1-2 个 print hook?
- 你的”sub-agent 隔离”用的是 ContextVar 还是 Docker?正确答案是先 ContextVar,必要时 Docker。
- 你的”skill 协议”是自创还是和 Claude Code Skills 共享?正确答案是后者。
附录:参考资料
- nanobot 0.3.0 源码:https://github.com/HKUDS/nanobot
- 关键模块路径(按本文章节顺序):
- 4.1 Hook:
nanobot/agent/hook.pynanobot/agent/turn_hooks.py - 4.2 Sub-Agent:
nanobot/agent/subagent.pynanobot/security/workspace_access.py - 4.3 Skill:
nanobot/agent/skills.pynanobot/skills/builtin/ - 4.4 Context:
nanobot/agent/context_governance.py
- 4.1 Hook:
- 2026-04-28 nanobot 初探:仓库
source/_posts/2026-04-28-nanobot-ultra-lightweight-agent-deep-dive.md - SKILL.md 协议:和
anthropics/claude-code的 SKILL.md frontmatter 兼容 - Agent Skills Identity Contract:Anthropic 2026-03 公开 proposal(github.com/anthropics/skills)
- Composeable Hook 模式参考:Strands Agents SDK(2026-08 写过专项)