工具调用协议与并发执行
工具调用协议与并发执行
学习目标
理解 Nanobot 如何基于 OpenAI 风格的工具调用协议实现工具执行,包括并发执行、批处理分组、错误分类与安全边界。
前置知识
- 工具调用协议与执行模型 — Function Calling 协议基础
- 安全沙箱设计 — 沙箱多层防御架构
下文假设你已理解上述概念,直接聚焦 Nanobot 的具体实现。
项目实践
统一 ToolCallRequest 抽象
Nanobot 定义了 ToolCallRequest 数据结构,统一表示来自 LLM 的工具调用请求:
@dataclassclass 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的工具时,先执行之前的批次,再单独执行该工具
这解决了两个问题:
- 独立性:读取文件、搜索等独立操作可并发,减少延迟
- 依赖性:写文件等副作用操作串行执行,避免竞态条件
错误分类与回退策略
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() 会执行一串上下文治理操作:
_drop_orphan_tool_results()— 丢弃没有对应tool_call的孤立工具结果_backfill_missing_tool_results()— 为未完成的工具调用插入合成错误结果_microcompact()— 将旧的可压缩工具结果替换为单行摘要_apply_tool_result_budget()— 根据字符数限制裁剪工具结果_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 兼容提供商的支持。