commit
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user