首页 / 文章 / 我们用SigNoz为AI智能体集群插桩,其遥测数据揭示我们原先的判断全错了。
← 返回
AI技术

我们用SigNoz为AI智能体集群插桩,其遥测数据揭示我们原先的判断全错了。

✍️ zhirenhun 📅 2026/8/1 👁 207 阅读 ⏱ 34 分钟
我们用SigNoz为AI智能体集群插桩,其遥测数据揭示我们原先的判断全错了。

为2026年7月的WeMakeDevs Agents of SigNoz黑客松而构建。

DevSwarm任务控制:群图、实时追踪河流以及它构建的所有内容的机库
任务控制。图即群,下方的河流即实时span流,每个条柱都深度链接到SigNoz中的对应追踪。

DevSwarm将一个提示词转化为一个可运行的全栈应用。五个开放权重模型负责规划、构建、审查并修复它们自己的路由。它产出的任何东西都不会被盲目信任,每一步都是SigNoz中的一个OpenTelemetry span,包括出错的步骤。

我们首先构建了可观测性,期望它能证明系统是正常工作的。

它做了更有用的事情。它花了一周时间证明我们对自己系统的几乎所有认知都是错误的。我们把我们自己设定的限制归咎于某个模型。我们把供应商的中断归咎于该模型。我们以为审查代理是我们最强的一环,而实际上它是可衡量的最弱一环。我们花了数天编写一个设计系统,而最终测量结果显示它反而使输出更差。

这些发现没有一条是通过重读代码得到的。每一条都来自一个span事件、一行仪表板或一个与我们观点相左的基准测试。

所以这不是一篇架构文章。而是遥测六次告诉我们错了,并附上查询语句。

当前数字全部实时从SigNoz读取,而非手动键入幻灯片:22次生成、8个模型上的189次带追踪模型调用、285万token、225次批评者捕获、19次回退提升、以及24个生成应用,每个都用自己的服务名上报。

智能体群的实际构成

五个角色,每个角色都使用在该任务中表现最佳的开放权重模型:

角色 模型 职责
规划器 GLM-5.2 将提示词转化为类型化的构建计划和锁定的API契约
前端 GLM-5.2 根据该契约生成一个自包含的index.html
后端 Qwen3-Coder-480B 根据同一契约生成一个Express服务器
评论者 Kimi-K2.7-Code 审查前后端,控制合并,将捕获的问题路由回其所有者
医生 GLM-5.2 读取智能体群自身的追踪并修复其模型路由

DevSwarm落地页
我们落地页上的每个数字都是对追踪存储的实时ClickHouse查询。从结构上就杜绝了与遥测数据脱节的营销文案。

所有内容都通过Hugging Face推理提供商提供服务。系统中没有一次封闭模型API调用,这一点后来证明很重要,原因出乎我们意料(参见下文提供商部分)。

批评者(Critic)是承重构件。前端和后端从同一份契约并行生成,然后由一个独立模型对两者进行审查,检查契约一致性、安全性和运行时缺陷。真实捕获到的问题会被路由回负责该部分的智能体,由该智能体修补自己的文件,随后批评者仅重新审查差异部分。经过两轮重新生成后,无论结果如何,都会附上一份诚实的判定并交付。

同样的审查关卡也运行在变更上。对已完成的应用程序提出"在添加书籍时可为每本书设置星级评分"这样的请求,会依据存储的契约重新规划,仅重新运行该指令所涉及的智能体,并将结果送入相同的审查流程。一次优化请求本身就是一个独立的根链路(root span),因此变更请求在事后与之前的构建一样可追踪。

为什么我们先做可观测性,再做优化

多智能体系统的失败方式与单模型工具截然不同。一次调用可能成功,却产出不可用的产物;一次回退可能顺畅地挽救请求,以至于没人发现主路径已经失效;延迟可能因为某个角色悄然多思考了一倍时间而变成原来的三倍。这些都不会出现在请求日志中。

因此,这个项目中第一个真正跑通的并不是代码生成,而是一条链路追踪(Trace)。

SigNoz 通过 Foundry 自托管部署,只需单一配置文件即可完成安装。我们的 casting.yamlcasting.yaml.lock 已提交到代码仓库中,任何人都可以复现该部署,包括评审人员在内。

三个信号,各自承载什么

链路追踪(Traces)。 每一次模型调用都是一个名为 llm.<role> 的 span(跨度),携带 GenAI 语义约定:

span.setAttributes({
  'gen_ai.operation.name': 'chat',
  'gen_ai.request.model': model,
  'gen_ai.usage.input_tokens': usage.prompt_tokens,
  'gen_ai.usage.output_tokens': usage.completion_tokens,
  'devswarm.role': role
});
进入全屏模式 退出全屏模式

两个span事件承担了繁重的诊断工作。fallback_promotion记录主模型失败、由哪个模型接管以及失败的确切原因。critic_catch记录审查agent发现的每个问题,包括其目标和严重级别。两者特意作为事件而非独立的span存在:它们归属于所描述的调用,并且即使在调用最终成功时也会保留在追踪中。

指标。六个计数器和一个直方图,因为有些问题是时间序列问题而非追踪问题:devswarm.tokensdevswarm.llm.callsdevswarm.llm.durationdevswarm.fallback.promotionsdevswarm.critic.catchesdevswarm.generationsdevswarm.refinements。按角色、模型和结果进行标记。

日志。针对事件期间人类需要阅读的内容提供结构化记录:一次fallback提升事件、一次doctor诊断、一次带有判定结果和捕获计数的生成完成记录。与追踪使用相同的资源属性,因此日志行和span能够对齐。

整个追踪层现已提取为一个小型库,otel-swarm,任何多智能体系统都可以直接接入。DevSwarm将其作为真实依赖来使用,这意味着如果库出现问题,我们自己的仪表板会先变暗。这感觉像是发布它的诚实方式。

SigNoz中的一次生成追踪,展示嵌套的agent和llm span
一次生成以火焰图形式呈现。先是Planner,然后前端和后端并行,最后是critic。

SigNoz中的结构化日志,展示fallback提升和生成判定结果
一次糟糕运行期间的日志流。WARN行是fallback提升事件,每一行都指明了失败的模型及其原因。

从ClickHouse中读取swarm

五个仪表盘,全部以JSON形式提交在observability/dashboards/中。我们实际使用的是Command Center:它显示swarm是否健康,如果不健康,哪个角色出了问题。比较特别的是Born Observable,它完全不包含swarm数据,只有生成的应用程序以它们自己的服务名进行上报。

有三个查询值得分享,因为ClickHouse中的span事件第一次使用时不太明显。

角色健康状态,直接从trace存储中获取:

SELECT attributes_string['devswarm.role'] AS role,
       count() AS calls,
       countIf(statusCode = 2) AS errors,
       round(quantile(0.95)(durationNano) / 1e9, 1) AS p95_s,
       sum(attributes_number['gen_ai.usage.input_tokens']
         + attributes_number['gen_ai.usage.output_tokens']) AS tokens
FROM signoz_traces.distributed_signoz_index_v3
WHERE serviceName = 'devswarm' AND name LIKE 'llm.%'
GROUP BY role ORDER BY calls DESC
进入全屏模式 退出全屏模式

随时间推移的回退促销,这需要访问事件数组:

SELECT toStartOfInterval(timestamp, INTERVAL 30 MINUTE) AS ts,
       attributes_string['devswarm.role'] AS role,
       count() AS value
FROM signoz_traces.distributed_signoz_index_v3
WHERE serviceName = 'devswarm'
  AND arrayExists(e -> e LIKE '%fallback_promotion%', events)
GROUP BY ts, role ORDER BY ts
进入全屏模式 退出全屏模式

最费时弄明白的,是读取事件内部的内容,这样你就可以询问审查门实际捕获了什么,而不是它捕获了多少次:

SELECT JSONExtractString(e, 'attributeMap', 'severity') AS severity,
       JSONExtractString(e, 'attributeMap', 'target') AS target,
       count() AS catches
FROM signoz_traces.distributed_signoz_index_v3
ARRAY JOIN events AS e
WHERE serviceName = 'devswarm' AND name = 'agent.critic'
  AND JSONExtractString(e, 'name') = 'critic_catch'
GROUP BY severity, target ORDER BY catches DESC
进入全屏模式 退出全屏模式

答案,在过去一周:221个高,46个中,10个低,而前端智能体承担了其中70%的问题。那一行数据改变了我们对整个智能体集群的看法。系统中负责生成标记和客户端状态的那一半是bug的温床,而不是接触数据库的那一半。

我们庆幸的一个设计决策是:我们自己落地页上的营销数据也是在请求时从这些相同查询中获取的。页面不可能与遥测数据脱节,因为两者只有一个共同来源。

SigNoz 中的 DevSwarm 指挥中心仪表盘
指挥中心。顶行回答“智能体集群是否健康”,角色健康表回答“哪个角色”,而回退图表则应趋近于零。

SigNoz 中的 LLM 经济仪表盘
LLM 经济学:按角色和按模型的令牌与延迟,正是这个图表让我们发现前端角色将三分之二的预算烧在了推理上。

一个应用的实际成本

下表中的每个令牌都来自一个 span。这是一次真实运行的结果,即上面的凸版印刷网站,价格按 Hugging Face 路由器为我们使用的提供商所报告费率计算。

角色 模型 输入 输出 成本
前端 GLM-5.2 27,855 38,888 $0.2101
评审 Kimi-K2.7-Code 50,996 14,539 $0.1066
规划 GLM-5.2 670 4,090 $0.0189
后端 Qwen3-Coder-480B 2,800 3,778 $0.0069
82,321 61,295 $0.34

一个带有可用 Express 后端、一个验证邮箱并拒绝重复的等待名单,以及自身 OpenTelemetry 接线的设计营销网站,成本为三十四美分。在通过的运行中,成本范围约为 18 美分到 56 美分。

表中两件事让我们感到惊讶。

评审者的成本是后端编写者成本的十五倍。审查代码比编写代码昂贵得多,因为审查意味着要完整阅读两份产物各两遍,而后端代理只写一次文件。没有人为此做预算。如果你要在代理系统中构建审查门禁,它不是生成之上可以忽略的舍入误差,而是你账单的三分之一。

而且一次失败的运行比一次成功的运行花费更高。我们最差的生成烧掉了 232,000 个 token,触及了再生成上限,而最干净的一次只用了 74,000 个。因此收敛不仅是一个质量指标,它也是成本指标。修复第五个发现中的合约格式错误,对我们的单位经济性的改善超过我们所做的任何模型替换。

唤醒代理而非人类的告警

这是整个构建中我们最自豪的部分,而且它确实是少量的代码。

两条告警规则位于 observability/alerts/ 中:一条是回退使用量激增,另一条是评审者捕获率持平线。两者都会通知一个名为 swarm-doctor 的 webhook 频道,该频道指向 swarm 自身的 POST /api/doctor/webhook

当告警触发时,Doctor 会醒来,查询 swarm 自身最近一小时的追踪数据,并决定如何处理路由表。它会提升一个备份模型、重置一个已恢复的主模型,或者什么都不做,然后用它刚读到的数字以通俗易懂的英语解释自己的决定。它的第一次真实诊断,逐字如下:

“评审者角色是明显的问题区域:它的主模型在 9 次调用中触发了 6 次回退提升(67%),错误率为 22%,p95 延迟为 242 秒。后端、规划者和医生都健康。”

Doctor 自身的模型调用也被追踪了,所以治疗者与患者具有同等的可观测性。

Mission Control 显示 Swarm Doctor 的诊断面板
Doctor 报告一个健康的 swarm。它读取了自己 180 分钟的追踪数据来得出这一结论,并且它对样本量很诚实:“调用量非常低,因此延迟数据在统计上没有意义”。

两条来之不易的 SigNoz API 注意事项,因为我们在两者上都损失了数小时:

告警规则必须针对 /api/v2/rules 创建,并带有 schemaVersion: v2alpha1、一个 notificationSettings 块和至少一个频道。v1 端点会接受请求并返回 "alert rule is not valid",但不指示哪个字段有误。相比之下,仪表板则发送至 /api/v1/dashboards,带有 SIGNOZ-API-KEY 头,并且行为与文档完全一致。

另外:冷启动的 Docker 重启可能会让 ClickHouse 副本保持只读状态,直到 Keeper 重新连接。它通常在一分钟内自愈。如果没有,每个表的 SYSTEM RESTORE REPLICA 可以清除该状态。

天生可观测的应用

swarm 生成的每个应用都自带可观测性埋点。除了 index.htmlserver.js,每个生成的文件夹还会得到一个 otel.mjs 引导文件、一个 package.json,以及一个限定于该应用自身服务名称的 signoz-dashboard.json。如果配置了 SIGNOZ_API_TOKEN,仪表板会在生成时就在 SigNoz 中创建,早于用户打开预览之前。

因此,生成的应用程序在创建后几秒钟内就会作为自己的服务出现在SigNoz中,带有RED指标和路由表。目前,我们的实例中有二十四个这样的服务。

值得一提,因为人们往往会误以为:这个流程中完全没有图像模型。该集群在其构建的26个应用中生成了227个内联SVG元素,平均每个应用8.7个,而且每一个都是由语言模型以标记语言编写的。上图中的Vandercook印刷机是手绘SVG,不是生成的图像。我们唯一生成过的图像资产是DevSwarm自己的favicon和社交卡片,它们是该工具的品牌标识,而不是集群产生的任何东西。

Quoin and Roller,一个由一个句子生成的凸版印刷网站
其中一个输出。Vandercook印刷机是模型自己绘制内联SVG,这个应用从启动那一刻起就以自己的服务名向SigNoz报告。

我们不得不艰难地吸取一个教训:应用只在后端实际运行时才会发出span。早期,我们的预览静态地提供前端,因此生成的Express服务器从未启动,应用默默地回退到localStorage。预览看起来完美,但服务页面几乎为空。这种不匹配——本应有几十个span却只有两个——暴露了这个错误。现在,预览将真实服务器作为子进程生成并代理到它。

我们自己的遥测告诉我们的六件错事

这是我最想读的部分,所以它是最长的。

1. 一个“模型限制”是我们自己写的一个过时的常量。

跟踪信息显示每个前端故障都是finish: length,导致整页HTML被截断。我们得出结论,GLM-5.2的提供商将完成次数限制在16384个token,于是将该角色移到了另一个模型。这个限制在我们发现时是真实的。但它也存在于我们自己的配置中,当提供商后来取消了限制时,我们的常量仍然强制着一个不再存在的限制。平均每个前端工件需要约19,000个输出token。我们实际上保证了截断,却怪罪模型整整两周。

2. 真正的原因是提供商轮盘赌,只在span事件文本中可见。

在移除我们自己的限制后,GLM仍然间歇性地以400错误失败:max_completion_tokens is limited to 16384 for glm-5.2。Hugging Face路由器在提供该模型的每个提供商之间进行负载均衡,而它们的限制并不一致。我们探测了全部七个:scaleway限制为16384,featherless为32768,novita和zai-org为131072,而together、fireworks-ai和deepinfra接受200000或更多。在没有固定的情况下,大约每七个请求中就有一个命中严格的提供商并立即失败。将模型固定到一个提供商后,我们首次实现了零回退的生成。这整个诊断来自于一个fallback_promotion事件上的reason属性。

3. 我们的span属性隐藏了我们最想看到的失败。

当主模型失败时,我们的代码会用备用模型的名称覆盖 span 上的gen_ai.request.model。这看起来很整洁。这意味着仪表板上的每一行都将主模型的失败及其浪费的延迟归因于事后清理的备用模型。我们花了整个下午确信评论模型的备用模型既慢又容易出错。在基准测试中将它们隔离开来后,结果恰恰相反:备用模型 7 秒就完成了,问题出在主模型身上。如果你要从这篇文章中带走一个实现细节,那就是这个。记录你尝试过的模型,并把这次切换作为事件记录,而不是覆盖在你之后要用来分组的属性上。

4. 我们的评审关卡是整个集群中最弱的模型。

我们从未对评论模型进行过基准测试,于是我们构建了一个:一个包含三个已记录契约缺陷的生成应用,每个模型运行三次,按缺陷召回率评分。我们现有的主模型 DeepSeek-V4-Pro 在 9 个缺陷中找到了 2 个。其中一次运行烧掉了全部 32768 个 token 的预算,返回了无法解析的内容。Kimi-K2.7-Code 在 9 个中找到了 8 个,且多次运行表现一致。数据中最清晰的规律是:在评审任务中,推理量与缺陷召回率成正比——那些简洁的模型用不到 250 个输出 token 就作答,却漏掉了真正的缺陷。

5. 20% 的通过率源于契约中缺失的一句话。

我们的计划只指定了字段名和类型,却从未指定格式、范围或可空性。于是后端拒绝了rating: 0,而前端把 0 作为默认值发送;后端要求YYYY-MM-DD格式,前端却发送完整的 ISO 字符串。连续三代生成都恰好在这一类不一致上触发了重新生成上限。解决办法是让规划器为每个字段编写一条具有约束力的规则字符串,例如"integer 0 to 5 inclusive, where 0 means unrated and is a valid value"。两个构建器现在读取的是同一句话。捕获的问题从 9 个降到了 3 个,下一次运行就通过了。

6. 我们的设计系统让输出变得更糟了。

我们精心编写了一份设计指南,让生成的应用看起来不像生成的应用。然后我们做了测量:相同的模型、相同的提示词,唯一的变量是指南是否附加。没有指南时:14 个内联 SVG、3 个动画和一套精心搭配的字体组合。有指南时:5 个 SVG、1 个动画和 Courier New。这是我们自己的三条规则造成的。"系统字体栈就够用了"告诉模型不必费心选择字体。"删掉任何不为主题服务的动画"被理解为剥离装饰的许可。而我们的前端提示词禁止所有外部请求,这变相禁止了 Google Fonts——即使它想选一款真正的字体也选不了。我们写了一份禁止清单,它擅长防止糟糕的输出,却不擅长产生好的输出。

使用系统字体和彩虹色卡片颜色的生成书架应用
之前:来自系统字体栈的等宽标签,以及后端随机生成的卡片颜色。

相同的提示词生成了一个带有 Fraunces 字体和手绘 SVG 书架的书架应用
之后。相同的模型,相同的提示词,从我们的指南中移除了三条规则:Fraunces 展示字体、手绘 logo 标记、带有实时计数的筛选标签,以及书籍以书脊形式渲染在书架上。

这还有第二层含义。指南中有一个很大的部分用于营销网站,涵盖滚动显现、入场序列和分层深度,而给应用留下的部分却小得可怜。这个集群构建的每个应用都被刻意置于比每个网站更朴素的标准之下,没有人注意到这一点,因为网站看起来确实很棒。

这是如何构建的

DevSwarm 是使用 Claude Code 构建的,这值得直接说明,而不是留作推断。一个 AI 编码代理帮助构建了另一个 AI 编码代理,而黑客马拉松规则要求参赛者声明对辅助工具的使用,所以在此声明。

这也与本帖的主旨相关。上述每一项发现最初都是我们(无论是人类还是 AI 助手 alike)所持有的一个自信的错误信念。过时的 token 上限、我们曾归咎于供应商限制的模型、我们曾认为是自己最强一环的审查关卡、我们确信正在起效的设计系统。所有这些都不是靠对代码进行更深入的推理来解决的。它们是由一个 span 事件、一个仪表板行或一个与我们意见相悖的基准测试来解决的。

这就是为什么应该尽早为代理系统添加可观测性。当你与代理(以及作为一个代理)一起构建时,遥测数据是对话中唯一没有立场需要辩护的参与者。

这个构建中有哪些值得借鉴之处

如果你正在为代理系统添加可观测性,有三件事立刻得到了回报。

将原因文本放在 span 事件中。不是代码,不是枚举,而是实际的服务提供商错误字符串。我们最严重的两个 bug 都通过读取该字段得以解决,而这两个 bug 在指标中都无法显现。

永远不要覆盖你打算用于分组的属性。要添加,不要替换。

让你的产品读取自身的遥测数据。我们的落地页统计、我们的 Doctor 诊断和我们的仪表板都对同一个 trace 存储运行相同的查询。这消除了一整类漂移,并使你的可观测性技术栈从调试工具变成了一个功能。

令人不适的部分

以上所有发现的共同主线都是一样的,而我对这个主线的思考超出了我的预期。

这些问题中没有一个是难题。一个过时的常量。一个限制不同的服务提供商。一个在错误位置被覆盖的属性。合同中缺失的一句话。风格指南中三条过于谨慎的规则。如果当时我们知道,任何一个都只需五分钟就能修复。但它们加在一起让我们耗费了大半周的时间,并且从外部看起来,就像是模型在让我们失望。

事实并非如此。每一次,模型都完全按照我们的配置来执行。失败总是发生在模型上游,在我们已经写出但不再关注的地方。

我认为这才是用智能体构建的真正教训,而且这并不令人舒适。调试技能不是提示工程,而是愿意相信你的可观测性数据,而不是你对自己三天前配置的记忆。我们之所以能发现问题,只是因为遥测数据不断产生数字,让我们的解释站不住脚。

如果你正在构建类似的东西,我真的很想知道你的经验是否相符。我怀疑很多“模型不够好”的说法,实际上是因为“我的配置已经过时,而我却看不到它”。

链接

仪表盘、告警规则和Foundry的casting.yaml都位于仓库的observability/目录下,因此SigNoz这边的整个流程都是可复现的,而非仅靠文字描述。

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

ai observability opentelemetry showdev
← 上一篇
使用 Kotlin Agent 开发工具包(ADK)构建 AI 智能体
下一篇 →
氛围编程:终局

📌 相关推荐

停止相信仅文本代理排行榜:来自 Cua-Bench 和 Factorio 的教训
2026/8/26
Agent Memory 有两种不同含义,回答引擎给出的却是错误的那一种
2026/8/26
LLM的止境:AI辅助VAPT流水线的确定性评分
2026/8/22
← 返回文章列表