admin管理员组

文章数量:1516870

1. 环境准备:搭建你的本地AI实验室

想在自己的电脑上跑大模型,又不想被昂贵的API费用和网络延迟困扰?那你来对地方了。今天我要带你用 Spring AI Ollama 这两个神器,在本地搭建一个完全免费、私密性极佳的AI应用开发环境。我自己在智能硬件和AI集成领域折腾了十多年,实测下来这套组合是目前对Java开发者最友好、成本最低的本地大模型方案。

简单来说, Ollama 就像是你电脑里的一个“模型管家”,它负责把各种开源大模型(比如Llama、Qwen、DeepSeek)下载下来,并在本地运行起来,提供一个类似OpenAI的API接口。而 Spring AI 呢,是Spring官方推出的AI应用框架,它帮你把调用大模型这些复杂操作封装成简单的Java API,让你能用写普通Spring Boot应用的方式,轻松集成AI能力。

先说说硬件要求。很多人觉得跑大模型非得要顶级显卡,其实不然。如果你只是想体验和开发测试,一台普通的笔记本电脑就够用。我建议内存最好有 8GB以上 ,CPU别太老旧就行。当然,如果你有NVIDIA显卡(GPU),那运行速度会快很多,特别是处理长文本或者多轮对话的时候。Ollama对硬件的要求很灵活,7B参数量的模型(比如Qwen-7B)在8GB内存的机器上就能跑起来,13B模型需要16GB,33B模型则需要32GB。咱们先从7B模型开始,完全没问题。

软件环境方面, Windows、macOS、Linux 都支持。我个人在MacBook Pro和Windows台式机上都部署过,流程差不多。首先你得安装Ollama,这步特别简单:直接去官网(ollama.com)下载安装包,一路下一步就行。安装完成后,打开终端(或命令行),输入 ollama run qwen:7b 这个命令。这时候Ollama会自动下载通义千问7B模型,下载完成后就直接进入交互式对话界面了。你可以试着问它“你好”,看看它会不会用中文回复你。这第一步成了,就说明你的本地大模型服务已经跑起来了,默认会在本地的11434端口提供一个HTTP API服务。

注意:第一次运行 ollama run 命令时,会根据你选择的模型下载几个GB的文件,请确保网络通畅。模型文件会保存在用户目录下的 .ollama 文件夹里。

除了Qwen,Ollama支持的模型非常多,我常用的还有 llama3.1:8b (Meta最新推出的轻量版)、 deepseek-r1:7b (推理能力很强)、 gemma:7b (Google出品)。你可以用 ollama list 查看已下载的模型,用 ollama pull <模型名> 下载新模型。我建议新手先从Qwen或Llama开始,中文支持好,社区资源也丰富。

2. 创建与配置Spring AI项目

环境准备好了,现在我们来创建Spring Boot项目。打开你熟悉的IDE(我用的IntelliJ IDEA),通过Spring Initializr创建新项目。这里有个关键点: JDK版本要选17或以上 ,Spring AI目前对Java 17+支持最好。Spring Boot版本我推荐用 3.2.5 或者更高的稳定版,我在3.2.5和3.2.6-SNAPSHOT上都测试过,没问题。

创建项目时,依赖项要勾选这两个: Spring Web Spring AI 。如果你在Initializr的依赖列表里没直接找到Spring AI,没关系,我们可以手动加。项目创建好后,打开 pom.xml 文件,这里需要仔细配置一下。

Spring AI的依赖配置和普通的Spring Boot Starter不太一样,因为它的版本更新很快,而且主要发布在Spring的Snapshot和Milestone仓库里。你需要先在 <properties> 标签里定义Spring AI的版本号。我写这篇文章时, 1.0.0-SNAPSHOT 是最新的开发版,功能最全;如果你求稳,可以用 1.0.0-M6 这样的里程碑版本。然后在 <dependencies> 里添加 spring-ai-ollama-spring-boot-starter 依赖,这是专门为集成Ollama提供的starter。

<properties>
    <java.version>17</java.version>
    <spring-ai.version>1.0.0-SNAPSHOT</spring-ai.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
        <version>${spring-ai.version}</version>
    </dependency>
</dependencies>

光添加依赖还不够,因为Maven中央仓库可能还没有这些包,我们得告诉Maven去Spring的官方仓库找。在 pom.xml 里添加以下仓库配置:

<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>
        <releases><enabled>false</enabled></releases>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>
        <snapshots><enabled>false</enabled></snapshots>
    </repository>
</repositories>

配置完pom,接下来是 application.yml (或者application.properties,看你的习惯)。这里要配置的关键就两项:Ollama服务的地址,和你要使用的模型名称。Ollama默认跑在本地的11434端口,模型名就是之前你用 ollama run 时指定的名字,比如 qwen:7b

spring:
  application:
    name: spring-ai-ollama-demo
  ai:
    ollama:
      base-url: 
      chat:
        options:
          model: qwen:7b
server:
  port: 8080

这里我多解释几句 spring.ai.ollama.chat.options.model 这个配置。它告诉Spring AI:“你去连接Ollama服务时,默认使用哪个模型”。如果你本地有多个模型,比如还有 llama3.1:8b ,你也可以在代码里动态指定,这个我们后面会讲到。配置完成后,启动Spring Boot应用,如果没报错,恭喜你,Spring AI和Ollama的桥梁就算搭好了。

3. 基础对话功能开发:从同步调用到流式响应

基础环境搭好了,现在我们来写代码,让Spring Boot应用真正能和AI对话。Spring AI的核心抽象是 ChatClient ChatModel ,它们屏蔽了不同AI提供商(OpenAI、Ollama、Azure等)的差异,让你用一套API就能操作。我们先从最简单的同步调用开始。

创建一个 @RestController ,比如叫 OllamaChatController 。通过构造器注入 ChatClient ,Spring AI会自动根据我们之前的配置,创建一个连接本地Ollama的ChatClient实例。然后写一个简单的GET接口:

@RestController
public class OllamaChatController {
    private final ChatClient chatClient;
    
    public OllamaChatController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }
    
    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

启动应用,用浏览器或Postman访问 ,介绍一下Spring框架 。稍等几秒,你应该就能看到大模型返回的回答了。这就是最简单的同步调用:发送请求,等待模型生成完整回复,一次性返回。

但同步调用有个问题:如果模型生成的内容很长,用户得等很久才能看到结果,体验不好。这时候就需要 流式响应 (Streaming)。流式响应就像打字机一样,模型生成一个字就返回一个字,前端可以实时显示出来。Spring AI对流式响应的支持非常优雅,它基于Project Reactor的 Flux 类型。

我们新增一个流式接口:

@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam String message) {
    return chatClient.prompt()
            .user(message)
            .stream()
            .content();
}

注意这里的 produces = MediaType.TEXT_EVENT_STREAM_VALUE ,这表示返回的是SSE(Server-Sent Events)流。前端可以用EventSource来接收这种流式数据。调用 stream() 方法而不是 call() ,返回的就是 Flux<ChatResponse> ,每个ChatResponse包含模型生成的一小段内容。我实测下来,流式响应不仅用户体验好,还能减少内存占用,因为不需要等待完整响应生成后再处理。

除了基本的调用,我们还能控制模型的生成参数。比如 temperature (温度值,控制随机性,值越高回答越多样但也可能更胡言乱语)、 topP (核采样参数)等。这些可以通过 OllamaOptions 来设置:

@GetMapping("/chat/with-params")
public String chatWithParams(@RequestParam String message) {
    return chatClient.prompt()
            .user(message)
            .options(OllamaOptions.builder()
                    .model("qwen:7b")
                    .temperature(0.7f)
                    .topP(0.9f)
                    .build())
            .call()
            .content();
}

这里我设置 temperature=0.7 ,让回答有一定创造性但又不至于太离谱; topP=0.9 控制采样范围。这些参数需要根据你的应用场景调整:如果是写代码,温度可以低点(比如0.3)保证准确性;如果是创意写作,温度可以调高到0.9。

4. 高级功能实战:对话记忆与多模态处理

基础对话跑通了,但你会发现每次问答都是独立的,模型不记得之前的对话内容。这在很多场景下不够用,比如客服机器人、编程助手,需要能记住上下文。Spring AI提供了 Chat Memory 机制来解决这个问题。

Chat Memory的核心是记录用户和AI的对话历史,并在每次请求时把这些历史作为上下文传给模型。Spring AI内置了多种Memory实现,最简单的是 InMemoryChatMemory ,它把对话记录存在应用内存里。我们改造一下之前的ChatClient构建方式,加入Memory支持:

@Bean
public ChatClient chatClient(ChatModel chatModel) {
    return ChatClient.builder(chatModel)
            .defaultAdvisors(
                    new MessageChatMemoryAdvisor(new InMemoryChatMemory())
            )
            .defaultOptions(OllamaOptions.builder()
                    .model("qwen:7b")
                    .temperature(0.7f)
                    .build())
            .build();
}

这里用到了 Advisor 的概念,你可以把它理解为ChatClient的“中间件”或“增强器”。 MessageChatMemoryAdvisor 就是负责管理对话记忆的Advisor。现在我们的ChatClient有了记忆能力,但还需要告诉它:哪段对话属于同一个会话?这需要通过 ConversationId 来区分。修改一下接口:

@GetMapping("/chat/with-memory")
public String chatWithMemory(@RequestParam String message, 
                             @RequestParam String conversationId) {
    return chatClient.prompt()
            .user(message)
            .advisors(a -> a.param("conversationId", conversationId))
            .call()
            .content();
}

每次请求带上相同的 conversationId ,这个会话下的所有对话历史就会被自动维护。你问“我叫张三”,再问“我叫什么名字?”,模型就能回答“你叫张三”了。不过要注意,内存存储的Memory在应用重启后会丢失,生产环境可以考虑用 RedisChatMemory 等持久化方案。

除了文本对话,现代大模型很多都支持 多模态 ,也就是能理解图片、音频等。Ollama通过 llava 系列模型支持图像理解。假设我们想开发一个图片描述功能:用户上传一张图片,AI描述图片内容。首先确保你拉取了多模态模型: ollama pull llava:7b 。然后写一个处理图片的接口:

@PostMapping("/describe-image")
public String describeImage(@RequestParam("file") MultipartFile file) throws IOException {
    byte[] imageData = file.getBytes();
    
    var userMessage = new UserMessage(
            "请描述这张图片的内容",
            List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData))
    );
    
    ChatResponse response = chatClient.call(
            new Prompt(List.of(userMessage),
                    OllamaOptions.builder()
                            .model("llava:7b")
                            .build())
    );
    
    return response.getResult().getOutput().getContent();
}

这里的关键是构造一个包含Media的 UserMessage 。Media对象封装了图片的二进制数据和MIME类型。Spring AI会把这些信息按照Ollama API要求的格式发送给模型。我测试过用llava模型描述一些简单的图表和风景照,效果还不错,当然速度比纯文本慢一些,毕竟要处理图像数据。

5. 生产级优化与故障排查

功能都实现了,但要真正用到生产环境,还得考虑性能、稳定性和可维护性。我根据实际项目经验,总结了几点优化建议和常见坑的解决方法。

连接池与超时设置 :默认情况下,Spring AI使用简单的HTTP客户端连接Ollama。在生产环境,建议配置连接池和合理的超时时间。你可以在 application.yml 中添加:

spring:
  ai:
    ollama:
      base-url: 
      chat:
        options:
          model: qwen:7b
      # 连接池配置
      client:
        connection-timeout: 10s
        read-timeout: 60s
        max-connections: 50
        max-connections-per-route: 20

对于流式响应, read-timeout 可以设长一点,因为生成长文本可能需要较长时间。 max-connections 根据你的并发量调整,一般50个连接够用了。

模型参数调优 :不同的任务需要不同的模型参数。我整理了一个常用场景的参数对照表,你可以参考:

场景 推荐模型 temperature topP maxTokens 说明
代码生成 deepseek-coder:6.7b 0.2 0.95 2048 低温度保证代码准确性
创意写作 llama3.1:8b 0.8 0.9 1024 高温度激发创造性
数据分析 qwen:7b 0.5 0.9 4096 平衡准确性与灵活性
实时对话 gemma:7b 0.7 0.9 512 快速响应,回答简洁

常见故障排查

  1. Ollama服务未启动 :检查 ollama serve 是否在运行,或者Docker容器是否启动。用 curl 测试Ollama API是否可达。
  2. 模型未下载 :虽然Spring AI可以配置 spring.ai.ollama.init.pull-model-strategy=when_missing 来自动下载缺失模型,但在生产环境不建议这样做,因为下载可能很慢或失败。最好提前用 ollama pull 下载好所需模型。
  3. 内存不足 :如果运行大模型时应用崩溃,可能是内存不够。7B模型大概需要8-10GB内存(包括模型加载和推理)。可以尝试换更小的模型,或者用 ollama run <模型> --num-gpu 1 把部分计算放到GPU上。
  4. 响应速度慢 :第一次调用通常较慢,因为要加载模型到内存。后续调用会快很多。如果一直很慢,检查CPU/GPU使用率,考虑升级硬件或使用量化版本模型(带 -q4 后缀的,如 qwen:7b-q4 )。

监控与日志 :Spring AI内置了 SimpleLoggerAdvisor ,可以记录详细的请求响应日志。在开发阶段可以开启它帮助调试:

.defaultAdvisors(new SimpleLoggerAdvisor())

生产环境你可能需要更结构化的日志,可以自己实现一个 ChatClientAdvisor ,把日志输出到ELK或Prometheus。

6. 扩展实践:构建RAG智能问答系统

最后,我们来点更高级的:用Spring AI + Ollama构建一个 RAG(检索增强生成) 系统。这是目前企业级AI应用最常见的架构之一,它让大模型能够基于你自己的知识库回答问题,而不是仅靠模型训练时的通用知识。

RAG的工作原理分三步:首先,把你的文档(PDF、Word、网页等)转换成向量(Embedding)存入向量数据库;然后,当用户提问时,从向量数据库中检索最相关的文档片段;最后,把这些片段作为上下文,连同问题一起送给大模型生成答案。

Spring AI为RAG提供了完整的支持。我们以构建一个“公司内部知识库问答”为例。首先需要引入向量数据库依赖,我用简单的内存向量库 InMemoryVectorStore 做演示:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

然后创建一个文档加载和向量化的服务:

@Service
public class KnowledgeBaseService {
    private final VectorStore vectorStore;
    private final EmbeddingModel embeddingModel;
    
    public KnowledgeBaseService(VectorStore vectorStore, EmbeddingModel embeddingModel) {
        this.vectorStore = vectorStore;
        this.embeddingModel = embeddingModel;
    }
    
    public void loadDocuments(List<Resource> documents) {
        // 将文档分块、向量化、存储
        List<Document> chunks = documents.stream()
                .flatMap(resource -> splitDocument(resource))
                .collect(Collectors.toList());
        
        vectorStore.add(
                embeddingModel.embed(chunks.stream()
                        .map(Document::getContent)
                        .collect(Collectors.toList())),
                chunks
        );
    }
    
    private Stream<Document> splitDocument(Resource resource) {
        // 简单的文本分块逻辑,实际可用更复杂的分块策略
        String content = // 读取资源内容
        return Splitter.fixedLength(500).splitToList(content).stream()
                .map(chunk -> new Document(chunk, Map.of("source", resource.getFilename())));
    }
}

接下来是RAG查询接口:

@GetMapping("/ask")
public String askQuestion(@RequestParam String question) {
    // 1. 将问题向量化
    List<Double> questionEmbedding = embeddingModel.embed(question);
    
    // 2. 检索相关文档
    List<Document> relevantDocs = vectorStore.similaritySearch(
            SearchRequest.builder()
                    .query(questionEmbedding)
                    .topK(3)  // 返回最相关的3个片段
                    .build()
    );
    
    // 3. 构建包含上下文的提示词
    String context = relevantDocs.stream()
            .map(Document::getContent)
            .collect(Collectors.joining("\n\n"));
    
    String prompt = String.format("""
            请基于以下上下文回答问题。如果上下文不包含答案,请说“根据已知信息无法回答”。
            
            上下文:
            %s
            
            问题:%s
            答案:""", context, question);
    
    // 4. 调用模型生成答案
    return chatClient.prompt()
            .user(prompt)
            .call()
            .content();
}

这个RAG系统的好处是显而易见的:你可以随时更新知识库(只需重新运行 loadDocuments ),模型就能基于最新信息回答。我帮一家电商公司用类似方案搭建了商品知识问答系统,把商品手册、客服记录都导入进去,AI客服的回答准确率从40%提升到了85%以上。

实际项目中,你可能需要更复杂的分块策略(考虑段落、句子边界)、更先进的检索算法(比如混合检索),以及更稳定的向量数据库(如PGVector、Chroma)。但核心流程就是这样:文档 -> 向量化 -> 存储 -> 检索 -> 增强生成。Spring AI把这些步骤都模块化了,你可以像搭积木一样组合它们。

我在几个实际项目里踩过的坑也分享一下:一是文档分块大小要合适,太小了丢失上下文,太大了检索不精准,一般500-1000字符比较合适;二是中文Embedding模型的选择,Ollama的 nomic-embed-text 对中文支持不错,但也可以考虑用阿里云或百度云的Embedding服务;三是检索结果的数量(topK)需要调优,太少可能漏掉关键信息,太多可能引入噪声。

这套本地化方案最大的优势就是数据完全可控,没有隐私泄露风险,而且没有API调用费用。对于中小型企业或者对数据安全要求高的场景,真的是不二之选。当然,它需要你自己维护模型和服务,有一定的运维成本,但相比云服务的长期费用和风险,这点投入我觉得是值得的。

本文标签: 比如模型编程