跳到主要内容

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

查看所有标签

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

· 阅读需 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)现在受限于两个互不相识的人独立进行上下文切换的速度。

你的定时 Agent 有四个时钟,而你信任的是错误的那一个

· 阅读需 14 分钟
Tian Pan
Software Engineer

一个每日站会总结被安排在 UTC 时间 09:00。定时任务(Cron)准时触发。两秒钟后,一个工作节点容器组(Worker pod)启动。LLM 调用又耗费了四十秒的往返时间。模型在撰写总结时认为现在是去年的 2 月,因为那是其训练数据最后确信的时间点。工具层在 UTC 时间 09:00:42 根据挂钟时间(Wall clock)发送了 Slack 消息,模型从未提及具体日期,因为没人要求它这样做。消息进入了正确的频道,昨天的站会笔记被总结成了“今天的”,而且整整三周都没有人察觉。

这并不是任何单一组件的 bug。这是一种在四个不同的时钟之间、谁也没有写下来的契约,而这四个时钟都认为自己知道“现在”是什么时候。

你的 Agent 端点是一个伪装成函数调用的分布式系统

· 阅读需 10 分钟
Tian Pan
Software Engineer

现代 AI 应用中最危险的一行代码看起来完全无害:

result = await agent.run(user_query)

它读起来就像一个函数调用。它有名称,接受参数,返回数值。你的 IDE 会自动补全它。你的类型检查器也觉得没问题。然而,就在这个单一的 await 背后,隐藏着一个远程的、多跳的、部分失效的分布式系统,而它却套着本地过程的语法外壳。代码看起来的样子与它实际表现出的行为之间的鸿沟,正是大多数生产环境 Agent 事故发生的地方。

你的重试逻辑正在给 Agent 传达错误的教训

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个工具调用失败了。你的 Agent 框架使用指数退避(exponential backoff)重试了三次。第三次尝试成功了。追踪记录(trace)显示一个绿色的对勾。没人收到报警,错误计数器没有增加,用户得到了他们的答案。根据你所有的仪表盘,系统运行正常。

事实并非如此。工具失败是因为 Agent 传递了一个格式错误的参数,而第三次尝试之所以成功,仅仅是因为 Agent 在每次采样时表现不同,刚好在第三次尝试时正确表述了调用。你并没有从瞬时故障(transient fault)中恢复。你只是在玩老虎机直到它中奖,然后记录下中奖结果,并扔掉了那两次告诉你 Agent 已经坏掉的拉杆记录。

这就是重试逻辑悄悄腐蚀 Agent 系统的方式。重试是为“调用者正确且网络不稳定”的世界设计的。而 Agent 颠覆了这个假设:网络通常是正常的,而调用者才是不可靠的部分。当你把为第一种世界构建的重试策略应用到第二种世界时,它就不再是一种恢复机制,而变成了一种将 Bug “洗”成绿色对勾的手段。

你的智能体从未发送的幂等键

· 阅读需 12 分钟
Tian Pan
Software Engineer

一位客户曾因一次退货收到了三次退款。这不是因为模型幻觉出了某种政策,也不是因为人类填错了表单——而是因为退款工具超时了两次,智能体随之重试了两次,而每次重试都发起了一个全新的请求,导致支付处理器根本无法识别这其实是它处理过的工作。三个干净的 HTTP 200。三次真实的资金转移。智能体完全按照指令行事:当调用失败时,重试。

这个 Bug 不在模型中。Bug 出在一个从未发送的 Header(标头)中。

重试是智能体最自然不过的本能。工具调用返回错误,或者更糟,什么都不返回,而循环系统的直觉——无论是编码在框架、提示词还是模型自身的训练中——就是重新尝试该动作。这种直觉对于“读”操作是正确的,而对于“写”操作则是灾难性的。一个韧性十足的智能体与一个会向客户重复收费的智能体之间的区别,不在于智力高低,而在于每一次改变状态的工具调用是否携带了幂等键(Idempotency Key),以及另一端的系统是否真正履行了它。

当两个 Agent 共享一个工具:多 Agent 系统中的并发 Bug

· 阅读需 11 分钟
Tian Pan
Software Engineer

当你输入“启动另一个智能体来并行处理”的那一刻,你就已经成为了一名分布式系统工程师。你可能没有注意到。框架让它变成了一行代码的改动,演示运行良好,延迟也降低了。但在底层,你刚刚引入了两个在没有协调的情况下读写共享状态的进程 —— 而困扰了数据库领域五十年的每一个竞态条件 (race condition)、更新丢失 (lost update) 和脏读 (dirty read),现在都潜伏在你的智能体堆栈中,伺机而动。

这种情况之所以棘手,是因为故障看起来不像并发 Bug,而像是某个智能体出错了。输出在语法上是有效的,流水线显示绿色,没有抛出异常 —— 然而,客户被收取了两次费用,或者文件丢失了一半预期的内容,又或者一个智能体自信地根据另一个智能体已经覆盖的数字采取了行动。你去调试那个“愚蠢的智能体”,发现它的提示词 (prompt) 没有任何问题,因为提示词根本就不是问题的症结所在。

工具延迟尾部:为什么 p99 重塑了智能体架构而 p50 掩盖了问题

· 阅读需 11 分钟
Tian Pan
Software Engineer

我上个季度合作过的一个团队发布了一个包含七个步骤的智能体(agent),并以显而易见的方式构建了其延迟预算:搜索返回耗时 200ms,SQL 查询耗时 80ms,电子邮件发送耗时 150ms,链条中的其他环节以此类推。将中位数相加,再加入一些缓冲,数学计算表明该智能体能轻松地保持在两秒的 SLA 之内。仪表板连续几周证实了这一点。中值延迟(median latency)表现优异。接着,客户开始抱怨该功能慢得无法使用,而仪表板看起来依然是代表正常的绿色。

他们互相转述的故事是错误的,因为他们是围绕 sum(p50) 构建的架构,而用户体验到的却是 sum(p99)。经过三到四个步骤后,链条中的 任何 环节掉入其自身尾部概率(tail probability)的可能性就不再微不足道了。经过七个步骤后,这种概率接近于掷硬币。没有任何单项工具的仪表板变红,因为没有任何单项服务表现异常 —— 问题在于没有人负责这种乘法复合(multiplicative composition)效应。

这并不是什么新教训。分布式系统研究人员已经为此撰写了四十年的文章。新鲜的是,每个构建智能体的团队都在最后期限的压力下,痛苦地重新发现这一点。

解读智能体堆栈跟踪:在模型、工具与 Harness 之间定位故障

· 阅读需 11 分钟
Tian Pan
Software Engineer

用户报告 Agent 给出了错误答案。你打开 Trace。模型的推理过程看起来没问题。工具调用全部返回 200 OK。Harness 日志显示没有重试、没有截断、没有异常。然而,答案就是错的。于是你花了接下来的两个小时,将三个具有不同格式、不同时钟的独立日志流缝合在一起,最终发现某个工具针对特定的查询形状静默返回了 {"result": null},模型将这个 null 合理化为一个听起来合乎逻辑的事实,而 Harness 则愉快地将这个幻觉转发给了用户。这三个层级中的任何一个都没有单独记录任何警报。故障发生在连接处。

这是生产级 Agent 系统中最主要的故障模式,而大多数团队都在使用单层工具进行调试。模型团队归咎于工具。工具团队归咎于模型。平台团队归咎于 Harness。每个人都部分正确,因为 Agent 故障几乎从来不是单一组件的 Bug —— 它是三个组件之间的失配,而每个组件都在不同的“步骤”心理模型上运行。在你的 Trace 基础设施反映这一现实之前,你将不断为披着不同外衣的同类事故买单。