【AI】Java 调用大模型 API 实战:从 OpenAI 协议到 SiliconFlow 流式响应解析

👨‍💻程序员三明治个人主页
🔥 个人专栏 : 《设计模式精解》 《重学数据结构》

🤞先做到 再看见!


目录

    • [一、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。常见角色有三种:

  • system
  • user
  • assistant

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 或相关角色。


三、常用请求参数说明

除了 modelmessages,还有几个常用参数需要掌握。

参数 类型 说明 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 也是控制随机性的参数。

一般来说,temperaturetop_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. 注册步骤

  1. 访问 SiliconFlow 官网:
plain 复制代码
https://siliconflow.cn
  1. 点击右上角"注册",使用手机号注册账号。
  2. 登录后进入控制台。
  3. 在左侧菜单中找到"API 密钥"。
  4. 点击"新建 API 密钥"。
  5. 系统会生成一个以 sk- 开头的密钥字符串。
  6. 复制并保存该密钥。

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 的 RestTemplateWebClient,主要是因为 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]

解析时需要注意:

  1. 第一段数据中的 delta 可能包含 role: "assistant",表示助手开始回答。
  2. 中间数据块中的 delta.content 是模型新增生成的内容。
  3. 倒数第二个数据块中,delta 可能为空,finish_reason 变为 "stop"
  4. 最后一行 data: [DONE] 是结束标记,不是 JSON。
  5. 数据块之间可能存在空行,解析时需要跳过。

要得到完整回答,需要把所有数据块中的:

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 问答链路,本质上都会复用这套基础能力。

这篇文章打下的基础,后面会反复用到。

如果我的内容对你有帮助,请辛苦动动您的手指为我点赞,评论,收藏。感谢大家!!

相关推荐
xinlianyq2 小时前
文艺复兴科技新增AI因子,量化基金重夺主导权
人工智能·ai
数据牧羊人的成长笔记2 小时前
机器学习预备知识
人工智能·机器学习
世界尽头与你2 小时前
Go 语言高级函数特性
开发语言·golang
互联网推荐官2 小时前
上海小程序开发:从技术架构到工程落地的完整拆解
人工智能·物联网·软件工程
人工智能培训2 小时前
大模型部署资源不足?轻量化部署解决方案
人工智能·机器学习·prompt·agent·智能体
小小de风呀2 小时前
de风——【从零开始学C++】(三):类和对象(中序):默认成员函数全解析
开发语言·c++
两万五千个小时2 小时前
Agent 任务没做完就停了?我扒了 Claude Code 源码,找到了 4 层原因
人工智能·程序员·架构
老成说AI2 小时前
DEEPSEEK V4 实测:它不够炸裂,但正在啃最硬的骨头
人工智能·ai·deepseek
2501_913061342 小时前
JVM虚拟机——面试中的八股文
java·jvm·面试