跳到主要内容

65 篇博文 含有标签「distributed-systems」

查看所有标签

那个因等待另一个 Agent 而死锁的 Agent

· 阅读需 10 分钟
Tian Pan
Software Engineer

一个研究员智能体向一个检索智能体请求一份文档。检索智能体在任务执行到一半时,决定需要研究员智能体澄清查询意图后才能继续搜索。而正在等待文档的研究员智能体,在拿到文档之前不会做出响应。它们谁都没有出错,也没有陷入死循环。它们只是都在礼貌地、无限期地等待着对方——而你的编排器(orchestrator)根本没有“这两个智能体正在互相阻塞”的概念,它会一直愉快地维持这个状态,直到你从未配置过的超时设置最终触发,或者直到某个大活人注意到这个运行已经“进行中”整整 40 分钟了。

这就是死锁(Deadlock)。它是计算领域最古老的故障模式之一,且与你的模型有多聪明毫无关系。它是工作协调方式的一种属性,而不是工作执行方式的属性。过去一年多智能体研究中一个令人不安的发现是:在智能体集群中,大部分出问题的地方都在这里,即协调层,而不是任何单个智能体的推理能力。

单智能体思维永远无法暴露这些 Bug。当一个模型运行工具调用循环时,它最坏的情况也就是空转——而空转至少是肉眼可见的。一旦你拥有两个或更多可以互相等待的智能体,你就继承了分布式系统所有的病理特征:循环等待、活锁、丢包、过早终止、共享状态下的竞态条件。没有人会坐下来决定构建一个分布式系统,但在你添加第二个智能体的那一天,你就已经构建了一个分布式系统。

你的智能体从未执行过的补偿事务

· 阅读需 11 分钟
Tian Pan
Software Engineer

当你的 Agent 执行退款、发送邮件、关闭工单或写入数据行时,该操作便脱离了系统并进入了现实世界。现实世界没有回滚按钮。客户已经看到了退款,收件人已经阅读了邮件。而当 Agent 在三步之后走错了方向,你的恢复计划通常只是复盘会议中的一句话:“我们告诉它以后不要再那样做了。”

“以后不要再那样做”不是撤销(undo)。它是对未来的承诺,却被用来解决过去的问题。令人不安的事实是,大多数 Agent 架构根本没有撤销已完成副作用的机制——不是机制不好,而是根本没有机制。Agent 可以规划、调用工具并重试,但它无法回头。它只有前进挡,没有倒车挡。

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

· 阅读需 10 分钟
Tian Pan
Software Engineer

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

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

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

你的 Agent 链路中无人分配的延迟预算

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的 Agent 有一个延迟 SLO。有人把它写在了文档里:“响应时间低于 8 秒,p95”。但没人决定这 8 秒该如何分配。检索调用没有细分预算,规划步骤没有细分预算,模型因为感到不确定而决定调用的第三个工具也没有细分预算。预算在边界处只是一个数字,而在内部则完全不存在。因此,当一个五跳链路冲破 8 秒时,值班工程师盯着追踪链路(trace),却无法回答那个唯一重要的问题:到底是哪一跳超时了?

这就是拥有“延迟预算”的服务与拥有“延迟愿望”的服务之间的区别。预算是分配给每个组件并强制执行的;而愿望是在出口处测量并祈祷达标的。大多数 Agent 系统发布时都带着愿望,因为跳数结构是动态的——模型决定调用多少次工具——而且为无法控制的事情制定预算感觉是不可能的。事实并非如此。正因为你无法控制它,你才更需要为它制定预算。

429 错误背后的惊群效应:速率限制是一个分布式系统问题

· 阅读需 13 分钟
Tian Pan
Software Engineer

调出你上次遇到持续 429 错误时的请求日志。你可能会发现一些奇怪的现象:错误并不是以稳定的流形式出现的。它们成波浪式出现——一波 429 爆发,接着是一段安静的间隔,然后是更大的爆发,再接着是另一个间隔。服务商的配额在波动期间并没有改变。你的流量也没有激增。你所看到的其实是你自己的重试逻辑在自我同步。每个在第 0 秒失败的客户端都计算了相同的退避(backoff)延迟,睡眠了相同的时长,并在同一瞬间醒来,然后再次共同失败。

这就是“惊群效应”(thundering herd),而最讽刺的是,标准的修复方案——指数退避(exponential backoff)——并不能阻止它。确定性的指数退避“组织”了惊群。它将一群几乎在同一时刻失败的客户端聚集起来,并让他们步调一致地前进:所有人都在 1 秒后重试,然后所有人都在 2 秒后,再然后是 4 秒。负载峰值的间隔变得更远了,但每个峰值的高度依然和第一个一样。如果正是这个峰值触发了你的速率限制,那么你实际上构建了一个永远重新触发它的节拍器。

你的智能体需要的是监督者,而不是重试循环

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的 Agent 在一个包含十二个步骤的任务的第七步挂掉了。框架捕获了异常,经过指数退避(exponential backoff)后进行了重试。它重试了该步骤 —— 但使用的是同样的上下文窗口(context window),其中已经堆积了三次失败的工具调用、一段解析了一半的错误消息,以及一个模型已经放弃的计划。当然,重试也失败了,因为重试是一场赌注,赌的是世界发生了变化,而那个 Agent 的世界没有任何改变。需要改变的是 Agent 的状态 —— 而任何 Agent 框架中的重试策略都不会做出这种决定。

Erlang 的 OTP 库在三十年前就为必须运行数十年的电话交换机制定了这项决策。主管树(supervisor trees)背后的洞察力从来不是“在崩溃时重启”。而是“如何恢复”是一个与“执行工作”完全解耦的关注点,它由一个独立的进程负责,并排列在一个层级结构中,每一层都对恢复的含义有更深一点的理解。现如今的大多数 Agent 框架将重试强加到单个调用上,这就像在电话交换机的每一行代码周围都套上 try/catch 一样。它们真正需要的是这种层级结构。

在你的智能体能够自我重试之前,精确一次性处理(Exactly-Once)曾是一件难事

· 阅读需 10 分钟
Tian Pan
Software Engineer

我们花了二十年的时间来教导服务如何安全地进行重试。这个方案已经非常成熟了:客户端生成一个唯一的幂等键 (idempotency key),将其附加到请求中,服务器在执行工作的同一个事务中记录该键及其结果。掉线、超时、500 错误 —— 客户端使用相同的键进行重试,服务器识别出该键,并返回记录的结果,而不是再次扣款。Stripe 多年前就推出了这种模式,它已成为任何涉及资金业务的 API 的基本要求。

整个设计都基于一个无人提及的假设:调用者会逐字节地重复其请求。 重试携带相同的键,是因为重试是同一段代码路径使用相同的变量重新执行。一旦打破这个假设,整个方案就会悄无声息地失效。

代理墙钟预算:一场与工具超时机制的赛跑

· 阅读需 12 分钟
Tian Pan
Software Engineer

有一种 Agent 漏洞,当你孤立地观察任何单个组件时,它都不会出现。模型没问题,工具没问题,重试策略也没问题。纸面上的超时值甚至可以说很慷慨。然而,一个通常在 8 秒内完成的工具,却总是在一个已经在 7.9 秒时将其宣告为失败的 Agent 面前折戟。Agent 围绕一个从未发生过的“错误”重新规划,并启动了第二次调用,而第一次调用的结果即将与其发生碰撞。

漏洞不在任何一个框框里。它存在于两个没人同意应该同步的时钟之间的缝隙中。

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

· 阅读需 13 分钟
Tian Pan
Software Engineer

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

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

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

· 阅读需 11 分钟
Tian Pan
Software Engineer

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

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

缓存击穿:这次冲击的是你的模型提供商,而不是数据库

· 阅读需 11 分钟
Tian Pan
Software Engineer

传呼机在 UTC 时间 14:02 响了。不是因为延迟,也不是因为错误——而是因为开销。费用仪表盘显示出一条垂直线:三分钟的输入 Token 计费大约是过去一小时平均水平的九倍,然后又恢复了正常。没有发布回归版本。没有新租户上线。流量精确到分钟来看都是平稳的。唯一改变的是,一个单一的 Prompt 前缀——集群中每个 Agent 共享的 14K Token 系统消息——在提供商端悄悄过期了,一千个 Worker 全都在同一个 200ms 的窗口内认定,自己就是那个需要将其写回的人。

这就是缓存雪崩(Cache Stampede)。这是自 2003 年 memcached 发布以来,运维人员一直在写事故复盘报告的那个老问题。2026 年的新变化在于,发生雪崩的缓存不再属于你。它存在于你的模型提供商内部,你无法检查其状态,而且每一次未命中(Miss)消耗的是真金白银,而不仅仅是几次额外的数据库查询。数据库工程师在二十年前就学会通过抖动(Jitter)来化解的同步 Bug,已经悄然出现在了一个没人想过要防御的账单细目中。

悬在两张日历上的多智能体死锁

· 阅读需 11 分钟
Tian Pan
Software Engineer

Agent A 向 Agent B 请求完成其任务所需的一项数据。Agent B 在回答之前,向 Agent A 请求生成该数据所需的一项上下文。这两项请求在发出途中都跨越了“需要人工审核”的边界。第一项请求落入了由 Priya 监看的 Slack 审批频道。第二项落入了由 Marcus 监看的 Jira 队列。Priya 正在吃午饭。Marcus 正在接听客户电话。两人都不知道对方的存在。工作流挂起了 19 个小时,直到一次客户投诉迫使有人去询问为什么汇总结果从未送达,才有人注意到。

这不是一种新颖的失败模式。它是分布式系统中最古老的失败模式,只是换了一身新装。Coffman 条件——互斥(mutual exclusion)、占有且等待(hold and wait)、不可剥夺(no preemption)、循环等待(circular wait)——早在 1971 年就被命名了,而一个带有“人机协同”(human-in-the-loop)审批队列的多 Agent 系统默认满足这所有四个条件。新的麻烦在于,死锁中的“资源”之一是人的注意力,这意味着你的活性保证(liveness guarantee)现在受限于两个互不相识的人独立进行上下文切换的速度。