当你发现 Agent 调用工具总是失败,报错信息却含糊不清时,问题通常不在工具本身,而在参数从模型输出到实际函数执行之间丢失了什么。参数校验失败是这一类问题中最常见的表现:模型生成了看似合理的参数,但程序在解析、校验或注入时发现它不合法。定位这类问题,不能只盯着最终报错日志,必须把整条链路拆开看。
先确定失败发生在哪一层
一次工具调用要经过几个环节:模型生成参数 JSON → 解析 JSON → 校验参数是否符合工具声明 → 注入执行函数 → 函数自身执行业务校验。参数校验失败可能发生在任意一环,但错误信息通常只在最后出现,容易误判为模型问题。
常见情况有这几种:
- 模型返回的是合法 JSON,但字段类型不匹配,比如把温度写成了字符串
"25"而不是数字25。 - JSON 本身无法解析,比如模型输出被截断、多了一个逗号、或者包在 Markdown 代码块里。
- 参数通过了系统校验,但注入时被中间层转换坏了,例如时区被丢弃、整数被转成浮点、字段名被重命名。
- 工具函数自己的业务校验拒绝,比如日期超出可查询范围,这已经属于业务错误,不是参数格式错误。
定位的第一步,就是给每个工具定义一份明确的参数 schema,并让校验发生在注入之前。
用 JSON Schema 固定“正确”的样子
不要只在代码注释里写“参数应该是字符串”,而是为每个工具建立完整的 JSON Schema。这样既能供模型参考,也能在运行时执行校验。
以一个查询天气的工具为例:
{
"type": "object",
"properties": {
"city": {"type": "string"},
"date": {"type": "string", "format": "date"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city", "date"],
"additionalProperties": false
}
这里用 required 声明必填字段,type 约束基础类型,enum 约束可选值,format 描述日期格式。需要注意的是,JSON Schema 的 format 字段是否被强制校验取决于具体实现,不一定所有校验库都会检查日期格式,所以必要时要自己加正则或格式化函数。
记录模型原始输出,而不是加工后的结果
很多调试失败是因为日志里只记录了最终传给函数的参数对象,没有记录模型原始返回的字符串。模型输出的原始 JSON 是最关键的现场证据。
建议在工具调用链路上增加一个结构化日志点,至少记录三样东西:
- 模型返回的原始 tool call 参数文本(字符串)
- 解析后得到的 Python 对象
- 校验结果与错误详情
在 Python 中可以这样打点:
import json
import logging
logger = logging.getLogger("agent.tool_call")
def parse_tool_args(raw_args: str):
try:
args = json.loads(raw_args)
return {"ok": True, "args": args}
except json.JSONDecodeError as e:
logger.error(
"tool_args_parse_failed",
extra={
"raw_args": raw_args,
"error_pos": e.pos,
"error_msg": e.msg,
}
)
return {"ok": False, "args": None}
注意,raw_args 一定要记录完整文本,而不是截断后的版本。模型如果输出了多余的解释性文字,也要保留,因为你能看到它到底生成了什么。
在执行前用校验库拦截错误
许多 Agent 框架会在调用函数前做一次参数校验,但往往只返回一个笼统的 Invalid arguments。你需要自己把校验层加进去,并且把错误路径打印出来。
使用 jsonschema 库可以做到:
from jsonschema import validate, ValidationError
def validate_tool_args(raw_args: str, schema):
parsed = parse_tool_args(raw_args)
if not parsed["ok"]:
return {"stage": "json_parse", "ok": False, "error": "invalid json"}
try:
validate(instance=parsed["args"], schema=schema)
return {"stage": "schema", "ok": True, "args": parsed["args"]}
except ValidationError as e:
return {
"stage": "schema",
"ok": False,
"reason": e.message,
"path": list(e.absolute_path),
"raw_args": raw_args,
}
e.absolute_path 会给出出错的字段路径,例如 ["date"] 表示 date 字段有问题。这比只看 is not of type 'string' 有用得多。
检查参数注入点
即使模型输出的参数通过了 schema 校验,也可能在注入函数时出错。典型场景包括:
- 模型输出
unit是大写,而函数只接受小写。 - 模型输出
date是2025/01/01,函数内部用datetime.strptime解析,但格式写成%Y-%m-%d。 - 中间层把 JSON 的
null转换成了空字符串,导致函数逻辑走错分支。
一个简单的排查手段是在最终调用函数的入口打一行日志:
def invoke_tool(func, kwargs):
logger.info("tool_invoke", extra={"kwargs": kwargs})
return func(**kwargs)
然后对比这个 kwargs 和模型原始输出解析后的对象。如果两者不一致,说明中间层做了隐式转换;如果一致,说明问题在工具自身。
常见错误模式与定位方向
根据实际调试经验,参数校验失败通常表现为下面几种模式。遇到时可以直接按对应方向查。
类型不匹配(type mismatch)
模型输出了错误类型,比如字符串数字。常见原因是 prompt 中的例子不够明确,或者模型为了满足 format 主动做了字符串化。可以在 schema 中加 type 约束,并考虑在解析后做一次宽松转换,但必须记录转换日志。
缺少必填字段(missing required)
模型没有生成声明为 required 的字段。可能原因是 prompt 没有强调字段必填,或者模型认为可以从上下文推断。排查时看原始输出中是否真的没有该字段,如果字段存在但值为空字符串,那是另一个问题。
多出了未定义字段(additional properties)
如果 schema 设置了 additionalProperties: false,多出的字段会被校验为失败。但很多框架默认允许多余字段,导致参数被静默丢弃。建议在 schema 中显式禁止,并打印多余字段名,这往往能发现模型输出与工具定义不一致。
枚举值越界(enum violation)
模型生成了相似但并不在 enum 列表中的值,例如 "farenheit" 而不是 "fahrenheit"。这说明模型对工具定义的理解有偏差,应该在 prompt 中提供更完整的取值示例,而不是只依赖 schema。
嵌套对象深层错误
当参数是嵌套结构时,错误信息往往指向最外层。使用 absolute_path 可以拿到具体到内层字段的路径。排查时要看完整路径,例如 ["filters", "time_range", "start"],然后回查模型对这层结构的描述是否正确。
从定位走向预防
定位成功的标志,不是你这次修好了一个参数,而是下次出现类似错误时能在 5 分钟内找到原因。要做到这一点,需要建立几个工程习惯。
第一,稳定性复现。把每次工具调用失败的原始参数保存到文件或测试集里,作为回归用例。这样每次修改 prompt 或工具定义后,都能快速跑一遍历史失败案例,确认没有引入新问题。
第二,降低工具参数复杂度。如果工具定义了太多必填字段、深层嵌套或异常格式,模型天然更容易生成错误参数。简化工具接口通常比增强模型能力更有效。在设计阶段,尽可能让参数扁平化,只暴露必要字段,并提供合理的默认值。
第三,在开发环境里使用记录型 mock。你可以临时将工具函数替换为一个只写日志的 mock,让它把每次调用收到的 kwargs 完整打印出来。这样能直观看到模型实际生成的参数与工具声明之间的差异。
最后要说明的是,参数校验失败并不总是模型的问题。工具定义本身是否存在歧义、中间层是否做了破坏性转换、日志是否完整,这些都会影响定位效率。真正有效的调试流程是:先确保现场可完整记录,再逐步缩小范围,最后才会落到模型输出这一个点上。