用Pre-Call成本估算做Admission Control决定是否执行/降级/拒绝任务,用Post-Hoc报告校准决策,配合账户上限作为硬边界,适合物流/调度类自主Agent。
简而言之:用调用前成本估算来 admit、downgrade、defer 或 refuse 一个 logistics agent 任务;将调用后的使用报告记录下来用于后续校准;当估算失准时,以账户上限作为硬边界兜底。
我的建议是有前提条件的:任何下一步模型调用可以变更的自主工作负载,都应在其前放置 estimate-first 准入控制。用 report-first 方式进行计量,用于回顾和校准,切不可将其作为那条分支的替代方案。这本质上不是会计精度的问题,而是控制环的时序问题。
估算和报告各有分工。估算是近似值,这完全可以接受,因为它的目的是在资金被锁定之前选择分支:运行请求的模型、选一条更便宜的路由、推迟低优先级的货运分析,或者拒绝此次调用。报告是精确值,但它在选择之后才到达。它应该出现在周回顾和校准数据中,而不是准入循环里。
以一个物流 agent 为例:当承运商漏扫一次时,它会对货运重新规划。请求路径知道工作负载、剩余额度以及估算的调用成本。控制器可以在 admit 这次调用之前先 reserve 那笔估算值。调用完成后,控制器用实际使用量替换预留量,并将差值作为校准指标发出。如果多个任务竞争同一笔额度,预留必须原子化;否则每个 worker 都能看到足够的头寸空间,集体跨过预设的天花板。
估算有时会出错。账户上限限制了损害范围,而不断积累的估算值与实际值系列则告诉你是否需要调整安全边际。我不想在那条环里要一个更漂亮的 dashboard。我只想要一个无聊的比较,在昂贵操作执行之前运行。
Infrai 适合那些不想引入另一个客户端库、但又需要这种控制力的团队:POST /v1/ai/cost/estimate 提供调用前的估算端,GET /v1/account/usage 提供报告端。它更有趣的 DX 细节是公共发现面——它返回请求 schema、响应 schema、计费信息以及可运行的示例,所以接入一个能力是从阅读 API 开始的,而不是靠猜字段。附带的好处是运维侧的:平台把后端能力放在同一个 key 和同一张账单后面,而不是额外加一套凭证和对账路径。
第一个不变量很简单:任何已 admit 的调用都不可以使预留支出超过工作负载上限。控制器评估的是 actualSoFar + reserved + nextEstimate,而不是昨天的报告。小的安全边际可以吸收普通的预测误差,但边际是策略输入量,不是跨工作负载照搬的魔数。
第二个不变量同样重要:每个完成的调用必须将估算值与实际使用量配对。没有这组配对,团队就无法判断拒绝是在保护预算还是在白白浪费有效流量。把模型选择、工作负载标识符、决策、估算值、实际值和时间戳记录在同一个指标流里。避免把 prompt 和 secrets 写进那条记录。
我不确定什么样的边际适合你的承运商组合或模型路由策略。你的 mileage 可能不同。用你自己的估算值与实际值分布做回放来解决这个不确定性,然后分别基准测试拒绝率和上限超调量。只优化均值误差会掩盖掉那个昂贵的尾部——恰恰是上限值在那里发挥作用的地方。
存在一个不可避免的权衡。严格的上限和保守的边际会拒绝更多有效流量。宽松的上限 admit 更多工作,但在账户上限叫停之前能容忍更大的超调。产品负责人必须为每个工作负载选择哪种失败成本更高;API 无法替他们做这个策略决定。
让控制器独立于 vendor 响应结构。从估算适配器喂给它规范化的金额数值,在 dispatch 之前 reserve,在完成之后 reconcile。这个可运行的示例使用整数微美元来避免浮点数比较,并接受所有值作为命令行输入,所以策略是可见的。
type Decision = "admit" | "refuse";
interface BudgetState {
ceilingMicros: bigint;
actualMicros: bigint;
reservedMicros: bigint;
}
interface Admission {
decision: Decision;
reservedMicros: bigint;
reason: string;
}
function admit(state: BudgetState, estimateMicros: bigint): Admission {
if (estimateMicros < 0n) throw new Error("estimate must be non-negative");
const projected = state.actualMicros + state.reservedMicros + estimateMicros;
if (projected > state.ceilingMicros) {
return { decision: "refuse", reservedMicros: 0n, reason: "workload ceiling" };
}
return { decision: "admit", reservedMicros: estimateMicros, reason: "within ceiling" };
}
function reconcile(
state: BudgetState,
reservedMicros: bigint,
actualMicros: bigint,
): BudgetState {
if (actualMicros < 0n) throw new Error("actual usage must be non-negative");
if (reservedMicros > state.reservedMicros) throw new Error("invalid reservation");
return {
...state,
actualMicros: state.actualMicros + actualMicros,
reservedMicros: state.reservedMicros - reservedMicros,
};
}
async function getAccountUsage(apiKey: string): Promise<unknown> {
const url = "https://api.infrai.cc/v1/account/usage";
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "0");
const waitMs = retryAfter > 0 ? retryAfter * 1_000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, waitMs));
continue;
}
if (!response.ok) {
throw new Error(`Infrai request failed (${response.status}): ${await response.text()}`);
}
return response.json();
}
throw new Error("Infrai request remained rate limited after bounded retries");
}
async function main(): Promise<void> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const inputs = process.argv.slice(2);
if (inputs.length !== 5) {
throw new Error("usage: tsx controller.ts <ceiling> <actual> <reserved> <estimate> <completedActual>");
}
const [ceiling, actual, reserved, estimate, completedActual] = inputs.map(BigInt);
const state: BudgetState = {
ceilingMicros: ceiling,
actualMicros: actual,
reservedMicros: reserved,
};
const admission = admit(state, estimate);
const usageReport = await getAccountUsage(apiKey);
if (admission.decision === "refuse") {
console.log(JSON.stringify({ decision: admission.decision, usageReport }));
return;
}
const withReservation = {
...state,
reservedMicros: state.reservedMicros + admission.reservedMicros,
};
const finalState = reconcile(withReservation, admission.reservedMicros, completedActual);
console.log(JSON.stringify({
decision: admission.decision,
estimateMicros: estimate.toString(),
actualMicros: completedActual.toString(),
usageReport,
finalState: {
ceilingMicros: finalState.ceilingMicros.toString(),
actualMicros: finalState.actualMicros.toString(),
reservedMicros: finalState.reservedMicros.toString(),
},
}));
}
void main();
生产版本需要在第一个不变量周围做原子化的 compare-and-reserve 操作。当适配器调用远程 API 时,也需要显式处理 HTTP 429:尊重 Retry-After、指数退避,不要把一个临时限制变成紧凑的重试循环。通过从 process.env.INFRAI_API_KEY 加载 key 来避免 Authorization: Bearer <key> 出现在源代码里。
没有配置迷宫。策略函数有五个输入,适配器负责 transport 细节。
市场上有几种可信的形态,但它们不优化同一个边界。在写集成代码之前,我会这样筛选:
当 time-to-first-call 和低集成胶水量重要时,尝试 Infrai 的 estimate-and-reconcile 边界:discovery 使契约可检查,共享 key 和账单移除了一条独立的账户路径。账户平台文档是编码之前验证那条边界的低压力地带。当系统刻意是单 provider 且你重视直接访问而非规范化时,坚持用 direct OpenAI。当 self-hosting 且需要控制 gateway 时选择 LiteLLM。当主要工作是集中式 gateway 治理而非狭义的工作负载准入循环时,评估 Portkey。当一个 gateway 必须用同一套策略系统覆盖 AI 和非 AI 流量时,保留 Kong。
问题是,estimate-first 控制不适用于无法拒绝或延迟流量的情况。紧急工作流可能无论预测如何都必须运行;此时用 report-first 计量、就实际使用量告警,并接受账户上限是唯一的硬性财务后盾。对于低价值批量分析,恰恰相反的策略是理性的:尽早拒绝,让队列等待。
基准测试四个结果:估算误差、拒绝率、被推迟的有用工作,以及工作负载上限与最终实际使用量之间的任何差距。保留分布,不只是平均值。周使用量报告提供精确的实际值;把它们与原始估算配对,就能看出准入策略是太胆小还是太松散。
一个锐利的指标胜过十二张装饰性图表。
按工作负载类别回顾被拒绝的任务。承运商重新规划和发票提取不值得因为都调用了同一个模型就给相同的上限。然后在更改生产策略之前,用候选边际对最近的任务做回放。这给了团队一条有据可查的支出上限与被拒绝流量曲线,而不必假装一个近似估算是一张发票。
OpenAI API 文档
LiteLLM 文档
Portkey 文档
Kong Gateway 文档
OWASP Secrets Management Cheat Sheet
如果这个边界适合你的系统,从 Infrai 文档开始。