大模型应用进入真实业务后,后端承担的工作会迅速超出一次模型接口调用。用户请求需要经过身份校验、参数校验、任务创建、状态保存、异步执行、外部工具调用、结果回写和异常恢复。模型响应较慢、长任务持续占用线程、重复请求产生重复执行、服务重启导致状态丢失,这些问题都需要依靠稳定的 Java 后端能力解决。【本质上仍然属于应用系统,只不过加入了生产级Agent】
企业级 Agent 系统通常建立在 Spring Boot、MySQL、Redis 和消息队列之上。Java 并发负责控制任务执行,Spring 负责组织接口和业务对象,MySQL 保存可靠状态,Redis 管理短期状态和流量,消息队列承担长任务分发与重试。HTTP、Linux、Maven、测试和 Docker 则保证系统能够开发、验证和部署。
目录
一、企业级Agent后端架构
一条 Agent 请求通常会经过接入层、业务层(应用层)、任务执行层、数据层和外部能力层。接入层负责认证、限流和路由;业务层负责参数校验、任务编排和状态流转;执行层负责异步调用模型、知识库和业务工具;数据层负责保存任务、结果和审计记录;外部能力层则包含模型服务、检索服务和第三方接口。
| 层次 | 核心职责 | 常见技术 |
|---|---|---|
| 接入层 | 认证、限流、请求路由 | HTTP、Gateway、JWT、Redis |
| 应用层 | 参数校验、业务编排、异常处理 | Spring Boot、MVC、IoC、AOP |
| 执行层 | 异步任务、并发控制、超时管理 | 线程池、锁、CompletableFuture |
| 数据层 | 任务状态、结果、审计记录 | MySQL、事务、索引 |
| 状态层 | 会话、缓存、限流计数 | Redis |
| 消息层 | 长任务、重试、削峰、解耦 | Kafka、RabbitMQ、RocketMQ |
| 运行层 | 构建、测试、部署、排障 | Maven、Git、Linux、Docker |
这套结构的重点在于职责分离。HTTP 请求线程负责快速接收任务,后台执行器负责耗时操作,数据库负责可靠保存,缓存负责高频访问,消息队列负责跨服务传递任务。任何一个组件承担过多职责,都会增加故障传播和维护成本。
二、Spring Boot组织业务入口
Spring Boot是 Java 企业后端的应用基础。Spring MVC将 HTTP 请求映射到控制器,IoC 容器负责创建并注入业务对象,AOP可以统一处理日志、权限和耗时统计。参数校验与全局异常处理则保证接口输入和错误响应具有稳定格式。
一个任务接口只需要完成接收请求、调用业务服务和返回任务编号,耗时执行不应直接堆积在控制器中。
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@PostMapping
public ResponseEntity<TaskResponse> create(
@Valid @RequestBody TaskRequest request) {
TaskResponse task = taskService.create(request);
return ResponseEntity.accepted().body(task);
}
}
控制器保持轻量后,模型调用、状态更新和重试逻辑可以集中放入 Service。这样便于测试,也能避免接口层逐渐演变成难以维护的大型方法。
三、Java并发承担任务执行
Agent任务经常包含模型推理、知识检索和外部工具调用,这些操作具有明显的等待时间。如果所有操作都绑定在请求线程中,少量慢请求就可能占满 Web 容器线程。线程池可以限制并发数量,
CompletableFuture可以组织多个异步阶段,锁、volatile和 CAS则用于保证共享状态的一致性。
下面的代码展示了最核心的异步任务结构。接口接收任务后立即返回,后台线程负责执行耗时操作。
@Service
public class TaskService {
private final Executor taskExecutor;
public TaskService(Executor taskExecutor) {
this.taskExecutor = taskExecutor;
}
public TaskResponse create(TaskRequest request) {
String taskId = UUID.randomUUID().toString();
CompletableFuture.runAsync(
() -> executeTask(taskId, request),
taskExecutor
);
return new TaskResponse(taskId, "ACCEPTED");
}
private void executeTask(
String taskId,
TaskRequest request) {
// 更新状态为 RUNNING
// 调用模型、检索和业务工具
// 保存结果并更新为 SUCCEEDED
}
}
线程池必须设置最大线程数和有界队列。无界队列会持续接收任务,当生产速度长期高于消费速度时,任务对象会不断占用堆内存,最终引发频繁 GC 或内存溢出。线程数量也不能简单设置得越大越好,数据库连接数、模型接口配额和下游服务容量都会形成真实上限。
四、MySQL保存可靠业务状态
MySQL适合保存需要长期保留和准确查询的数据,例如任务主体、执行状态、模型配置、结果索引和审计记录。Agent任务通常会经历已接收、执行中、成功、失败和取消等状态,状态更新需要通过事务保证相关数据同步提交。
任务表可以保留最关键的业务字段。
CREATE TABLE agent_task (
id VARCHAR(64) PRIMARY KEY,
user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
request_data JSON NOT NULL,
result_data JSON,
error_message VARCHAR(500),
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
INDEX idx_user_status (user_id, status),
INDEX idx_updated_at (updated_at)
);
索引需要围绕真实查询设计。根据用户和状态查询任务列表时,可以使用联合索引;根据更新时间扫描超时任务时,需要为 updated_at 建立索引。索引过少会导致全表扫描,索引过多则会增加写入成本。事务范围也应保持紧凑,模型调用和网络请求不应放在数据库事务内部,否则长时间占用连接和锁。
五、Redis管理临时状态与流量
Redis适合保存访问频率高、生命周期较短的数据。Agent系统中的会话上下文、任务进度、验证码、限流计数和热点配置都可以放入 Redis。数据库保存最终可信状态,Redis提供快速读取,两者分工能够降低数据库压力。
一个简单的接口限流可以使用计数器与过期时间实现。
public boolean allowRequest(
String userId,
StringRedisTemplate redisTemplate) {
String key = "rate:user:" + userId;
Long count = redisTemplate.opsForValue().increment(key);
if (count != null && count == 1) {
redisTemplate.expire(key, Duration.ofMinutes(1));
}
return count != null && count <= 30;
}
这种实现适合基础频率限制,复杂场景还需要令牌桶或滑动窗口。Redis分布式锁可以协调多实例之间的互斥操作,但必须处理过期时间、锁续期和误释放问题。普通任务状态不应依赖分布式锁完成一致性控制,数据库唯一约束、条件更新和幂等设计通常更加可靠。
六、消息队列承接长任务与重试
当任务执行时间较长、执行实例较多,或者系统需要失败重试时,消息队列比本地线程池更稳定。任务创建成功后,生产者发送一条任务消息;消费者读取消息、执行模型和工具调用,再更新数据库状态。请求接入和任务执行由此解耦,系统可以独立扩展消费者数量。
消息队列通常只能保证消息至少被消费一次,重复投递是正常现象。消费者必须根据任务编号进行幂等判断,避免重复调用模型、重复扣费或重复写入结果。
public void consume(TaskMessage message) {
int updated = taskRepository.markRunning(
message.taskId(),
"ACCEPTED",
"RUNNING"
);
if (updated == 0) {
return;
}
executeTask(message);
}
这里使用条件更新保证只有状态为 ACCEPTED 的任务能够进入 RUNNING。同一消息再次到达时,更新行数为零,消费者可以直接结束。失败任务可以进入重试队列,超过最大次数后转入死信队列,等待人工检查或补偿处理。
Kafka更适合高吞吐事件流和审计日志,RabbitMQ擅长灵活路由与任务队列,RocketMQ在延迟消息、事务消息和重试机制方面应用较多。具体选择应结合吞吐量、消息顺序、延迟任务和运维条件,避免只根据框架热度决定。
七、Java基础知识在Agent系统中的位置
Java集合用于保存任务步骤、工具参数和中间结果。ArrayList适合顺序访问,HashMap适合根据键快速查找,ConcurrentHashMap适合并发读写,但无法替代数据库和 Redis 的分布式状态管理。异常机制用于区分参数错误、业务错误、网络超时和系统故障,异常分类越清晰,重试和告警策略越准确。
Java IO负责文件上传、流式响应和网络数据处理。大文件不应一次性读入内存,应使用缓冲流或分块读取。JVM内存与 GC(垃圾回收机制)则决定对象如何分配和回收,任务队列过长、缓存未释放和大文本长期驻留都会增加堆内存压力。出现响应变慢和 CPU 升高时,需要结合线程栈、堆内存和 GC 日志进行判断。
volatile适合保证单个变量的可见性,无法保证复合操作的原子性。CAS适合实现轻量级无锁更新,竞争激烈时可能产生大量重试。锁适合保护临界区,但锁范围过大会降低并发能力。学习这些内容时,应始终关注共享数据、竞争范围和故障结果。
八、工程运行与质量保障
HTTP定义客户端与服务之间的通信方式,常见接口需要正确使用状态码、超时、重试和幂等键。Linux提供进程、端口、日志和资源排查能力。Git负责代码版本管理,Maven负责依赖、编译和测试。单元测试验证业务逻辑,集成测试验证数据库、Redis和消息队列之间的协作。
Docker可以将应用及其运行环境封装成统一镜像,Docker Compose适合在本地启动 Java 服务、MySQL、Redis和消息队列。容器化能够减少环境差异,但仍需明确数据卷、健康检查、环境变量和资源限制。
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
- redis
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: agent
redis:
image: redis:7
九、关键架构风险
Agent后端最常见的风险集中在同步阻塞、状态丢失、任务重复和容量失控。长任务直接占用请求线程,会降低接口吞吐量;任务状态只保存在 JVM 内存中,服务重启或多实例部署后会失去一致性;消息消费者缺少幂等控制,可能导致重复执行;线程池和消息积压缺少容量限制,则可能将局部故障扩散为系统故障。
稳定的设计通常遵循几个原则。请求快速返回任务编号,可靠状态写入 MySQL,短期状态和限流交给 Redis,长任务通过线程池或消息队列执行,所有外部调用配置超时,消息消费具备幂等能力,错误经过统一分类后再决定重试、降级或人工处理。Java后端知识最终都会落到这些具体设计中,并共同决定 Agent 系统能否稳定运行。