315 lines
14 KiB
Markdown
315 lines
14 KiB
Markdown
# 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
|
||
```
|