SpringAi+milvus向量数据库实现基于本地知识问答

实现思路

利用 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 得分了,会报异常;

至此结束!!!

相关推荐
SHIPKING3936 小时前
【Prompt工程—文生图】案例大全
llm·prompt·文生图
水煮蛋不加蛋7 小时前
AutoGen 框架解析:微软开源的多人 Agent 协作新范式
人工智能·microsoft·ai·开源·大模型·llm·agent
Two summers ago17 小时前
arXiv2025 | TTRL: Test-Time Reinforcement Learning
论文阅读·人工智能·机器学习·llm·强化学习
AI大模型顾潇1 天前
[特殊字符] Milvus + LLM大模型:打造智能电影知识库系统
数据库·人工智能·机器学习·大模型·llm·llama·milvus
Violet_Stray1 天前
【Ollama】docker离线部署Ollama+deepseek
docker·部署·ollama·deepseek
Alfred king3 天前
华为昇腾910B通过vllm部署InternVL3-8B教程
llm·nlp·vllm部署
CoderJia程序员甲4 天前
AI驱动的Kubernetes管理:kubectl-ai 如何简化你的云原生运维
运维·人工智能·云原生·kubernetes·llm
董厂长4 天前
LLM :Function Call、MCP协议与A2A协议
网络·人工智能·深度学习·llm
tangjunjun-owen4 天前
第三章:langchain加载word文档构建RAG检索教程(基于FAISS库为例)
langchain·llm·word·faiss·rag
yutianzuijin5 天前
大模型推理--从零搭建大模型推理服务器:硬件选购、Ubuntu双系统安装与环境配置
服务器·ubuntu·llm·大模型推理