
Function Calling 参数总出错?只修参数就行
上线 Agent 之后最先崩掉的东西,通常不是模型不够聪明,而是工具参数校验。
模型返回 {"priority": "urgent"},你的 schema 里只有 low/normal/high;
模型把 retry 填成 true,你的下游要 int;
模型多塞了一个 foo,你的 create_ticket(**args) 直接抛 TypeError。
很多人的处理方式是:重发整轮对话让模型重写。这是最贵的做法------正文重写一遍,token 翻倍,而且模型很可能第二次还错在同一个地方。
正确的做法是:把校验错误当成工具的执行结果,回爌给模型,只让它修参数。
一、先写一个"会说人话"的校验器
python
PY_TYPES = {"string": str, "integer": int, "array": list, "boolean": bool}
def validate(args, schema=SCHEMA):
"""返回错误列表;空列表代表参数合法,可以真正执行工具。"""
errors = []
if not isinstance(args, dict):
return ["arguments 不是 JSON 对象"]
props = schema["parameters"]["properties"]
for key in schema["parameters"].get("required", []):
if key not in args:
errors.append(f"缺少必填字段: {key}")
for key, val in args.items():
spec = props.get(key)
if spec is None:
errors.append(f"未知字段: {key}(schema 未定义,应删除)")
continue
want = spec["type"]
py_t = PY_TYPES.get(want)
# bool 是 int 的子类,必须显式排除,否则 True 会被当成合法的 integer
if want == "integer" and isinstance(val, bool):
errors.append(f"{key} 类型应为 integer,实际是 boolean")
continue
if py_t and not isinstance(val, py_t):
errors.append(f"{key} 类型应为 {want},实际是 {type(val).__name__}")
continue
if "enum" in spec and val not in spec["enum"]:
errors.append(f"{key} 取值 {val!r} 不在允许值 {spec['enum']} 内")
if want == "string" and len(val) < spec.get("minLength", 0):
errors.append(f"{key} 长度不足 {spec['minLength']}")
if want == "integer":
if "minimum" in spec and val < spec["minimum"]:
errors.append(f"{key} 小于最小值 {spec['minimum']}")
if "maximum" in spec and val > spec["maximum"]:
errors.append(f"{key} 超过最大值 {spec['maximum']}")
return errors
这里面有一个特别隐蔽的坑 :isinstance(True, int) 在 Python 里是 True。
如果你只写 isinstance(val, int),那么模型把布尔值填进整型字段时校验会通过 ,
然后一路错到数据库层。所以 bool 必须单独挡一次。
二、把错误回灌给模型,只修参数
python
def call_with_repair(model, max_attempt=3):
attempt, errors, args = 0, None, None
while attempt < max_attempt:
args = model.repair(errors, args, attempt) # 把上一轮的错误喂回去
errors = validate(args)
print(f"[第 {attempt + 1} 轮] 参数={json.dumps(args, ensure_ascii=False)}")
if not errors:
return run_tool(args) # 只有校验通过才真正执行
for e in errors:
print(" *", e) # 机器可读的错误,不是"参数不对"
attempt += 1
return {"ok": False, "reason": "超过最大重试次数"}
用一段模拟的"模型"跑一遍(第 1 轮故意给三种养型错误):
[第 1 轮] 参数={"title": "bug", "priority": "urgent", "foo": 1}
-> 校验失败,回灌给模型:
* title 长度不足 5
* priority 取值 'urgent' 不在允许值 ['low', 'normal', 'high'] 内
* 未知字段: foo(schema 未暂定,应删除)
[一第 2 轮] 参数={"title": "登录页 500", "priority": "urgent", "retry": true}
-> 校验失败,回灌给模型:
* priority 取值 'urgent' 不在允许值 ['low', 'normal', 'high'] 内
* retry 类型应为 integer,实际是 boolean
[一第 3 轮] 参数={"title": "登录页 500", "priority": "high", "retry": 2}
-> 校验通过
[扮行工儷] create_ticket({"title": "更新确认", "priority": "high", "retry": 2})
{'ok': True, 'id': 'T-1024'}
三轮收教。我注意模型每轮只义报错指出的那几处,荃文(对话厅史)一个字都没重写------这是 token 成本能降下来的关键。
三、五个高频坑
坑 1:错误信息不够具体。
回猄 "参数不正确",模型只能矎匟;回猄 "priority 取值 'urgent' 不在允许值 'low','normal','high' 内",模型一次就能改对。校验信息要写成机器可读的,而不是给人看的。
坐 2:没挡 bool / 数字字符串。
isinstance(True, int) is True,以及模型爱远回 "3" 而不是 3。要么严格拒绝,要么在边界层做显式转换(转换失败的报错同样要具体)。
坑 3:没有重试上限。
模型可能在同一处反复失败。max_attempt=3 是我的经验值,超了就降级(改成让用户确认参数,或走默认值),不要无限循环烧 token。
坐 4:不知道工具是否幂等,就重试。
参数校验在执行前 ,所以"校验失败 → 重试"是安全的。但如果工具已经执行、只是返回结果解析失败,再重试就会产生重复订单/重复工单。*区分"执行前重试"和"执行后重试",后者必须带幂等键。�
坑 5:把 schema 写得过于宽松。
没有 enum、没有 minLength、没有 required,等于把校验工作全推给下游业务代码。schema 是你唯一能免费拿到的约束,写严一点。
四、生产环境建议
- 用
pydantic/jsonschema替代手写校验器,但保留"错误信息可回灌"这一层------库抛出的异常太啰嗦,直接喂给模型反而费 token; - 把
validate()的调用放在工具包装层(decorator)里,业务函数只关心合法参数; - 记录每次修复的轮次和错误系型,一周后你会看到模型的系统性弱点(比如总在枚举值上出错,那就是 schema 描述没写清楚);
- 参数校验通过 ≠ 业务校验通过。业务规则(库存、权限)仍然要在工兴内部再挡一层。
小结
- Function Calling 的稳定性,靠的是校验器 + 错误回灌,不是换更贵的模型;
- 错误信息要具体到"哪个字段、期望什么、实际什么";
- 只修参数,不重写正文,token 成本立刻降一个量级。
文中的完整脚本和参数表我整理在自己的笔记站(道满PythonAI):https://www.daomanpy.com/llm/agent-development-practice