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 工程实践。

相关推荐
To_OC8 小时前
数据集划分不是随便切:手把手切分大众点评情感数据集
人工智能·llm·agent
人活一口气11 小时前
Spring Boot与AIGC的完美结合:从零搭建智能内容生成平台
java·spring boot·aigc
想要成为糕糕手13 小时前
深入理解AI Agent工具调用:从原理到代码实现
llm·agent
yLDeveloper13 小时前
从矩阵乘法到多模态大模型 - LLM 篇
llm·nlp
像我这样帅的人丶你还13 小时前
Java 后端详解(三):全局异常处理与 JPA 数据库映射
java·后端
NE_STOP13 小时前
vibe Coding -- 小项目实战
java
前端君18 小时前
Claude Code 如何配置本地Ollama模型或别的模型(Deepseek等)
llm·agent·claude
Darling噜啦啦18 小时前
LLM 数据工程实战:从数据集划分到交叉验证——大模型智能的根基
llm
未秃头的程序猿19 小时前
Java 26正式发布!这3个新特性,让代码量直接减半
java·后端·面试
用户2986985301419 小时前
Word 文档文本查找与替换的 Java 实现方案
java·后端