大模型就是一个 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、用任何中间件没有区别------配连接、发请求、收响应、处理超时、记日志、加重试。
几个实在话:
- 先调通,别看原理。 你学 Spring Boot 是先写
@RestController,不是先看 Servlet 规范。大模型一样------先用 API,原理以后再说。 - API Key 走环境变量。 别写死在
application.yml里然后推到 Git 上。 - Temperature 不确定就用 0。 保守配置不出错,跟生产环境一个道理。
- System Prompt 要测试、要版本管理。 改了一句话效果可能天差地别------跟改数据库配置一样,改完测一下。
- 加超时、加重试、记好日志。 这是 HTTP 调用,它会超时、会 429、会 500。你写 RestTemplate 怎么处理的,这里一样处理。
- Token 就是钱。 每次调用后看一眼
usage.total_tokens,养成习惯,跟看慢查询日志一样。
下次产品经理说"接个大模型",你内心可以翻译成:"加一个 POST 请求,目标地址 api.deepseek.com,入参 JSON,出参 JSON,超时设 30 秒,失败重试 2 次,需要申请一个新的 API Key 环境变量。"
跟对接一个短信接口没有任何区别------唯一的区别是这个接口返回的文字是自己写的。