大模型到底是什么——写给Java开发者的白话指南

大模型就是一个 HTTP 接口。你 POST 一段文字过去,它返回一段文字回来。 跟你调过的短信网关、支付回调、OCR 识别没有任何本质区别------只不过这个接口背后是一个几千亿参数的函数,而且每次返回的内容可能不一样。

去年我们组接需求:给客服系统加"智能问答"。产品经理说:"就接个大模型,OpenAI 有 API,调一下就行。"

我调了两天。这两天主要花在理解几个概念上------这些概念一旦用 Java 常识去套,三分钟就懂了。这篇文章就是我当时希望有人提前写给我的。

先给一张对照表,然后逐个展开:

大模型概念 Java 世界里的等价物 一句话
Token String.split("") 文本切片,计费单位
Context Window JVM 堆内存 (-Xmx) 一次能塞多少东西,超了就溢出
Temperature GC 调优激进程度 保守=稳定,激进=有创意但可能崩
System Prompt application.yml 全局配置,每次请求都生效

Token:就是 String.split(""),只不过切得更聪明

vbnet 复制代码
String text = "大模型是什么";
String[] chars = text.split("");
// ["大", "模", "型", "是", "什", "么"]  ← 6 个"字符"

// Token 不是按字符切的,但逻辑类似:
// "大模型是什么" → 约 4-6 个 Token(取决于具体模型的分词算法)
// "Hello world"  → ["Hello", " world"]  → 2 个 Token(注意空格属于后一个)

中文大致 1-1.5 个字符 ≈ 1 个 Token,英文大致 1 个单词 ≈ 1-2 个 Token。你不需要知道分词算法怎么写,你只需要知道 API 按这个计费。

用 Java 调一次 API,响应里直接返回消耗了多少 Token:

arduino 复制代码
// 使用 JDK 内置 HttpClient(Java 11+),零额外依赖
HttpClient client = HttpClient.newHttpClient();
String apiKey = System.getenv("DEEPSEEK_API_KEY"); // 别硬编码!

String body = """
    {
      "model": "deepseek-chat",
      "messages": [{"role": "user", "content": "什么是Java的volatile关键字?"}],
      "temperature": 0
    }""";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.deepseek.com/v1/chat/completions"))
    .header("Authorization", "Bearer " + apiKey)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

// 解析 usage,看花了多少钱
// response body 里:
// "usage": {"prompt_tokens": 15, "completion_tokens": 120, "total_tokens": 135}
// DeepSeek 当前约 1 元/百万 Token → 这次调用花了 0.000135 元

记住这个就够了:Token 是计费单位。英文比中文便宜(Token 更少)。想看具体消耗,看响应里的 usage 字段。


Context Window:就是 JVM 堆内存

调过 JVM 参数的人都懂这一段:

bash 复制代码
# JVM:堆越大,能装的对象越多 → 但 GC 暂停越长
-Xms2g -Xmx4g

# 大模型:Context Window 越大,一次能塞的上下文越多 → 但推理越慢、越贵
# DeepSeek 默认 128K tokens,约等于一本中篇小说的长度

Context Window = 大模型一次性能"看到"的最大 Token 数。 输入(你的 prompt)+ 输出(模型的回复),加起来不能超过这个窗口。

超了会怎样?不会报错,会悄悄丢掉最前面的内容。 这比 OOM 还坑------系统不崩,但前面的对话"失忆"了。

所以长对话要做滑动窗口管理,跟 JVM 里的对象池一个思路:

java 复制代码
public class ChatWindowManager {
    private final Deque<Message> messages = new ArrayDeque<>();
    private static final int MAX_TOKENS = 120_000; // 留 8K 余量给回复
    private int estimatedTokens = 0;

    public void append(Message msg) {
        messages.addLast(msg);
        estimatedTokens += estimate(msg); // 粗略估算:中文 1 字≈1 token,英文 1 词≈1.5 token

        // 超了就从头开始丢弃
        while (estimatedTokens > MAX_TOKENS && !messages.isEmpty()) {
            Message removed = messages.removeFirst();
            estimatedTokens -= estimate(removed);
        }
    }

    private int estimate(Message msg) {
        return msg.content().length(); // 简单估算法,生产环境可用更精确的计数器
    }

    public List<Message> snapshot() {
        return new ArrayList<>(messages); // 发给 API 的最终消息列表
    }
}

这就是为什么你跟 ChatGPT 聊了五十轮以后,它会忘记你第一句说了什么------前面的被"GC"掉了。


Temperature:就是 GC 调优的激进程度

ruby 复制代码
# GC 配置:
-XX:+UseG1GC          # 保守分区回收 → 稳定但吞吐一般
-XX:+UseParallelGC     # 激进并行回收 → 吞吐高但偶尔卡顿
-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC  # 不回收 → 迟早崩

# Temperature:
temperature = 0.0  →  保守:每次都选概率最高的词 → 稳定、可复现
temperature = 0.7  →  适中:大部分选高概率词,偶尔挑个冷门的
temperature = 1.5  →  激进:什么词都敢选 → 有创意但随时胡说八道

Temperature 控制输出的"随机性"。 0 意味着同一个问题问 100 次,答案几乎一模一样。1.5 意味着每次都不一样,而且可能跑偏。

scss 复制代码
// 不同场景选不同值------就跟生产环境选 GC 一样,要的就是"稳"还是"快"
public enum TemperaturePreset {
    CODE_GENERATION(0.0),    // 写代码 → 要准确,不要创意
    TRANSLATION(0.1),        // 翻译 → 基本确定,偶尔允许措辞变化
    Q_and_A(0.3),            // 问答 → 略微灵活
    WRITING(0.7),            // 写文章 → 需要一些发散
    BRAINSTORMING(1.0),      // 头脑风暴 → 多点想法
    POETRY(1.5);             // 写诗 → 越离谱越有意思,崩了拉倒

    public final double value;
    TemperaturePreset(double value) { this.value = value; }
}

// 调用时:
String requestBody = """
    {
      "model": "deepseek-chat",
      "messages": [{"role": "user", "content": "把这行Python转成Java:print('hello')"}],
      "temperature": %s
    }""".formatted(TemperaturePreset.CODE_GENERATION.value);

实用建议:不确定就用 0。 就像生产环境不确定就用 G1。先跑通,再调参。


System Prompt:就是 application.yml

yaml 复制代码
# application.yml ------ 定义整个 Spring Boot 应用的全局行为
spring:
  messages:
    basename: i18n/messages
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai
rust 复制代码
// System Prompt ------ 定义大模型在整段对话里的全局行为
// 用户看不到这条消息,但模型每一轮回复都会遵守它

String systemPrompt = """
    你是一个 Java 技术客服。规则如下:
    - 所有代码示例使用 Java 17+ 语法(record、sealed class、text block)
    - 数据库相关只推荐 MyBatis-Plus,不要提 JPA
    - 不确定的问题直接说"不确定",严禁编造
    - 回答用中文,代码注释用英文
    - 如果用户问非 Java 问题,礼貌引导回 Java 方向
    """;

// 放进 messages 数组的第一个元素(role 是 system 而非 user):
String body = """
    {
      "model": "deepseek-chat",
      "messages": [
        {"role": "system", "content": "%s"},
        {"role": "user", "content": "怎么连接数据库?"}
      ],
      "temperature": 0.3
    }""".formatted(systemPrompt.replace("\n", "\n").replace(""", "\""));

就跟 application.yml 管全局一样,System Prompt 管整段对话。 你会把 application.yml 放进 Git 管理,System Prompt 也应该放进去------写好、测试、版本管理,不要每次在代码里临时拼。


Hello World:三行核心代码调通第一个模型

依赖?不需要。Java 11 以上内置了 java.net.http.HttpClient。我用 DeepSeek(注册送 500 万 Token,不要钱)。

完整可运行代码:

arduino 复制代码
import java.net.URI;
import java.net.http.*;

public class FirstLLMCall {
    public static void main(String[] args) throws Exception {
        // 第 1 行:拿 API Key(从环境变量,别写死在代码里)
        String apiKey = System.getenv("DEEPSEEK_API_KEY");

        // 第 2 行:构建请求(model + messages + temperature)
        String jsonBody = """
            {
              "model": "deepseek-chat",
              "messages": [{"role": "user", "content": "用Java写一个Hello World"}],
              "temperature": 0
            }""";

        // 第 3 行:发请求,收响应
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.deepseek.com/v1/chat/completions"))
            .header("Authorization", "Bearer " + apiKey)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
            .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.body());
        // 输出里取 "choices"[0]["message"]["content"] 就是模型的回答
    }
}

运行:

ini 复制代码
export DEEPSEEK_API_KEY="sk-你的key"
javac FirstLLMCall.java && java FirstLLMCall

输出大概长这样:

swift 复制代码
{
  "id": "chatcmpl-xxx",
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "以下是一个简单的Java Hello World程序:\n\n```java\npublic class HelloWorld {\n    public static void main(String[] args) {\n        System.out.println("Hello, World!");\n    }\n}\n```"
      }
    }
  ],
  "usage": { "prompt_tokens": 10, "completion_tokens": 45, "total_tokens": 55 }
}

就这。 POST 一个 JSON,收一个 JSON,取 choices[0].message.content。跟你调过的任何第三方 REST API 一模一样。


聊完了,收拾盘子

大模型不是魔法。你用它的方式和你用 Redis、用 Elasticsearch、用任何中间件没有区别------配连接、发请求、收响应、处理超时、记日志、加重试。

几个实在话:

  1. 先调通,别看原理。 你学 Spring Boot 是先写 @RestController,不是先看 Servlet 规范。大模型一样------先用 API,原理以后再说。
  2. API Key 走环境变量。 别写死在 application.yml 里然后推到 Git 上。
  3. Temperature 不确定就用 0。 保守配置不出错,跟生产环境一个道理。
  4. System Prompt 要测试、要版本管理。 改了一句话效果可能天差地别------跟改数据库配置一样,改完测一下。
  5. 加超时、加重试、记好日志。 这是 HTTP 调用,它会超时、会 429、会 500。你写 RestTemplate 怎么处理的,这里一样处理。
  6. Token 就是钱。 每次调用后看一眼 usage.total_tokens,养成习惯,跟看慢查询日志一样。

下次产品经理说"接个大模型",你内心可以翻译成:"加一个 POST 请求,目标地址 api.deepseek.com,入参 JSON,出参 JSON,超时设 30 秒,失败重试 2 次,需要申请一个新的 API Key 环境变量。"

跟对接一个短信接口没有任何区别------唯一的区别是这个接口返回的文字是自己写的。

相关推荐
huzhongqiang1 小时前
用元类实现类属性:打造更优雅的服务访问机制
后端·python
understandme1 小时前
CI/CD 坑点 记录
后端
只做人间不老仙1 小时前
C++ grpc 元数据示例学习
后端·grpc
程序员陆业聪1 小时前
数据压缩与缓存策略:把带宽用到极致 | Android网络优化系列(4)
后端
詩飛1 小时前
Spring Boot 事务管理完全指南
后端
程序员陆业聪1 小时前
网络监控与容灾:让网络问题无处遁形 | Android网络优化系列(5·完结)
后端
fliter2 小时前
Rust 能帮你捕获什么,又不能捕获什么
后端
ZHOUPUYU2 小时前
PHP8高性能Web开发实战指南
后端·html·php
fliter2 小时前
一个 Emoji 是怎么让 rust-analyzer 崩溃的
后端