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

315 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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推理 → 最终回答
```
### 架构图
```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<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. 幂等上传——元数据驱动的去重策略
```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<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 行)
```java
// 核心思想: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
- [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
```