☰
Function Calling 类型缺口:模型没错,错在边界不校验
2026/10/14 16:48:20 网站建设 项目流程

问题出在哪

先看一段代码。模型通过 Function Calling 返回参数,代码这么写:

args=json.loads(tool_call.function.arguments)ifargs["include_refunded"]:query=build_query(include_refunded=True)

这一段要看清一个前提:只有当 schema 把参数声明成string、或者漏写type时,模型才会给出字符串。声明为boolean时它给的是真布尔——这一点后面有实测。

arguments里的值是带引号的字符串"false":

{"include_refunded":"false"}

这行进了 Python 之后,json.loads把它变成字符串'false',而 Python 里非空字符串是真值:

bool('false') = True bool('False') = True bool('FALSE') = True bool('0') = True bool('no') = True bool('') = False

六个里只有空字符串是假。所以if args["include_refunded"]走进True分支,退款订单被算了进去。

没有任何报错。类型错了,但 Python 不吭声,数字就这么错了。

一个想当然的推断

初稿的结论曾是「模型经常把布尔参数写成字符串」。这听起来合理:模型输出文本,字符串布尔是常见现象,网上的文章也这么说。

但这句话经不起追问:有实测支撑吗?

没有。于是做了一组实测:三个模型 × 四个自然语言提示词 × 参数声明为type: boolean=12 次调用。下面只贴了其中一个模型的 4 条(完整 12 条在证据文件里):

布尔 ✓ False (bool) 查一下最近的订单,不要包含已退款的。 957ms 布尔 ✓ True (bool) 列出订单,退款的那些也一起给我。 1068ms 布尔 ✓ False (bool) 我要看不含退款的订单,最近 10 条。 862ms 布尔 ✓ False (bool) 帮我查订单,包含退款和不包含退款的都查一次… 1449ms

12 次全部返回真布尔。字符串 0 次。

这个前提错了。所以又做了第二组实验:字符串到底在什么条件下出现?

决定返回类型的是 schema,不是模型

固定同一个提示词,只改 schema 里那个参数的写法:

参数写法三个模型的返回
① 声明type: boolean3 个都是bool
② 声明type: string3 个都是str
③ 不声明 type,只给描述2 个bool,1 个str(不一致)
④ 描述里带引号示例3 个都是bool

结论很清楚:模型老实按 schema 声明返回。

第 ④ 种要说清它的局限:那一组只改了description的文字,type: boolean一直在,所以不能据此说「描述比示例权重高」——起作用的一直是type。它只证明了一件事:描述里出现带引号的示例写法,不会让模型改用字符串。

所以真正的问题不在模型。问题在于:schema 声明了什么类型,与代码运行时实际拿到什么类型,这两件事之间没有校验。

关于模型名,说明一句:deepseek-flash、deepseek-v4-pro、glm-5.3-flashx是调用时实际传的model参数值,不是化名。本文实验日期为 2026 年 10 月,走的是各厂商 OpenAI 兼容接口,全部为非流式调用(流式返回未测,流式下tool_calls的分片拼装是另一套逻辑)。模型版本会随厂商更新,隔一段时间复现可能得到不同结果——这正是这类结论必须附实验时间的原因。

第 ③ 种最该盯住:三个模型给出两种类型。这一组只测了 3 个样本,不足以断言「哪个模型一定偏向哪种」,但足以说明会出现不一致——而代码里若写着if isinstance(v, bool),它就只在部分模型上正确,不会报错,只会在换模型时突然失效。

后果:数字错了,但没有报错

用 6 条订单数据(其中 1 条已退款)对比两种写法:

天真写法 if args["include_refunded"]: → 命中 6 条 合计 3779.5 显式归一 to_bool(args["include_refunded"]) → 命中 5 条 合计 3691.5

差 1 条订单、88.0 元。没有异常抛出,日志里也不会出现任何错误。

左边是现在多数的走法:值进了if,非空字符串判为真,退款被算进去,没有任何提示。右边是归一之后的结果。

怎么改

不要信「模型应该给对类型」,也不要信「schema 声明了运行时就不必校验」。在边界上做一次显式归一。

defto_bool(value)->bool:"""把模型给的值归一到布尔。认不出来就抛错,不猜。"""ifisinstance(value,bool):returnvalueifisinstance(value,int)andvaluein(0,1):returnbool(value)ifnotisinstance(value,str):raiseValueError(f"无法当布尔处理:{value!r}")low=value.strip().lower()iflowin("true","1","yes","on"):returnTrueiflowin("false","0","no","off"):returnFalseraiseValueError(f"无法当布尔处理:{value!r}")

它的行为:

to_bool('false') = False to_bool('true') = True to_bool('False') = False to_bool('1') = True to_bool('FALSE') = False to_bool('yes') = True to_bool('0') = False to_bool(True) = True to_bool('no') = False to_bool(False) = False to_bool('off') = False to_bool(1) = True to_bool(0) = False

大小写、数字、缩写都归一掉,"false"不再可能变成True。

注意最后那条路径:认不出来就raise,不返回False。

两个容易被忽略的边界

None和空字符串要拒绝,不要默认成False。

to_bool(None) -> 拒绝: 无法当布尔处理: None to_bool('') -> 拒绝: 无法当布尔处理: '' to_bool('maybe') -> 拒绝: 无法当布尔处理: 'maybe'

这两者语义不同:None通常是模型没给这个参数(等于没表态),空字符串是没有信息。把它们当成False,等于替用户做了「不包含退款」的决定。宁可抛错,让它暴露出来。

不要把json.loads和eval相提并论。

初稿写过「用eval或json.loads兜底就等于把任意输入交给解析器」。这句话是错的。json.loads不执行代码,它的失败方式是抛JSONDecodeError;eval会执行代码。两者风险差着量级,并列会让人误判。

json.loads在本文语境里要防的是另一件事:它不校验类型。{"include_refunded": "false"}是合法 JSON,它会老老实实给你一个字符串——这正是类型缺口溜进来的地方。

抛错之后,这一轮工具调用怎么办

to_bool抛出ValueError,工具函数就失败了。在 agent 循环里,这一轮工具调用拿不到结果——接下来怎么做?

能把错误说清楚就回传给模型,让它改。Function Calling 的标准做法是把工具结果
(包括错误)作为一条tool消息写回对话,模型看到之后可以重新调一次:

try:include=to_bool(args["include_refunded"])exceptValueErrorasexc:return{"error":str(exc),"hint":"该参数只接受 true/false"}

这条错误信息回了模型,它有机会把"maybe"改成true或false重试。

但有两类不该让它自愈。

一是语义没表态的情况:None或空字符串表示模型没给这个参数,或者给了个空值。
这不是拼写问题,回传过去它可能随便猜一个值来满足格式要求——而「随机猜一个」
比直接失败更糟,因为猜错不会报错。

二是涉及金额、权限、删除这类不可逆动作的参数。这种时候重试的代价高于失败,
应当直接中断并让调用方看到,不要指望模型自己纠对。

所以判断标准是:拼写、大小写这类可纠正问题可以回传重试;语义没表态和涉及金额、权限、删除这类不可逆动作的参数必须当场停。

上手动作

第一步,查代码里有没有这个缺口。Windows 上用 PowerShell:

# 找把模型参数直接丢进条件判断的地方Get-ChildItem-Recurse-Filter*.py|Select-String-Pattern'if\s+args\[','if\s+args\.get\(','bool\(args\['

不要只搜bool\(——那会命中所有普通类型转换代码,噪声远大于信号,
跑一遍基本定位不到真正的类型缺口。要搜的是参数直接进条件判断的写法。

Linux 或 macOS:

grep-rn"if args\["--include=*.pygrep-rn"args.get("--include=*.py

第二步,对照本文第 ③ 种 schema 写法检查自己的 tool 定义。每个布尔、数字、枚举参数是不是都写了type。少写一个type,你就落在「不同模型给不同类型」那个区间里。

第三步,凡是来自模型的值进业务分支之前,过一次显式归一。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询