JAVA调用大模型API
一.OpenAI接口协议
OpenAI接口协议是什么:
类似于HTTP是WEB世界的通用协议,OpenAI的Chat Completions API已经成为大模型API领域的事实标准之一。
OpenAI接口协议就类似于大模型世界的普通话. 无论是DeepSeek,Qwen,GLM,GPT,Claude Code,Gemini,都支持OpenAI接口协议.
在切换模型或者平台时,很多情况下只需要修改:
baseURL
apiKey
model
核心调用逻辑基本不用重写。
二.请求格式详解
调用大模型API,本质上就是发送一个HTTP POST请求,请求体通常是一个JSON。
下面是一个企业知识库问答助手的请求示例。
{
"model": "Qwen/Qwen3-32B",
"messages": [
{
"role": "system",
"content": "你是一个企业知识库问答助手,只回答公司制度、报销流程、项目问题。"
},
{
"role": "user",
"content": "公司的年假可以拆分使用吗?"
}
],
"temperature": 0.1,
"max_tokens": 512,
"stream": false
}
这个JSON中包含几个关键字段
model:指定要调用的模型messages:对话消息的数组temperature:控制回答的随机性max_tokens:限制模型输出的最大长度stream:是否开启流式返回
下面来进行逐个说明
1.model:指定要调用的模型
model 字段用于告诉平台:这次请求要调用哪个模型。
不同大模型的模型ID格式可能不同,推荐去模型官方文档查找.
2.messages:对话消息数组
messages 是整个请求中最核心的字段。
它是一个数组,数组中的每条消息都包含两个属性:
role
content
其中:
role表示这条消息的角色;content表示这条消息的内容。
模型并不是只看用户当前输入的这一句话,而是会读取整个 messages 数组。你可以把它理解为一段完整的对话记录。
模型会基于这段对话上下文生成回答。
![[Pasted image 20260512151612.png]]
3.角色机制
messages 数组中的每条消息都有一个 role。常见角色有三种:
systemuserassistant
3.1system:系统角色
system 消息用于定义模型的行为规则,相当于给模型一份“工作说明书”。
例如:
{
"role": "system",
"content": "你是一个企业知识库问答助手,只回答公司制度、报销流程、项目规范相关的问题。"
}
这个系统消息告诉模型:
- 你的身份是企业知识库问答助手;
- 你的回答范围是公司制度、报销流程、项目规范;
- 不相关的问题应该尽量避免回答。
比如用户问:
今晚吃什么?
模型就应该意识到这个问题不属于企业知识库问答范围.
3.2 user:用户角色
user 消息表示用户输入的问题。
例如:
{
"role": "user",
"content": "公司的年假可以拆分使用吗?"
}
这就是用户真正想问的问题。
3.3 assistant:助手角色
assistant 消息表示模型之前的回答,通常用于构建多轮对话上下文。
例如,一段多轮对话可以这样组织:
{
"messages": [
{"role": "system", "content": "你是一个企业知识库问答助手"},
{"role": "user", "content": "公司的年假可以拆分使用吗?"},
{"role": "assistant", "content": "可以。根据公司制度,年假支持按半天或整天为单位拆分使用,具体以审批系统中的可用余额为准。"},
{"role": "user", "content": "那需要提前几天申请?"}
]
}
模型看到这段上下文后,就能理解最后一句:
那需要提前几天申请?
问的是“年假申请需要提前几天”,而不是其他流程。
如果你只发送最后一句“那需要提前几天申请?”,模型就很难判断用户到底在问年假、报销、出差,还是其他审批流程。
4.大模型没有自动记忆,多轮对话需要手动传历史
这里有一个非常重要的点:
大模型 API 的每次调用都是独立的。
模型不会自动记住上一次 API 调用的内容。所谓多轮对话,本质上是开发者在每次请求中,把历史消息一起放进 messages 数组里,让模型看到完整上下文。
也就是说,多轮对话并不是模型自己“记住了”,而是你每次都把历史对话重新发给了模型。
这也是为什么对话越长,消耗的 Token 越多:
因为每次请求都要把历史消息重新发送一遍
5. system消息会影响模型的回答方式
同一个用户问题,如果 system 消息不同,模型的回答风格也会发生明显变化。
比如用户问题都是:
SpringBoot的自动装配底层原理是什么?
不同的 system 消息会得到不同风格的回答:
| system消息 | 用户问题 | 模型回答风格 |
|---|---|---|
| 你是专业的技术顾问 | SpringBoot的自动装配底层原理是什么? | 客观解释底层原理 |
| 你是一个面试官 | SpringBoot的自动装配底层原理是什么? | 用面试追问的方式引导回答 |
| 你是一个面向初学者的 技术导师 | SpringBoot的自动装配底层原理是什么? | 用通俗类比解释概念 |
这就是 system 消息的作用:
它可以从根本上影响模型的身份、语气、回答边界和输出风格.
三. 常用请求参数说明
除了 model 和 messages,还有几个常用参数需要掌握。
| 参数 | 类型 | 说明 |
|---|---|---|
temperature | float | 控制回答随机性,值越高回答越发散 |
max_tokens | int | 控制模型最多生成多少个 Token |
stream | boolean | 是否启用流式返回 |
四.非流式返回相应格式详解
当steam设置为false时,模型会返回一个完整的JSON
示例
{
"model": "deepseek-v4-flash",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "可以。根据公司制度,年假通常支持按半天或整天为单位拆分使用,具体可用天数和申请规则以公司审批系统中的制度说明为准。"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 46,
"completion_tokens": 58,
"total_tokens": 104
}
}
这里有几个关键字段
choiceschoices是模型回答的数组,一般情况下只有一个choices[0]元素,除非你设置了参数要求模型一次生成多个回答choices[0].message这是模型最终生成的回答,其中的content字段就是我们最终要展示给用户的内容finish_reasonfinish_reason表示模型停止生成的原因。 常见值如下:stop:标志正常结束,模型认为回答已经完整length:达到长度上限:模型输出的token已经达到max_tokens
usage用于统计token消耗
五、为什么很多厂商都兼容 OpenAI 协议
OpenAI 的 Chat Completions API 形成了较大的生态,很多框架、工具和教程都围绕这套协议展开。
例如:
- LangChain;
- Spring AI;
- 各类命令行工具;
- Postman 请求模板;
- 各种开源示例项目。
因此,很多大模型平台都会提供 OpenAI 兼容接口,降低开发者迁移成本。
六.非流式调用示例
非流式调用是最简单的大模型 API 调用方式。
它的特点是:
客户端发送请求,等待模型生成完毕后,一次性拿到完整回答。
就像调用普通 REST API 一样。
-
添加maven依赖
在
pom.xml中添加以下依赖:```
<dependencies>
<!-- OkHttp:HTTP 客户端 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- Gson:JSON 处理 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.13.1</version>
</dependency>
</dependencies>
这里使用 OkHttp,而不是 Spring 的 RestTemplate 或 WebClient,主要是因为 OkHttp 是纯 HTTP 客户端,不依赖 Spring 框架,代码更简洁,也方便在任意 Java 项目中使用
- 完整代码实现
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import okhttp3.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class NonStreamingChat {
private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";
private static final String API_KEY = "sk-4c715b5fdae44f609df899975256a8fb";
public static void main(String[] args) throws IOException {
// 1. 构建请求体 JSON
JsonObject requestBody = new JsonObject();
requestBody.addProperty("model", "deepseek-v4-flash");
requestBody.addProperty("temperature", 0);
requestBody.addProperty("max_tokens", 1024);
requestBody.addProperty("stream", false);
// 构建 messages 数组
JsonArray messages = new JsonArray();
// system 消息:定义模型的行为规则
JsonObject systemMsg = new JsonObject();
systemMsg.addProperty("role", "system");
systemMsg.addProperty("content", "你是一个企业知识库问答助手,回答要简洁明了。");
messages.add(systemMsg);
// user 消息:用户的问题
JsonObject userMsg = new JsonObject();
userMsg.addProperty("role", "user");
userMsg.addProperty("content", "公司的年假可以拆分使用吗?");
messages.add(userMsg);
requestBody.add("messages", messages);
// 2. 创建 OkHttp 客户端(设置超时时间,大模型响应可能较慢)
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
// 3. 构建 HTTP 请求
Request request = new Request.Builder()
.url(API_URL)
.addHeader("Authorization", "Bearer " + API_KEY)
.addHeader("Content-Type", "application/json")
.post(RequestBody.create(
requestBody.toString(),
MediaType.parse("application/json")
))
.build();
// 4. 发送请求并处理响应
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
System.out.println("请求失败,状态码:" + response.code());
System.out.println("错误信息:" + response.body().string());
return;
}
// 5. 解析 JSON 响应
String responseBody = response.body().string();
Gson gson = new Gson();
JsonObject jsonResponse = gson.fromJson(responseBody, JsonObject.class);
// 提取模型的回答
String answer = jsonResponse
.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.getAsJsonObject("message")
.get("content").getAsString();
// 提取 finish_reason String finishReason = jsonResponse
.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.get("finish_reason").getAsString();
// 提取 Token 用量
JsonObject usage = jsonResponse.getAsJsonObject("usage");
int promptTokens = usage.get("prompt_tokens").getAsInt();
int completionTokens = usage.get("completion_tokens").getAsInt();
int totalTokens = usage.get("total_tokens").getAsInt();
// 6. 打印结果
System.out.println("=== 模型回答 ===");
System.out.println(answer);
System.out.println();
System.out.println("=== 调用信息 ===");
System.out.println("结束原因:" + finishReason);
System.out.println("输入 Token:" + promptTokens);
System.out.println("输出 Token:" + completionTokens);
System.out.println("总 Token:" + totalTokens);
}
}
}
七.流式调用详解
1.为什么需要流式调用
非流式调用有一个体验问题:模型必须生成完整内容后,才会一次性返回结果。
如果回答比较短,这个问题不明显。
但如果回答比较长,比如用户问:
请总结一下公司研发流程文档中的代码评审规范
模型可能需要几秒甚至十几秒才能生成完整回答。在这段时间里,如果页面没有任何变化,用户很容易觉得系统卡住了。
流式调用就是为了解决这个问题。
开启流式调用后,模型每生成一小段内容,就会立刻推送给客户端。客户端收到一段就展示一段,用户看到的效果就是文字逐步出现。
这就是 ChatGPT、DeepSeek 网页端常见的“打字机效果”。
![[Pasted image 20260512162325.png]]
2. SSE协议简介
流式调用通常基于SSE,也就是Server-Sent Events,中文可以理解为服务端推送事件
普通HTTP请求是一问一答模式
客户端发送请求 - 服务端返回完整相应 - 连接关闭
SSE:
客户端发送请求 - 服务端持续推送数据块,保持连接 - 推送完成后关闭连接
每个数据块以data开头,当所有内容完毕后,服务端发送一个特殊标记:data:[DONE].
3. 流式响应的数据格式
流式相应和非流式相应的JSON结构有一个关键区别:
- 非流式相应中,模型回答在
choices[0].message里 - 流式响应中,每个数据块的增量在
choices[0].delta里.
一个完整的流式相应的数据流大致如下
data: {"id":"chatcmpl-abc123","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","choices":[{"index":0,"delta":{"content":"可以"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","choices":[{"index":0,"delta":{"content":"的。"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","choices":[{"index":0,"delta":{"content":"根据"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","choices":[{"index":0,"delta":{"content":"公司"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","choices":[{"index":0,"delta":{"content":"制度"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
解析时需要注意:
- 第一段数据中的
delta可能包含role: "assistant",表示助手开始回答。 - 中间数据块中的
delta.content是模型新增生成的内容。 - 倒数第二个数据块中,
delta可能为空,finish_reason 变为 "stop"。 - 最后一行
data: [DONE]是结束标记,不是JSON。 - 数据块之间可能存在空行,解析时需要跳过。
要得到完整回答,需要把所有数据块中的delta.content接起来
八.流式调用完整代码实现
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import okhttp3.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.TimeUnit;
public class StreamingChat {
private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";
private static final String API_KEY = "sk-4c715b5fdae44f609df899975256a8fb";
public static void main(String[] args) throws IOException {
// 1. 构建请求体(注意 stream 设为 true)
JsonObject requestBody = new JsonObject();
requestBody.addProperty("model", "deepseek-v4-flash");
requestBody.addProperty("temperature", 0.1);
requestBody.addProperty("max_tokens", 1024);
requestBody.addProperty("stream", true); // 开启流式
JsonArray messages = new JsonArray();
JsonObject systemMsg = new JsonObject();
systemMsg.addProperty("role", "system");
systemMsg.addProperty("content", "你是一个企业知识库问答助手,回答要简洁明了。");
messages.add(systemMsg);
JsonObject userMsg = new JsonObject();
userMsg.addProperty("role", "user");
userMsg.addProperty("content", "请简单说明公司报销流程一般包括哪些步骤。");
messages.add(userMsg);
requestBody.add("messages", messages);
// 2. 创建 OkHttp 客户端
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS) // 流式调用需要更长的读取超时
.build();
// 3. 构建请求
Request request = new Request.Builder()
.url(API_URL)
.addHeader("Authorization", "Bearer " + API_KEY)
.addHeader("Content-Type", "application/json")
.post(RequestBody.create(
requestBody.toString(),
MediaType.parse("application/json")
))
.build();
// 4. 发送请求并逐行读取 SSE 响应
Gson gson = new Gson();
StringBuilder fullContent = new StringBuilder();
System.out.println("=== 模型回答(流式输出)===");
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
System.out.println("请求失败,状态码:" + response.code());
System.out.println("错误信息:" + response.body().string());
return;
}
// 逐行读取响应体
BufferedReader reader = new BufferedReader(
new InputStreamReader(response.body().byteStream())
);
String line;
while ((line = reader.readLine()) != null) {
// 跳过空行
if (line.isEmpty()) {
continue;
}
// 每行以 "data: " 开头,去掉前缀
if (!line.startsWith("data: ")) {
continue;
}
String data = line.substring(6); // 去掉 "data: " 前缀(6 个字符)
// 检查是否是结束标记
if ("[DONE]".equals(data)) {
break;
}
// 解析 JSON,提取增量内容
JsonObject chunk = gson.fromJson(data, JsonObject.class);
JsonArray choices = chunk.getAsJsonArray("choices");
if (choices != null && choices.size() > 0) {
JsonObject delta = choices.get(0).getAsJsonObject()
.getAsJsonObject("delta");
if (delta != null && delta.has("content")) {
JsonElement contentElement = delta.get("content");
if (!contentElement.isJsonNull()) {
String content = contentElement.getAsString();
// 实时打印增量内容(不换行,模拟打字效果)
System.out.print(content);
fullContent.append(content);
}
}
}
}
}
// 输出完毕,换行
System.out.println();
System.out.println();
System.out.println("=== 完整回答 ===");
System.out.println(fullContent);
}
}
从最终内容来看,流式调用和非流式调用都能得到完整回答。
但用户体验不同:
- 非流式:等待一段时间后,一次性看到完整回答;
- 流式:几乎立刻看到内容开始输出。
在控制台中,逐字效果可能不是特别明显,因为网络传输可能会批量返回多个字符。但如果接入前端页面,用户看到的就是标准的打字机效果。