本文定位:AI 应用性能 / 成本治理 / 高可用设计
示例环境:Java 21、Spring Boot 3.3、Redis、PostgreSQL、OpenTelemetry。模型价格会变化,文章只讨论测量与治理方法,不绑定某个供应商价格。
摘要
AI Demo 上线后,最容易失控的两个指标是成本和延迟。Prompt 变长、检索片段增多、工具重复调用、失败重试和长历史对话都会让一次请求消耗更多 Token;模型响应慢、Rerank 排队、数据库连接池耗尽又会把用户体验拖垮。
本文先建立成本与延迟的测量模型,再讨论上下文裁剪、缓存、批处理、流式输出、模型路由、限流、超时、熔断和降级。重点是说明优化顺序:先测出钱花在哪里,再决定哪些内容值得缓存或压缩,避免为了追求一个理论上的低延迟而牺牲正确性和安全性。
一、成本模型
一次 RAG 请求成本可以拆成:
text
总成本 = Embedding成本
+ 关键词/向量检索成本
+ Rerank成本
+ 模型输入Token成本
+ 模型输出Token成本
+ 重试成本
+ 工具和基础设施成本
很多团队只看模型输出 Token,忽略了输入上下文。RAG 把 10 个大 Chunk 塞进 Prompt 后,输入成本和延迟可能远高于回答本身。每次请求至少记录输入 Token、输出 Token、检索条数、Rerank 次数、重试次数和最终模型。
二、延迟分解
#mermaid-svg-D8d3HZTQUtrQaK67{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-D8d3HZTQUtrQaK67 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-D8d3HZTQUtrQaK67 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-D8d3HZTQUtrQaK67 .error-icon{fill:#552222;}#mermaid-svg-D8d3HZTQUtrQaK67 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D8d3HZTQUtrQaK67 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-D8d3HZTQUtrQaK67 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D8d3HZTQUtrQaK67 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D8d3HZTQUtrQaK67 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-D8d3HZTQUtrQaK67 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D8d3HZTQUtrQaK67 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D8d3HZTQUtrQaK67 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D8d3HZTQUtrQaK67 .marker.cross{stroke:#333333;}#mermaid-svg-D8d3HZTQUtrQaK67 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D8d3HZTQUtrQaK67 p{margin:0;}#mermaid-svg-D8d3HZTQUtrQaK67 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-D8d3HZTQUtrQaK67 .cluster-label text{fill:#333;}#mermaid-svg-D8d3HZTQUtrQaK67 .cluster-label span{color:#333;}#mermaid-svg-D8d3HZTQUtrQaK67 .cluster-label span p{background-color:transparent;}#mermaid-svg-D8d3HZTQUtrQaK67 .label text,#mermaid-svg-D8d3HZTQUtrQaK67 span{fill:#333;color:#333;}#mermaid-svg-D8d3HZTQUtrQaK67 .node rect,#mermaid-svg-D8d3HZTQUtrQaK67 .node circle,#mermaid-svg-D8d3HZTQUtrQaK67 .node ellipse,#mermaid-svg-D8d3HZTQUtrQaK67 .node polygon,#mermaid-svg-D8d3HZTQUtrQaK67 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D8d3HZTQUtrQaK67 .rough-node .label text,#mermaid-svg-D8d3HZTQUtrQaK67 .node .label text,#mermaid-svg-D8d3HZTQUtrQaK67 .image-shape .label,#mermaid-svg-D8d3HZTQUtrQaK67 .icon-shape .label{text-anchor:middle;}#mermaid-svg-D8d3HZTQUtrQaK67 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-D8d3HZTQUtrQaK67 .rough-node .label,#mermaid-svg-D8d3HZTQUtrQaK67 .node .label,#mermaid-svg-D8d3HZTQUtrQaK67 .image-shape .label,#mermaid-svg-D8d3HZTQUtrQaK67 .icon-shape .label{text-align:center;}#mermaid-svg-D8d3HZTQUtrQaK67 .node.clickable{cursor:pointer;}#mermaid-svg-D8d3HZTQUtrQaK67 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-D8d3HZTQUtrQaK67 .arrowheadPath{fill:#333333;}#mermaid-svg-D8d3HZTQUtrQaK67 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-D8d3HZTQUtrQaK67 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-D8d3HZTQUtrQaK67 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D8d3HZTQUtrQaK67 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-D8d3HZTQUtrQaK67 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D8d3HZTQUtrQaK67 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-D8d3HZTQUtrQaK67 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-D8d3HZTQUtrQaK67 .cluster text{fill:#333;}#mermaid-svg-D8d3HZTQUtrQaK67 .cluster span{color:#333;}#mermaid-svg-D8d3HZTQUtrQaK67 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-D8d3HZTQUtrQaK67 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-D8d3HZTQUtrQaK67 rect.text{fill:none;stroke-width:0;}#mermaid-svg-D8d3HZTQUtrQaK67 .icon-shape,#mermaid-svg-D8d3HZTQUtrQaK67 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D8d3HZTQUtrQaK67 .icon-shape p,#mermaid-svg-D8d3HZTQUtrQaK67 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-D8d3HZTQUtrQaK67 .icon-shape .label rect,#mermaid-svg-D8d3HZTQUtrQaK67 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D8d3HZTQUtrQaK67 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-D8d3HZTQUtrQaK67 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-D8d3HZTQUtrQaK67 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 网关排队
问题改写
Embedding
关键词/向量检索
Rerank
模型首Token
模型完成
输出校验
用户感知的流式体验主要受首 Token 延迟影响,但系统总耗时还包括完整输出和后处理。监控 P50、P95、P99,以及首 Token、每 Token 间隔和完成时间,不能只看平均响应时间。
三、上下文裁剪比换模型更先做
上下文应经过"过滤---排序---去重---压缩---限长"流程:
- 过滤无权限、过期和低相关文档。
- 按 Rerank 分数、版本和来源排序。
- 对重复或高度相似 Chunk 去重。
- 对长文本只保留与问题相关的句子,但保留引用定位。
- 设置输入 Token 硬上限。
java
public List<Evidence> trimContext(List<Evidence> evidence, int maxChars) {
Set<String> seen = new HashSet<>();
List<Evidence> result = new ArrayList<>();
int used = 0;
for (Evidence item : evidence) {
String fingerprint = normalize(item.content());
if (!seen.add(fingerprint)) continue;
if (used + item.content().length() > maxChars) break;
result.add(item);
used += item.content().length();
}
return List.copyOf(result);
}
裁剪不能只按字符串长度截断。切掉条款的例外条件会导致答案错误,因此最好按句子、列表项或语义单元裁剪,并保留文档标题、版本和页码。
四、缓存设计
缓存适合稳定、重复和可安全复用的数据:Embedding 结果、公开文档的检索结果、相同问题的最终答案。用户权限和知识库版本必须进入缓存键。
text
embedding:{model}:{textHash}
retrieve:{tenant}:{permissionHash}:{kbVersion}:{queryHash}
answer:{tenant}:{permissionHash}:{promptVersion}:{model}:{queryHash}
最终答案缓存需要注意两个问题:一是用户权限可能发生变化,二是文档更新后旧答案可能失效。缓存 TTL 不能代替版本失效,发布新知识库版本时要主动失效相关键。对高风险问题,可以只缓存检索结果,不缓存最终答案。
五、流式输出和取消
流式输出能降低用户感知等待,但不能解决模型总耗时。客户端断开后,服务端需要取消模型请求和工具任务,否则会出现用户看不到结果但后台仍持续消耗成本。
java
public Flux<String> stream(Publisher<AiEvent> events, CancellationToken token) {
return Flux.from(events)
.takeUntil(event -> token.isCancelled())
.map(AiEvent::safeText)
.doOnCancel(() -> {
token.cancel();
audit.record("client_cancelled");
});
}
对于有副作用的工具,取消请求不一定等于取消业务动作。必须区分"停止生成文本"和"停止已经提交的业务执行",后者需要状态机和补偿策略。
六、模型路由与备用模型
可以按任务复杂度选择模型:简单分类和查询使用低成本模型,复杂总结或多步推理使用更强模型。路由规则必须可解释,并记录实际命中模型。
java
public ModelRoute route(RequestProfile profile) {
if (profile.requiresTool() && profile.risk() == RiskLevel.HIGH) {
return ModelRoute.of("quality-model", Duration.ofSeconds(30));
}
if (profile.inputTokens() < 600 && profile.intent() == Intent.CLASSIFY) {
return ModelRoute.of("fast-model", Duration.ofSeconds(8));
}
return ModelRoute.of("balanced-model", Duration.ofSeconds(15));
}
备用模型切换需要注意上下文和工具兼容性。若备用模型不支持某种工具调用格式,不能在超时后无条件切换。高风险场景宁可降级为"展示证据并转人工",也不要为了成功率让不兼容模型执行动作。
七、限流、超时和熔断
限流至少分为用户、租户、接口和模型供应商四层。对 Token 而不是只对请求数限流,因为一个超长请求可能消耗几十个普通请求的资源。
java
public void checkQuota(AuthContext auth, UsageEstimate estimate) {
Quota quota = quotaService.current(auth.tenantId());
if (quota.remainingTokens() < estimate.inputTokens()) {
throw new QuotaExceededException("超过租户Token额度");
}
rateLimiter.acquire(auth.tenantId(), estimate.weight());
}
超时要分阶段设置:Embedding、检索、Rerank、模型首 Token、模型完成和工具调用分别配置。一个总超时不能替代阶段超时,否则某个下游卡住后很难定位。
熔断发生后,降级逻辑不能让模型凭空回答。可以选择关键词检索、返回已确认的缓存结果、提交异步任务或转人工。
八、重试会放大成本
只对明确的临时错误重试,并设置最大次数和指数退避。重试请求要带相同幂等键,特别是涉及工具调用时。每次重试增加的 Token 和供应商费用都要进入成本统计。
| 错误 | 是否通常重试 | 说明 |
|---|---|---|
| 认证失败 | 否 | 修复配置或权限 |
| 参数校验失败 | 否 | 修改输入 |
| 429 限流 | 有条件 | 按 Retry-After 退避 |
| 网络超时 | 有条件 | 限次、幂等 |
| 供应商 5xx | 有条件 | 备用模型或降级 |
| 引用校验失败 | 最多一次 | 第二次失败转人工 |
无限重试既可能制造账单,也可能造成工具重复执行。
九、成本报表
按天、租户、应用、模型、工作流和用户群统计:请求数、输入输出 Token、平均成本、P95 延迟、失败率、重试率和人工转接率。成本报表要能回答"钱花在什么场景、哪个版本、哪个租户、哪类问题"。
高成本请求要保存原因标签,例如上下文过长、工具轮次过多、重复提问、超时重试或模型路由错误。没有原因标签,成本优化只能靠猜。
十、上线检查
- 是否记录输入输出 Token 和每阶段耗时。
- 缓存键是否包含租户、权限和知识库版本。
- 客户端断开后是否取消可取消的后台请求。
- 工具调用是否使用幂等键。
- 是否按阶段设置超时和限流。
- 备用模型是否支持相同的输出和工具协议。
- 熔断后是否有安全降级,而非无依据回答。
- 是否有租户级成本预算和告警。
十一、一次可复现的性能实验
建议建立三个固定场景:短问题、长上下文问题和需要工具调用的问题。每个场景分别用 1、5、20、50 并发运行,预热后重复多轮,记录首 Token、完整耗时、输入输出 Token、错误率、重试次数和峰值显存或连接数。不要只跑一次,因为网络抖动和供应商排队会影响结果。
实验报告中至少包含:代码提交号、模型与 Prompt 版本、知识库版本、缓存状态、测试机器、并发、样本数量和统计方式。P95 应说明是对所有请求统计,还是只统计成功请求;失败请求不能被简单剔除,否则结果会过于乐观。
还可以做一个"上下文裁剪前后"的对照:保持模型和问题集不变,只改变证据条数与去重策略,再比较引用准确率、输入 Token 和延迟。如果质量没有下降而成本明显降低,才说明裁剪真正有效。
十二、预算与配额
成本治理最好有租户级预算、应用级预算和单请求硬上限。预算接近阈值时,可以降低非关键场景的模型级别或暂停批量任务;高风险业务则应转人工,而不是悄悄降低质量。所有预算拒绝和降级都要对用户给出明确提示,并在审计中记录。
十三、总结
性能和成本优化的第一步是测量,不是换模型。先把 Token、检索候选、Rerank、模型首 Token、完成时间、重试和工具轮次记录下来,再针对最昂贵、最慢或最容易失败的阶段优化。
一个成熟的 AI 系统应该在"质量、成本、延迟、可靠性"之间做明确取舍:普通问题追求快和便宜,高风险问题追求可验证和可追责,所有降级都必须保留安全边界。
读者讨论
如果你的 AI 应用费用突然上升,建议先按"输入 Token、重试次数、上下文条数、工具轮次"四项拆账,通常很快能定位主因。