
👨💻程序员三明治 :个人主页
🔥 个人专栏 : 《设计模式精解》 《重学数据结构》
🤞先做到 再看见!
目录
-
- [一、OpenAI 接口协议:大模型 API 的"普通话"](#一、OpenAI 接口协议:大模型 API 的“普通话”)
-
- [1. 为什么要先讲 OpenAI 协议](#1. 为什么要先讲 OpenAI 协议)
- 二、请求格式详解
-
- [1. model:指定要调用的模型](#1. model:指定要调用的模型)
- [2. messages:对话消息数组](#2. messages:对话消息数组)
- [3. messages 中的角色机制](#3. messages 中的角色机制)
-
- [3.1 system:系统角色](#3.1 system:系统角色)
- [3.2 user:用户角色](#3.2 user:用户角色)
- [3.3 assistant:助手角色](#3.3 assistant:助手角色)
- [4. 大模型没有自动记忆,多轮对话需要手动传历史](#4. 大模型没有自动记忆,多轮对话需要手动传历史)
- [5. system 消息会影响模型的回答方式](#5. system 消息会影响模型的回答方式)
- [6. 关于 OpenAI 的 developer 角色](#6. 关于 OpenAI 的 developer 角色)
- 三、常用请求参数说明
-
- [1. temperature](#1. temperature)
- [2. max_tokens](#2. max_tokens)
- [3. top_p](#3. top_p)
- [4. stream](#4. stream)
- 四、非流式响应格式详解
-
- [1. choices](#1. choices)
- [2. choices[0].message](#2. choices[0].message)
- [3. finish_reason](#3. finish_reason)
- [4. usage](#4. usage)
- [五、为什么很多厂商都兼容 OpenAI 协议](#五、为什么很多厂商都兼容 OpenAI 协议)
- [六、SiliconFlow 平台:注册与获取 API Key](#六、SiliconFlow 平台:注册与获取 API Key)
-
- [1. 为什么选择 SiliconFlow](#1. 为什么选择 SiliconFlow)
- [2. 注册步骤](#2. 注册步骤)
- [3. 本系列会用到的模型](#3. 本系列会用到的模型)
- 七、非流式调用:发送问题,获取完整回答
- 八、流式调用:像打字一样逐步输出
-
- [1. 为什么需要流式调用](#1. 为什么需要流式调用)
- [2. 非流式和流式的体验对比](#2. 非流式和流式的体验对比)
- [九、SSE 协议简介](#九、SSE 协议简介)
- 十、流式响应的数据格式
- 十一、流式调用完整代码实现
- 十二、流式调用运行效果
- 十三、流式代码的关键点
-
- [1. 请求体中 stream 必须设置为 true](#1. 请求体中 stream 必须设置为 true)
- [2. 不能一次性读取响应体](#2. 不能一次性读取响应体)
- [3. 解析 delta,而不是 message](#3. 解析 delta,而不是 message)
- [4. 处理 [DONE] 结束标记](#4. 处理 [DONE] 结束标记)
- [5. 使用 System.out.print 实时输出](#5. 使用 System.out.print 实时输出)
- [6. readTimeout 建议设置得更长](#6. readTimeout 建议设置得更长)
- [十四、非流式 vs 流式:实际开发中怎么选](#十四、非流式 vs 流式:实际开发中怎么选)
-
- [1. 在 RAG 系统中的选择](#1. 在 RAG 系统中的选择)
-
- [Chat API:根据场景选择](#Chat API:根据场景选择)
- [Embedding API:通常使用非流式](#Embedding API:通常使用非流式)
- [Reranker API:通常使用非流式](#Reranker API:通常使用非流式)
- [2. 一个实用建议](#2. 一个实用建议)
- [十五、动手实验:修改 System Prompt 看效果](#十五、动手实验:修改 System Prompt 看效果)
- 十六、文末小结
-
- [1. OpenAI Chat Completions API 是常见的大模型接口协议](#1. OpenAI Chat Completions API 是常见的大模型接口协议)
- [2. messages 数组是 Chat API 的核心](#2. messages 数组是 Chat API 的核心)
- [3. 非流式和流式适合不同场景](#3. 非流式和流式适合不同场景)
- [4. Java + OkHttp + Gson 就能完成大模型 API 调用](#4. Java + OkHttp + Gson 就能完成大模型 API 调用)
上一篇我们已经搞清楚了大模型的一些基础概念,比如参数量、Token、上下文窗口、Temperature,以及 Chat 模型和基座模型的区别。
但对于开发者来说,仅仅理解概念还不够。真正要把大模型接入到业务系统中,最终还是要回到一个问题:
如何通过代码调用大模型 API?
这一篇,我们正式开始动手实践。
读完本文后,你将能够使用 Java 程序调用大模型 API,向模型发送一个问题,并拿到模型返回的回答。整个过程和调用普通 HTTP 接口非常类似。
本文会重点演示两种调用方式:
- 非流式调用:一次性拿到完整回答;
- 流式调用:像 ChatGPT 一样逐步输出回答内容。
为了让示例更贴近后续的 RAG 系列,本文统一使用一个场景:
构建一个"企业知识库问答助手",用于回答公司制度、流程文档、项目规范等问题。
在写代码之前,我们先要搞清楚一件事:请求应该按照什么格式发送?响应又会按照什么格式返回?
这就需要先理解 OpenAI 接口协议。
一、OpenAI 接口协议:大模型 API 的"普通话"
1. 为什么要先讲 OpenAI 协议
你可能会问:后面我们使用的是 SiliconFlow 平台上的 Qwen 模型,为什么要先讲 OpenAI 协议?
原因很简单:OpenAI 的 Chat Completions API 已经成为大模型 API 领域的事实标准之一。
就像 HTTP 是 Web 世界的通用协议一样,OpenAI 的接口格式也可以理解为大模型 API 世界里的"普通话"。
国内的 DeepSeek、通义千问、智谱 GLM、SiliconFlow,国外的 Anthropic、Google 等厂商,很多都提供了兼容 OpenAI 协议的 API 接口。即使部分厂商有自己的原生 API 协议,也通常会提供 OpenAI 兼容层。
这意味着什么?
你只需要先掌握一套通用接口格式,就可以快速上手不同平台的大模型服务。切换模型或平台时,很多情况下只需要修改:
plain
baseURL
apiKey
model
核心调用逻辑基本不用重写。
所以,这套协议非常值得先搞清楚。后续系列中的 Chat API、Embedding API、Reranker API,也都会建立在这个基础之上。
二、请求格式详解
调用大模型 API,本质上就是发送一个 HTTP POST 请求。请求体通常是一个 JSON。
下面是一个企业知识库问答助手的请求示例:
plain
{
"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 格式可能不同:
| 平台 | 模型 ID 示例 | 说明 |
|---|---|---|
| SiliconFlow | Qwen/Qwen3-32B |
厂商/模型名格式 |
| OpenAI | gpt-4o |
直接使用模型名 |
| DeepSeek | deepseek-chat |
直接使用模型名 |
本系列统一使用 SiliconFlow 平台上的:
plain
Qwen/Qwen3-32B
它的中文效果较好,适合学习和实验。该模型为付费模型,但价格较低,充值少量金额即可完成本文中的实验。
2. messages:对话消息数组
messages 是整个请求中最核心的字段。
它是一个数组,数组中的每条消息都包含两个属性:
plain
role
content
其中:
role表示这条消息的角色;content表示这条消息的内容。
模型并不是只看用户当前输入的这一句话,而是会读取整个 messages 数组。你可以把它理解为一段完整的对话记录。
模型会基于这段对话上下文生成回答。

3. messages 中的角色机制
messages 数组中的每条消息都有一个 role。常见角色有三种:
systemuserassistant
3.1 system:系统角色
system 消息用于定义模型的行为规则,相当于给模型一份"工作说明书"。
例如:
plain
{
"role": "system",
"content": "你是一个企业知识库问答助手,只回答公司制度、报销流程、项目规范相关的问题。"
}
这个系统消息告诉模型:
- 你的身份是企业知识库问答助手;
- 你的回答范围是公司制度、报销流程、项目规范;
- 不相关的问题应该尽量避免回答。
比如用户问:
plain
今晚吃什么?
模型就应该意识到这个问题不属于企业知识库问答范围。
在后续 RAG 系统中,system 消息会非常重要。我们会通过它告诉模型:
plain
请根据以下参考资料回答用户的问题。
如果参考资料中没有相关信息,请如实告知,不要编造答案。
这也是 RAG 系统中非常核心的 Prompt 模式。
3.2 user:用户角色
user 消息表示用户输入的问题。
例如:
plain
{
"role": "user",
"content": "公司的年假可以拆分使用吗?"
}
这就是用户真正想问的问题。
3.3 assistant:助手角色
assistant 消息表示模型之前的回答,通常用于构建多轮对话上下文。
例如,一段多轮对话可以这样组织:
plain
{
"messages": [
{"role": "system", "content": "你是一个企业知识库问答助手"},
{"role": "user", "content": "公司的年假可以拆分使用吗?"},
{"role": "assistant", "content": "可以。根据公司制度,年假支持按半天或整天为单位拆分使用,具体以审批系统中的可用余额为准。"},
{"role": "user", "content": "那需要提前几天申请?"}
]
}
模型看到这段上下文后,就能理解最后一句:
plain
那需要提前几天申请?
问的是"年假申请需要提前几天",而不是其他流程。
如果你只发送最后一句"那需要提前几天申请?",模型就很难判断用户到底在问年假、报销、出差,还是其他审批流程。
4. 大模型没有自动记忆,多轮对话需要手动传历史
这里有一个非常重要的点:
大模型 API 的每次调用都是独立的。
模型不会自动记住上一次 API 调用的内容。所谓多轮对话,本质上是开发者在每次请求中,把历史消息一起放进 messages 数组里,让模型看到完整上下文。
也就是说,多轮对话并不是模型自己"记住了",而是你每次都把历史对话重新发给了模型。
这也是为什么对话越长,消耗的 Token 越多:
因为每次请求都要把历史消息重新发送一遍。
5. system 消息会影响模型的回答方式
同一个用户问题,如果 system 消息不同,模型的回答风格也会发生明显变化。
比如用户问题都是:
plain
向量数据库在 RAG 系统里有什么作用?
不同的 system 消息会得到不同风格的回答:
| system 消息 | 用户问题 | 模型回答风格 |
|---|---|---|
| 你是一个专业的 RAG 技术顾问 | 向量数据库在 RAG 系统里有什么作用? | 客观解释向量数据库的作用和适用场景 |
| 你是一个面试官 | 向量数据库在 RAG 系统里有什么作用? | 用面试追问的方式引导回答 |
| 你是一个面向初学者的 AI 技术导师 | 向量数据库在 RAG 系统里有什么作用? | 用通俗类比解释概念 |
这就是 system 消息的作用:
它可以从根本上影响模型的身份、语气、回答边界和输出风格。
6. 关于 OpenAI 的 developer 角色
OpenAI 在新版 API 中引入了 developer 角色,用于表达开发者侧的指令。
它和传统的 system 角色在用途上有相似之处,都是用于定义模型行为规则。不过,目前很多兼容 OpenAI 协议的平台,包括 SiliconFlow、DeepSeek 等,仍然使用 system 角色。
所以,本系列统一使用:
plain
system
如果你直接调用 OpenAI 官方 API,可以根据官方文档要求,选择使用 developer 或相关角色。
三、常用请求参数说明
除了 model 和 messages,还有几个常用参数需要掌握。
| 参数 | 类型 | 说明 | RAG 场景推荐值 |
|---|---|---|---|
temperature |
float | 控制回答随机性,值越高回答越发散 | 0~0.3 |
max_tokens |
int | 控制模型最多生成多少个 Token | 512~2048 |
top_p |
float | 另一种控制随机性的方式,通常和 temperature 二选一 |
0.7~0.9 |
stream |
boolean | 是否启用流式返回 | 根据场景选择 |
1. temperature
temperature 用于控制回答的随机性。
在企业知识库问答、RAG 问答这类场景中,我们通常希望模型回答稳定、准确,不要随意发挥。因此推荐设置得低一些,比如:
plain
0
0.1
0.2
0.3
本文示例中会使用:
plain
temperature = 0
或者:
plain
temperature = 0.1
2. max_tokens
max_tokens 用于限制模型最多生成多少个 Token。
如果设置太小,回答可能被截断;如果设置太大,可能会造成不必要的 Token 消耗。
例如:
plain
"max_tokens": 1024
表示模型最多生成 1024 个 Token。
3. top_p
top_p 也是控制随机性的参数。
一般来说,temperature 和 top_p 只需要调整其中一个即可。为了方便理解,本系列统一使用 temperature,不重点展开 top_p。
4. stream
stream 用于控制响应方式:
plain
stream: false
表示非流式响应,模型生成完所有内容后,一次性返回完整 JSON。
plain
stream: true
表示流式响应,模型每生成一小段内容,就推送给客户端。
后面我们会分别演示这两种方式。
四、非流式响应格式详解
当 stream 设置为 false 时,模型会返回一个完整 JSON。
示例:
plain
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1700000000,
"model": "Qwen/Qwen3-32B",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "可以。根据公司制度,年假通常支持按半天或整天为单位拆分使用,具体可用天数和申请规则以公司审批系统中的制度说明为准。"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 46,
"completion_tokens": 58,
"total_tokens": 104
}
}
这个响应中有几个关键字段。
1. choices
choices 是模型回答数组。
通常情况下,里面只有一个元素:
plain
choices[0]
除非你设置了 n 参数,要求模型一次生成多个回答。
2. choices[0].message
模型最终回答在这里:
plain
choices[0].message.content
例如:
plain
"content": "可以。根据公司制度,年假通常支持按半天或整天为单位拆分使用..."
这就是我们真正需要展示给用户的内容。
3. finish_reason
finish_reason 表示模型停止生成的原因。
常见值如下:
| 值 | 含义 | 说明 |
|---|---|---|
stop |
正常结束 | 模型认为回答已经完整 |
length |
达到长度上限 | 回答被 max_tokens 截断 |
如果经常看到:
plain
finish_reason: "length"
说明 max_tokens 可能设置得太小,模型回答被截断了。此时可以适当调大 max_tokens。
4. usage
usage 用于统计 Token 消耗。
| 字段 | 含义 |
|---|---|
prompt_tokens |
输入内容消耗的 Token 数,包括 system、user、assistant 历史消息 |
completion_tokens |
模型生成回答消耗的 Token 数 |
total_tokens |
总 Token 数,即输入 + 输出 |
API 通常按 Token 计费,因此 usage 字段可以帮助我们监控调用成本。
五、为什么很多厂商都兼容 OpenAI 协议
OpenAI 的 Chat Completions API 形成了较大的生态,很多框架、工具和教程都围绕这套协议展开。
例如:
- LangChain;
- Spring AI;
- LlamaIndex;
- 各类命令行工具;
- Postman 请求模板;
- 各种开源示例项目。
因此,很多大模型平台都会提供 OpenAI 兼容接口,降低开发者迁移成本。
比如你用 SiliconFlow 写好了调用代码,后续想切换到 DeepSeek 官方 API,很多情况下只需要修改:
plain
// SiliconFlow
baseURL = "https://api.siliconflow.cn/v1/chat/completions"
apiKey = "你的 SiliconFlow API Key"
// 切换到 DeepSeek
baseURL = "https://api.deepseek.com/v1/chat/completions"
apiKey = "你的 DeepSeek API Key"
核心请求结构依然是:
plain
model
messages
temperature
max_tokens
stream
这就是兼容协议带来的便利。
六、SiliconFlow 平台:注册与获取 API Key

1. 为什么选择 SiliconFlow
本系列选择 SiliconFlow,主要是因为它适合学习和实验:
- 国内平台,访问相对方便;
- 注册流程简单;
- 支持 Qwen、DeepSeek、GLM、Llama 等多种主流模型;
- API 兼容 OpenAI 协议;
- 部分模型可免费调用;
- 付费模型价格较低,适合实验。
2. 注册步骤
- 访问 SiliconFlow 官网:
plain
https://siliconflow.cn
- 点击右上角"注册",使用手机号注册账号。
- 登录后进入控制台。
- 在左侧菜单中找到"API 密钥"。
- 点击"新建 API 密钥"。
- 系统会生成一个以
sk-开头的密钥字符串。 - 复制并保存该密钥。
API Key 是调用 API 的身份凭证,相当于密码。不要把它提交到 Git 仓库,也不要写在公开代码中。
后面的代码中,我们会使用:
plain
YOUR_API_KEY
作为占位符。实际运行时,需要替换成自己的真实 API Key。
3. 本系列会用到的模型
| 模型 ID | 类型 | 用途 | 本系列使用场景 |
|---|---|---|---|
Qwen/Qwen3-32B |
Chat 模型 | 对话、问答、文本生成 | 本篇 API 调用实战,后续 RAG 生成环节 |
BAAI/bge-m3 |
Embedding 模型 | 文本向量化 | 后续 RAG 向量化环节 |
BAAI/bge-reranker-v2-m3 |
Reranker 模型 | 检索结果重排序 | 后续 RAG 检索环节 |
模型的可用性和价格可能会变化,实际情况以 SiliconFlow 官网展示为准。
拿到 API Key 之后,就可以开始写代码了。
七、非流式调用:发送问题,获取完整回答
非流式调用是最简单的大模型 API 调用方式。
它的特点是:
客户端发送请求,等待模型生成完毕后,一次性拿到完整回答。
就像调用普通 REST API 一样。
完整示例可以查看 TinyRAG 项目 com.nageoffer.ai.tinyrag.openai 目录下的代码。
1. 添加 Maven 依赖
在 pom.xml 中添加以下依赖:
plain
<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 项目中使用。
后续系列中,所有 API 调用都会使用 OkHttp + Gson 这套组合。
2. 完整代码实现
plain
public class NonStreamingChat {
// SiliconFlow API 地址
private static final String API_URL = "https://api.siliconflow.cn/v1/chat/completions";
// 替换成你自己的 API Key
private static final String API_KEY = "YOUR_API_KEY";
public static void main(String[] args) throws IOException {
// 1. 构建请求体 JSON
JsonObject requestBody = new JsonObject();
requestBody.addProperty("model", "Qwen/Qwen3-32B");
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);
}
}
}
3. 运行效果
将 YOUR_API_KEY 替换成自己的 API Key 后,运行代码,控制台输出大致如下:
plain
=== 模型回答 ===
可以。根据公司制度,年假通常支持按半天或整天为单位拆分使用,具体可用天数和申请规则以公司审批系统中的制度说明为准。
如果是跨部门或项目关键节点期间申请,建议提前和直属负责人沟通,避免影响项目排期。
=== 调用信息 ===
结束原因:stop
输入 Token:36
输出 Token:96
总 Token:132
因为代码中设置了:
plain
temperature = 0
所以模型输出通常会相对稳定。不过由于模型服务端实现和底层计算存在一定不确定性,极少数情况下仍可能出现细微差别。
4. 代码流程解读
上面的代码主要分为六个步骤。
第一步:构建请求体
使用 Gson 的 JsonObject 手动拼装 JSON 请求体。
核心字段包括:
plain
model
messages
temperature
max_tokens
stream
其中,messages 数组中包含一条 system 消息和一条 user 消息。
第二步:创建 HTTP 客户端
使用 OkHttp 创建客户端:
plain
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
大模型生成回答可能需要几秒到十几秒,因此建议显式设置超时时间。
第三步:构建 HTTP 请求
请求方式是 POST,请求地址是 SiliconFlow 的 Chat API 地址:
plain
https://api.siliconflow.cn/v1/chat/completions
请求头中需要设置:
plain
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
其中 Authorization 用于传递 API Key。
第四步:发送请求
代码通过:
plain
client.newCall(request).execute()
发送同步请求。
这个方法会阻塞当前线程,直到收到响应。代码中使用 try-with-resources,确保响应体被正确关闭。
第五步:解析响应
从 JSON 响应中提取模型回答:
plain
choices[0].message.content
同时提取:
plain
finish_reason
usage
用于了解模型为什么停止生成,以及本次调用消耗了多少 Token。
第六步:打印结果
最后将回答内容、结束原因和 Token 用量打印到控制台。
整体流程和调用普通第三方 REST API 没有本质区别:
plain
构建请求体 → 发送 HTTP 请求 → 解析 JSON 响应
八、流式调用:像打字一样逐步输出
1. 为什么需要流式调用
非流式调用有一个体验问题:模型必须生成完整内容后,才会一次性返回结果。
如果回答比较短,这个问题不明显。
但如果回答比较长,比如用户问:
plain
请总结一下公司研发流程文档中的代码评审规范。
模型可能需要几秒甚至十几秒才能生成完整回答。在这段时间里,如果页面没有任何变化,用户很容易觉得系统卡住了。
流式调用就是为了解决这个问题。
开启流式调用后,模型每生成一小段内容,就会立刻推送给客户端。客户端收到一段就展示一段,用户看到的效果就是文字逐步出现。
这就是 ChatGPT、DeepSeek 网页端常见的"打字机效果"。
2. 非流式和流式的体验对比
| 方式 | 用户体验 |
|---|---|
| 非流式 | 等待一段时间后,完整回答一次性出现 |
| 流式 | 几乎立刻开始输出,内容逐步展示 |
两者的总生成时间通常差不多,但流式调用能明显降低用户等待感。
用户不需要等到完整回答生成完,看到第一段内容后,就知道系统正在正常工作。

九、SSE 协议简介
流式调用通常基于 SSE,也就是 Server-Sent Events,中文可以理解为"服务端推送事件"。
普通 HTTP 请求是一问一答模式:
plain
客户端发送请求 → 服务端返回完整响应 → 连接关闭
SSE 不一样:
plain
客户端发送请求 → 服务端保持连接 → 持续推送数据块 → 推送完成后关闭连接
每个数据块通常以:
plain
data:
开头。
当所有内容推送完毕后,服务端会发送一个特殊结束标记:
plain
data: [DONE]
一句话概括:
SSE 就是服务端持续推送,客户端持续接收。
十、流式响应的数据格式
流式响应和非流式响应的 JSON 结构有一个关键区别:
- 非流式响应中,模型回答在
choices[0].message里; - 流式响应中,每个数据块的增量内容在
choices[0].delta里。
一个完整的流式响应数据流大致如下:
plain
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。 - 数据块之间可能存在空行,解析时需要跳过。
要得到完整回答,需要把所有数据块中的:
plain
delta.content
依次拼接起来。
十一、流式调用完整代码实现
plain
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.siliconflow.cn/v1/chat/completions";
private static final String API_KEY = "YOUR_API_KEY";
public static void main(String[] args) throws IOException {
// 1. 构建请求体(注意 stream 设为 true)
JsonObject requestBody = new JsonObject();
requestBody.addProperty("model", "Qwen/Qwen3-32B");
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);
}
}
十二、流式调用运行效果
运行代码后,你会看到控制台上的文字逐步输出,而不是等待几秒后一次性出现。
示例输出如下:
plain
=== 模型回答(流式输出)===
公司报销流程一般包括以下几个步骤:
1. 提交报销申请:在审批系统中填写报销类型、金额、事由等信息。
2. 上传凭证材料:上传发票、付款记录、合同或其他相关附件。
3. 直属负责人审批:负责人确认费用是否合理、是否符合项目或部门预算。
4. 财务审核:财务人员核对票据、金额和报销规则。
5. 打款入账:审核通过后,报销金额会按公司流程发放到指定账户。
具体流程以公司内部制度和审批系统要求为准。
=== 完整回答 ===
公司报销流程一般包括以下几个步骤:
1. 提交报销申请:在审批系统中填写报销类型、金额、事由等信息。
2. 上传凭证材料:上传发票、付款记录、合同或其他相关附件。
3. 直属负责人审批:负责人确认费用是否合理、是否符合项目或部门预算。
4. 财务审核:财务人员核对票据、金额和报销规则。
5. 打款入账:审核通过后,报销金额会按公司流程发放到指定账户。
具体流程以公司内部制度和审批系统要求为准。
从最终内容来看,流式调用和非流式调用都能得到完整回答。
但用户体验不同:
- 非流式:等待一段时间后,一次性看到完整回答;
- 流式:几乎立刻看到内容开始输出。
在控制台中,逐字效果可能不是特别明显,因为网络传输可能会批量返回多个字符。但如果接入前端页面,用户看到的就是标准的打字机效果。
十三、流式代码的关键点
和非流式调用相比,流式调用有几个关键差异。
1. 请求体中 stream 必须设置为 true
plain
requestBody.addProperty("stream", true);
这表示告诉服务端:请使用流式方式返回结果。
2. 不能一次性读取响应体
非流式响应可以这样读取:
plain
response.body().string()
但流式响应是持续推送的,不能这样一次性读取。
正确做法是使用 BufferedReader 逐行读取:
plain
BufferedReader reader = new BufferedReader(
new InputStreamReader(response.body().byteStream())
);
3. 解析 delta,而不是 message
非流式响应中,回答内容在:
plain
choices[0].message.content
流式响应中,增量内容在:
plain
choices[0].delta.content
这是两种响应格式最关键的区别。
4. 处理 [DONE] 结束标记
当收到:
plain
data: [DONE]
说明服务端已经推送完所有内容,可以停止读取。
5. 使用 System.out.print 实时输出
为了模拟打字机效果,需要使用:
plain
System.out.print(content);
而不是:
plain
System.out.println(content);
否则每个数据块都会换行,效果就不自然了。
6. readTimeout 建议设置得更长
流式连接会保持较长时间,因此读取超时时间建议设置得更长一些:
plain
.readTimeout(120, TimeUnit.SECONDS)
十四、非流式 vs 流式:实际开发中怎么选
非流式和流式没有绝对的好坏,主要看业务场景。
| 对比维度 | 非流式(stream=false) | 流式(stream=true) |
|---|---|---|
| 响应方式 | 模型生成完后一次性返回 | 模型边生成边推送 |
| 首字延迟 | 较高 | 较低 |
| 用户体验 | 等待感较强 | 体验更流畅 |
| 实现复杂度 | 简单 | 稍复杂 |
| 响应体格式 | 完整 JSON | 多个 SSE 数据块 |
| 解析方式 | 直接解析 message.content |
拼接多个 delta.content |
| Token 用量统计 | 通常直接包含 usage |
部分平台流式响应不返回 usage |
| 适用场景 | 后台任务、批处理、调试 | 在线聊天、知识库问答、实时生成 |
1. 在 RAG 系统中的选择
在 RAG 系统中,两种方式都会用到。
Chat API:根据场景选择
如果是面向用户的知识库问答页面,建议使用流式调用,让用户更快看到内容。
如果是后台测试、接口调试、批量生成数据,可以使用非流式调用。
Embedding API:通常使用非流式
文本向量化属于后台处理,不需要实时展示结果,因此使用非流式即可。
Reranker API:通常使用非流式
检索结果重排序也是后台处理,不需要打字机效果,因此使用非流式即可。
2. 一个实用建议
开发阶段建议先使用非流式调用。
原因很简单:
- 响应是完整 JSON;
- 调试更方便;
- 日志更好看;
- 解析逻辑更简单。
等功能跑通后,再把面向用户的部分改成流式调用。
十五、动手实验:修改 System Prompt 看效果
到这里,你已经能够使用 Java 调用大模型 API 了。
在结束之前,可以做一个小实验,感受一下 system 消息对模型回答风格的影响。
把非流式调用代码中的 system 消息改成:
plain
systemMsg.addProperty("content", "你是一个面向初学者的 AI 技术导师,回答要通俗、简洁,并尽量用类比解释。");
然后把 user 消息改成:
plain
userMsg.addProperty("content", "向量数据库在 RAG 系统里有什么作用?");
运行后,可能会看到类似输出:
plain
向量数据库可以理解为 RAG 系统里的"语义检索引擎"。
普通数据库更擅长按照关键词精确查找,而向量数据库会把文本转换成向量,再根据语义相似度进行检索。
比如用户问"公司年假怎么申请",即使知识库原文写的是"员工休假流程",向量数据库也有机会把这段相关内容找出来。
所以在 RAG 系统中,向量数据库主要负责从知识库中找到和用户问题最相关的内容。
这个实验说明:同一个问题,只要修改 system 消息,模型的回答风格就会发生变化。
在后续 RAG 系统中,我们会通过 system 消息告诉模型:
plain
你是一个企业知识库问答助手。请根据以下参考资料回答用户的问题。
如果参考资料中没有相关信息,请如实告知用户,不要编造答案。
参考资料:
{这里放检索到的文本片段}
这就是 RAG 的核心 Prompt 模式:
先检索相关资料,再把资料放进 Prompt,让模型基于资料回答问题。
理解了 system 消息的机制,后面学习 RAG 的 Prompt 设计就会自然很多。
十六、文末小结
本文从接口协议讲到 Java 代码实现,完成了从"理解大模型"到"动手调用大模型"的第一步。
核心内容可以总结为四点。
1. OpenAI Chat Completions API 是常见的大模型接口协议
很多模型平台都提供 OpenAI 兼容接口。
学会这套协议后,切换模型或平台时,通常只需要修改:
plain
baseURL
apiKey
model
核心代码逻辑基本保持一致。
2. messages 数组是 Chat API 的核心
messages 数组用于描述完整对话上下文。
其中:
system定义模型行为;user表示用户输入;assistant表示模型历史回答。
多轮对话不是模型自动记忆,而是开发者每次请求时把历史消息一起传给模型。
3. 非流式和流式适合不同场景
非流式调用简单直接,适合:
- 后台任务;
- 批处理;
- 接口调试;
- Embedding;
- Reranker。
流式调用体验更好,适合:
- 在线聊天;
- 知识库问答;
- AI 助手;
- 需要实时反馈的生成场景。
4. Java + OkHttp + Gson 就能完成大模型 API 调用
整个调用流程并不复杂:
plain
构建 JSON 请求体 → 发送 HTTP POST 请求 → 解析 JSON 响应
非流式调用解析:
plain
choices[0].message.content
流式调用解析:
plain
choices[0].delta.content
到这里,你已经拥有了一个可以和大模型对话的 Java 程序。
后续系列中,无论是 Embedding API、Reranker API,还是完整的 RAG 问答链路,本质上都会复用这套基础能力。
这篇文章打下的基础,后面会反复用到。
如果我的内容对你有帮助,请辛苦动动您的手指为我点赞,评论,收藏。感谢大家!!
