Gemini 工具白名单放在哪一层
Gemini 的工具白名单分三层:不声明、allowed_tools 限定、执行环兜底;tool_choice 有 auto/any/none/validated 四种模式,写在 generation_config 里。(核验 2026-11-05,F84)
一、Gemini 工具白名单放在哪一层:问题不是写不写清单,而是清单管不管得住
「白名单」这个词在提示词工程里被用得很随意——很多人理解的实现,是在系统指令里加一句「你只能使用查询类工具」。这句话有用,但它不是白名单,是请求。模型可以不遵守请求,代码不会因此报错。
真正的白名单要能在模型不配合时也生效。官方在函数调用页给了两个可落地的位置:一是 tool_choice 里的 allowed_tools,用 mode 加 tools 限定这一次请求能用哪些函数(F84);二是工具声明本身——没写进 tools 的函数,模型连知道它的机会都没有。
把这两处再加上执行环的注册表,就形成三层:声明层、协议层、执行层。三层挡的东西不一样,少一层就有绕过路径。
二、Gemini 工具白名单三层校验:最小可运行示例
下面这段把三层依次跑一遍,看每条提议死在哪一层。
## EXPECT: 声明层拦 1 个:delete_all | 协议层拦 1 个:refund | 执行层拦 0 个:无 | 最终可执行 1 个:get_ticket
DECLARED = {"get_ticket", "refund"} # 声明层:写进 tools 的函数
ALLOWED = {"get_ticket"} # 协议层:allowed_tools.tools
REGISTRY = {"get_ticket": lambda **kw: {"ok": True}} # 执行层:真正实现了的函数
PROPOSALS = ["get_ticket", "refund", "delete_all"]
def review(proposals, declared, allowed, registry):
passed_decl = [n for n in proposals if n in declared]
blocked_decl = [n for n in proposals if n not in declared]
passed_proto = [n for n in passed_decl if n in allowed]
blocked_proto = [n for n in passed_decl if n not in allowed]
passed_exec = [n for n in passed_proto if n in registry]
blocked_exec = [n for n in passed_proto if n not in registry]
return passed_exec, (blocked_decl, blocked_proto, blocked_exec)
passed, (bd, bp, be) = review(PROPOSALS, DECLARED, ALLOWED, REGISTRY)
assert passed == ["get_ticket"] and bd == ["delete_all"] and bp == ["refund"]
exec_text = "、".join(be) if be else "无"
print(f"声明层拦 {len(bd)} 个:{'、'.join(bd)} | 协议层拦 {len(bp)} 个:{'、'.join(bp)} | "
f"执行层拦 {len(be)} 个:{exec_text} | 最终可执行 {len(passed)} 个:{'、'.join(passed)}")
三层拦下的不是同一类风险:delete_all 根本没被声明,模型是「凭空发明」了一个名字;refund 声明了、但这次请求不允许;get_ticket 一路通过。真实系统里三层都该有,因为每一层能防的绕过方式不一样。
三、解释与边界:三条机制,加一条数量预算
声明层防「知道」。 没写进 tools 的函数,模型不可能正确调用——它能编出一个名字,但参数格式、字段含义全靠猜,成功率极低。这一层最省事:不想让它做的事,别写进声明。
协议层防「选错」。 tool_choice 写在 generation_config 里,allowed_tools.tools 可以列出这次允许的函数名(F84)。四种模式各管一件事:
| 模式 | 行为 | 适合场景 |
|---|---|---|
auto |
模型自己决定调不调 | 默认,绝大多数对话 |
any |
必须调函数 | 「一定要给出结构化调用」的抽取任务 |
none |
禁止调函数 | 纯问答,或工具故障时的降级 |
validated |
模型的调用必须符合声明 schema,形状不对由服务端拦住 | 参数形状必须严格时 |
模式回答的是「要不要调」,allowed_tools 回答的是「能调哪些」,两个维度不要混。有人在 any 模式下调了半天提示词想让它别调工具——模式已经强制了,改提示词没用。
执行层防「说得到做不到」。 声明了、也允许了,但代码里没实现(或者被 feature flag 关了),这时候注册表返回 None,请求照样能被拦住。这一层是最后一道,也是最容易被忽略的一道——因为它不在 API 文档里,在你自己代码里。
三层之外还有一条数量预算。 官方建议同时激活的工具控制在 10 到 20 个以内(F87)。工具表越长,模型选错的概率越高,而且所有声明会一起参与编译(见 6.4 的复杂度上限)。白名单不只是「允许哪些」,也包括「最多允许几个」。
错误码对应哪一层? unexpected_tool_call 表示模型调用了没声明的工具(F27)——出现它,说明声明层或协议层没有把它收住,先去核对这次请求到底传了哪些 tools,而不是去改提示词。
白名单是长期资产,要按变更管理对待。 三层里最容易腐化的不是代码,是清单:某个功能下线了,声明没删;某个接口改名了,白名单还留着旧名;测试环境放开的工具,上线时忘了收。腐化的直接后果是「模型能调一个已经不存在的东西」,报错信息还指向模型。可操作的做法有三条:清单与代码同仓、改动走评审;按环境区分,测试与生产的 allowed_tools 不共用一份;每次发布前跑一次「声明 — 白名单 — 注册表」三者取交集的脚本,把差集打出来。这个脚本很短,但它能把「以为收紧了、其实没生效」这类问题提前一段暴露。
边界。 本节只管「能不能调」。调了之后参数对不对,见 6.4;多个工具怎么排序,见 6.2;执行环本身怎么写,见 6.1。白名单不是权限系统的全部——真正的权限还在你的鉴权层,别指望工具清单替它干活。
四、预期输出与异常分支
真跑输出:
声明层拦 1 个:delete_all | 协议层拦 1 个:refund | 执行层拦 0 个:无 | 最终可执行 1 个:get_ticket
六类分支。模型编了没声明的工具名:这一层拦得住,但要在日志里记下来,频繁出现说明声明描述漏了能力。声明了但这次不该用:靠 allowed_tools 收窄,别靠删声明——删了会让其他正常场景也用不了。unexpected_tool_call 频发:先核对请求里 tools 的实际内容,再看 tool_choice 的模式,最后才怀疑模型(F27)。any 模式下工具全被白名单排掉了:请求会失败或空转,白名单和模式要一起设,别只改一个。执行层没有对应实现:看起来是「白名单过了」,实际跑起来返回 None,这类最隐蔽,因为它不报错。声明与白名单不一致:某次热更新改了声明、白名单没跟上,表现为「这个功能时好时坏」;发布前跑一次三者交集检查就能提前发现。
五、判据卡:白名单该放在哪一层
| 该这么做 | 不该这么做 |
|---|---|
| 不想让模型碰的能力,根本不写进声明 | 写进声明再用提示词禁止 |
用 allowed_tools 按请求收窄范围 |
为一次特殊场景删掉整条声明 |
| 执行环注册表兜住「声明了但没实现」 | 假设 API 允许就等于代码能跑 |
| 工具总数控制在 10 到 20 个以内 | 把所有工具挂上去,让模型自己挑 |
六、来源与核验
https://aistudio.google.com/docs/function-calling.md(核验 2026-11-05,F84、F87)https://aistudio.google.com/docs/api-errors.md(核验 2026-11-05,F27)
四种模式与 allowed_tools 的位置属于「API 结构」档事实,复核周期三个月,见 1.3。若官方新增模式或调整字段嵌套,本节的对照表要整表重核,不能只补一行。
七、动手试
- 把
ALLOWED改成{"get_ticket", "refund"},重跑。预期协议层拦 0 个、最终可执行 2 个;验收标准是你能说清「这次放宽的是哪一层,以及为什么声明层仍然拦得住 delete_all」。 - 把
REGISTRY里的get_ticket删掉,重跑。预期它在执行层被拦;验收标准是给出一个「声明了、允许了、但代码没实现」的真实场景(提示:feature flag 关掉的旧接口)。 - 打开你项目的工具清单,数一下这次请求真正传了几个
tools。验收标准:给出数量与各自的用途;超过 20 个,就把不常用的拆到另一条链路去。

TRAE-AI编程


更多建议: