Appearance
18|工具接口:能力边界的声明
第三阶段让 agent 能安全地一步步做。但循环里调用的工具本身还没设计。
前面几篇的工具都是简单函数:find_owner(name)、read_log(service, lines)。能跑,但生产环境里工具不是函数,是带声明的接口——它得告诉系统自己能不能并发、是不是只读、有没有破坏性、要不要查权限。本篇讲工具接口设计:能力边界怎么用声明表达。
工具不是函数
简单函数定义只有入参和返回值。工具接口要多出几样东西:能力声明、权限检查、执行上下文、结果处理。

为什么不能只是函数?因为循环和权限系统需要知道这个工具的"性质"才能正确处理它。第 17 篇的并发分区要看 is_concurrency_safe,下一篇的权限体系要看 is_read_only 和 is_destructive。这些性质如果靠函数名猜或文档记录,迟早会漏。让工具自己声明,系统按声明处理。
四个能力声明
工具接口的核心是四个声明,默认值都偏保守:
python
from dataclasses import dataclass
from typing import Callable
@dataclass
class Tool:
name: str
description: str
parameters: dict
handler: Callable
is_concurrency_safe: bool = False # 默认不能并发
is_read_only: bool = False # 默认按可能写入处理
is_destructive: bool = False # 默认不假设破坏性
check_permissions: bool = True # 默认走权限检查is_concurrency_safe 默认 False:没声明就按不能并发处理(第 17 篇的 fail-closed)。
is_read_only 默认 False:没声明就按可能写入处理。这样只读工具忘记标 True,权限系统会要求确认,宁可多确认一次也不放过写操作。
is_destructive 默认 False:不随意假设破坏性,避免过度警告。真正的破坏性操作(删文件、重启服务)要显式标 True,触发更严格的审批。
check_permissions 默认走权限检查:工具层面的默认放行交给外层权限体系统一处理,但每个工具仍可在 check_permissions 里实现自定义硬规则。
错误声明比不声明更危险
这里有个设计要点:默认值偏保守是为了防"忘记声明",但"错误声明"比"不声明"更危险。
一个实际会写入的工具被标成 is_read_only=True,权限系统会把它当成安全工具放行——本来该确认的写操作直接执行了。所以工具开发者要根据实际行为正确声明,不能为了"少弹确认"故意把写操作标成只读。声明要基于工具的完整实现行为,包括副作用。
python
# 错误:read_log 里加了缓存写入,但仍标 is_read_only
def read_log(service, lines):
cache[service] = fetch_log(service) # 副作用:写缓存
return cache[service]
read_log_tool = Tool(name="read_log", ..., is_read_only=True) # 声明错了read_log 内部写了缓存,并发执行时缓存状态可能不一致。要么去掉副作用让 is_read_only 成立,要么老实标 False。只读工具应该是纯函数,缓存放外层调度器。
ToolUseContext:工具执行不是纯函数
工具执行需要上下文:取消信号、UI 回调、文件历史、agent 标识、token 预算控制。这些不能靠全局变量传,要打包成 ToolUseContext:
python
@dataclass
class ToolUseContext:
session_id: str
cancel_event: object # 取消信号
ui_callback: Callable # UI 渲染回调
file_history: dict # 文件历史(FileEditTool 要看)
token_budget: int # token 预算控制工具执行不是纯函数——它要读文件状态、响应取消、反馈进度。这些上下文通过 ToolUseContext 传入,不通过全局状态。全局状态在并发执行时会有竞态(第 17 篇讲过),ToolUseContext 是每个工具调用独立的,没有这个问题。
contextModifier:受控的上下文修改
有些工具执行后要影响后续工具的上下文。比如 FileEditTool 编辑完文件后,后续工具看到的文件历史要更新。但不能让工具直接改全局上下文,于是通过受控的 modifier:

python
def file_edit(file_path, old_string, new_string, context):
# 执行编辑
edit_file(file_path, old_string, new_string)
# 返回修改器,不直接改 context
modifier = {"file_history": {file_path: new_content}}
return {"ok": True}, modifier工具返回结果和 modifier,由外层调度器统一合并到上下文。并发工具之间的 modifier 不能互相覆盖,这是为什么上下文修改和并发安全绑定得紧——并发工具不能返回会冲突的 modifier。
工具接口之后
工具接口建立了能力边界的声明:并发安全、只读、破坏性、权限检查。这些声明不是文档,是系统处理工具的依据。
下一篇讲权限体系。工具声明了能力边界,但"能做什么"和"被允许做什么"是两回事——只读工具也可能读到不该读的内容,破坏性工具需要更严格的审批。权限体系用这些声明决定每次工具调用要不要放行。