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.ymlapplication-pgvector.yml,Service 層程式碼完全不變
  • 專案包含 Vue 3 前端Docker + Cloud Run 部署配置,從開發到上線一步到位

為什麼要遷移到 Spring Boot?

Kotlin Notebook 非常適合學習和原型開發,但正式上線需要 Spring Boot[1]

面向Kotlin NotebookSpring Boot
適合場景學習、實驗、原型正式開發、生產部署
API 端點無法提供 HTTP APIREST Controller + SSE
依賴管理@file:DependsOnGradle/Maven(版本管理更完善)
設定管理硬編碼或 .envapplication.yml + Spring Profile
資料庫SimpleVectorStore(記憶體)PGVector / Milvus(持久化)
部署無法部署Docker → Cloud Run / K8s
前端整合無法提供 UIVue/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 架構:

LessonNotebook 寫法Spring Boot 對應
L1 ChatClient直接建立 ChatClient.builder()ChatService 中的 @Bean 或建構子初始化
L2 Streaming.stream().content().doOnNext{print(it)}Controller 回傳 Flux<String> + SSE
L3 Prompt 模板字串或 PromptTemplateresources/prompts/system.st + @Value
L4 Tool Calling內聯 class + .tools()DateTimeTools.kt + .defaultTools()
L5 ChatMemory直接建立 MemoryChatService 中初始化 + conversationId
L6-7 RAGQuestionAnswerAdvisorChatService.chat() 手動 RAG 含來源追蹤
L8 多模態ImagePrompt + SpeechPromptChatService.generateImage() + API 端點
L9 文件處理TokenTextSplitterChatService.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功能
ChatL1+L4+L5+L6RAG 問答 + 記憶 + 工具,顯示參考來源
StreamL2+L5SSE 即時串流,打字機效果
SearchL7直接向量搜尋,顯示相似度分數
AnalyzeL3結構化分析(情感、關鍵字、摘要)
ImageL8DALL-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.ymlAPI 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 系統

Spring Boot 架構就緒,繼續前進!

專案架構完成,下一步升級到企業級——PGVector 持久化、權限管控、生產監控。

前往 Lesson 11:企業級 RAG 系統 →