ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Spring AI 成本流水怎样防重记:request、attempt 与 unknown

Spring AI 成本流水怎样防重记:request、attempt 与 unknown

用户点了一次提交,网关重试两次,模型流又在结束前断开。看板上的“一次请求”到底应该对应几笔成本?这个问题先于Token单价,也先于Micrometer标签怎么命名。

可以用requestId关联用户任务,用attemptId标识每次实际调用,再保存供应商返回的请求ID。request并不能充当费用去重键:同一个任务的两次尝试可能都产生费用;同一尝试收到重复回调,也不能记两笔。

封面中的多张单据表示同一attempt重复到达;不同attempt必须分别记账。

缺usage是一种状态

Spring AI观测文档描述模型与工具的观测信息,但监控输出不是供应商账单。usage缺失、迟到、字段口径不同,都需要明细流水承接。观测参考为持续维护文档。

建议至少保留UNKNOWN、ESTIMATED、REPORTED三态。缺usage时既不要填0,也不要从客户端收到的字数反推成确定费用。估算可以用于预算提示,但要带估算方法、版本和价格快照,晚到实报覆盖估算时留下修订轨迹。

一个去重与迟到处理的离线例子

为了只检验状态迁移,下面金额均为手写的“教学计价单位”,不是任何模型的真实价格。实际账本还需存Token分类、币种、价目生效时间、区域和折扣。

import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; public class AttemptLedger { static class Entry { final BigDecimal amount; final boolean reported; Entry(String amount, boolean reported) { this.amount = new BigDecimal(amount); this.reported = reported; if (this.amount.signum() < 0) throw new IllegalArgumentException(); } } final Map<String, Entry> rows = new HashMap<>(); void observe(String attempt, Entry incoming) { if (attempt == null || attempt.isBlank()) throw new IllegalArgumentException(); Entry old = rows.get(attempt); if (old != null && old.reported) { if (!incoming.reported) return; if (old.amount.compareTo(incoming.amount) != 0) throw new IllegalStateException("conflicting report"); return; } rows.put(attempt, incoming); } BigDecimal reportedTotal() { return rows.values().stream().filter(x -> x.reported) .map(x -> x.amount).reduce(BigDecimal.ZERO, BigDecimal::add); } public static void main(String[] args) { AttemptLedger l = new AttemptLedger(); l.observe("r1/a1", new Entry("0.10", false)); l.observe("r1/a1", new Entry("0.12", true)); l.observe("r1/a1", new Entry("0.12", true)); l.observe("r1/a1", new Entry("9.00", false)); l.observe("r1/a2", new Entry("0.20", true)); assert l.rows.size() == 2; assert l.reportedTotal().compareTo(new BigDecimal("0.32")) == 0; boolean conflict = false; try { l.observe("r1/a1", new Entry("0.13", true)); } catch (IllegalStateException expected) { conflict = true; } assert conflict; System.out.println("ledger: duplicate, late estimate, conflict checked"); } }

保存为AttemptLedger.java,执行javac --release 11 AttemptLedger.java && java -ea AttemptLedger。没有出现在Map中的尝试仍可能是UNKNOWN,不能因不在求和中就宣称费用为零。这个例子没有并发或持久化保证;真实服务应使用数据库唯一键和事务处理同一attempt的更新。

看板和账本分工

高基数的requestId、完整Prompt、用户邮箱不适合作Prometheus标签。明细放受权限控制的流水,指标按provider、模型版本线、结果类别等有限维度聚合。估算金额与实报金额也要分别命名,避免迟到修正把累计Counter倒减。

Prometheus的rate(counter[1h])表示该窗口内估计的每秒速率,不是一小时金额;需要窗口增量时应理解increase的外推与采样条件,账单核对仍回到明细。函数说明支持这种量纲区分。

本例仅验证合成流水的去重规则,没有调用模型或取得实际账单。十月SDK/API变化提醒我们保存版本身份,但不能据此填造usage字段或节费比例。最终对账还要确认供应商实报、价格快照与财务账单分别覆盖什么。

本地验证记录:Oracle JDK 12.0.1,javac --release 11与java -ea,运行成功,输出ledger: duplicate, late estimate, conflict checked。仅覆盖本例的离线输入;框架、供应商和生产集成仍未测试。

返回列表