Files
SuperBizAgent-java/docs/analysis/essence-report-rag.md
2026-05-29 21:38:16 +08:00

14 KiB
Raw Permalink Blame History

Essence Report: SuperBizAgent-java — RAG 实现

Lens: mechanical(机械论——结构、接口、数据流)
Design analyzed: RAG 管道——从文档上传到 Agent 辅助检索的完整写入/读取双路径
Files examined: 12
Pattern: Pipeline-as-Services + Agent-Mediated Retrieval
Status: complete


Phase 1: 定位 — 设计目标确认

来自 /explore 报告的「设计二:完整的 RAG 管道(5 级流水线)」。用户指定深入 RAG 实现部分。

涉及 12 个核心文件,跨越 controller → service → client → constant 四层。


Phase 2: Deep Dive — 逐文件追踪

核心文件清单

# 文件 角色 暴露接口
1 constant/MilvusConstants.java Schema 契约常量 VECTOR_DIM=1024, COLLECTION_NAME="biz"
2 client/MilvusClientFactory.java 数据库初始化 createClient() → 自动建表+建索引
3 config/DocumentChunkConfig.java 分块参数 maxSize=800, overlap=100
4 dto/DocumentChunk.java 分块实体 content, startIndex/endIndex, chunkIndex, title
5 service/DocumentChunkService.java 智能分块器 chunkDocument(content, filePath) → List<DocumentChunk>
6 service/VectorEmbeddingService.java 向量化网关 generateEmbedding(text) → List<Float> (1024-dim)
7 service/VectorIndexService.java 写入管道编排 indexSingleFile(path) → 读→删旧→分块→向量化→写
8 service/VectorSearchService.java 语义检索 searchSimilarDocuments(query, topK) → List<SearchResult>
9 service/RagService.java 全栈 RAG 问答 queryStream(question, history, callback) → SSE流式
10 agent/tool/InternalDocsTools.java Agent 工具桥 queryInternalDocs(query) → JSON(仅检索,不生文)
11 controller/FileUploadController.java 写入入口 POST /api/upload → 文件存储 + 自动索引
12 controller/ChatController.java 读取入口 POST /api/chat(_stream) → ReactAgent + 工具调用

完整的调用链(双路径)

写入路径(索引管道)

POST /api/upload
  └─ FileUploadController.upload()                          [L34]
       ├─ Files.copy() → 保存文件到 uploadPath
       └─ VectorIndexService.indexSingleFile()              [L124]
            ├─ Files.readString()                            [L135]
            ├─ deleteExistingData()                          [L173]
            │    └─ milvusClient.delete()                    [L198]
            │         expr: metadata["_source"] == "/path/to/file"
            ├─ chunkService.chunkDocument()                  [L142]
            │    ├─ splitByHeadings()                        [L61]
            │    │    └─ 正则: ^(#{1,6})\s+(.+)$
            │    ├─ chunkSection() × N                       [L104]
            │    │    ├─ splitByParagraphs()                 [L174]
            │    │    └─ getOverlapText()                    [L193]
            │    └─ → List<DocumentChunk>
            └─ for each chunk:                               [L146]
                 ├─ embeddingService.generateEmbedding()     [L76]
                 │    └─ DashScope TextEmbedding API → List<Float>[1024]
                 └─ insertToMilvus()                         [L255]
                      └─ UUID(source+chunkIndex) + vector + content + metadata(JSON)

读取路径(Agent 中介检索)

POST /api/chat_stream
  └─ ChatController.chatStream()                            [L143]
       └─ chatService.createReactAgent()                     [L183]
            └─ tools: [DateTimeTools, InternalDocsTools, QueryMetricsTools, QueryLogsTools]
       └─ agent.stream(question)                             [L189]
            └─ Agent 自主决策 → 调用 queryInternalDocs
                 └─ InternalDocsTools.queryInternalDocs()    [L53]
                      └─ VectorSearchService.searchSimilarDocuments()  [L42]
                           ├─ embeddingService.generateQueryVector()    [L47]
                           ├─ milvusClient.search()                    [L51]
                           │    └─ L2距离, IVF_FLAT, nprobe=10
                           └─ → List<SearchResult>{id, content, score, metadata}
                      └─ return JSON to Agent
                           └─ Agent 融合检索结果 + LLM推理 → 最终回答

架构图

graph TB
    subgraph 写入路径
        UPLOAD[POST /api/upload]
        FC[FileUploadController]
        VIS[VectorIndexService]
        DCS[DocumentChunkService]
        VES[VectorEmbeddingService]
        MV_W[(Milvus)]
    end

    subgraph 读取路径
        CHAT[POST /api/chat_stream]
        CC[ChatController]
        AGENT[ReactAgent]
        IDT[InternalDocsTools<br/>@Tool注解]
        VSS[VectorSearchService]
        MV_R[(Milvus)]
        LLM[DashScope LLM]
    end

    UPLOAD --> FC
    FC --> VIS
    VIS --> DCS --> VIS
    VIS --> VES --> VIS
    VIS --> MV_W

    CHAT --> CC
    CC --> AGENT
    AGENT -->|自主决策调用| IDT
    IDT --> VSS
    VSS --> VES --> VSS
    VSS --> MV_R
    IDT -->|JSON结果| AGENT
    AGENT --> LLM
    LLM -->|SSE流式| CC

关键设计决策(代码证据)

1. 幂等上传——元数据驱动的去重策略

// VectorIndexService.java:138-139
// 删除该文件的旧数据(如果存在)
deleteExistingData(path.toString());

deleteExistingData() (L173-215) 使用 metadata["_source"] == filePath 作为删除表达式。每次上传同一文件时,先清空旧向量再写入新数据,保证数据一致性。

2. 路径标准化——跨平台一致性

// VectorIndexService.java:176-178
Path path = Paths.get(filePath).normalize();
String normalizedPath = path.toString().replace(File.separator, "/");

Windows \ 和 Unix / 统一为正斜杠,避免 Milvus 表达式解析错误。在 deleteExistingData() 和 buildMetadata() 中均有应用。

3. 重叠窗口 + 句子边界感知

// DocumentChunkService.java:132-148
if (currentChunk.length() > 0 && 
    currentChunk.length() + paragraph.length() > chunkConfig.getMaxSize()) {
    // 保存当前分片
    String overlap = getOverlapText(chunkContent);  // 提取重叠文本
    currentChunk = new StringBuilder(overlap);       // 新分片以重叠文本开头

getOverlapText() (L193-213) 更进一步:在重叠文本中寻找句子边界(。?!),避免在句子中间截断。当句子边界超过 overlapSize/2 时才使用,否则退回原始重叠策略。

4. 检索与生成分离

InternalDocsTools.queryInternalDocs() 只做检索,不做生成。它将搜索结果序列化为 JSON 返回给 Agent,由 Agent 的 LLM 自行判断如何使用这些信息。

// InternalDocsTools.java:68
String resultJson = objectMapper.writeValueAsString(searchResults);
return resultJson;

对比 RagService.queryStream() 则完整执行「检索→构建上下文→LLM 生成」三步,是一个独立的全栈 RAG 备用路径。

5. Milvus Schema 设计

// MilvusClientFactory.java:109-142
// 四个字段:
//   id        VarChar(256)    主键 — UUID(source + chunkIndex)
//   vector    FloatVector(1024) — text-embedding-v4 输出
//   content   VarChar(8192)   — 分块后的文本内容
//   metadata  JSON            — {_source, _extension, _file_name, chunkIndex, totalChunks, title}
// 索引: IVF_FLAT, L2距离, nlist=128

Phase 3: Extract Pattern

设计模式:Pipeline-as-Services + Agent-Mediated Retrieval

问题: 如何将知识库文档转化为 AI Agent 可检索、可利用的语义记忆?

传统方案的问题:

  • 关键词检索:无法理解语义相似的查询
  • 硬编码 FAQ:无法应对未见过的问题
  • 直接向量检索 + 固定提示词:所有问题都触发检索,浪费资源

本项目的方案:两阶段架构

┌──────────────────────────────────────────────────┐
│  STAGE 1: 写入管道 (离线/上传时触发)                │
│                                                    │
│  文档 ──→ 智能分块 ──→ 向量化 ──→ Milvus存储      │
│         (标题+段落     (text-embedding  (IVF_FLAT   │
│          边界感知)      -v4, 1024-dim)   L2索引)     │
│                                                    │
│  接口契约:                                         │
│    IN:  File → OUT: N × (vector + content + meta)  │
└──────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────┐
│  STAGE 2: 读取管道 (Agent 决策时触发)               │
│                                                    │
│  用户问题 ──→ Agent 思考 ──→ 决定查知识库          │
│                              │                     │
│                              ▼                     │
│              向量检索 (L2距离) ──→ Top-K 文档片段   │
│                              │                     │
│                              ▼                     │
│              Agent 融合检索结果 + LLM推理 → 回答    │
│                                                    │
│  接口契约:                                         │
│    IN:  query(自然语言) → OUT: JSON(检索结果)       │
│    Agent 自主决定: 是否调用 / 如何使用结果           │
└──────────────────────────────────────────────────┘

接口契约(隐式——通过 Spring DI 实现)

契约 生产者 消费者 数据形状
List<DocumentChunk> DocumentChunkService VectorIndexService {content, startIndex, endIndex, chunkIndex, title}
List<Float>[1024] VectorEmbeddingService VectorIndexService, VectorSearchService DashScope text-embedding-v4 输出
List<SearchResult> VectorSearchService InternalDocsTools, RagService {id, content, score, metadata}
StreamCallback RagService (外部调用者) {onSearchResults, onContentChunk, onComplete, onError}

替代方案对比

方案 本项目 LangChain4j 纯 DashScope API
分块策略 标题感知 + 段落边界 + 句子重叠 多种内置 Splitter 无,需自建
向量库 Milvus (IVF_FLAT) 多后端支持 无
Agent 集成 Spring AI @Tool 注解,Agent 自主决策 AiServices + @Tool 无 Agent 框架
去重 metadata["_source"] 匹配删除 需自定义 不适用

为什么选择这种设计?

  1. 「检索」和「生成」分离:InternalDocsTools 只返回检索结果,生成由 Agent 的 LLM 完成。Agent 可以选择不使用检索结果(如果检索质量不高),或者交叉验证多次检索的结果
  2. 工具化 RAG:将 RAG 暴露为 Agent 工具而非独立 API,让 Agent 在合适的时机触发检索——而非对所有问题都做 RAG
  3. 5 个独立 Service:每个阶段可单独替换。想换分块策略?只改 DocumentChunkService。想换向量库?只改 VectorSearchService + VectorIndexService

Phase 4: Migrate — 可迁移的设计

可迁移性评估

这个 RAG 设计高度可迁移到任何需要「知识库 + AI Agent」的 Java 项目。核心依赖是 Spring AI 生态 + 一个向量数据库。

Steal-it 示例(12 行)

// 核心思想:Pipeline-as-Services + Agent Tool Bridge
// 以下骨架可直接用于任何 Spring Boot 项目

// 1. 分块器:语义感知分割
public List<Chunk> chunk(String doc) {
    return splitByHeadings(doc).stream()
        .flatMap(s -> splitToFit(s, MAX_SIZE, OVERLAP))
        .toList();
}

// 2. Agent 工具桥:检索但不生文
@Component
class KnowledgeBaseTool {
    @Tool(description = "搜索内部知识库获取相关信息")
    public String search(@ToolParam(description="查询内容") String query) {
        List<Float> qv = embedder.embed(query);         // 向量化
        var results = vectorDB.search(qv, TOP_K);        // 语义检索
        return toJson(results);                          // 返回给Agent
    }
}

落地陷阱

陷阱 说明 本项目如何规避
路径分隔符不一致 Windows \ vs Unix / 导致 Milvus 表达式解析失败 VectorIndexService.java:177 强制 replace(File.separator, "/")
重复上传污染数据 同一文件多次上传产生重复向量 VectorIndexService.java:138-139 delete-before-insert
分块边界截断语义 固定长度切割可能切断句子 DocumentChunkService.java:203-206 在重叠区找句子边界
Agent 未触发工具 Agent 不知道何时该查知识库 InternalDocsTools.java:49-52 @Tool description 用英文详细描述触发场景
向量维度不匹配 embedding 模型输出维度与 Milvus schema 不一致 MilvusConstants.java:18 集中管理 VECTOR_DIM=1024
API Key 未初始化 静态 Constants 被其他线程覆盖 VectorEmbeddingService.java:86-89 每次调用前检查并修复

Self-review

  • 设计真实存在 — 每个声明均有文件+行号证据
  • 分析深度足够 — 完整追踪了写入/读取两条全路径
  • 迁移示例≤20行 — 仅提取 Pipeline + Tool Bridge 骨架
  • 陷阱具体 — 每个都有代码规避证据
  • 可解释为什么优于替代方案 — Agent 自主决策 vs 强制 RAG

Essence Report: SuperBizAgent-java
Lens: mechanical
Design analyzed: RAG 管道 — Pipeline-as-Services + Agent-Mediated Retrieval
Files examined: 12
Pattern: Pipeline-as-Services + Agent Tool Bridge
Migration: 12-line steal-it skeleton
HTML generated: no
Status: complete