要让 AI 同事调用内部系统,先别急着讨论它能做多少事,先把三处位置找出来:工具注册写在哪、参数在哪校验、权限白名单在哪配。OpenDots 这类工具调用框架里,调不通的原因通常落在这三处之一——模型没选中工具、参数被校验挡回、工具根本没进白名单。把这三处各自验证一次,比反复改提示词有效。
最小可验证路径是:注册一个只读、无副作用的工具,给参数加类型和范围校验,把工具名写进白名单,然后跑一次成功调用、一次越权调用。成功调用看返回结构里有没有结果字段和 trace_id;越权调用看日志里有没有拒绝记录、业务系统有没有真的收到请求。白名单建议默认拒绝,校验失败建议返回结构化错误而不是裸异常。注册接口名和配置路径需结合你部署的版本确认。
找到工具注册入口并加一个最小工具
注册入口一般在三种位置之一:工具目录下的清单文件(YAML 或 JSON)、启动时调用的注册函数、插件包的入口模块导出。先在项目里搜工具名或 register_tools 这类关键字,确认是哪一种,不要一边翻文档一边改业务代码。
注册时至少要提供三类信息,它们各自决定一件不同的事:
- 名称(name):调用标识,白名单、日志、错误返回都靠它对齐,建议用「动词_名词」并保持全项目唯一;
- 描述(description):模型判断什么时候该选这个工具的依据,写清用途、是否只读、数据范围,不要写营销语;
- 参数(parameters):模型能传什么,是校验和白名单之外的第二道约束。
通用骨架如下,字段名按你环境里的注册接口替换:
def register_tools(registry):
registry.register(
name='query_order_status',
description='按订单号查询订单状态,只读,不修改任何数据',
parameters=ORDER_STATUS_PARAMS,
handler=query_order_status,
scopes=['order:read'],
)
注册完成后的第一个验证动作不是问模型,而是看工具清单:服务启动日志里能否搜到 query_order_status,调试页面或工具列表输出里是否出现同一个名字。这一步过不去,后面的调用都无从谈起。
给工具参数写校验,避免模型传进非法值
模型生成的参数属于外部输入,和表单提交没有本质区别,不能直接拼进 SQL 或原样转发给内部接口。校验建议放在 handler 之前做一次显式检查,而不是指望模型自觉。
ORDER_STATUS_PARAMS = {
'order_id': {'type': 'string', 'required': True, 'pattern': r'^[A-Za-z0-9]{6,32}$'},
'days': {'type': 'integer', 'required': False, 'default': 7, 'min': 1, 'max': 30},
}
def validate(params):
errors = []
# 逐字段检查类型、必填、正则与数值范围,未声明的字段视为非法
return errors
校验失败时的返回要和成功返回区分开,让模型和调用方都能读懂:
{"ok": false, "error_code": "INVALID_ARGUMENT", "field": "days", "message": "days 必须在 1 到 30 之间"}
message 可以回传给模型,让它自行修正后重试;内部异常堆栈、SQL 片段、表名和字段名不要出现在返回里。另外建议把未声明的多余参数一律按非法处理,避免模型顺手多传一个能改变行为的字段。
用白名单限制可调用的工具范围
白名单的落点通常在工具分发层:模型给出工具名之后、真正执行 handler 之前。如果框架只提供一处配置,优先配在这里。写法上建议默认拒绝:
allow = set(config.get('tools.allow', [])) # 配置缺失时得到空集合
if tool_name not in allow:
raise ToolNotAllowed(tool_name)
默认拒绝的含义是「没写进去就不能用」,而不是「写了才不能用」,两者在配置读取失败时差别很明显。落地时注意几点:白名单按最小必要给,只读查询和写操作分开配;每个工具对应的 scopes 与工具名一起维护,不要只靠工具名做判断;配置变更要能回答谁改的、什么时候生效。如果 agent 侧和工具侧各有一个开关,两边保持一致更省排查成本。
跑一次成功调用并查看返回结构
先用一个确定存在的入参跑通只读工具,HTTP 骨架大致是:
POST /agent/invoke
{"tool": "query_order_status", "arguments": {"order_id": "A1234567", "days": 3}}
期望返回:
{"ok": true, "tool": "query_order_status", "data": {...}, "trace_id": "..."}
重点看三个字段:ok 表示这次执行本身是否成功,和模型有没有选中这个工具是两回事;data 是业务结果,结构由工具自己定义并尽量保持稳定;trace_id 用来把调用和服务端日志串起来。拿 trace_id 去日志里找同一条记录,确认入参、校验结果、是否命中白名单、handler 耗时能对上。对不上,说明日志埋点位置需要调整。
构造一次越权调用看是否被拦下
验证方式是主动调用一个没进白名单的工具名,或者调用一个缺少对应 scope 的写操作工具。观察两点:请求有没有进入 handler,业务系统一侧有没有留下访问痕迹。
被拒绝时的典型返回:
{"ok": false, "error_code": "TOOL_NOT_ALLOWED", "message": "tool not in allowlist"}
日志侧一般能看到工具名、拒绝原因(not in allowlist 或 missing scope)和调用来源,但看不到 handler 的执行记录。如果拒绝日志和业务系统的访问日志同时出现,说明拦截点放在了调用 handler 之后,需要把判断往前移到工具分发层。还要区分一种情况:模型压根没选到这个工具,trace 里同样没有 handler 记录,这时要结合错误码判断是拒绝还是未命中。