Nahida Bot 开发回顾:给 LLM Agent 造一个「虚空终端」

一个以纳西妲为主题、Agent 为灵魂的 Python Bot 框架:从 2026 年 4 月正式开工到现在的开发史,以及中途几个值得记住的设计决策。

2026.08.19 · 8 min · Python / LLM Agent / 开发日志 / Nahida Bot

Nahida Bot 是一个以 Agent 为核心、以工作空间(Workspace)为中心、可通过插件扩展的 Python Bot 框架。表面上是聊天机器人,实际上是给 LLM 搭的一套有家可归的框架:文件就是上下文,插件就是衣橱,模型随时可换,通道随意接入。

这篇回顾按开发时间线梳理它从骨架到现在的样子,也记录几个中途真正改变方向的设计决策。

项目是什么

一句话:Agent 不是被 LLM 驱动的聊天框,LLM 才是被 Agent Loop 驱动的头脑。

三条设计理念贯穿始终:

  • Agent-first(意识主导):Agent Loop 是中枢,LLM 在这里不是外挂的工具人,而是主导大脑。
  • Workspace-native(专属花盆):文件即上下文,工作空间作为一等公民对待,指令文件(AGENTS.md / SOUL.md / USER.md)注入上下文。
  • Plugin-driven(百变衣橱):新能力不需要改核心代码,装个插件就行。

技术栈是 Python 3.12 + asyncio,FastAPI 做 Gateway,SQLite 持久化,uv 管依赖,structlog 管日志,pyright + pytest 守质量闸门。桌宠端另有一条 Tauri 2 + Rust + Vue 3 + Live2D(PixiJS)的路线,WebUI 是 Vue 3 + Vite + shadcn-vue。

时间线

2025-03:一个文档驱动的设想

仓库最早一次提交在 2025 年 3 月,只有 README、LICENSE 和目录结构,之后基于早期的 LLM 调用做了一个早期版本,那个时候的版本还更加接近一个传统的机器人,只是加上了一些 LLM 的交互。2026 年 4 月受到

2026-04:从地基开始

正式开工后,前两周全部花在 Phase 0 和 Phase 1——先立质量闸门,再写业务。当时定下的一条原则是:不要先写业务逻辑再补工具链uv syncruffpyrightpytest 全部跑通,应用容器、分层配置、事件总线、结构化日志逐一落地,才进入 Agent 部分。

回头看这是整个项目最重要的决策。后期 392 次提交里几乎没有「基础工具链返工」,质量闸门挡掉了大量后面才可能爆的雷。

2026-04 ~ 05:Agent 与 Workspace 联合阶段

Phase 2 是把「智能闭环」打通的过程:

  • Workspace 文件沙盒与路径穿越防护;
  • AGENTS.md / SOUL.md / USER.md 的注入优先级;
  • Agent Loop 的组装 → 调用 → 工具回填 → 终止条件;
  • Tool Calling 协议与生命周期;
  • 记忆模型:SQLite 持久化 + FTS 关键词检索(jieba 分词)+ 向量索引双路召回;记忆按 global / chat / person / account / collection / workspace 六级作用域隔离,读取走身份感知的级联;
  • Provider 抽象:OpenAI 兼容族、DeepSeek-R1/V4 的 reasoning_content、Anthropic Claude 的 thinking 块、GLM / Groq / MiniMax 多后端归一化;
  • 多模态:vision 原生传图、非 vision 自动 fallback 描述、image_understand 工具三模式自适应。

这一阶段最有价值的产出是 Provider 适配层的统一接口——DeepSeek 和 Claude 各自的推理链格式差异很大,ReasoningPolicy(strip/append/budget)把「推理内容如何回传上下文」收敛成了可配置策略,后面接新后端基本是填表格。

2026-05:插件系统与 Channel

Phase 3 做出整个项目最关键的一个架构决策:Channel 不是独立层,而是插件。Telegram、QQ、OneBot 都以普通 Plugin 的形式通过 register_channel() 注册为 ChannelService,复用插件的权限模型、生命周期管理和能力注册机制。平台差异被收敛在 adapter 内部,Agent 核心完全无感。

同期还做了 Subagent 编排(主 Agent 可 spawn 子 Agent、等待、停止)和 Provider 插件化。Provider 插件化留下了一个很有意思的难题,见下文「鸡生蛋的加载时序」。

2026-06 ~ 07:通道扩展与运维形态

  • Telegram Channel 完整闭环(长轮询、消息标准化、媒体降级);
  • Milky QQ Channel(Lagrange.Milky WebSocket 事件流、segment 建模、合并转发递归解析);
  • OneBot v11 Channel(正向 WebSocket);
  • WebUI 运维面板与登录体系;
  • Gateway REST API 与 SSE 实时事件。

2026-07 ~ 08:桌宠与分布式

  • Desktop 桌宠:Tauri + Rust + Vue 3 + Live2D,边缘隐藏窗口、鼠标接近唤出、本地 TTS 发声管线,后期还加了服务端 motion planner(便宜 LLM 生成 DisplayPlan 驱动 Live2D 动作);
  • Process Supervisor:把 SSH 隧道、frpc、cloudflared 等 sidecar 统一交给核心监管;
  • Gateway-Node 分布式协议:Wire protocol、Python Protocol SDK、节点配对与心跳、capability.invoke 远程执行,14 个跨语言 JSON fixture 作为 Python/Rust 契约基线;
  • nahida-bot-sdk 独立成包,插件作者可以在独立仓库开发。

几个值得记住的设计决策

先立质量闸门

Phase 0 的验收标准全部是工具链:uv sync、lint、类型检查、测试可跑通。当时的理由是「避免先写业务逻辑再补工具链」。一个 Agent 框架的复杂度主要来自异步生命周期和插件隔离,没有类型检查和分层测试,后期根本不敢重构。

Channel 是插件,不是层

一开始的设计是给 Channel 定义独立协议标签,后来放弃了。真正的问题是:宿主不需要知道插件内部用了什么传输协议,只需要两件事——它有没有显式注册 ChannelService,以及它是否需要宿主提供额外扩展点。Channel / Provider / Tool / Command 不是互斥类别,一个普通 Plugin 可以同时注册多种能力。

鸡生蛋的加载时序

Provider 插件化遇到一个典型难题:ProviderManager 必须在插件注册 Provider 类型之后创建,但常规插件的加载又在 ProviderManager 创建之后。当时评估了四种解法(阶段化加载、依赖声明、惰性初始化、两遍扫描),最终选了成本最低的阶段化加载:给 manifest 加一个 load_phase 字段,Provider 插件声明 pre-agent,其余默认 post-agent_load_plugins() 拆成两步。这类「先有鸡还是先有蛋」的问题在插件系统里几乎必然出现,阶段化加载是性价比最高的默认答案。

回复信号协议

参考 OpenClaw 的 sentinel token 设计,给 Agent 回复管线加了 NO_REPLYHEARTBEAT_OK 两个哨兵令牌。模型通过纯文本输出表达「这条不用回」或「心跳空转」。检测层做了精确匹配、JSON 包络、尾部剥离多层防护,避免误杀正常大写文本。这让定时任务的空转不再刷屏,也是群聊噪音控制的基础。

安全闸门

ROADMAP 里有一张已知漏洞表:符号链接攻击、TOCTOU 竞态、硬链接、Unicode 绕过、特殊文件对象、无文件大小限制——全部标记为「待修复(Phase 2.7)」,并且明确写了一句:在开放不可信第三方插件、远程执行或高权限文件工具前必须完成加固。可信本地 MVP 可以先跑,但安全边界是闸门不是可选项。这个「先跑通、再设闸」的节奏感,比一开始就追求完美更可持续。

记忆:检索层与巩固层分开

初版记忆只是「SQLite 存 + FTS 搜」,后来调研 OpenClaw、NanoBot、Hermes 这些框架时意识到:记忆系统真正难的从来不是把东西存下来,而是决定什么值得长期记住。于是把记忆拆成两层——检索层负责把已固化的记忆找回来(FTS 关键词 + 向量双路召回),巩固层负责在对话结束后决定要不要记住。

巩固层又走两条互补的路径:规则提取器抓显式的「请记住…」信号,LLM Dreaming 则把整段对话里值得长期保留的片段结构化成新增/归档指令。两条路径的产出统一进入 MemoryConsolidator 提升为持久记忆,再投影回工作区的 MEMORY.md——这样 LLM 的上下文始终能看到最近固化下来的内容,而不是每次都要先检索。敏感度分类和可移植元数据让记忆既不会把敏感信息写进长期存储,也能在换设备、换后端时带走。这个「对话中只做轻量提取、事后异步巩固」的节奏,比在对话里当场做重度记忆处理要干净得多。

跨语言契约 fixture

Gateway-Node 协议一旦发布就是稳定契约,只能向后兼容。为此在 tests/fixtures/gateway_node/ 放了一批跨语言报文样例,作为 Python 和 Rust 两侧的对齐基线。协议代码可以先不完美,但契约必须先固定。

现状与下一步

目前已点亮的能力包括:三 Channel(Telegram / Milky QQ / OneBot v11)、Multi-Provider、12 个内置命令与 16+ 内置工具、Subagent 编排、多模态、Cron 调度与记忆 Dreaming、知识库与生图插件、WebUI 运维面板、Live2D 桌宠、SSE 实时事件、Process Supervisor、独立 SDK。

接下来重点在四件事:OneBot Channel 收尾(反向 WebSocket、多账号);Workspace Sandbox 安全加固(开放不可信插件前的闸门);记忆与文档检索的合并重构;以及 Gateway-Node 的桌面端 Rust node 接入与 capability 真实执行桥接。

结语

Nahida Bot 这一年多的历程,主线其实只有一条:先把 Agent 的核心闭环做扎实,再用插件系统把一切边缘能力变成可插拔,最后让形态自由生长——从 Telegram 机器人到 WebUI 控制台,再到屏幕角落里的 Live2D 桌宠。框架本身是工具,真正留下来的,是那些在「先有鸡还是先有蛋」和「安全边界该什么时候设」之间做出的选择。