跳到主要内容
Spring AI 本地接入 DeepSeek、RAG 和多模态实践 | 极客日志
Java AI java
Spring AI 本地接入 DeepSeek、RAG 和多模态实践 这篇实践记录了用 Spring AI 接入本地 Ollama 的完整链路:先拉起 deepseek-r1:7b 做对话,再接入 all-minilm 和 pgvector,把文本写入向量库并通过 QuestionAnswerAdvisor 做 RAG;随后补了 PDF 入库、Function Call 自定义函数调用,以及用 llava 做图片理解的多模态示例。整体结论很直接:Spring AI 的这些能力可以串在同一套代码里,但模型支持面不一样,像 function call 和多模态需要换到对应模型。
暗影行者 发布于 2026/6/30 更新于 2026/7/12 9 浏览Java 调用 DeepSeek
如果本地已经装好 Ollama、Docker 和 Open WebUI,后面的步骤会顺很多;没装的话,先把环境补齐,再往下做。
下载 DeepSeek 模型
先在命令行里拉一下模型:
ollama run deepseek-r1:7b
本地先跑通一次
打开 Docker Desktop,启动 Open WebUI。
在界面里选 DeepSeek-R1 模型,先随便问一句,确认模型和 UI 是通的。
Java 里接入模型
先把之前的 moonshot 相关依赖注释掉,或者直接删掉。不然启动时大概率会因为 Bean 冲突或者配置残留报错。
然后引入 Ollama 依赖:
<dependency >
<groupId > org.springframework.ai</groupId >
<artifactId > spring-ai-ollama-spring-boot-starter</artifactId >
</dependency >
配置类也要换成 Ollama 版本:
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;
org.springframework.ai.ollama.OllamaChatModel;
org.springframework.context.annotation.Bean;
org.springframework.context.annotation.Configuration;
{
OllamaChatModel model2;
ChatClient {
ChatClient.builder(model2)
.defaultSystem( )
.defaultAdvisors( (chatMemory))
.build();
}
ChatMemory {
();
}
}
import
import
import
@Configuration
@RequiredArgsConstructor
public
class
Init
final
@Bean
public
chatClient
(ChatMemory chatMemory)
return
"假如你是特朗普,接下来的对话你必须以特朗普的语气来进行?"
new
MessageChatMemoryAdvisor
@Bean
public
chatMemory
()
return
new
InMemoryChatMemory
spring:
ai:
ollama:
chat:
options:
model: deepseek-r1:7b
base-url: http://localhost:11434
启动项目后看一下输出,能正常返回就说明模型已经接上了。
构建数据库和 RAG
增强检索 Embedding 就是把文本、物品、用户这些对象映射成向量。它的用处不算玄乎,核心就是让模型能按语义去找内容,而不是只做字符串匹配。
这里先拉一个小一点的 embedding 模型,适合练手,不太适合直接上企业项目:
向量数据库 这里用的是 pgvector,也可以换成 Redis。
pgvector 是 PostgreSQL 的向量扩展,能在关系型数据库里做语义搜索。对我来说,它的好处是少引一套系统,坏处是规模上来后要更认真地看索引和性能。
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
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 文件。
接着新建 vector 包和 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" ;
}
}
运行后,数据会被写入向量库。控制台和数据库里能看到记录,就说明这步没问题。
这时候直接问模型,回答通常还是偏'普通聊天模型'的风格,没真正用上知识库。
RAG 也是通过 advisor 拼进去的,所以要改一下 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.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 ();
}
}
再测一次,RAG 生效后,回答会明显更贴近导入的内容。
chatpdf
<dependency >
<groupId > org.springframework.ai</groupId >
<artifactId > spring-ai-pdf-document-reader</artifactId >
</dependency >
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 这类模型目前还不支持这个能力,Moonshot、OpenAI、Gemini 之类可以直接用。
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) { }
}
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();
}
}
调用的时候直接在 ChatClient 里挂上函数名:
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、Moonshot 这类模型不支持这个能力,要试的话得换成 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();
}
}
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online