SpringAI 结合 Ollama 本地部署 Deepseek 实现对话机器人
在上一节中我们完成了 Ollama、Open WebUI 和 Docker 的本地可视化部署。本节将深入实战,演示如何在 Java 项目中通过 SpringAI 调用本地运行的 Deepseek 模型,并扩展 RAG 检索、PDF 解析及多模态功能。
Java 调用 Deepseek
首先确保本地已安装 Ollama 并拉取模型。在命令行执行以下命令:
ollama run deepseek-r1:7b
本地测试
启动 Docker Desktop 运行 Open WebUI,选择 deepseek-r1 模型进行基础对话测试,确认服务正常响应。
集成 SpringAI
依赖配置 移除原有的 Moonshot 相关依赖,引入 Ollama Starter:
<!-- 引入 Ollama 依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
配置类编写 定义 ChatClient 和 ChatMemory Bean,设置系统提示词以模拟特定角色(如特朗普语气),并启用内存对话记忆。
package com.yan.springai;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class Init {
final OllamaChatModel model2;
@Bean
public ChatClient chatClient(ChatMemory chatMemory) {
return ChatClient.builder(model2)
.defaultSystem("假如你是特朗普,接下来的对话你必须以特朗普的语气来进行?")
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build();
}
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
}
配置文件
在 application.yml 中指定模型名称和 Ollama 地址:
spring:
ai:
ollama:
chat:
options:
model: deepseek-r1:7b
base-url: http://localhost:11434
运行应用后,控制台应能输出符合设定语气的回复。
构建数据库与 RAG
增强检索 (RAG)
Embedding 是将文本转化为数值向量的过程,用于捕捉语义相似性。我们需要一个 Embedding 模型,这里选用轻量级的 all-minilm:
ollama pull all-minilm
向量数据库 (pgvector)
使用 PostgreSQL 配合 pgvector 扩展存储向量数据。启动容器时注意端口映射:
docker run -d --name pgvector -p 5433:5432 \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
pgvector/pgvector:pg16
Spring Boot 集成 引入 pgvector 依赖并配置连接参数:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
spring:
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 384
batching-strategy: TOKEN_COUNT
max-document-batch-size: 1000
ollama:
chat:
options:
model: deepseek-r1:7b
embedding:
enabled: true
model: all-minilm
base-url: http://localhost:11434
datasource:
url: jdbc:postgresql://localhost:5433/springai
username: postgres
password: postgres
初始化表结构 连接数据库后执行 SQL 脚本创建向量表:
create extension if not exists vector;
create extension if not exists hstore;
create extension if not exists "uuid-ossp";
create TABLE if not exists vector_store(
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
content text,
metadata json,
embedding vector(384)
);
create index on vector_store using HNSW(embedding vector_cosine_ops);
数据导入与检索
在 resources 目录下放置待处理的文本文件(如 ncode.txt)。创建 VectorAPI 控制器读取文件并按句号分片写入向量库:
package com.yan.springai.vector;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
@RestController
@RequiredArgsConstructor
public class VectorAPI {
final VectorStore store;
@GetMapping("/vec/write")
public String write() throws IOException {
StringBuffer text = new StringBuffer();
ClassLoader classLoader = getClass().getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("ncode.txt");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
text.append(line);
}
}
// 按句号分片并写入
store.write(Arrays.stream(text.toString().split("。"))
.map(Document::new).toList());
return "success";
}
}
调用接口后,查看数据库确认数据已入库。此时若直接提问,模型仅凭自身知识回答。为了利用外部知识库,需修改 Init 类,加入 QuestionAnswerAdvisor:
// ... imports ...
@Configuration
@RequiredArgsConstructor
public class Init {
final OllamaChatModel model2;
final VectorStore vectorStore;
@Bean
public ChatClient chatClient(ChatMemory chatMemory) {
return ChatClient.builder(model2)
.defaultSystem("假如你是特朗普,接下来的对话你必须以特朗普的语气来进行?")
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory),
new QuestionAnswerAdvisor(vectorStore)
)
.build();
}
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
}
再次测试,模型现在能基于向量库中的文档内容回答问题了。
PDF 文档处理
SpringAI 支持直接读取 PDF 文件并转为向量。引入依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
编写 Controller 读取 baogao.pdf 并写入向量库:
package com.yan.springai.Pdf;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class Pdf {
final VectorStore store;
@GetMapping("/pdf/read")
public String getDocsFromPdf() {
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(
"classpath:/baogao.pdf",
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageExtractedTextFormatter(
ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0)
.build()
)
.withPagesPerDocument(1)
.build()
);
store.write(pdfReader.read());
return "success";
}
}
Function Call 自定义函数
部分模型支持 Function Calling,允许 AI 调用外部逻辑。注意 Deepseek 在此功能上支持有限,Moonshot 或 OpenAI 效果更佳。
定义服务
package com.yan.springai.func;
import java.util.function.Function;
public class OaService implements Function<OaService.Rquest, OaService.Response> {
public Response apply(Rquest rquest) {
System.err.printf("%s is token off%n", rquest.who);
return new Response(10);
}
public record Rquest(String who) { }
public record Response(int days) { }
}
注册到 Spring 容器
package com.yan.springai.func;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FunctionRegistry {
@Bean
public FunctionCallback askForLeaveCallBack() {
return FunctionCallback.builder()
.function("askForLeave", new OaService())
.description("当有人请假时,返回请假天数")
.build();
}
}
调用示例
package com.yan.springai.func;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class FuncAPI {
final ChatClient chatClient;
@GetMapping("/ai/func")
public String funcCall(@RequestParam(value = "message") String message) {
return chatClient.prompt(message)
.functions("askForLeave")
.call().content();
}
}
多模态能力
多模态模型(如 LLaVA)能理解图片内容。Deepseek 原生不支持,需切换模型。
拉取模型
ollama run llava
代码实现
将图片放入 resources 目录,编写接口接收请求:
package com.yan.springai.model;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Media;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaModel;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class ImageAPI {
final OllamaChatModel model;
@GetMapping("/ai/chatWithPic")
public String chatWithPic() {
ClassPathResource imageData = new ClassPathResource("/cat.png");
Message userMessage = new UserMessage(
"请用中文描述一下这张图片是什么东西?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData))
);
return model.call(new Prompt(
List.of(userMessage),
ChatOptions.builder()
.model(OllamaModel.LLAVA.getName())
.build()
)).getResult().getOutput().getText();
}
}
至此,我们完成了从文本对话、知识库检索、文档解析到图像识别的全流程搭建。


