乐于分享
好东西不私藏

nanobot源码学习(四):从能跑到能部署

nanobot源码学习(四):从能跑到能部署

前 3 篇把 nanobot 的核心架构讲透了:消息总线、Tool + Provider、状态机。一路下来,理论上已经能拼出一个能跑通 LLM 对话的"迷你 nanobot"。

但有个问题——这个迷你版只能在自己电脑上 python xxx.py

没有 CLI 命令、没有配置文件、不能装到别人电脑上用。它是个 demo,不是产品

这一篇讲怎么把 demo 变成产品。s06 入口与配置 解决"怎么让用户装上就能用",s07 Provider 实战 解决"怎么让用户不写代码就能切 LLM 厂商"。


1. demo 和产品的差距在哪

前 3 篇拼出来的"迷你 nanobot"只能在自己电脑上 python xxx.py 跑。

几个具体的的缺口:

  • 没有统一入口——你写个 agent.py,客户问"装到哪、怎么启动"
  • 没有配置文件——API key 写死在代码里,换个 key 要重发版
  • 不支持多厂商切换——用户想从 Anthropic 换到 DeepSeek,得改业务代码

s06 章节处理前 2 个,s07 章节处理第 3 个。


2. nanobot核心设计:三个入口汇入同一个 Config 校验

nanobot 提供了三种"启动方式":

入口 怎么用 适用场景
CLI nanobot agent -m "你好" 命令行交互、CI/CD
SDK from nanobot import Nanobot Python 项目里嵌入
HTTP API POST /v1/chat/completions Web 前端、其他服务对接

这三种入口都要先过同一个 Config 校验,再启动消息泵

为什么这样设计?因为配置错的 Agent 跑起来会出各种诡异问题。在启动前把配置校验好,比跑起来再 debug 高效 100 倍

nanobot 用 Pydantic v2 写这个校验:


  
    
    
    
  
  python
from pydantic import BaseModel, Field

class ProviderConfig(BaseModel):
    name: str                       # 厂商名
    base_url: str | None = None     # 走代理时填
    api_key: str = ""               # 留空时从 env 读


class Config(BaseModel):
    model_config = {"populate_by_name"True}

    provider: ProviderConfig = Field(default_factory=ProviderConfig)
    api_key: str = Field(default="", alias="ANTHROPIC_API_KEY")
    # ... 几十个字段

两个关键点:

1. camelCase 别名让 JSON 和 Python 类属性无缝互转

JSON 配置习惯 apiKey,Python 类属性习惯 api_key。用 alias 让两种写法都能用:


  
    
    
    
  
  json
{ "apiKey": "sk-xxx" }   // JSON 用户习惯
{ "api_key": "sk-xxx" }  // Python 用户习惯

Pydantic v2 都接受。

2. 配置加载优先级:环境变量 > 配置文件 > 默认值

这是 Pydantic 的 BaseSettings 行为。默认用环境变量兜底意味着:

  • CI/CD:把 API key 放在 secret env var 里,不用管配置文件
  • 本地开发:写在 ~/.nanobot/config.json
  • 测试:用默认值(mock)

3. 一个常见疑问:为什么改了配置不生效

假设你改了 ~/.nanobot/config.json 里的 apiKey,会发现程序读到的还是旧的 key。

这不是 bug,是 Pydantic v2 的 BaseSettings 默认行为:loader.pyos.environ 优先于文件。

三种解法:

  1. 把 JSON 里的 apiKey 改成 api_key(snake_case 让 Pydantic 别用 alias 匹配)
  2. 改成用 NANOBOT_API_KEY 环境变量
  3. 重启 gateway(配置不热加载)

关键经验:schema 里那个字段的 alias 决定了 JSON 里能用哪个 key——想加新字段时必须同时改 schema 和 alias


4. OpenAI 兼容协议 = 一个类覆盖几十家

第 2 篇讲过 Provider 抽象的设计思想(s03)。现在s07 翻开 nanobot 真实代码,看这个抽象设计怎么落地

最让人眼前一亮的是 OpenAICompatProvider 这个类——一个类,适配几十家厂商

为什么?因为 2024 年 OpenAI 把 Chat Completions API 开放出来后,几乎所有"二线"厂商都按这个规范实现了一遍自己:

  • OpenRouter(聚合多家)
  • vLLM(自部署)
  • Ollama(本地)
  • DeepSeek
  • 智谱 BigModel
  • Moonshot
  • 月之暗面 Kimi
  • 阿里 DashScope(部分模型)
  • ……

大家 API 长一个样。 nanobot 写一个 OpenAICompatProvider 类就吃下了:


  
    
    
    
  
  python
class OpenAICompatProvider(LLMProvider):
    def __init__(self, spec: ProviderSpec):
        self.client = OpenAI(
            base_url=spec.base_url,
            api_key=os.getenv(spec.env_key),
        )

    async def chat(self, messages, tools):
        return self.client.chat.completions.create(
            model=spec.model,
            messages=messages,
            tools=tools,
        )

PROVIDERS 表里几十家厂商都是 backend 指向 openai_compat,只是 base_urlenv_key 不同:


  
    
    
    
  
  python
ProviderSpec(name="deepseek",   base_url="https://api.deepseek.com/v1",         env_key="DEEPSEEK_API_KEY")
ProviderSpec(name="zhipu",      base_url="https://open.bigmodel.cn/api/paas/v4", env_key="ZHIPU_API_KEY")
ProviderSpec(name="ollama",     base_url="http://localhost:11434/v1",           env_key="OLLAMA_API_KEY")
ProviderSpec(name="vllm",       base_url="http://localhost:8000/v1",            env_key="VLLM_API_KEY")

新增一家厂商只要加一行 ProviderSpec,核心 OpenAICompatProvider 一行不动。


5. 一个对比:传统接入 vs OpenAI 兼容协议

国产 LLM 厂商这几年大量采用 OpenAI 兼容协议——月之暗面 Kimi、智谱 BigModel、Moonshot 都是。

传统接入:新写一个 kimi_provider.py,继承 LLMProvider,自己处理 HTTP 请求、调 API、解析响应。200 行起步,调试一周不稀奇

OpenAI 兼容协议接入:加一行 ProviderSpec


  
    
    
    
  
  python
ProviderSpec(name="kimi", base_url="https://api.moonshot.cn/v1", env_key="KIMI_API_KEY")

业务代码一行不动,Kimi 接入完成。OpenAICompatProvider 已经被写好,新厂商只是配置里的一行

这就是 OpenAI 兼容协议的威力——它让"换 LLM 厂商"从"写适配代码"变成"填配置"。


6. 三种接入新厂商的方式

新加厂商时一般走方案 1:

方案 1:最简,不写新代码

把新厂商的 base URL 配在 config.jsonproviders[].baseUrl,backend 选 openai_compat90% 的场景走这条


  
    
    
    
  
  json
{
  "providers": [
    {
      "name": "kimi",
      "baseUrl": "https://api.moonshot.cn/v1",
      "envKey": "KIMI_API_KEY"
    }
  ]
}

方案 2:品牌化(贡献 PR)

registry.pyPROVIDERS 列表加一项,让 nanobot 官方支持你的厂商。给开源项目做贡献。

方案 3:完全新 backend

nanobot/providers/zhipu_provider.py 继承 LLMProvider,并在 factory.py 注册。只有厂商 API 不是 OpenAI 兼容时才需要


7. 一个常见的误区:配置写死在 Python 文件里

很多 Agent 项目的第一版长这样:


  
    
    
    
  
  python
# config.py
PROVIDER = "anthropic"
API_KEY = "sk-xxx"

代码能跑,但有几个问题:

  • 换 key 要重发版
  • 没法支持"用户在网页里改配置"——改 Python 文件得重启整个服务
  • 多环境(开发/测试/生产)得手动改文件

正确做法:用 Pydantic v2 把配置抽成 schema,配置文件 + 环境变量两条路都能用


  
    
    
    
  
  python
class Config(BaseModel):
    provider: str = "anthropic"
    api_key: str = Field(default="", alias="ANTHROPIC_API_KEY")

再配一个 file watcher 热加载,用户在网页改了配置 5 秒内生效

一句话建议:别把配置写死在 Python 文件里。从一开始就建 config/schema.py + config/loader.py,让配置文件和环境变量两条路都能用。前期投入小,后面扩展省事。


8. 下一篇

下一篇进 s08 + s09 章节,讲多渠道架构——17 个聊天平台(Telegram / Discord / 飞书 / 微信 / 邮件 / Slack……)是怎么用同一个 BaseChannel 抽象统一接入的,以及 session_key 这个设计怎么决定"哪些算一段对话"。


9. 参考资料

  • nanobot 源码:https://github.com/HKUDS/nanobot
  • nanobot-tutorial(14 章配套教程):https://github.com/yaoweizhang/nanobot_tutorial
  • nanobot 官方 Roadmap:https://github.com/HKUDS/nanobot/discussions/431

跟读建议:跑 python s06_entry_config/code.py 看 Pydantic 配置校验流程,再翻 nanobot/config/schema.py 看真实 schema 定义。跑 python s07_providers/code.pybase_url 切换效果。