实现思路
利用 AI 大模型(如 GPT、BERT)对文本、图像等数据生成高维向量(Embedding),捕捉其语义特征,然后将这些向量存储到向量数据库(如 Milvus、Pinecone)中并建立索引。当用户提问时,使用相同的模型将问题转换为向量,在数据库中通过相似性搜索快速找到最相关的知识条目。整个过程在本地完成,确保隐私和低延迟,同时结合大模型可进一步优化回答质量,实现智能化的知识检索与交互。
开发环境
JDK17、maven 3.8.6、springBoot 3.4.4、springAi 1.0.0-M6、milvus 2.3.8
效果展示


安装Milvus向量数据库
Milvus 简介
Milvus 是一个开源的向量数据库,专为高效存储、检索和管理大规模嵌入向量(Embeddings)而设计。它广泛应用于人工智能和机器学习领域,尤其是在需要处理高维数据的场景中,例如推荐系统、图像搜索、自然语言处理(NLP)、视频分析等。Milvus 的核心目标是解决传统数据库在处理非结构化数据时的性能瓶颈,提供快速、灵活且可扩展的解决方案。
Docker安装Milvus服务
bash
wget https://raw.githubusercontent.com/milvus-io/milvus/2.3/scripts/standalone_embed.sh
bash standalone_embed.sh start
使用客户端查看数据

call-mcp-server
java代码
CallMcpServerApplication.java
typescript
package com.ahucoding.rocket.callmcpserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CallMcpServerApplication {
public static void main(String[] args) {
SpringApplication.run(CallMcpServerApplication.class, args);
}
}
IndexController.java
kotlin
package com.ahucoding.rocket.callmcpserver.view;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/")
public String chat(Model model) {
return "index";
}
}
ChatController.java
less
package com.ahucoding.rocket.callmcpserver.view;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import io.modelcontextprotocol.client.McpSyncClient;
import jakarta.servlet.http.HttpServletResponse;
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.client.advisor.SafeGuardAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
@RestController
@RequestMapping("/dashscope/chat-client")
@CrossOrigin("*")
public class ChatController {
private final ChatClient chatClient;
private final ChatMemory chatMemory = new InMemoryChatMemory();
public ChatController(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpSyncClients, ToolCallbackProvider tools, MilvusVectorStore milvusVectorStore) {
this.chatClient = chatClientBuilder
// 它定义了聊天机器人在回答问题时应当遵循的风格和角色定位。
// .defaultSystem("以专业天气预报主持人的风格回答问题。")
// 指定聊天客户端可用的工具
.defaultTools(tools)
.defaultAdvisors(
// 这里可以添加多个顾问 order(优先级)越小,越先执行
// 注意:顾问添加到链中的顺序至关重要,因为它决定了其执行的顺序。每个顾问都会以某种方式修改提示或上下文,一个顾问所做的更改会传递给链中的下一个顾问。
// 在此配置中,将首先执行MessageChatMemoryAdvisor,将对话历史记录添加到提示中。然后,问答顾问将根据用户的问题和添加的对话历史进行搜索,从而可能提供更相关的结果。
new MessageChatMemoryAdvisor(chatMemory, "chat-memory-advisor", 10),
// QuestionAnswerAdvisor 此顾问使用矢量存储提供问答功能,实现RAG(检索增强生成)模式
QuestionAnswerAdvisor.builder(milvusVectorStore).userTextAdvise("""
下文信息如下,由 --------------------- 包围。
---------------------
{question_answer_context}
---------------------
根据给定的上下文以及提供的历史信息来回复用户的评论,不要依据先前的知识。如果答案不在上下文中,要告知用户你无法回答该问题。
""").order(1).build(),
// SafeGuardAdvisor是一个安全防护顾问,它确保生成的内容符合道德和法律标准。
SafeGuardAdvisor.builder().sensitiveWords(List.of("色情", "暴力")) // 敏感词列表
.order(2) // 设置优先级
.failureResponse("抱歉,我无法回答这个问题。").build(), // 敏感词过滤失败时的响应
// SimpleLoggerAdvisor是一个记录ChatClient的请求和响应数据的顾问。这对于调试和监控您的AI交互非常有用,建议将其添加到链的末尾。
new SimpleLoggerAdvisor()
)
.defaultOptions(DashScopeChatOptions.builder()
.withEnableSearch(true) // 开启互联网搜索
.withTopP(0.7) // 取值越大,生成的随机性越高;取值越低,生成的随机性越低。默认值为0.8
.build())
.build();
}
@RequestMapping(value = "/generate_stream", method = RequestMethod.GET)
public Flux<ChatResponse> generateStream(HttpServletResponse response, @RequestParam("id") String id, @RequestParam("prompt") String prompt) {
response.setCharacterEncoding("UTF-8");
return this.chatClient.prompt()
.user(prompt) // 用户输入
.advisors(a -> a.param(MessageChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, id) // 设置对话历史记录的ID
.param(MessageChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) // 设置对话历史记录的大小
.stream()
.chatResponse()
.onErrorResume(e -> {
System.out.println("Error: " + e.getMessage());
return Mono.empty();
});
}
}
MilvusEmbeddingService.java
用于创建向量集合,及插入数据;
scss
package com.ahucoding.rocket.callmcpserver.milvus.service;
import com.alibaba.fastjson.JSONObject;
import io.milvus.client.MilvusServiceClient;
import io.milvus.param.IndexType;
import io.milvus.param.MetricType;
import io.milvus.param.collection.CollectionSchemaParam;
import io.milvus.param.collection.CreateCollectionParam;
import io.milvus.param.collection.FieldType;
import io.milvus.param.dml.InsertParam;
import io.milvus.param.index.CreateIndexParam;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Service
@Slf4j
public class MilvusEmbeddingService {
//类似于mysql中的表,定义一个名称为collection_02的集合
private static final String COLLECTION_NAME = MilvusVectorStore.DEFAULT_COLLECTION_NAME;
//向量维度定义1536,跟阿里巴巴embedding向量服务返回的维度保持一致
private static final int VECTOR_DIM = 1536;
@Resource
private MilvusVectorStore client;
//注入阿里巴巴EmbeddingModel
@Resource
private EmbeddingModel embeddingModel;
final String doc_id = MilvusVectorStore.DOC_ID_FIELD_NAME;
final String content = MilvusVectorStore.CONTENT_FIELD_NAME;
final String metadata = MilvusVectorStore.METADATA_FIELD_NAME;
final String embedding = MilvusVectorStore.EMBEDDING_FIELD_NAME;
public void createCollection() {
MilvusServiceClient milvusServiceClient = (MilvusServiceClient) client.getNativeClient().get();
CollectionSchemaParam schema = CollectionSchemaParam.newBuilder()
.addFieldType(FieldType.newBuilder()
.withName(doc_id)
.withDataType(io.milvus.grpc.DataType.VarChar)
.withMaxLength(100)
.withPrimaryKey(true)
.withAutoID(false)
.build())
.addFieldType(FieldType.newBuilder()
.withName(content)
.withDataType(io.milvus.grpc.DataType.VarChar)
.withMaxLength(5000)
.build())
.addFieldType(FieldType.newBuilder()
.withName(metadata)
.withDataType(io.milvus.grpc.DataType.JSON)
.build())
.addFieldType(FieldType.newBuilder()
.withName(embedding)
.withDataType(io.milvus.grpc.DataType.FloatVector)
.withDimension(VECTOR_DIM)
.build())
.build();
CreateCollectionParam createCollectionParam = CreateCollectionParam.newBuilder()
.withCollectionName(COLLECTION_NAME)
.withSchema(schema)
.withDatabaseName(MilvusVectorStore.DEFAULT_DATABASE_NAME)
.build();
milvusServiceClient.createCollection(createCollectionParam);
CreateIndexParam createIndexParam = CreateIndexParam.newBuilder()
.withDatabaseName(MilvusVectorStore.DEFAULT_DATABASE_NAME)
.withCollectionName(COLLECTION_NAME)
.withFieldName(embedding)
.withMetricType(MetricType.COSINE)
.withIndexType(IndexType.IVF_FLAT)
.withExtraParam("{"nlist":128}")
.build();
milvusServiceClient.createIndex(createIndexParam);
}
public void insertRecord(Document document) {
MilvusServiceClient milvusServiceClient = (MilvusServiceClient) client.getNativeClient().get();
// 准备插入 Milvus 的数据
List<InsertParam.Field> fields = new ArrayList<>();
fields.add(InsertParam.Field.builder().name(doc_id).values(Collections.singletonList(document.getId())).build());
fields.add(InsertParam.Field.builder().name(content).values(Collections.singletonList(document.getText())).build());
fields.add(InsertParam.Field.builder().name(metadata).values(Collections.singletonList(new JSONObject())).build());
//调用阿里向量模型服务,返回1536维向量float
List<List<Float>> book_intro_array = new ArrayList<>();
List<Float> vectorList = new ArrayList<>();
float[] floatArray = embeddingModel.embed(document.getText());
for (float f : floatArray) {
vectorList.add(f);
}
book_intro_array.add(vectorList);
fields.add(InsertParam.Field.builder().name(embedding).values(book_intro_array).build());
milvusServiceClient.insert(InsertParam.newBuilder()
.withDatabaseName(MilvusVectorStore.DEFAULT_DATABASE_NAME)
.withCollectionName(COLLECTION_NAME)
.withFields(fields)
.build());
}
}
McpClientCfg.java
java
package com.ahucoding.rocket.callmcpserver.cfg;
import io.modelcontextprotocol.client.McpClient;
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class McpClientCfg implements McpSyncClientCustomizer {
@Override
public void customize(String name, McpClient.SyncSpec spec) {
// do nothing
spec.requestTimeout(Duration.ofSeconds(30));
}
}
application.yaml
yaml
server:
port: 9999
# https://docs.spring.io/spring-ai/reference/1.0/api/mcp/mcp-client-boot-starter-docs.html
spring:
ai:
vectorstore:
milvus:
client:
host: "192.168.137.92"
port: 19530
#username: "root"
#password: "milvus"
databaseName: "default"
collectionName: "vector_store"
embeddingDimension: 1536
indexType: IVF_FLAT
metricType: COSINE
# 通义大模型配置
dashscope:
api-key: sk-xxxxxxxxxx
chat:
options:
model: qwen-max
workspace-id: "llm-xxxxxxxx"
POM.XML
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ahucoding.rocket</groupId>
<artifactId>call-mcp-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>call-mcp-server</name>
<description>call-mcp-server</description>
<properties>
<java.version>17</java.version>
<spring.ai.alibaba>1.0.0-M6.1</spring.ai.alibaba>
<spring.ai.version>1.0.0-M6</spring.ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring.ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>${spring.ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
<version>${spring.ai.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring.ai.alibaba}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
AI交互页面
xml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 对话助手</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto p-4 max-w-3xl">
<!-- 标题 -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-800">AI 对话助手</h1>
<p class="text-gray-600 mt-2">基于 Spring AI 的流式对话系统 By AhuCodingBeast</p>
</div>
<!-- 聊天容器 -->
<div id="chat-container" class="bg-white rounded-xl shadow-lg p-4 mb-4 h-[500px] overflow-y-auto space-y-4">
<!-- 初始欢迎消息 -->
<div class="ai-message flex items-start gap-3">
<div class="bg-green-100 p-3 rounded-lg max-w-[85%]">
<span class="text-gray-800">您好!我是AI助手,有什么可以帮您?</span>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="flex gap-2">
<input type="text" id="message-input"
class="flex-1 border border-gray-300 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="输入您的问题...">
<button id="send-button"
class="bg-blue-500 text-white px-6 py-3 rounded-xl hover:bg-blue-600 transition-colors flex items-center">
<span>发送</span>
<svg id="loading-spinner" class="hidden w-4 h-4 ml-2 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
</button>
</div>
</div>
<script>
const chatContainer = document.getElementById('chat-container');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const loadingSpinner = document.getElementById('loading-spinner');
// 发送消息处理
function handleSend() {
const message = messageInput.value.trim();
if (!message) return;
// 添加用户消息
addMessage(message, 'user');
messageInput.value = '';
// 构建API URL
const apiUrl = new URL('http://localhost:9999/dashscope/chat-client/generate_stream');
apiUrl.searchParams.append('id', '01');
apiUrl.searchParams.append('prompt', message);
// 显示加载状态
sendButton.disabled = true;
loadingSpinner.classList.remove('hidden');
// 创建EventSource连接
const eventSource = new EventSource(apiUrl);
let aiMessageElement = null;
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log(data);
const content = data.result?.output?.text || '';
const finishReason = data.result?.metadata?.finishReason;
// 创建消息容器(如果不存在)
if (!aiMessageElement) {
aiMessageElement = addMessage('', 'ai');
}
// 追加内容
if (content) {
aiMessageElement.querySelector('.message-content').textContent += content;
autoScroll();
}
// 处理结束
if (finishReason === 'STOP') {
eventSource.close();
sendButton.disabled = false;
loadingSpinner.classList.add('hidden');
}
} catch (error) {
console.error('解析错误:', error);
}
};
eventSource.onerror = (error) => {
console.error('连接错误:', error);
eventSource.close();
sendButton.disabled = false;
loadingSpinner.classList.add('hidden');
addMessage('对话连接异常,请重试', 'ai', true);
};
}
// 添加消息到容器
function addMessage(content, type, isError = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `${type}-message flex items-start gap-3`;
const bubble = document.createElement('div');
bubble.className = `p-3 rounded-lg max-w-[85%] ${
type === 'user'
? 'bg-blue-500 text-white ml-auto'
: `bg-green-100 ${isError ? 'text-red-500' : 'text-gray-800'}`
}`;
const contentSpan = document.createElement('span');
contentSpan.className = 'message-content';
contentSpan.textContent = content;
bubble.appendChild(contentSpan);
messageDiv.appendChild(bubble);
chatContainer.appendChild(messageDiv);
autoScroll();
return bubble;
}
// 自动滚动到底部
function autoScroll() {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// 事件监听
sendButton.addEventListener('click', handleSend);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
</script>
</body>
</html>
注意点:
1、Milvus 最新版本,已经将 distance 改为 score 得分了,会报异常;
至此结束!!!