企业智能知识库助手落地实践:从RAG到Multi-Agent

一次完整的AI工程化落地之旅,记录从零到一构建企业级智能知识库系统的全过程
技术栈: Spring AI Alibaba + Vue 3 + ElasticSearch + Redis


目录


一、引言:为什么需要企业智能知识库?

1.1 企业知识管理的痛点

在数字化转型的浪潮中,每个企业都积累了海量的知识资产:技术文档、业务规范、API文档、FAQ、代码规范等等。但这些知识往往面临三大挑战:

  1. 分散难查找: 知识分散在Confluence、GitLab、Notion、Word等多个平台
  2. 获取门槛高: 新员工需要花费大量时间熟悉文档,技术人员反复查阅规范
  3. 更新不及时: 文档版本管理混乱,员工不知道哪个是最新版本

1.2 AI带来的范式转变

传统的知识管理是关键词检索:用户输入关键词→系统返回匹配文档→用户自己阅读理解。这种方式效率低、体验差。

而基于AI的知识助手实现了范式转变:用户提出问题→AI理解意图→检索相关知识→生成准确答案。用户获得的不再是文档列表,而是直接可用的答案。

1.3 本文要解决的问题

本文将分享我们从零到一构建企业智能知识库助手的完整经验,包括:

  • 技术选型: 为什么选择Spring AI Alibaba而非LangChain4j?
  • 架构设计: 如何将RAG技术融入微服务架构?
  • 核心实现: 文档向量化、语义检索、答案生成的完整流程
  • 高级特性: Multi-Agent协作、Graph Workflow、Function Call
  • 工程化: 性能优化、监控告警、部署运维的最佳实践

二、核心概念:深入理解RAG技术栈

在开始实践之前,我们需要理解几个关键概念。我尽量用通俗的语言解释,避免晦涩的学术术语。

2.1 什么是RAG?

RAG(Retrieval-Augmented Generation) 直译是"检索增强生成",听起来很玄乎,其实原理很简单:

传统LLM: 直接问AI→AI基于训练数据回答→可能出现幻觉(编造答案)

RAG模式: 先从知识库检索相关内容→把检索结果喂给AI→AI基于检索内容回答→答案准确可溯源

用一个比喻:传统LLM像考试时靠记忆答题,RAG像开卷考试,允许翻书找答案。

2.2 Embedding与向量化

Embedding是将文本转换为向量(一串数字)的过程。为什么要转成向量?因为:

  1. 语义相似度: 意思相近的文本,向量也相近
  2. 高效检索: 向量数据库可以快速找出最相似的向量

举个例子:

复制代码
"数据库表设计规范" → [0.12, 0.34, 0.56, ..., 0.78] (1536维向量)
"MySQL建表规范"    → [0.13, 0.35, 0.55, ..., 0.77] (相似度>0.95)
"天气怎么样"       → [0.89, 0.21, 0.05, ..., 0.32] (相似度<0.1)

2.3 向量数据库

向量数据库专门用来存储和检索向量,常见的有:

  • ElasticSearch: 我们的选择,同时支持向量检索和全文检索
  • Milvus: 专业向量数据库,性能更高但运维复杂
  • Pinecone: 云服务,按量付费

我们选择ElasticSearch因为它能同时做向量检索 (语义相似度)和全文检索(关键词匹配),两者结合效果更好。

2.4 RAG工作流程

让我用一个完整的流程图展示RAG的工作原理:
文档上传
检索
用户提问: 数据库表设计规范?
问题向量化
向量数据库检索Top-5
获取相关文档片段
构建Prompt
LLM生成回答
返回答案+来源
知识库
文档解析
智能分块
向量化
向量数据库

2.5 核心技术点

理解了RAG后,实现企业知识库需要掌握几个技术点:

  1. 文档解析: PDF/Word/Markdown等格式的文本提取
  2. 智能分块: 将长文档切成合适大小的片段(chunk)
  3. 向量化: 使用Embedding模型生成向量
  4. 混合检索: 向量检索+全文检索,提升准确率
  5. Prompt工程: 设计合理的提示词,让AI生成高质量答案

三、技术选型:为什么选择Spring AI Alibaba?

3.1 主流方案对比

实现RAG应用有多种选择,我对比了三种主流方案:

方案 优点 缺点 适用场景
LangChain(Python) 生态最成熟,功能最全 与Java技术栈割裂,需要跨语言调用 Python项目
LangChain4j Java原生,社区活跃 需要大量代码组装,缺少开箱即用的RAG 对灵活性要求高的项目
Spring AI Alibaba 与Spring无缝集成,RAG开箱即用 相对较新,文档较少 Java+Spring项目(我们的情况)

3.2 为什么选Spring AI Alibaba?

我们最终选择Spring AI Alibaba,主要基于以下考虑:

1. 原生Spring体验
java 复制代码
// Spring AI Alibaba - 原生Spring风格
@Autowired
private ChatClient chatClient;

@Autowired
private VectorStore vectorStore;

public String chat(String question) {
    return chatClient.prompt()
        .user(question)
        .advisors(new QuestionAnswerAdvisor(vectorStore))  // 一行代码启用RAG
        .call()
        .content();
}

无需学习新的API,Spring开发者零门槛上手。

2. RAG开箱即用

Spring AI Alibaba内置完整的RAG Workflow:

  • DocumentReader: 支持PDF、Word、Markdown等格式解析
  • TextSplitter: 智能文档分块
  • EmbeddingModel: 向量化模型
  • VectorStore: 向量数据库集成
  • QuestionAnswerAdvisor: RAG增强器

这些组件开箱即用,不需要从零实现。

3. 阿里云生态
  • 通义千问: 国产大模型,价格实惠(¥0.004/1K tokens)
  • 文本向量化: text-embedding-v3模型,1536维向量
  • 企业级稳定性: 阿里云背书,经过大规模生产验证
4. 对比LangChain4j
Spring AI Alibaba LangChain4j
集成难度 ⭐⭐⭐⭐⭐ 声明式配置 ⭐⭐⭐ 需要手动组装
RAG能力 ⭐⭐⭐⭐⭐ 开箱即用 ⭐⭐⭐⭐ 功能完整但需组装
向量数据库 ⭐⭐⭐⭐⭐ 内置ES/Milvus ⭐⭐⭐⭐ 支持但需配置
可观测性 ⭐⭐⭐⭐⭐ SDK级调用链 ⭐⭐⭐ 需手动埋点
企业特性 ⭐⭐⭐⭐⭐ 生产级稳定 ⭐⭐⭐ 社区驱动

3.3 整体技术栈

最终确定的技术栈:

后端:

  • Spring Boot 3.3.0 + Spring Cloud 2023.0.2
  • Spring AI Alibaba 1.0.0.2-RC1
  • MySQL 8.4.0 + MyBatis-Plus 3.5.6
  • Redis 7.0 (会话缓存)
  • ElasticSearch 8.11.0 (向量+全文检索)
  • 通义千问qwen-plus (对话模型)
  • text-embedding-v3 (向量模型)

前端:

  • Vue 3.5 + TypeScript 5.7
  • Element Plus 2.9 (UI组件)
  • Pinia 2.2 (状态管理)
  • Vite 6.4 (构建工具)

四、架构设计:微服务+RAG的完美融合

4.1 整体架构图

让我用一个完整的架构图展示系统设计:
外部服务
数据层
Spring AI Alibaba核心
应用层
接入层
前端层
调用
存储
调用
检索
Vue 3 Web应用
移动端H5
API Gateway
知识管理服务
对话服务
Prompt管理
ChatClient
VectorStore
EmbeddingModel
QuestionAnswerAdvisor
MySQL

业务数据
Redis

会话缓存
ElasticSearch

向量索引
OSS

文件存储
通义千问

qwen-plus
Embedding

text-embedding-v3

4.2 分层架构设计

我们采用经典的DDD分层架构:

复制代码
Controller层 → Service层 → Repository层 → Mapper层 → Database
     ↓            ↓            ↓
  参数校验    业务逻辑    数据持久化
  返回Response  事务管理    lambda查询

4.3 微服务划分

虽然是单一功能,但我们预留了拆分空间:

当前 : 单一微服务 mythos-knowledge-assistant

未来可拆分为:

  • knowledge-management-service: 知识管理
  • chat-service: 对话服务
  • retrieval-service: 检索服务
  • prompt-service: Prompt管理

为什么当前不拆分?

  • 业务内聚性高,强行拆分会增加复杂度
  • 避免过度设计,先快速迭代验证
  • 单一服务性能足够(QPS 500+)

4.4 数据模型设计

核心表结构

contains
contains
knowledge_document
bigint
id
PK
varchar
doc_id
UK
varchar
doc_title
tinyint
doc_type
varchar
category
int
chunk_count
tinyint
status
knowledge_chunk
bigint
id
PK
varchar
chunk_id
UK
varchar
doc_id
FK
text
content
varchar
vector_id
int
chunk_tokens
chat_session
bigint
id
PK
varchar
session_id
UK
bigint
user_id
varchar
session_title
tinyint
session_status
int
message_count
chat_message
bigint
id
PK
varchar
message_id
UK
varchar
session_id
FK
varchar
role
text
content
text
retrieved_docs
tinyint
satisfaction

存储分离策略
  • MySQL: 业务数据(文档元数据、对话历史)
  • ElasticSearch: 向量索引(1536维向量+全文检索)
  • Redis: 会话缓存(最近3轮对话、用户状态)
  • COS: 原始文件(PDF、Word等)

这样设计的好处:

  • 各存储引擎发挥所长
  • MySQL不存储大文本,性能更好
  • 向量检索由ES优化,速度快

五、核心实现:文档向量化全流程

文档向量化是知识库的基础,我将详细拆解整个流程。

5.1 文档上传流程

MySQL ElasticSearch EmbeddingModel 腾讯云COS DocumentService Controller MySQL ElasticSearch EmbeddingModel 腾讯云COS DocumentService Controller chunk_size=500 overlap=100 用户 上传PDF文档 参数校验(格式/大小) uploadDocument() 上传原始文件 返回文件URL 解析文档(PDFReader) 智能分块(TextSplitter) 批量向量化 返回1536维向量 存储向量+文本 保存元数据 返回doc_id 上传成功 用户

5.2 代码实现详解

1. 文档解析

Spring AI Alibaba提供了多种DocumentReader:

java 复制代码
@Service
public class DocumentServiceImpl implements DocumentService {

    @Autowired
    private VectorStore vectorStore;

    @Autowired
    private TextSplitter textSplitter;

    @Autowired
    private TencentCosService cosService;

    public Response<String> uploadDocument(MultipartFile file,
                                          String title,
                                          Integer docType,
                                          String category) {

        // 1. 上传文件到COS
        String fileUrl = cosService.uploadFile(file);

        // 2. 根据文件类型选择解析器
        List<Document> documents = parseDocument(file);

        // 3. 智能分块
        List<Document> chunks = textSplitter.apply(documents);

        // 4. 向量化并存储到ElasticSearch
        vectorStore.add(chunks);

        // 5. 保存元数据到MySQL
        String docId = saveMetadata(title, docType, category,
                                   fileUrl, chunks.size());

        return Response.success(docId);
    }

    private List<Document> parseDocument(MultipartFile file) {
        String filename = file.getOriginalFilename();
        Resource resource = new MultipartFileAdapter(file);

        DocumentReader reader;
        if (filename.endsWith(".pdf")) {
            // PDF解析
            reader = new PagePdfDocumentReader(resource);
        } else if (filename.endsWith(".docx") || filename.endsWith(".doc")) {
            // Word解析(使用Apache Tika)
            reader = new TikaDocumentReader(resource);
        } else if (filename.endsWith(".md")) {
            // Markdown解析
            reader = new TextResourceDocumentReader(resource);
        } else {
            throw new BusinessException("不支持的文件格式");
        }

        return reader.get();
    }
}
2. 智能分块策略

为什么要分块?

  • LLM有上下文长度限制(如8K tokens)
  • 小块更精准,大块包含噪音
  • 合理分块能平衡精度和召回率

我们的分块配置:

java 复制代码
@Bean
public TextSplitter textSplitter() {
    return new TokenTextSplitter(
        500,   // 每个chunk的Token数量
        100    // chunk之间的重叠(保持语义连贯)
    );
}

为什么重叠100 tokens?

假设一个规范分散在chunk边界:

复制代码
Chunk 1: ...索引设计原则:1.高区分度字段在前 2.联合
Chunk 2: 索引最多5个字段 3.避免使用函数索引...

如果没有重叠,用户问"联合索引最多几个字段",可能检索不到。有100 tokens重叠,Chunk 1的结尾会包含在Chunk 2的开头,确保完整性。

3. 向量化与存储
java 复制代码
// Spring AI Alibaba自动处理向量化
vectorStore.add(chunks);  // 一行代码完成:向量化+存储

底层发生了什么?

  1. 调用text-embedding-v3生成向量
  2. 将向量存入ElasticSearch的dense_vector字段
  3. 同时存储原文到text字段(用于全文检索)

ElasticSearch索引结构:

json 复制代码
{
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "analyzer": "ik_max_word"  // 中文分词
      },
      "embedding": {
        "type": "dense_vector",
        "dims": 1536,
        "index": true,
        "similarity": "cosine"  // 余弦相似度
      },
      "doc_id": {"type": "keyword"},
      "section_title": {"type": "text"}
    }
  }
}

5.3 性能优化点

1. 批量向量化
java 复制代码
// ❌ 不好:逐个向量化,慢
for (Document doc : documents) {
    vectorStore.add(List.of(doc));
}

// ✅ 好:批量向量化,快
vectorStore.add(documents);  // 底层会批量调用Embedding API

批量调用可以减少网络开销,提升5-10倍性能。

2. 异步处理

文档向量化是耗时操作(10MB PDF需要30-60秒),我们采用异步处理:

java 复制代码
@Async
public CompletableFuture<String> uploadDocumentAsync(MultipartFile file) {
    String docId = uploadDocument(file, ...);
    return CompletableFuture.completedFuture(docId);
}

用户上传后立即返回,后台异步处理。前端轮询状态:

javascript 复制代码
// 上传文档
const { docId } = await uploadDocument(formData);

// 轮询处理状态
const interval = setInterval(async () => {
    const { status } = await getDocumentStatus(docId);
    if (status === 'COMPLETED') {
        clearInterval(interval);
        ElMessage.success('文档处理完成!');
    }
}, 2000);

六、智能对话:RAG检索增强生成

文档向量化完成后,就可以进行智能问答了。

6.1 对话流程



用户提问
是否新会话?
创建session_id
加载历史对话
问题向量化
向量检索Top-5
相似度过滤>0.7
构建RAG Prompt
LLM生成回答
提取来源文档
推荐相关问题
保存对话历史
返回用户

6.2 核心代码实现

java 复制代码
@Service
public class ChatServiceImpl implements ChatService {

    @Autowired
    private ChatClient chatClient;  // Spring AI提供

    @Autowired
    private SessionService sessionService;

    @Autowired
    private ChatMessageRepository messageRepository;

    public Response<ChatResponseDTO> chat(ChatRequest request) {
        long startTime = System.currentTimeMillis();

        // 1. 获取或创建会话
        String sessionId = sessionService.getOrCreateSession(
            request.getUserId(), request.getSessionId());

        // 2. 加载对话历史(最近3轮)
        List<ChatMessage> history = sessionService.getRecentMessages(sessionId, 3);

        // 3. 构建Prompt
        Prompt prompt = buildPrompt(request.getQuestion(), history);

        // 4. RAG增强的对话 (核心)
        ChatResponse chatResponse = chatClient.prompt(prompt)
            .advisors(questionAnswerAdvisor)  // 启用RAG
            .call();

        // 5. 提取答案和来源
        String answer = chatResponse.getResult().getOutput().getContent();
        List<DocumentSourceDTO> sources = extractSources(chatResponse);

        // 6. 推荐相关问题
        List<String> relatedQuestions = generateRelatedQuestions(
            request.getQuestion(), answer);

        // 7. 保存对话历史
        saveChatHistory(sessionId, request.getQuestion(), answer, sources);

        // 8. 组装响应
        return Response.success(ChatResponseDTO.builder()
            .sessionId(sessionId)
            .answer(answer)
            .sources(sources)
            .relatedQuestions(relatedQuestions)
            .responseTime(System.currentTimeMillis() - startTime)
            .build());
    }
}

6.3 Prompt工程

Prompt是RAG的灵魂,我们精心设计了多层Prompt:

1. 系统Prompt (定义角色)
text 复制代码
你是神话公司的企业智能知识助手,名为"小智"。
你的职责是帮助员工快速、准确地获取企业内部知识。

【你的能力】
- 回答技术规范、业务流程、操作指引等问题
- 提供代码示例、配置示例
- 推荐相关文档和最佳实践

【你的原则】
1. 只基于知识库内容回答,不编造信息
2. 如果知识库中没有相关信息,明确告知用户
3. 回答要准确、完整、易懂,使用Markdown格式
4. 标注答案来源(文档名称和章节)
5. 保护用户隐私和企业信息安全
2. RAG Prompt (检索内容注入)

这部分由Spring AI Alibaba自动生成:

text 复制代码
【知识库内容】
{context}  // 自动填充检索到的Top-5文档

【用户问题】
{question}

【回答要求】
1. 基于上述知识库内容回答
2. 如果知识库中没有相关信息,回答"知识库中暂无相关信息"
3. 使用Markdown格式组织答案
4. 标注答案来源
3. 相关问题生成Prompt
java 复制代码
private List<String> generateRelatedQuestions(String question, String answer) {
    String prompt = String.format("""
        基于用户的问题和你的回答,生成3个相关的问题,引导用户深入了解。

        用户问题:%s
        你的回答:%s

        要求:
        1. 问题要有递进关系(从基础到进阶)
        2. 问题要具体,不要太宽泛
        3. 只返回问题,不要编号,每行一个
        """, question, answer);

    String result = chatClient.prompt().user(prompt).call().content();
    return Arrays.asList(result.split("\n"));
}

6.4 混合检索策略

纯向量检索有时会漏掉关键词匹配,我们采用混合检索:

java 复制代码
@Service
public class RetrievalServiceImpl implements RetrievalService {

    @Autowired
    private VectorStore vectorStore;

    @Autowired
    private ElasticsearchClient esClient;

    public List<Document> hybridSearch(String question, int topK) {

        // 1. 向量检索(语义相似度)
        List<Document> vectorResults = vectorStore.similaritySearch(
            SearchRequest.query(question).withTopK(topK * 2)
        );

        // 2. 全文检索(关键词匹配)
        List<Document> fullTextResults = fullTextSearch(question, topK * 2);

        // 3. 结果融合:向量70% + 全文30%
        Map<String, SearchResult> merged = new HashMap<>();

        for (Document doc : vectorResults) {
            float score = doc.getScore() * 0.7f;
            merged.put(doc.getId(), new SearchResult(doc, score));
        }

        for (Document doc : fullTextResults) {
            float score = doc.getScore() * 0.3f;
            merged.merge(doc.getId(), new SearchResult(doc, score),
                (old, newVal) -> {
                    old.setScore(old.getScore() + newVal.getScore());
                    return old;
                });
        }

        // 4. 排序并返回Top-K
        return merged.values().stream()
            .sorted(Comparator.comparing(SearchResult::getScore).reversed())
            .limit(topK)
            .map(SearchResult::getDocument)
            .collect(Collectors.toList());
    }

    private List<Document> fullTextSearch(String question, int topK) {
        // 使用ElasticSearch的BM25算法
        SearchRequest request = SearchRequest.of(s -> s
            .index("knowledge_vector")
            .query(q -> q.multiMatch(m -> m
                .query(question)
                .fields("content^3", "doc_title^2", "section_title^2")
            ))
            .size(topK)
        );

        SearchResponse<Document> response = esClient.search(request, Document.class);
        return response.hits().hits().stream()
            .map(Hit::source)
            .collect(Collectors.toList());
    }
}

混合检索的效果提升:

实际测试中,混合检索比纯向量检索准确率提升15-20%,特别是对包含专业术语的问题效果明显。

6.5 会话管理与上下文

多轮对话的关键是维护上下文:

Redis缓存设计
复制代码
# 会话基本信息(Hash)
Key: session:{session_id}
TTL: 15分钟
Fields:
  - user_id: 用户ID
  - session_id: 会话ID
  - message_count: 消息数量
  - last_activity: 最后活动时间

# 对话历史(List,最多10条)
Key: session:history:{session_id}
TTL: 15分钟
Value: [message1, message2, ..., message10]

# 用户当前会话映射(String)
Key: user:session:{user_id}
TTL: 30分钟
Value: session_id
代码实现
java 复制代码
@Service
public class SessionServiceImpl implements SessionService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String SESSION_KEY = "session:";
    private static final String HISTORY_KEY = "session:history:";
    private static final int SESSION_TTL = 15 * 60; // 15分钟

    public List<ChatMessage> getRecentMessages(String sessionId, int limit) {
        String key = HISTORY_KEY + sessionId;

        // 从Redis获取最近N条
        List<Object> messages = redisTemplate.opsForList()
            .range(key, -limit, -1);

        if (CollectionUtils.isEmpty(messages)) {
            // Redis没有,从MySQL加载
            messages = loadFromDatabase(sessionId, limit);
            // 回填Redis
            if (!messages.isEmpty()) {
                redisTemplate.opsForList().rightPushAll(key, messages);
                redisTemplate.expire(key, SESSION_TTL, TimeUnit.SECONDS);
            }
        }

        return messages.stream()
            .map(obj -> (ChatMessage) obj)
            .collect(Collectors.toList());
    }

    public void saveMessage(ChatMessage message) {
        // 1. MySQL持久化
        messageRepository.saveCustom(message);

        // 2. Redis缓存(最近10条)
        String key = HISTORY_KEY + message.getSessionId();
        redisTemplate.opsForList().rightPush(key, message);

        Long size = redisTemplate.opsForList().size(key);
        if (size > 10) {
            redisTemplate.opsForList().trim(key, -10, -1);
        }

        // 3. 刷新TTL
        redisTemplate.expire(key, SESSION_TTL, TimeUnit.SECONDS);
    }
}

七、高级特性:Multi-Agent协作框架

简单的RAG已经能解决80%的问题,但对于复杂场景,我们引入了Multi-Agent协作。

7.1 什么是Multi-Agent?

传统RAG是单一智能体:检索→生成,一气呵成。

Multi-Agent是多智能体协作:每个Agent负责特定任务,协作完成复杂目标。

举个例子,用户问:"帮我分析一下订单系统的数据库设计,并给出优化建议"

单一Agent: 检索相关文档→生成回答(可能不够全面)

Multi-Agent:

  1. QueryAgent: 理解问题,拆解为子任务
  2. RetrievalAgent: 检索订单相关的表设计文档
  3. AnalysisAgent: 分析当前设计的问题
  4. RecommendAgent: 基于最佳实践给出优化建议
  5. AnswerAgent: 整合所有结果,生成完整回答

7.2 Agent架构设计

我们基于Spring AI Alibaba的Graph框架实现Multi-Agent:

java 复制代码
public interface Agent {
    /**
     * Agent名称
     */
    String getName();

    /**
     * Agent能力描述
     */
    String getDescription();

    /**
     * 执行Agent任务
     */
    AgentContext execute(AgentContext context);

    /**
     * 判断是否应该执行该Agent
     */
    boolean shouldExecute(AgentContext context);
}
实现的Agent
  1. QueryAgent: 问题理解与拆解
  2. RetrievalAgent: 知识检索
  3. AnswerAgent: 答案生成
  4. QualityAgent: 答案质量检查

7.3 Graph Workflow

使用Spring AI的Graph框架编排Agent:

java 复制代码
@Service
public class KnowledgeGraphService {

    @Autowired
    private QueryAgent queryAgent;

    @Autowired
    private RetrievalAgent retrievalAgent;

    @Autowired
    private AnswerAgent answerAgent;

    @Autowired
    private QualityAgent qualityAgent;

    public ChatResponse processWithGraph(String question) {

        // 定义Graph Workflow
        var graph = Graph.builder(ChatState::new)
            // 节点1:问题分析
            .addNode("query", context -> {
                return queryAgent.execute(context);
            })
            // 节点2:知识检索
            .addNode("retrieval", context -> {
                return retrievalAgent.execute(context);
            })
            // 节点3:答案生成
            .addNode("answer", context -> {
                return answerAgent.execute(context);
            })
            // 节点4:质量检查
            .addNode("quality", context -> {
                return qualityAgent.execute(context);
            })
            // 定义边(执行顺序)
            .addEdge("query", "retrieval")
            .addEdge("retrieval", "answer")
            .addEdge("answer", "quality")
            // 条件边:质量不合格重新生成
            .addConditionalEdge("quality", context -> {
                return context.getQualityScore() > 0.8
                    ? "END"  // 质量合格,结束
                    : "retrieval";  // 质量不合格,重新检索
            })
            .build();

        // 执行Graph
        ChatState initialState = new ChatState();
        initialState.setQuestion(question);

        ChatState finalState = graph.invoke(initialState);

        return finalState.getResponse();
    }
}

7.4 Graph可视化

Graph执行流程:
用户提问
拆解子问题
检索到Top-5文档
生成初步答案
质量<0.8,重新检索
质量≥0.8,返回答案
QueryAgent
RetrievalAgent
AnswerAgent
QualityAgent
理解问题意图

识别是否需要工具

拆解复杂问题
混合检索(向量+全文)

结果重排序

上下文扩展
基于检索结果生成

添加代码示例

标注来源
检查答案完整性

验证答案准确性

评估用户满意度

7.5 实际效果对比

测试问题: "帮我分析订单系统的数据库设计,有哪些优化点?"

单一RAG:

  • 检索时间: 200ms
  • 生成时间: 1.5s
  • 总时间: 1.7s
  • 答案质量: 7/10 (只给出了表设计,没有优化建议)

Multi-Agent:

  • QueryAgent: 100ms (拆解为"表设计"+"性能优化"两个子问题)
  • RetrievalAgent: 300ms (检索更全面)
  • AnswerAgent: 2s
  • QualityAgent: 500ms
  • 总时间: 2.9s
  • 答案质量: 9/10 (完整分析+具体优化建议+示例代码)

结论: Multi-Agent响应时间增加70%,但答案质量提升28%,值得!


八、工程化实践:从开发到生产

理论再好,也要能跑起来。这一章分享工程化实践。

8.1 性能优化

1. 向量检索缓存

相同问题重复查询ES浪费资源,我们用Redis缓存:

java 复制代码
@Service
public class RetrievalServiceImpl implements RetrievalService {

    @Autowired
    private RedisTemplate<String, List<Document>> redisTemplate;

    public List<Document> vectorSearchWithCache(String question, int topK) {
        // 计算问题的MD5哈希
        String cacheKey = "vector:cache:" + DigestUtils.md5Hex(question);

        // 尝试从缓存获取
        List<Document> cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            log.info("向量检索命中缓存, question={}", question);
            return cached;
        }

        // 缓存未命中,查询ES
        List<Document> results = vectorStore.similaritySearch(
            SearchRequest.query(question).withTopK(topK)
        );

        // 存入缓存,TTL=1小时
        redisTemplate.opsForValue().set(cacheKey, results, 1, TimeUnit.HOURS);

        return results;
    }
}

效果: 缓存命中率35%,这部分请求延迟从200ms降至5ms。

2. 批量向量化
java 复制代码
// ❌ 低效:逐个向量化
for (Document doc : documents) {
    embeddingModel.embed(doc.getContent());
}

// ✅ 高效:批量向量化
List<String> texts = documents.stream()
    .map(Document::getContent)
    .collect(Collectors.toList());

List<float[]> embeddings = embeddingModel.embed(texts);  // 批量调用

效果: 1000个chunk向量化时间从5分钟降至30秒。

3. 异步文档处理
java 复制代码
@Service
public class DocumentServiceImpl {

    @Async("taskExecutor")
    public CompletableFuture<String> uploadDocumentAsync(MultipartFile file) {
        try {
            String docId = uploadDocument(file, ...);
            return CompletableFuture.completedFuture(docId);
        } catch (Exception e) {
            return CompletableFuture.failedFuture(e);
        }
    }
}

@Configuration
public class AsyncConfig {
    @Bean("taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("doc-upload-");
        executor.initialize();
        return executor;
    }
}

8.2 监控与可观测性

Spring AI Alibaba内置可观测性,自动记录:

yaml 复制代码
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: "*"
  metrics:
    export:
      prometheus:
        enabled: true
  tracing:
    sampling:
      probability: 1.0  # 生产环境建议0.1

spring:
  ai:
    observability:
      enabled: true

自动追踪的指标:

  • 模型调用: prompt、response、tokens、耗时
  • 向量检索: query、results、similarity、耗时
  • 工具调用: function name、params、result
Grafana Dashboard

我们配置的监控大盘:

核心指标:

  • QPS: 每秒请求数
  • P95响应时间: <3秒
  • 成功率: >99%
  • LLM调用耗时: <2秒
  • 向量检索耗时: <500ms

告警规则:

yaml 复制代码
# Prometheus告警
groups:
  - name: knowledge_assistant
    rules:
      - alert: HighResponseTime
        expr: http_request_duration_seconds{job="knowledge-assistant"} > 3
        for: 5m
        annotations:
          summary: "响应时间过高"
          description: "P95响应时间超过3秒"

      - alert: LowSuccessRate
        expr: http_request_success_rate{job="knowledge-assistant"} < 0.99
        for: 5m
        annotations:
          summary: "成功率过低"
          description: "接口成功率低于99%"

8.3 部署方案

Docker Compose(开发/测试环境)
yaml 复制代码
version: '3.8'

services:
  knowledge-assistant:
    build: .
    ports:
      - "8092:8092"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
      - MYSQL_HOST=mysql
      - REDIS_HOST=redis
      - ELASTICSEARCH_URIS=http://elasticsearch:9200
    depends_on:
      - mysql
      - redis
      - elasticsearch

  mysql:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_DATABASE=knowledge_assistant
    volumes:
      - mysql-data:/var/lib/mysql

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=true
      - ELASTIC_PASSWORD=${ES_PASSWORD}
    volumes:
      - es-data:/usr/share/elasticsearch/data

volumes:
  mysql-data:
  es-data:
Kubernetes(生产环境)
yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: knowledge-assistant
spec:
  replicas: 3
  selector:
    matchLabels:
      app: knowledge-assistant
  template:
    metadata:
      labels:
        app: knowledge-assistant
    spec:
      containers:
      - name: knowledge-assistant
        image: registry.mythos.com/knowledge-assistant:1.0.0
        ports:
        - containerPort: 8092
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "prod"
        - name: DASHSCOPE_API_KEY
          valueFrom:
            secretKeyRef:
              name: ai-secrets
              key: dashscope-api-key
        resources:
          requests:
            memory: "1Gi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8092
          initialDelaySeconds: 60
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8092
          initialDelaySeconds: 30
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: knowledge-assistant-service
spec:
  selector:
    app: knowledge-assistant
  ports:
  - port: 80
    targetPort: 8092
  type: ClusterIP

8.4 成本优化

1. LLM成本

通义千问定价:

  • qwen-turbo: ¥0.003/1K tokens (便宜但效果一般)
  • qwen-plus: ¥0.004/1K tokens (性价比最高)
  • qwen-max: ¥0.02/1K tokens (最强但贵)

我们的策略:

  • 简单问答: qwen-plus
  • 复杂推理: qwen-max
  • Prompt优化: 减少冗余内容,降低token消耗

实测: 平均每次对话消耗1500 tokens,成本¥0.006/次,月活1000用户,月成本约¥180。

2. 向量存储成本

ElasticSearch存储:

  • 1篇文档(10KB) → 20个chunks → 20个向量(1536维 × 4字节) = 120KB
  • 1000篇文档 ≈ 120MB
  • 加上全文索引和元数据,总计约200MB

相比专业向量数据库Milvus,ES更省钱(无需额外部署)。


九、实战经验:踩过的坑与解决方案

理论很美好,实践很骨感。分享我们踩过的坑。

9.1 文档分块踩坑

问题: 最初用固定500字符分块,结果:

  • Markdown代码块被截断
  • 表格被切分成多个chunk
  • 检索效果差

解决: 改用TokenTextSplitter,按Token而非字符,且识别Markdown结构:

java 复制代码
// ❌ 错误:按字符分块
new CharacterTextSplitter(500, 100);

// ✅ 正确:按Token分块,识别Markdown
new TokenTextSplitter(500, 100);

9.2 向量检索召回率低

问题: 用户问"金额字段用什么类型",检索不到"金额必须用DECIMAL"的文档。

原因: 问题和文档的表述差异大,向量相似度不高。

解决: 混合检索(向量70% + 全文30%)

java 复制代码
// 全文检索能匹配关键词"金额""字段""类型"
List<Document> fullTextResults = fullTextSearch(question, topK);

// 向量检索理解语义
List<Document> vectorResults = vectorStore.similaritySearch(...);

// 融合结果
return mergeResults(vectorResults, fullTextResults);

效果: 召回率从75%提升到92%。

9.3 会话上下文丢失

问题: 用户追问"它有哪些限制",AI回答"抱歉,我不知道'它'指什么"。

原因: 没有携带上下文。

解决: Redis缓存最近3轮对话

java 复制代码
List<ChatMessage> history = sessionService.getRecentMessages(sessionId, 3);

// 构建Prompt时加入历史
Prompt prompt = new Prompt(
    new SystemMessage(systemPrompt),
    ...history.stream().map(this::toMessage),  // 历史对话
    new UserMessage(question)  // 当前问题
);

9.4 LLM幻觉问题

问题: 知识库明明没有的内容,AI却编造答案。

原因: Prompt没有约束LLM只基于知识库回答。

解决: 严格的Prompt约束

text 复制代码
【回答要求】
1. 严格基于上述知识库内容回答
2. 如果知识库中没有相关信息,必须回答"知识库中暂无相关信息"
3. 禁止编造、猜测、假设任何内容
4. 不确定时明确告知用户

效果: 幻觉率从15%降至<3%。

9.5 性能问题

问题: 高峰期响应时间3-5秒,用户体验差。

瓶颈分析:

  • 向量检索: 200ms
  • MySQL查询历史: 300ms (热点)
  • LLM生成: 1.5s
  • 总计: 2s+

优化方案:

  1. Redis缓存会话: MySQL查询降至5ms
  2. 向量检索缓存: 命中率35%,降至5ms
  3. 异步文档处理: 不影响对话响应

效果: P95响应时间从3.5s降至1.8s。


附录

A. 相关资源

官方文档:

开源项目:

推荐阅读:

  • 《大型语言模型:从理论到实践》
  • 《RAG技术原理与实践》
  • 《企业级AI应用架构设计》
相关推荐
3***68842 小时前
Spring Boot中使用Server-Sent Events (SSE) 实现实时数据推送教程
java·spring boot·后端
C***u1762 小时前
Spring Boot问题总结
java·spring boot·后端
Elieal2 小时前
5 种方式快速创建 SpringBoot 项目
java·spring boot·后端
better_liang2 小时前
每日Java面试场景题知识点之-Java修饰符
java·访问控制·static·abstract·final·修饰符·企业级开发
rgeshfgreh2 小时前
Spring事务传播机制深度解析
java·前端·数据库
无名-CODING2 小时前
Java Spring 事务管理深度指南
java·数据库·spring
xiaolyuh1232 小时前
Spring MVC Bean 参数校验 @Validated
java·spring·mvc
蕨蕨学AI3 小时前
【Wolfram语言】45.2 真实数据集
java·数据库
予枫的编程笔记3 小时前
【Java集合】深入浅出 Java HashMap:从链表到红黑树的“进化”之路
java·开发语言·数据结构·人工智能·链表·哈希算法