SpringAI Kotlin 本地调用 Ollama

这里写目录标题

  • [1 Ollama 软件下载](#1 Ollama 软件下载)
  • [2 Ollama 模型下载](#2 Ollama 模型下载)
    • [2.1 Ollama下载脚本](#2.1 Ollama下载脚本)
    • [2.3 Ollama模型列表](#2.3 Ollama模型列表)
    • [2.3 Ollama模型运行](#2.3 Ollama模型运行)
  • [3 Kotlin SpringAI 配置](#3 Kotlin SpringAI 配置)
    • [1 build.gradle.kts](#1 build.gradle.kts)
    • [2 settings.gradle.kts](#2 settings.gradle.kts)
    • [3 application.yml](#3 application.yml)
  • [4 Kotlin SpringAI 代码](#4 Kotlin SpringAI 代码)
    • [1 LogUtil.kt](#1 LogUtil.kt)
    • [2 MemConf.kt](#2 MemConf.kt)
    • [3 OllamaController.kt](#3 OllamaController.kt)
    • [4 Application.kt](#4 Application.kt)
  • [5 Kotlin SpringAI 结果](#5 Kotlin SpringAI 结果)

LangChain4j VS Spring AI

维度 LangChain4j Spring AI
定位 专注于 LLM 应用开发的工具链,类似 Java 版 LangChain Spring 生态的官方 AI 扩展,提供标准化集成
生态依赖 独立框架,可与任何 Java 框架集成(包括 Spring) 深度依赖 Spring 生态(如 Spring Boot、Spring Cloud)
核心能力 提供完整的 LLM 应用组件(链、记忆、工具等) 聚焦于模型接口标准化、数据处理、集成能力
模型支持 支持主流闭源/开源模型,配置灵活 支持主流模型,通过统一接口抽象,符合 Spring 风格
易用性 需手动组合组件,灵活性高,但学习成本略高 遵循 Spring 约定优于配置,开箱即用,适合 Spring 开发者
企业级特性 需自行集成安全、监控等特性 天然支持 Spring 生态的企业级特性(如安全、审计、监控)
社区与更新 社区活跃,更新频率中等 官方背书,更新频繁,与 Spring 版本同步演进
适用场景 复杂 LLM 应用(如多链协作、自定义记忆策略) 快速集成 LLM 到 Spring 应用,追求标准化和稳定性

选择建议

  • 若项目已基于 Spring 生态(如 Spring Boot),且希望快速集成 LLM 并利用 Spring 的企业级特性,优先选择 Spring AI
  • 若需要更灵活的 LLM 应用构建能力,或项目不依赖 Spring 框架,LangChain4j 是更合适的选择。
  • 若需实现复杂的 RAG、多轮对话记忆或工具调用逻辑,LangChain4j 的组件化设计可能更具优势。

1 Ollama 软件下载

Ollama

2 Ollama 模型下载

2.1 Ollama下载脚本

Win11 下载脚本

复制内容到ollama_download.ps1文件中

powershell 复制代码
# 定义模型名称避免硬编码
$modelName = "gemma3:27b"

# 设置重试参数
$maxRetries = 50
$retryInterval = 3  # 秒
$downloadTimeout = 80  # 秒

for ($retry = 1; $retry -le $maxRetries; $retry++) {
    # 精确检查模型是否存在
    $modelExists = ollama list | Where-Object { $_ -match "\b$modelName\b" }
    
    if ($modelExists) {
        Write-Host "[$(Get-Date)] model is already downloaded!"
        exit 0
    }

    Write-Host "[$(Get-Date)] start $retry times downloading..."
    
    # 启动进程并显示实时输出
    $process = Start-Process -FilePath "ollama" `
        -ArgumentList "run", $modelName `
        -PassThru `
        -NoNewWindow

    # 等待下载完成或超时
    try {
        $process | Wait-Process -Timeout $downloadTimeout -ErrorAction Stop
    } catch {
        # 超时处理
        Write-Host "download timeout, safe terminate process..."
        $process | Stop-Process -Force
    }

    if (-not $process.HasExited) {
        $process | Stop-Process -Force
        Write-Host "process terminated due to timeout."
    } else {
        # 检查退出代码
        if ($process.ExitCode -eq 0) {
            Write-Host "download success!"
            exit 0
        }
        Write-Host "download failed, exit code: $($process.ExitCode)"
    }

    Start-Sleep -Seconds $retryInterval
}

Write-Host "exceeded maximum retries ($maxRetries), download failed."
exit 1
powershell 复制代码
PS C:\Users\xuhya\Desktop> .\ollama_download.ps1
[10/09/2025 21:42:20] start 1 times downloading...
pulling manifest
pulling e796792eba26:  98% ▕████████████████████████████████████████████████████████  ▏  17 GB/ 17 GB  1.7 MB/s   3m20sdownload timeout, safe terminate process...
download failed, exit code: -1
[10/09/2025 21:43:43] start 2 times downloading...
pulling manifest
pulling e796792eba26: 100% ▕██████████████████████████████████████████████████████████▏  17 GB
pulling e0a42594d802: 100% ▕██████████████████████████████████████████████████████████▏  358 B
pulling dd084c7d92a3: 100% ▕██████████████████████████████████████████████████████████▏ 8.4 KB
pulling 3116c5225075: 100% ▕██████████████████████████████████████████████████████████▏   77 B
pulling f838f048d368: 100% ▕██████████████████████████████████████████████████████████▏  490 B
verifying sha256 digest ⠸ download timeout, safe terminate process...
download failed, exit code: -1
[10/09/2025 21:45:06] start 3 times downloading...
pulling manifest
pulling e796792eba26: 100% ▕██████████████████████████████████████████████████████████▏  17 GB
pulling e0a42594d802: 100% ▕██████████████████████████████████████████████████████████▏  358 B
pulling dd084c7d92a3: 100% ▕██████████████████████████████████████████████████████████▏ 8.4 KB
pulling 3116c5225075: 100% ▕██████████████████████████████████████████████████████████▏   77 B
pulling f838f048d368: 100% ▕██████████████████████████████████████████████████████████▏  490 B
verifying sha256 digest
writing manifest
success
⠏ download timeout, safe terminate process...
download failed, exit code: -1
[10/09/2025 21:46:29] model is already downloaded!
PS C:\Users\xuhya\Desktop>

2.3 Ollama模型列表

powershell 复制代码
PS C:\Users\hyacinth\Desktop> ollama list
NAME                      ID              SIZE      MODIFIED
llava-llama3:latest       44c161b1f465    5.5 GB    4 days ago
qwen3:latest              500a1f067a9f    5.2 GB    4 days ago
llama3.2-vision:latest    6f2f9757ae97    7.8 GB    4 days ago
deepseek-r1:latest        6995872bfe4c    5.2 GB    4 days ago
deepseek-coder:6.7b       ce298d984115    3.8 GB    4 days ago
gemma3:4b                 a2af6cc3eb7f    3.3 GB    4 days ago
llama3.2:latest           a80c4f17acd5    2.0 GB    4 days ago
PS C:\Users\hyacinth\Desktop>

2.3 Ollama模型运行

powershell 复制代码
PS C:\Users\hyacinth\Desktop> ollama ps
NAME                      ID              SIZE     PROCESSOR    CONTEXT    UNTIL
llama3.2-vision:latest    6f2f9757ae97    34 GB    100% CPU     262144     2 minutes from now
PS C:\Users\hyacinth\Desktop>

3 Kotlin SpringAI 配置

1 build.gradle.kts

kts 复制代码
plugins {
    // Kotlin JVM 插件,用于支持 Kotlin 语言编译到 JVM 字节码,版本 1.9.25。
	kotlin("jvm") version "1.9.25"
    // Kotlin 官方提供的 Spring 插件,用于增强 Kotlin 与 Spring 的兼容性(例如自动开启 open 类支持,因为 Spring AOP 需要类可继承),版本与 Kotlin 一致。
	kotlin("plugin.spring") version "1.9.25"
    // Spring Boot 插件,提供 Spring Boot 特有的构建功能(如打包可执行 JAR、自动配置依赖版本等),版本 3.5.6。
	id("org.springframework.boot") version "3.5.6"
    // Spring 依赖管理插件,用于统一管理项目依赖的版本(配合 dependencyManagement 块使用),版本 1.1.7。
	id("io.spring.dependency-management") version "1.1.7"
}

group = "com.xu"
version = "1.0.0"
description = "Kotlin AI 示例项目,集成 Spring AI 和 Ollama 大模型"

// 定义项目使用的 Java 版本为 25,Gradle 会自动寻找或下载对应版本的 JDK 进行编译、运行。
//java {
//	toolchain {
//        // toolchain 是 Gradle 推荐的配置方式,比传统的 sourceCompatibility 更灵活,支持自动管理 JDK 版本。
//		languageVersion = JavaLanguageVersion.of(25)
//	}
//}


configurations {
    // 这里配置 compileOnly 继承 annotationProcessor 配置,确保注解处理器(如 Lombok)在编译时能被正确识别。
	compileOnly {
		extendsFrom(configurations.annotationProcessor.get())
	}
}

repositories {
    // 声明项目依赖的仓库为 Maven Central(中央仓库),Gradle 会从这里下载所需的依赖包。
	mavenCentral()
}

// 定义一个名为 springAiVersion 的全局属性,值为 1.0.3,用于统一管理 Spring AI 相关依赖的版本(后续可通过 ${property("springAiVersion")} 引用)。
extra["springAiVersion"] = "1.0.3"

dependencies {
    // Spring Boot Web  Starter(包含Spring MVC、Tomcat等Web相关依赖)
    implementation("org.springframework.boot:spring-boot-starter-web")
    // Jackson Kotlin模块(支持Kotlin数据类与JSON的序列化/反序列化)
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    // Kotlin反射库(Spring等框架需要反射支持)
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    // Spring AI Ollama Starter(集成Ollama大模型的依赖)
    implementation("org.springframework.ai:spring-ai-starter-model-ollama")
    // Lombok(编译时生成getter/setter等代码,仅编译时需要)
    compileOnly("org.projectlombok:lombok")
    // Spring Boot开发工具(热部署等开发期功能,仅开发环境需要)
    developmentOnly("org.springframework.boot:spring-boot-devtools")
    // Lombok注解处理器(编译时处理Lombok注解)
    annotationProcessor("org.projectlombok:lombok")
    // Spring Boot测试 Starter(包含JUnit、Mockito等测试依赖)
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    // Kotlin测试与JUnit5集成
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
    // JUnit平台启动器(支持IDE运行JUnit5测试)
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

// 通过导入 Maven BOM(Bill of Materials) 统一管理 Spring AI 相关依赖的版本。
dependencyManagement {
	imports {
		mavenBom("org.springframework.ai:spring-ai-bom:${property("springAiVersion")}")
	}
}

// 配置 Kotlin 编译器选项:
kotlin {
    // -Xjsr305=strict:启用 JSR-305 注解(如 @Nullable、@Nonnull)的严格检查,增强代码的空安全。
	compilerOptions {
		freeCompilerArgs.addAll("-Xjsr305=strict")
	}
}

// 单元测试
tasks.withType<Test> {
	useJUnitPlatform()
}

2 settings.gradle.kts

kts 复制代码
rootProject.name = "kotlin-ai"

3 application.yml

yml 复制代码
spring:
  application:
    name: spring-ai
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        model: llama3.2-vision:latest
      init:
        timeout: 50m
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB
      max-request-size: 10MB
      file-size-threshold: 10MB

4 Kotlin SpringAI 代码

1 LogUtil.kt

java 复制代码
package com.xu.conf

import org.slf4j.Logger
import org.slf4j.LoggerFactory

@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LogUtil(){
    companion object{
        val <reified T> T.log: Logger
            inline get() = LoggerFactory.getLogger(T::class.java)
    }
}

2 MemConf.kt

java 复制代码
package com.xu.conf

import com.xu.conf.LogUtil.Companion.log
import org.springframework.ai.chat.memory.ChatMemory
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository
import org.springframework.ai.chat.memory.MessageWindowChatMemory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@LogUtil
@Configuration
class MemConf {

    @Bean
    fun chatMemory(): ChatMemory {
        log.info("初始化聊天记忆成功!!!")
        return MessageWindowChatMemory.builder()
            .chatMemoryRepository(InMemoryChatMemoryRepository())
            .maxMessages(20)
            .build()
    }

}

3 OllamaController.kt

java 复制代码
package com.xu.controller

import com.xu.conf.LogUtil
import com.xu.conf.LogUtil.Companion.log
import org.springframework.ai.chat.memory.ChatMemory
import org.springframework.ai.chat.messages.UserMessage
import org.springframework.ai.chat.prompt.Prompt
import org.springframework.ai.content.Media
import org.springframework.ai.ollama.OllamaChatModel
import org.springframework.http.MediaType
import org.springframework.util.MimeType
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import reactor.core.publisher.Flux

@LogUtil
@RestController
@RequestMapping("/ollama")
class OllamaController(val chatMemory: ChatMemory, val model: OllamaChatModel) {

    /**
     * 聊天
     *
     * @param content 聊天内容
     * @return 聊天结果
     */
    @RequestMapping("/common/chat")
    fun chat(content: String?): Any? {
        return model.call(content)
    }

    /**
     * 聊天记忆
     *
     * @param content 聊天内容
     * @return 聊天结果
     */
    @RequestMapping("/common/memory")
    fun memory(content: String, msgId: String): Any? {
        val message = UserMessage.builder().text(content).build()
        chatMemory.add(msgId, message)
        val response = model.call(Prompt(message))
        chatMemory.add(msgId, response.result.output)
        return response.result.output.text
    }

    /**
     * 聊天图片识别
     *
     * @param content 聊天内容
     * @return 聊天结果
     */
    @RequestMapping("/common/image")
    fun image(@RequestParam("content") content: String, @RequestParam("file") file: MultipartFile): Any? {
        val t1 = System.currentTimeMillis()
        log.info("聊天图片识别开始...")
        if (file.isEmpty) {
            return model.call(content)
        }
        val media = Media(MimeType("image", "png"), file.resource)
        val prompt = Prompt(UserMessage.builder().media(media).text(content).build())
        val response = model.call(prompt)
        val t2 = System.currentTimeMillis()
        log.info("聊天图片识别结束...${(t2 - t1) / 1000.0}秒")
        return response.result.output.text
    }

    /**
     * 聊天流式
     *
     * @param content 聊天内容
     * @return 聊天结果
     */
    @RequestMapping(value = ["/stream/chat"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
    fun stream(content: String?): Flux<String?>? {
        log.info("开始流式聊天...")
        return model.stream(content)
    }

}

4 Application.kt

kotlin 复制代码
package com.xu

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class KotlinAiApplication

fun main(args: Array<String>) {
    runApplication<KotlinAiApplication>(*args)
    println("当前 Java 版本: " + System.getProperty("java.version"))
    println("当前 Kotlin 版本: " + KotlinVersion.CURRENT)
}

5 Kotlin SpringAI 结果

java 复制代码
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.6)

2025-10-21T15:29:48.706+08:00  INFO 26288 --- [spring-ai] [  restartedMain] com.xu.ApplicationKt                     : Starting ApplicationKt using Java 25 with PID 26288 (D:\SourceCode\KotlinLearn\kotlin-ai\build\classes\kotlin\main started by hyacinth in D:\SourceCode\KotlinLearn\kotlin-ai)
2025-10-21T15:29:48.708+08:00  INFO 26288 --- [spring-ai] [  restartedMain] com.xu.ApplicationKt                     : No active profile set, falling back to 1 default profile: "default"
2025-10-21T15:29:48.738+08:00  INFO 26288 --- [spring-ai] [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2025-10-21T15:29:48.738+08:00  INFO 26288 --- [spring-ai] [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2025-10-21T15:29:49.487+08:00  INFO 26288 --- [spring-ai] [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2025-10-21T15:29:49.501+08:00  INFO 26288 --- [spring-ai] [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2025-10-21T15:29:49.501+08:00  INFO 26288 --- [spring-ai] [  restartedMain] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.46]
2025-10-21T15:29:49.537+08:00  INFO 26288 --- [spring-ai] [  restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2025-10-21T15:29:49.537+08:00  INFO 26288 --- [spring-ai] [  restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 799 ms
2025-10-21T15:29:49.653+08:00  INFO 26288 --- [spring-ai] [  restartedMain] com.xu.conf.MemConf                      : 初始化聊天记忆成功!!!
2025-10-21T15:29:50.434+08:00  INFO 26288 --- [spring-ai] [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2025-10-21T15:29:50.452+08:00  INFO 26288 --- [spring-ai] [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-10-21T15:29:51.464+08:00  INFO 26288 --- [spring-ai] [  restartedMain] com.xu.ApplicationKt                     : Started ApplicationKt in 3.459 seconds (process running for 4.469)
当前 Java 版本: 25
当前 Kotlin 版本: 2.1.20
2025-10-21T15:29:52.290+08:00  INFO 26288 --- [spring-ai] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2025-10-21T15:29:52.290+08:00  INFO 26288 --- [spring-ai] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2025-10-21T15:29:52.291+08:00  INFO 26288 --- [spring-ai] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2025-10-21T15:29:52.368+08:00  INFO 26288 --- [spring-ai] [nio-8080-exec-1] com.xu.controller.OllamaController       : 聊天图片识别开始...
2025-10-21T15:31:36.664+08:00  INFO 26288 --- [spring-ai] [nio-8080-exec-1] com.xu.controller.OllamaController       : 聊天图片识别结束...104.295秒
相关推荐
Kapaseker1 天前
一杯美式搞定 Kotlin 空安全
android·kotlin
FunnySaltyFish2 天前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
Kapaseker2 天前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
Kapaseker3 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z5 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton5 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
lhDream6 天前
Kotlin 开发者必看!JetBrains 开源 LLM 框架 Koog 快速上手指南(含示例)
kotlin
RdoZam6 天前
Android-封装基类Activity\Fragment,从0到1记录
android·kotlin
Kapaseker6 天前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
糖猫猫cc7 天前
Kite:两种方式实现动态表名
java·kotlin·orm·kite