iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Java × AI:30天打造智慧健康助理系列 第 22 篇

Spring Boot 串接生成式 AI API

  • 分享至 

  • xImage
  •  

Day 22|Spring Boot 串接生成式 AI API

昨天我們認識了生成式 AI、LLM、Prompt,以及 AI API 的基本概念。

雖然已經知道 Java 可以透過 HTTP 和 AI 模型溝通,但到目前為止,我們還沒有真正寫出呼叫 AI 的程式。

所以今天終於要來實作了!

這次會使用 Spring Boot 搭配 Java 的 HttpClient,呼叫 OpenAI API,讓智慧健康助理第一次取得 AI 產生的文字。

想到前面花了 21 天建立後端,現在終於要讓 AI 出場,還是有點期待 XD

今天的目標是:

使用 Java 發送 HTTP Request 至生成式 AI API,接收模型回覆,並建立第一個 AI 測試 API。


一、今天要完成什麼功能?

今天準備新增一個 API:

GET /health/ai/test

當我們透過瀏覽器或 Postman 呼叫這個 API 時,Spring Boot 會向外部 AI 服務發送請求。

整個流程如下:

使用者
   ↓
GET /health/ai/test
   ↓
Spring Boot
   ↓
AIService
   ↓
Java HttpClient
   ↓
OpenAI API
   ↓
AI 模型產生文字
   ↓
Spring Boot 接收回覆
   ↓
回傳 JSON

例如我們希望 AI 回答:

請用一句繁體中文介紹智慧健康助理。

最後可能得到:

{
  "message": "智慧健康助理可以協助整理每日健康紀錄,讓使用者更方便觀察生活習慣。"
}

不過這次 AI 產生的文字不一定每次都完全相同。

因為生成式 AI 並不是使用固定答案回覆。

今天先使用簡單的測試 Prompt,確認 Java 和 AI API 可以正常溝通。

等到下一篇,再將我們之前建立的健康摘要資料交給 AI。


二、今天使用哪些技術?

今天會使用以下工具:

技術 用途
Java 撰寫後端程式
Spring Boot 建立 REST API
Java HttpClient 發送 HTTP Request
Jackson ObjectMapper 處理 JSON
OpenAI API 呼叫生成式 AI 模型
Postman 測試 API
環境變數 保存 API Key

這裡有一個好消息。

因為我們前面建立的 Spring Boot 專案已經包含 Spring Web,所以通常不需要為了今天的範例再加入額外的 HTTP Client 套件。

今天使用的是 Java 11 開始提供的:

java.net.http.HttpClient

如果目前專案使用 Java 17 或更新版本,就可以使用這個功能。


三、先認識 OpenAI API

今天選擇使用 OpenAI API 作為範例。

它可以讓開發者透過程式呼叫 AI 模型。

例如我們可以傳送:

請用繁體中文介紹健康紀錄的重要性。

再取得模型產生的文字。

這次會使用 OpenAI 的 Responses API。

API Endpoint 為:

https://api.openai.com/v1/responses

我們會透過:

POST

發送請求。

基本的 Request Body 可以長這樣:

{
  "model": "gpt-4.1-mini",
  "input": "請用一句繁體中文介紹智慧健康助理。"
}

這裡有兩個重要欄位。

model

"model": "gpt-4.1-mini"

代表要使用哪個 AI 模型。

這裡使用 gpt-4.1-mini 作為教學範例。

實際使用時,需要確認自己的 API 專案是否可以使用該模型,以及目前的價格與模型供應狀況。

input

"input": "請用一句繁體中文介紹智慧健康助理。"

代表我們要提供給模型的輸入內容。

今天先使用簡單的文字。

未來就可以改成:

請根據以下健康紀錄產生每日摘要。

四、準備 API Key

在正式呼叫 API 之前,我們需要先準備 API Key。

可以到 OpenAI API 平台:

https://platform.openai.com/

登入後,進入 API Keys 相關頁面建立金鑰。

建立完成後,會取得一組 API Key。

這組金鑰非常重要。

因為它和 API 使用權限及費用有關。

不要將真正的 API Key 放到公開的 GitHub Repository,也不要直接寫進教學文章或截圖。

另外要注意:

使用 ChatGPT 不代表已經有 API 免費額度。

API 使用量通常會另外計費,因此開始測試前,記得確認 API 專案的計費與使用限制。


五、使用環境變數保存 API Key

今天不會把金鑰直接寫進 Java 程式。

而是使用環境變數。

環境變數名稱設定為:

OPENAI_API_KEY

Windows PowerShell

如果使用 Windows,可以在 PowerShell 輸入:

$env:OPENAI_API_KEY="你的_API_Key"

這個設定只會套用到目前的 PowerShell 工作階段,以及從該工作階段啟動的程式。

如果關閉 PowerShell,通常就需要重新設定。

macOS / Linux

可以使用:

export OPENAI_API_KEY="你的_API_Key"

同樣是設定目前終端機工作階段的環境變數。

如果使用 IntelliJ IDEA

如果是直接從 IntelliJ IDEA 執行 Spring Boot,可以到:

Run
  ↓
Edit Configurations
  ↓
Environment variables

加入:

OPENAI_API_KEY=你的_API_Key

這樣 Spring Boot 啟動時,就可以讀取金鑰。

記得不要把含有真正金鑰的設定檔提交到 Git。


六、在 application.properties 加入設定

接下來打開:

src/main/resources/application.properties

保留前面 H2 Database 的設定。

再加入:

openai.api.key=${OPENAI_API_KEY}
openai.api.url=https://api.openai.com/v1/responses
openai.api.model=gpt-4.1-mini

這裡使用:

${OPENAI_API_KEY}

代表 Spring Boot 會從環境變數取得 API Key。

而:

openai.api.url

則是 API 的網址。

最後:

openai.api.model

代表我們要使用的模型。

這樣以後如果想更換模型,就不用每次都修改 Java 程式。

只要調整設定檔即可。


七、先認識 Java HttpClient

在開始寫 AIService 之前,我們先認識今天會用到的:

HttpClient

它是 Java 提供的 HTTP Client。

可以用來發送:

GET
POST
PUT
DELETE

等 HTTP 請求。

例如:

HttpClient client = HttpClient.newHttpClient();

就可以建立一個 HTTP Client。

接著使用:

HttpRequest

建立請求。

例如:

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://example.com"))
        .GET()
        .build();

最後透過:

client.send()

發送請求並取得回應。

今天我們會使用相同的概念,只是改成向 AI API 發送 POST Request。


八、先理解 AI API 的 Request 格式

今天呼叫 OpenAI API 時,會發送類似以下的 HTTP Request:

POST /v1/responses HTTP/1.1
Host: api.openai.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

Request Body:

{
  "model": "gpt-4.1-mini",
  "input": "請用一句繁體中文介紹智慧健康助理。"
}

其中最重要的是:

Authorization

這個 Header。

我們需要使用:

Bearer API_KEY

的方式提供驗證資訊。

例如:

.header("Authorization", "Bearer " + apiKey)

另外還需要:

.header("Content-Type", "application/json")

告訴 API 我們傳送的是 JSON 資料。


九、建立 AIService

接下來終於開始寫 Java 程式了!

在:

service

Package 裡面建立:

AIService.java

這個 Service 將負責:

  1. 讀取 API 設定。
  2. 建立 HTTP Request。
  3. 呼叫 OpenAI API。
  4. 接收 JSON Response。
  5. 擷取 AI 產生的文字。

先看完整程式:

package com.example.healthassistant.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;

@Service
public class AIService {

    private final String apiKey;
    private final String apiUrl;
    private final String model;

    private final HttpClient httpClient;
    private final ObjectMapper objectMapper;

    public AIService(
            @Value("${openai.api.key}") String apiKey,
            @Value("${openai.api.url}") String apiUrl,
            @Value("${openai.api.model}") String model,
            ObjectMapper objectMapper) {

        this.apiKey = apiKey;
        this.apiUrl = apiUrl;
        this.model = model;
        this.objectMapper = objectMapper;

        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();
    }

    public String generateText(String prompt) {

        try {

            Map<String, Object> requestBody = Map.of(
                    "model", model,
                    "input", prompt,
                    "max_output_tokens", 150
            );

            String jsonBody =
                    objectMapper.writeValueAsString(requestBody);

            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(apiUrl))
                    .timeout(Duration.ofSeconds(30))
                    .header("Authorization", "Bearer " + apiKey)
                    .header("Content-Type", "application/json")
                    .POST(
                        HttpRequest.BodyPublishers.ofString(
                            jsonBody,
                            StandardCharsets.UTF_8
                        )
                    )
                    .build();

            HttpResponse<String> response =
                    httpClient.send(
                        request,
                        HttpResponse.BodyHandlers.ofString(
                            StandardCharsets.UTF_8
                        )
                    );

            if (response.statusCode() < 200
                    || response.statusCode() >= 300) {

                throw new IllegalStateException(
                        "AI API request failed. HTTP status: "
                                + response.statusCode()
                );
            }

            JsonNode root =
                    objectMapper.readTree(response.body());

            StringBuilder result = new StringBuilder();

            JsonNode output = root.path("output");

            if (output.isArray()) {

                for (JsonNode item : output) {

                    if (!"message".equals(
                            item.path("type").asText())) {
                        continue;
                    }

                    JsonNode content = item.path("content");

                    if (content.isArray()) {

                        for (JsonNode part : content) {

                            if ("output_text".equals(
                                    part.path("type").asText())) {

                                result.append(
                                        part.path("text").asText()
                                );
                            }
                        }
                    }
                }
            }

            if (result.length() == 0) {
                throw new IllegalStateException(
                        "AI API returned no text output."
                );
            }

            return result.toString();

        } catch (InterruptedException e) {

            Thread.currentThread().interrupt();

            throw new IllegalStateException(
                    "AI API request was interrupted.", e
            );

        } catch (Exception e) {

            throw new IllegalStateException(
                    "Failed to generate AI text.", e
            );
        }
    }
}

這段程式看起來有點長,但其實就是把剛剛學過的 HTTP Request 流程寫成 Java。

接下來我們拆開來看。


十、使用 @Value 讀取設定

首先看到:

@Value("${openai.api.key}")

這是 Spring 提供的功能。

可以從:

application.properties

讀取設定值。

例如:

public AIService(
        @Value("${openai.api.key}") String apiKey,
        @Value("${openai.api.url}") String apiUrl,
        @Value("${openai.api.model}") String model,
        ObjectMapper objectMapper)

就可以取得:

API Key
API URL
Model

這樣我們不需要把這些資訊直接寫死在程式裡。

未來如果想更換模型,也會比較方便。


十一、使用 ObjectMapper 建立 JSON

接下來看到:

Map<String, Object> requestBody = Map.of(
        "model", model,
        "input", prompt,
        "max_output_tokens", 150
);

這裡先使用 Java 的 Map 建立請求資料。

例如:

model → gpt-4.1-mini
input → 請用一句繁體中文介紹智慧健康助理。
max_output_tokens → 150

接著使用:

objectMapper.writeValueAsString(requestBody);

把 Java 物件轉換成 JSON 字串。

例如:

{
  "model": "gpt-4.1-mini",
  "input": "請用一句繁體中文介紹智慧健康助理。",
  "max_output_tokens": 150
}

這裡的 max_output_tokens 是限制模型輸出的 Token 數量。

它不是中文字數限制,也不能保證回覆剛好在指定字數內。

另外,如果輸出上限太低,也可能導致模型無法完成回覆。


十二、建立 HTTP Request

接著是今天的核心程式:

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(apiUrl))
        .timeout(Duration.ofSeconds(30))
        .header("Authorization", "Bearer " + apiKey)
        .header("Content-Type", "application/json")
        .POST(
            HttpRequest.BodyPublishers.ofString(
                jsonBody,
                StandardCharsets.UTF_8
            )
        )
        .build();

我們可以分成幾個部分理解。

1. 設定 API 網址

.uri(URI.create(apiUrl))

代表要將請求發送到哪裡。

2. 設定逾時時間

.timeout(Duration.ofSeconds(30))

代表這次 HTTP Request 最多等待約 30 秒。

如果外部服務沒有在期限內完成回應,程式就可能拋出逾時例外。

3. 設定 API Key

.header("Authorization", "Bearer " + apiKey)

提供 API 驗證資訊。

4. 設定 JSON 格式

.header("Content-Type", "application/json")

表示傳送的資料格式是 JSON。

5. 使用 POST 發送資料

.POST(
    HttpRequest.BodyPublishers.ofString(jsonBody)
)

代表使用 POST Method,並將 JSON 字串放入 Request Body。

這樣就完成一個基本的 AI API Request。


十三、使用 HttpClient 發送請求

建立好 Request 後,就可以真正發送了。

程式如下:

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

這裡使用:

send()

同步發送 HTTP Request。

也就是說,Java 會等待外部 API 回應後,才繼續執行後面的程式。

接著可以透過:

response.statusCode()

取得 HTTP Status Code。

以及:

response.body()

取得回傳的 JSON 字串。

例如:

int statusCode = response.statusCode();

String responseBody = response.body();

不過實際的 AI API Response 通常不會只有一段簡單文字。

所以還需要解析 JSON。


十四、解析 AI 回傳的 JSON

今天使用的 Responses API,回傳內容通常包含多個欄位。

以下是簡化後的示意:

{
  "id": "resp_example",
  "status": "completed",
  "output": [
    {
      "type": "message",
      "role": "assistant",
      "content": [
        {
          "type": "output_text",
          "text": "智慧健康助理可以協助整理每日健康紀錄。"
        }
      ]
    }
  ]
}

這只是示意格式,不代表每次 API 都會回傳完全相同的結構。

其中我們最想取得的是:

"text": "智慧健康助理可以協助整理每日健康紀錄。"

所以今天使用:

JsonNode root =
        objectMapper.readTree(response.body());

先將 JSON 字串轉成 JsonNode。

接著從:

output

裡面尋找:

message

再從:

content

裡面尋找:

output_text

最後取得:

part.path("text").asText()

這樣就可以拿到模型產生的文字。

為什麼不直接使用固定的陣列位置,例如:

root.path("output").get(0)

因為 AI API 的輸出可能包含不同類型的項目。

直接假設第一個項目一定是文字訊息,可能不夠穩定。

因此今天採用逐一檢查輸出項目類型的方式。


十五、建立 AIController

AIService 完成後,接下來要建立 Controller。

在:

controller

Package 裡面新增:

AIController.java

完整程式如下:

package com.example.healthassistant.controller;

import com.example.healthassistant.service.AIService;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/health/ai")
public class AIController {

    private final AIService aiService;

    public AIController(AIService aiService) {
        this.aiService = aiService;
    }

    @GetMapping("/test")
    public ResponseEntity<Map<String, String>> testAI() {

        String prompt =
                "請用一句繁體中文介紹智慧健康助理,"
                + "不要提供醫療診斷或治療建議。";

        String result = aiService.generateText(prompt);

        return ResponseEntity.ok(
                Map.of("message", result)
        );
    }
}

這次的 Controller 其實非常簡單。

因為呼叫 AI 的工作已經交給:

AIService

Controller 只需要:

建立測試 Prompt
       ↓
呼叫 AIService
       ↓
取得 AI 文字
       ↓
回傳 JSON

這也再次用到了前面一直練習的 Controller 和 Service 分層架構。


十六、使用 Postman 測試 AI API

接下來終於要測試了!

先確認:

  1. API Key 已經設定在環境變數。
  2. Spring Boot 已經重新啟動。
  3. API 專案有可用的模型權限及額度。
  4. 電腦可以連接外部網路。

接著打開 Postman。

HTTP Method:

GET

URL:

http://localhost:8080/health/ai/test

按下:

Send

這時候 Spring Boot 就會呼叫外部 AI API。

如果請求成功,預期回傳類似:

{
  "message": "智慧健康助理可以協助整理每日飲水與睡眠紀錄,讓使用者更方便觀察生活習慣。"
}

這裡要特別注意:

這是預期回覆範例,不是固定答案。

實際的 AI 回覆可能會使用不同文字。

只要可以正常取得模型產生的內容,就代表基本串接流程已經完成。


十七、如果 API 呼叫失敗怎麼辦?

第一次串接外部 API 時,可能會遇到不少錯誤。

所以今天也整理幾個常見情況。

狀況 可能原因 檢查方向
401 Unauthorized API Key 無效或未正確傳送 檢查環境變數與金鑰
403 Forbidden 專案權限不足 檢查 API 專案權限
429 Too Many Requests 速率限制或額度問題 檢查使用限制與計費
400 Bad Request Request Body 格式錯誤 檢查 model 與 JSON
連線逾時 網路或服務回應問題 檢查網路與逾時設定
沒有文字輸出 回覆未完成或輸出結構不同 檢查回覆狀態與內容

另外,今天的 AIService 已經加入基本錯誤處理。

例如:

if (response.statusCode() < 200
        || response.statusCode() >= 300) {

    throw new IllegalStateException(
            "AI API request failed. HTTP status: "
                    + response.statusCode()
    );
}

這樣可以避免 API 明明回傳錯誤,程式卻還把它當成成功結果處理。

不過目前只是教學版本。

如果未來要正式提供服務,還需要加入更完整的錯誤回應、重試策略、使用量控管和監控機制。

也要避免把 API Key 或外部服務的敏感錯誤內容直接回傳給使用者。


十八、為什麼不讓前端直接呼叫 AI API?

這裡還有一個很重要的觀念。

如果未來我們建立網頁介面,能不能直接從 JavaScript 呼叫 AI API?

技術上可以從瀏覽器發送 HTTP Request,但如果把私密 API Key 放進前端程式,就會有很大的安全問題。

因為瀏覽器端的程式碼和網路請求,使用者都可能查看。

所以我們不應該把真正的 API Key 放在:

const apiKey = "真正的_API_Key";

比較適合的架構是:

前端網頁
    ↓
Spring Boot API
    ↓
AIService
    ↓
OpenAI API

這樣 API Key 就可以保存在後端環境中。

未來也比較容易加入:

使用者驗證
請求次數限制
費用控管
輸入內容驗證

不過要注意,API Key 放在後端只是其中一項安全措施,不能取代完整的權限控管。


十九、目前的專案架構

今天新增了:

AIService.java
AIController.java

目前專案結構如下:

health-assistant
│
├── src/main/java/com/example/healthassistant
│   │
│   ├── HealthAssistantApplication.java
│   │
│   ├── controller
│   │   ├── HealthController.java
│   │   ├── WaterController.java
│   │   ├── SleepController.java
│   │   ├── HealthSummaryController.java
│   │   └── AIController.java
│   │
│   ├── service
│   │   ├── BmiService.java
│   │   ├── WaterService.java
│   │   ├── SleepService.java
│   │   ├── HealthSummaryService.java
│   │   └── AIService.java
│   │
│   ├── repository
│   │   ├── WaterRepository.java
│   │   └── SleepRepository.java
│   │
│   ├── dto
│   │   ├── HealthRequest.java
│   │   ├── WaterRequest.java
│   │   ├── WaterHistoryResponse.java
│   │   ├── WaterStatisticsResponse.java
│   │   ├── SleepRequest.java
│   │   ├── SleepResponse.java
│   │   ├── SleepHistoryResponse.java
│   │   ├── SleepStatisticsResponse.java
│   │   ├── WaterSummaryResponse.java
│   │   ├── SleepSummaryResponse.java
│   │   └── HealthSummaryResponse.java
│   │
│   └── exception
│       └── GlobalExceptionHandler.java
│
├── src/main/resources
│   ├── application.properties
│   └── schema.sql
│
└── pom.xml

今天最大的改變,就是系統開始可以和外部 AI 服務溝通。

整個架構也從:

Spring Boot
    ↓
H2 Database

擴充成:

             Spring Boot
                  │
          ┌───────┴───────┐
          ↓               ↓
      H2 Database      AIService
                          ↓
                      OpenAI API

這樣後面就可以把健康紀錄和 AI 功能結合起來。


二十、目前的 AI 功能還有哪些限制?

雖然今天已經建立第一個 AI 測試 API,但目前還只是初步串接。

1. Prompt 是固定的

目前我們直接在 Controller 裡面寫:

String prompt =
        "請用一句繁體中文介紹智慧健康助理。";

所以每次呼叫 API,輸入的問題都是相同的。

未來可以讓 Prompt 根據健康紀錄自動產生。

2. 還沒有整合健康資料

今天只是確認:

Java → AI API → Java

這個流程可以正常運作。

還沒有把飲水量或睡眠紀錄傳給 AI。

3. 尚未加入使用者驗證

目前任何能存取本機 API 的呼叫者,都可能觸發 AI API 請求。

如果未來要部署到網路上,就需要加入身分驗證、權限控管及請求限制,避免 API 被濫用。

4. AI 回覆仍然需要檢查

即使模型可以正常回覆,也不代表產生的內容一定正確。

尤其是健康相關資訊,不能直接把模型輸出當成醫療診斷。

未來需要進一步限制 Prompt 的用途,並檢查輸出是否符合預期。


Day 22 小結

今天終於成功建立智慧健康助理的第一個 AI 串接功能!

從前面一直使用 Java 處理資料,到今天開始透過 HTTP Request 呼叫生成式 AI API,算是一個重要的進展。

今天學到的重點包括:

  1. 認識 OpenAI Responses API
  2. 準備與保護 API Key
  3. 使用環境變數管理金鑰
  4. 使用 @Value 讀取 Spring Boot 設定
  5. 使用 Java HttpClient 發送 HTTP Request
  6. 使用 ObjectMapper 建立 JSON
  7. 解析 AI API 回傳的 JSON
  8. 建立 AIService
  9. 建立 AIController
  10. 使用 Postman 測試 AI API

目前智慧健康助理已經具備:

智慧健康助理
│
├── BMI 計算與分類
│
├── 飲水量紀錄
│   ├── 新增紀錄
│   ├── 歷史查詢
│   └── 七天統計
│
├── 睡眠時間紀錄
│   ├── 新增紀錄
│   ├── 指定日期查詢
│   └── 七天統計
│
├── 每日健康摘要
│   └── 整合飲水與睡眠資料
│
└── AI 功能
    └── 呼叫生成式 AI API

今天最大的收穫是:

原來 Java 不只可以建立自己的 API,也可以透過 HTTP Client 呼叫外部 AI 服務,將生成式 AI 的能力整合進 Spring Boot 專案。

而且透過 Controller 和 Service 分層,我們可以把 AI 串接邏輯獨立管理。

這樣後面要新增健康摘要功能時,就不需要重新撰寫整套 HTTP 呼叫程式。


明日預告

Day 23|讓 AI 看懂健康紀錄!自動產生每日健康文字摘要

今天已經完成第一次 Java 與 AI API 的串接。

但目前 AI 只能回答固定的測試 Prompt。

所以下一篇準備將之前建立的每日健康摘要資料,真正交給 AI 處理!

預計會學習:

  • 如何取得每日健康摘要 DTO
  • 如何將飲水與睡眠資料整理成 Prompt
  • 如何避免將沒有紀錄誤認成 0
  • 如何讓 AI 產生繁體中文健康紀錄摘要
  • 建立新的 AI 健康摘要 API
  • 使用 Postman 測試整合功能

讓智慧健康助理不只會計算和儲存資料,也開始能夠用自然語言整理每天的健康紀錄!


上一篇
認識生成式 AI API 與 Prompt
下一篇
用 Java 自動產生每日健康文字摘要
系列文
Java × AI:30天打造智慧健康助理 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言