Gemini 函数调用由谁执行
Gemini 的函数调用分四步:定义声明、带声明调用、由你的应用执行、把结果回传;外部函数的执行权在调用方,模型只给出调用提议。(核验 2026-11-05,F82)
一、Gemini 函数调用由谁执行:问题不是能不能调,而是谁按下执行键
第一次读函数调用文档的人,容易产生一个错觉:声明了 delete_user,模型就能去删用户。于是有人在提示词里写「如果需要就调用删除接口」,然后等着数据库发生变化。
不会发生的。模型的产出是一段提议——调用哪个函数、参数是什么;真正把函数跑起来的那一步,官方写在你的职责栏里:第三步标题是「Execute Function Code (Your Responsibility)」,正文还专门加了一句「The model doesn't execute the function itself」(F82)。
这句话决定了整章的三件事:执行边界、审计位置、以及出问题时该查哪一侧。既然执行权在你手上,那「能跑起来什么」就取决于你注册了什么;它提议得再合理,没有对应的执行函数也只是一段文本。
二、Gemini 函数调用执行环:最小可运行示例
下面这段模拟一次完整的「提议—执行」环:模型给出两条提议,其中一条你根本没注册。
## EXPECT: 提议 2 个:get_ticket、drop_users | 已执行 1 个:get_ticket | 被拦 1 个:drop_users(未注册) | 回传片段 1 条
PROPOSALS = [
{"name": "get_ticket", "args": {"ticket_id": "TKT-A"}},
{"name": "drop_users", "args": {"table": "users"}},
]
def get_ticket(ticket_id):
return {"ticket_id": ticket_id, "status": "open"}
REGISTRY = {"get_ticket": get_ticket}
def execute(proposals, registry):
done, blocked = [], []
for p in proposals:
fn = registry.get(p["name"])
if fn is None:
blocked.append(p["name"])
continue
done.append({"name": p["name"], "result": fn(**p["args"])})
return done, blocked
done, blocked = execute(PROPOSALS, REGISTRY)
assert len(done) == 1 and blocked == ["drop_users"]
print(f"提议 {len(PROPOSALS)} 个:{'、'.join(p['name'] for p in PROPOSALS)} | "
f"已执行 {len(done)} 个:{'、'.join(d['name'] for d in done)} | "
f"被拦 {len(blocked)} 个:{'、'.join(blocked)}(未注册) | 回传片段 {len(done)} 条")
真正要看的不是「拦住了 drop_users」,而是它为什么能被提议:模型完全可以在没有任何声明的情况下吐出一个名字,因为那只是文本。拦住它的不是模型的自律,是 REGISTRY.get() 返回了 None。这一行是执行边界的最后一道闸——它挡住的是「代码里根本没有这个能力」,不是「这个用户没权限」,后者见 6.4。
三、解释与边界:执行权在你手上的三个后果
后果一,能跑什么取决于你注册了什么。 执行环只能调用 REGISTRY 里存在的键。想把危险动作挡在外面,最有效的做法不是写一长串提示词禁令,而是根本不注册那个函数——模型提议一百次,也是 None。这也是为什么白名单要单独讲一轮(见 6.3):声明层与 allowed_tools 在前面两道,注册表是最后一道。
后果二,审计点唯一。 所有执行都经过 execute(),日志可以在一处打全:谁提议、参数是什么、执行结果如何、哪些被拦。如果放任每个业务模块自己去处理模型的返回,日志会散落在十几个文件里,出事后拼不出时间线。
后果三,错误归属清楚。 提案里参数给错,是模型侧的问题,改声明描述或加校验;函数自己抛异常,是你业务侧的问题,跟模型无关。分不清这两侧,排查会一直在错的地方打转。
三个常见的执行环缺陷。 一是同步串行:所有提议一条条跑,明明互不依赖也排队,一次交互凭空多出几倍延迟;二是没有超时:某个工具卡在外部接口上,整轮交互就挂在那里,用户看到的是一个转不动的圈;三是没有幂等:模型重发一次提议,你的系统就重做一次。这三条都不在 API 文档里,但都在真实系统里出过事。修法也不复杂——无依赖的提议并发执行、每个调用带超时、写操作带业务唯一键。落到代码上是三处小改动,比事后对账便宜得多。
模型为什么会编出一个你没注册的名字。 常见三种来路:声明描述写得太宽(比如「可以处理用户的任何请求」),模型就顺着语义往下猜;上一轮的工具结果被原样喂回上下文,模型照抄了里面出现过的字段名;或者它从别的系统提示里带过来了方法名。这三种在日志上能区分:看被拦提议的名字与你声明名的编辑距离,差得远的多半是「编」,只差一两个字的多半是「抄错」。前者要收紧描述,后者要在回传结果时别把内部字段名暴露出去。
还有一件常被忽略的事:官方的四步会跨多轮重复(F82)。工具结果回传之后,模型可能再提议下一次调用,直到它认为可以给最终答复。所以执行环要有轮次上限,否则一个总也查不到数据的查询会无限循环——这一点和「重试要停」是同一个道理,见 5.4。
边界。 本节只定「谁执行」。系统指令怎么写、注入怎么防,见 4.2;内置工具与自定义函数怎么组合,见 6.2;白名单放在哪一层,见 6.3;参数怎么校验,见 6.4。本节也不讨论模型为什么倾向于调某个工具——那属于提示词与声明描述的质量问题。
四、预期输出与异常分支
真跑输出:
提议 2 个:get_ticket、drop_users | 已执行 1 个:get_ticket | 被拦 1 个:drop_users(未注册) | 回传片段 1 条
六类分支。提议了没注册的工具:REGISTRY.get() 返回 None,拦住即可,但要记一笔日志——频繁出现说明声明描述让模型误以为有这些能力。参数类型对但业务不存在:ticket_id 合法格式却查不到记录,函数返回空对象还是抛异常,要提前定死,别让模型去猜。同一提议重复执行:多轮里模型可能重发一次,写操作必须带幂等键,否则会重复扣款、重复关单。执行成功但回传格式模型读不懂:把 result 包成模型能理解的结构(键名稳定、别塞二进制),否则下一轮它会拿着这段结果胡乱推理。提案里有多个工具调用:独立的可以一起执行,有依赖的必须串行——顺序错了,第二个调用拿到的就是空的。执行环自己成了瓶颈:某个工具平均耗时很高,串行执行把整轮交互拖到十几秒,用户体感是「AI 卡了」;把无依赖的提议并发化,往往是性价比最高的一次优化,但要先量出各工具的实际耗时再改,别一上来就上线程池。
五、判据卡:谁该按下执行键
| 该这么做 | 不该这么做 |
|---|---|
| 只把允许的动作注册进执行环 | 用提示词写「禁止调用 XX」当边界 |
| 执行环统一打日志(提议、参数、结果) | 各业务模块自行处理模型返回 |
| 写操作一律带幂等键 | 假设模型不会重复提议 |
| 给执行环设轮次上限 | 让查询循环跑到模型自己停 |
六、来源与核验
https://aistudio.google.com/docs/function-calling.md(核验 2026-11-05,F82)https://aistudio.google.com/llms.txt(核验 2026-11-05,F4)
执行归属属于「API 结构」档事实,复核周期三个月,见 1.3。如果官方后续推出「服务端代执行」的托管工具,本节的前提就变了,届时以函数调用页的四步流程原文为准,不能只改一句话。
七、动手试
- 给
PROPOSALS加一条{"name": "get_ticket", "args": {"ticket_id": "TKT-B"}},重跑。预期已执行 2 个、回传片段 2 条;验收标准是你能说清「两条提议为什么可以并行执行」。 - 把
drop_users注册进REGISTRY,让它执行lambda table: {"dropped": table},重跑。预期被拦 0 个;验收标准是你能说出这次改动到底放宽了哪一层权限——以及为什么这正是最危险的一种「为了让 demo 跑通」的改动。 - 打开你项目里处理模型返回的代码,找一下有没有统一入口。验收标准:能指出「提议、参数、执行结果」这三样分别记在哪里;找不到统一入口,就说明执行权和审计权已经散掉了。

TRAE-AI编程


更多建议: