info-spring-ai-with-java-generative-ai-rest-api-spring-boot

ผลลัพธ์การเรียนรู้: เมื่อจบ Workshop ผู้อ่านจะสามารถสร้าง REST API สำหรับ Chatbot ด้วย Spring AI, ส่งคำตอบแบบ Streaming, ลงทะเบียน Java Method ให้ LLM เรียกด้วย Tool Calling, รับผลแบบ Structured Output และอธิบายการทำ RAG ผ่าน PGvector ได้

ขอบเขตการใช้งาน: ตัวอย่างนี้ออกแบบเพื่อการเรียนรู้และใช้งานบนเครื่องพัฒนา ยังไม่เหมาะกับการเปิดเป็น Public API โดยไม่มีระบบยืนยันตัวตน การจำกัดอัตราการใช้งาน และการจัดการความลับ

#1. Spring AI คืออะไร?

Spring AI คือเฟรมเวิร์กที่ช่วยให้แอปพลิเคชัน Java/Spring เชื่อมต่อโมเดล AI ได้ผ่าน API ที่ออกแบบให้เป็นแนวทางเดียวกัน เช่น ChatModel, ChatClient, EmbeddingModel, VectorStore, Advisor และการเรียกเครื่องมือจากฝั่งแอป (Tool Calling) จึงลดความซับซ้อนในการผูกระบบ Spring Boot เข้ากับผู้ให้บริการโมเดลหลายราย

ตัวอย่างงานที่เหมาะสม ได้แก่ chatbot, API สรุปเอกสาร, ผู้ช่วยตอบคำถามภายในองค์กร, ระบบสืบค้นข้อมูลด้วย RAG, AI Assistant ที่เรียก Java Service และระบบสรุปผลเป็น JSON สำหรับต่อกับระบบอื่น

Spring AI ไม่ใช่ตัว LLM แต่เป็นชั้น integration/orchestration เพื่อเชื่อมโมเดล, ข้อมูล และเครื่องมือของแอปเข้าด้วยกัน

#2. องค์ประกอบของ Spring AI

องค์ประกอบ หน้าที่ ตัวอย่าง
ChatClient Fluent API สำหรับเรียกใช้โมเดล .prompt().user(...).call().content()
ChatModel Abstraction ของโมเดลสนทนา OpenAI, Ollama, ฯลฯ
EmbeddingModel เปลี่ยนข้อความเป็นเวกเตอร์ Embedding เพื่อใช้ค้นหาคล้ายกัน
VectorStore เพิ่มและค้นหาเอกสารด้วยเวกเตอร์ PGvector, Redis, Qdrant
Advisor เสริมกระบวนการก่อน/หลังเรียกโมเดล QuestionAnswerAdvisor สำหรับ RAG
@Tool เปิด Java method ให้โมเดลเลือกเรียก ค้นรหัสวิชา/ตรวจข้อมูลจาก Service
Structured Output แปลงคำตอบของ AI เป็นชนิดข้อมูล Java .entity(StudyPlan.class)

#3. ภาพรวมสถาปัตยกรรม

flowchart LR
    U[Client / Postman / Web] --> A[Spring Boot REST Controller]
    A --> S[AiService]
    S --> C[Spring AI ChatClient]
    C --> L[OpenAI Chat Model]
    C --> T[Java @Tool Method]
    C -. เมื่อเพิ่ม RAG .-> R[QuestionAnswerAdvisor]
    R --> V[PGvector Vector Store]
    V --> E[Embedding Model]

ลำดับทำงาน: Client เรียก REST API → Controller ตรวจข้อมูล → Service สร้าง Prompt → ChatClient ติดต่อโมเดล → รับคำตอบ/เรียก Tool ตามความจำเป็น → คืนค่า JSON หรือ stream ให้ Client

เปรียบเทียบความสามารถ

รูปแบบ จุดประสงค์ ใช้แหล่งข้อมูลภายนอกอย่างไร
Chat ถาม–ตอบทั่วไป อาศัยข้อมูลจาก Prompt และความสามารถของโมเดล
Tool Calling เรียกใช้ Business Logic เรียก Java Method/Service ที่แอปอนุญาต
RAG ถาม–ตอบจากเอกสาร ค้นเนื้อหาใน Vector Store แล้วประกอบ Prompt

หมายเหตุ: ChatClient ในตัวอย่างนี้ไม่ได้เก็บประวัติการสนทนาข้ามคำขอโดยอัตโนมัติ หากต้องการสนทนาต่อเนื่องให้เพิ่ม Chat Memory และกำหนด Conversation ID อย่างชัดเจน

#4. สิ่งที่ต้องเตรียม

  • JDK 21 (Spring Boot 4.0.x รองรับตั้งแต่ Java 17 แต่ใช้ 21 เพื่อให้เหมือนตัวอย่าง)
  • Maven 3.6.3 ขึ้นไป หรือ Docker Engine + Docker Compose
  • IDE เช่น IntelliJ IDEA / VS Code
  • OpenAI API key ที่มีสิทธิ์เข้าถึงโมเดลและมีการตั้งค่าบัญชีใช้งาน API
  • เครื่องทดสอบ REST API เช่น curl, Postman หรือ Bruno

ข้อควรระวัง: OpenAI API มีค่าใช้จ่ายตามการใช้งาน การสมัคร ChatGPT และการใช้ OpenAI API เป็นบริการคนละส่วนกัน; อย่าฝัง API key ลงใน Source Code หรือ Git และกำหนดงบประมาณทดสอบก่อนเริ่ม

#5. สร้างโปรเจกต์ Spring Boot

สร้างโปรเจกต์ Maven ที่มี Group dev.example, Artifact spring-ai-java-workshop, Java 21 และเลือก dependency ที่จำเป็น ดังนี้

  • spring-boot-starter-webmvc สำหรับ REST API (ชื่อที่แนะนำใน Spring Boot 4)
  • spring-boot-starter-webflux เพื่อใช้ Reactor/streaming ของ ChatClient พร้อมกับ Web MVC
  • spring-boot-starter-validation เพื่อตรวจสอบ JSON request
  • spring-ai-starter-model-openai สำหรับ OpenAI integration
  • spring-boot-starter-test สำหรับ Unit Testing

สอง starter คือ Web MVC และ WebFlux ถูกใส่ร่วมกันใน Workshop นี้เพื่อให้ REST Controller ทำงานแบบ Servlet และ ChatClient.stream() มี Reactive support; หากไม่ต้องทำ streaming ให้ตัด WebFlux ออกได้

ไฟล์ pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.8</version>
        <relativePath/>
    </parent>
    <groupId>dev.example</groupId>
    <artifactId>spring-ai-java-workshop</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-ai-java-workshop</name>
    <properties>
        <java.version>21</java.version>
        <spring-ai.version>2.0.1</spring-ai.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

#โครงสร้างโปรเจกต์

spring-ai-java-workshop/
├── pom.xml
├── .env.example
├── .gitignore
├── Dockerfile
├── compose.yaml
├── compose.pgvector.yaml          # ใช้เมื่อทำ RAG เพิ่มเติม
└── src/
    ├── main/
    │   ├── java/dev/example/springai/
    │   │   ├── SpringAiWorkshopApplication.java
    │   │   ├── AiController.java
    │   │   ├── AiService.java
    │   │   ├── StudyPlan.java
    │   │   └── CourseTools.java
    │   └── resources/application.yml
    └── test/java/dev/example/springai/CourseToolsTest.java

#6. กำหนดค่าโมเดลด้วย application.yml

Spring AI 2.0.1 ใช้การกำหนดค่าโมเดลผ่าน spring.ai.openai.chat.model (ระวังไม่คัดลอก key รุ่นเก่าบางตัวอย่างที่ใช้ .options.model)

ไฟล์ src/main/resources/application.yml

server:
  port: 8080
spring:
  application:
    name: spring-ai-java-workshop
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        model: ${OPENAI_CHAT_MODEL:gpt-4o-mini}

ก่อนรันให้กำหนด API key ผ่าน environment variable:

# macOS / Linux
export OPENAI_API_KEY="sk-your-api-key"
# PowerShell
$env:OPENAI_API_KEY="sk-your-api-key"

ค่าตัวอย่างของโมเดลคือ gpt-4o-mini เปลี่ยนได้โดยตั้ง OPENAI_CHAT_MODEL ให้เป็นชื่อโมเดลที่บัญชีของคุณรองรับ

#7. เขียนคลาสเริ่มต้น Spring Boot

ไฟล์ src/main/java/dev/example/springai/SpringAiWorkshopApplication.java

package dev.example.springai;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringAiWorkshopApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringAiWorkshopApplication.class, args);
    }
}

#8. สร้าง Java Tool สำหรับให้ AI เรียกใช้งาน

เราใช้ข้อมูลรายวิชาตัวอย่างเพียงสองรายการโดยตั้งใจ เพื่อสาธิตว่าโมเดลไม่ได้เรียกฐานข้อมูลเอง แต่จะร้องขอให้แอปเรียก Java method ที่ประกาศผ่าน @Tool แล้วส่งผลกลับให้โมเดลนำไปตอบผู้ใช้ ข้อมูลนี้เป็นข้อมูลสมมติ ไม่ใช่รายวิชาจริง

ไฟล์ src/main/java/dev/example/springai/CourseTools.java

package dev.example.springai;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

@Component
public class CourseTools {

    @Tool(description = "Retrieve demo course information by course code, such as SE101 or DS201")
    public String findCourse(
            @ToolParam(description = "Course code, e.g. SE101 or DS201") String courseCode) {
        if (courseCode == null) {
            return "กรุณาระบุรหัสรายวิชา";
        }
        return switch (courseCode.trim().toUpperCase()) {
            case "SE101" -> "SE101: Software Engineering Basics, 3 credits, 15 weeks";
            case "DS201" -> "DS201: Applied Data Science, 3 credits, 15 weeks";
            default -> "ไม่พบข้อมูลรายวิชารหัส " + courseCode;
        };
    }
}

ด้านความปลอดภัย: อย่าเปิด method สำหรับลบข้อมูล โอนเงิน หรือส่งข้อความโดยไม่มีการยืนยันสิทธิ์/ตรวจทานคำสั่ง โปรดผูกสิทธิ์ของผู้ใช้กับ Tool และตรวจสอบ input เสมอ

#9. สร้างชนิดข้อมูลสำหรับ Structured Output

Spring AI แปลงผลลัพธ์เป็น Java record ได้ผ่าน .entity(StudyPlan.class) เหมาะกับระบบที่ต้องการโครงสร้างผลลัพธ์แทนข้อความทั่วไป ทั้งนี้ควรทำ validation เพิ่มในระบบจริง เพราะผลลัพธ์จากโมเดลอาจขาดฟิลด์หรือมีค่าที่ไม่เหมาะสม

ไฟล์ src/main/java/dev/example/springai/StudyPlan.java

package dev.example.springai;

import java.util.List;

public record StudyPlan(String topic, List<String> steps, int estimatedHours) {}

#10. เขียน Service สำหรับเรียก Spring AI

สิ่งที่ทดลองในคลาสนี้ มี 4 กรณี: Chat ปกติ, Streaming, Tool Calling และ Structured Output

ไฟล์ src/main/java/dev/example/springai/AiService.java

package dev.example.springai;

import java.util.Objects;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Service
public class AiService {

    private final ChatClient chatClient;
    private final CourseTools courseTools;

    public AiService(ChatClient.Builder chatClientBuilder, CourseTools courseTools) {
        this.chatClient = chatClientBuilder
                .defaultSystem("คุณเป็นผู้ช่วยด้านการเรียนวิศวกรรมซอฟต์แวร์ ตอบภาษาไทยอย่างกระชับและชัดเจน")
                .build();
        this.courseTools = courseTools;
    }

    public String ask(String message) {
        return Objects.requireNonNullElse(
                chatClient.prompt().user(message).call().content(), "ไม่พบคำตอบ");
    }

    public Flux<String> stream(String message) {
        return chatClient.prompt().user(message).stream().content();
    }

    public String askWithTools(String message) {
        return Objects.requireNonNullElse(
                chatClient.prompt()
                        .user(message)
                        .tools(courseTools)
                        .call()
                        .content(),
                "ไม่พบคำตอบ");
    }

    public StudyPlan createStudyPlan(String topic) {
        return Objects.requireNonNull(
                chatClient.prompt()
                        .user("จัดทำแผนการเรียนรู้สำหรับหัวข้อ: " + topic
                                + " โดยตอบเป็นข้อมูลโครงสร้างที่ประกอบด้วย topic, steps, estimatedHours")
                        .call()
                        .entity(StudyPlan.class),
                "โมเดลไม่สามารถสร้างแผนการเรียนรู้ได้");
    }
}

อธิบายเมธอดสำคัญ

  • prompt().user(message).call().content() เรียกโมเดลแบบ synchronous แล้วรับข้อความ
  • prompt().user(message).stream().content() รับชิ้นส่วนข้อความเป็น Flux<String>
  • .tools(courseTools) แนบ Tool ที่โมเดลสามารถเลือกใช้เฉพาะคำขอนั้น; การทำ Tool Calling ขึ้นอยู่กับความสามารถของโมเดลและคำถาม
  • .entity(StudyPlan.class) แปลงคำตอบที่สมบูรณ์แล้วเป็นชนิดข้อมูล Java; ไม่ใช่การ stream JSON ทีละชิ้น

#11. สร้าง REST Controller

กำหนด API 4 เส้นทาง: /chat, /stream, /tool-chat และ /study-plan โดย request อยู่ในรูปแบบ {"message":"..."}

ไฟล์ src/main/java/dev/example/springai/AiController.java

package dev.example.springai;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/ai")
public class AiController {

    private final AiService service;

    public AiController(AiService service) {
        this.service = service;
    }

    public record ChatRequest(@NotBlank @Size(max = 2000) String message) {}
    public record ChatReply(String answer) {}

    @PostMapping("/chat")
    public ChatReply chat(@Valid @RequestBody ChatRequest request) {
        return new ChatReply(service.ask(request.message()));
    }

    @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> stream(@Valid @RequestBody ChatRequest request) {
        return service.stream(request.message());
    }

    @PostMapping("/tool-chat")
    public ChatReply toolChat(@Valid @RequestBody ChatRequest request) {
        return new ChatReply(service.askWithTools(request.message()));
    }

    @PostMapping("/study-plan")
    public StudyPlan studyPlan(@Valid @RequestBody ChatRequest request) {
        return service.createStudyPlan(request.message());
    }
}

#12. ทดลองรันโปรเจกต์

mvn clean spring-boot:run

หรือหากมี Maven wrapper จาก Spring Initializr ให้ใช้ ./mvnw spring-boot:run

#12.1 Chat แบบปกติ

curl -X POST http://localhost:8080/api/ai/chat \
  -H 'Content-Type: application/json' \
  -d '{"message":"อธิบาย SOLID Principles แบบเข้าใจง่าย"}'

ตัวอย่างโครงสร้างคำตอบ (เนื้อหาจริงอาจเปลี่ยนได้):

{"answer":"SOLID เป็นหลักการออกแบบซอฟต์แวร์เชิงวัตถุ 5 ข้อ ..."}

#12.2 Streaming (SSE)

curl -N -X POST http://localhost:8080/api/ai/stream \
  -H 'Content-Type: application/json' \
  -d '{"message":"สรุปการทำงานของ REST API เป็น 5 ข้อ"}'

คำตอบจะถูกส่งมาเป็นหลายเหตุการณ์/ชิ้นส่วนตามที่โมเดล generate; คำขอไม่จำเป็นต้องรอคำตอบทั้งหมดก่อนจึงเริ่มแสดงผล

#12.3 Tool Calling

curl -X POST http://localhost:8080/api/ai/tool-chat \
  -H 'Content-Type: application/json' \
  -d '{"message":"ช่วยค้นข้อมูลรายวิชา SE101 จากเครื่องมือให้หน่อย"}'

โมเดลอาจเลือกเรียก findCourse("SE101") และนำผลกลับมาสรุปเป็นภาษาไทย พฤติกรรมการเลือก Tool ไม่ได้รับประกันว่าจะเหมือนกันทุกครั้ง

#12.4 Structured Output

curl -X POST http://localhost:8080/api/ai/study-plan \
  -H 'Content-Type: application/json' \
  -d '{"message":"Unit Testing ด้วย JUnit 5"}'

ตัวอย่าง JSON (เพื่อแสดง schema):

{
  "topic": "Unit Testing ด้วย JUnit 5",
  "steps": ["ทำความเข้าใจ Assertions", "ฝึกเขียน Test Case", "ทดลอง Mocking"],
  "estimatedHours": 6
}

#13. เขียน Unit Test โดยไม่เรียก AI API

การทดสอบ CourseTools ควรเป็น deterministic unit test เพื่อไม่เพิ่มค่าใช้จ่าย API และไม่ทำให้ CI ล้มเหลวเพราะเครือข่าย/โมเดล

ไฟล์ src/test/java/dev/example/springai/CourseToolsTest.java

package dev.example.springai;

import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;

class CourseToolsTest {

    private final CourseTools courseTools = new CourseTools();

    @Test
    void findsKnownCourse() {
        assertTrue(courseTools.findCourse("se101").contains("3 credits"));
    }

    @Test
    void handlesUnknownCourse() {
        assertTrue(courseTools.findCourse("NO999").contains("ไม่พบ"));
    }
}

รันการทดสอบ:

mvn test

Unit Test นี้ตรวจเฉพาะตรรกะของ Tool ยังไม่ได้ทดสอบการเรียกโมเดลจริง, รูปแบบ Structured Output หรือความถูกต้องของคำตอบ ควรเพิ่ม contract/evaluation test ของ AI เป็นชุดแยก โดยใช้ test dataset, metrics และ mock provider ตามความเหมาะสม

#14. ใช้ Docker และ Docker Compose

ใช้ Multi-stage build เพื่อบรรจุ Java Application ลงใน container (ต้องมีอินเทอร์เน็ตสำหรับดาวน์โหลด Maven dependencies ในครั้งแรก)

ไฟล์ Dockerfile

FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /workspace
COPY pom.xml ./
COPY src ./src
RUN mvn -B -DskipTests package

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /workspace/target/*.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

ไฟล์ compose.yaml

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      OPENAI_API_KEY: ${OPENAI_API_KEY:?Set OPENAI_API_KEY in .env}
      OPENAI_CHAT_MODEL: ${OPENAI_CHAT_MODEL:-gpt-4o-mini}

#ตัวอย่าง .env

ไฟล์ .env.example

# Copy this file to .env and fill in your own key (never commit .env)
OPENAI_API_KEY=replace-with-your-own-key
OPENAI_CHAT_MODEL=gpt-4o-mini

ใช้งาน:

cp .env.example .env
# แก้ค่า OPENAI_API_KEY ในไฟล์ .env เป็นของคุณ
docker compose up --build -d
docker compose logs -f app
# เมื่อเลิกใช้งาน
docker compose down

Docker Compose .env ไม่ได้เข้ารหัสความลับ: ตัวอย่างนี้ใช้เพื่อการศึกษา สำหรับ production ควรใช้ Secret Manager / runtime secrets และหลีกเลี่ยงการเก็บ API key ใน image หรือ Git

#15. ต่อขยายเป็น RAG ด้วย PostgreSQL + PGvector (Optional)

Retrieval-Augmented Generation (RAG) ช่วยให้โมเดลตอบคำถามโดยอาศัยเนื้อหาที่ค้นคืนจากเอกสารขององค์กร ไม่ต้องฝึกโมเดลใหม่ทุกครั้งเมื่อข้อมูลเปลี่ยน

#15.1 ลำดับการทำงานของ RAG

flowchart TD
    D[เอกสารภายใน] --> I[แบ่งข้อความ / สร้าง Document]
    I --> EM[Embedding Model]
    EM --> VS[(PostgreSQL + PGvector)]
    Q[คำถาม] --> ADV[QuestionAnswerAdvisor]
    ADV --> VS
    VS --> K[Context ที่เกี่ยวข้อง]
    K --> LLM[ChatClient + LLM]
    Q --> LLM
    LLM --> ANS[คำตอบอ้างอิง Context]

ไม่ควรเข้าใจผิดว่า RAG ป้องกัน hallucination ได้หมด: ถ้าเอกสารไม่ครบ, การค้นคืนไม่ตรงคำถาม หรือ prompt ไม่ดี คำตอบก็อาจผิดได้

#15.2 เพิ่ม dependencies เฉพาะเมื่อเริ่มทำ RAG

เปิดไฟล์ pom.xml และเพิ่มรายการต่อไปนี้ภายใน <dependencies>:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-vector-store-advisor</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

#15.3 สร้าง PGvector ด้วย Compose

ไฟล์ Compose สำหรับระบบฐานข้อมูลแยกออกมา เพื่อไม่บังคับให้ผู้เรียนที่ต้องการเฉพาะ Chat ใช้ PostgreSQL

ไฟล์ compose.pgvector.yaml

services:
  pgvector:
    image: pgvector/pgvector:0.8.6-pg17
    environment:
      POSTGRES_DB: aiworkshop
      POSTGRES_USER: aiuser
      POSTGRES_PASSWORD: change-me-for-local-demo
    ports:
      - "5433:5432"
    volumes:
      - pgvector_data:/var/lib/postgresql/data
volumes:
  pgvector_data:
docker compose -f compose.pgvector.yaml up -d

#15.4 เพิ่มค่าคอนฟิก RAG

ใน application.yml เพิ่มส่วน spring.datasource และ spring.ai โดยจัด indentation ให้ถูกต้อง ตัวอย่างการตั้งค่าเพิ่มเติม:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5433/aiworkshop
    username: aiuser
    password: ${DB_PASSWORD:change-me-for-local-demo}
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        model: ${OPENAI_CHAT_MODEL:gpt-4o-mini}
      embedding:
        model: text-embedding-3-small
    vectorstore:
      pgvector:
        initialize-schema: true
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1536

เมื่อเชื่อมต่อผ่าน localhost:5433 ให้รัน Java application บนเครื่อง host หากรันแอปภายใน Docker network ต้องเปลี่ยน host/port ให้ตรงกับ service name pgvector:5432 และใส่ service ใน Compose เดียวกัน

สำคัญ: dimensions=1536 ใช้ได้กับ text-embedding-3-small ที่ไม่ได้ override ขนาดเวกเตอร์ เมื่อเปลี่ยน embedding model หรือ dimensions ของตารางเดิม ต้องวางแผน re-index/recreate schema ให้ตรงกัน; อย่าใช้ remove-existing-vector-store-table=true กับฐานข้อมูลจริง นอกจากนี้คำสั่ง vectorStore.add(...) จะเรียก Embedding API ซึ่งอาจมีค่าใช้จ่าย

#15.5 เพิ่ม REST API สำหรับ ingest เอกสารและถามตอบ

เพิ่มไฟล์ RagController.java ภายใต้ package เดียวกัน หลังจากเพิ่ม dependencies และ PGvector config แล้ว:

package dev.example.springai;

import java.util.List;
import java.util.Map;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/rag")
public class RagController {
    private final VectorStore vectorStore;
    private final ChatClient chatClient;

    public RagController(VectorStore vectorStore, ChatClient.Builder builder) {
        this.vectorStore = vectorStore;
        this.chatClient = builder.build();
    }

    public record DocumentRequest(String text) {}
    public record QuestionRequest(String question) {}

    @PostMapping("/documents")
    public Map<String, String> addDocument(@RequestBody DocumentRequest request) {
        if (request.text() == null || request.text().isBlank()) {
            throw new IllegalArgumentException("Document text must not be blank");
        }
        vectorStore.add(List.of(new Document(request.text(), Map.of("source", "workshop"))));
        return Map.of("status", "indexed");
    }

    @PostMapping("/ask")
    public Map<String, String> ask(@RequestBody QuestionRequest request) {
        var advisor = QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(SearchRequest.builder().topK(3).build())
                .build();
        String answer = chatClient.prompt()
                .user(request.question())
                .advisors(advisor)
                .call()
                .content();
        return Map.of("answer", answer == null ? "" : answer);
    }
}

นำเข้าเอกสารสมมติ:

curl -X POST http://localhost:8080/api/rag/documents \
  -H 'Content-Type: application/json' \
  -d '{"text":"การฝึกงานของหลักสูตรตัวอย่างใช้เวลา 16 สัปดาห์ โดยต้องส่งรายงานทุก 2 สัปดาห์"}'

ถามข้อมูลจากเอกสาร:

curl -X POST http://localhost:8080/api/rag/ask \
  -H 'Content-Type: application/json' \
  -d '{"question":"หลักสูตรตัวอย่างกำหนดให้ส่งรายงานฝึกงานบ่อยแค่ไหน"}'

ข้อจำกัดของตัวอย่าง: รับเอกสารเป็นข้อความล้วนและไม่แบ่ง chunk, ยังไม่มีการยืนยันตัวตน/สิทธิ์เข้าถึงเอกสาร, ไม่จัดการซ้ำ, ไม่คืน citation ระดับเอกสาร และไม่รองรับ multi-tenant ดังนั้นควรต่อยอดก่อนนำไปใช้จริง

แนวทางประเมินผล RAG สำหรับการทดลอง: เตรียมคำถามที่มีคำตอบอ้างอิง 20 ข้อ จากเอกสารที่นำเข้า แยกคำถามที่ตอบได้/ตอบไม่ได้จากเอกสาร จากนั้นเปรียบเทียบการตอบแบบปกติกับการตอบผ่าน RAG ด้วยเกณฑ์ Accuracy, Faithfulness (ความสอดคล้องกับเอกสาร), Retrieval Recall, Latency และ Token Usage โดยใช้ชุดคำถามเดียวกันทุกเงื่อนไข

#16. แนวทางใช้ใน Production

ประเด็น สิ่งที่ควรทำ
API Security ใช้ Spring Security, JWT/OAuth2, ตรวจสิทธิ์แยกตามผู้ใช้และ tenant
Credentials เก็บ API key ใน secret manager, ห้ามส่งคีย์ไป frontend
Abuse & Cost rate limiting, request quotas, จำกัด token และดูค่าบริการของโมเดล
Data Privacy ลบ/ปกปิด PII, พิจารณานโยบายข้อมูลของผู้ให้บริการโมเดล
Prompt Injection ปฏิบัติต่อ input/เอกสารที่ค้นคืนเป็นข้อมูลที่ไม่เชื่อถือ ไม่ให้สิทธิ์ทำ destructive tool โดยพลการ
Observability เก็บ latency, token usage, error rate, tracing ด้วย Micrometer/Actuator และไม่ log ความลับ
Evaluation ทำชุดคำถามอ้างอิง, วัด factual correctness, retrieval recall, faithfulness และ cost/latency
Reliability timeout, backoff, retry เฉพาะ error ที่เหมาะสม, circuit breaker และ fallback
RAG ใช้ chunking + metadata, version เอกสาร, filter ตามสิทธิ์, คืนแหล่งอ้างอิงและวัดคุณภาพการค้นคืน

#17. แนวทางทำแบบฝึกหัดในชั้นเรียน

  1. Level 1 — REST Chat: สร้าง endpoint /chat และเพิ่ม system prompt ให้ตอบเฉพาะ Software Engineering
  2. Level 2 — Tool Calling: เพิ่ม findCourse() รองรับรหัสวิชาใหม่ และทดสอบกรณีไม่พบข้อมูล
  3. Level 3 — Structured Output: เปลี่ยน StudyPlan ให้มีระดับความยากและตรวจสอบค่าที่โมเดลส่งกลับ
  4. Level 4 — RAG: เอกสาร 10 ฉบับใน PGvector เปรียบเทียบคำตอบแบบมี/ไม่มี RAG
  5. Level 5 — AI Evaluation: สร้าง test set 20 คำถาม วัดความถูกต้อง latency และราคาเฉลี่ยต่อคำถาม

#คำถามทบทวน

  • ChatClient และ ChatModel ต่างกันอย่างไร?
  • ทำไมจึงไม่ควรส่ง API key ไปให้ JavaScript บน Browser?
  • คำตอบของ Tool Calling ต่างจาก RAG อย่างไร?
  • การใช้ @Tool จำเป็นต้องให้โมเดลเป็นผู้ตัดสินใจเรียกทุกครั้งหรือไม่?
  • เพราะเหตุใด Structured Output ยังควรมี validation บนฝั่ง Java?

#18. ปัญหาที่พบบ่อย

อาการ แนวทางตรวจสอบ
401 Unauthorized จาก OpenAI ตรวจสอบ OPENAI_API_KEY และบัญชี API
429 Too Many Requests ตรวจสอบ quota, rate limit, budget และนโยบาย retry
รันไม่ขึ้นเพราะ environment variable ตั้งค่า OPENAI_API_KEY ใน shell/process ที่รัน Spring Boot
404 model not found ตรวจสอบชื่อโมเดลและสิทธิ์เข้าถึงในบัญชี
Stream ไม่แสดงผลระหว่างทาง ใช้ curl -N, ตรวจ proxy buffering และการรองรับ SSE
Tool ไม่ถูกเรียก ตรวจว่า request ใช้ /tool-chat, ส่ง .tools(...), โมเดลรองรับ tools และข้อความชัดเจน
PGvector เชื่อมไม่ได้ ตรวจ port 5433, Docker volume, username/password และ network
RAG ตอบไม่ตรง ตรวจเนื้อหาเอกสาร, chunking, metadata, embedding dimensions และ topK

#19. สรุป

Spring AI ช่วยให้ระบบ Java ที่พัฒนาด้วย Spring Boot ผสานโมเดลภาษาเข้ากับ business logic ผ่าน interface ที่เป็นมาตรฐาน ทั้งการสนทนาแบบปกติ การส่งผลแบบ streaming การให้โมเดลเลือกเรียก Java Tools และการแปลงข้อความเป็น Structured Output ก่อนต่อยอดเป็น RAG ด้วย Vector Store

แนวทางเริ่มต้นที่เหมาะสำหรับผู้เรียน คือทำ REST Chat ให้สำเร็จ → ทดลอง Structured Output → เพิ่ม Tool Calling → สร้าง RAG → เพิ่มการทดสอบ ความปลอดภัย และระบบติดตามคุณภาพ

#เอกสารอ้างอิง (ตรวจสอบ ณ 14 ตุลาคม 2026)

  1. Spring AI — https://spring.io/projects/spring-ai
  2. Spring AI Getting Started — https://docs.spring.io/spring-ai/reference/getting-started.html
  3. Spring AI ChatClient API — https://docs.spring.io/spring-ai/reference/api/chatclient.html
  4. Spring AI OpenAI Chat — https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html
  5. Spring AI Tool Calling — https://docs.spring.io/spring-ai/reference/api/tools.html
  6. Spring AI RAG — https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html
  7. Spring AI PGvector — https://docs.spring.io/spring-ai/reference/api/vectordbs/pgvector.html
  8. Spring Boot 4 Web — https://docs.spring.io/spring-boot/4.0/reference/web/
  9. Spring AI OpenAI Embeddings — https://docs.spring.io/spring-ai/reference/api/embeddings/openai-embeddings.html
  10. pgvector Docker — https://github.com/pgvector/pgvector#docker

หมายเหตุ: Source Code เป็นตัวอย่างสำหรับการศึกษาและตรวจสอบเทียบกับเอกสาร API แล้ว แต่ยังไม่ได้รัน Maven build หรือทดสอบ end-to-end กับ OpenAI API ในสภาพแวดล้อมนี้ และผู้ใช้ต้องจัดเตรียม API key/สิทธิ์โมเดลเองก่อนรันจริง