跳到主要内容

24 篇博文 含有标签「tool-calling」

查看所有标签

你的智能体忘记发送的幂等键

· 阅读需 10 分钟
Tian Pan
Software Engineer

你的智能体中最昂贵的 Bug 不是幻觉。而是重试。

在你的技术栈中,总有一个工具负责扣款、发送邮件、关闭工单或写入数据行。智能体调用它,调用耗时过长导致超时,于是智能体——作为一个优秀的、具有弹性的软件——再次调用它。棘手之处在于,第一次调用其实已经成功了,只是响应从未返回。结果你扣了客户两次款,无论你如何提示“请在支付时保持谨慎”,都无法阻止这种情况发生。

这是分布式系统中最古老的故障模式,只是披上了一件新装。十年前,我们通过幂等键(idempotency keys)为 HTTP API 解决了这个问题。但大多数智能体技术栈通过将原本为读取设计的重试逻辑直接用于执行写入的工具,且从未发送那个能让重试变得安全的字段,从而重新引入了这个问题。

对你的工具接收的内容保持严格:波斯塔尔法则在智能体系统中失效

· 阅读需 12 分钟
Tian Pan
Software Engineer

“发送时要保守,接收时要开放。”波斯塔尔法则(Postel's Law)无疑是网络历史上最成功的设计原则——正是依靠它,来自不同厂商的 TCP 实现才得以在 20 世纪 80 年代实现互操作,并塑造了四十多年来的协议和 API 设计。然而,根据 IETF 自身的判断,这一原则会随着时间的推移而变质:RFC 9413 源于一份标题直白为《稳健性原则的有害后果》(The Harmful Consequences of the Robustness Principle)的草案,认为开放式的接收在短期内有助于互操作性,但从长远来看,却在悄悄腐蚀整个生态系统。

智能体(Agent)系统将这种长期的腐蚀缩短到了几周。当“发送者”是一个发出工具调用(tool calls)的大语言模型时,每一次开放式的接收——将 "5" 强制转换为 5、丢弃未知字段、对枚举拼写错误进行模糊匹配——都会破坏维持系统健康所需的精确信号。在智能体架构中,工具边界是唯一一处“大声报错”即是可靠性特性的地方,而大多数团队却反其道而行之。

你的工具 Schema 验证的是类型,而非单位

· 阅读需 13 分钟
Tian Pan
Software Engineer

一个退款智能体正在处理一笔 42 美元的客户请求。它发出的工具调用是 refund(amount: 4200) —— 等等,这正确吗?如果后端以“分”为单位存储金额,那它完全正确。如果后端以“美元”为单位存储,那么客户刚刚得到了 4,200 美元的退款。两次调用在语法上都是完美的。两者都在微秒级内通过了 JSON Schema 验证。Schema 规定 amount 是一个数字,而 4200 毫无疑问是一个数字。

这是一类单元测试、Schema 验证器和大多数智能体评估(Agent evals)都会忽略的错误:工具调用边界处的单位混淆。模型会根据其训练数据进行数值模式匹配 —— 以美元计价的货币、以秒计价的时间、由周围文本暗示的重量单位 —— 而你的工具则期望的是分、毫秒或公斤。类型系统不会提出任何异议。发票金额只是偏差了 100 倍。

用户关闭对话后才完成的异步工具调用

· 阅读需 13 分钟
Tian Pan
Software Engineer

智能体(agent)会话模型出现故障的最明显标志,就是当工具结果无处可去时。智能体发起了一个耗时较长的调用——例如渲染、资源配置任务或多步查询。用户盯着加载图标看了几秒钟,觉得终究还是不需要,于是关闭标签页并离开了。40 秒后工具运行结束。它的回调(callback)携带着一个不再指向任何内容的 conversation_id 命中你的网关。网关面临两个同样糟糕的选择:默默丢弃该结果,或者将其缝合到接管该 ID 的下一个会话中。

大多数团队发现这种失败模式的方式都如出一辙:一张服务工单,用户在里面反馈看到了一个他们没问过的问题的答案,且挂载在一个他们并未开启的对话中。或者是下游系统对同一笔费用进行了两次扣款,因为网关“热心地”针对下一个活动会话重试了交付。或者——最常见的情况——表面上什么也看不出来,只是完成率指标(completion metrics)在缓慢下滑,而没人能将其与任何具体原因联系起来,因为这些失败不会触发警报;它们只会触发“空无”。

法律免责声明如何从答案泄露到工具调用参数中

· 阅读需 10 分钟
Tian Pan
Software Engineer

你的法律顾问批准了一个单行的系统提示词指令:在每一个涉及受监管领域的回答中附加 “此信息不构成法律建议,不应以此为依据”。三周后,一个用户提交了一个 Bug,因为他们的日历事件描述字段以该行开头,随后是智能体本应放入会议邀请的合同摘要。智能体并没有发生故障。它完全按照系统提示词的要求执行了操作,结果发现这种行为涵盖了模型输出文本的每一个渠道——包括它调用的下一个工具的 JSON 参数。

该指令是一条内容格式规则,而模型也将其视为一条规则。它没有区分 “面向用户的回答” 和 “工具调用参数”,因为提示词中没有任何内容告诉它这些是不同的表面。免责声明最终出现在日历中、邮件草稿中,以及你的智能体代表用户发布的 Slack 消息中。这些中的每一个都是独立的下游系统,其作者根本不知道会有合规字符串被注入到结构化字段中,且每个系统的清理成本各不相同。

一个工具请求的 OAuth 作用域,为何被其他所有工具悄悄继承了?

· 阅读需 11 分钟
Tian Pan
Software Engineer

设计文档规定每个工具都拥有独立的 OAuth 令牌,并被限制在该工具所需的最小权限范围内。而实现代码则使用 (user_id, provider) 作为键(key)来存储令牌。在 v1 版本发布当天,这两个表述都是成立的,因为当时每个提供商(provider)恰好只有一个工具。当针对同一提供商的第二个工具上线时,设计文档依然成立,但存储层却在悄无声息中使其失效了。

六个月后,一次安全审查将一起事故追溯到了那行模式(schema)定义。一个日历读取工具通过日程描述中的提示词注入(prompt injection)被攻破,并成功调用了用户主日历上的 events.delete 接口。读取工具从未被授予过该作用域(scope),但写入工具被授予了。令牌存储层并没有区分它们。

这种故障模式在于,基于每个提供商(per-provider)的键结构会在共享同一提供商的工具之间悄悄累积权限——这也让人们在架构上意识到:OAuth 作用域是令牌(token)的属性,而不是工具(tool)的属性。

与工具本身脱节的工具描述

· 阅读需 12 分钟
Tian Pan
Software Engineer

一名后端工程师将参数从 user_id 重命名为 account_id,因为两者在半年前就不再是同一个东西了,而且一个支持工单终于让这种歧义变得无法忍受。JSON schema 在发布重命名的 pull request 中得到了更新。该工具的文本描述——即模型实际阅读以决定是否调用该工具以及如何调用的那个段落——却存在于另一个代码库中,由另一个团队负责,通过工单队列更新,并且仍然写着“传递 user_id 以查找账户”。没有人标记出这一点。模型尽职尽责地使用正确的 schema 调用工具,填充正确的字段,并在每一个快乐路径(happy-path)查询中获得正确答案。这个 bug 是隐形的,直到有一天用户输入的内容中,他们经过身份验证的 user_id 与他们询问的 account_id 是两个不同的实体,而智能体自信地返回了别人的数据。

那些被你的智能体“触发并遗忘”的异步工具调用

· 阅读需 11 分钟
Tian Pan
Software Engineer

智能体工具调用抽象失效最明显的标志是:追踪记录(trace)显示该步骤已标记为完成,而下游系统却显示什么都没发生。模型调用了一个工具,收到了一个任务 ID 返回值,将该任务 ID 视为答案,然后继续执行。三分钟后,实际工作要么在无人监听的情况下成功,要么失败并产生一条记录在无人查看的日志中的错误。用户看到的是一份自信的总结;而操作队列看到的却是一个搁浅的任务。

这就是函数调用(function-calling)抽象悄然允许的失效模式。JSON Schema 描述了参数和返回类型,但它们无法区分“此工具返回一个结果”与“此工具返回一个操作回执,你稍后需要查询其结果”。模型对两者一视同仁,因为在规划器(planner)看来,它们看起来是一样的——都是一个带有非错误负载的成功工具调用。

通过了 Schema 验证的虚假工具参数

· 阅读需 9 分钟
Tian Pan
Software Engineer

Agent 调用 fetch_order,参数为 order_id: "ORD-739241"。Schema 接受了它 —— 三个字母、一个连字符、六位数字,完全符合模式。工具返回了 404。Agent 开始含糊其辞,生成了 "ORD-739242" 并再次调用,又得到一个 404,接着又生成了 "ORD-739243"。你的仪表盘记录了三次成功的工具调用和三次干净的 Schema 校验。客户在等待。在追踪记录的某个地方,安全栈的每一层都报告为绿色,而模型正在全速虚构标识符。

团队认为 Schema 拦截了错误。Schema 确实拦截了它能拦截的东西:形状(shape)。它检查了参数是否为字符串,是否匹配正则表达式,以及必填字段是否存在。Schema 无法检查 ORD-739241 是否对应数据库中的真实订单,因为 Schema 根本不知道数据库的存在。这种差距 —— 句法上的合理性与语义上的正确性之间 —— 正是大多数生产环境工具调用 bug 的所在地,而且这种失败非常隐蔽,唯一的信号就是客户的困惑。

填充式工具调用:当智能体在表演勤奋而不是真正干活

· 阅读需 11 分钟
Tian Pan
Software Engineer

打开任何一个生产环境智能体的 trace,看一看在用户提问和第一个真正有用的动作之间到底跑了哪些工具调用。你会看到一个 get_user_profile 返回了一个根本没人用的名字、一个 check_status 返回绿色然后再也没被引用、一个 list_recent_orders 的结果被总结成"ok"然后直接丢掉。这些调用没有一个改变了最终答案。但每一个都花了真金白银的钱、真实的延迟,以及在 trace 里真实占一行。你的智能体已经学会了表演勤奋——而表演勤奋如今是你最大的单一浪费来源。

这就是填充式工具调用:智能体发出一个动作,不是因为它需要那个结果,而是因为"先想一想,再行动"的整体模式在训练时被反复奖励,多到模型现在会把"显得周全"当作回答任何问题的副作用来执行。这是一个 LLM 版本的初级分析师,故意打开五个根本不会读的标签页,好让坐在对面的高级同事看到自己很忙。区别只在于:初级分析师会累。智能体永远不会。

MCP Server 蔓延:无人监管的无边界工具表面

· 阅读需 10 分钟
Tian Pan
Software Engineer

Model Context Protocol (MCP) 正如其初衷所愿:它让赋予智能体(agent)新能力变得几乎零成本。接入日历服务器、数据库服务器、公司内部服务器,或是厂商现今发布的 30,000 个工具目录中的任何一个,都只是一个配置更改,而不是一个开发项目。这种无摩擦感是其特性,但也是问题所在。

正因为添加工具的成本极低,每个团队都在添加工具。数据团队接入了仓库服务器。支持团队添加了工单服务器。有人为了某次性任务连接了文件系统服务器后就再也没移除。这些决策本身都没有错。但没有任何一个决策者对这些工具的“总和”负责——即你的智能体在每次请求中携带的聚合工具表面(tool surface)。工具列表已变成了一个具有实际持有成本的依赖图,而在大多数组织中,这是唯一一个无人负责的依赖图。

结果就是蔓延:工具目录单调递增,无人审查,每季度成本都在上涨,并悄悄地让智能体变得更糟。这就是无人负责的表面,它理应受到和你对待 API 表面以及 npm 树同等程度的审视。

智能体从未接收到的服务降级信号

· 阅读需 10 分钟
Tian Pan
Software Engineer

当下游 API 开始出现波动时,人类操作员在任何事情真正崩溃之前,就会通过十几种方式察觉到。状态页变为黄色。变更日志邮件飞进收件箱。提供商的仪表板上出现警告横幅。值班频道因有人在日志中发现 429 错误而热闹起来。队友发帖询问:“还有人看到写入变慢吗?”这些都不是对请求的响应。它们是围绕 API 的环境运行信号,人类几乎是被动地吸收了这些信号。

调用同一个 API 的智能体(agent)只收到一样东西:它刚刚发出的请求的响应。状态码、Header、Body。这就是全部的渠道。它没有收件箱,没有仪表板,没有 Slack,没有外围视野。它察觉不到最后十个调用每个耗时都是之前十个的两倍。它读不了状态页,因为没人给它 URL,它也没有查看状态页的常规指令。当依赖项降级时,智能体是系统中最后一个知情方——而且它通常是通过失败才知晓的。

这种不对称性并非模型能力问题。更聪明的模型也解决不了。智能体对运行信号是盲目的,因为底层设施(plumbing)从未传递过这些信号,而且大多数智能体架构在出厂时,甚至没人注意到缺失了这些底层设施。