AI 工程基础体系 · 第 30/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。
AI 可观测性与成本治理:Trace、Token、TTFT、预算、缓存和降级
生成式 AI 系统的故障,通常不是“模型调用失败”这么简单。一次用户请求可能经过网关、身份校验、检索、重排、提示词组装、模型调用、工具调用、结果校验和流式传输;其中任一环节都可能增加延迟、消耗 Token 或改变最终质量。
因此,生产系统需要同时回答四个问题:
- 这次请求经过了哪些组件,在哪个环节变慢或失败?
- 消耗了多少输入、输出、缓存和推理 Token?
- 延迟是排队、首 Token 生成慢,还是后续输出速度慢?
- 在预算、限流、缓存命中和服务降级发生时,系统是否仍然可控?
这里的“可观测性”不是简单记录日志,“成本治理”也不是月底统计账单。两者必须共享同一条请求链路:每一个成本数字都能追溯到调用上下文,每一次降级都能解释其预算原因,每一个质量回归都能关联到模型、提示词、数据和缓存策略。
一、先建立统一对象:一次 AI 请求是什么
在普通 Web 服务中,一个请求通常由 HTTP 请求和若干数据库、RPC 调用组成。AI 请求更复杂,因为模型调用本身具有:
- 不同输入和输出长度;
- 流式和非流式两种完成方式;
- 首 Token 延迟与后续生成速度不同;
- 重试、取消和工具调用;
- 可能存在缓存输入、推理 Token、批处理等供应商特有计费项;
- 输出质量无法只用 HTTP 状态码描述。
可以把一次用户请求抽象为一个根 Trace:
用户请求
└── AI workflow
├── 身份认证与预算检查
├── 检索 / 重排
├── Prompt 组装
├── LLM 调用
│ ├── 第一次尝试
│ └── 重试
├── 工具调用
├── 输出校验
└── 流式传输 / 响应
1. Trace、Span 和事件
Trace 是一次完整操作的因果链。它拥有一个全局唯一的 trace_id。
Span 是 Trace 中具有起止时间的操作,例如:
http.requestretrieval.searchllm.calltool.calloutput.validation
一个 Span 至少应记录:
trace_id
span_id
parent_span_id
开始时间
结束时间
状态:ok / error / cancelled
服务与版本
模型及模型版本
关键业务维度
事件(event) 是 Span 内某个时刻发生的事情,例如:
- 收到上游响应头;
- 收到第一个非空输出 Token;
- 收到工具调用请求;
- 用户取消连接;
- 达到输出 Token 上限;
- 触发预算降级。
事件没有独立持续时间,但对解释 TTFT、取消和流式错误非常重要。
Trace 的因果关系不能只依靠日志时间戳推断。例如两个并发工具调用可能交错完成:
trace_id = t1
├── retrieval span: 100ms ~ 250ms
├── llm span: 300ms ~ 1800ms
└── tool span: 700ms ~ 900ms
如果没有 parent_span_id,仅凭日志时间可能无法判断工具调用属于哪一次模型生成。
2. 不要把所有字段都放进指标标签
指标系统适合低基数维度,例如:
model_family
region
route
status
cache_result
degradation_level
不适合直接作为标签的字段包括:
- 用户 ID;
- 完整 Prompt;
- 完整回答;
trace_id;- 文档内容;
- 任意 URL。
这些字段放入日志或 Trace 事件,并设置采样、脱敏和访问权限。把 trace_id 作为 Prometheus 标签,会造成时间序列数量随请求数增长,最终让监控系统本身失效。
OpenTelemetry 已有面向生成式 AI 的语义约定,但不同语言 SDK、供应商和版本的字段仍可能变化。工程上应固定自己的内部字段,例如 ai.input_tokens、ai.output_tokens、ai.ttft_ms,再把供应商字段映射过来;不能假设所有 API 都返回完全相同的 usage 结构。
二、Token:成本和容量的共同计量单位
1. Token 不等于字符,也不等于词
Token 是模型分词器产生的离散单元。英文中一个单词可能被拆成多个 Token;中文中一个汉字常常接近一个 Token,但这不是规范保证。代码、JSON、URL 和特殊标记往往比自然语言更难压缩。
因此,下面这种估算不可靠:
输入字符数 × 固定比例 = Token 数
准确计算应使用目标模型对应的 tokenizer,或者使用供应商提供的 Token 计数接口。不同模型的 tokenizer 可能不同,不能用一个模型的计数器为所有模型估算。
2. 输入、输出、缓存和推理 Token
常见的计量项包括:
- 输入 Token:发送给模型的内容,包括系统提示词、用户输入、历史消息、工具定义和检索结果;
- 输出 Token:模型返回给应用的可见输出;
- 缓存输入 Token:模型供应商识别并复用的输入部分,可能有不同价格;
- 推理 Token:模型内部推理使用的 Token,有的模型单独计费或单独返回;
- 总 Token:供应商定义的汇总值,不能擅自认为等于输入加输出。
一个通用成本模型是:
其中:
- :普通输入 Token;
- :输出 Token;
- :缓存输入 Token;
- :推理 Token;
- :对应的每百万 Token 单价;
- :本次模型调用成本。
如果某供应商没有单独返回 或 ,就不能把它们凭空拆出来。应记录供应商返回的原始 usage,并在内部标记:
usage_completeness = complete / partial / estimated
estimated 表示估算值,只能用于预算预警,不能作为财务结算的唯一依据。
3. 重试会造成真实重复成本
假设一次请求第一次调用发生超时,但供应商已经生成了 800 个输出 Token,客户端没有收到完整响应,随后重试一次:
第一次:输入 2,000,输出 800,结果客户端视为失败
第二次:输入 2,000,输出 600,成功
实际成本是两次调用之和,而不是成功响应那一次:
如果只在最终成功的 Span 上记账,成本会被低估,错误率也会被低估。正确做法是:
- 每次供应商调用都有独立的
attempt_span; - 根 Trace 聚合所有尝试;
- 重试原因单独记录;
- 取消前已经产生的 usage 不应被删除。
4. 完整算例:Token 成本如何聚合
假设价格如下,仅用于说明计算方法:
普通输入:$2 / 1M Token
缓存输入:$0.5 / 1M Token
输出: $8 / 1M Token
一次工作流包含两个模型调用:
调用 A:普通输入 12,000,缓存输入 3,000,输出 1,500
调用 B:普通输入 4,000,缓存输入 0,输出 800
则:
整条 Trace 的模型成本:
如果系统只统计最终回答的调用 B,会把成本记成 $0.0144,低估约 59%。这也是为什么成本聚合必须以 Trace 为边界,而不是以 HTTP 响应为边界。
三、TTFT:首 Token 延迟和总延迟不是一回事
1. TTFT 的定义
TTFT(Time To First Token) 是从客户端发起模型请求,到客户端收到第一个有效输出 Token 的时间。
在流式系统中,应明确时间点。一个实用定义是:
其中:
- :请求真正发送给模型供应商的时间;
- :收到第一个非空文本增量的时间。
不要把“收到 HTTP 响应头”自动当成首 Token。响应头可能已经到达,但正文尚未返回有效内容。
端到端首 Token 延迟还可以拆成:
其中:
- :本地限流器或并发队列等待;
- :请求上传;
- :供应商排队;
- :模型读取和处理输入;
- :首个输出片段返回。
输入 Token 越多,通常会增加 prefill 时间,但具体关系依赖模型架构、硬件和服务实现,不能直接断言“输入 Token 增加一倍,TTFT 也增加一倍”。
2. TTFT 与生成速度
总延迟可以近似写成:
其中:
- :输出 Token 数;
- :生成速度,单位为 Token/s。
两个服务可能有不同的性能特征:
服务 A:TTFT = 300ms,生成速度 = 20 Token/s,输出 200 Token
总时间 ≈ 0.3 + 200/20 = 10.3s
服务 B:TTFT = 1.2s,生成速度 = 60 Token/s,输出 200 Token
总时间 ≈ 1.2 + 200/60 = 4.53s
如果产品强调“用户尽快看到反馈”,A 可能更好;如果产品强调完整答案尽快结束,B 更好。只看平均总延迟会掩盖这两类差异。
应同时监控:
TTFT p50 / p95 / p99
总延迟 p50 / p95 / p99
inter-token latency
output tokens per second
首 Token 超时率
流式中途断开率
3. 流式输出的正确计时边界
流式 Span 的生命周期应覆盖整个流,而不是收到首 Token 就结束:
llm.call span
├── start
├── request_sent
├── first_token -> TTFT
├── token_delta ...
├── stream_cancelled / stream_error
└── completed -> total latency, usage
伪代码如下:
import time
def consume_stream(stream):
start = time.monotonic()
first_token_at = None
output_parts = []
try:
for event in stream:
text = getattr(event, "text", "") or ""
if text:
if first_token_at is None:
first_token_at = time.monotonic()
record_metric(
"ai_ttft_ms",
(first_token_at - start) * 1000,
)
output_parts.append(text)
send_to_user(text)
end = time.monotonic()
record_metric("ai_total_latency_ms", (end - start) * 1000)
return "".join(output_parts)
except ClientDisconnected:
# 已经发出的 Token 仍然产生了真实成本
record_event("stream_cancelled_by_client")
raise
这里的 event.text 只是供应商适配层归一化后的字段。不同 API 的流式事件结构不同,不能把这段代码直接当成所有 SDK 的通用接口。适配层应负责把供应商事件转换为:
text_delta
tool_call_delta
usage
completed
error
四、预算:先授权,再执行,再结算
1. 预算不是账单阈值
预算 是在执行前限制资源使用,在执行中监控消耗,并在执行后核算实际成本的机制。
至少需要区分三种预算:
- 请求预算:单次请求最多允许多少 Token 或多少钱;
- 用户 / 租户预算:某个周期内允许使用的金额;
- 系统预算:整个服务、区域或模型池的上限。
如果只在账单产生后报警,预算就只是报表,而不是控制机制。
2. 预算授权的必要性
模型调用前通常只有估算值,调用完成后才有实际 usage。因此需要两个数字:
estimated_cost:执行前估算;actual_cost:执行后结算。
假设租户剩余预算为 $0.10,两个并发请求都估算为 $0.08:
请求 A 读取余额:0.10,判断足够
请求 B 读取余额:0.10,判断足够
A 执行并扣除 0.08
B 执行并扣除 0.08
最终消耗 0.16,超过预算
这是典型的检查与扣除之间的竞态。预算授权必须原子化,或者使用带版本号的乐观锁。
SQL 示例:
UPDATE tenant_budget
SET reserved_amount = reserved_amount + :estimate,
version = version + 1
WHERE tenant_id = :tenant_id
AND reserved_amount + spent_amount + :estimate <= budget_limit;
应用检查受影响行数:
affected_rows = 1:授权成功
affected_rows = 0:预算不足或版本冲突,需要重新读取
授权成功后,调用结束必须执行结算:
实际成本 < 估算成本:释放多余预留
实际成本 > 估算成本:补扣差额或标记超支
调用失败但供应商已产生 usage:按实际 usage 结算
客户端取消:按已产生 usage 结算
3. 完整预算算例
租户月预算为 $10,当前已结算 $8.50,已预留 $0.80,可用预算为:
新请求估算成本 $0.60,授权后:
spent_amount = 8.50
reserved_amount = 1.40
remaining = 0.10
实际调用只花费 $0.42,结算释放 $0.18:
spent_amount = 8.92
reserved_amount = 0.82
remaining = 0.26
如果实际花费 $0.75,则超出原预留 $0.15。系统需要根据策略:
- 允许小额超支并记录;
- 从可用余额扣除;
- 中止后续工作流;
- 把租户置为只读或降级状态。
不能假设“估算 Token 上限就是实际成本上限”。供应商计费项、工具调用和重试可能使最终成本超过最初估算。
五、缓存:降低成本,但会改变正确性边界
1. 精确缓存与语义缓存
精确缓存要求规范化后的请求完全一致:
model
system_prompt_version
user_input
conversation_state
tool_schema_version
retrieval_document_ids
generation_parameters
这些字段组合后生成缓存键,例如:
只要其中一个会影响输出的因素发生变化,就应改变键。
语义缓存则根据向量相似度判断两个输入“意思接近”,可能返回之前的答案。它能提高命中率,但正确性条件更严格:相似问题不一定需要相同答案。例如:
- “今天北京天气如何?”
- “明天北京天气如何?”
语义上很接近,但答案不能复用。
对涉及时间、权限、库存、价格、账户状态和法律政策的请求,不应仅凭语义相似度复用结果。
2. 应区分应用缓存和供应商前缀缓存
应用缓存是:
命中后不调用模型,直接返回完整结果
它减少了模型调用次数、延迟和成本,但返回的是历史答案。
供应商的 Prompt Prefix Caching 通常是:
仍然调用模型,但复用重复输入前缀的计算或计费
它不会跳过模型生成,也不等价于应用层缓存。两者在 Trace 中应分别记录:
application_cache = hit / miss / bypass
provider_cached_input_tokens
否则容易把供应商缓存命中误报为“请求没有调用模型”。
3. 缓存的失效条件
缓存项至少需要包含:
created_at
expires_at
model_version
prompt_version
data_version
policy_version
tenant_scope
检索增强生成(RAG)尤其需要 data_version。如果知识库文档更新,但缓存键不变,系统会继续返回旧答案。
权限也必须进入缓存边界。一个用户能看到的检索结果,不能被另一个无权用户命中。安全上更稳妥的做法是:
cache_scope = tenant_id + authorization_policy_version
而不是只使用用户问题作为键。
4. 缓存反而可能增加成本的情况
缓存并不总是省钱:
- 为生成缓存键而发送过大的规范化内容;
- 缓存命中率低,但每次都执行昂贵的向量检索;
- 缓存污染导致错误回答,引发重试和人工处理;
- 缓存答案过期,用户再次追问或触发校正流程;
- 把高质量模型的答案缓存给低风险和高风险场景,造成质量不匹配。
因此应记录缓存的真实收益:
其中 with cache 还应包含向量检索、缓存存储和校验成本,而不是只比较模型账单。
六、降级:预算和故障驱动的状态机
降级 是在资源、延迟或质量约束无法同时满足时,主动选择较低成本、较低能力或较低时效性的路径。
降级不是简单地“换一个小模型”。它可能包括:
完整 RAG
→ 减少检索文档数量
→ 只使用缓存答案
→ 使用较小模型
→ 禁止工具调用
→ 缩短输出上限
→ 返回结构化摘要
→ 返回明确的暂时不可用
降级策略必须有明确触发条件,否则不同开发者会在不同位置随意修改 Prompt,最终无法解释质量变化。
1. 一个可验证的降级状态机
stateDiagram-v2
[*] --> Normal
Normal --> BudgetWarning: 预算使用率 >= 80%
Normal --> LatencyWarning: TTFT p95 超阈值
Normal --> ProviderError: 连续错误超过阈值
BudgetWarning --> ReducedContext: 预算仍可用
LatencyWarning --> SmallerModel: 延迟持续恶化
ProviderError --> FallbackModel: 备用模型可用
ReducedContext --> CacheOnly: 预算不足
SmallerModel --> CacheOnly: 备用路径可用
FallbackModel --> CacheOnly: 备用模型失败
CacheOnly --> Refuse: 无可信缓存
Normal --> Completed
ReducedContext --> Completed
SmallerModel --> Completed
FallbackModel --> Completed
CacheOnly --> Completed
Refuse --> [*]
Completed --> [*]
每次状态变化都应在 Trace 中记录:
degradation_level
degradation_reason
policy_version
original_route
selected_route
estimated_savings
quality_risk
例如:
{
"degradation_level": "reduced_context",
"degradation_reason": "tenant_budget_remaining_low",
"policy_version": "budget-policy-2025-03",
"original_route": "large-model-rag-tools",
"selected_route": "small-model-top3-docs-no-tools",
"estimated_savings_usd": 0.021
}
2. 降级的反例:无限重试造成雪崩
假设主模型超时后:
- 重试主模型;
- 调用备用模型;
- 备用模型也超时;
- 再次重试;
- 每次都重新执行检索和工具调用。
这会导致单个用户请求产生多个模型调用,扩大成本和并发压力。正确做法是为整个根请求设置:
deadline
remaining_budget
max_attempts
retryable_error_types
每次重试前同时判断:
当前时间是否超过 deadline?
剩余预算是否足够?
错误是否属于可重试类别?
这次重试是否可能改变结果?
连接失败、限流和供应商暂时不可用通常可能重试;参数错误、权限错误、内容策略拒绝通常不应重试。网络层的超时不等于模型没有生成内容,因此仍需依据返回的 usage 或供应商账单进行结算。
3. 降级不能绕过安全和权限
成本降级可以减少上下文或切换模型,但不能:
- 跳过身份认证;
- 放宽租户隔离;
- 使用没有权限的缓存;
- 关闭输出安全校验;
- 把内部错误详情返回给用户。
“低成本路径”仍然必须满足同样的权限和数据治理约束。尤其是缓存命中路径,不能因为没有调用模型就跳过授权检查。
七、一个可运行的本地示例:统计 TTFT、Token 和预算
下面示例不依赖真实模型服务,而是用可控的流模拟模型流式响应,便于验证计时、预算和取消逻辑。生产环境只需将 FakeProvider.stream 替换为具体 SDK 的适配器。
from dataclasses import dataclass
import time
from typing import Iterator
@dataclass
class Usage:
input_tokens: int
output_tokens: int
cached_input_tokens: int = 0
@dataclass
class Chunk:
text: str
usage: Usage | None = None
done: bool = False
class FakeProvider:
def stream(self, prompt: str) -> Iterator[Chunk]:
# 模拟供应商先等待,再逐片返回内容
time.sleep(0.08)
for text in ["可观测性", "需要", "同时记录", "延迟和成本。"]:
time.sleep(0.02)
yield Chunk(text=text)
yield Chunk(
text="",
usage=Usage(
input_tokens=len(prompt),
output_tokens=4,
cached_input_tokens=0,
),
done=True,
)
class Budget:
def __init__(self, limit_usd: float):
self.limit_usd = limit_usd
self.reserved_usd = 0.0
self.spent_usd = 0.0
def reserve(self, amount: float) -> bool:
if self.reserved_usd + self.spent_usd + amount > self.limit_usd:
return False
self.reserved_usd += amount
return True
def settle(self, reserved: float, actual: float) -> None:
self.reserved_usd -= reserved
self.spent_usd += actual
def calculate_cost(usage: Usage) -> float:
# 价格仅用于本地演示,不代表任何供应商价格
input_price = 2.0 / 1_000_000
cached_price = 0.5 / 1_000_000
output_price = 8.0 / 1_000_000
normal_input = usage.input_tokens - usage.cached_input_tokens
return (
normal_input * input_price
+ usage.cached_input_tokens * cached_price
+ usage.output_tokens * output_price
)
def call_with_observability(prompt: str, budget: Budget) -> str:
estimated_cost = 0.001 # 生产环境应由 Token 估算器和 max_output_tokens 推导
if not budget.reserve(estimated_cost):
raise RuntimeError("budget_exceeded_before_call")
provider = FakeProvider()
started = time.monotonic()
first_token_at = None
output = []
final_usage = None
try:
for chunk in provider.stream(prompt):
if chunk.text:
if first_token_at is None:
first_token_at = time.monotonic()
ttft_ms = (first_token_at - started) * 1000
print(f"TTFT: {ttft_ms:.1f} ms")
output.append(chunk.text)
print(chunk.text, end="", flush=True)
if chunk.done:
final_usage = chunk.usage
if final_usage is None:
raise RuntimeError("provider_finished_without_usage")
actual_cost = calculate_cost(final_usage)
budget.settle(estimated_cost, actual_cost)
print()
print(f"usage={final_usage}")
print(f"actual_cost=${actual_cost:.8f}")
return "".join(output)
except Exception:
# 真实系统应根据供应商返回的部分 usage 进行结算;
# 这里为了演示,保留预留金额,避免把失败请求误记为零成本。
raise
if __name__ == "__main__":
budget = Budget(limit_usd=0.01)
answer = call_with_observability(
prompt="解释 AI 可观测性与成本治理",
budget=budget,
)
print(f"\nanswer={answer}")
print(f"spent=${budget.spent_usd:.8f}")
运行时会看到类似输出:
TTFT: 80.x ms
可观测性需要同时记录延迟和成本。
usage=Usage(input_tokens=11, output_tokens=4, cached_input_tokens=0)
actual_cost=$0.00005400
这个例子有三个重要边界:
time.sleep只是模拟网络和生成过程,不能用于推断真实模型性能;len(prompt)不是 Token 计数,只是为了让示例可运行;- 生产 SDK 可能在流结束事件、独立 usage 事件或最终响应对象中返回 usage,必须以具体 API 文档为准。
生产适配器至少应统一以下异常:
AuthenticationError
InvalidRequestError
RateLimitError
TimeoutError
ProviderUnavailableError
ContentPolicyError
ClientCancelled
统一后,重试器和降级器才能基于错误类别工作,而不是通过匹配字符串判断。
八、可观测性数据应该如何关联
一次模型 Span 建议包含以下字段:
{
"trace_id": "8f...",
"span_id": "31...",
"route": "answer_with_rag",
"model": "provider-model-name",
"model_version": "provider-version-if-available",
"prompt_version": "prompt-42",
"input_tokens": 12000,
"cached_input_tokens": 3000,
"output_tokens": 1500,
"usage_completeness": "complete",
"ttft_ms": 420,
"total_latency_ms": 2380,
"streaming": true,
"retry_attempt": 1,
"cache_result": "miss",
"degradation_level": "normal",
"status": "ok"
}
但不应默认记录完整 Prompt 和回答。它们可能包含个人信息、密钥、内部文档和受监管数据。可以采用以下方式:
- 默认只记录 Prompt 哈希、长度、版本和 Token 数;
- 对调试 Trace 使用显式采样;
- 先做密钥、邮箱、手机号和身份标识脱敏;
- 对完整内容实施加密、访问审计和短期保留;
- 将业务日志与财务聚合日志分开保存。
Trace 还应关联模型、数据和评测版本:
model_version
prompt_version
retriever_version
embedding_model_version
dataset_version
policy_version
evaluation_run_id
这样才能回答:
成本上升是因为模型价格变化,还是 Prompt 变长?
TTFT 变慢是模型服务问题,还是检索返回了更多文档?
质量下降是模型切换,还是降级减少了上下文?
缓存命中率上升后,答案错误率是否也上升?
对于机器学习和深度学习训练任务,同样可以使用 Trace 思路,只是 Span 的粒度不同:
training_run
├── dataset_load
├── preprocessing
├── train_epoch
├── validation
├── checkpoint_save
├── evaluation
└── model_registry_publish
训练成本除了 GPU 时间,还包括数据处理、存储、网络和失败重跑。训练任务应记录:
dataset hash
code commit
hyperparameters
GPU type and count
training duration
checkpoint
validation metrics
artifact location
这使得线上模型问题能够追溯到训练数据、代码提交和评测结果,而不是把生成式 AI 与传统机器学习治理割裂开。
九、从指标判断故障,而不是只看平均数
1. 成本指标
成本应至少按以下层次聚合:
请求级
Trace 级
用户 / 租户级
路由级
模型级
区域级
日 / 周 / 月周期
常用指标包括:
cost_usd_total
cost_usd_per_request
input_tokens_total
output_tokens_total
retry_cost_usd
cache_savings_usd
degradation_count
budget_rejection_count
“平均每请求成本”可能被少数长上下文请求掩盖,因此还要观察 p50、p95 和最大值。
2. 延迟诊断矩阵
| 现象 | 可能原因 | 优先检查 |
|---|---|---|
| TTFT 高、生成速度正常 | 输入过长、供应商排队、网络上传 | 输入 Token、队列等待、模型侧时间 |
| TTFT 正常、总延迟高 | 输出过长、生成速度下降、下游发送慢 | 输出 Token、Token/s、客户端写入 |
| 总延迟正常、用户很晚看到内容 | 缓冲、代理未刷新、客户端读取不及时 | SSE/HTTP 缓冲、首片段转发 |
| 成本升高、Token 未明显增加 | 重试、推理 Token、模型价格变化 | attempt Span、usage 明细、价格表版本 |
| 缓存命中率升高、质量下降 | 缓存键过宽或过期 | 数据版本、权限边界、语义阈值 |
| 错误率不高、预算快速耗尽 | 单次请求很贵或隐式重试 | Trace 成本分布、工具循环、输出上限 |
3. 质量不能被成本指标替代
成本下降不等于系统变好。降级、截断上下文和缓存复用都可能降低答案质量。应把成本治理与评测关联:
cost_per_successful_task
cost_per_accepted_answer
quality_score / dollar
groundedness / dollar
如果 Judge 评测、规则校验或人工反馈表明某种降级使任务成功率从 95% 降到 70%,即使模型成本下降 30%,单位成功任务成本也可能上升。
例如:
完整路径:每次 $0.10,成功率 95%
降级路径:每次 $0.07,成功率 70%
每个成功任务的期望成本为:
完整路径:
降级路径:
此时降级路径略便宜,但如果成功率进一步降到 60%,则:
成本治理必须使用“每个成功任务的成本”,而不是只看单次调用价格。
十、生产边界和常见误解
误解一:HTTP 200 就表示请求成功
模型可能返回 HTTP 200,但输出为空、格式非法、引用不存在或违反业务约束。状态应至少分为:
transport_status
provider_status
application_status
quality_status
例如模型调用成功但 JSON 校验失败,不能把整次 Trace 标记为完全成功。
误解二:客户端取消意味着没有成本
取消只表示客户端不再等待或连接被关闭,不代表供应商已经停止计算。系统应向供应商发送取消信号,并记录:
cancel_requested_at
provider_cancel_ack_at
tokens_before_cancel
cancel_effective
如果供应商没有确认取消,就按可能产生费用处理。
误解三:设置 max output tokens 就能限制总成本
输出上限只能限制输出部分。输入历史、工具定义、检索文档、重试次数和内部推理 Token 仍可能产生成本。总预算必须同时约束:
max_input_tokens
max_output_tokens
max_tool_rounds
max_retries
deadline
estimated_cost
误解四:缓存命中率越高越好
缓存命中率是手段,不是目标。缓存应与新鲜度、权限和质量一起评估。一个 99% 命中但返回过期数据的系统,并不比低命中率系统更可靠。
误解五:只采样错误 Trace 就足够
错误 Trace 对排障重要,但只采样错误会丢失正常基线。至少应保留:
- 固定比例的正常 Trace;
- 所有预算拒绝和降级 Trace;
- 所有供应商错误;
- 延迟异常 Trace;
- 评测和回归关联 Trace。
采样策略本身也应记录,否则某次采样规则变化可能被误认为错误率变化。
十一、落地时应形成闭环
一个完整的生产闭环如下:
请求进入
→ 创建 Trace
→ 认证与租户预算原子授权
→ 检查精确缓存
→ 执行检索、模型和工具调用
→ 每次尝试独立记录 usage、TTFT 和错误
→ 流式传输并处理取消
→ 结算实际成本
→ 更新缓存和评测样本
→ 聚合指标、触发告警和调整降级策略
其中最容易遗漏的是“执行中”的状态。只有请求前预算,没有执行中截止时间,会出现单次调用拖垮并发;只有结束后 Token 统计,没有每次尝试记录,会低估重试成本;只有总延迟,没有 TTFT,会无法解释用户为何长时间看不到任何输出;只有缓存命中率,没有版本和权限边界,会把错误答案稳定地复用下去。
最终,AI 生产系统应把模型、数据、评测、权限和成本放在同一条可追溯链路中:
trace_id
→ route / prompt / model
→ retrieval / tool / cache
→ tokens / TTFT / retries
→ budget / degradation
→ evaluation / user outcome
这样才能从“模型调用了多少次”进一步回答“为什么调用、花了多少、是否值得、是否安全,以及下一次应如何改变系统”。
系列导航与关联阅读
- 系列入口:AI 工程完整学习路线:从机器学习与 Transformer 到 RAG、Agent 和生产治理
- 上一篇:LLM 与 Agent 评测:数据集、规则、Judge、轨迹、回归和统计
- 下一篇:AI 安全工程:提示注入、数据泄漏、越权工具、模型供应链和红队
- 延伸:LLM API 工程:客户端、流式输出、取消、重试、限流和兼容层
官方资料
本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论