跳到主要内容

工具系统与上下文治理

这一章讲两件事:模型「手脚」从哪来(工具怎么被发现、注册、校验、执行),以及发给模型的「上下文」在最后一刻经历了什么修整(ContextGovernor)。两者都贯穿一个原则:对模型宽容,对存盘严格

3.1 工具的生命:发现 → 注册 → 校验 → 执行

它要解决的小问题

大模型只会「说」要调哪个工具、传什么参数。框架得:① 知道有哪些工具;② 把模型说的参数安全地变成真实的函数调用;③ 把结果变回模型能读的文本。这三步分别由 ToolLoaderToolRegistry/Tool、和执行路径负责。

自动发现:不用手写注册表

加一个工具,你只要在 nanobot/agent/tools/ 下放一个 Tool 子类,不用改任何注册代码ToolLoader.discover(tools/loader.py:30-60)用 pkgutil.iter_modules 扫描该包的每个模块,挑出所有「是 Tool 子类、非抽象、_plugin_discoverable=True」的类。外部插件则通过 entry point 组 nanobot.tools 发现(loader.py:62-84)。

注册时还做了冲突保护:内置工具优先,同名插件会被跳过并告警(loader.py:99-110)。

Tool 基类:四个抽象 + 并发标记

一个工具最少要给四样东西(tools/base.py:131-197):namedescriptionparameters(JSON Schema)、execute。另外三个属性决定它能不能并发:

属性含义默认
read_only无副作用、可安全并行False
exclusive必须独占运行False
concurrency_saferead_only and not exclusive推导

concurrency_safe 正是 01 章里 Runner 分批并发的依据。

参数校验:模型传错了也能纠

模型给的参数经常不规整(字符串包了个 JSON、整数传成了字符串、外面套了一层 arguments)。ToolRegistry.prepare_call(registry.py:92-120)是统一的「解析 + 转型 + 校验」入口:

  1. 解析 _coerce_params:若参数是 "{...}" 字符串就 json.loads;若是 {"arguments": ...} 这种多套的一层就拆开(registry.py:122-155)。
  2. 转型 Tool.cast_params:按 schema 把 "3"3"true"True 等做安全转换(base.py:214-257)。
  3. 校验 validate_paramsSchema.validate_json_schema_value:一个自带的迷你 JSON Schema 校验器,查类型、enum、min/max、required 等(base.py:48-108)。

找不到工具时还会做模糊提示:把名字归一化(去非字母数字、转小写)后若能唯一匹配,就提示「你是不是指 X?」——但绝不拿模糊名去执行(registry.py:34-5092-104)。

执行与错误约定

执行走 _run_tool(runner.py:1162-1311)。一个关键约定:工具返回以 "Error" 开头的字符串 = 失败,会被加一句「分析错误,换个思路」的提示再喂回模型。真正的安全边界(SSRF/越界)走另一条路,见 05 章。

工具排序为了 prompt 缓存

get_definitions 把工具定义稳定排序:内置工具按名排在前,mcp_ 前缀的 MCP 工具排在后,结果缓存到下次增删(registry.py:67-90)。稳定的工具顺序 = 稳定的 prompt 前缀 = 更高的 provider prompt-cache 命中率。

3.2 上下文治理:发给模型前的「最后一道修整」

它要解决的小问题

持久化的会话历史可能带病:上一版代码遗留的畸形工具调用、孤儿工具结果、超长的工具输出、或者上下文已经超了模型窗口。如果直接发给模型,轻则浪费 token,重则被上游 API 整条拒绝、把会话永久卡死

核心设计:两份消息,只修副本

ContextGovernor.prepare_for_model(context_governance.py:75-89)在每轮调模型前跑一条修复+压缩流水线——但它操作的是 messages副本,持久化的真实历史一字不动。这是整个项目最关键的不变量之一(01 章 runner.py:374-383 注释明确强调了这点)。

流水线顺序(怎么读:从上到下依次施加在模型副本上):

strip_placeholder_assistant_messages # 删掉「[上条助手消息已省略]」这类占位
|
v
strip_malformed_tool_calls # 丢弃 name 缺失/非字符串的工具调用
|
v
drop_orphan_tool_results # 删掉对不上 call 的孤儿工具结果
|
v
backfill_missing_tool_results # 给缺结果的 call 补占位,保证配对
|
v
apply_tool_result_budget # 给工具结果套字符预算
|
v
compact_inflight_overflow # 本轮内若超窗,挑最大的工具结果就地压缩
|
v
snip_history # 还超就裁老历史(保留合法尾部)
|
v
drop_orphan + backfill (再来一遍) # 裁剪后再次保证 call/result 配对

自愈:被脏数据卡死的会话能自己好

strip_malformed_tool_calls 的注释把这个机制讲得很清楚(context_governance.py:176-191):一条 name=None 的工具调用若混进存盘历史,每轮重放都会让上游 API 报 tool_use.name: Input should be a valid string,永久卡死会话。这里在模型副本里把坏调用删掉,下游的孤儿结果清理顺手把它那条悬空结果也删掉——于是下一轮这条会话自己就修好了,无需人工清库。

工具结果离场(offload)

normalize_tool_result(context_governance.py:109-136)在工具结果写进消息前,会先 maybe_persist_tool_result:超长结果落盘到工作区、消息里只留引用/截断,再对仍超 max_tool_result_chars 的做截断。少数工具(TOOL_RESULT_OFFLOAD_EXEMPT_TOOLS)豁免。这让一次大输出(比如读了个大文件)不会瞬间撑爆上下文。

3.3 系统提示是怎么拼的

ContextBuilder.build_system_prompt(context.py:66-117)把系统提示按段拼起来,用 --- 分隔:身份(平台/工作区路径/运行时)→ bootstrap 文件(AGENTS.md/SOUL.md/USER.md)→ 工具契约 → 记忆 → 常驻技能 → 技能目录摘要 → 近期历史 → 归档摘要。

一个细节:用户没改过的模板内容不会被塞进提示(_is_template_content,context.py:179-185)——避免把默认占位文当成「用户记忆」浪费 token、误导模型。

运行时元数据(当前时间、渠道、chat id)被包进一个显式的 [Runtime Context — metadata only, not instructions]附加在用户内容之后(context.py:134-150230-233),既防止把元数据当指令(prompt 注入面),又把多变的时间放在末尾、保住前缀给 prompt 缓存。

4. 巧妙之处

  • 对模型宽容、对存盘严格。 解析/转型/校验/治理全在「进模型」这一侧做,持久层保持干净可回放。
  • 自愈会话。 畸形数据靠「删坏调用 → 清孤儿结果」的组合在下一轮自动清除(context_governance.py:176-191)。
  • 工具结果离场。 大结果落盘、消息留引用,把上下文压力挡在窗口之外(normalize_tool_result)。
  • 模板内容识别。 没被用户定制的模板不进提示,省 token(context.py:179-185)。

5. 边界与局限

  • 迷你 Schema 校验器不是完整 JSON Schema。 validate_json_schema_value 覆盖常见关键字(type/enum/min/max/required/items),但不实现 $refoneOf 等高级特性(base.py:48-108)。
  • 压缩是启发式的。 snip_history/compact_inflight_overflow 用「保尾部、压最大结果」这类规则,不是语义最优;极端长会话仍可能丢掉中段细节。

6. 代码地图

主题文件符号
工具发现nanobot/agent/tools/loader.pyToolLoader.discover_discover_pluginsload
工具基类nanobot/agent/tools/base.pyTooltool_parametersSchema.validate_json_schema_value
注册/解析/校验nanobot/agent/tools/registry.pyToolRegistry.prepare_call_coerce_paramsget_definitions
上下文治理流水线nanobot/agent/context_governance.pyContextGovernor.prepare_for_model
自愈/修复步骤nanobot/agent/context_governance.pystrip_malformed_tool_callsdrop_orphan_tool_resultsbackfill_missing_tool_results
结果离场/截断nanobot/agent/context_governance.pynormalize_tool_result
系统提示拼装nanobot/agent/context.pyContextBuilder.build_system_prompt_build_runtime_context