Spring AI + Ollama
1. Ollama
- 定位:本地/容器化的 大模型推理服务引擎
- 作用:负责加载模型、执行推理、返回结果
- 支持的模型:Qwen、DeepSeek、nomic-embed-text(embedding 模型)等
- 交互方式:REST API(默认
http://localhost:11434
)
ollama run qwen2.5:7b-instruct
2. Spring AI
- 定位:Spring 官方推出的 AI 接入框架
- 作用:统一封装对各种 AI 服务的调用逻辑,让开发者更容易接入(OpenAI、Ollama 等)
- 关键点
OllamaChatModel
:封装了调用 Ollama 的对话接口OllamaOptions
:指定模型、参数(温度、上下文大小等)PgVectorStore
:对接向量数据库(Spring AI 提供的默认向量数据库适配器。)TokenTextSplitter
:默认的文本切分器 (Spring AI 内置逻辑)
3.pgVectorStore
pgVectorStore
是 Spring AI 提供的向量存储接口实现,底层基于 PostgreSQL + pgvector 插件。它在项目中的作用:
存储向量和元数据:
pgVectorStore.accept(splits);
这行代码会:
- 将
splits
中每个Document
的text
→ 调用 embedding 模型 → 生成向量 - 将
Document.metadata
→ 存入 PostgreSQL 的vector_store
表(以 JSONB 格式存储) - 将生成的向量(embedding) 存入
vector_store
表中的embedding
列 - 将
Document.text
(原始文本内容) 存入vector_store
表中的content
列
检索时自动生成 SQL:
List<Document> documents = pgVectorStore.similaritySearch(request);
在执行检索时,pgVectorStore
会自动生成 SQL 查询,结合 向量相似度 和 filterExpression
(如 metadata->>'knowledge' = 'xxx'
)进行查询,返回最相关的文档片段。
Docker如何开启GPU加速大模型推理
巨坑!
默认是CPU跑大模型,deepseek1.5b勉强能跑动,但速度很慢,一换qwen2.5:7b模型瞬间就跑不动了,这才发现一直在用CPU跑!!
如何排查?
1.查看ollama日志:
load_backend: loaded CPU backend from /usr/lib/ollama/libggml-cpu-alderlake.so
2.本地cmd命令行输入,查看显存占用率。
nvidia-smi
| 0 NVIDIA GeForce RTX 4060 ... WDDM | 00000000:01:00.0 On | N/A |
| N/A 58C P0 24W / 110W | 2261MiB / 8188MiB | 6% Default |
设备状态:
- RTX 4060显卡驱动(576.02)和CUDA 12.9环境正常
- 当前GPU利用率仅6%(
GPU-Util
列) - 显存占用2261MB/8188MB(约27.6%)
可见模型推理的时候压根没有用GPU!!!
解决
参考博客:1-3 Windows Docker Desktop安装与设置docker实现容器GPU加速_windows docker gpu-CSDN博客
1)配置WSL2,打开 PowerShell(以管理员身份运行),执行以下命令:
wsl --install -d Ubuntu # 安装 Linux 发行版
wsl --set-default-version 2 # 设为默认版本
wsl --update # 更新内核
- 重启计算机以使更改生效。
2)检查wsl是否安装成功
C:\Users\zhangsan>wsl --list --verbose
NAME STATE VERSION
* Ubuntu Running 2
docker-desktop Running 2
这个命令会列出所有已安装的Linux发行版及其状态。如果看到列出的Linux发行版,说明WSL已成功安装。
3)安装docker desktop默认配置:

4)保险起见也启用一下开启 Windows 的 Hyper-V 虚拟化技术:
搜索“启用或关闭Windows功能”,勾选:Hyper-V 虚拟化技术。

5) 命令提示符输入nvidia-smi
C:\Users\zhangsan>nvidia-smi
Tue Aug 19 21:18:11 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 576.02 Driver Version: 576.02 CUDA Version: 12.9 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Driver-Model | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA GeForce RTX 4060 ... WDDM | 00000000:01:00.0 On | N/A |
| N/A 55C P4 12W / 129W | 2132MiB / 8188MiB | 33% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
若显示GPU信息(如型号、显存等),则表示支持。
6)Docker Desktop配置**(重要)**
-
打开 Docker 设置 → Resources → WSL Integration → 启用 Ubuntu 实例。
-
进入 Docker 设置 → Docker Engine,添加以下配置:
-
{ "experimental": true, "features": { "buildkit": true }, "registry-mirrors": ["https://registry.docker-cn.com"] // 可选镜像加速 }
-
-
保存并重启 Docker。
7)验证 GPU 加速
docker run --rm -it --gpus=all nvcr.io/nvidia/k8s/cuda-sample:nbody nbody -gpu -benchmark
NOTE: The CUDA Samples are not meant for performance measurements. Results may vary when GPU Boost is enabled.
> Windowed mode
> Simulation data stored in video memory
> Single precision floating point simulation
> 1 Devices used for simulation
MapSMtoCores for SM 8.9 is undefined. Default to use 128 Cores/SM
MapSMtoArchName for SM 8.9 is undefined. Default to use Ampere
GPU Device 0: "Ampere" with compute capability 8.9
> Compute 8.9 CUDA device: [NVIDIA GeForce RTX 4060 Laptop GPU]
24576 bodies, total time for 10 iterations: 17.351 ms
= 348.102 billion interactions per second
= 6962.039 single-precision GFLOP/s at 20 flops per interaction
成功输出应包含 GPU 型号及性能指标 [NVIDIA GeForce RTX 4060 Laptop GPU]
8)Ollama验证:
在容器中也能显示GPU了,说明配置成功了!!!
version: '3.9'
services:
ollama:
image: registry.cn-hangzhou.aliyuncs.com/xfg-studio/ollama:0.5.10
container_name: ollama
restart: unless-stopped
ports:
- "11434:11434"
volumes:
- ./ollama:/root/.ollama
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=all
RAG(检索增强生成)
postgre向量数据库
表结构:
PgAdmin软件下:ai-rag-knowledge->架构->public->表->vector_store
id
→ 每条数据的唯一标识(主键/uuid)。
content
→ 存放 chunk 的原始文本。
metadata
→ 存放 JSON 格式的额外信息(文件名、路径、知识库标签等)。
embedding
→ 存放向量,类型是 vector(N)
(N 由 Embedding 模型决定,比如 768)。
所有文件切分出来的 chunk 都存在这一张表里,每条记录就是一个 chunk。
作用
相似度检索:用 embedding <-> embedding
。
结果展示:取出 content
。
溯源/过滤:用 metadata
。
查询

还有 metadata
和embedding
列显示不下。
A. 索引/入库(Ingestion)
-
文档读取器
- 用
TikaDocumentReader
把上传的 PDF、Word、TXT、PPT 等文件解析成List<Document>
。 - 每个
Document
包含text
和metadata
(比如page
、source
)。 - 相当于是“把二进制文件 → 转成纯文本段落”。
- 用
-
清洗过滤
- 剔除空文本,避免无效数据进入后续流程。
-
文档切分器
- 用
TokenTextSplitter
把每个Document
再切成 chunk(默认 800 tokens 一段,可配置)。 - 目的是避免超长文本超过 Embedding 模型的输入限制。
@Bean public TokenTextSplitter tokenTextSplitter() { return TokenTextSplitter.builder() .withChunkSize(600) // 每段最多 600 token .withMinChunkSizeChars(300) // 每段至少 300 字符 .withMinChunkLengthToEmbed(10) .withMaxNumChunks(10000) .withKeepSeparator(true) .build(); }
withMinChunkSizeChars(300): 如果某个切分块的文本少于 300 个字符,
TokenTextSplitter
会避免直接拆分它,而是尝试 合并它与下一个切分块,直到符合字符数要求。 - 用
-
打元数据
- 给原始文档和 chunk 都加上
knowledge
(ragTag)、path
、original_filename
等信息。 - 方便后续检索时追溯来源。
- 给原始文档和 chunk 都加上
-
Embedding 模型
- 对 每个 chunk 调用 Ollama 的
nomic-embed-text
,生成一个固定维度的向量(如 768 维)。 - ⚠️ 注意:这是对 chunk 整体嵌入,不是对单个 token。
- 对 每个 chunk 调用 Ollama 的
-
向量存储
- 用
pgvector
存储[embedding 向量 + metadata + 原始文本]
。 - 后续可以通过向量相似度检索,结合 metadata 实现溯源。
- ⚠️ 向量维度由 Embedding 模型决定,pgvector 表的维度必须保持一致(如 768/1024/1536)。
- 用
/**
* RAG 知识库构建:读取文件、拆分、打标签、存储到向量库
*/
private void processAndStoreFile(
org.springframework.core.io.Resource resource,
String ragTag,
String normalizedPath,
String originalFilename) {
try {
// 1. 读取文件内容
TikaDocumentReader documentReader = new TikaDocumentReader(resource);
List<Document> documents = documentReader.get();
// 2. 过滤空文档
List<Document> docs = new ArrayList<>(documents);
docs.removeIf(d -> d.getText() == null || d.getText().trim().isEmpty());
if (docs.isEmpty()) {
log.warn("文件内容为空,跳过处理: {}", normalizedPath);
return;
}
// 3. 文本切分(默认 800 tokens/块)
List<Document> splits = tokenTextSplitter.apply(docs);
// 4. 设置元数据(原始文档 + 拆分文档)
docs.forEach(doc -> {
doc.getMetadata().put("knowledge", ragTag);
doc.getMetadata().put("path", normalizedPath);
doc.getMetadata().put("original_filename", originalFilename);
});
splits.forEach(doc -> {
doc.getMetadata().put("knowledge", ragTag);
doc.getMetadata().put("path", normalizedPath);
doc.getMetadata().put("original_filename", originalFilename);
});
// 5. 存储到向量数据库(只需要写入拆分后的块)
pgVectorStore.accept(splits);
log.info("文件处理完成: {}", normalizedPath);
} catch (Exception e) {
log.error("文件处理失败:{} - {}", normalizedPath, e.getMessage(), e);
}
}
B. 检索/回答(Query)
接收用户问题
- 输入用户的问题文本
message
。 - 使用同一个 Embedding 模型(如
nomic-embed-text
)将问题转为查询向量。
相似度检索
- 在向量库中用 余弦相似度 / 内积 搜索相似 chunk。
- 可附加条件过滤:如
knowledge == 'xxx'
,只在某个知识库范围内查。 - 常用参数:
topK
:取最相似的前 K 个结果(例如 5~8)。minSimilarityScore
(可选):过滤低相关度结果。
// 1) 相似度检索(带 ragTag 过滤)
SearchRequest request = SearchRequest.builder()
.query(message)
.topK(8)
.filterExpression("knowledge == '" + ragTag + "'")
.build();
List<Document> documents = pgVectorStore.similaritySearch(request);
⚡️ 注意:这里的 knowledge
是存储在 metadata
JSONB 里的字段,pgVectorStore
会自动翻译成 SQL(如 metadata->>'knowledge' = 'xxx'
)。
拼装文档上下文
- 把检索到的文档片段拼接成系统提示中的 DOCUMENTS 部分。
- 可以在拼接时附带 metadata(如文件名、页码),方便溯源。
String documentContent = documents.stream()
.map(doc -> "[来源:" + doc.getMetadata().get("original_filename") + "]\n" + doc.getText())
.collect(Collectors.joining("\n\n---\n\n"));
构造提示词(Prompt)
- 使用
SystemPromptTemplate
注入 DOCUMENTS 内容。 - System Prompt 应该放在 用户消息之前,确保模型优先遵循规则。
Message ragMessage = new SystemPromptTemplate(SYSTEM_PROMPT)
.createMessage(Map.of("documents", documentContent));
List<Message> messages = new ArrayList<>();
messages.add(ragMessage); // 先放系统提示
messages.add(new UserMessage(message));
调用对话模型(流式返回)
return ollamaChatModel.stream(new Prompt(
messages,
OllamaOptions.builder().model(model).build()
));
优化
1.优化分词逻辑
这块比较复杂,要切的恰到好处...切的不大不小。
2.更新向量嵌入模型
MCP服务
1)引入POM
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
</dependency>
2)配置MCP
resources/config/mcp-servers-config.json
{
"mcpServers": {
"filesystem": {
"command": "npx.cmd",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"D:/folder/MCP-test",
"D:/folder/MCP-test"
]
},
"mcp-server-computer": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-jar",
"D:/folder/study/apache-maven-3.8.4/mvn_repo/edu/whut/mcp/mcp-server-computer/1.0.0/mcp-server-computer-1.0.0.jar"
]
}
}
}
application.yml:
这实际上告诉系统用 npx
去启动一个 MCP Filesystem Server,路径指向你的 Desktop
。
需要提前下载该文件服务,https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem
1)先装 Node.js,配置环境变量
2)安装服务
npm install -g @modelcontextprotocol/server-filesystem
3)配置客户端
如果有多套 AI 对话模型,包括Ollama(deepseek、qwen)、OpenAI(gpt-4o),需要指定使用调用哪个接口。
@Bean
public ChatClient.Builder chatClientBuilder(OllamaChatModel ollamaChatModel) {
return new DefaultChatClientBuilder(
ollamaChatModel,
ObservationRegistry.NOOP,
(ChatClientObservationConvention) null
);
}
本项目调用的ollamaChatModel。注意,它是一个 Ollama 服务的客户端,真正的模型选择(deepseek、qwen、mistral…)是通过调用时传入的 OllamaOptions
来指定的。eg:
ChatResponse response = chatClientBuilder
.defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build())
.build()
.prompt("你好,介绍一下你自己")
.call();
4)测试
大模型会自动调用所能使用的工具!!!
@GetMapping("/test-workflow")
public String testWorkflow(@RequestParam String question) {
var chatClient = chatClientBuilder
.defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build())
.build();
ChatResponse response = chatClient
.prompt(question)
.tools(tools)
.call()
.chatResponse();
return response.toString();
}
@GetMapping("/tools")
public Object listTools() {
return Arrays.stream(tools.getToolCallbacks())
.map(cb -> Map.of(
"name", cb.getName(),
"description", cb.getDescription()
))
.toList();
}
有时候大模型不会去调用Tools,可能是模型能力不够。