欢迎光临
我们一直在努力

AI Agent 调用工具时,为什么不能直接相信模型生成的参数?

👋 关于作者

你好,这里是一条泥憨鱼 | 专注于 后端开发、AI 前沿技术与计算机基础 。

🏠 个人主页:一条泥憨鱼

通过技术博客记录技术成长,分享学习心得与经验。期待与您共同交流!

📚 精选技术专栏:

❄️后端开发 ❄️Redis| 黑马点评
❄️从0开始学习计算机网络 ❄️数据结构与算法
❄️苍穹外卖 ❄️探秘AI与agent

用户说的是“上周”,Agent 却把 startDate 填成了 2026-01-01,工具照常执行,链路毫无异常。这篇从这个安静的故障讲起,拆开模型生成参数不可信的原因、错误的三类形态、校验该落在哪一层,以及错误怎么回传才不会让 Agent 原地打转。

参数错误的三种来源

把参数错误当成一个整体来看是没法处理的,因为它们的拦截位置完全不同。按“错在哪一层”拆开,大致是三类。

第一类:格式合法但取值越界。 模型生成了一个长得像 ORD-2024-88731 的订单号,格式完全正确,只是这个订单号不存在;或者工具描述里列了 PENDING / PAID / SHIPPED / CLOSED 四个状态,模型生成了 PROCESSING。这类错误的共同点是:值域问题,结构上看不出毛病。JSON Schema 能告诉你 status 是 string,但如果你没写 enum,它不会告诉你 PROCESSING 不在白名单里;即使写了 enum,模型也可能在长上下文里把枚举值记串。

第二类:类型正确但语义错位。时间、单位、相对指代是重灾区。用户说“上周”,模型需要把它换算成一个具体区间,这一步涉及当前时间、时区、周起始日是周一还是周日,任何一环出错结果都是错的,但参数依然是一个合法的日期字符串。单位错位同样隐蔽:接口要的是毫秒时间戳,模型给了秒;金额字段要的是“分”,模型给了“元”。还有一类是指代错位,用户说“把这个订单取消掉”,上下文里有两个订单号,模型挑了错的那个。这些错误在类型层面完全合法。

第三类:结构正确但业务前置条件不满足。 参数全对,但当前状态不允许执行。订单已经是 CLOSED,工具却被调用来“取消订单”;用户没有该订单所属商户的操作权限;库存已经为 0 却要下单。这类问题连业务参数本身都没错,错的是“现在能不能做这件事”。

三类错误的共同点是:它们都发生在 JSON Schema 的视野之外。Schema 描述的是“数据长什么样”,不是“数据对不对”。把校验全部押在 Schema 上,等于只检查了信封有没有贴邮票,没检查里面装的是不是要寄的东西。

为什么模型天生不可靠

要理解为什么提示词治不了这个问题,得先看清楚模型在生成参数时到底在做什么。

模型做的是 token 概率采样,不是数据库查询。给它一段工具描述和用户意图,它逐个 token 地生成最可能的下一个字符。它内部没有一个“订单表”可以查,也就没有“知道这个 ID 不存在”的能力。它能做的,是生成一个在统计上看起来最像订单号的字符串。这就是为什么编造出来的 ID 往往格式极其逼真——它学的就是真实 ID 的分布。

这个机制决定了三件事。

其一,模型不会主动说“我不知道这个订单号”,因为它的输出目标不是表达不确定性,而是续写最可能的序列。

其二,temperature 只是把这个采样的分布调平或调尖,调低能让输出更确定,但确定地输出一个错误答案和随机地输出一个错误答案,对下游来说没有区别。

其三,上下文越长、工具越多、描述越含糊,采样时被干扰的因素就越多,参数串味的概率越高。

工具描述的歧义会直接放大问题。如果两个工具都叫“查询订单”,一个按订单号一个按用户 ID,模型选错的概率相当高;如果参数叫 date,没写清楚是下单时间还是支付时间,模型只能猜。这不是模型笨,是它拿到的信息本来就不足以确定。

所以指望用一句“请确保参数准确”的提示词解决问题,方向就错了。提示词能改善概率,不能提供保证。而副作用操作需要的是保证。

校验该放在哪一层

结论很直接:校验必须放在代码里,而且要分层。模型负责“提议”,代码负责“裁决”。

三层各自管什么,边界要划清楚:

Schema 层管类型、必填、基本结构。这一层最便宜,也最容易被当成全部。它能拦住“该给数组给了字符串”这种低级错误,但对取值一无所知。

语义层管值域。枚举必须写成白名单,数值必须有上下界,字符串必须过正则。这一层的关键是把“合法取值”显式写进代码,而不是留在工具描述里让模型自觉遵守。

业务层管现实世界。ID 存不存在要查库,状态机允不允许要判断,权限够不够要鉴权。这一层最贵,但它是唯一能拦住“订单号格式正确但不存在”的地方。

下面是一个把三层串起来的工具执行器骨架,用 Java 写,思路和语言无关:

public class OrderToolExecutor {

    // 语义层:枚举白名单,不依赖模型自觉

    private static final Set<String> ALLOWED_STATUS =

        Set.of("PENDING", "PAID", "SHIPPED", "CLOSED");

    public ToolResult execute(CancelOrderArgs args, UserContext user) {

        // 1. Schema 层通常由框架完成,这里只做兜底

        if (args.orderId() == null || args.orderId().isBlank()) {

            return ToolResult.retryable("缺少 orderId,请从上下文中的订单号重新提取");

        }

        // 2. 语义层:格式与值域

        if (!args.orderId().matches("^ORD-\\\\d{4}-\\\\d{5}$")) {

            return ToolResult.retryable("orderId 格式应为 ORD-年份-5位数字,收到的值不合法");

        }

        if (args.targetStatus() != null && !ALLOWED_STATUS.contains(args.targetStatus())) {

            return ToolResult.retryable("targetStatus 只能是 " + ALLOWED_STATUS);

        }

        // 3. 业务层:存在性、状态机、权限

        Order order = orderRepository.findById(args.orderId()).orElse(null);

        if (order == null) {

            return ToolResult.retryable("订单 " + args.orderId() + " 不存在,请确认订单号");

        }

        if (!order.getStatus().equals("PENDING") && !order.getStatus().equals("PAID")) {

            return ToolResult.fatal("订单当前状态为 " + order.getStatus() + ",不允许取消");

        }

        if (!permissionService.canOperate(user, order)) {

            return ToolResult.fatal("当前用户无权操作该订单");

        }

        orderService.cancel(order);

        return ToolResult.ok("订单 " + args.orderId() + " 已取消");

    }

}

这段代码里有几个刻意的设计。业务层查库那一步不能省,因为它是唯一能确认“这个 ID 真实存在”的地方;状态判断放在存在性判断之后,因为对一个不存在的订单谈状态没有意义;权限判断放在最后,避免在无权限时泄露订单状态信息。

工具的参数定义里如果有 enum,一定要和代码里的白名单来自同一个常量源。两边各写一份,迟早会漂移,而漂移之后的错误极难排查——模型按描述生成的值被代码拒绝,日志里看起来像是模型不听话,实际是配置不一致。

错误回传给模型的方式

校验拦住之后,错误怎么返回,直接决定了 Agent 接下来是自我纠正还是原地打转。

第一个原则:返回可读、可纠正的信息,不要抛堆栈。模型看不懂 NullPointerException at OrderToolExecutor.java:47,但它能理解“订单 ORD-2024-88731 不存在,请确认订单号”。错误信息里应该包含三样东西:哪个参数错了、期望是什么、下一步该怎么做。

第二个原则:区分可重试和不可重试。参数错误、ID 不存在这类问题,模型拿到明确提示后有机会改对,属于可重试;权限不足、系统内部错误这类问题,重试多少次结果都一样,属于不可重试,应该直接终止并如实告知用户。

第三个原则,也是最容易被忽略的:必须有重试上限和降级策略。模型在收到“订单不存在”之后,很可能再编一个订单号,然后又被拒绝,如此循环。实际项目里这个循环能烧掉不少 token,还会让用户等很久。常见的做法是给同一个工具的调用次数设上限(比如 3 次),超限后不再把错误喂回模型,而是直接向用户输出“我没能完成这个操作,原因是……”。

// 伪代码:带重试上限的调用循环

int attempts = 0;

while (attempts < MAX_TOOL_ATTEMPTS) {

    ToolCall call = model.nextToolCall();

    ToolResult result = executor.execute(call);

    if (result.isOk()) return result;

    if (!result.isRetryable()) return fallbackToUser(result.message());

    // 只有可重试错误才把信息喂回模型

    model.appendToolResult(call.id(), result.message());

    attempts++;

}

return fallbackToUser("多次尝试仍未成功,请人工确认参数");

result.isRetryable() 这个判断发生在把错误喂回模型之前。不可重试的错误一旦喂回去,模型往往会换个说法再试一次,徒增一轮无效调用。

判断方法

回到开头那个故障:如果查询工具在拿到 2024-01-01 时能问一句“这个日期和用户说的‘上周’对得上吗”,问题当场就会暴露。但工具没有用户原话,做不了这个判断,所以真正的防线应该更靠前——在把参数交给工具之前,先明确这个值是怎么来的、有没有被验证过。

可以迁移到任何 Agent 工具调用场景的判断方法是:凡是模型生成的参数要进入有副作用的操作之前,都问一句“这个值如果错了,下游能不能自己发现”。查询类工具错了顶多返回空结果,下游还有机会察觉;但取消订单、扣款、发消息这类操作,一旦执行就收不回来,下游没有任何机制能发现参数是错的。对后者,校验不能寄希望于模型自觉,必须在调用前用代码拦住,拦住之后再把清晰的错误交给模型去纠正。

赞(0)
未经允许不得转载:171主机测评 » AI Agent 调用工具时,为什么不能直接相信模型生成的参数?
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址