数据截至 (上游 commit d7b3abe58992)
工具系统与上下文治理
这一章讲两件事:模型「手脚」从哪来(工具怎么被发现、注册、校验、执行),以及发给模型的「上下文」在最后一刻经历了什么修整(
ContextGovernor)。两者都贯穿一个原则:对模型宽容,对存盘严格。
3.1 工具的生命:发现 → 注册 → 校验 → 执行
它要解决的小问题
大模型只会「说」要调哪个工具、传什么参数。框架得:① 知道有哪些工具;② 把模型说的参数安全地变成真实的函数调用;③ 把结果变回模型能读的文本。这三步分别由 ToolLoader、ToolRegistry/Tool、和执行路径负责。
自动发现:不用手写注册表
加一个工具,你只要在 nanobot/agent/tools/ 下放一个 Tool 子类,不用改任何注册代码。ToolLoader.discover(tools/loader.py:36-66)用 pkgutil.iter_modules 扫描该包的每个模块,挑出所有「是 Tool 子类、非抽象、_plugin_discoverable=True」的类。外部插件则通过 entry point 组 nanobot.tools 发现(loader.py:62-84)。
注册时还做了冲突保护:内置工具优先,同名插件会被跳过并告警(loader.py:99-110)。
Tool 基类:四个抽象 + 并发标记
一个工具最少要给四样东西(tools/base.py:159-229):name、description、parameters(JSON Schema)、execute。另外三个属性决定它能不能并发:
| 属性 | 含义 | 默认 |
|---|---|---|
read_only | 无副作用、可安全并行 | False |
exclusive | 必须独占运行 | False |
concurrency_safe | read_only and not exclusive | 推导 |
concurrency_safe 正是 01 章里 Runner 分批并发的依据。
参数校验:模型传错了也能纠
模型给的参数经常不规整(字符串包了个 JSON、整数传成了字符串、外面套了一层 arguments)。ToolRegistry.prepare_call(registry.py:92-120)是统一的「解析 + 转型 + 校验」入口:
- 解析
_coerce_params:若参数是"{...}"字符串就json.loads;若是{"arguments": ...}这种多套的一层就拆开(registry.py:122-155)。 - 转型
Tool.cast_params:按 schema 把"3"→3、"true"→True等做安全转换(base.py:214-257)。 - 校验
validate_params→Schema.validate_json_schema_value:一个自带的迷你 JSON Schema 校验器,查类型、enum、min/max、required 等(base.py:48-108)。
找不到工具时还会做模糊提示:把名字归一化(去非字母数字、转小写)后若能唯一匹配,就提示「你是不是指 X?」——但绝不拿模糊名去执行(registry.py:34-50、92-104)。
执行与错误约定
执行走 _run_tool(runner.py:1450-1563)。一个关键约定:工具返回以 "Error" 开头的字符串 = 失败,会被加一句「分析错误,换个思路」的提示再喂回模型。真正的安全边界(SSRF/越界)走另一条路,见 05 章。