- 1. Spring AI คืออะไร?
- 2. องค์ประกอบของ Spring AI
- 3. ภาพรวมสถาปัตยกรรม
- 4. สิ่งที่ต้องเตรียม
- 5. สร้างโปรเจกต์ Spring Boot
- 6. กำหนดค่าโมเดลด้วย application.yml
- 7. เขียนคลาสเริ่มต้น Spring Boot
- 8. สร้าง Java Tool สำหรับให้ AI เรียกใช้งาน
- 9. สร้างชนิดข้อมูลสำหรับ Structured Output
- 10. เขียน Service สำหรับเรียก Spring AI
- 11. สร้าง REST Controller
- 12. ทดลองรันโปรเจกต์
- 13. เขียน Unit Test โดยไม่เรียก AI API
- 14. ใช้ Docker และ Docker Compose
- 15. ต่อขยายเป็น RAG ด้วย PostgreSQL + PGvector (Optional)
- 16. แนวทางใช้ใน Production
- 17. แนวทางทำแบบฝึกหัดในชั้นเรียน
- 18. ปัญหาที่พบบ่อย
- 19. สรุป
- เอกสารอ้างอิง (ตรวจสอบ ณ 14 ตุลาคม 2026)

ผลลัพธ์การเรียนรู้: เมื่อจบ 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 MVCspring-boot-starter-validationเพื่อตรวจสอบ JSON requestspring-ai-starter-model-openaiสำหรับ OpenAI integrationspring-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 namepgvector: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. แนวทางทำแบบฝึกหัดในชั้นเรียน
- Level 1 — REST Chat: สร้าง endpoint
/chatและเพิ่ม system prompt ให้ตอบเฉพาะ Software Engineering - Level 2 — Tool Calling: เพิ่ม
findCourse()รองรับรหัสวิชาใหม่ และทดสอบกรณีไม่พบข้อมูล - Level 3 — Structured Output: เปลี่ยน
StudyPlanให้มีระดับความยากและตรวจสอบค่าที่โมเดลส่งกลับ - Level 4 — RAG: เอกสาร 10 ฉบับใน PGvector เปรียบเทียบคำตอบแบบมี/ไม่มี RAG
- 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)
- Spring AI — https://spring.io/projects/spring-ai
- Spring AI Getting Started — https://docs.spring.io/spring-ai/reference/getting-started.html
- Spring AI ChatClient API — https://docs.spring.io/spring-ai/reference/api/chatclient.html
- Spring AI OpenAI Chat — https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html
- Spring AI Tool Calling — https://docs.spring.io/spring-ai/reference/api/tools.html
- Spring AI RAG — https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html
- Spring AI PGvector — https://docs.spring.io/spring-ai/reference/api/vectordbs/pgvector.html
- Spring Boot 4 Web — https://docs.spring.io/spring-boot/4.0/reference/web/
- Spring AI OpenAI Embeddings — https://docs.spring.io/spring-ai/reference/api/embeddings/openai-embeddings.html
- pgvector Docker — https://github.com/pgvector/pgvector#docker
หมายเหตุ: Source Code เป็นตัวอย่างสำหรับการศึกษาและตรวจสอบเทียบกับเอกสาร API แล้ว แต่ยังไม่ได้รัน Maven build หรือทดสอบ end-to-end กับ OpenAI API ในสภาพแวดล้อมนี้ และผู้ใช้ต้องจัดเตรียม API key/สิทธิ์โมเดลเองก่อนรันจริง