Skip to content

Instantly share code, notes, and snippets.

@frostming
Last active March 11, 2026 06:07
Show Gist options
  • Select an option

  • Save frostming/67dcf2c02dcfc5aa585212adc2a27ad4 to your computer and use it in GitHub Desktop.

Select an option

Save frostming/67dcf2c02dcfc5aa585212adc2a27ad4 to your computer and use it in GitHub Desktop.
为什么我认为 Bub 比 nanobot 更像一个“长期可演进”的工程底座

为什么我认为 Bub 比 nanobot 更像一个“长期可演进”的工程底座

最近把当前项目 bubnanobot 放在一起看了一遍。这里文中的 nanobot,指的是该本地 checkout 当前对应的上游项目 HKUDS/nanobot 代码。

为了避免“讨论的不是同一版代码”,先把本文评估所基于的提交写清楚:

  • bub: 370a68ad29b5877d923053c19656c9562c6583ea
  • nanobot: 82f4607b99818d3c785b7f2b540d6044bb06b4cc

如果先给结论:

  • bub 更像一个克制的框架内核,抽象边界更整齐,架构更清楚,长期可维护性更好。
  • nanobot 更像一个快速生长中的产品型 agent 平台,功能覆盖更广,现成能力更多,但复杂度管理已经开始变成主要挑战。

换句话说:

  • 如果我要选一个适合继续打磨、适合做长期二次开发的底座,我会更倾向 bub
  • 如果我要选一个“现在就能接很多渠道、很多 provider、很多能力”的现成系统,nanobot 更强。

这篇文章不只给判断,也会给出代码片段,说明这个判断是怎么来的。

先说整体印象:两者分别在优化什么

看完代码后,一个很明显的感受是,两边的优化目标并不完全一样。

bub 在优化的是“内核的清晰度”。它的主问题像是:

  • 框架骨架怎么定义
  • 扩展点怎么暴露
  • 默认实现怎么挂上去
  • turn lifecycle 怎么稳定下来

nanobot 在优化的是“产品能力的覆盖面”。它更像是在持续回答这些问题:

  • 再加一个 channel 怎么办
  • provider 再多一个怎么接
  • MCP、cron、memory、subagent 如何都放进系统里
  • CLI 和 gateway 怎样更方便用户

这不是谁对谁错,而是关注点不同。但工程上会导致一个直接后果:

  • bub 更容易在架构上保持干净
  • nanobot 更容易在功能上快速变强

Bub 的核心优点:它的主路径非常短,而且抽象边界稳定

bub 里最值得肯定的地方,不是某个技巧,而是它的主流程足够短,足够清楚。

BubFramework.process_inbound(),你基本一眼就能看出系统的运行骨架:

async def process_inbound(self, inbound: Envelope) -> TurnResult:
    session_id = await self._hook_runtime.call_first(
        "resolve_session", message=inbound
    ) or self._default_session_id(inbound)

    state = {"_runtime_workspace": str(self.workspace)}
    for hook_state in reversed(
        await self._hook_runtime.call_many("load_state", message=inbound, session_id=session_id)
    ):
        if isinstance(hook_state, dict):
            state.update(hook_state)

    prompt = await self._hook_runtime.call_first(
        "build_prompt", message=inbound, session_id=session_id, state=state
    )

    model_output = await self._hook_runtime.call_first(
        "run_model", prompt=prompt, session_id=session_id, state=state
    )

    await self._hook_runtime.call_many(
        "save_state",
        session_id=session_id,
        state=state,
        message=inbound,
        model_output=model_output,
    )

    outbounds = await self._collect_outbounds(inbound, session_id, state, model_output)
    for outbound in outbounds:
        await self._hook_runtime.call_many("dispatch_outbound", message=outbound)

这段代码的好处非常直接:它建立了一个很稳定的心智模型。

你不需要先读很多背景,几乎就能立即知道系统是怎么流动的:

  1. 先确定 session
  2. 再加载状态
  3. 再构造 prompt
  4. 然后跑模型
  5. 保存状态
  6. 生成 outbound
  7. 派发 outbound

对于一个框架来说,这种“主路径清楚”本身就是质量。

对应源文件:

Bub 的第二个优点:把 hook 语义单独抽出来了

很多项目也会有 plugin / hook 机制,但常见问题是 hook 只是“存在”,真正的执行语义还是散落在业务流程里。

bub 在这点上做得比较认真。它专门有一个 HookRuntime 来处理 hook 调用的规则,比如:

  • 是拿第一个结果还是拿所有结果
  • sync / async 怎么兼容
  • on_error 观察者失败时是否影响其他观察者
  • 参数如何裁剪给不同 hook 实现

例如下面这段:

async def call_first(self, hook_name: str, **kwargs: Any) -> Any:
    for impl in self._iter_hookimpls(hook_name):
        call_kwargs = self._kwargs_for_impl(impl, kwargs)
        value = await self._invoke_impl_async(
            hook_name=hook_name, impl=impl, call_kwargs=call_kwargs, kwargs=kwargs
        )
        if value is _SKIP_VALUE:
            continue
        if value is not None:
            return value
    return None

以及错误观察者隔离:

async def notify_error(self, *, stage: str, error: Exception, message: Envelope | None) -> None:
    for impl in self._iter_hookimpls("on_error"):
        call_kwargs = self._kwargs_for_impl(impl, {"stage": stage, "error": error, "message": message})
        try:
            value = impl.function(**call_kwargs)
            if inspect.isawaitable(value):
                await value
        except Exception:
            logger.opt(exception=True).warning(
                "hook.on_error_failed stage={} adapter={}",
                stage,
                impl.plugin_name or "<unknown>",
            )

这类设计的价值在于,它把“扩展机制的行为定义”从框架业务里提纯了出来。以后如果有人要理解优先级、兼容性、错误处理,不需要到处 grep。

对应源文件:

Bub 为什么更像框架,而不是一个不断膨胀的应用

bub 的核心不是 builtin,而是 contract。

hookspecs.py,扩展点本身就是一份相对稳定的契约:

class BubHookSpecs:
    @hookspec(firstresult=True)
    def resolve_session(self, message: Envelope) -> str:
        raise NotImplementedError

    @hookspec(firstresult=True)
    def load_state(self, message: Envelope, session_id: str) -> State:
        raise NotImplementedError

    @hookspec(firstresult=True)
    def build_prompt(self, message: Envelope, session_id: str, state: State) -> str | list[dict]:
        raise NotImplementedError

    @hookspec(firstresult=True)
    def run_model(self, prompt: str | list[dict], session_id: str, state: State) -> str:
        raise NotImplementedError

    @hookspec
    def render_outbound(
        self,
        message: Envelope,
        session_id: str,
        state: State,
        model_output: str,
    ) -> list[Envelope]:
        raise NotImplementedError

这会直接带来一个工程上的好处:新增能力时,默认思路更容易是“实现一个 hook”,而不是“去改中央控制器”。

这类项目通常更能活得久。因为它优先稳定的是结构,而不是单次功能交付。

对应源文件:

但是 Bub 也不是没有问题

如果只夸 bub,结论会失真。它有几个问题也很明显。

1. 弱类型边界仍然很多

文档里其实已经自己承认了这一点:Envelope 和跨插件 state 目前都还是弱约束模型。

这意味着在项目还小时很灵活,但如果插件数、状态字段、上下游协作者变多,治理成本会逐渐上升。你会开始面对这些问题:

  • 同一个字段到底谁写谁读
  • 某个 plugin 依赖的 state 键有没有被别的 plugin 覆盖
  • 类型约定靠文档还是靠运行时猜测

这不是眼前的大 bug,但确实是未来的架构风险点。

2. builtin 已经有一点“默认实现过重”的趋势

BuiltinImpl 现在承担了默认 session、state、prompt、model、CLI 命令注册、channel 提供、error outbound 等很多职责:

class BuiltinImpl:
    @hookimpl
    def resolve_session(self, message: ChannelMessage) -> str:
        ...

    @hookimpl
    async def load_state(self, message: ChannelMessage, session_id: str) -> State:
        ...

    @hookimpl
    def build_prompt(self, message: ChannelMessage, session_id: str, state: State) -> str | list[dict]:
        ...

    @hookimpl
    async def run_model(self, prompt: str | list[dict], session_id: str, state: State) -> str:
        ...

    @hookimpl
    def register_cli_commands(self, app: typer.Typer) -> None:
        ...

    @hookimpl
    def provide_channels(self, message_handler: MessageHandler) -> list[Channel]:
        ...

这还没有糟糕到需要重构,但如果 builtin 继续增长而没有二次拆分,它会成为 bub 未来最容易变重的地方。

对应源文件:

nanobot 的优点:它不是“理念先行”,而是真的做了很多东西

如果反过来看 nanobot,最明显的优点并不是代码多,而是它确实已经把很多产品能力做出来了。

从目录和配置模型就能看出,它的覆盖范围非常广:

  • 多个 chat channel
  • 多个 LLM provider
  • MCP
  • cron
  • heartbeat
  • memory
  • subagent
  • bridge

例如 ChannelsConfig 就已经说明它面对的是一个更完整的产品系统:

class ChannelsConfig(Base):
    send_progress: bool = True
    send_tool_hints: bool = False
    whatsapp: WhatsAppConfig = Field(default_factory=WhatsAppConfig)
    telegram: TelegramConfig = Field(default_factory=TelegramConfig)
    discord: DiscordConfig = Field(default_factory=DiscordConfig)
    feishu: FeishuConfig = Field(default_factory=FeishuConfig)
    mochat: MochatConfig = Field(default_factory=MochatConfig)
    dingtalk: DingTalkConfig = Field(default_factory=DingTalkConfig)
    email: EmailConfig = Field(default_factory=EmailConfig)
    slack: SlackConfig = Field(default_factory=SlackConfig)
    qq: QQConfig = Field(default_factory=QQConfig)
    matrix: MatrixConfig = Field(default_factory=MatrixConfig)

从“现成能力”这个角度说,nanobot 明显更强,而且 README、安装、快速开始、渠道说明也更成熟。

对应源文件:

nanobot 的主要问题:复杂度开始回流到少数几个中心对象

我对 nanobot 最大的保留,不是它写得乱,而是它已经开始出现典型的“成长型项目中心过载”。

最典型的是 AgentLoop

不过这里要补一个基于最新代码的修正:和我上一次看时相比,nanobot 最近已经在做一些收敛动作,例如把一部分记忆相关职责抽成了 MemoryConsolidator,同时把 provider 调用收口到 chat_with_retry。这说明它不是毫无节制地继续堆功能,而是在尝试把复杂度往外拆。

它的构造函数就已经暴露出这个问题:

class AgentLoop:
    def __init__(
        self,
        bus: MessageBus,
        provider: LLMProvider,
        workspace: Path,
        model: str | None = None,
        max_iterations: int = 40,
        temperature: float = 0.1,
        max_tokens: int = 4096,
        reasoning_effort: str | None = None,
        context_window_tokens: int = 65_536,
        brave_api_key: str | None = None,
        web_proxy: str | None = None,
        exec_config: ExecToolConfig | None = None,
        cron_service: CronService | None = None,
        restrict_to_workspace: bool = False,
        session_manager: SessionManager | None = None,
        mcp_servers: dict | None = None,
        channels_config: ChannelsConfig | None = None,
    ):
        ...

参数很多不是原罪,但它通常是一个信号:这个对象正在吸收越来越多系统层面的职责。

继续往下看,它内部做的事情也很多:

self.context = ContextBuilder(workspace)
self.sessions = session_manager or SessionManager(workspace)
self.tools = ToolRegistry()
self.subagents = SubagentManager(...)

self._mcp_servers = mcp_servers or {}
self._mcp_stack: AsyncExitStack | None = None
self._mcp_connected = False
self._mcp_connecting = False
self._active_tasks: dict[str, list[asyncio.Task]] = {}
self._processing_lock = asyncio.Lock()
self.memory_consolidator = MemoryConsolidator(
    workspace=workspace,
    provider=provider,
    model=self.model,
    sessions=self.sessions,
    context_window_tokens=context_window_tokens,
    build_messages=self.context.build_messages,
    get_tool_definitions=self.tools.get_definitions,
)
self._register_default_tools()

这段新代码其实也正好说明了我的评价为什么需要稍微修正,但不需要推翻。修正点在于:

  • nanobot 确实在继续抽离子职责,这是一种正向变化
  • AgentLoop 仍然是一个很重的系统级协调器

也就是说,它比之前更收敛了一些,但尚未从根本上改变“中央对象偏重”的结构特征。

后面的方法也印证了这一点,它既负责 tool 注册,也负责 MCP 连接、响应 stop、任务派发、异常兜底、消息发送。这种对象在项目早期很高效,但当系统规模继续上升时,修改成本会越来越集中。

对应源文件:

另一个信号:CLI 已经开始承载过多职责

nanobot 的 CLI 很实用,但它也已经是一个很大的聚合点。

例如 commands.py 同时包含:

  • 终端兼容处理
  • prompt_toolkit 输入逻辑
  • onboard
  • provider 构造
  • config 装载
  • gateway 启动
  • 其他 command 的入口

单看片段就能感觉到职责密度:

app = typer.Typer(
    name="nanobot",
    help=f"{__logo__} nanobot - Personal AI Assistant",
    no_args_is_help=True,
)

@app.command()
def onboard():
    ...

def _make_provider(config: Config):
    ...

def _load_runtime_config(config: str | None = None, workspace: str | None = None) -> Config:
    ...

@app.command()
def gateway(
    port: int = typer.Option(18790, "--port", "-p", help="Gateway port"),
    workspace: str | None = typer.Option(None, "--workspace", "-w", help="Workspace directory"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
    config: str | None = typer.Option(None, "--config", "-c", help="Path to config file"),
):
    ...

这种写法并不是错,但它说明 CLI 现在更像一个总装层,而不是薄薄的一层命令入口。对小项目这没问题,但项目再长大,拆分收益会很明显。

对应源文件:

如果只谈“代码质量”,我为什么仍然更偏向 Bub

很多时候,“代码质量”不是看谁更炫,而是看谁更稳定、谁更少让维护者迷路。

在这点上,bub 的平均质量感受更好,原因主要有三个。

第一,关键路径更短

bub 的主流程、hook 语义、builtin 默认实现之间关系相对简单。阅读顺序也比较自然:

  • 先看 framework
  • 再看 hookspecs
  • 再看 hook runtime
  • 最后看 builtin

这种阅读路径本身就是架构质量的一部分。

第二,测试更贴着抽象边界

bub 的测试里有一类我比较喜欢的内容:它不是只测表面命令,而是在测系统契约本身。

例如这个测试就在验证 hook 优先级语义:

@pytest.mark.asyncio
async def test_call_first_respects_priority_and_returns_first_non_none() -> None:
    called: list[str] = []

    class LowPriority:
        @hookimpl
        def resolve_session(self, message):
            called.append("low")
            return "low"

    class MidPriority:
        @hookimpl
        def resolve_session(self, message):
            called.append("mid")
            return "mid"

    class HighPriorityReturnsNone:
        @hookimpl
        def resolve_session(self, message):
            called.append("high")
            return None

    result = await runtime.call_first("resolve_session", message={"session_id": "x"})
    assert result == "mid"
    assert called == ["high", "mid"]

这类测试的价值很高,因为它锁住的是“系统的行为定义”。

对应源文件:

第三,文档与代码是对齐的

bub 的架构文档不是那种“有图但没约束”的文档,而是真的在描述当前系统行为。对长期项目来说,这一点非常重要。

对应文件:

但如果只谈“产品能力”,nanobot 更强

这个也要说清楚。bub 现在更像一个框架雏形已经长出清晰骨架,但它的现成功能面明显没有 nanobot 宽。

nanobot 的强项是:

  • 更完整的渠道体系
  • 更完整的 provider 体系
  • 更丰富的产品功能
  • 更成熟的 README / onboarding / quick start

所以如果评价标准更偏“我今天拿来能用多少东西”,那它会更占优。

只是从长期工程的角度看,功能丰富本身并不自动等于架构健康。功能越多,反而越需要更强的复杂度管理。当前 nanobot 最需要补的,恰好就是这部分。

从软件工程角度看,两者各自成熟在不同层面

Bub 的成熟,偏向“底座工程”

pyproject.toml 可以看出,bub 在静态检查、测试、文档、开发命令上是有意识地做治理的:

[tool.mypy]
files = ["src"]
no_implicit_optional = true
check_untyped_defs = true
warn_return_any = true

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
target-version = "py312"
line-length = 120
fix = true

这类配置不花哨,但能说明项目是把“长期修改质量”当回事的。

对应文件:

nanobot 的成熟,偏向“产品交付”

nanobot 的 README、安装路径、使用方式、发布节奏都明显更强,说明它已经更接近一个可被外部用户实际消费的产品:

**1. Initialize**

```bash
nanobot onboard

2. Configure (~/.nanobot/config.json)

3. Chat

nanobot agent

这个层面的成熟度,bub 目前确实还不如它。

对应文件:

一个更中肯的总结:谁更好,取决于你问的是哪一层

如果问题是:

“谁更像一个值得长期维护和继续演化的框架底座?”

那我的答案是 bub

因为它有几个很关键的优点:

  • 主路径短
  • 抽象边界清楚
  • hook 语义被单独封装
  • 文档与代码一致
  • 测试贴着契约而不是只贴着表层功能

如果问题是:

“谁现在已经更像一个能直接拿来用的多能力 agent 产品?”

那答案会是 nanobot

因为它已经做出了更多现实能力:

  • 更多 channel
  • 更多 provider
  • 更多周边系统能力
  • 更完整的用户入口

最终评价

最后给一个尽量克制的判断。

bub 不是“功能上更强”的那个,但它是“结构上更稳”的那个。 nanobot 不是“架构上更差”的那个,但它是“复杂度已经开始逼近边界”的那个。

所以如果按你最初提的几个维度综合来看:

  • 代码质量:bub 略优
  • 架构清晰度:bub 明显优
  • 可扩展性:
    • 框架级扩展:bub 更优
    • 产品级扩展:nanobot 更优
  • 软件工程整体平衡:bub 略优
  • 产品成熟度与现成功能:nanobot 明显优

我的总体倾向仍然是:

如果目标是做一个能继续长很多年的工程底座,我会更看好 bub

附:本文引用所基于的版本与主要代码位置

本文所有判断与代码片段,均基于以下两个仓库的当前提交:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment