TL;DR:对于一个小型物流 SaaS,我会从一个狭窄的自定义仪表盘开始,将 AI 智能体成本与延迟、完成率和队列深度放在一起。每次智能体轮次输出一条指标记录,将租户和货运标识符排除在标签之外,并按工作流和模型进行聚合。Plausible 更适合隐私友好的 Web 分析,PostHog 适合产品行为分析,Grafana Cloud 适合严肃的运维探索。当一个屏幕必须混合产品 KPI 和后端计数器,而又不想引入完整的可观测性栈时,一个简单的指标 API(应用程序编程接口) 就有其价值。
决定性因素是归因,而不是最漂亮的图表。一个承运商报价智能体一天花费 18 美元,这对我说明不了什么。我需要知道是哪个工作流花掉了这笔钱、完成了多少任务,以及是重试还是缓慢的工具调用导致了这一数字。这就是运维仪表盘和虚荣仪表盘之间的区别。
对于自定义 API 路径,Infrai 的实际优势是在后端能力之间使用单个 API 密钥、一份账单和一个纯 REST API,因此无需安装 SDK(软件开发工具包)。该 API 真正是自描述的:其公共发现接口无需密钥,并返回某个能力的请求模式、响应模式和可运行示例。这缩短了接线工作。它并不提供产品分析模型。
从决策开始。在这种情况下,运维人员需要查看报价生成是否健康,而支付模型账单的人需要知道每个已完成报价的成本。这些视图共享数据,因此过早拆分产品事件和后端指标会产生对账工作。
我会在第一个屏幕上放六个数字:智能体轮次、已完成工作流、失败工作流、p50 延迟、p95 延迟和模型成本。队列深度应放在它们旁边,因为低请求延迟可能掩盖不断增长的积压。注册和转化可以加入同一个仪表盘,但它们应保持为独立的指标名称,而不是附加到每个智能体轮次上的维度。
const costPerCompletedWorkflow = completedWorkflows === 0 ? null : totalCostUsd / completedWorkflows ;
这个 null 很重要。当没有任何完成时报告零,会让一个损坏的智能体看起来是免费的。
避免高基数标签。workflow: "carrier_quote" 是有用的。货运 ID、提示词、电子邮件地址或客户名称则不是。它会提高指标系统中的存储和查询成本,并将原本适度的隐私面变成个人或商业敏感数据。对于欧盟/美国管理面板,仅当内部区域或计划队列能回答真实运营问题时才使用它们。
一个人每周发布,无法维护三个重叠的“已完成”定义。每小时收入视角很直接:花在对账分析事件、应用程序计数器和发票导出上的时间,就是没有花在改进工作流上的时间。
因此指标契约优先。成本必须来自产生它的模型响应或计费元数据;不要根据硬编码的价格表来估算。延迟应使用单调时钟。重试是一个单独的轮次,但属于同一个工作流执行,这使仪表盘能够暴露重试成本,而无需将执行 ID 放入指标标签。
还有一个边界。指标是聚合数据。它们不足以证明“为什么货运 8F3 失败了?”保留日志或错误事件用于该调查,并在可用时用 trace 和 span ID 将它们连接起来。指标图表不是分布式 span 树。
首先读取指标能力,而不是猜测其负载。这个 TypeScript 程序向自描述 API 请求确切的 metrics.report schema 和可运行示例。将 INFRAI_BASE_URL 设置为 API 的版本化基础 URL,并将密钥保存在 INFRAI_API_KEY 中;URL 不应纳入源代码管理。发现接口是公开的,但使用与最终上报器相同的认证设置可以让适配器保持诚实。
const
baseUrl
=
process
.
env
.
INFRAI_BASE_URL
;
const
apiKey
=
process
.
env
.
INFRAI_API_KEY
;
if
(
!
baseUrl
||
!
apiKey
)
{
throw
new
Error
(
"
INFRAI_BASE_URL and INFRAI_API_KEY are required
"
);
}
async
function
loadMetricCapability
(
attempt
=
0
):
Promise
<
unknown
>
{
const
response
=
await
fetch
(
`
${
baseUrl
}
/discovery/metrics.report`
,
{
method
:
"
GET
"
,
headers
:
{
Authorization
:
`Bearer
${
apiKey
}
`
},
});
if
(
response
.
status
===
429
&&
attempt
<
4
)
{
const
retryAfter
=
Number
(
response
.
headers
.
get
(
"
retry-after
"
));
const
delayMs
=
Number
.
isFinite
(
retryAfter
)
?
retryAfter
*
1
_000
:
250
*
2
**
attempt
;
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
delayMs
));
return
loadMetricCapability
(
attempt
+
1
);
}
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Discovery failed (
${
response
.
status
}
):
${
await
response
.
text
()}
`
);
}
return
response
.
json
();
}
console
.
log
(
JSON
.
stringify
(
await
loadMetricCapability
(),
null
,
2
));该响应就是导出器应使用的契约:完整的请求 JSON Schema、响应 schema、计费数据以及可运行示例。这一点很重要,因为查询过滤器并未声明;自行发明一个方便的标签过滤器会把可复制的示例变成虚构。
下一个 TypeScript 文件使用 Node.js 22 运行,命令为 npx tsx agent-metrics.ts。它在内存中记录智能体轮次,生成紧凑快照,并将导出器保持在一个接口之后。将 runAgentTurn 替换为真实的智能体调用,并透传该提供商或网关报告的成本。
import
{
performance
}
from
"
node:perf_hooks
"
;
type
Workflow
=
"
carrier_quote
"
|
"
shipment_exception
"
;
type
Outcome
=
"
completed
"
|
"
failed
"
;
type
AgentTurnMetric
=
{
workflow
:
Workflow
;
outcome
:
Outcome
;
latencyMs
:
number
;
costUsd
:
number
;
queueDepth
:
number
;
recordedAt
:
string
;
};
interface
MetricSink
{
report
(
metric
:
AgentTurnMetric
):
Promise
<
void
>
;
}
class
MemoryMetricSink
implements
MetricSink
{
readonly
records
:
AgentTurnMetric
[]
=
[];
async
report
(
metric
:
AgentTurnMetric
):
Promise
<
void
>
{
this
.
records
.
push
(
metric
);
}
}
type
AgentResult
=
{
outcome
:
Outcome
;
costUsd
:
number
;
};
async
function
measureAgentTurn
(
sink
:
MetricSink
,
workflow
:
Workflow
,
queueDepth
:
number
,
run
:
()
=>
Promise
<
AgentResult
>
,
):
Promise
<
void
>
{
const
startedAt
=
performance
.
now
();
const
result
=
await
run
();
await
sink
.
report
({
workflow
,
outcome
:
result
.
outcome
,
latencyMs
:
performance
.
now
()
-
startedAt
,
costUsd
:
result
.
costUsd
,
queueDepth
,
recordedAt
:
new
Date
().
toISOString
(),
});
}
function
percentile
(
values
:
number
[],
fraction
:
number
):
number
|
null
{
if
(
values
.
length
===
0
)
return
null
;
const
sorted
=
[...
values
].
sort
((
a
,
b
)
=>
a
-
b
);
const
index
=
Math
.
ceil
(
fraction
*
sorted
.
length
)
-
1
;
return
sorted
[
Math
.
max
(
0
,
index
)];
}
function
summarize
(
records
:
AgentTurnMetric
[])
{
const
completed
=
records
.
filter
((
item
)
=>
item
.
outcome
===
"
completed
"
);
const
totalCostUsd
=
records
.
reduce
((
sum
,
item
)
=>
sum
+
item
.
costUsd
,
0
);
return
{
turns
:
records
.
length
,
completed
:
completed
.
length
,
failed
:
records
.
length
-
completed
.
length
,
latencyP50Ms
:
percentile
(
records
.
map
((
item
)
=>
item
.
latencyMs
),
0.5
),
latencyP95Ms
:
percentile
(
records
.
map
((
item
)
=>
item
.
latencyMs
),
0.95
),
totalCostUsd
,
costPerCompletedWorkflow
:
completed
.
length
===
0
?
null
:
totalCostUsd
/
completed
.
length
,
latestQueueDepth
:
records
.
at
(
-
1
)?.
queueDepth
??
0
,
};
}
async
function
runAgentTurn
():
Promise
<
AgentResult
>
{
return
{
outcome
:
"
completed
"
,
costUsd
:
0.012
};
}
const
sink
=
new
MemoryMetricSink
();
await
measureAgentTurn
(
sink
,
"
carrier_quote
"
,
7
,
runAgentTurn
);
console
.
log
(
JSON
.
stringify
(
summarize
(
sink
.
records
),
null
,
2
));0.012 这个值是夹具数据,以便文件可以运行;它不是模型价格。在生产环境中,智能体适配器必须将其替换为实际的每次调用成本。该示例还只测量了一个轮次,而不是整个工作流。在编排边界添加工作流执行计数,这样三轮成功仍然算作一次完成的报价。
如果从请求路径发送每条原始记录会增加用户可见的延迟,就不要这样做。有界的本地缓冲区或队列工作进程是合适的,但投递必须被视为至少一次:在适配器边界为每条记录提供稳定标识,并在下游去重。批量刷新,限制内存,并明确决定当指标后端不可用时会发生什么。
这些产品有重叠,但它们针对不同的问题进行了优化。
| 选项 | 最适合 | 成本归因 | 主要边界 | | --- | --- | --- | --- | | Plausible | 注重隐私的网站流量和转化目标 | 可通过自定义事件实现,但后端成本计数器并非其重心 | 运营指标和基础设施探索能力有限 | | PostHog | 产品分析、漏斗、路径、留存以及更丰富的用户行为 | 自定义事件可以携带使用事实,但将基础设施计数器混入产品分析需要纪律 | 比狭窄的内部仪表盘所需的更广泛的产品平台 | | Grafana Cloud | 运营指标、仪表盘、告警和更深入的探索 | 当成本作为指标发出并与工作负载标签配对时表现强大 | 对于一个小型应用屏幕来说,需要掌握更多概念和技术栈表面 | | 自定义指标 API 和 UI | 一组固定的产品 KPI 加上后端计数器 | 直接控制工作流级别的成本语义 | 你需要自行负责指标定义、图表、保留决策和告警逻辑 |
当真正的工作是网站流量、目标以及刻意更小的分析足迹时,Plausible 是这里最干净的选择。我不会把它扭曲成队列监控器。当下一个问题是哪一系列产品操作导致了激活或留存时,PostHog 更合适。它的广度很有价值,但仅仅为了六个运营数字而采用这种广度很难证明合理。
一旦探索和告警成为核心需求,Grafana Cloud 就会胜出。如果值班工程师需要关联跨服务的饱和度、反复切分时间序列并维护严肃的告警规则,那么手工制作的仪表盘就是虚假经济。设置成本换来了运营深度。
自定义 API 选项适合这种狭窄场景,因为添加一个指标从读取其请求和响应 schema 开始。每个有文档记录的能力都附带 10 种语言的可运行示例。它仍然比 Plausible 或 PostHog 更少主观意见,并且比 Grafana Cloud 轻量得多;应用程序必须定义并发出自己的指标。
最后这句话就是权衡所在。将无差异的传输外包出去,但要保留对“每完成一次报价的成本”含义的所有权。
隐私友好的部署是一种架构选择,而不是供应商的勾选项。在导出前尽量减少标签,定义保留策略,将产品身份与运维指标分离,并为任何残留的个人数据记录合法依据。Plausible 的设计可以减少分析数据收集,但自定义属性仍然值得审查。自托管也无法修复过度收集的事件模式。
轻量级自定义路线有硬性限制。当需要复杂的过滤、分布式追踪导航或托管告警升级时,Infrai 并不适合;此时应选择 Grafana Cloud。查询过滤参数未声明,因此不能假设支持复杂的指标过滤。没有依据将其视为分布式追踪查询系统、会话回放工具、source-map 处理器或 Electron minidump 符号化工具。日志可能携带 trace 和 span ID 用于关联,但这与渲染 span 树不是一回事。
告警也需要单独处理。如果没有阈值、电话、短信或 webhook 通知路由,从定时 worker 轮询查询可以支持小型内部检查,但这是你现在要自己维护的代码。静默的“任务从未运行”故障需要外部心跳监控,例如 Healthchecks。不要让被监控的系统成为它已停止运行的唯一见证者。
这就是一个狭窄的仪表盘在工程注意力上不再便宜的地方,即使它的账单仍然很小。
首先,我会用持久化的批量导出器替换内存 sink,并保留相同的指标契约。仪表盘不应关心记录存储在哪里。我还会添加根据服务级别目标选择的直方图桶,而不是永远从原始记录计算百分位数。
其次,当智能体循环跨越足够多的 worker 和工具,以至于聚合延迟不再能解释延迟时,我会在服务边界引入 OpenTelemetry。采样是一个真正的设计决策:头部采样早且成本低,而尾部采样可以在观察结果后保留有趣的 trace。两者都无法恢复从未记录的维度。
最后,当告警所有权、多服务探索或追踪导航成为每周工作时,我会迁移到 Grafana Cloud 或其他完整的可观测性后端。如果产品漏斗很重要,PostHog 会继续保留;Plausible 仍将是一个合理、更小的 Web 分析选择。整合只有在减少运维思考时才有用。
对于单人 SaaS,决策规则很简单:从改变明天工作的六个指标开始。从第一天起就在 schema 中保留成本归属。当未回答的问题——而不是功能羡慕——消耗的时间超过更大平台所需时,再升级。
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:DEV.to
原文作者:ValerianBlack3895;如有侵权,请联系管理员删除。