Java 后端接入大模型:从 Token、并发到推理成本的完整估算方法

Java 后端接入大模型:从 Token、并发到推理成本的完整估算方法

摘要:大模型接口不是普通 HTTP 接口,Java 后端接入前不能只看 QPS,还要看输入 Token、输出 Token、首字延迟、整体耗时、重试和峰值并发。本文用一套可落地的估算方法,说明如何在 Spring Boot 服务里记录 LLM 调用耗时、Token 用量和成本,并给出上线前的限流、缓存、降级和监控检查清单。示例代码用于工程设计说明,成本单价请以你使用的模型供应商最新官方计费为准。

1. 为什么大模型接口不能按普通 HTTP 接口估算

很多后端系统接第三方接口时,习惯先问两个指标:

  • QPS 能到多少;
  • 平均 RT 是多少。

接大模型接口时,这两个指标还不够。

原因是 LLM 调用的成本和耗时都不是固定值。一条请求可能只问一句话,也可能带上几千字上下文;一次回答可能只有 100 个字,也可能输出一整段代码。输入越长、输出越长,推理时间和费用通常都会上升。

更准确的估算要看这几个变量:

指标 含义 为什么重要
输入 Token Prompt、上下文、历史消息、检索结果 影响请求成本和模型处理时间
输出 Token 模型实际生成的内容长度 影响成本、接口耗时和用户等待时间
首字延迟 从请求发出到第一个 token 返回 影响流式输出体验
总耗时 完整回答生成完成的时间 影响接口超时和线程占用
峰值并发 同一时间挂起的 LLM 请求数 影响连接池、线程池和预算消耗
重试次数 超时、限流或网络异常后的再次调用 直接放大成本和流量

所以,Java 后端接入大模型前,至少要回答三个问题:

2. 一个最小成本公式

大模型接口常见计费方式是按输入 Token 和输出 Token 分别计费。不同供应商、不同模型、不同缓存策略的价格不一样,但估算公式大体可以抽象成:

复制代码
单次调用成本 = 输入 Token 数 * 输入单价 + 输出 Token 数 * 输出单价

如果要估算一天成本:

复制代码
日成本 = 日调用次数 * 单次平均成本 * 重试放大系数

如果要估算峰值预算:

复制代码
峰值小时成本 = 峰值小时调用次数 * 单次 P95 成本 * 重试放大系数

这里建议不要只用平均值。上线前至少记录三组口径:

口径 用途
avg 估算日常预算
p95 估算高峰期和容量风险
max 发现异常 Prompt、超长上下文和失控输出

一个简单示例:

复制代码
平均输入:1200 tokens
平均输出:500 tokens
输入单价:每 1000 tokens 0.001 元
输出单价:每 1000 tokens 0.004 元

单次成本 = 1200 / 1000 * 0.001 + 500 / 1000 * 0.004
        = 0.0012 + 0.002
        = 0.0032 元

这只是示例价格,不代表任何供应商真实报价。工程里不要把价格写死在代码里,建议放到配置中心,按模型维度维护。

3. 环境和前置条件

本文示例按下面技术栈理解:

项目 版本/说明
JDK 17 或 21
Spring Boot 3.x
HTTP Client JDK HttpClient、OkHttp、WebClient 均可
调用方式 普通非流式接口示例
成本数据 来自模型接口返回的 usage 字段,或网关侧统计

验证状态说明:

  • Token 计费公式来自主流大模型 API 的通用计费模型,具体字段名和单价以供应商官方文档为准。
  • Java 示例代码用于说明后端工程落地方式,未绑定某一个供应商 SDK。
  • 如果你的接口经过 LiteLLM、OpenRouter、自建模型网关或公司内部网关,优先在网关层统一记录 Token 和费用。

4. 推荐先定义一份调用指标模型

不要等接完模型再补监控。建议先定义 LLM 调用记录模型,后续无论换供应商、换模型还是接入网关,都能沉淀统一指标。

java 复制代码
`import java.math.BigDecimal;
import java.time.Instant;

public class LlmCallMetric {

    private String requestId;
    private String bizType;
    private String provider;
    private String model;

    private int promptTokens;
    private int completionTokens;
    private int totalTokens;

    private long firstTokenLatencyMs;
    private long totalLatencyMs;
    private boolean success;
    private String errorCode;

    private BigDecimal estimatedCost;
    private Instant createdAt;

    // getter/setter 省略
}
`

字段建议不要只记 totalTokens。输入和输出要拆开,因为两者价格通常不同,优化手段也不同:

  • 输入 Token 高:重点优化 Prompt、历史消息、RAG 返回片段、系统提示词;
  • 输出 Token 高:重点限制 max_tokens,优化回答格式,控制模型啰嗦程度;
  • 总耗时高:重点看模型、供应商、网络、输出长度和是否流式返回;
  • 失败率高:重点看限流、超时、鉴权、网关错误和重试策略。

5. 用配置维护模型价格,不要写死在代码里

可以先用 application.yml 做最小配置,生产环境建议迁移到配置中心。

复制代码
llm:
  cost:
    models:
      gpt-4o-mini:
        input-price-per-1k: 0.001
        output-price-per-1k: 0.004
      qwen-plus:
        input-price-per-1k: 0.0008
        output-price-per-1k: 0.002
  guardrail:
    max-prompt-tokens: 6000
    max-completion-tokens: 1200
    daily-budget: 200

对应的配置类可以这样写:

java 复制代码
`import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "llm.cost")
public class LlmCostProperties {

    private Map<String, ModelCost> models = new HashMap<>();

    public Map<String, ModelCost> getModels() {
        return models;
    }

    public void setModels(Map<String, ModelCost> models) {
        this.models = models;
    }

    public static class ModelCost {
        private BigDecimal inputPricePer1k;
        private BigDecimal outputPricePer1k;

        public BigDecimal getInputPricePer1k() {
            return inputPricePer1k;
        }

        public void setInputPricePer1k(BigDecimal inputPricePer1k) {
            this.inputPricePer1k = inputPricePer1k;
        }

        public BigDecimal getOutputPricePer1k() {
            return outputPricePer1k;
        }

        public void setOutputPricePer1k(BigDecimal outputPricePer1k) {
            this.outputPricePer1k = outputPricePer1k;
        }
    }
}
`

成本计算服务:

java 复制代码
`import java.math.BigDecimal;
import java.math.RoundingMode;

public class LlmCostCalculator {

    private final LlmCostProperties properties;

    public LlmCostCalculator(LlmCostProperties properties) {
        this.properties = properties;
    }

    public BigDecimal calculate(String model, int promptTokens, int completionTokens) {
        LlmCostProperties.ModelCost cost = properties.getModels().get(model);
        if (cost == null) {
            throw new IllegalArgumentException("Unknown model cost config: " + model);
        }

        BigDecimal inputCost = BigDecimal.valueOf(promptTokens)
                .divide(BigDecimal.valueOf(1000), 6, RoundingMode.HALF_UP)
                .multiply(cost.getInputPricePer1k());

        BigDecimal outputCost = BigDecimal.valueOf(completionTokens)
                .divide(BigDecimal.valueOf(1000), 6, RoundingMode.HALF_UP)
                .multiply(cost.getOutputPricePer1k());

        return inputCost.add(outputCost).setScale(6, RoundingMode.HALF_UP);
    }
}
`

这个服务不依赖具体模型供应商。只要你能拿到 promptTokenscompletionTokens,就能统一估算成本。

6. 在调用层记录耗时、Token 和异常

大多数模型 API 会在响应里返回 usage 信息,例如:

复制代码
{
  "model": "example-model",
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "..."
      }
    }
  ],
  "usage": {
    "prompt_tokens": 1200,
    "completion_tokens": 500,
    "total_tokens": 1700
  }
}

Java 调用层可以按下面方式记录指标。这里用伪客户端接口表示真实模型调用,方便把重点放在指标采集上。

java 复制代码
`import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;

public class LlmClientWrapper {

    private final RawLlmClient rawLlmClient;
    private final LlmCostCalculator costCalculator;
    private final LlmMetricRepository metricRepository;

    public LlmClientWrapper(RawLlmClient rawLlmClient,
                            LlmCostCalculator costCalculator,
                            LlmMetricRepository metricRepository) {
        this.rawLlmClient = rawLlmClient;
        this.costCalculator = costCalculator;
        this.metricRepository = metricRepository;
    }

    public String chat(String requestId, String bizType, String model, String prompt) {
        Instant start = Instant.now();
        LlmCallMetric metric = new LlmCallMetric();
        metric.setRequestId(requestId);
        metric.setBizType(bizType);
        metric.setProvider("model-gateway");
        metric.setModel(model);
        metric.setCreatedAt(start);

        try {
            LlmResponse response = rawLlmClient.chat(model, prompt);
            long totalLatencyMs = Duration.between(start, Instant.now()).toMillis();

            int promptTokens = response.usage().promptTokens();
            int completionTokens = response.usage().completionTokens();
            BigDecimal cost = costCalculator.calculate(model, promptTokens, completionTokens);

            metric.setPromptTokens(promptTokens);
            metric.setCompletionTokens(completionTokens);
            metric.setTotalTokens(response.usage().totalTokens());
            metric.setTotalLatencyMs(totalLatencyMs);
            metric.setEstimatedCost(cost);
            metric.setSuccess(true);

            return response.content();
        } catch (Exception ex) {
            metric.setSuccess(false);
            metric.setErrorCode(ex.getClass().getSimpleName());
            metric.setTotalLatencyMs(Duration.between(start, Instant.now()).toMillis());
            throw ex;
        } finally {
            metricRepository.save(metric);
        }
    }
}
`

RawLlmClientLlmMetricRepository 可以先定义成接口:

java 复制代码
`public interface RawLlmClient {
    LlmResponse chat(String model, String prompt);
}

public interface LlmMetricRepository {
    void save(LlmCallMetric metric);
}

public record LlmResponse(String content, Usage usage) {
}

public record Usage(int promptTokens, int completionTokens, int totalTokens) {
}
`

如果你使用的是流式接口,还要额外记录首字延迟。典型做法是在收到第一个 chunk 时记录 firstTokenLatencyMs,在流结束时记录 totalLatencyMs

7. 上线前先做一张容量估算表

接入大模型前,建议至少准备一张这样的估算表:

业务场景 日调用量 峰值 QPS 平均输入 Token 平均输出 Token P95 耗时 单日预算
智能客服摘要 10000 5 1500 300 4s 按配置计算
文章标题生成 2000 2 800 120 2s 按配置计算
代码解释助手 1000 1 3000 800 8s 按配置计算

然后按下面公式换算峰值并发:

复制代码
峰值并发数 ≈ 峰值 QPS * P95 接口耗时秒数

例如:

复制代码
峰值 QPS = 5
P95 耗时 = 8 秒
峰值并发数 ≈ 40

这意味着同一时间可能有 40 个请求挂在模型调用上。如果你是同步阻塞调用,就要检查:

  • Tomcat/Undertow 工作线程是否会被占满;
  • HTTP Client 连接池是否足够;
  • 上游接口超时时间是否合理;
  • 网关是否有整体超时;
  • 用户是否能接受 8 秒等待。

对于耗时较长的任务,例如报告生成、代码解释、长文摘要,通常不建议一直同步阻塞。更稳的方式是改成异步任务:

复制代码
用户提交任务
  -> 立即返回 taskId
  -> 后端异步调用 LLM
  -> 前端轮询或 WebSocket 获取结果

8. 成本失控通常不是模型贵,而是上下文失控

LLM 成本失控最常见的几个原因:

问题 典型表现 处理方式
历史消息无限追加 对话越久越慢、越贵 做消息窗口和摘要压缩
RAG 返回片段过多 每次输入 token 很高 限制 topK,做重排和截断
System Prompt 过长 所有请求都带一大段固定文本 拆分场景 Prompt,删除无效规则
输出不受控 模型生成长篇解释 设置 max_tokens 和输出格式
异常重试过多 失败时成本倍增 区分可重试和不可重试错误

上线前可以加一层 Prompt 预算检查:

java 复制代码
`public class LlmRequestGuard {

    private final int maxPromptTokens;
    private final TokenEstimator tokenEstimator;

    public LlmRequestGuard(int maxPromptTokens, TokenEstimator tokenEstimator) {
        this.maxPromptTokens = maxPromptTokens;
        this.tokenEstimator = tokenEstimator;
    }

    public void checkPrompt(String prompt) {
        int estimatedTokens = tokenEstimator.estimate(prompt);
        if (estimatedTokens > maxPromptTokens) {
            throw new IllegalArgumentException(
                    "Prompt too long, estimated tokens=" + estimatedTokens
                            + ", max=" + maxPromptTokens);
        }
    }
}
`

TokenEstimator 可以先做粗略估算,生产环境最好使用模型供应商推荐的 tokenizer 或网关侧真实 usage 回填。

java 复制代码
`public interface TokenEstimator {
    int estimate(String text);
}

public class SimpleTokenEstimator implements TokenEstimator {
    @Override
    public int estimate(String text) {
        if (text == null || text.isBlank()) {
            return 0;
        }
        return Math.max(1, text.length() / 2);
    }
}
`

注意:text.length() / 2 只是非常粗的兜底估算,不适合做精确计费。它的价值是提前拦截明显过长的请求,真实成本仍应以接口返回 usage 为准。

9. 超时、重试和降级要提前定规则

大模型接口慢,不一定是异常。模型输出越长,耗时越长,这是正常现象。但后端系统不能无限等待。

建议按业务类型设置不同超时:

场景 建议策略
在线问答 短超时 + 流式输出 + 失败提示
后台摘要 中等超时 + 异步任务 + 可重试
批量生成 队列削峰 + 并发限制 + 预算控制
核心交易链路 尽量不要强依赖 LLM 实时返回

重试策略要特别谨慎。不是所有错误都应该重试:

错误类型 是否建议重试
网络瞬断 可以有限重试
429 限流 等待退避后重试,或降级
5xx 服务异常 可以有限重试
400 参数错误 不重试
上下文超长 不重试,先裁剪输入
余额不足 / 权限错误 不重试,告警处理

一个简单的降级顺序:

复制代码
优先使用高质量模型
  -> 超时或预算紧张时切换便宜模型
  -> 仍失败时返回模板化兜底结果
  -> 非核心功能可提示稍后重试

如果你通过模型网关统一调用,可以把降级规则放在网关层;如果是业务服务直连模型供应商,至少也要在业务服务里把超时、重试和预算保护写清楚。

10. 监控指标建议

LLM 接入上线后,建议至少看下面几类指标:

复制代码
llm_request_total{bizType,model,success}
llm_latency_ms{bizType,model}
llm_prompt_tokens{bizType,model}
llm_completion_tokens{bizType,model}
llm_estimated_cost{bizType,model}
llm_error_total{bizType,model,errorCode}

如果用 Micrometer,可以在保存指标时同步打点:

java 复制代码
`import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;

public class LlmMetricsRecorder {

    private final MeterRegistry meterRegistry;

    public LlmMetricsRecorder(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    public void record(LlmCallMetric metric) {
        Tags tags = Tags.of(
                "bizType", metric.getBizType(),
                "model", metric.getModel(),
                "success", String.valueOf(metric.isSuccess())
        );

        meterRegistry.counter("llm_request_total", tags).increment();
        meterRegistry.timer("llm_latency", tags).record(
                java.time.Duration.ofMillis(metric.getTotalLatencyMs())
        );
        meterRegistry.summary("llm_prompt_tokens", tags).record(metric.getPromptTokens());
        meterRegistry.summary("llm_completion_tokens", tags).record(metric.getCompletionTokens());
        meterRegistry.summary("llm_estimated_cost", tags).record(metric.getEstimatedCost().doubleValue());
    }
}
`

告警可以先从三类开始:

  • 费用告警:小时成本或日成本超过阈值;
  • 性能告警:P95/P99 耗时持续升高;
  • 稳定性告警:限流、超时、5xx、余额不足等错误增加。

11. 上线前检查清单

最后给一份 Java 后端接入大模型前的检查清单:

检查项 是否必须 说明
Token usage 记录 至少记录输入、输出、总 Token
成本配置化 不要把模型单价写死在代码里
超时配置 区分在线接口和后台任务
重试上限 防止失败请求放大成本
Prompt 长度限制 防止上下文失控
输出长度限制 控制耗时和费用
业务维度监控 按 bizType 统计,不要只看总量
降级策略 建议 高峰、超时、预算紧张时要有退路
异步任务化 视场景 长耗时生成任务不要长期阻塞请求线程
日预算告警 建议 早期尤其重要

如果只能先做三件事,优先做:

12. 总结

Java 后端接入大模型,真正的难点不是把 HTTP 请求调通,而是把成本、延迟、并发和失败策略纳入工程治理。

普通接口通常按 QPS 和 RT 估算容量;LLM 接口还必须把 Token、输出长度、重试、峰值并发和预算一起算进去。否则 Demo 阶段看起来没问题,一旦流量上来,就容易遇到接口变慢、线程被占满、账单超预期、重试放大故障这些问题。

更稳的落地方式是:先统一记录 usage 和耗时,再配置化管理模型单价和预算,最后按业务场景设计限流、缓存、异步化和降级策略。

如果你关注 Java 后端、Spring Boot、MCP/Agent 落地和线上问题排查,可以关注我的 CSDN 专栏,后面会继续拆解更接近生产环境的 Java AI 工程实践。

相关推荐
BlackTurn2 小时前
技术经理投标
java
YG亲测源码屋2 小时前
java配置环境变量、jdk环境变量配置、java环境变量设置方法
java·开发语言
MIUMIUKK2 小时前
从语法层面,看懂 Python 的特殊处
java·开发语言·python
hujinyuan201602 小时前
2026年3月 中国电子学会青少年软件编程(Python)三级考试试卷 真题及答案
java·python·算法
装不满的克莱因瓶2 小时前
学习 Agent 基础概念及不同 Agent 的适用场景
人工智能·ai·大模型·llm·智能体
dozenyaoyida2 小时前
AI与大模型新闻日报 | 2026-06-01
人工智能·ai·大模型·新闻
basketball6162 小时前
C++ 高级编程:2. 基本线程池实现
java·开发语言·c++
MageGojo3 小时前
天气 API 接入实战:基于 ApiZero 实现实时天气、分钟级降水和 15 天预报查询
java·后端·spring·api 接口接入·接口实战
自动跟随3 小时前
UWB自动跟随技术全栈解析:从定位算法到“位控一体化“
java·网络·人工智能