05 - File 智能体

一、定位

FileReactAgent 是基于 ReAct + RAG 的文件问答智能体,专注于围绕已上传文件内容的对话式问答。核心工具是 FileContentService.loadContent,会根据文件的 embed 字段自动选择直接加载RAG 语义检索模式。

二、核心特性

特性说明
多文件类型支持 PDF / DOCX / TXT(文本) + PNG / JPG / GIF / BMP(图片)
RAG 自适应embed=1 → RAG;embed=0/null → 全量
多轮 ReAct可调用 loadContent 多次,支持追问
持久化记忆默认从 MySQL 加载最近 30 条消息
图片问答图片上传时使用 Qwen-VL 识别,存入 extractedText
引用溯源引用类型响应携带 fileid 等元数据

三、执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Client ──HTTP──→ AgentController.fileStream()


initFileReactAgent() ─── 注入 FileContentService


createPersistentChatMemory(conversationId, 30)


stream(conversationId, query, fileId)


streamInternal():
1. checkRunningTask()
2. registerTask()
3. 构造 messages:
- SystemMessage (ReactAgentPrompts.getFilePrompt())
- [optional] SystemMessage (systemPrompt)
- History (loadChatHistory)
- UserMessage("<question>...</question>")
- UserMessage("<fileid>...</fileid>")
4. saveQuestion(SaveQuestionRequest)


ReAct 循环(同 WebSearch)
- AI 决策: 调用 loadContent?
→ 是: loadContent(fileId, question)
→ embed=1: RAG 检索
→ embed=0: 直接加载
→ 否: 输出最终答案


doFinally():
- saveSessionResult() 保存到 ai_session
- taskManager.stopTask() 清理任务

四、关键代码解析

4.1 消息构建

1
2
3
4
5
6
7
8
9
10
11
12
13
// ===== 加载 System Prompt(始终放在最开始)=====
messages.add(new SystemMessage(ReactAgentPrompts.getFilePrompt()));
if (StringUtils.hasText(systemPrompt)) {
messages.add(new SystemMessage(systemPrompt));
}

// ===== 加载历史记忆 =====
loadChatHistory(conversationId, messages, true, true);

// ===== 加载文件内容或文件信息 =====
// (已废弃:loadFileContent 改为通过工具调用)
messages.add(new UserMessage("<question>" + question + "</question>"));
messages.add(new UserMessage("<fileid>" + currentFileId + "</fileid>"));

提示词约束getFilePrompt):

你的回答必须基于当前文件的内容,禁止编造信息。
文件的具体内容请必须调用 loadContent 工具来获取。

4.2 工具调用识别

1
2
3
4
5
6
7
// 如果是 loadContent 工具,解析参数并发送 thinking 消息
if (toolName.contains("loadContent")) {
JSONObject args = JSON.parseObject(argsJson);
String question = (String) args.get("question");
String loadThink = "📂 正在检索文件内容,请稍等...";
sink.tryEmitNext(createThinkingResponse(loadThink));
}

4.3 Builder 模式

1
2
3
4
5
6
7
FileReactAgent agent = FileReactAgent.builder()
.name("file react")
.chatModel(chatModel)
.tools(fileContentService) // 包装为 ToolCallback
.sessionService(sessionService)
.taskManager(taskManager)
.build();

注意:tools(...) 接受 ToolCallback...(变参)或 List<ToolCallback>

五、FileContentService 工具

tool/FileContentService.java 是 File Agent 的核心工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Tool(description = "根据文件ID加载文件内容或进行RAG语义检索...")
public String loadContent(
@ToolParam(description = "文件ID") String fileId,
@ToolParam(description = "用户的问题,用于语义检索(可选)") String question) {

// 1. 查询文件信息
var fileInfo = fileManageService.getFileInfo(fileId);

// 2. 检查处理状态
if (fileInfo.getStatus() != FileInfo.FileStatus.SUCCESS) {
return "文件处理中或处理失败,当前状态: " + fileInfo.getStatus();
}

// 3. 根据 embed 字段选择加载方式
Integer embed = fileInfo.getEmbed();
if (embed != null && embed == 1) {
return retrieveWithRAG(fileId, fileInfo, question); // RAG
} else {
return loadDirectly(fileId, fileInfo); // 全量
}
}

5.1 加载策略

场景触发条件行为
小文件embed=0embed=nullloadDirectly() → 返回完整 extractedText
大文件embed=1retrieveWithRAG() → 语义检索 TopK=5 片段
文件未就绪status != SUCCESS返回提示信息
文件不存在fileInfo == null返回错误信息

5.2 响应格式

1
2
3
4
5
6
=== 文件信息 ===
文件名: xxx.pdf
文件类型: pdf

=== 文件内容 ===
[内容或检索结果]

六、文件上传与向量化

文件上传和向量化由 FileManageServiceFileParserService 协作完成(详见 10 文件管理服务):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HTTP POST /file/upload (MultipartFile)


FileManageService.uploadFile()

├── 1. 生成 fileId
├── 2. 保存到数据库(状态 PROCESSING)
├── 3. 上传到 MinIO
├── 4. 更新数据库(状态 SUCCESS + minioPath)

├── 5. isTextFile?
│ └── 解析(PDF/Word/TXT)→ extractedText
│ └── 文本长度 ≥ 5000?
│ ├── 是: processLargeFileEmbedding()
│ │ └── 切分 → 向量化 → 存储到 PgVector
│ │ (embed=1)
│ └── 否: (embed=0)

└── 6. isImageFile?
└── 调用 Qwen-VL 多模态识别
└── 识别结果存入 extractedText
(embed=0)

大文件阈值LARGE_FILE_THRESHOLD = 5000 字符

切分器OverlapParagraphTextSplitter(500, 50) - 每块 500 字符,相邻重叠 50 字符

七、与 WebSearch Agent 的对比

维度WebSearch AgentFile Agent
工具Tavily MCP(搜索)loadContent 工具(文件)
数据源公网MinIO + PgVector
工具调用search / tavilyloadContent
maxRounds5默认 5
thinking 消息🔍 正在搜索...📂 正在检索文件内容...
输出 referenceTavily 链接列表暂未启用(可扩展)

八、典型请求/响应

请求

1
GET /agent/file/stream?query=这份文档的核心观点是什么&conversationId=user-456&fileId=abc-123

流式响应

1
2
3
4
5
6
7
data: {"type":"thinking","content":"📂 正在检索文件内容,请稍等..."}

data: {"type":"thinking","content":"好的,我已经获取了文件内容"}

data: {"type":"text","content":"根据文件内容"}

data: {"type":"text","content":",这份文档的核心观点是..."}

九、错误处理

场景处理
fileId 为空Flux.error(new IllegalArgumentException(“文件ID不能为空”))
query 为空Flux.error(new IllegalArgumentException(“查询参数不能为空”))
任务正在执行Flux.error(new IllegalStateException(“该会话正在执行中,请稍后再试”))
文件未找到工具返回 “文件不存在,文件ID: xxx”
文件处理中工具返回 “文件处理中或处理失败…”
LLM 调用失败doOnError 触发 sink.tryEmitError

十、扩展方向

  • 多文件同时问答:扩展 loadContent 接受 fileIds 数组
  • 跨文件 RAG:使用全局向量检索(去除 fileid 过滤)
  • 结构化抽取:增加 extractTables / extractEntities 等专用工具
  • 引用溯源:在响应中携带 fileid + chunkId 供前端高亮