Key Findings
- 從 Kotlin Notebook 遷移到 Spring Boot 的核心是三層架構:Controller(API 端點)、Service(業務邏輯)、Config(AI 元件配置)——Notebook 裡的程式碼幾乎可以直接複製到 Service 層
- 課程附帶完整的 spring-ai-demo 專案,整合了 Lesson 1~9 的所有功能:聊天(L1)、串流(L2)、結構化輸出(L3)、Tool Calling(L4)、記憶(L5)、RAG(L6-7)、多模態(L8)、文件上傳(L9)
- PGVector 取代 SimpleVectorStore 只需切換 Spring Profile——從
application.yml到application-pgvector.yml,Service 層程式碼完全不變 - 專案包含 Vue 3 前端與 Docker + Cloud Run 部署配置,從開發到上線一步到位
為什麼要遷移到 Spring Boot?
Kotlin Notebook 非常適合學習和原型開發,但正式上線需要 Spring Boot[1]:
| 面向 | Kotlin Notebook | Spring Boot |
|---|---|---|
| 適合場景 | 學習、實驗、原型 | 正式開發、生產部署 |
| API 端點 | 無法提供 HTTP API | REST Controller + SSE |
| 依賴管理 | @file:DependsOn | Gradle/Maven(版本管理更完善) |
| 設定管理 | 硬編碼或 .env | application.yml + Spring Profile |
| 資料庫 | SimpleVectorStore(記憶體) | PGVector / Milvus(持久化) |
| 部署 | 無法部署 | Docker → Cloud Run / K8s |
| 前端整合 | 無法提供 UI | Vue/React + SSE 即時串流 |
專案架構總覽
課程提供的 spring-ai-demo 是一個完整的全端專案:
spring-ai-demo/
├── build.gradle.kts # 依賴管理(Spring AI BOM 1.0.0)
├── docker-compose.yml # PGVector 資料庫
├── Dockerfile # 多階段建構
├── .github/workflows/deploy.yml # Cloud Run 自動部署
│
├── src/main/kotlin/com/example/demo/
│ ├── DemoApplication.kt # 啟動入口
│ ├── config/AiConfig.kt # AI 元件配置
│ ├── controller/ChatController.kt # REST API 端點
│ ├── service/ChatService.kt # 核心業務邏輯(整合 L1-L9)
│ └── tool/DateTimeTools.kt # @Tool 工具(L4)
│
├── src/main/resources/
│ ├── application.yml # 預設配置
│ ├── application-pgvector.yml # PGVector 配置
│ └── prompts/system.st # 系統 Prompt 模板(L3)
│
├── frontend/ # Vue 3 + Vite 前端
│ └── src/components/ # 聊天介面元件
│
└── http/ # IntelliJ HTTP 測試檔案
├── 01-basic-chat.http
├── 02-stream-chat.http
└── 03-memory-chat.http
Step 1:Gradle 依賴配置
Spring AI 使用 BOM(Bill of Materials)統一管理版本:
// build.gradle.kts
plugins {
kotlin("jvm") version "2.1.0"
kotlin("plugin.spring") version "2.1.0"
id("org.springframework.boot") version "3.4.1"
}
dependencyManagement {
imports {
mavenBom("org.springframework.ai:spring-ai-bom:1.0.0")
}
}
dependencies {
// Spring AI 核心
implementation("org.springframework.ai:spring-ai-starter-model-openai")
implementation("org.springframework.ai:spring-ai-advisors-vector-store")
implementation("org.springframework.ai:spring-ai-tika-document-reader")
// 向量資料庫(PGVector)
implementation("org.springframework.ai:spring-ai-starter-vector-store-pgvector")
runtimeOnly("org.postgresql:postgresql")
// Spring Boot
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
}
Step 2:application.yml 配置
# application.yml — 預設配置
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
temperature: 0.7
# 預設排除 PGVector(使用 SimpleVectorStore)
autoconfigure:
exclude:
- org.springframework.ai.autoconfigure.vectorstore.pgvector.PgVectorStoreAutoConfiguration
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
# application-pgvector.yml — 啟用 PGVector
spring:
autoconfigure:
exclude: [] # 清除排除,啟用自動配置
datasource:
url: jdbc:postgresql://localhost:5433/spring_ai
username: spring_ai
password: spring_ai_password
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
切換只需啟動參數:--spring.profiles.active=pgvector
Step 3:Config——AI 元件配置
AiConfig.kt 負責初始化向量資料庫並載入初始文件:
@Configuration
class AiConfig {
@Bean
@Profile("!pgvector")
fun simpleVectorStore(embeddingModel: EmbeddingModel): VectorStore {
return SimpleVectorStore.builder(embeddingModel).build()
}
@Bean
fun initializeKnowledgeBase(vectorStore: VectorStore) = CommandLineRunner {
val documents = listOf(
Document("年假規定:到職滿一年享有 7 天特休假...",
mapOf("source" to "員工手冊", "category" to "leave")),
Document("程式碼審查:所有 PR 需資深工程師審查...",
mapOf("source" to "工程規範", "category" to "tech")),
Document("密碼規範:每 90 天更換,至少 12 字元...",
mapOf("source" to "資安政策", "category" to "security")),
// ... 共 8 篇初始文件
)
vectorStore.add(documents)
println("✓ 已載入 ${documents.size} 篇知識庫文件")
}
}
Step 4:Service 層——核心業務邏輯
ChatService.kt 整合了 Lesson 1~9 的所有功能。這是整個應用最核心的類別:
@Service
class ChatService(
private val chatModel: ChatModel,
private val vectorStore: VectorStore
) {
// L5: ChatMemory
private val chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20).build()
// L1+L4+L5: 帶記憶 + 工具的 ChatClient
private val assistant = ChatClient.builder(chatModel)
.defaultSystem(Resource("classpath:prompts/system.st"))
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.defaultTools(DateTimeTools())
.build()
// L2+L5: 串流用的 ChatClient
private val streamClient = ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.defaultTools(DateTimeTools())
.build()
// L1+L6: 聊天 + 手動 RAG(含來源追蹤)
fun chat(message: String, sessionId: String): ChatResult {
// 向量搜尋
val docs = vectorStore.similaritySearch(
SearchRequest.builder().query(message).topK(3).build()
)
// 組裝上下文(含來源)
val context = docs.joinToString("\n\n") {
"【${it.metadata["source"]}】${it.text}"
}
val sources = docs.map { it.metadata["source"] as String }.distinct()
// 帶上下文的提問
val answer = assistant.prompt()
.user("參考資料:\n$context\n\n問題: $message")
.advisors { it.param(CONVERSATION_ID, sessionId) }
.call().content() ?: ""
return ChatResult(answer, sources)
}
// L2: SSE 串流
fun streamChat(message: String, sessionId: String): Flux<String> {
return streamClient.prompt()
.user(message)
.advisors { it.param(CONVERSATION_ID, sessionId) }
.stream().content()
}
// L3: 結構化輸出(情感分析)
fun analyzeText(text: String): String {
return simpleClient.prompt()
.user("""分析以下文字,回傳 JSON 格式:
{"sentiment": "正面/負面/中性",
"keywords": [...],
"summary": "一句話摘要",
"category": "類別"}
文字: $text""")
.call().content() ?: "{}"
}
// L8: 圖片生成(中文→英文 Prompt 優化 + DALL-E 3)
fun generateImage(description: String): String {
val prompt = simpleClient.prompt()
.system("將中文描述轉為英文 DALL-E 3 Prompt。")
.user(description)
.call().content() ?: description
val response = imageModel.call(ImagePrompt(prompt,
OpenAiImageOptions.builder()
.model("dall-e-3").quality("standard")
.height(1024).width(1024).build()
))
return response.result.output.url
}
// L9: 文件上傳
fun uploadDocument(file: MultipartFile): Int {
val content = file.inputStream.bufferedReader().readText()
val doc = Document(content,
mapOf("source" to file.originalFilename, "format" to "uploaded"))
val chunks = TokenTextSplitter.builder()
.withChunkSize(400).build()
.split(listOf(doc))
vectorStore.add(chunks)
return chunks.size
}
}
Step 5:Controller 層——REST API 端點
@RestController
@RequestMapping("/api")
class ChatController(private val chatService: ChatService) {
// L1+L5+L6: 聊天(含記憶 + RAG + 來源追蹤)
@PostMapping("/chat")
fun chat(@RequestBody request: ChatRequest): ChatResult {
return chatService.chat(request.message, request.sessionId)
}
// L2: SSE 串流
@GetMapping("/chat/stream", produces = [TEXT_EVENT_STREAM_VALUE])
fun streamChat(
@RequestParam message: String,
@RequestParam sessionId: String
): Flux<String> {
return chatService.streamChat(message, sessionId)
}
// L3: 結構化分析
@PostMapping("/analyze")
fun analyze(@RequestBody request: AnalyzeRequest): String {
return chatService.analyzeText(request.text)
}
// L7: 向量搜尋
@PostMapping("/search")
fun search(@RequestBody request: SearchRequest): List<SearchResult> {
return chatService.searchKnowledgeBase(request.query)
}
// L8: 圖片生成
@PostMapping("/image")
fun generateImage(@RequestBody request: ImageRequest): ImageResult {
return ImageResult(chatService.generateImage(request.description))
}
// L9: 文件上傳
@PostMapping("/documents/upload")
fun upload(@RequestParam file: MultipartFile): UploadResult {
val chunks = chatService.uploadDocument(file)
return UploadResult(file.originalFilename ?: "", chunks)
}
}
Step 6:Notebook → Spring Boot 對照表
每個 Lesson 的程式碼如何對應到 Spring Boot 架構:
| Lesson | Notebook 寫法 | Spring Boot 對應 |
|---|---|---|
| L1 ChatClient | 直接建立 ChatClient.builder() | ChatService 中的 @Bean 或建構子初始化 |
| L2 Streaming | .stream().content().doOnNext{print(it)} | Controller 回傳 Flux<String> + SSE |
| L3 Prompt 模板 | 字串或 PromptTemplate | resources/prompts/system.st + @Value |
| L4 Tool Calling | 內聯 class + .tools() | DateTimeTools.kt + .defaultTools() |
| L5 ChatMemory | 直接建立 Memory | ChatService 中初始化 + conversationId |
| L6-7 RAG | QuestionAnswerAdvisor | ChatService.chat() 手動 RAG 含來源追蹤 |
| L8 多模態 | ImagePrompt + SpeechPrompt | ChatService.generateImage() + API 端點 |
| L9 文件處理 | TokenTextSplitter | ChatService.uploadDocument() + MultipartFile |
Step 7:PGVector 向量資料庫
用 Docker Compose 一鍵啟動 PGVector[4]:
# docker-compose.yml
services:
pgvector:
image: pgvector/pgvector:pg16
ports:
- "5433:5432"
environment:
POSTGRES_DB: spring_ai
POSTGRES_USER: spring_ai
POSTGRES_PASSWORD: spring_ai_password
volumes:
- pgvector_data:/var/lib/postgresql/data
volumes:
pgvector_data:
# 啟動資料庫
docker compose up -d
# 以 PGVector 模式啟動 Spring Boot
./gradlew bootRun --args='--spring.profiles.active=pgvector'
PGVector 的配置[2]使用 HNSW 索引(高效能近似最近鄰搜尋):
# application-pgvector.yml
spring:
ai:
vectorstore:
pgvector:
index-type: HNSW # 高效能索引
distance-type: COSINE_DISTANCE # 餘弦距離
dimensions: 1536 # text-embedding-3-small 的維度
Step 8:Vue 3 前端聊天介面
專案包含完整的 Vue 3 + Vite 前端,提供五種互動模式:
| 模式 | 對應 Lesson | 功能 |
|---|---|---|
| Chat | L1+L4+L5+L6 | RAG 問答 + 記憶 + 工具,顯示參考來源 |
| Stream | L2+L5 | SSE 即時串流,打字機效果 |
| Search | L7 | 直接向量搜尋,顯示相似度分數 |
| Analyze | L3 | 結構化分析(情感、關鍵字、摘要) |
| Image | L8 | DALL-E 3 圖片生成 |
前端透過 EventSource API 接收 SSE 串流(Lesson 2 學過的技術),並支援文件上傳與知識庫瀏覽。
Step 9:測試與部署
IntelliJ HTTP Client 測試
### 基本聊天
POST http://localhost:8080/api/chat
Content-Type: application/json
{"message": "年假有幾天?", "sessionId": "test-001"}
### SSE 串流
GET http://localhost:8080/api/chat/stream?message=什麼是RAG&sessionId=test-001
### 文件上傳
POST http://localhost:8080/api/documents/upload
Content-Type: multipart/form-data; boundary=boundary
--boundary
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
...文件內容...
--boundary--
Docker + Cloud Run 部署
# Dockerfile(多階段建構)
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY . .
RUN ./gradlew bootJar --no-daemon
FROM eclipse-temurin:21-jre
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
GitHub Actions 自動部署到 Google Cloud Run(asia-east1 區域),環境變數透過 Secret Manager 管理。
本課重點回顧
| 概念 | 重點 |
|---|---|
| 三層架構 | Controller(API)→ Service(邏輯)→ Config(配置) |
| Spring AI BOM | 統一管理所有 Spring AI 依賴版本 |
| application.yml | API Key、模型參數、PGVector 設定 |
| Spring Profile | 切換 SimpleVectorStore ↔ PGVector 只需一個參數 |
| REST + SSE | @PostMapping 同步 + @GetMapping(SSE) 串流 |
| 來源追蹤 | 手動 RAG 保留 metadata.source 資訊 |
| Docker | 多階段建構,JRE 21 運行 |
下一步:Lesson 11 — 企業級 RAG 系統
Spring Boot 專案架構已就緒。下一堂課將加入企業級功能:
- 權限管控 — 不同部門只能查看授權的文件
- 多格式文件入庫 — PDF、Word、Excel 自動匯入
- Function Calling 整合 — AI 直接查詢 ERP/CRM 系統


