# 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` | | 6 | `service/VectorEmbeddingService.java` | 向量化网关 | `generateEmbedding(text)` → `List` (1024-dim) | | 7 | `service/VectorIndexService.java` | 写入管道编排 | `indexSingleFile(path)` → 读→删旧→分块→向量化→写 | | 8 | `service/VectorSearchService.java` | 语义检索 | `searchSimilarDocuments(query, topK)` → `List` | | 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 └─ for each chunk: [L146] ├─ embeddingService.generateEmbedding() [L76] │ └─ DashScope TextEmbedding API → List[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{id, content, score, metadata} └─ return JSON to Agent └─ Agent 融合检索结果 + LLM推理 → 最终回答 ``` ### 架构图 ```mermaid 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
@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. 幂等上传——元数据驱动的去重策略 ```java // VectorIndexService.java:138-139 // 删除该文件的旧数据(如果存在) deleteExistingData(path.toString()); ``` `deleteExistingData()` (L173-215) 使用 `metadata["_source"] == filePath` 作为删除表达式。每次上传同一文件时,先清空旧向量再写入新数据,保证数据一致性。 #### 2. 路径标准化——跨平台一致性 ```java // VectorIndexService.java:176-178 Path path = Paths.get(filePath).normalize(); String normalizedPath = path.toString().replace(File.separator, "/"); ``` Windows `\` 和 Unix `/` 统一为正斜杠,避免 Milvus 表达式解析错误。在 `deleteExistingData()` 和 `buildMetadata()` 中均有应用。 #### 3. 重叠窗口 + 句子边界感知 ```java // 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 自行判断如何使用这些信息。 ```java // InternalDocsTools.java:68 String resultJson = objectMapper.writeValueAsString(searchResults); return resultJson; ``` 对比 `RagService.queryStream()` 则完整执行「检索→构建上下文→LLM 生成」三步,是一个独立的全栈 RAG 备用路径。 #### 5. Milvus Schema 设计 ```java // 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` | DocumentChunkService | VectorIndexService | `{content, startIndex, endIndex, chunkIndex, title}` | | `List[1024]` | VectorEmbeddingService | VectorIndexService, VectorSearchService | DashScope text-embedding-v4 输出 | | `List` | 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 行) ```java // 核心思想:Pipeline-as-Services + Agent Tool Bridge // 以下骨架可直接用于任何 Spring Boot 项目 // 1. 分块器:语义感知分割 public List 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 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 - [x] 设计真实存在 — 每个声明均有文件+行号证据 - [x] 分析深度足够 — 完整追踪了写入/读取两条全路径 - [x] 迁移示例≤20行 — 仅提取 Pipeline + Tool Bridge 骨架 - [x] 陷阱具体 — 每个都有代码规避证据 - [x] 可解释为什么优于替代方案 — 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 ```