跳转到内容

工具调用协议与并发执行

工具调用协议与并发执行

学习目标

理解 Nanobot 如何基于 OpenAI 风格的工具调用协议实现工具执行,包括并发执行、批处理分组、错误分类与安全边界。

前置知识

下文假设你已理解上述概念,直接聚焦 Nanobot 的具体实现。

项目实践

统一 ToolCallRequest 抽象

Nanobot 定义了 ToolCallRequest 数据结构,统一表示来自 LLM 的工具调用请求:

@dataclass
class ToolCallRequest:
id: str
name: str
arguments: dict[str, Any]
extra_content: dict[str, Any] | None = None
provider_specific_fields: dict[str, Any] | None = None

关键设计:to_openai_tool_call() 方法将工具调用序列化为 OpenAI 兼容格式,确保不同提供商的工具调用响应都能被统一处理。

并发执行与批处理分组

Nanobot 的 AgentRunner._execute_tools() 支持两种执行模式:

分组逻辑

  • 每个工具标记 concurrency_safe 属性
  • 连续的 concurrency_safe=True 工具合并为一个批次,使用 asyncio.gather 并发执行
  • 遇到 concurrency_safe=False 的工具时,先执行之前的批次,再单独执行该工具

这解决了两个问题:

  1. 独立性:读取文件、搜索等独立操作可并发,减少延迟
  2. 依赖性:写文件等副作用操作串行执行,避免竞态条件

错误分类与回退策略

Nanobot 将工具错误分为三类:

错误类型检测方式处理方式
SSRF 违规匹配 internal/private url detected 等标记硬拦截,不可重试,告知 LLM 这是不可逾越的安全边界
工作区违规匹配 outside the configured workspace 等标记软拒绝,允许重试,但重复违规会升级提示
一般错误其他 BaseException返回错误信息给 LLM,附带重试提示

重复违规升级:对于重复的工作区违规,Nanobot 会升级提示强度,从简单的错误消息变为更强的指令:

escalation = repeated_workspace_violation_error(
tool_call.name, tool_call.arguments, workspace_violation_counts,
)

上下文治理管线

在每次工具执行前,AgentRunner.run() 会执行一串上下文治理操作:

  1. _drop_orphan_tool_results() — 丢弃没有对应 tool_call 的孤立工具结果
  2. _backfill_missing_tool_results() — 为未完成的工具调用插入合成错误结果
  3. _microcompact() — 将旧的可压缩工具结果替换为单行摘要
  4. _apply_tool_result_budget() — 根据字符数限制裁剪工具结果
  5. _snip_history() — 根据 Token 预算裁剪历史消息

这些操作确保了发送给 LLM 的消息序列始终合法且符合预算。

问题与规避

空响应重试

问题:LLM 可能返回空内容,导致 Turn 无法推进。

规避:Nanobot 最多重试 2 次(_MAX_EMPTY_RETRIES = 2),重试失败后使用 _request_finalization_retry() 发送专门的提示要求 LLM 回复。

输出截断恢复

问题:当 LLM 输出被 max_tokens 截断时,回复不完整。

规避:最多 3 次恢复周期(_MAX_LENGTH_RECOVERIES = 3),每次追加一个 continuation 提示词让 LLM 继续。

注射消息上限

问题:中间轮次用户可能发送多条消息,如果无限制注入会导致循环膨胀。

规避:每个 Turn 最多注入 3 条消息(_MAX_INJECTIONS_PER_TURN = 3),全 Turn 最多 5 个注射周期(_MAX_INJECTION_CYCLES = 5)。

设计取舍

为什么用 OpenAI 格式而非 Anthropic 格式

选择原因:OpenAI 的 tool_use/tool_result 格式已成为行业事实标准,大多数提供商(包括 Anthropic 的 OpenAI 兼容端点)都支持该格式。

代价:Anthropic 原生 provider 需要在内部做格式转换。

替代方案:直接使用 Anthropic 的 tool_use 格式,但会失去对其他 OpenAI 兼容提供商的支持。

参考来源