文章提出将工具引用视为(name, version, schema_hash, capabilities, effect_class, credential_version)元组而非字符串,避免工具同名但行为变更导致的兼容性问题。
工具名不等于 API 契约。
如果一个 Agent 调用了 send_invoice,仅凭这个名称,你无法知道它将使用哪个输入 schema、哪些权限集合、什么样的副作用行为,或者由哪个 provider 版本执行。同一个工具可以保持名称不变,却修改了必填字段、扩大了资源作用域,或者把一次空跑(dry-run)变成了真正的变更操作。
本文展示一个轻量的工具契约注册中心,以及一套使契约漂移(contract drift)变得可见的测试计划。目标是让不兼容的变更在模型调用它之前就失败。
把工具引用当作一个元组(tuple),而不是字符串:
PURE、IDEMPOTENT、REPLAYABLE、UNKNOWN 或 NON_RETRYABLE模型可以看到友好的描述。但运行时必须解析并强制执行这个元组。
下面是一个刻意精简的 Python 设计。解析发生在执行之前,解析后的契约会随这次运行一起被记录下来。
from dataclasses import dataclass
from hashlib import sha256
import json
@dataclass(frozen=True)
class ToolContract:
name: str
version: str
schema_hash: str
capabilities: frozenset[str]
effect_class: str
credential_version: int
def canonical_hash(schema: dict) -> str:
raw = json.dumps(schema, sort_keys=True, separators=(",", ":"))
return sha256(raw.encode()).hexdigest()[:16]
def resolve(registry, requested, current_credential_version):
contract = registry.get((requested["name"], requested["version"]))
if contract is None:
raise RuntimeError("tool_contract_not_found")
if contract.schema_hash != requested["schema_hash"]:
raise RuntimeError("tool_schema_drift")
if contract.credential_version != current_credential_version:
raise RuntimeError("credential_policy_changed")
return contract
不要让模型自行选择 capabilities、effect_class 或 credential_version。这些是运行时持有的字段。
在副作用开始之前存储一条调度记录:
{
"run_id": "run_0187",
"effect_id": "effect_0187_03",
"tool": "billing.send_invoice",
"version": "2.1",
"schema_hash": "a91e7c3b44e0c012",
"capabilities": ["invoice:write:tenant-42"],
"effect_class": "IDEMPOTENT",
"credential_version": 19,
"status": "DISPATCHED"
}
如果 worker 在调度后崩溃,这条记录让协调器(reconciler)可以向 provider 确认副作用是否真的发生了。它不能悄无声息地用最新的契约重试。
扩展(Expand):在新契约旁边发布旧契约。只有在适配器保留了旧的效果语义时才添加它。
迁移(Migrate):更新调用方、prompts、缓存的计划以及队列中的工作,使其请求新版本。拒绝混用版本的新工作。
收缩(Contract):只有在队列、重试记录和协调任务中不再包含对旧版本的引用后,才删除旧版本。
对于小版本变更,只有在能证明输入输出完全向后兼容时,才保持同一主版本号。新增一个可选字段并不自动意味着安全——如果它改变了授权逻辑、费用或副作用行为,则另当别论。
最有价值的测试是刻意的不匹配:发送一个请求,工具名有效但 schema 哈希错误。如果它仍然执行了,说明注册中心只是装饰品。
这个边界分离了三种不同的失败:
它们需要不同的恢复路径。重启 worker 可以解决第一种。它解决不了第二种,而且不能对第三种进行猜测。
如果你持续运行 OpenClaw 或其他 Agent,宿主机层只有在注册中心、持久的调度记录和协调任务能在重启中存活下来时才有价值。托管运行时(例如 Ampere 上的托管 OpenClaw 托管方案)可以作为评估始终在线工作负载的一种部署选项,但它不会定义你的工具契约,也不会消除凭证和 prompt 注入风险。
UNKNOWN 是否通过 provider 证据而非盲目重试来协调?模型可以选择它想做什么。运行时必须决定使用完全相同的工具契约来做是否仍然安全。
当前 Agent 运行时中,哪个最小的契约字段你让它保持隐式了?