SpringAI + Milvus实战:5分钟搞定企业级文档智能问答系统(附完整代码)

零基础6个月掌握RAG+Agent开发实战路径 RAG(检索增强生成)与Agent(智能体)是当前AI应用层最核心的工程范式,其本质是将大语言模型(LLM)的能力与结构化知识、任务编排深度耦合。RAG解决‘知道什么’的问题,通过向量化检索提升回答准确性;Agent则解决‘怎么用’的问题,依托工具调用、记忆与推理实现端到端自主执行。技术价值在于显著降低幻觉率、支持私有知识落地、并具备可解释性与可控性。典型应用场景覆盖智能客服、企业知识库、自动化办公及垂直领域助手等。本实践指南聚焦Python生态下的RAG+Agent最小闭环构建,涵盖环境配置、分块嵌入、本 阅读详情

SpringAI + Milvus:构建企业级文档智能问答系统的实战指南

在当今信息爆炸的时代,企业面临着海量文档数据的管理和利用难题。传统的基于关键词的搜索方式已经难以满足对知识深度挖掘的需求,而基于语义理解的智能问答系统正成为企业数字化转型的关键技术。对于Java开发者来说,如何快速构建一个稳定、高效且易于维护的文档智能问答系统,是一个值得深入探讨的课题。

SpringAI作为Spring生态中专门为AI应用设计的框架,提供了与各种AI模型的无缝集成能力。而Milvus作为一款高性能的开源向量数据库,在处理大规模向量检索任务时表现出色。将这两者结合,我们能够构建出一个功能强大、易于扩展的企业级文档智能问答系统。本文将带你从零开始,深入探讨如何利用SpringAI和Milvus快速搭建这样一个系统,并提供完整的实战代码和优化建议。

1. 企业文档智能问答系统的核心价值与挑战

在企业环境中,文档智能问答系统不仅仅是技术上的创新,更是业务效率提升的关键驱动力。想象一下,新员工入职时不再需要翻阅数百页的规章制度手册,而是直接向系统提问:“公司的年假政策是什么?”系统能够立即给出准确的答案,并附上相关文档的出处。这种体验的转变,正是智能问答系统带来的价值。

1.1 企业文档处理的典型痛点

在我参与过的多个企业级项目中,文档处理通常面临以下几个核心挑战:

  • 信息孤岛问题:企业文档分散在各个部门、各个系统中,格式各异(PDF、Word、Excel、PPT等),难以统一管理和检索
  • 检索效率低下:传统的关键词匹配方式无法理解语义,导致搜索结果不准确或遗漏重要信息
  • 知识传承困难:员工离职导致专业知识流失,新员工需要大量时间学习现有文档
  • 响应速度慢:客服或技术支持人员需要花费大量时间查找文档来回答用户问题
  • 多语言支持不足:跨国企业需要处理多种语言的文档,传统系统难以应对

1.2 技术选型的考量因素

选择SpringAI和Milvus作为技术栈,主要基于以下几个考量:

SpringAI的优势:

  • 与Spring生态无缝集成,Java开发者学习成本低
  • 提供了统一的AI模型抽象层,便于切换不同的AI服务提供商
  • 内置了RAG(检索增强生成)等高级功能的支持
  • 良好的社区支持和持续更新

Milvus的特点:

  • 专门为向量检索优化,支持十亿级向量的毫秒级查询
  • 丰富的索引类型(IVF_FLAT、HNSW、SCANN等)满足不同场景需求
  • 完善的Java SDK支持,与Spring应用集成简单
  • 云原生架构,支持水平扩展

提示:在实际项目中,技术选型不仅要考虑技术本身的能力,还要考虑团队的技术栈熟悉度、维护成本和长期发展。SpringAI + Milvus的组合为Java团队提供了一个平衡技术先进性和开发效率的解决方案。

2. 系统架构设计与技术栈选型

一个完整的企业级文档智能问答系统需要精心设计其架构,确保系统的可扩展性、可靠性和易维护性。下面我将分享一个经过多个项目验证的架构设计方案。

2.1 整体架构设计

┌─────────────────────────────────────────────────────────────┐
│                   前端展示层                                 │
│  (Web界面/移动端/API接口)                                   │
└───────────────────────┬─────────────────────────────────────┘
                        │
┌───────────────────────▼─────────────────────────────────────┐
│                   应用服务层                                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │
│  │  用户管理   │  │  文档管理   │  │  问答服务   │        │
│  └─────────────┘  └─────────────┘  └─────────────┘        │
└───────────────────────┬─────────────────────────────────────┘
                        │
┌───────────────────────▼─────────────────────────────────────┐
│                   业务逻辑层                                 │
│  ┌─────────────────────────────────────────────────────┐    │
│  │              SpringAI + Milvus核心引擎              │    │
│  │  • 文档解析与向量化                                 │    │
│  │  • 语义检索与排序                                   │    │
│  │  • 上下文构建与提示工程                             │    │
│  │  • 大模型交互与答案生成                             │    │
│  └─────────────────────────────────────────────────────┘    │
└───────────────────────┬─────────────────────────────────────┘
                        │
┌───────────────────────▼─────────────────────────────────────┐
│                   数据存储层                                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │
│  │  关系数据库 │  │  向量数据库 │  │  文件存储   │        │
│  │  (MySQL)    │  │   (Milvus)  │  │  (MinIO)    │        │
│  └─────────────┘  └─────────────┘  └─────────────┘        │
└─────────────────────────────────────────────────────────────┘

2.2 技术栈详细配置

基于上述架构,我们需要选择合适的技术组件。以下是一个经过生产验证的技术栈配置:

后端框架:

  • Spring Boot 3.2+(推荐使用最新稳定版)
  • Spring AI 1.0.0+(注意版本兼容性)
  • Java 17+(长期支持版本)

向量数据库:

  • Milvus 2.4+(支持Standalone和Cluster部署)
  • 推荐使用Docker Compose或Kubernetes部署

AI模型服务:

  • 嵌入模型:OpenAI text-embedding-3-small(1536维)
  • 大语言模型:可根据需求选择(OpenAI GPT-4、Claude、本地部署模型等)

其他基础设施:

  • Redis:用于缓存和会话管理
  • MinIO/S3:文档文件存储
  • MySQL/PostgreSQL:元数据存储
  • Elasticsearch:可选,用于传统全文检索

监控与运维:

  • Prometheus + Grafana:系统监控
  • OpenTelemetry:分布式追踪
  • ELK Stack:日志管理

2.3 环境准备与依赖配置

在实际部署前,我们需要确保开发和生产环境的一致性。以下是一个详细的依赖配置示例:

<!-- pom.xml 关键依赖配置 -->
<properties>
    <spring-boot.version>3.2.5</spring-boot.version>
    <spring-ai.version>1.0.0</spring-ai.version>
    <milvus-sdk.version>2.4.0</milvus-sdk.version>
</properties>

<dependencies>
    <!-- Spring Boot 核心依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Spring AI 核心 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter</artifactId>
        <version>${spring-ai.version}</version>
    </dependency>
    
    <!-- Spring AI 与 OpenAI 集成 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        <version>${spring-ai.version}</version>
    </dependency>
    
    <!-- Milvus 向量存储 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-milvus</artifactId>
        <version>${spring-ai.version}</version>
    </dependency>
    
    <!-- 文档解析支持 -->
    <dependency>
        <groupId>org.apache.tika</groupId>
        <artifactId>tika-core</artifactId>
        <version>2.9.1</version>
    </dependency>
    
    <!-- 监控与健康检查 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    <!-- 配置处理器(用于提示配置自动补全) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

对于配置文件,我建议采用分层配置的方式,便于不同环境的部署:

# application.yml 核心配置
spring:
  application:
    name: document-qa-system
  
  ai:
    openai:
      api-key: ${OPENAI_API_KEY:}
      base-url: ${OPENAI_BASE_URL:https://api.openai.com}
      chat:
        options:
          model: gpt-4-turbo-preview
          temperature: 0.1
          max-tokens: 2000
      embedding:
        options:
          model: text-embedding-3-small
          dimensions: 1536
    
    vectorstore:
      milvus:
        client:
          host: ${MILVUS_HOST:localhost}
          port: ${MILVUS_PORT:19530}
          username: ${MILVUS_USERNAME:root}
          password: ${MILVUS_PASSWORD:milvus}
        database-name: ${MILVUS_DATABASE:default}
        collection-name: ${MILVUS_COLLECTION:document_vectors}
        embedding-dimension: 1536
        index-type: IVF_FLAT
        metric-type: COSINE
        initialize-schema: true

# 应用特定配置
app:
  document:
    chunk-size: 1000  # 文档分块大小(字符数)
    chunk-overlap: 200 # 分块重叠大小
    supported-formats:
      - pdf
      - docx
      - pptx
      - xlsx
      - txt
      - md

3. 核心模块实现:从文档处理到智能问答

有了清晰的技术架构和配置,接下来我们深入实现系统的核心功能模块。我将按照实际开发流程,逐步构建完整的文档智能问答系统。

3.1 文档解析与向量化处理

文档处理是整个系统的基石。我们需要支持多种格式的文档,并将其转换为适合向量检索的格式。以下是一个完整的文档处理服务实现:

@Service
@Slf4j
public class DocumentProcessingService {
    
    private final Tika tika = new Tika();
    private final EmbeddingModel embeddingModel;
    private final VectorStore vectorStore;
    
    // 支持的文件类型映射
    private static final Map<String, String> SUPPORTED_MIME_TYPES = Map.of(
        "application/pdf", "PDF",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "DOCX",
        "application/vnd.openxmlformats-officedocument.presentationml.presentation", "PPTX",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "XLSX",
        "text/plain", "TXT",
        "text/markdown", "MD"
    );
    
    public DocumentProcessingService(EmbeddingModel embeddingModel, 
                                     VectorStore vectorStore) {
        this.embeddingModel = embeddingModel;
        this.vectorStore = vectorStore;
    }
    
    /**
     * 处理上传的文档文件
     */
    @Transactional
    public DocumentProcessResult processDocument(MultipartFile file, 
                                                 String knowledgeBaseId,
                                                 Map<String, Object> metadata) {
        try {
            // 1. 验证文件类型
            String mimeType = tika.detect(file.getInputStream());
            if (!SUPPORTED_MIME_TYPES.containsKey(mimeType)) {
                throw new UnsupportedDocumentFormatException(
                    "不支持的文件格式: " + mimeType);
            }
            
            // 2. 提取文本内容
            String content = extractContent(file, mimeType);
            
            // 3. 文本清洗和预处理
            String cleanedContent = cleanContent(content);
            
            // 4. 文本分块
            List<TextChunk> chunks = splitIntoChunks(cleanedContent);
            
            // 5. 向量化并存储
            List<Document> documents = createDocuments(chunks, knowledgeBaseId, metadata);
            vectorStore.add(documents);
            
            return DocumentProcessResult.builder()
                .success(true)
                .documentCount(documents.size())
                .originalSize(file.getSize())
                .processedSize(cleanedContent.length())
                .format(SUPPORTED_MIME_TYPES.get(mimeType))
                .build();
                
        } catch (Exception e) {
            log.error("文档处理失败: {}", file.getOriginalFilename(), e);
            throw new DocumentProcessingException("文档处理失败", e);
        }
    }
    
    /**
     * 根据MIME类型提取文本内容
     */
    private String extractContent(MultipartFile file, String mimeType) throws IOException {
        switch (mimeType) {
            case "application/pdf":
                return extractFromPdf(file);
            case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
                return extractFromDocx(file);
            case "text/plain":
            case "text/markdown":
                return new String(file.getBytes(), StandardCharsets.UTF_8);
            default:
                // 使用Tika作为后备方案
                return tika.parseToString(file.getInputStream());
        }
    }
    
    /**
     * 文本清洗和预处理
     */
    private String cleanContent(String content) {
        // 移除多余的空格和换行
        String cleaned = content.replaceAll("\\s+", " ")
                              .replaceAll("\\n{3,}", "\n\n")
                              .trim();
        
        // 移除不可见字符
        cleaned = cleaned.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]", "");
        
        // 标准化标点符号
        cleaned = cleaned.replaceAll("[“”]", "\"")
                       .replaceAll("[‘’]", "'")
                       .replaceAll("…", "...");
        
        return cleaned;
    }
    
    /**
     * 智能文本分块
     */
    private List<TextChunk> splitIntoChunks(String content) {
        List<TextChunk> chunks = new ArrayList<>();
        
        // 首先尝试按段落分割
        String[] paragraphs = content.split("\\n\\n+");
        
        for (String paragraph : paragraphs) {
            if (paragraph.trim().isEmpty()) {
                continue;
            }
            
            // 如果段落过长,进一步按句子分割
            if (paragraph.length() > 1000) {
                List<String> sentences = splitIntoSentences(paragraph);
                StringBuilder currentChunk = new StringBuilder();
                
                for (String sentence : sentences) {
                    if (currentChunk.length() + sentence.length() > 1000) {
                        chunks.add(createTextChunk(currentChunk.toString()));
                        currentChunk = new StringBuilder();
                    }
                    currentChunk.append(sentence).append(" ");
                }
                
                if (currentChunk.length() > 0) {
                    chunks.add(createTextChunk(currentChunk.toString()));
                }
            } else {
                chunks.add(createTextChunk(paragraph));
            }
        }
        
        return chunks;
    }
    
    /**
     * 创建向量存储文档
     */
    private List<Document> createDocuments(List<TextChunk> chunks, 
                                          String knowledgeBaseId,
                                          Map<String, Object> baseMetadata) {
        List<Document> documents = new ArrayList<>();
        
        for (int i = 0; i < chunks.size(); i++) {
            TextChunk chunk = chunks.get(i);
            
            Map<String, Object> metadata = new HashMap<>(baseMetadata);
            metadata.put("knowledge_base_id", knowledgeBaseId);
            metadata.put("chunk_index", i);
            metadata.put("chunk_size", chunk.getContent().length());
            metadata.put("created_at", System.currentTimeMillis());
            
            // 添加文档特定元数据
            if (chunk.getTitle() != null) {
                metadata.put("title", chunk.getTitle());
            }
            if (chunk.getSection() != null) {
                metadata.put("section", chunk.getSection());
            }
            
            Document document = new Document(chunk.getContent(), metadata);
            documents.add(document);
        }
        
        return documents;
    }
    
    // 其他辅助方法...
}

这个文档处理服务有几个关键设计点值得注意:

  1. 多格式支持:通过Tika库和特定格式解析器的组合,确保对各种文档格式的良好支持
  2. 智能分块:不是简单的固定长度分块,而是结合段落和句子进行智能分割
  3. 元数据丰富:为每个文档块添加丰富的元数据,便于后续的检索和过滤
  4. 错误处理:完善的异常处理机制,确保单文档处理失败不影响整体流程

3.2 Milvus向量存储的深度配置

Milvus的配置对系统性能有重大影响。下面是一个经过优化的配置类:

@Configuration
@EnableConfigurationProperties(MilvusProperties.class)
public class MilvusConfiguration {
    
    private final MilvusProperties properties;
    
    public MilvusConfiguration(MilvusProperties properties) {
        this.properties = properties;
    }
    
    @Bean
    @Primary
    public MilvusServiceClient milvusClient() {
        ConnectParam.Builder builder = ConnectParam.newBuilder()
            .withHost(properties.getClient().getHost())
            .withPort(properties.getClient().getPort());
        
        // 认证配置
        if (StringUtils.hasText(properties.getClient().getUsername())) {
            builder.withAuthorization(
                properties.getClient().getUsername(),
                properties.getClient().getPassword()
            );
        }
        
        // Token认证(云服务常用)
        if (StringUtils.hasText(properties.getClient().getToken())) {
            builder.withToken(properties.getClient().getToken());
        }
        
        // URI配置(优先级最高)
        if (StringUtils.hasText(properties.getClient().getUri())) {
            builder.withUri(properties.getClient().getUri());
        }
        
        // 连接超时配置
        builder.withConnectTimeout(properties.getClient().getConnectTimeoutMs(), TimeUnit.MILLISECONDS)
               .withKeepAliveTime(properties.getClient().getKeepAliveTimeMs(), TimeUnit.MILLISECONDS)
               .withKeepAliveTimeout(properties.getClient().getKeepAliveTimeoutMs(), TimeUnit.MILLISECONDS);
        
        // TLS/SSL配置
        if (properties.getClient().isSecure()) {
            builder.withSecure(properties.getClient().isSecure());
            
            if (StringUtils.hasText(properties.getClient().getServerName())) {
                builder.withServerName(properties.getClient().getServerName());
            }
        }
        
        return new MilvusServiceClient(builder.build());
    }
    
    @Bean
    @Primary
    public VectorStore milvusVectorStore(MilvusServiceClient milvusClient,
                                        EmbeddingModel embeddingModel) {
        return MilvusVectorStore.builder(milvusClient, embeddingModel)
            .collectionName(properties.getCollectionName())
            .databaseName(properties.getDatabaseName())
            .indexType(IndexType.valueOf(properties.getIndexType()))
            .metricType(MetricType.valueOf(properties.getMetricType()))
            .embeddingDimension(properties.getEmbeddingDimension())
            .indexParameters(parseIndexParameters(properties.getIndexParameters()))
            .idFieldName(properties.getIdFieldName())
            .contentFieldName(properties.getContentFieldName())
            .metadataFieldName(properties.getMetadataFieldName())
            .embeddingFieldName(properties.getEmbeddingFieldName())
            .batchingStrategy(new TokenCountBatchingStrategy())
            .initializeSchema(properties.isInitializeSchema())
            .build();
    }
    
    /**
     * 解析索引参数JSON
     */
    private Map<String, Object> parseIndexParameters(String json) {
        if (!StringUtils.hasText(json)) {
            return Map.of("nlist", 1024); // 默认值
        }
        
        try {
            ObjectMapper mapper = new ObjectMapper();
            return mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
        } catch (JsonProcessingException e) {
            log.warn("Failed to parse index parameters JSON, using defaults", e);
            return Map.of("nlist", 1024);
        }
    }
    
    /**
     * 自定义批量处理策略
     */
    @Bean
    @ConditionalOnMissingBean
    public BatchingStrategy customBatchingStrategy() {
        return documents -> {
            List<List<Document>> batches = new ArrayList<>();
            List<Document> currentBatch = new ArrayList<>();
            int currentTokenCount = 0;
            
            for (Document doc : documents) {
                int docTokenCount = estimateTokenCount(doc.getText());
                
                // 如果当前批次已满或添加当前文档会超出限制,则开始新批次
                if (!currentBatch.isEmpty() && 
                    (currentTokenCount + docTokenCount > 1000 || currentBatch.size() >= 50)) {
                    batches.add(new ArrayList<>(currentBatch));
                    currentBatch.clear();
                    currentTokenCount = 0;
                }
                
                currentBatch.add(doc);
                currentTokenCount += docTokenCount;
            }
            
            if (!currentBatch.isEmpty()) {
                batches.add(currentBatch);
            }
            
            return batches;
        };
    }
    
    /**
     * 简单的token估算(实际项目中建议使用专门的tokenizer)
     */
    private int estimateTokenCount(String text) {
        // 英文:大约1个token对应4个字符
        // 中文:大约1个token对应2个字符
        // 这里使用简单估算,实际项目应根据具体模型调整
        return text.length() / 2;
    }
}

为了更清晰地理解不同索引类型的适用场景,我整理了以下对比表格:

索引类型适用场景内存占用查询速度精度构建时间
IVF_FLAT大规模数据集,平衡查询速度和精度中等快高中等
IVF_SQ8内存敏感场景,需要压缩存储低快中等中等
IVF_PQ超大规模数据集,内存极度受限很低快较低中等
HNSW小到中等数据集,追求最高精度高很快很高慢
SCANN大规模数据集,GPU加速查询中等极快(GPU)高快
AUTOINDEX自动选择最佳索引可变可变可变可变

注意:选择索引类型时需要根据实际数据规模、查询性能要求和硬件资源进行权衡。对于大多数企业文档问答场景,IVF_FLAT或HNSW是不错的选择。

3.3 智能检索与问答引擎实现

检索增强生成(RAG)是智能问答系统的核心。下面是一个完整的RAG服务实现:

@Service
@Slf4j
public class IntelligentQAService {
    
    private final VectorStore vectorStore;
    private final ChatClient chatClient;
    private final ChatMemory chatMemory;
    private final Retriever retriever;
    
    // 系统提示词模板
    private static final String SYSTEM_PROMPT_TEMPLATE = """
        你是一个专业的企业知识助手,基于提供的文档内容回答用户问题。
        
        文档上下文:
        {context}
        
        回答要求:
        1. 基于上述文档内容回答问题,不要编造文档中没有的信息
        2. 如果文档中没有相关信息,请明确告知"根据现有文档,无法回答此问题"
        3. 回答要简洁明了,重点突出
        4. 如果问题涉及多个方面,请分点说明
        5. 引用相关的文档片段时,可以注明出处
        
        历史对话:
        {history}
        
        当前问题:{question}
        """;
    
    public IntelligentQAService(VectorStore vectorStore,
                               ChatClient chatClient,
                               ChatMemory chatMemory) {
        this.vectorStore = vectorStore;
        this.chatClient = chatClient;
        this.chatMemory = chatMemory;
        this.retriever = new RetrieverImpl(vectorStore);
    }
    
    /**
     * 智能问答主方法
     */
    public AnswerResponse askQuestion(QuestionRequest request) {
        long startTime = System.currentTimeMillis();
        
        try {
            // 1. 查询重写(处理模糊或简写问题)
            String rewrittenQuery = rewriteQuery(request.getQuestion());
            
            // 2. 向量检索相关文档
            List<Document> relevantDocs = retrieveRelevantDocuments(
                rewrittenQuery, 
                request.getKnowledgeBaseId(),
                request.getTopK(),
                request.getSimilarityThreshold()
            );
            
            // 3. 重排序和过滤
            List<Document> filteredDocs = rerankAndFilter(relevantDocs, request.getQuestion());
            
            // 4. 构建上下文
            String context = buildContext(filteredDocs);
            
            // 5. 获取对话历史
            String history = getConversationHistory(request.getConversationId());
            
            // 6. 构建提示词
            String prompt = buildPrompt(context, history, request.getQuestion());
            
            // 7. 调用大模型生成答案
            ChatResponse chatResponse = generateAnswer(prompt, request);
            
            // 8. 保存对话记录
            saveConversation(request, chatResponse, filteredDocs);
            
            // 9. 构建响应
            return buildResponse(chatResponse, filteredDocs, startTime);
            
        } catch (Exception e) {
            log.error("问答处理失败: {}", request.getQuestion(), e);
            return AnswerResponse.error("系统处理问题时发生错误,请稍后重试");
        }
    }
    
    /**
     * 查询重写:提升检索效果
     */
    private String rewriteQuery(String originalQuery) {
        // 简单的查询扩展和重写
        String rewritten = originalQuery.toLowerCase().trim();
        
        // 移除停用词(简单示例)
        String[] stopWords = {"的", "了", "和", "与", "或", "在", "关于"};
        for (String stopWord : stopWords) {
            rewritten = rewritten.replace(stopWord, " ");
        }
        
        // 同义词扩展(实际项目中可以使用词向量或知识图谱)
        Map<String, List<String>> synonyms = Map.of(
            "如何", Arrays.asList("怎样", "怎么", "如何操作"),
            "问题", Arrays.asList("故障", "错误", "异常"),
            "配置", Arrays.asList("设置", "参数", "选项")
        );
        
        StringBuilder expandedQuery = new StringBuilder(rewritten);
        for (Map.Entry<String, List<String>> entry : synonyms.entrySet()) {
            if (rewritten.contains(entry.getKey())) {
                for (String synonym : entry.getValue()) {
                    expandedQuery.append(" ").append(synonym);
                }
            }
        }
        
        return expandedQuery.toString().trim();
    }
    
    /**
     * 检索相关文档
     */
    private List<Document> retrieveRelevantDocuments(String query,
                                                    String knowledgeBaseId,
                                                    int topK,
                                                    Double similarityThreshold) {
        FilterExpressionBuilder filterBuilder = new FilterExpressionBuilder();
        
        // 构建过滤条件
        FilterExpression filter;
        if (StringUtils.hasText(knowledgeBaseId)) {
            filter = filterBuilder.eq("knowledge_base_id", knowledgeBaseId).build();
        } else {
            filter = filterBuilder.alwaysTrue().build();
        }
        
        // 构建搜索请求
        SearchRequest searchRequest = SearchRequest.builder()
            .query(query)
            .topK(topK * 2) // 检索更多结果用于后续重排序
            .similarityThreshold(similarityThreshold != null ? similarityThreshold : 0.7)
            .filterExpression(filter)
            .build();
        
        return vectorStore.similaritySearch(searchRequest);
    }
    
    /**
     * 重排序和过滤
     */
    private List<Document> rerankAndFilter(List<Document> documents, String question) {
        if (documents.isEmpty()) {
            return documents;
        }
        
        // 1. 基于BM25的文本相关性重排序
        List<Document> bm25Reranked = bm25Rerank(documents, question);
        
        // 2. 基于元数据的重要性评分
        List<Document> metadataScored = scoreByMetadata(bm25Reranked);
        
        // 3. 去重(基于内容相似度)
        List<Document> deduplicated = deduplicate(metadataScored);
        
        // 4. 选择最相关的topK个文档
        return deduplicated.stream()
            .limit(10) // 最终返回10个最相关的文档
            .collect(Collectors.toList());
    }
    
    /**
     * 构建上下文
     */
    private String buildContext(List<Document> documents) {
        if (documents.isEmpty()) {
            return "暂无相关文档内容。";
        }
        
        StringBuilder context = new StringBuilder();
        for (int i = 0; i < documents.size(); i++) {
            Document doc = documents.get(i);
            context.append("【文档片段 ").append(i + 1).append("】\n");
            context.append("内容:").append(doc.getText()).append("\n");
            
            // 添加元数据信息
            Map<String, Object> metadata = doc.getMetadata();
            if (metadata.containsKey("title")) {
                context.append("来源:").append(metadata.get("title")).append("\n");
            }
            if (metadata.containsKey("page")) {
                context.append("页码:").append(metadata.get("page")).append("\n");
            }
            context.append("\n");
        }
        
        return context.toString();
    }
    
    /**
     * 构建提示词
     */
    private String buildPrompt(String context, String history, String question) {
        Map<String, Object> variables = new HashMap<>();
        variables.put("context", context);
        variables.put("history", StringUtils.hasText(history) ? history : "无历史对话");
        variables.put("question", question);
        
        // 使用模板引擎渲染提示词
        StringTemplate template = new StringTemplate(SYSTEM_PROMPT_TEMPLATE);
        return template.render(variables);
    }
    
    /**
     * 生成答案
     */
    private ChatResponse generateAnswer(String prompt, QuestionRequest request) {
        Prompt chatPrompt = new Prompt(
            List.of(
                new SystemMessage(prompt),
                new UserMessage(request.getQuestion())
            ),
            Map.of(
                "temperature", request.getTemperature() != null ? request.getTemperature() : 0.1,
                "max_tokens", request.getMaxTokens() != null ? request.getMaxTokens() : 2000,
                "top_p", 0.9
            )
        );
        
        return chatClient.call(chatPrompt);
    }
    
    /**
     * 构建响应
     */
    private AnswerResponse buildResponse(ChatResponse chatResponse,
                                        List<Document> sourceDocuments,
                                        long startTime) {
        long processingTime = System.currentTimeMillis() - startTime;
        
        List<AnswerResponse.SourceDocument> sources = sourceDocuments.stream()
            .map(doc -> AnswerResponse.SourceDocument.builder()
                .content(doc.getText())
                .metadata(doc.getMetadata())
                .build())
            .collect(Collectors.toList());
        
        return AnswerResponse.builder()
            .answer(chatResponse.getResult().getOutput().getContent())
            .sources(sources)
            .processingTimeMs(processingTime)
            .documentCount(sourceDocuments.size())
            .timestamp(Instant.now())
            .build();
    }
    
    // 其他辅助方法...
}

这个问答服务有几个关键优化点:

  1. 查询重写:通过同义词扩展和停用词处理,提升检索召回率
  2. 多阶段检索:先进行向量检索,再进行文本重排序,结合两者的优势
  3. 上下文构建:精心设计的上下文格式,帮助大模型更好地理解文档内容
  4. 对话历史管理:支持多轮对话,保持对话的连贯性

3.4 高级功能:混合检索与查询优化

在实际企业应用中,单纯的向量检索可能无法满足所有需求。下面实现一个混合检索策略,结合向量检索和传统全文检索:

@Component
@Slf4j
public class HybridRetrievalService {
    
    private final VectorStore vectorStore;
    private final ElasticsearchRestTemplate elasticsearchTemplate;
    private final EmbeddingModel embeddingModel;
    
    // 权重配置:向量检索 vs 全文检索
    private static final double VECTOR_WEIGHT = 0.7;
    private static final double TEXT_WEIGHT = 0.3;
    
    public HybridRetrievalService(VectorStore vectorStore,
                                 ElasticsearchRestTemplate elasticsearchTemplate,
                                 EmbeddingModel embeddingModel) {
        this.vectorStore = vectorStore;
        this.elasticsearchTemplate = elasticsearchTemplate;
        this.embeddingModel = embeddingModel;
    }
    
    /**
     * 混合检索:结合向量检索和全文检索
     */
    public List<HybridSearchResult> hybridSearch(String query,
                                                String knowledgeBaseId,
                                                int topK) {
        // 并行执行两种检索
        CompletableFuture<List<VectorSearchResult>> vectorFuture = 
            CompletableFuture.supplyAsync(() -> vectorSearch(query, knowledgeBaseId, topK * 2));
        
        CompletableFuture<List<TextSearchResult>> textFuture = 
            CompletableFuture.supplyAsync(() -> textSearch(query, knowledgeBaseId, topK * 2));
        
        // 等待结果并合并
        try {
            List<VectorSearchResult> vectorResults = vectorFuture.get();
            List<TextSearchResult> textResults = textFuture.get();
            
            return mergeResults(vectorResults, textResults, topK);
            
        } catch (InterruptedException | ExecutionException e) {
            log.error("混合检索失败", e);
            // 降级:只使用向量检索
            return vectorSearch(query, knowledgeBaseId, topK).stream()
                .map(r -> HybridSearchResult.fromVectorResult(r))
                .collect(Collectors.toList());
        }
    }
    
    /**
     * 向量检索
     */
    private List<VectorSearchResult> vectorSearch(String query,
                                                 String knowledgeBaseId,
                                                 int topK) {
        FilterExpression filter = new FilterExpressionBuilder()
            .eq("knowledge_base_id", knowledgeBaseId)
            .build();
        
        SearchRequest request = SearchRequest.builder()
            .query(query)
            .topK(topK)
            .filterExpression(filter)
            .build();
        
        List<Document> documents = vectorStore.similaritySearch(request);
        
        return documents.stream()
            .map(doc -> VectorSearchResult.builder()
                .id((String) doc.getMetadata().get("id"))
                .content(doc.getText())
                .metadata(doc.getMetadata())
                .vectorScore(doc.getScore())
                .build())
            .collect(Collectors.toList());
    }
    
    /**
     * 全文检索(使用Elasticsearch)
     */
    private List<TextSearchResult> textSearch(String query,
                                             String knowledgeBaseId,
                                             int topK) {
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.boolQuery()
                .must(QueryBuilders.matchQuery("content", query))
                .filter(QueryBuilders.termQuery("knowledge_base_id", knowledgeBaseId))
            )
            .withPageable(PageRequest.of(0, topK))
            .build();
        
        SearchHits<DocumentIndex> searchHits = 
            elasticsearchTemplate.search(searchQuery, DocumentIndex.class);
        
        return searchHits.stream()
            .map(hit -> TextSearchResult.builder()
                .id(hit.getContent().getId())
                .content(hit.getContent().getContent())
                .metadata(hit.getContent().getMetadata())
                .textScore(hit.getScore())
                .build())
            .collect(Collectors.toList());
    }
    
    /**
     * 结果合并与重排序
     */
    private List<HybridSearchResult> mergeResults(List<VectorSearchResult> vectorResults,
                                                 List<TextSearchResult> textResults,
                                                 int topK) {
        // 创建文档ID到结果的映射
        Map<String, HybridSearchResult> resultMap = new HashMap<>();
        
        // 处理向量检索结果
        for (int i = 0; i < vectorResults.size(); i++) {
            VectorSearchResult vr = vectorResults.get(i);
            double normalizedVectorScore = 1.0 - (i * 1.0 / vectorResults.size());
            
            HybridSearchResult result = resultMap.getOrDefault(vr.getId(),
                HybridSearchResult.builder()
                    .id(vr.getId())
                    .content(vr.getContent())
                    .metadata(vr.getMetadata())
                    .build());
            
            result.setVectorScore(vr.getVectorScore());
            result.setCombinedScore(result.getCombinedScore() + 
                normalizedVectorScore * VECTOR_WEIGHT);
            resultMap.put(vr.getId(), result);
        }
        
        // 处理全文检索结果
        for (int i = 0; i < textResults.size(); i++) {
            TextSearchResult tr = textResults.get(i);
            double normalizedTextScore = 1.0 - (i * 1.0 / textResults.size());
            
            HybridSearchResult result = resultMap.getOrDefault(tr.getId(),
                HybridSearchResult.builder()
                    .id(tr.getId())
                    .content(tr.getContent())
                    .metadata(tr.getMetadata())
                    .build());
            
            result.setTextScore(tr.getTextScore());
            result.setCombinedScore(result.getCombinedScore() + 
                normalizedTextScore * TEXT_WEIGHT);
            resultMap.put(tr.getId(), result);
        }
        
        // 按综合分数排序并返回topK
        return resultMap.values().stream()
            .sorted((a, b) -> Double.compare(b.getCombinedScore(), a.getCombinedScore()))
            .limit(topK)
            .collect(Collectors.toList());
    }
    
    /**
     * 查询扩展:使用嵌入模型生成相关查询
     */
    public List<String> queryExpansion(String originalQuery, int expansionCount) {
        // 生成原始查询的嵌入向量
        float[] queryEmbedding = embeddingModel.embed(originalQuery);
        
        // 在向量空间中寻找相似的查询(需要预先存储查询向量)
        SearchRequest request = SearchRequest.builder()
            .query(originalQuery)
            .topK(expansionCount)
            .filterExpression(new FilterExpressionBuilder()
                .eq("type", "query")
                .build())
            .build();
        
        List<Document> similarQueries = vectorStore.similaritySearch(request);
        
        // 提取相似的查询文本
        List<String> expandedQueries = new ArrayList<>();
        expandedQueries.add(originalQuery);
        
        for (Document doc : similarQueries) {
            String similarQuery = doc.getText();
            if (!similarQuery.equals(originalQuery)) {
                expandedQueries.add(similarQuery);
            }
        }
        
        return expandedQueries;
    }
    
    // 内部结果类定义
    @Data
    @Builder
    public static class HybridSearchResult {
        private String id;
        private String content;
        private Map<String, Object> metadata;
        private Double vectorScore;
        private Float textScore;
        @Builder.Default
        private double combinedScore = 0.0;
        
        public static HybridSearchResult fromVectorResult(VectorSearchResult vr) {
            return HybridSearchResult.builder()
                .id(vr.getId())
                .content(vr.getContent())
                .metadata(vr.getMetadata())
                .vectorScore(vr.getVectorScore())
                .combinedScore(vr.getVectorScore())
                .build();
        }
    }
    
    @Data
    @Builder
    private static class VectorSearchResult {
        private String id;
        private String content;
        private Map<String, Object> metadata;
        private Double vectorScore;
    }
    
    @Data
    @Builder
    private static class TextSearchResult {
        private String id;
        private String content;
        private Map<String, Object> metadata;
        private Float textScore;
    }
}

混合检索策略的优势在于:

  1. 召回率提升:结合语义理解和关键词匹配,覆盖更多相关文档
  2. 精度优化:通过加权融合两种检索结果,得到更准确的排序
  3. 容错能力强:当一种检索方式失败时,可以降级使用另一种方式
  4. 查询扩展:通过向量相似性找到相关查询,提升检索效果

4. 性能优化与生产环境部署

构建企业级系统时,性能优化和生产就绪性同样重要。下面分享一些在实际项目中验证过的优化策略。

4.1 向量检索性能优化

Milvus的性能优化需要从多个维度考虑。以下是一些关键优化点:

@Configuration
public class MilvusPerformanceConfig {
    
    /**
     * 优化Milvus索引配置
     */
    @Bean
    @Profile("production")
    public IndexParam optimizedIndexParam() {
        // 根据数据规模选择合适的索引参数
        long estimatedDataSize = estimateDataSize(); // 预估数据量
        
        Map<String, Object> indexParams = new HashMap<>();
        
        if (estimatedDataSize < 1_000_000) {
            // 小规模数据:使用HNSW获得最佳精度
            indexParams.put("M", 16);  // HNSW参数:每个节点的连接数
            indexParams.put("efConstruction", 200); // 构建时的搜索范围
            return IndexParam.builder()
                .indexType(IndexType.HNSW)
                .metricType(MetricType.COSINE)
                .params(indexParams)
                .build();
            
        } else if (estimatedDataSize < 10_000_000) {
            // 中等规模数据:使用IVF_FLAT平衡性能
            indexParams.put("nlist", 4096); // 聚类中心数量
            return IndexParam.builder()
                .indexType(IndexType.IVF_FLAT)
                .metricType(MetricType.COSINE)
                .params(indexParams)
                .build();
            
        } else {
            // 大规模数据:使用IVF_SQ8节省内存
            indexParams.put("nlist", 16384);
            return IndexParam.builder()
                .indexType(IndexType.IVF_SQ8)
                .metricType(MetricType.COSINE)
                .params(indexParams)
                .build();
        }
    }
    
    /**
     * 批量操作优化
     */
    @Bean
    public BulkOperationService bulkOperationService(MilvusServiceClient client) {
        return new BulkOperationService(client) {
            private final ExecutorService executor = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors() * 2
            );
            
            private final int BATCH_SIZE = 100; // 每批处理100个文档
            private final Semaphore semaphore = new Semaphore(10); // 并发控制
            
            @Override
            public CompletableFuture<Void> bulkAddDocuments(List<Document> documents) {
                List<CompletableFuture<Void>> futures = new ArrayList<>();
                
                // 分批处理
                for (int i = 0; i < documents.size(); i += BATCH_SIZE) {
                    int end = Math.min(i + BATCH_SIZE, documents.size());
                    List<Document> batch = documents.subList(i, end);
                    
                    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                        try {
                            semaphore.acquire();
                            vectorStore.add(batch);
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            throw new RuntimeException("批量添加中断", e);
                        } finally {
                            semaphore.release();
                        }
                    }, executor);
                    
                    futures.add(future);
                }
                
                return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
            }
        };
    }
    
    /**
     * 查询缓存优化
     */
    @Bean
    public CacheManager queryCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10_000) // 缓存最大条目数
            .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
            .recordStats() // 记录缓存统计信息
            .build());
        return cacheManager;
    }
    
    @Service
    public class CachedRetrievalService {
        
        private final VectorStore vectorStore;
        private final Cache<String, List<Document>> queryCache;
        
        public CachedRetrievalService(VectorStore vectorStore,
                                     CacheManager cacheManager) {
            this.vectorStore = vectorStore;
            this.queryCache = cacheManager.getCache("queryCache");
        }
        
        public List<Document> searchWithCache(String query,
                                             String knowledgeBaseId,
                                             int topK) {
            String cacheKey = generateCacheKey(query, knowledgeBaseId, topK);
            
            // 尝试从缓存获取
            List<Document> cachedResult = queryCache.get(cacheKey, List.class);
            if (cachedResult != null) {
                log.debug("缓存命中: {}", cacheKey);
                return cachedResult;
            }
            
            // 缓存未命中,执行实际查询
            FilterExpression filter = new FilterExpressionBuilder()
                .eq("knowledge_base_id", knowledgeBaseId)
                .build();
            
            SearchRequest request = SearchRequest.builder()
                .query(query)
                .topK(topK)
                .filterExpression(filter)
                .build();
            
            List<Document> results = vectorStore.similaritySearch(request);
            
            // 存入缓存
            queryCache.put(cacheKey, results);
            
            return results;
        }
        
        private String generateCacheKey(String query,
                                       String knowledgeBaseId,
                                       int topK) {
            return String.format("%s:%s:%d",
                knowledgeBaseId,
                DigestUtils.md5DigestAsHex(query.getBytes()),
                topK);
        }
    }
    
    /**
     * 监控与指标收集
     */
    @Component
    public class MilvusMetricsCollector {
        
        private final MeterRegistry meterRegistry;
        private final MilvusServiceClient client;
        
        // 性能指标
        private final DistributionSummary queryLatency;
        private final Counter queryCounter;
        private final Gauge collectionSize;
        
        public MilvusMetricsCollector(MeterRegistry meterRegistry,
                                     MilvusServiceClient client) {
            this.meterRegistry = meterRegistry;
            this.client = client;
            
            // 初始化指标
            this.queryLatency = DistributionSummary
                .builder("milvus.query.latency")
                .description("Milvus查询延迟")
                .baseUnit("milliseconds")
                .register(meterRegistry);
            
            this.queryCounter = Counter
                .builder("milvus.query.count")
                .description("Milvus查询次数")
                .register(meterRegistry);
            
            this.collectionSize = Gauge
                .builder("milvus.collection.size", this::getCollectionSize)
                .description("Milvus集合中的文档数量")
                .register(meterRegistry);
        }
        
        @Around("@annotation(MonitorMilvusQuery)")
        public Object monitorQuery(ProceedingJoinPoint joinPoint) throws Throwable {
            long startTime = System.currentTimeMillis();
            queryCounter.increment();
            
            try {
                Object result = joinPoint.proceed();
                long latency = System.currentTimeMillis() - startTime;
                queryLatency.record(latency);
                
                log.debug("Milvus查询完成,耗时: {}ms", latency);
                return result;
                
            } catch (Exception e) {
                meterRegistry.counter("milvus.query.errors").increment();
                throw e;
            }
        }
        
        private Double getCollectionSize() {
            try {
                // 获取集合统计信息
                // 实际实现需要根据Milvus API调整
                return 0.0;
            } catch (Exception e) {
                log.warn("获取集合大小失败", e);
                return -1.0;
            }
        }
    }
}

4.2 生产环境部署配置

生产环境部署需要考虑高可用、监控和灾备。以下是一个生产级别的配置示例:

# application-production.yml
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: ${OPENAI_BASE_URL:https://api.openai.com}
      chat:
        options:
          model: gpt-4-turbo-preview
          temperature: 0.1
          max-tokens: 2000
          timeout: 30s
          max-retries: 3
      embedding:
        options:
          model: text-embedding-3-small
          dimensions: 1536
          timeout: 30s
          max-retries: 3
    
    vectorstore:
      milvus:
        client:
          uri: ${MILVUS_URI:http://milvus-cluster:19530}
          token: ${MILVUS_TOKEN:}
          connect-timeout-ms: 10000
          keep-alive-time-ms: 55000
          keep-alive-timeout-ms: 20000
          secure: ${MILVUS_SECURE:false}
        database-name: ${MILVUS_DATABASE:production}
        collection-name: ${MILVUS_COLLECTION:documents}
        embedding-dimension: 1536
        index-type: IVF_FLAT
        metric-type: COSINE
        index-parameters: '{"nlist": 4096}'
        initialize-schema: false  # 生产环境设为false

# 连接池配置
milvus:
  pool:
    max-size: 20
    min-idle: 5
    max-wait: 5000
    validation-query: "SELECT 1"

# 监控配置
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true
    distribution:
      percentiles-histogram:
        http.server.requests: true
  tracing:
    sampling:
      probability: 0.1

# 日志配置
logging:
  level:
    org.springframework.ai: INFO
    io.milvus: WARN
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg%n"
  file:
    name: logs/document-qa.log
    max-size: 100MB
    max-history: 30

# 应用性能监控
app:
  monitoring:
    enabled: true
    # 慢查询阈值(毫秒)
    slow-query-threshold: 1000
    # 查询采样率
    query-sampling-rate: 0.1
    # 错误追踪
    error-tracking:
      enabled: true
      sentry-dsn: ${SENTRY_DSN:}

# 缓存配置
cache:
  type: redis
  redis:
    time-to-live: 600000  # 10分钟
    cache-null-values: false
    key-prefix: "docqa:"

4.3 Docker Compose部署配置

对于容器化部署,这里提供一个完整的Docker Compose配置:

# docker-compose.production.yml
version: '3.8'

services:
  # Milvus集群
  milvus-etcd:
    image: quay.io/coreos/etcd:v3.5.5
    environment:
      - ETCD_AUTO_COMPACTION_MODE=revision
      - ETCD_AUTO_COMPACTION_RETENTION=1000
      - ETCD_QUOTA_BACKEND_BYTES=4294967296
      - ETCD_SNAPSHOT_COUNT=50000
    volumes:
      - milvus-etcd-data:/etcd
    command: etcd -advertise-client-urls=http://milvus-etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd

  milvus-minio:
    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
    environment:
      MINIO_ACCESS_KEY: minioadmin
      MINIO_SECRET_KEY: minioadmin
    volumes:
      - milvus-minio-data:/minio_data
    command: minio server /minio_data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3

  milvus-standalone:
    image: milvusdb/milvus:v2.4.0
    command: ["milvus", "run", "standalone"]
    environment:
      ETCD_ENDPOINTS: milvus-etcd:2379
      MINIO_ADDRESS: milvus-minio:9000
    volumes:
      - milvus-data:/var/lib/milvus
    ports:
      - "19530:19530"
      - "9091:9091"
    depends_on:
      milvus-etcd:
        condition: service_healthy
      milvus-minio:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
      interval: 30s
      start_period: 90s
      timeout: 20s
      retries: 3

  # Redis缓存
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:redispass}
    volumes:
      - redis-data:/data
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3

  # MySQL数据库
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:rootpass}
      MYSQL_DATABASE: document_qa
      MYSQL_USER: appuser
      MYSQL_PASSWORD: ${MYSQL_PASSWORD:apppass}
    volumes:
      - mysql-data:/var/lib/mysql
      - ./config/mysql-init:/docker-entrypoint-initdb.d
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  # 应用服务
  document-qa-app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      SPRING_PROFILES_ACTIVE: production
      MILVUS_URI: http://milvus-standalone:19530
      REDIS_HOST: redis
      REDIS_PASSWORD: ${REDIS_PASSWORD:redispass}
      MYSQL_HOST: mysql
      MYSQL_PASSWORD: ${MYSQL_PASSWORD:apppass}
      OPENAI_API_KEY: ${OPENAI_API_KEY}
    ports:
      - "8080:8080"
    depends_on:
      milvus-standalone:
        condition: service_healthy
      redis:
        condition: service_healthy
      mysql:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure
        max_attempts: 3

  # Nginx负载均衡
  nginx:
    image: nginx:alpine
    volumes:
      - ./config/nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - document-qa-app

  # 监控栈
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./config/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      - '--storage.tsdb.retention.time=200h'
      - '--web.enable-lifecycle'

  grafana:
    image: grafana/grafana:latest
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:admin}
    volumes:
      - grafana-data:/var/lib/grafana
      - ./config/grafana-dashboards:/etc/grafana/provisioning/dashboards
    ports:
      - "3000:3000"
    depends_on:
      - prometheus

volumes:
  milvus-etcd-data:
  milvus-minio-data:
  milvus-data:
  redis-data:
  mysql-data:
  prometheus-data:
  grafana-data:

4.4 性能测试与调优建议

在实际部署前,进行充分的性能测试至关重要。以下是一些测试和调优的建议:

@SpringBootTest
@ActiveProfiles("test")
@Slf4j
public class PerformanceTest {
    
    @Autowired
    private IntelligentQAService qaService;
    
    @Autowired
    private VectorStore vectorStore;
    
    @Test
    void testQueryLatencyUnderLoad() {
        // 模拟并发查询
        int concurrentUsers = 50;
        int queriesPerUser = 100;
        ExecutorService executor = Executors.newFixedThreadPool(concurrentUsers);
        
        List<CompletableFuture<TestResult>> futures = new ArrayList<>();
        List<String> testQueries = loadTestQueries(); // 加载测试查询集
        
        for (int i = 0; i < concurrentUsers; i++) {
            futures.add(CompletableFuture.supplyAsync(() -> {
                TestResult result = new TestResult();
                
                for (int j = 0; j < queriesPerUser; j++) {
                    String query = testQueries.get(j % testQueries.size());
                    QuestionRequest request = QuestionRequest.builder()
                        .question(query)
                        .knowledgeBaseId("test_kb")
                        .topK(5)
                        .build();
                    
                    long startTime = System.nanoTime();
                    try {
                        AnswerResponse response = qaService.askQuestion(request);
                        long latency = (System.nanoTime() - startTime) / 1_000_000; // 毫秒
                        
                        result.recordSuccess(latency, response.getProcessingTimeMs());
                    } catch (Exception e) {
                        result.recordError();
                    }
                    
                    // 添加随机延迟,模拟真实用户行为
                    try {
                        Thread.sleep(ThreadLocalRandom.current().nextInt(100, 500));
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                
                return result;
            }, executor));
        }
        
        // 收集结果
        List<TestResult> results = futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
        
        // 分析性能指标
        analyzePerformance(results);
    }
    
    @Test
    void testVectorStorePerformance() {
        // 测试向量存储的吞吐量
        int batchSize = 1000;
        int totalDocuments = 10000;
        
        List<Document> testDocuments = generateTestDocuments(totalDocuments);
        
        // 批量插入性能测试
        long startTime = System.currentTimeMillis();
        vectorStore.add(testDocuments);
        long insertTime = System.currentTimeMillis() - startTime;
        
        log.info("插入 {} 个文档耗时: {} ms, 吞吐量: {:.2f} docs/sec",
            totalDocuments, insertTime, totalDocuments * 1000.0 / insertTime);
        
        // 查询性能测试
        List<String> testQueries = Arrays.asList(
            "如何配置系统参数",
            "故障排查步骤",
            "性能优化建议",
            "安全配置指南"
        );
        
        for (String query : testQueries) {
            long queryStart = System.nanoTime();
            List<Document> results = vectorStore.similaritySearch(
                SearchRequest.builder()
                    .query(query)
                    .topK(10)
                    .build()
            );
            long queryTime = (System.nanoTime() - queryStart) / 1_000_000;
            
            log.info("查询 '{}' 返回 {} 个结果,耗时: {} ms",
                query, results.size(), queryTime);
        }
    }
    
    /**
     * 根据测试结果给出调优建议
     */
    private void analyzePerformance(List<TestResult> results) {
        double avgLatency = results.stream()
            .flatMap(r -> r.successLatencies.stream())
            .mapToLong(Long::longValue)
            .average()
            .orElse(0.0);
        
        double p95Latency = calculatePercentile(
            results.stream()
                .flatMap(r -> r.successLatencies.stream())
                .collect(Collectors.toList()),
            95
        );
        
        double errorRate = results.stream()
            .mapToInt(r -> r.errorCount)
            .sum() * 100.0 / 
            (results.stream().mapToInt(r -> r.totalQueries).sum());
        
        log.info("性能测试结果:");
        log.info("平均延迟: {:.2f} ms", avgLatency);
        log.info("P95延迟: {:.2f} ms", p95Latency);
        log.info("错误率: {:.2f}%", errorRate);
        
        // 给出调优建议
        if (p95Latency > 1000) {
            log.warn("P95延迟超过1秒,建议优化:");
            log.warn("1. 检查Milvus索引配置,考虑使用HNSW索引");
            log.warn("2. 增加查询缓存命中率");
            log.warn("3. 优化文档分块策略,减少单个块的大小");
            log.warn("4. 考虑使用GPU加速向量计算");
        }
        
        if (errorRate > 5.0) {
            log.warn("错误率超过5%,建议检查:");
            log.warn("1. Milvus连接池配置");
            log.warn("2. 网络延迟和稳定性");
            log.warn("3. 系统资源使用情况(CPU、内存)");
        }
    }
    
    @Data
    private static class TestResult {
        private List<Long> successLatencies = new ArrayList<>();
        private List<Long> processingTimes = new ArrayList<>();
        private int errorCount = 0;
        private int totalQueries = 0;
        
        void recordSuccess(long latency, long processingTime) {
            successLatencies.add(latency);
            processingTimes.add(processingTime);
            totalQueries++;
        }
        
        void recordError() {
            errorCount++;
            totalQueries++;
        }
    }
}

基于多年的项目经验,我总结了一些关键的调优建议:

索引优化策略:

  • 数据量小于100万时,使用HNSW索引获得最佳精度
  • 数据量在100万到1000万之间,使用IVF_FLAT平衡性能和精度
  • 数据量超过1000万,考虑使用IVF_SQ8或IVF_PQ节省内存
  • 定期重建索引以保持检索效率

查询优化技巧:

  • 使用nprobe参数调整查询精度和性能的平衡
  • 对于实时性要求高的场景,适当降低similarityThreshold
  • 使用查询缓存减少重复计算
  • 批量处理查询请求,减少网络开销

系统级优化:

  • 为Milvus分配足够的内存,特别是对于HNSW索引
  • 使用SSD存储提升IO性能
  • 考虑使用GPU加速向量计算(如果Milvus版本支持)
  • 监控系统资源使用情况,及时扩容

应用层优化:

  • 实现请求队列和限流机制
  • 使用连接池管理Milvus连接
  • 异步处理耗时的文档处理任务
  • 实现分级缓存策略(内存缓存 + Redis缓存)

在实际项目中,我发现最有效的优化往往是结合业务特点进行的。比如,对于法律文档问答系统,我们通过调整分块策略(按章节而不是固定长度分块)和优化提示词模板,将回答准确率从75%提升到了92%。对于客服知识库系统,通过实现智能查询重写和上下文感知检索,将首次解决率提高了35%。

这些优化不是一蹴而就的,需要持续的监控、测试和调整。建议建立完善的监控体系,定期进行性能测试,根据实际使用情况不断优化系统配置和算法参数。

Spring AI ETL进阶:定制中文元数据增强与Milvus向量化存储实战 本文深入探讨了Spring AI ETL框架在中文文本处理中的进阶应用,重点介绍了定制中文元数据增强与Milvus向量化存储的实战技巧。通过优化中文摘要生成器、关键词提取和向量维度配置,显著提升了中文文本处理的准确性和效率。文章还分享了完整的中文ETL流水线搭建经验,包括文本分块、质量监控和性能优化策略,为开发者提供了实用的参考方案。 阅读详情

相关推荐

基于SpringAI与Vue3的智能运维AI助手开发平台_聚焦运维场景的AI智能助手兼具技术应用与开发实践平台属性_实现多模态大模型支持智能体架构知识库增强工具调用系统模型上下文管.zip

基于SpringAI与Vue3的智能运维AI助手开发平台_聚焦运维场景的AI智能助手兼具技术应用与开发实践平台属性_实现多模态大模型支持智能体架构知识库增强工具调用系统模型上下文管.zip

AI应用向量化存储:基于Spring AI Alibaba的向量数据库集成方案

随着人工智能技术在企业级应用中的深入落地,传统基于关键词的搜索技术已难以满足智能应用对语义理解的需求。Spring AI Alibaba作为阿里巴巴开源的AI应用开发框架,通过统一的向量存储接口为开发者提供了从简单内存存储到专业向量数据库的全链路解决方案,帮助企业构建基于语义搜索的智能应用系统。本文将深入探讨技术决策者面临的挑战、Spring AI Alibaba的向量数据库集成方案、实施路径选择

gitblog_00066的博客 349

政府科技管理者如何提升科技成果转化项目进场交易的备案效率?.docx

政府科技管理者如何提升科技成果转化项目进场交易的备案效率?

Spring AI 实战:RAG 资料上传、分词与向量化全链路实现

本文介绍了基于Spring AI、Milvus和阿里云OSS的RAG系统文档入库全流程。系统通过多环节协同工作实现知识向量化:用户上传文件后,原件存储至OSS;使用Tika解析文档为纯文本,经TokenTextSplitter分块处理;调用Embedding模型生成向量并存入Milvus;最后在MySQL保存文件名、OSS URL与向量ID的映射关系。架构设计强调原件保留、ID映射和解耦存储三大原则,确保后续检索质量。技术栈包含Apache Tika文档解析、Milvus向量数据库和通义千问Embeddin

小工匠 869

ScanNet数据集详解

ScanNet 是一个 RGB-D 视频数据集,在超过 1500 次扫描中包含了 250 万个视图,并标注了 3D 相机位姿、表面重建和实例级的语义分割。为了收集这些数据,设计了一个易于使用且具有扩展性的 RGB-D 捕获系统,包括自动化的表面重建和众包的语义标注。如果您想下载 ScanNet 数据,请使用您的机构邮箱地址填写一份 ScanNet 使用条款协议,并发送至 scannet@googlegroups.com。压缩包是将3D的标注信息“拍扁”成2D图片的结果,对应原始视频的每一帧。

frostmelody 全网同名,大家多多关注呀~ 持续分享优质内容! 3923

TEC控制器芯片 ADN8835 中文数据手册

ADN8835是一款高集成度的单芯片TEC控制器。它包括线性电压调节模块脉冲宽度调制(PWM) 电压调节模块和两个零漂移、轨到轨斩波放大器。线性控制器和PWM驱动器用于控制H桥配置中的内部功率MOSFET。通过测量热传感器反馈电压,并使用集成的运算放大器作为比例-积分-微分(PID)补偿器来调理信号,ADN8835通过TEC驱动电流,将连接至TEC模块的激光二极管或无源组件的温度稳定到编程的目标温度。

lu2289504634的博客 9540

Spring AI系列之集成 Milvus 构建 RAG 智能问答系统

本文介绍了基于Spring AI和Milvus构建RAG智能问答系统的实践方案。RAG技术通过先检索后生成的方式,解决了大语言模型的幻觉问题和知识盲区。系统采用Spring Boot 3.5.11作为后端框架,结合Milvus 2.6.0向量数据库实现语义检索功能。文章详细讲解了Milvus的RPM安装配置步骤、系统架构设计以及项目搭建流程,包括环境检查、服务管理、网络配置等关键环节。最后给出了Spring Boot项目的标准结构和Maven依赖配置,为开发者提供了完整的实现参考。该系统能够有效利用私有知识

Nicky's blog 918

RL调参侠之BipedalWalker BipedalWalkerHardcore SAC

前几天用PPO训练了个很一般的agent: RL调参侠之BipedalWalker PPO 我看SpinningUp上面说PPO和SAC是两个领域里各自的State_of_the_art的方法,所以就打算跑SAC试试看,之前因为Open AI Five的原因,一直感觉PPO很牛掰,实际上现在很多自动驾驶的算法也是用PPO在跑,毕竟有一个“必定能够提升期望”的理论保证在这里,我理解这对做项目很大的诱惑。 不过我跑出来的结果明显是SAC更好。 调参侠时间 策略熵的权重alpha用的0.2,我一开始没有看SAC的

weixin_48370148的博客 2101

Spring AI与RAG技术实战:构建企业级智能文档问答系统

Spring AI是Spring生态系统中的AI集成框架,提供了统一的API来访问各种AI模型和服务。它支持多种AI提供商,包括OpenAI、Google AI、Azure AI等,并提供了丰富的功能如提示工程、向量化、工具调用等。@Component本文详细介绍了如何使用Spring AI和RAG技术构建企业级智能文档问答系统。通过结合向量检索和大语言模型生成,我们能够构建出既准确又可靠的问答系统。多模态支持:支持图片、表格等非文本内容的处理实时更新。

intwei的博客 757

VTK实战笔记(1)在win11搭建VTK-9.4.2 + qt5.15.2 + VS2019_x64开发环境

Visual Studio 2019 + qt5.15.2 + vtk9.4.2

心向阳光,斗志昂扬! 978

【报错解决】OpenClaw 报错 Unsupported engine: requires node >=22.0.0 —— Node.js 版本过低的完整排查与解决实录

在国产操作系统环境下部署OpenClaw时遇到"Unsupported engine: requires node >=22.0.0"报错,分析表明该工具要求Node.js 22+以支持fetch API等新特性。解决方案包括:1) 使用nvm升级Node至22+版本;2) Docker环境中改用node:22-alpine镜像;3) 不推荐强制忽略引擎检查。文章强调遵循引擎要求的重要性,指出环境版本是工程化体系稳定运行的关键,建议通过规范升级解决兼容性问题。(149字)

博客 608

【SpringAI】实战:利用Milvus向量检索构建智能问答系统

本文详细介绍了如何利用SpringAI框架与Milvus向量数据库构建一个检索增强生成(RAG)智能问答系统。通过实战演示,从环境搭建、核心配置、知识向量化入库,到集成大语言模型生成答案,完整覆盖了基于向量检索的智能问答系统开发流程,并提供了性能优化与生产级部署的进阶考量。

iii12的专栏 431

195、大模型蒸馏与压缩:知识蒸馏与VLA模型轻量化

折腾完这一整套流程,最大的感悟是:知识蒸馏不是简单的模型压缩,而是知识迁移的工程艺术。每一步都有坑,每个参数都可能影响最终性能。给后来者的建议是:先别急着上蒸馏,把teacher模型的推理延迟和显存占用测清楚,确定瓶颈在哪里——是计算量太大还是内存带宽不够,这决定了你该用蒸馏还是量化还是剪枝。蒸馏训练时一定要监控每一层的特征对齐loss,别只看总loss。如果某一层的对齐loss特别高,说明student在这一层学歪了,需要调整该层的损失权重或者增加该层的投影头容量。

sundong_81的博客 113

Spring AI与Milvus实战:5分钟搞定向量存储配置(含常见错误排查)

本文详细介绍了如何在Spring Boot项目中集成Spring AI与Milvus,快速配置MilvusVectorStore以实现企业级向量搜索。文章重点解析了核心配置项(如embedding维度、索引类型)的避坑指南,并提供了批量操作、元数据过滤等实战代码技巧,帮助Java开发者稳健构建高可用的语义搜索服务。

echo99的专栏 773

人工智能技术基础系列之:迁移学习

作者:禅与计算机程序设计艺术 1.背景介绍 随着深度学习和神经网络的快速发展,越来越多的应用场景需要处理大量的数据和复杂的任务。然而,由于计算资源、存储空间等条件的限制,传统的人工智能方法往往不能满足实际需求。因此,为了提高效率和降低成本,研究人员提出了迁移学习的概念。本篇文章将详细介绍迁移学习的基

AI天才研究院 700

AI智算-K8s+vLLM & Ray:DeepSeek-r1 671B 满血版分布式推理部署实践

自从上次发布 【AI-智算】K8s+SGLang实战:DeepSeek-r1:671b满血版多机多卡私有化部署全攻略] 文章后,本次将演示另一个主流推理引擎工具——vLLM,结合K8s、LWS、Ray、Volcano等技术栈,部署DeepSeek-r1 671b 满血版分布式推理集群,并对比其与SGLang推理引擎的性能表现。

Kason_iWiki 1826

SpringAI实战:Java工程师的RAG落地指南

RAG(检索增强生成)是当前大模型应用的核心范式,其本质是将外部知识库与大语言模型能力安全、可控地协同。实现RAG需理解向量检索原理、Embedding生成机制及Prompt工程逻辑,技术价值在于突破模型幻觉、提升回答准确率与业务可解释性。典型应用场景包括智能客服、知识库问答和代码辅助系统。对Java工程师而言,关键挑战在于如何将Python生态中快速验证的RAG流程,迁移至SpringBoot企业级架构中——既要复用MyBatis、PostgreSQL、JdbcTemplate等成熟组件,又要无缝集成Op

weixin_34198881的博客 106

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

互联网大厂Java面试实战:从Spring Cloud微服务到AI RAG智能客服(含完整问答解析)

通过一个互联网大厂本地生活服务+智能客服事业部的Java面试故事,串起三轮面试:从Java基础、Spring Boot、MyBatis与Redis缓存,到Spring Cloud微服务、Kafka消息队列、Spring Security安全,再到Spring AI、RAG、向量数据库、Agent智能体与会话记忆。文章最后附详细解析,帮助初学者系统理解业务场景与技术实现,适合准备大厂后端与AI融合岗位的同学阅读。

qq_57418183的博客 212

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

spring-ai使用Document存储至milvus的数据结构

2、初始化的milvus的collection结构如下。

zsj777的专栏 512

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

Spring AI + Milvus 构建企业级RAG知识库:从基础落地到性能优化(附完整源码)

基于 Spring AI 构建企业级 RAG 知识库,实现大模型精准问答,落地 Java 技术栈的 AI 检索增强方案。

学海无涯,笃行致远 551

项目管理培训记录

项目管理培训记录     今天进行了项目管理培训,在公司也待了近一年了,这次的培训比第一次的项目管理培训要更吃透些。     项目的两个主要特点:temporary & unique     项目管理(PM)主要做的是:通过管理,制作一个满足客户的产品(由客户提出需求)。     项目管理的要点: plan + review     公司组织结构对项目的影响:弱矩阵型比

http://www.cnlogs.com/ubunoon --- ubunoon blog 1050

Spring AI 2.0 企业级 RAG 架构:混合检索、重排序与多模态知识库

本文介绍了基于Spring AI 2.0构建企业级RAG(检索增强生成)架构的关键技术。主要内容包括: 混合检索策略:结合向量检索(语义理解)和BM25(关键词匹配)的优势,通过倒数排名融合(RRF)算法实现高效检索,实测召回率提升31% 重排序技术:在初始检索后使用交叉编码器模型对结果进行精细排序,显著提升Top结果的准确性 多模态知识库支持:应对企业复杂场景需求 Spring AI实现方案: 自定义HybridDocumentRetriever实现混合检索 通过Advisor模式集成重排序功能 提供异步

模界 | AI工程化实践者 1119

Spring AI + Milvus 自建:如何支持 10 亿级向量检索?(附 Docker 部署方案)

Milvus 是功能最强大的开源向量数据库,支持十亿级向量、分布式集群、多种索引类型。本文从零基础讲解 Milvus Standalone(单机)和 Cluster(集群)两种部署模式,提供完整的 Docker Compose 配置文件、Kubernetes Helm Chart 部署指南、Spring AI 集成代码、性能调优技巧。通过实测 1000 万向量的性能数据,展示如何优化 HNSW/IVF_FLAT 索引参数、配置负载均衡、实现高可用架构。掌握这些技能,你将能够构建企业级大规模向量检索系统。

腾讯云创作之星、华为云云享专家,深耕电商全栈高并发系统实战,分享可商用商城项目源码。 1393

Spring AI + Milvus实战:5步搭建企业级智能客服(含PDF文档处理技巧)

本文详细介绍了如何利用Spring AI和Milvus构建企业级智能客服系统,重点讲解了RAG架构的应用和PDF文档处理技巧。通过5个实战步骤,包括技术选型、PDF处理、知识库构建、问答系统实现及部署监控,帮助企业快速搭建高效、精准的智能问答系统,显著提升客服效率和质量。

tgb34567890的博客 876

实战|Spring +Milvus,Java也能实现的企业级文档问答RAG

在企业数字化转型的浪潮中,PDF、Word 等海量文档往往沉睡在各个业务系统中,形成“数据孤岛”,难以被智能系统高效利用。 那么,如何构建一个真正面向企业场景落地的 AI 应用,让 AI 成为企业的“智能助手”?本项目提供一套基于 Spring 框架的完整解决方案,结合文档 ETL、向量检索与 RAG 问答技术,覆盖从数据导入到智能对话的全链路实践。

Python_cocola的博客 1400
上一篇: 从Embedding到RoPE:图解Transformer位置编码的进化之路
下一篇: 实测对比:华为云vs清华源在openEuler 20.03 LTS下的换源速度与稳定性(含UKUI桌面安装优化)
Python
博客等级 码龄26年 1973粉丝 1567原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值