共计 2706 个字符,预计需要花费 7 分钟才能阅读完成。
为什么需要重构传统 AI Agent 实现?
最近在团队里接手了一个对话型 AI 项目,发现旧版实现存在几个典型问题:

- 同步阻塞:每次 LLM 调用都阻塞 Tomcat 线程,QPS 超过 20 就开始报 503
- 状态管理失控:用 HashMap 存会话上下文,服务重启就丢数据
- 监控缺失:无法区分是模型响应慢还是网络延迟
技术选型的思考过程
对比了三种主流方案:
- Spring Reactor:
- 优势:与 Spring 生态无缝集成,背压 (backpressure) 支持完善
-
局限:需要理解函数式编程范式
-
Akka:
- 优势:分布式 actor 模型天然适合对话场景
-
局限:学习曲线陡峭,线程模型复杂
-
Vert.x:
- 优势:事件驱动架构性能极致
- 局限:需要重构现有 Spring 服务
最终选择 Spring Boot + LangChain4j 组合,因为:
- LangChain4j 的 API 设计更符合 Java 习惯
- 内置对 OpenAI/Azure Claude 等主流模型的支持
- 自动处理 prompt 模板和聊天历史管理
核心实现分层拆解
1. 接口层设计
@RestController
@RequestMapping("/api/v1/agent")
public class AgentController {@PostMapping("/chat")
public Mono<ResponseEntity<ApiResponse>> chat(@RequestBody ChatRequest request) {return agentService.handleMessage(request)
.map(response -> ResponseEntity.ok(ApiResponse.success(response)));
}
}
关键点:
- 使用 Spring WebFlux 实现非阻塞 IO
- 统一返回 Mono 封装异步结果
- 标准化 ApiResponse 结构(含 traceId)
2. 业务逻辑层
@Service
@RequiredArgsConstructor
public class AgentServiceImpl implements AgentService {
private final ChatLanguageModel llm;
private final ChatMemoryStore memoryStore;
@Override
public Mono<ChatResponse> handleMessage(ChatRequest request) {return Mono.fromCallable(() -> {
// 1. 从 Redis 加载历史对话
List<ChatMessage> history = memoryStore.load(request.getSessionId());
// 2. 构建 prompt
PromptTemplate prompt = new PromptTemplate("你是一位专业顾问...");
// 3. 调用 LLM(带熔断)return llm.generate(prompt.render(history));
}).subscribeOn(Schedulers.boundedElastic());
}
}
3. 基础设施层
LangChain4j 配置类:
@Configuration
public class LlmConfig {
@Bean
@ConditionalOnProperty(name = "llm.provider", havingValue = "openai")
public ChatLanguageModel openAiClient() {return OpenAiChatModel.builder()
.apiKey(env.getProperty("openai.key"))
.temperature(0.7)
.timeout(Duration.ofSeconds(30))
.build();}
@Bean
public CircuitBreakerConfig circuitBreakerConfig() {return CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(60))
.build();}
}
生产环境关键优化点
内存泄漏防护
特别注意 ThreadLocal 的使用场景:
try {UserContextHolder.set(user); // 存储用户信息
return doProcess();} finally {UserContextHolder.clear(); // 必须清理!}
监控埋点示例
@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> metrics() {
return registry -> {registry.config().commonTags("application", "ai-agent");
// 记录 LLM 调用耗时
Timer.builder("llm.latency")
.description("LLM 响应时间")
.register(registry);
};
}
Redis 存储优化
使用 Hash 结构存储对话历史:
HSET session:1234
message_1 "{...}"
message_2 "{...}"
配置 TTL 自动过期:
@Bean
public RedisTemplate<String, Object> redisTemplate() {RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setExpire(Duration.ofHours(2)); // 全局过期时间
return template;
}
三大避坑指南
- 流式响应超时
- 问题:浏览器在 SSE 连接时默认 2 分钟超时
-
方案:配置 Nginx
proxy_read_timeout 1h; -
Token 计数误差
- 问题:中文 token 计算与模型实际消耗不一致
-
方案:使用
TikToken库精确统计 -
会话 ID 碰撞
- 问题:UUID.randomUUID()在高并发时可能重复
- 方案:改用
Snowflake算法生成 ID
扩展思考
当前架构如何支持多模态处理?可以考虑:
- 在 LangChain4j 中集成 ImageModel
- 使用 MinIO 存储生成图片
- 前端通过 WebSocket 接收混合式响应
完整代码示例已开源在 GitHub(伪代码已脱敏),欢迎交流讨论!
正文完
发表至: 编程开发
近一天内
