RAG Agent Platform:多租户智能体 SaaS 平台
项目信息
GitHub:NEDONION/rag-agent-platform · 在线演示:39.97.58.27/explore
Java 17 Spring Boot 3.2.3 LangChain4j Next.js 15 React 19 PostgreSQL + PGVector RabbitMQ S3 MCP
本文是架构与设计沉淀;本地环境搭建步骤见 RAG Agent Platform - LLM/SaaS 环境搭建。

1 项目定位
一句话:把知识库、Agent 编排与 MCP 工具收进一套可自托管的多租户工作流。
它要覆盖的是一条完整链路, 而不是某个单点能力:
选择自托管而不是接第三方 SaaS,核心诉求是:数据、模型和部署方式全部可控,模型提供商可以一键切换。
2 整体架构
2.1 技术分层
| 区域 | 实现 |
|---|---|
| 后端 | Java 17、Spring Boot 3.2.3、LangChain4j 1.0.4.3-beta7 |
| 前端 | Next.js 15、React 19、Radix UI + Tailwind |
| 数据 | PostgreSQL 14+ 与 PGVector |
| ORM | MyBatis-Plus 3.5.11(多租户插件、乐观锁、逻辑删除) |
| 异步任务 | RabbitMQ(Direct/Topic Exchange + 手动 ACK + DLX) |
| 文件存储 | S3 兼容对象存储(七牛 KODO / AWS S3 / 腾讯 COS) |
| 流式响应 | Server-Sent Events (SSE) |
2.2 DDD 四层
严格分层,上层依赖下层,领域层通过 Repository 接口反向依赖基础设施:
Interfaces ──→ Application ──→ Domain ──→ Infrastructure
↑ │
└──────────────┘
| 层 | 职责 | 典型内容 |
|---|---|---|
| Interfaces | 收请求、转 DTO、参数校验、异常兜底 | @RestController、@WebSocketHandler |
| Application | 编排领域服务、事务边界、流程组合 | AgentAppService、RagAppService |
| Domain | 核心业务规则、领域实体、领域服务 | AgentDomainService、EmbeddingDomainService |
| Infrastructure | 数据访问实现、外部服务集成 | AgentRepositoryImpl、RerankForestApi |
应用层做编排,不写业务规则;领域层写业务规则,不碰技术细节。例如创建 Agent:
@Service
public class AgentAppService {
private final AgentDomainService agentDomainService;
private final ToolDomainService toolDomainService;
@Transactional
public AgentDTO createAgent(CreateAgentRequest request) {
// 应用层只负责编排多个领域服务 + 划事务边界
AgentEntity agent = agentDomainService.createAgent(
request.getName(), request.getSystemPrompt());
if (request.getToolIds() != null) {
toolDomainService.bindToolsToAgent(agent.getId(), request.getToolIds());
}
return AgentAssembler.toDTO(agent);
}
}
3 RAG 模块:文档处理流水线
这是整个项目工程量最大、也最能体现异步设计价值的部分。
3.1 三阶段异步流水线
为什么要拆成两段 MQ? OCR 和向量化的失败原因、重试成本、资源瓶颈完全不同:OCR 卡在 Vision 模型的吞吐,向量化卡在 Embedding API 的限流。拆开之后可以独立设置并发消费者数量和重试策略,一段失败不会拖累另一段已完成的工作。
3.2 策略模式处理多格式
Spring 会把同类型 Bean 按名字注入 Map,这个特性让策略路由几乎零成本:
public interface RagDocSyncOcrStrategy {
void handle(RagDocSyncOcrMessage message, String strategy);
byte[] getFileData(RagDocSyncOcrMessage message, String strategy);
Map<Integer, String> processFile(byte[] fileBytes, int totalPages);
}
@Service("ragDocSyncOcr-PDF")
public class PDFRagDocSyncOcrStrategyImpl implements RagDocSyncOcrStrategy { /* ... */ }
@Service("ragDocSyncOcr-WORD")
public class WORDRagDocSyncOcrStrategyImpl implements RagDocSyncOcrStrategy { /* ... */ }
@Component
public class RagDocSyncOcrContext {
@Resource
private Map<String, RagDocSyncOcrStrategy> strategyMap; // Spring 自动注入
public RagDocSyncOcrStrategy getTaskExportStrategy(String fileType) {
return strategyMap.get("ragDocSyncOcr-" + fileType);
}
}
新增格式只要加一个 @Service("ragDocSyncOcr-XXX"),不用改任何调度代码。
3.3 状态机管理文件生命周期
文件处理有六个状态、两条异常分支,用 if-else 写必然失控,改成状态处理器:
public interface FileProcessingStateProcessor {
boolean canHandle(FileProcessingEventEnum event);
FileProcessingStatusEnum handle(String fileId, String userId, FileProcessingEventEnum event);
FileProcessingStatusEnum currentState();
}
@Service
public class FileProcessingStateMachineService {
private final Map<FileProcessingStatusEnum, FileProcessingStateProcessor> stateProcessors;
public boolean handleEvent(String fileId, String userId, FileProcessingEventEnum event) {
FileDetailEntity file = fileDetailRepository.selectById(fileId);
FileProcessingStatusEnum current =
FileProcessingStatusEnum.fromCode(file.getProcessingStatus());
FileProcessingStateProcessor processor = stateProcessors.get(current);
if (!processor.canHandle(event)) {
return false; // 非法状态转换直接拒绝,而不是写坏数据
}
FileProcessingStatusEnum next = processor.handle(fileId, userId, event);
fileDetailRepository.update(fileId, next);
return true;
}
}
收益:消息重复投递时,非法转换会被自然拒绝,等于免费获得了幂等性。
