SpringAI 结合 Ollama 本地部署 Deepseek 模型实现对话机器人
Java 调用 Deepseek
本地没有安装 Ollama、Docker、Open WebUI,可先学习相关部署文档。
下载 Deepseek 模型
打开命令行窗口,拉取一下 Deepseek 模型:
ollama run deepseek-r1:7b
本地测试
运行 Open WebUI 容器,选择 Deepseek-r1 模型进行测试。
Java 调用模型
注释掉以前的 moonshot 依赖并删除相关代码,引入 ollama 依赖:
<!-- 引入 Ollama 依赖-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
修改配置类 Init:
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:
spring:
ai:
ollama:
chat:
options:
model: deepseek-r1:7b
base-url: http://localhost:11434
构建数据库
增强检索 RAG
Embedding 是一种将对象表示为数值向量的方法。使用 ollama 拉取 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
Springboot 集成 pgvector
引入依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
配置 application.yml:
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
连接数据库后执行建表语句:
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 中放置 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("code.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:
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.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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();
}
}
ChatPDF
引入依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
编写代码读取 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 调用自定义函数
部分模型(如 Deepseek)可能不支持 Function Call,建议参考 Moonshot、OpenAI 等支持情况。
创建逻辑函数:
package com.yan.springai.func;
import java.util.function.Function;
public class OaService implements Function<OaService.Request, OaService.Response> {
public Response apply(Request request) {
System.err.printf("%s is token off%n", request.who);
return new Response(10);
}
public record Request(String who) {}
public record Response(int days) {}
}
注册 Function 到 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();
}
}
多模态能力
多模态大语言模型能够理解和生成多种类型数据,包括文本、图片、音频和视频等。
Deepseek 等模型可能不支持多模态,可下载 llava 模型:
ollama run llava
在 resources 传入图片,编写 ImageAPI 类:
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();
}
}

