1. 为什么企业需要的不是 Demo,而是可运营的 RAG 平台

很多团队第一次接触 RAG,往往从一个最小闭环开始:

  1. 文档切块
  2. 向量化
  3. 向量检索
  4. 拼 Prompt
  5. 调大模型生成答案

这个流程本身没有问题,但它只能证明“技术可行”,不能证明“业务可用”。

企业场景真正关注的是下面这些问题:

  • • 文档每天都在更新,如何保证知识新鲜度
  • • 多部门、多租户同时接入,如何隔离权限与数据
  • • 高峰期问题暴增,如何保证低延迟和稳定性
  • • LLM 输出有不确定性,如何降低幻觉和错误引用
  • • 线上故障时如何降级,不能让客服系统整体不可用
  • • 如何评估检索效果与答案质量,而不是只看主观体验
  • • 如何把 RAG 从一个功能点,演进为企业级知识能力底座

因此,企业级 RAG 的目标不是“回答一个问题”,而是构建一个完整的知识服务系统:

  • • 上游要支持多源知识接入
  • • 中台要支持索引、检索、重排、治理与评估
  • • 下游要支持 API、客服工作台、搜索、Copilot、Agent 等多种消费方式

从架构视角看,RAG 不是一个模型调用问题,而是一个“知识数据工程 + 检索系统工程 + LLM 应用工程”的组合问题。


  1. RAG 的核心原理与系统边界

2.1 RAG 到底解决了什么问题

大模型擅长语言理解和生成,但天然存在三个局限:

  • • 参数知识滞后,无法实时掌握企业最新文档
  • • 对企业私域知识无记忆
  • • 面对事实性问题容易产生幻觉

RAG 的本质是把“知识记忆”从模型参数中外置出来,让模型在回答前先查资料,再基于资料作答。

它的本质链路可以抽象为:

用户问题  -> Query 理解与改写  -> 检索召回  -> 重排过滤  -> 上下文压缩  -> Prompt 装配  -> LLM 生成  -> 引用与结果后处理

2.2 企业级 RAG 的五层结构

接入层:Web / App / OpenAPI / Agent / IM Bot服务层:问答服务 / 文档摄入服务 / 会话服务 / 评估服务检索层:Query Rewrite / Recall / Rerank / Context Builder存储层:对象存储 / PostgreSQL + PGVector / Redis / Kafka模型层:Embedding Model / Rerank Model / Chat Model / Guard Model

2.3 RAG 不是万能的

以下问题不适合直接用基础 RAG 解决:

  • • 需要复杂步骤规划的任务型问题
  • • 需要调用多个系统完成动作的场景
  • • 需要事务一致性的业务写操作
  • • 需要精确计算和规则校验的领域

更合理的边界是:

  • • RAG 负责“找知识、给依据、辅助判断”
  • • Workflow / Agent 负责“调系统、做决策、执行动作”
  • • 规则引擎负责“强约束、合规校验、确定性判断”

  1. 企业级架构设计:从单体问答到平台化能力中心

3.1 总体架构

┌─────────────────────┐                               │   Web / App / API   │                               └─────────┬───────────┘                                         │                               ┌─────────▼───────────┐                               │     API Gateway     │                               │ 鉴权 / 限流 / 灰度   │                               └─────────┬───────────┘                                         │         ┌───────────────────────────────┼───────────────────────────────┐         │                               │                               │┌────────▼────────┐             ┌────────▼────────┐             ┌────────▼────────┐│  rag-query      │             │  rag-ingest     │             │ rag-session     ││ 查询与回答服务   │             │ 文档摄入与索引   │             │ 会话与记忆服务   │└────────┬────────┘             └────────┬────────┘             └────────┬────────┘         │                               │                               │         │                               │                               │         │                     ┌─────────▼─────────┐                     │         │                     │       Kafka       │                     │         │                     │ 异步任务 / 重试 / DLQ │                 │         │                     └─────────┬─────────┘                     │         │                               │                               │┌────────▼────────┐             ┌────────▼────────┐             ┌────────▼────────┐│ PostgreSQL      │             │ Redis Cluster   │             │ Object Storage  ││ 业务元数据 + 向量 │             │ 缓存 / 会话 / 限流 │             │ 原始文档 / 切块产物 ││ PGVector        │             └─────────────────┘             └─────────────────┘└────────┬────────┘         │┌────────▼────────────────────────────────────────────────────────────────┐│ Embedding Model / Rerank Model / Chat Model / Safety & Audit Model     │└─────────────────────────────────────────────────────────────────────────┘

3.2 核心服务拆分原则

当文档接入量与查询量都上来后,单服务架构通常会出现两个问题:

  • • 查询线程被摄入任务挤占
  • • 模型调用、向量写入、文档解析耦合在一个进程内,扩容策略不一致

推荐拆分为四类服务:

    1. rag-gateway
      统一入口,负责鉴权、租户隔离、流量控制、AB 实验、接口聚合。
    1. rag-query-service
      只负责在线链路,目标是低延迟、可横向扩展、尽量无状态。
    1. rag-ingest-service
      负责文档上传、解析、切块、嵌入、索引构建,天然适合异步化。
    1. rag-admin/eval-service
      负责知识集管理、离线评估、数据标注、召回分析与运行报表。

3.3 核心设计原则

  • • 查询链路和摄入链路分离
  • • 在线服务尽量无状态
  • • 文档处理异步化、可重试、可回放
  • • 向量索引与业务元数据统一治理
  • • 指标、日志、链路追踪从第一天就接入
  • • 每一个模型调用都应当可限流、可熔断、可降级

  1. 技术选型:为什么选择 Spring Boot 3 + LangChain4j + PGVector

4.1 Spring Boot 3 的价值

对于企业 Java 团队,Spring Boot 3 的优势非常明显:

  • • 与现有微服务体系兼容
  • • 配置治理、监控、鉴权、事务、消息中间件生态成熟
  • • 团队学习成本低,工程规范容易统一
  • • 易于接入 Nacos、Redis、Kafka、Prometheus、K8s 等基础设施

4.2 LangChain4j 的价值

LangChain4j 并不是“帮你少写几行代码”那么简单,它的价值在于把 LLM 应用常见能力抽象为 Java 友好的组件:

  • • ChatModel、EmbeddingModel、StreamingChatModel 等接口清晰
  • • 与 Spring 集成自然
  • • 支持 AI Services 声明式风格
  • • 支持 Memory、Retriever、Tools 等能力组合
  • • 便于替换模型供应商,降低厂商耦合

4.3 为什么企业场景可以优先考虑 PGVector

很多人第一反应是直接上专业向量库,但在不少企业中,PGVector 是一个非常务实的选择:

  • • 复用 PostgreSQL 运维体系
  • • 元数据与向量可以统一管理
  • • 支持事务、备份、权限、审计
  • • 数据规模在百万级以内时,通常足够支撑多数知识问答场景

当然,PGVector 也有边界:

  • • 超大规模向量检索性能不如专业向量库
  • • 多索引、多模态、超复杂召回策略支持有限

典型建议如下:

场景 建议
10 万到 300 万片段,团队 PostgreSQL 能力强 优先 PGVector
亿级向量、多模态、高吞吐检索 考虑 Milvus / Weaviate / Elasticsearch Vector
极低延迟、小规模热数据 可考虑 Redis Stack

  1. 数据摄入链路设计:从原始文档到可检索知识

5.1 摄入链路并不是“上传文件后直接 embedding”

生产环境的知识摄入链路一般包含以下阶段:

上传文件 -> 存原始文档 -> 解析文本 -> 清洗标准化 -> 文档切块 -> 元数据增强 -> 嵌入向量化 -> 批量写入向量库 -> 构建索引 -> 发布版本

5.2 为什么要做知识版本管理

真实场景中,知识不是静态的:

  • • 商品价格会变
  • • 退货政策会变
  • • 合规制度会变
  • • FAQ 会不断修订

因此必须引入“知识版本”概念,而不是简单覆盖旧数据。

推荐最少设计三层标识:

  • knowledge_base_id
  • document_id
  • dataset_version

查询时可以支持:

  • • 查询最新已发布版本
  • • 回放历史版本回答
  • • 灰度新版本召回效果

5.3 文档切块不是固定 500 字符这么简单

切块策略直接影响召回准确率和上下文质量。

常见错误:

  • • 切块过大,召回片段冗长,噪音高
  • • 切块过小,上下文断裂,模型难以理解
  • • 不区分文档类型,统一策略硬切

推荐策略:

文档类型 切块建议
FAQ / 制度条款 按标题 + 段落切块
操作手册 按章节 + 步骤切块
技术文档 标题层级 + 代码块保护
表格规则 转换为结构化文本后切块
扫描件 PDF OCR 后先纠错再切块

更推荐做“结构化切块”而不是纯字符切块:

  • • 保留标题路径
  • • 保留章节层级
  • • 保留文档来源
  • • 保留发布时间、生效时间、业务域、租户等元数据

5.4 摄入链路的异步化设计

文档摄入耗时长、易失败、依赖外部模型服务,非常适合异步化:

客户端上传 -> 接口快速返回 taskId -> Kafka 投递摄入任务 -> Worker 消费任务 -> 分阶段推进状态机 -> 失败重试 -> 超限进入 DLQ

典型状态机:

  • UPLOADED
  • PARSING
  • CHUNKING
  • EMBEDDING
  • INDEXING
  • PUBLISHED
  • FAILED

这样设计的好处是:

  • • 接口不会被长耗时任务阻塞
  • • 可以并行扩容 worker
  • • 可以对不同阶段独立监控和重试
  • • 支持任务审计与回放

  1. 查询链路设计:从用户问题到可信答案

6.1 查询链路不应只做“一次向量检索”

一个成熟的查询链路通常包含以下步骤:

用户问题 -> 输入清洗 -> 租户/权限校验 -> Query Rewrite -> 多路召回 -> Rerank -> Context Builder -> Prompt 模板装配 -> LLM 生成 -> 引用拼装 -> 安全审查 -> 响应返回

6.2 Query Rewrite 的价值

用户输入往往很口语化,例如:

  • • “这个退吗”
  • • “昨天那个活动规则”
  • • “发票要怎么搞”

如果直接拿去做向量检索,召回效果可能一般。引入 Query Rewrite 后,可以把问题改写成更适合检索的形式:

  • • 显式补足主语
  • • 转成标准表达
  • • 提取业务关键词
  • • 将多轮上下文合并成完整问题

示例:

原始问题:这个能退吗结合上下文改写后:某电商平台已签收商品申请七天无理由退货的条件是什么

6.3 为什么企业级查询通常需要“多路召回”

仅靠语义向量检索往往不够,常见做法是混合召回:

  • • 向量召回:解决语义相似问题
  • • BM25 关键词召回:解决术语、型号、编号精确匹配
  • • 元数据过滤:解决租户、业务域、时间有效性隔离
  • • 规则召回:针对强规则类知识优先命中

最后对多路召回结果去重、归并、重排。

6.4 为什么还需要 Rerank

向量召回拿到的 TopK 往往“相关但不够准”。Rerank 模型可以基于“问题-文档片段”对进行更细粒度排序,把真正最有帮助的片段放到前面。

典型收益:

  • • 提高 Top3 命中率
  • • 降低无关片段进入 Prompt 的概率
  • • 在相同 token 预算下提升最终答案质量

6.5 上下文构建不是简单拼接

如果把 10 个片段无脑拼到 Prompt 里,会带来三个问题:

  • • Token 成本高
  • • 噪音增加,影响生成质量
  • • 上下文之间相互冲突

推荐的上下文构建策略:

  • • 控制总 token 上限
  • • 按相关性阈值截断
  • • 合并同源相邻片段
  • • 对长文本做摘要压缩
  • • 保留来源、标题、更新时间等可引用字段

6.6 回答阶段的关键约束

一个企业级 RAG Prompt 至少要明确四件事:

    1. 只能基于提供资料回答
    1. 找不到依据时必须明确说明不知道
    1. 输出时附带引用来源
    1. 涉及价格、政策、生效时间等信息时要优先采用最新有效版本

  1. 生产级代码实现:核心模块完整示例

下面给出一个可作为生产骨架参考的实现。重点不在于每一行代码都能原样复制,而在于体现企业级设计思路。

7.1 Maven 依赖

<?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.2.4</version>        <relativePath/>    </parent>    <groupId>com.company.ai</groupId>    <artifactId>rag-platform</artifactId>    <version>1.0.0</version>    <properties>        <java.version>17</java.version>        <langchain4j.version>0.35.0</langchain4j.version>        <spring-cloud.version>2023.0.1</spring-cloud.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-validation</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-actuator</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-data-jpa</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-data-redis</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.kafka</groupId>            <artifactId>spring-kafka</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.retry</groupId>            <artifactId>spring-retry</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>        </dependency>        <dependency>            <groupId>io.micrometer</groupId>            <artifactId>micrometer-registry-prometheus</artifactId>        </dependency>        <dependency>            <groupId>com.github.ben-manes.caffeine</groupId>            <artifactId>caffeine</artifactId>        </dependency>        <dependency>            <groupId>org.postgresql</groupId>            <artifactId>postgresql</artifactId>        </dependency>        <dependency>            <groupId>dev.langchain4j</groupId>            <artifactId>langchain4j</artifactId>            <version>${langchain4j.version}</version>        </dependency>        <dependency>            <groupId>dev.langchain4j</groupId>            <artifactId>langchain4j-dashscope</artifactId>            <version>${langchain4j.version}</version>        </dependency>        <dependency>            <groupId>dev.langchain4j</groupId>            <artifactId>langchain4j-pgvector</artifactId>            <version>${langchain4j.version}</version>        </dependency>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <optional>true</optional>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>    </dependencies>    <dependencyManagement>        <dependencies>            <dependency>                <groupId>org.springframework.cloud</groupId>                <artifactId>spring-cloud-dependencies</artifactId>                <version>${spring-cloud.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>        </dependencies>    </dependencyManagement></project>

7.2 配置文件

server:  port: 8080  tomcat:    threads:      max: 300      min-spare: 30    accept-count: 1000    connection-timeout: 5sspring:  application:    name: rag-query-service  datasource:    url: jdbc:postgresql://localhost:5432/rag_platform    username: rag_user    password: ${DB_PASSWORD:change_me}    hikari:      maximum-pool-size: 30      minimum-idle: 10      idle-timeout: 600000      max-lifetime: 1800000      connection-timeout: 3000  data:    redis:      host: localhost      port: 6379      timeout: 2s  kafka:    bootstrap-servers: localhost:9092    producer:      acks: all      retries: 3    consumer:      group-id: rag-ingest-group      auto-offset-reset: latest      enable-auto-commit: false    listener:      ack-mode: manual      concurrency: 6management:  endpoints:    web:      exposure:        include: health,info,metrics,prometheus  metrics:    tags:      application: ${spring.application.name}rag:  model:    chat-model: qwen-plus    embedding-model: text-embedding-v2  retrieval:    max-results: 12    min-score: 0.65    rerank-top-n: 6    max-context-tokens: 5000  chunk:    max-size: 600    overlap-size: 80  cache:    answer-ttl-minutes: 10    query-rewrite-ttl-minutes: 30  degrade:    enable-keyword-fallback: truedashscope:  api-key: ${DASHSCOPE_API_KEY:}resilience4j:  circuitbreaker:    instances:      llmChat:        sliding-window-size: 20        minimum-number-of-calls: 10        failure-rate-threshold: 50        wait-duration-in-open-state: 30s      embedding:        sliding-window-size: 20        minimum-number-of-calls: 10        failure-rate-threshold: 50        wait-duration-in-open-state: 30s  timelimiter:    instances:      llmChat:        timeout-duration: 8s      embedding:        timeout-duration: 5s

7.3 领域模型与数据库设计

企业级 RAG 至少要把“原始文档、切块片段、摄入任务、知识版本”分开建模。

CREATE EXTENSION IF NOT EXISTS vector;CREATE TABLE knowledge_document (    id                UUID PRIMARY KEY,    tenant_id         VARCHAR(64) NOT NULL,    knowledge_base_id VARCHAR(64) NOT NULL,    dataset_version   VARCHAR(32) NOT NULL,    file_name         VARCHAR(256) NOT NULL,    content_type      VARCHAR(64) NOT NULL,    source_uri        VARCHAR(1024),    status            VARCHAR(32) NOT NULL,    enabled           BOOLEAN NOT NULL DEFAULT TRUE,    created_at        TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,    updated_at        TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP);CREATE TABLE knowledge_chunk (    id                UUID PRIMARY KEY,    document_id       UUID NOT NULL,    tenant_id         VARCHAR(64) NOT NULL,    knowledge_base_id VARCHAR(64) NOT NULL,    dataset_version   VARCHAR(32) NOT NULL,    chunk_no          INT NOT NULL,    title_path        VARCHAR(512),    content           TEXT NOT NULL,    metadata          JSONB NOT NULL,    embedding         VECTOR(1536) NOT NULL,    created_at        TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP);CREATE INDEX idx_chunk_doc ON knowledge_chunk(document_id);CREATE INDEX idx_chunk_tenant_kb ON knowledge_chunk(tenant_id, knowledge_base_id);CREATE INDEX idx_chunk_embedding_hnswON knowledge_chunkUSING hnsw (embedding vector_cosine_ops)WITH (m = 16, ef_construction = 64);CREATE TABLE ingest_task (    id                UUID PRIMARY KEY,    tenant_id         VARCHAR(64) NOT NULL,    knowledge_base_id VARCHAR(64) NOT NULL,    document_id       UUID NOT NULL,    status            VARCHAR(32) NOT NULL,    retry_count       INT NOT NULL DEFAULT 0,    error_message     TEXT,    created_at        TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,    updated_at        TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP);

7.4 配置对象

package com.company.ai.rag.config;import jakarta.validation.constraints.Min;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.validation.annotation.Validated;@Data@Validated@ConfigurationProperties(prefix = "rag")public class RagProperties {    private Model model = new Model();    private Retrieval retrieval = new Retrieval();    private Chunk chunk = new Chunk();    private Cache cache = new Cache();    private Degrade degrade = new Degrade();    @Data    public static class Model {        private String chatModel;        private String embeddingModel;    }    @Data    public static class Retrieval {        @Min(1)        private int maxResults = 10;        private double minScore = 0.65;        @Min(1)        private int rerankTopN = 5;        @Min(1000)        private int maxContextTokens = 5000;    }    @Data    public static class Chunk {        @Min(100)        private int maxSize = 600;        @Min(0)        private int overlapSize = 80;    }    @Data    public static class Cache {        @Min(1)        private int answerTtlMinutes = 10;        @Min(1)        private int queryRewriteTtlMinutes = 30;    }    @Data    public static class Degrade {        private boolean enableKeywordFallback = true;    }}

7.5 LangChain4j 与基础设施配置

package com.company.ai.rag.config;import com.github.benmanes.caffeine.cache.Cache;import com.github.benmanes.caffeine.cache.Caffeine;import dev.langchain4j.data.segment.TextSegment;import dev.langchain4j.model.chat.ChatLanguageModel;import dev.langchain4j.model.dashscope.QwenChatModel;import dev.langchain4j.model.dashscope.QwenEmbeddingModel;import dev.langchain4j.model.embedding.EmbeddingModel;import dev.langchain4j.store.embedding.EmbeddingStore;import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.time.Duration;@Configuration@EnableConfigurationProperties(RagProperties.class)public class RagAutoConfiguration {    @Bean    public ChatLanguageModel chatLanguageModel(            @Value("${dashscope.api-key}") String apiKey,            RagProperties properties    ) {        return QwenChatModel.builder()                .apiKey(apiKey)                .modelName(properties.getModel().getChatModel())                .temperature(0.2)                .topP(0.8)                .maxTokens(1200)                .build();    }    @Bean    public EmbeddingModel embeddingModel(            @Value("${dashscope.api-key}") String apiKey,            RagProperties properties    ) {        return QwenEmbeddingModel.builder()                .apiKey(apiKey)                .modelName(properties.getModel().getEmbeddingModel())                .build();    }    @Bean    public EmbeddingStore<TextSegment> embeddingStore(            @Value("${spring.datasource.url}") String url,            @Value("${spring.datasource.username}") String username,            @Value("${spring.datasource.password}") String password    ) {        return PgVectorEmbeddingStore.builder()                .host("localhost")                .port(5432)                .database("rag_platform")                .user(username)                .password(password)                .table("knowledge_chunk")                .dimension(1536)                .createTable(false)                .dropTableFirst(false)                .indexType("HNSW")                .build();    }    @Bean    public Cache<String, String> answerCache(RagProperties properties) {        return Caffeine.newBuilder()                .maximumSize(100_000)                .expireAfterWrite(Duration.ofMinutes(properties.getCache().getAnswerTtlMinutes()))                .recordStats()                .build();    }    @Bean    public Cache<String, String> queryRewriteCache(RagProperties properties) {        return Caffeine.newBuilder()                .maximumSize(50_000)                .expireAfterWrite(Duration.ofMinutes(properties.getCache().getQueryRewriteTtlMinutes()))                .recordStats()                .build();    }}

说明:

  • • 生产环境不要从 JDBC URL 里手工解析主机和库名,建议显式配置
  • • 向量表建议手工建表和建索引,不要依赖启动期自动建表
  • • 大模型温度应根据场景调优,知识问答普遍不建议过高

7.6 查询请求与响应模型

package com.company.ai.rag.api.dto;import jakarta.validation.constraints.NotBlank;import lombok.Data;@Datapublic class AskRequest {    @NotBlank    private String tenantId;    @NotBlank    private String userId;    @NotBlank    private String knowledgeBaseId;    @NotBlank    private String question;    private String sessionId;    private String datasetVersion;}
``````plaintext
package com.company.ai.rag.api.dto;import lombok.Builder;import lombok.Value;import java.util.List;@Value@Builderpublic class AskResponse {    String answer;    List<ReferenceItem> references;    String rewrittenQuery;    long costMs;    @Value    @Builder    public static class ReferenceItem {        String documentId;        Integer chunkNo;        String fileName;        String titlePath;        Double score;    }}

7.7 Query Rewrite 服务

package com.company.ai.rag.service;import com.github.benmanes.caffeine.cache.Cache;import dev.langchain4j.model.chat.ChatLanguageModel;import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Service;import org.springframework.util.StringUtils;@Service@RequiredArgsConstructorpublic class QueryRewriteService {    private final ChatLanguageModel chatLanguageModel;    private final Cache<String, String> queryRewriteCache;    public String rewrite(String tenantId, String question, String conversationContext) {        String cacheKey = tenantId + "::" + question + "::" + conversationContext;        String cached = queryRewriteCache.getIfPresent(cacheKey);        if (cached != null) {            return cached;        }        String prompt = """                你是企业知识检索查询改写器。                目标:将用户问题改写为更适合知识库检索的标准问句。                要求:                1. 保留原意,不增加事实。                2. 若上下文能补足主语,请补足。                3. 输出仅返回改写后的一个问题。                会话上下文:                %s                用户问题:                %s                """.formatted(                StringUtils.hasText(conversationContext) ? conversationContext : "无",                question        );        String rewritten = chatLanguageModel.generate(prompt);        queryRewriteCache.put(cacheKey, rewritten);        return rewritten;    }}

7.8 检索结果模型

package com.company.ai.rag.service.retrieval;import lombok.Builder;import lombok.Value;import java.util.Map;import java.util.UUID;@Value@Builderpublic class RetrievedChunk {    UUID chunkId;    UUID documentId;    Integer chunkNo;    String content;    String fileName;    String titlePath;    Double score;    Map<String, Object> metadata;}

7.9 自定义检索仓储

package com.company.ai.rag.repository;import com.company.ai.rag.service.retrieval.RetrievedChunk;import lombok.RequiredArgsConstructor;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Repository;import java.sql.ResultSet;import java.sql.SQLException;import java.util.List;import java.util.UUID;@Repository@RequiredArgsConstructorpublic class KnowledgeChunkSearchRepository {    private final JdbcTemplate jdbcTemplate;    public List<RetrievedChunk> searchByEmbedding(            String tenantId,            String knowledgeBaseId,            String datasetVersion,            String embeddingVectorLiteral,            int limit    ) {        String sql = """                SELECT kc.id,                       kc.document_id,                       kc.chunk_no,                       kc.content,                       kc.metadata,                       kd.file_name,                       kc.title_path,                       1 - (kc.embedding <=> CAST(? AS vector)) AS score                FROM knowledge_chunk kc                JOIN knowledge_document kd ON kd.id = kc.document_id                WHERE kc.tenant_id = ?                  AND kc.knowledge_base_id = ?                  AND (? IS NULL OR kc.dataset_version = ?)                ORDER BY kc.embedding <=> CAST(? AS vector)                LIMIT ?                """;        return jdbcTemplate.query(                sql,                (rs, rowNum) -> map(rs),                embeddingVectorLiteral,                tenantId,                knowledgeBaseId,                datasetVersion,                datasetVersion,                embeddingVectorLiteral,                limit        );    }    private RetrievedChunk map(ResultSet rs) throws SQLException {        return RetrievedChunk.builder()                .chunkId(rs.getObject("id", UUID.class))                .documentId(rs.getObject("document_id", UUID.class))                .chunkNo(rs.getInt("chunk_no"))                .content(rs.getString("content"))                .fileName(rs.getString("file_name"))                .titlePath(rs.getString("title_path"))                .score(rs.getDouble("score"))                .metadata(java.util.Map.of())                .build();    }}

这里故意没有把一切都封装成黑盒组件,因为企业系统通常需要:

  • • 灵活控制 SQL 过滤条件
  • • 做租户隔离
  • • 做版本路由
  • • 结合业务字段排序和过滤

7.10 检索服务

package com.company.ai.rag.service.retrieval;import com.company.ai.rag.config.RagProperties;import com.company.ai.rag.repository.KnowledgeChunkSearchRepository;import dev.langchain4j.data.embedding.Embedding;import dev.langchain4j.model.embedding.EmbeddingModel;import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Service;import java.util.Comparator;import java.util.List;@Service@RequiredArgsConstructorpublic class RetrievalService {    private final EmbeddingModel embeddingModel;    private final KnowledgeChunkSearchRepository searchRepository;    private final RagProperties ragProperties;    public List<RetrievedChunk> retrieve(            String tenantId,            String knowledgeBaseId,            String datasetVersion,            String rewrittenQuery    ) {        Embedding embedding = embeddingModel.embed(rewrittenQuery).content();        String vectorLiteral = toVectorLiteral(embedding.vectorAsList());        List<RetrievedChunk> raw = searchRepository.searchByEmbedding(                tenantId,                knowledgeBaseId,                datasetVersion,                vectorLiteral,                ragProperties.getRetrieval().getMaxResults()        );        return raw.stream()                .filter(item -> item.getScore() >= ragProperties.getRetrieval().getMinScore())                .sorted(Comparator.comparing(RetrievedChunk::getScore).reversed())                .toList();    }    private String toVectorLiteral(List<Float> vector) {        StringBuilder builder = new StringBuilder("[");        for (int i = 0; i < vector.size(); i++) {            if (i > 0) {                builder.append(',');            }            builder.append(vector.get(i));        }        builder.append(']');        return builder.toString();    }}

7.11 Rerank 抽象

如果暂时没有独立 Rerank 模型,也建议先留出接口,避免查询链路被写死。

package com.company.ai.rag.service.retrieval;import java.util.List;public interface RerankService {    List<RetrievedChunk> rerank(String question, List<RetrievedChunk> chunks);}
``````plaintext
package com.company.ai.rag.service.retrieval;import com.company.ai.rag.config.RagProperties;import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Service;import java.util.Comparator;import java.util.List;@Service@RequiredArgsConstructorpublic class DefaultRerankService implements RerankService {    private final RagProperties ragProperties;    @Override    public List<RetrievedChunk> rerank(String question, List<RetrievedChunk> chunks) {        return chunks.stream()                .sorted(Comparator.comparing(RetrievedChunk::getScore).reversed())                .limit(ragProperties.getRetrieval().getRerankTopN())                .toList();    }}

7.12 上下文构建器

package com.company.ai.rag.service;import com.company.ai.rag.service.retrieval.RetrievedChunk;import org.springframework.stereotype.Component;import java.util.List;@Componentpublic class ContextAssembler {    public String assemble(List<RetrievedChunk> chunks) {        StringBuilder builder = new StringBuilder();        for (int i = 0; i < chunks.size(); i++) {            RetrievedChunk chunk = chunks.get(i);            builder.append("资料")                    .append(i + 1)                    .append(":\n")                    .append("来源文件: ")                    .append(chunk.getFileName())                    .append("\n")                    .append("标题路径: ")                    .append(chunk.getTitlePath())                    .append("\n")                    .append("内容: ")                    .append(chunk.getContent())                    .append("\n\n");        }        return builder.toString();    }}

7.13 核心问答服务

package com.company.ai.rag.service;import com.company.ai.rag.api.dto.AskRequest;import com.company.ai.rag.api.dto.AskResponse;import com.company.ai.rag.service.retrieval.RerankService;import com.company.ai.rag.service.retrieval.RetrievalService;import com.company.ai.rag.service.retrieval.RetrievedChunk;import com.github.benmanes.caffeine.cache.Cache;import dev.langchain4j.model.chat.ChatLanguageModel;import io.micrometer.core.instrument.MeterRegistry;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;import org.springframework.util.CollectionUtils;import java.util.List;@Slf4j@Service@RequiredArgsConstructorpublic class RagQueryService {    private final QueryRewriteService queryRewriteService;    private final RetrievalService retrievalService;    private final RerankService rerankService;    private final ContextAssembler contextAssembler;    private final ChatLanguageModel chatLanguageModel;    private final Cache<String, String> answerCache;    private final MeterRegistry meterRegistry;    public AskResponse ask(AskRequest request) {        long start = System.currentTimeMillis();        String cacheKey = buildCacheKey(request);        String cachedAnswer = answerCache.getIfPresent(cacheKey);        String rewrittenQuery = queryRewriteService.rewrite(                request.getTenantId(),                request.getQuestion(),                ""        );        if (cachedAnswer != null) {            meterRegistry.counter("rag.answer.cache.hit").increment();            return AskResponse.builder()                    .answer(cachedAnswer)                    .rewrittenQuery(rewrittenQuery)                    .references(List.of())                    .costMs(System.currentTimeMillis() - start)                    .build();        }        List<RetrievedChunk> recalled = retrievalService.retrieve(                request.getTenantId(),                request.getKnowledgeBaseId(),                request.getDatasetVersion(),                rewrittenQuery        );        List<RetrievedChunk> reranked = rerankService.rerank(rewrittenQuery, recalled);        if (CollectionUtils.isEmpty(reranked)) {            return AskResponse.builder()                    .answer("未在知识库中检索到足够可信的依据,请补充问题或联系人工支持。")                    .rewrittenQuery(rewrittenQuery)                    .references(List.of())                    .costMs(System.currentTimeMillis() - start)                    .build();        }        String context = contextAssembler.assemble(reranked);        String prompt = buildPrompt(request.getQuestion(), context);        String answer = chatLanguageModel.generate(prompt);        answerCache.put(cacheKey, answer);        meterRegistry.timer("rag.query.latency").record(System.currentTimeMillis() - start, java.util.concurrent.TimeUnit.MILLISECONDS);        return AskResponse.builder()                .answer(answer)                .rewrittenQuery(rewrittenQuery)                .references(reranked.stream().map(item ->                        AskResponse.ReferenceItem.builder()                                .documentId(item.getDocumentId().toString())                                .chunkNo(item.getChunkNo())                                .fileName(item.getFileName())                                .titlePath(item.getTitlePath())                                .score(item.getScore())                                .build()                ).toList())                .costMs(System.currentTimeMillis() - start)                .build();    }    private String buildPrompt(String question, String context) {        return """                你是企业知识库问答助手。                你的回答必须严格依据提供的资料,不得编造。                回答要求:                1. 若资料足够,给出简洁准确答案。                2. 若资料不足,明确说明资料不足,不要自行补充事实。                3. 优先使用最新、生效中的政策与规则。                4. 回答结尾给出“依据来源”摘要。                用户问题:                %s                参考资料:                %s                """.formatted(question, context);    }    private String buildCacheKey(AskRequest request) {        return String.join("::",                request.getTenantId(),                request.getKnowledgeBaseId(),                String.valueOf(request.getDatasetVersion()),                request.getQuestion()        );    }}

7.14 控制器

package com.company.ai.rag.api;import com.company.ai.rag.api.dto.AskRequest;import com.company.ai.rag.api.dto.AskResponse;import com.company.ai.rag.service.RagQueryService;import jakarta.validation.Valid;import lombok.RequiredArgsConstructor;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.Map;@RestController@RequestMapping("/api/v1/rag")@RequiredArgsConstructorpublic class RagController {    private final RagQueryService ragQueryService;    @PostMapping("/ask")    public ResponseEntity<Map<String, Object>> ask(@Valid @RequestBody AskRequest request) {        AskResponse response = ragQueryService.ask(request);        return ResponseEntity.ok(Map.of(                "code", 200,                "message", "success",                "data", response        ));    }}

7.15 文档摄入任务模型

package com.company.ai.rag.ingest;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;import java.io.Serializable;import java.util.UUID;@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic class IngestDocumentCommand implements Serializable {    private UUID taskId;    private UUID documentId;    private String tenantId;    private String knowledgeBaseId;    private String datasetVersion;    private String fileName;    private String contentType;    private String objectKey;}

7.16 摄入任务生产者

package com.company.ai.rag.ingest;import lombok.RequiredArgsConstructor;import org.springframework.kafka.core.KafkaTemplate;import org.springframework.stereotype.Component;@Component@RequiredArgsConstructorpublic class IngestTaskProducer {    private final KafkaTemplate<String, Object> kafkaTemplate;    public void send(IngestDocumentCommand command) {        kafkaTemplate.send("rag.ingest.document", command.getDocumentId().toString(), command);    }}

7.17 文档切块与向量写入服务

package com.company.ai.rag.ingest;import com.company.ai.rag.config.RagProperties;import dev.langchain4j.data.document.Document;import dev.langchain4j.data.document.splitter.DocumentSplitters;import dev.langchain4j.data.embedding.Embedding;import dev.langchain4j.data.segment.TextSegment;import dev.langchain4j.model.embedding.EmbeddingModel;import dev.langchain4j.model.output.Response;import dev.langchain4j.store.embedding.EmbeddingStore;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;import java.util.ArrayList;import java.util.List;import java.util.Map;@Slf4j@Service@RequiredArgsConstructorpublic class DocumentIndexingService {    private final RagProperties ragProperties;    private final EmbeddingModel embeddingModel;    private final EmbeddingStore<TextSegment> embeddingStore;    public void index(String text, Map<String, Object> metadata) {        Document document = Document.from(text, metadata);        List<TextSegment> segments = DocumentSplitters.recursive(                ragProperties.getChunk().getMaxSize(),                ragProperties.getChunk().getOverlapSize()        ).split(document);        int batchSize = 16;        for (int i = 0; i < segments.size(); i += batchSize) {            int end = Math.min(i + batchSize, segments.size());            List<TextSegment> batch = new ArrayList<>(segments.subList(i, end));            Response<List<Embedding>> embeddings = embeddingModel.embedAll(batch);            for (int j = 0; j < batch.size(); j++) {                embeddingStore.add(embeddings.content().get(j), batch.get(j));            }            log.info("indexed chunk batch: {}/{}", end, segments.size());        }    }}

这里有一个关键工程点:必须使用批量向量化与批量写入思路。否则单篇大文档就会产生大量串行模型请求,吞吐非常差。

7.18 Kafka 消费者与幂等处理

package com.company.ai.rag.ingest;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.apache.kafka.clients.consumer.ConsumerRecord;import org.springframework.kafka.annotation.KafkaListener;import org.springframework.kafka.support.Acknowledgment;import org.springframework.stereotype.Component;@Slf4j@Component@RequiredArgsConstructorpublic class IngestTaskConsumer {    private final IngestWorkflowService ingestWorkflowService;    @KafkaListener(topics = "rag.ingest.document", groupId = "rag-ingest-group")    public void onMessage(ConsumerRecord<String, IngestDocumentCommand> record, Acknowledgment acknowledgment) {        IngestDocumentCommand command = record.value();        try {            ingestWorkflowService.handle(command);            acknowledgment.acknowledge();        } catch (Exception ex) {            log.error("ingest task failed, documentId={}", command.getDocumentId(), ex);            throw ex;        }    }}
``````plaintext
package com.company.ai.rag.ingest;import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Service;@Service@RequiredArgsConstructorpublic class IngestWorkflowService {    private final DocumentIndexingService documentIndexingService;    public void handle(IngestDocumentCommand command) {        if (alreadyProcessed(command)) {            return;        }        String parsedText = loadAndParseText(command);        documentIndexingService.index(parsedText, java.util.Map.of(                "tenantId", command.getTenantId(),                "knowledgeBaseId", command.getKnowledgeBaseId(),                "datasetVersion", command.getDatasetVersion(),                "fileName", command.getFileName(),                "documentId", command.getDocumentId().toString()        ));        markAsProcessed(command);    }    private boolean alreadyProcessed(IngestDocumentCommand command) {        return false;    }    private String loadAndParseText(IngestDocumentCommand command) {        return "这里应从对象存储加载文件,再走 PDF/Word/Markdown 解析链路";    }    private void markAsProcessed(IngestDocumentCommand command) {    }}

幂等是摄入链路必须考虑的问题,因为消息系统、重试机制、人工补偿都可能导致重复处理。

7.19 启动类

package com.company.ai.rag;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.retry.annotation.EnableRetry;@SpringBootApplication@EnableRetrypublic class RagPlatformApplication {    public static void main(String[] args) {        SpringApplication.run(RagPlatformApplication.class, args);    }}

  1. 高并发与可扩展设计:从 100 QPS 到 1000+ QPS

8.1 企业级 RAG 的性能瓶颈通常在哪里

在真实链路中,延迟通常来自四块:

    1. Query Rewrite 模型调用
    1. Embedding 与检索
    1. Rerank 模型调用
    1. Chat LLM 生成

一个没有优化的系统,经常会出现这种耗时分布:

阶段 耗时占比
Query Rewrite 10%
向量化与召回 20%
Rerank 15%
LLM 生成 45%
其他网络与序列化 10%

8.2 提升吞吐的核心策略

方案一:查询链路无状态化

rag-query-service 不持久化用户状态,只从 Redis 或会话服务读取上下文,这样才能自由横向扩容。

方案二:多级缓存

推荐至少三层缓存:

  • • Query Rewrite 缓存
  • • 热门问题答案缓存
  • • Embedding 缓存

注意:

  • • 答案缓存必须带知识库版本,否则文档更新后会回答旧内容
  • • 对强时效问题,缓存 TTL 要谨慎设置
方案三:批量化

批量化是吞吐提升最有效的手段之一:

  • • 批量 embedding
  • • 批量写入向量库
  • • 批量消费 Kafka
方案四:读写分离

摄入链路是写多,查询链路是读多。

推荐拆开:

  • • 文档写入和版本发布走主库
  • • 在线检索尽量走独立读实例或独立检索集群
方案五:流式输出

即使总耗时没有明显下降,流式输出也会显著改善用户体感。

适合场景:

  • • 较长答案
  • • Copilot 类产品
  • • 客服辅助问答

8.3 线程池与资源隔离

不要把所有任务都丢进 Tomcat 或通用线程池。推荐拆出专用资源池:

  • • Web 请求线程池
  • • 文档解析线程池
  • • 模型调用线程池
  • • Kafka 消费线程池

这样可以避免某一类任务把整体资源拖垮。

8.4 1000+ QPS 的现实前提

必须说明,RAG 的 QPS 不是只看 Java 服务本身。

真正限制你上限的通常是:

  • • 外部 LLM 配额
  • • Embedding 调用限流
  • • 向量检索延迟
  • • Prompt 长度与生成长度

如果目标是 1000+ QPS,往往需要:

  • • 对热门问题命中缓存
  • • 对长回答采用异步/流式
  • • 缩减上下文 token
  • • 部分问题直接走规则或 FAQ 精准回答
  • • 大模型分级路由,小模型优先处理简单问题

8.5 一种典型的分级路由策略

问题进入 -> 规则命中    -> 直接返回 -> FAQ 精确命中    -> 直接返回 -> RAG 检索置信度高    -> 小模型生成 -> RAG 检索置信度一般    -> 大模型生成 -> 无可靠资料    -> 返回人工兜底

这类策略对成本控制非常有效。


  1. 可靠性治理:限流、熔断、降级、幂等与重试

9.1 为什么 AI 系统更需要治理

相对传统 CRUD 服务,RAG 系统更依赖外部服务:

  • • Embedding API
  • • Chat API
  • • OCR / 解析服务
  • • 对象存储
  • • 向量数据库

任意一个依赖抖动,都可能把链路拉长或拖垮。

9.2 限流

至少要做三层限流:

  • • 网关级限流:防止恶意流量与突发洪峰
  • • 租户级限流:保证多租户公平
  • • 模型级限流:保护下游 LLM 配额

9.3 熔断与超时

模型调用必须设置超时,不可无限等待。

推荐经验值:

  • • Query Rewrite:2 到 3 秒
  • • Embedding:3 到 5 秒
  • • Chat:6 到 10 秒

若超时或失败率升高,可触发熔断并降级:

  • • 关闭 Query Rewrite
  • • 跳过 Rerank
  • • 降级到关键词检索
  • • 返回“资料检索中,请稍后重试”

9.4 重试

不是所有失败都适合重试。

适合重试:

  • • 429
  • • 网络瞬断
  • • 下游短暂超时

不适合盲目重试:

  • • 参数错误
  • • 数据格式错误
  • • 文档解析本身失败

9.5 幂等

幂等主要发生在两个位置:

  • • 文档摄入任务消费
  • • 知识版本发布操作

推荐做法:

  • • 任务表记录处理状态
  • • 以 documentId + datasetVersion 作为幂等键
  • • 发布动作采用状态机推进,避免重复发布

  1. 可观测性与评估体系:没有指标就没有生产

10.1 监控不能只看接口 RT

企业级 RAG 至少要关注以下指标:

业务指标
  • • 问答总量
  • • 人工转接率
  • • 无答案率
  • • 用户追问率
  • • 知识命中率
检索指标
  • • Recall TopK 命中率
  • • Rerank 前后命中率变化
  • • 平均召回片段数
  • • 低于阈值的问题比例
模型指标
  • • 各模型请求数
  • • 成功率
  • • 平均耗时
  • • Token 输入输出量
  • • 单租户成本
系统指标
  • • 接口 P50 / P95 / P99
  • • 线程池队列长度
  • • Kafka Lag
  • • Redis 命中率
  • • PostgreSQL 慢查询

10.2 推荐打点位置

关键埋点包括:

  • • Query Rewrite 耗时
  • • 向量检索耗时
  • • Rerank 耗时
  • • LLM 生成耗时
  • • 总链路耗时
  • • 检索片段数
  • • 最终上下文 token 数

10.3 评估体系要区分离线与在线

离线评估

基于标注数据集评估:

  • • 检索命中率
  • • 精确率 / 召回率
  • • 答案事实一致性
  • • 引用正确率
在线评估

基于真实流量分析:

  • • AB 测试
  • • 人工审核
  • • 用户反馈
  • • 转人工率变化

10.4 一套常见的评估样本字段

字段 说明
question 用户问题
golden_answer 标准答案
golden_doc_ids 应命中的文档
business_domain 业务域
difficulty 难度级别
timestamp 提问时间

有了评估集,后续才能持续比较:

  • • 切块策略调整前后效果
  • • Rerank 模型切换前后效果
  • • Prompt 模板更新前后效果
  • • 新知识版本发布前后效果

  1. Kubernetes 部署与弹性扩缩容

11.1 Dockerfile

FROM maven:3.9-eclipse-temurin-17 AS builderWORKDIR /workspaceCOPY pom.xml .COPY src ./srcRUN mvn -B clean package -DskipTestsFROM eclipse-temurin:17-jre-alpineWORKDIR /appCOPY --from=builder /workspace/target/rag-platform-1.0.0.jar app.jarENV JAVA_OPTS="-Xms512m -Xmx2048m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"EXPOSE 8080ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]

11.2 Deployment

apiVersion: apps/v1kind: Deploymentmetadata:  name: rag-query-service  namespace: rag-prodspec:  replicas: 4  selector:    matchLabels:      app: rag-query-service  template:    metadata:      labels:        app: rag-query-service    spec:      containers:        - name: rag-query-service          image: registry.example.com/ai/rag-query-service:1.0.0          imagePullPolicy: IfNotPresent          ports:            - containerPort: 8080          env:            - name: SPRING_PROFILES_ACTIVE              value: prod            - name: DASHSCOPE_API_KEY              valueFrom:                secretKeyRef:                  name: dashscope-secret                  key: api-key            - name: DB_PASSWORD              valueFrom:                secretKeyRef:                  name: db-secret                  key: password          resources:            requests:              cpu: "500m"              memory: "512Mi"            limits:              cpu: "2"              memory: "2Gi"          readinessProbe:            httpGet:              path: /actuator/health              port: 8080            initialDelaySeconds: 20            periodSeconds: 5          livenessProbe:            httpGet:              path: /actuator/health              port: 8080            initialDelaySeconds: 60            periodSeconds: 10

11.3 HPA

apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata:  name: rag-query-service-hpa  namespace: rag-prodspec:  scaleTargetRef:    apiVersion: apps/v1    kind: Deployment    name: rag-query-service  minReplicas: 4  maxReplicas: 20  metrics:    - type: Resource      resource:        name: cpu        target:          type: Utilization          averageUtilization: 70    - type: Resource      resource:        name: memory        target:          type: Utilization          averageUtilization: 75

11.4 K8s 部署中的几个关键点

  • • Query 服务和 Ingest 服务应分开部署
  • • Query 服务优先保证低延迟,Ingest 服务优先保证吞吐
  • • Secret 存储 API Key,不要写入镜像
  • • readinessProbe 很关键,避免未准备完成的 Pod 被流量打穿
  • • HPA 只能解决“服务扩容”,解决不了“下游模型配额不足”

  1. 真实业务案例:电商客服知识问答系统

12.1 业务背景

某电商平台客服系统面临以下问题:

  • • 商品规则、促销政策、售后流程更新频繁
  • • 新客服培训时间长,且回答质量依赖个人经验
  • • 传统关键词搜索效果差,检索结果需要人工再判断
  • • 高峰期咨询量大,专家客服资源不足

12.2 目标能力

系统需要支持:

  • • 商品、售后、发票、物流等多知识域问答
  • • 多租户商家知识隔离
  • • 回答附引用,方便客服二次确认
  • • 高峰期稳定支撑,支持客服工作台和机器人同时接入

12.3 方案落地

知识来源:

  • • 商家后台配置规则
  • • 平台制度中心
  • • 商品说明书
  • • FAQ 与工单沉淀

在线链路:

  • • 用户提问先做 Query Rewrite
  • • 按租户、业务域、版本做过滤
  • • 向量召回 + 关键词召回混合
  • • Rerank 后挑选前 4 到 6 个片段
  • • LLM 生成答案并附引用

离线链路:

  • • 每日同步商品和政策数据
  • • 大版本知识发布前先做离线评估
  • • 新版本灰度到部分租户

12.4 典型收益

上线后通常可带来以下改进:

  • • 首轮答复命中率提升
  • • 人工转接率下降
  • • 新人客服培训周期缩短
  • • 客服平均处理时长下降
  • • 高风险问答可通过引用与规则兜底降低误答

  1. 常见线上问题与排查手册

13.1 问题一:明明有文档,却检索不到

优先排查:

    1. 文档是否进入正确知识库和版本
    1. 文档切块是否过碎或过粗
    1. 是否遗漏租户过滤条件
    1. embedding 模型与历史索引是否一致
    1. 查询是否需要 Rewrite

13.2 问题二:召回了相关资料,但答案仍然不准

通常不是向量库的问题,而是以下原因:

  • • 召回片段太多,噪音压过重点
  • • Prompt 没有限制模型必须基于资料回答
  • • 没有 Rerank,前几个片段并非最优
  • • 上下文中存在新旧版本冲突

13.3 问题三:高峰期延迟突然升高

排查顺序建议:

    1. 看 LLM 接口 RT 和错误率
    1. 看 Embedding 与 Rerank 是否限流
    1. 看 PostgreSQL 慢查询和索引是否生效
    1. 看线程池队列和连接池是否打满
    1. 看 Redis 是否抖动导致缓存失效

13.4 问题四:知识更新后用户还在看到旧答案

根因通常有三类:

  • • 答案缓存没有带版本号
  • • 向量索引切换不是原子发布
  • • 多实例本地缓存未失效

13.5 问题五:成本失控

最常见原因:

  • • 每个请求都走大模型
  • • 上下文拼得过长
  • • 没有缓存和分级路由
  • • 没有对低价值问题走 FAQ 或规则链路

  1. 演进路线:从 RAG 到 Agentic Knowledge Platform

当基础 RAG 稳定后,下一步演进通常有四个方向。

14.1 多模态知识

支持:

  • • 图片说明书
  • • OCR 扫描件
  • • 表格与流程图
  • • 音视频转录内容

14.2 更强检索编排

包括:

  • • Hybrid Search
  • • Multi Query Retrieval
  • • Parent-Child Chunk
  • • Graph RAG

14.3 面向任务的 Agent 化

当问题不再只是“问答”,而是:

  • • 查询订单状态
  • • 发起退款流程
  • • 校验库存和规则

就需要把 RAG 从“知识辅助”升级成“任务编排中的知识能力模块”。

14.4 平台化与治理化

最终成熟形态通常是一个知识平台,而不是单个应用功能:

  • • 知识库管理后台
  • • 数据集版本管理
  • • 模型路由配置
  • • Prompt 模板管理
  • • 评估集与自动评测
  • • 审计日志与合规治理

  1. 总结

企业级 RAG 的核心难点,从来都不是“把 LangChain4j 跑起来”,而是如何把知识数据、检索系统、大模型调用和微服务治理真正整合成一个稳定的平台。

如果只看功能闭环,RAG 很简单:

  • • 切块
  • • 向量化
  • • 检索
  • • 生成

但如果要进入生产,就必须系统考虑:

  • • 知识版本管理
  • • 摄入异步化
  • • 多路召回与重排
  • • 高并发与缓存
  • • 限流熔断与降级
  • • 可观测性与评估闭环
  • • 多租户与安全治理

Spring Boot 3 + LangChain4j 的价值,恰恰在于它既能承接 Java 企业应用的成熟工程体系,又能把 RAG 这类 AI 能力自然纳入既有微服务架构中。

对大多数企业团队来说,这是一条非常现实、性价比很高的落地路径。

学AI大模型的正确顺序,千万不要搞错了

🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!

有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!

就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

在这里插入图片描述

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇

学习路线:

✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经

以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!

我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐