当你发现 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 是大写,而函数只接受小写。
  • 模型输出 date2025/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 完整打印出来。这样能直观看到模型实际生成的参数与工具声明之间的差异。

最后要说明的是,参数校验失败并不总是模型的问题。工具定义本身是否存在歧义、中间层是否做了破坏性转换、日志是否完整,这些都会影响定位效率。真正有效的调试流程是:先确保现场可完整记录,再逐步缩小范围,最后才会落到模型输出这一个点上。