1. SecurityClaw 学习笔记/

11 工具调用与运行时:模型提议,代码执行

结论先说:Tool Calling 的本质不是“模型能调用函数”,而是“运行时收回执行权” #

一个可靠的 Agent,不会让模型直接调用数据库、文件系统或外部 API。模型只能提出:

  • 要调哪个工具
  • 参数是什么
  • 预期得到什么结果

真正决定“能不能执行”的,是运行时。


一、最小模型:Tool Calling 应该拆成哪几步 #

def dispatch(tool_name: str, args: dict, runtime: Runtime) -> ToolObservation:
    if tool_name not in runtime.available_tools:
        return ToolObservation(error="Tool not allowed")
    validation_error = runtime.validator.validate(tool_name, args)
    if validation_error:
        return ToolObservation(error=f"Args invalid: {validation_error}")
    if not runtime.authorizer.check(tool_name, runtime.identity):
        return ToolObservation(error="Permission denied")
    if runtime.budget.remaining < runtime.costs[tool_name]:
        return ToolObservation(error="Budget exhausted")
    return runtime.tools[tool_name](args)

这段伪代码就是 Tool Calling 最该记住的骨架:

  1. 工具名验证
  2. 参数验证
  3. 权限验证
  4. 预算验证
  5. 执行
  6. 返回结构化 observation

二、为什么“模型提出动作,代码执行”是唯一靠谱路线 #

如果你直接把数据库连接、文件句柄或 shell 暴露给模型,问题马上就会出现:

  • 模型可能调错工具名
  • 模型可能拼出非法参数
  • 模型可能绕过权限边界
  • 模型可能把同一个昂贵工具反复调用

所以真正的 Tool Calling,不是“让模型更强”,而是:

让模型负责语义选择,让代码负责系统边界。

这也是为什么我更喜欢把 Tool Calling 理解成“动作提议系统”,而不是“函数调用能力”。


三、运行时真正要保护的四个边界 #

3.1 工具名白名单 #

if tool_name not in runtime.available_tools:
    return ToolObservation(error="Tool not allowed")

这一步防的是模型幻觉:

  • 编了一个不存在的工具
  • 尝试调用当前会话没暴露的工具

3.2 参数契约 #

validation_error = runtime.validator.validate(tool_name, args)

这一步防的是:

  • 参数类型不对
  • 必填字段缺失
  • 枚举值非法
  • 动态字段越界

3.3 权限边界 #

if not runtime.authorizer.check(tool_name, runtime.identity):
    return ToolObservation(error="Permission denied")

这一步的核心思想是:

权限不是 prompt 规则,而是运行时硬约束。

3.4 预算边界 #

if runtime.budget.remaining < runtime.costs[tool_name]:
    return ToolObservation(error="Budget exhausted")

预算控制不是附加功能,而是防止 Agent 在失败路径里把自己烧穿。


四、为什么返回值必须结构化,而不是自然语言 #

很多早期 Agent 系统喜欢返回:

工具执行失败了,因为参数错误。

这对人类看着友好,但对下一轮模型规划很差。真正可用的返回应该是:

ToolObservation(
    tool_name="query_asset_db",
    status="error",
    error_code="invalid_args",
    error_message="field 'ip' missing",
    result=None,
)

这样下一轮模型看到的不是一句模糊抱怨,而是:

  • 哪个工具失败了
  • 为什么失败
  • 失败属于哪一类
  • 有没有结果

这就是为什么 Artifact / Observation 在 Agent 系统里很重要。结构化结果是多步协作的基础。


五、缓存与幂等:为什么同一个工具不该一遍遍重调 #

运行时除了验证,还应该考虑:

  • 同一工具 + 相同参数是否可复用结果
  • 有副作用操作能否安全重试
cache_key = hash((tool_name, normalize(args)))
if cache_key in runtime.cache:
    return runtime.cache[cache_key]

这个设计的好处:

  • 降低重复调用成本
  • 防止模型在循环里反复打一模一样的外部请求

坏处也有:

  • 需要决定缓存粒度
  • 对时间敏感数据不一定安全

所以缓存不是默认打开的“优化”,而是运行时策略的一部分。


六、失败路径:真正该测的是坏情况 #

Tool Calling 最有价值的测试,不是“工具成功执行”,而是:

  1. 非法工具名是否被拒绝
  2. 参数错误是否被结构化返回
  3. 权限不足是否阻断执行
  4. 预算不足是否提前停止
  5. 工具异常是否不会炸穿整个循环
def test_invalid_tool_rejected(): ...
def test_invalid_args_return_structured_error(): ...
def test_permission_denied_blocks_execution(): ...
def test_budget_exhausted_stops_call(): ...

这些测试比一段“最终输出很漂亮”的 demo 更能说明系统是否可靠。


七、这一篇真正要记住的框架 #

Tool Calling 的知识框架可以压成一句话:

模型负责提出动作,运行时负责验证动作,工具负责执行动作,结构化 observation 负责把结果带回下一轮。

对应链路就是:

LLM plan
  -> runtime validate
  -> tool execute
  -> observation record
  -> next-step planning

如果没有中间这层 runtime,Tool Calling 本质上就只是“把系统权限交给模型试运气”。


八、验证命令 #

pytest tests/ -k "runtime or tool or permission or budget" -v