从 Demo 到生产:LangChain4j 工程化落地全指南
前几篇我们分别深入了 RAG、Agent/Tool Calling、Chat Memory 三个核心能力。但说实话------能跑通 Demo 和能上线生产,中间还差着十万八千里。
这篇就来填这个坑:异常处理、限流保护、Token 追踪、健康检查、容器化部署。
为什么需要这篇
先看一个真实的 Demo 代码:
java
@GetMapping("/chat")
public String chat(@RequestParam String message) {
return chatModel.chat(message); // 就这一行
}
能跑。但生产环境会立刻暴露五个问题:
| 问题 | Demo 中的表现 | 生产后果 |
|---|---|---|
| 异常未处理 | LLM 超时 → 500 + 堆栈 | 前端拿到一堆英文报错 |
| 无限流 | 一个用户疯狂请求 | Token 额度秒光,账单爆炸 |
| Token 不可见 | 花了多少 token?不知道 | 无法核算成本,无法预警 |
| 无健康检查 | 服务挂了没人知道 | K8s/LB 无法探活 |
| 无部署方案 | 手动 java -jar | 无法弹性扩缩容 |
本文基于 LangChain4j 1.17.2 + Spring Boot 3.5.0 + DashScope(通义千问) 的真实项目,逐个解决这些问题。
一、统一异常处理 + LLM 降级
问题
Demo 版接口直接返回 String,LLM 一旦超时或 API Key 失效,前端拿到的是 Spring Boot 默认的错误页面或 JSON 堆栈------既不安全也不友好。
解决方案
三层防线:统一响应格式 + 全局异常处理器 + LLM 降级
第一层:统一响应格式 ApiResponse<T>
java
public class ApiResponse<T> {
private int code; // 200=成功, 400=业务错误, 429=限流, 500=系统异常
private String message; // 友好提示
private T data; // 业务数据
private String timestamp; // 时间戳
private TokenUsageInfo tokenUsage; // Token 消耗(可选)
public static <T> ApiResponse<T> success(T data, TokenUsageInfo tokenUsage) {
ApiResponse<T> response = new ApiResponse<>();
response.code = 200;
response.message = "success";
response.data = data;
response.tokenUsage = tokenUsage;
return response;
}
public static <T> ApiResponse<T> error(int code, String message) {
ApiResponse<T> response = new ApiResponse<>();
response.code = code;
response.message = message;
response.data = null;
return response;
}
}
前端只需判断 code === 200,其他都是错误。
第二层:全局异常处理器 @RestControllerAdvice
java
@RestControllerAdvice
public class GlobalExceptionHandler {
// 业务异常 → 400
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(e.getCode(), e.getMessage()));
}
// 限流异常 → 429
@ExceptionHandler(RateLimitException.class)
public ResponseEntity<ApiResponse<Void>> handleRateLimitException(RateLimitException e) {
log.warn("限流触发: {}", e.getMessage());
return ResponseEntity
.status(HttpStatus.TOO_MANY_REQUESTS)
.body(ApiResponse.error(429, e.getMessage()));
}
// 兜底:所有未捕获的异常 → 500
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception e) {
log.error("系统异常", e);
String message = "服务暂时不可用,请稍后重试";
// LLM 特定错误给更友好的提示
if (e.getMessage() != null && e.getMessage().contains("timeout")) {
message = "AI 服务响应超时,请稍后重试";
} else if (e.getMessage() != null && e.getMessage().contains("api key")) {
message = "AI 服务配置异常,请联系管理员";
}
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(500, message));
}
}
关键设计:不把异常堆栈暴露给前端,只返回友好提示。完整堆栈记录在服务端日志。
第三层:LLM 降级
在 Controller 层捕获 LLM 调用异常,返回预设的降级文案:
java
@GetMapping("/chat")
public ApiResponse<String> chat(@RequestParam String sessionId,
@RequestParam String message) {
// 1. 限流检查
rateLimiter.checkLimit(sessionId);
// 2. 调用 AI Service(Token 由 Listener 自动追踪)
String reply;
try {
reply = productionMallCustomerService.chat(sessionId, message);
} catch (Exception e) {
// 3. LLM 降级:返回友好提示而非暴露异常
log.error("LLM 调用失败: sessionId={}, error={}", sessionId, e.getMessage());
reply = "抱歉,客服系统暂时繁忙,请稍后再试。如需紧急帮助,请拨打客服热线 021-88889999。";
}
// 4. 获取本次调用的 token 用量
ApiResponse.TokenUsageInfo tokenUsage = tokenUsageTracker.getLastTokenUsage();
return ApiResponse.success(reply, tokenUsage);
}
对比效果:
| 场景 | Demo 版 | 生产版 |
|---|---|---|
| LLM 超时 | 500 Internal Server Error + 堆栈 |
200 OK + "客服系统暂时繁忙,请稍后再试" |
| 参数缺失 | 400 Required parameter is missing |
400 + {"message":"缺少必填参数: message"} |
| 限流触发 | 无限流 | 429 + {"message":"请求过于频繁,60秒内最多20次请求"} |
二、滑动窗口限流
问题
没有限流意味着:一个恶意用户可以用脚本每秒发 100 个请求,直接快速把你的 Token 额度打光。
解决方案
纯内存滑动窗口限流器,不依赖 Redis,适合单机部署:
java
@Component
public class RateLimiter {
private final int windowSeconds; // 时间窗口(秒),默认 60
private final int maxRequests; // 窗口内最大请求数,默认 20
// key → 请求时间戳队列(毫秒)
private final ConcurrentHashMap<String, Deque<Long>> requestMap = new ConcurrentHashMap<>();
public RateLimiter(
@Value("${production.rate-limit.window-seconds:60}") int windowSeconds,
@Value("${production.rate-limit.max-requests:20}") int maxRequests) {
this.windowSeconds = windowSeconds;
this.maxRequests = maxRequests;
}
public void checkLimit(String key) {
long now = System.currentTimeMillis();
long windowStart = now - (windowSeconds * 1000L);
Deque<Long> timestamps = requestMap.computeIfAbsent(key, k -> new ConcurrentLinkedDeque<>());
// 清理过期记录
timestamps.removeIf(ts -> ts < windowStart);
if (timestamps.size() >= maxRequests) {
long oldestInWindow = timestamps.isEmpty() ? 0 : timestamps.peekFirst();
long retryAfter = ((oldestInWindow + windowSeconds * 1000L) - now) / 1000;
throw new RateLimitException(
String.format("请求过于频繁,%d秒内最多%d次请求,请%d秒后重试",
windowSeconds, maxRequests, Math.max(retryAfter, 1)));
}
timestamps.addLast(now);
}
}
配置(application.yml):
yaml
production:
rate-limit:
window-seconds: 60 # 时间窗口(秒)
max-requests: 20 # 窗口内最大请求数
原理图解
lua
时间轴 →
|--60s窗口--|
^-- 清理过期 --^
|-- 当前请求队列(最多20个)--|
用户 A 第 21 次请求 → timestamps.size()=20 → 触发限流
返回:429 "请求过于频繁,60秒内最多20次请求,请3秒后重试"
多级限流策略
实际生产中可以按不同维度限流:
java
// 1. 按 sessionId 限流(防单用户刷)
rateLimiter.checkLimit("session:" + sessionId);
// 2. 按 IP 限流(防恶意爬虫)
rateLimiter.checkLimit("ip:" + request.getRemoteAddr());
// 3. 全局限流(保护 Token 额度)
rateLimiter.checkLimit("global");
实现支持任意 key 维度,只需传入不同的 key 前缀。
三、Token 消耗追踪------LangChain4j Listener 机制
问题
DashScope 控制台能看到总消耗,但看不到:
- 每个会话消耗了多少 token?
- 每次对话的 input/output 分别是多少?
- 成功率和失败率是多少?
- 估算费用是多少?
解决方案
使用 LangChain4j 1.17.2 的 AiService Listener 机制,零侵入追踪 Token 消耗。
API 验证
在动手之前,先通过 javap 反编译确认 LangChain4j 1.17.2 中的关键接口:
bash
# AiServices 支持 registerListeners
javap dev.langchain4j.service.AiServices
# → public AiServices<T> registerListeners(AiServiceListener<?>...)
# AiServiceResponseReceivedListener 监听成功响应
javap dev.langchain4j.observability.api.listener.AiServiceResponseReceivedListener
# → extends AiServiceListener<AiServiceResponseReceivedEvent>
# 事件中可以拿到 ChatResponse(含 TokenUsage)
javap dev.langchain4j.observability.api.event.AiServiceResponseReceivedEvent
# → public ChatResponse response()
# → public InvocationContext invocationContext()
# ChatResponse 中有 tokenUsage()
javap dev.langchain4j.model.chat.response.ChatResponse
# → public TokenUsage tokenUsage()
# TokenUsage 提供三个维度
javap dev.langchain4j.model.output.TokenUsage
# → public Integer inputTokenCount()
# → public Integer outputTokenCount()
# → public Integer totalTokenCount()
# InvocationContext 可以拿到 sessionId
javap dev.langchain4j.invocation.InvocationContext
# → public Object chatMemoryId() // 就是 @MemoryId 的值
踩坑:Java 不允许同时实现两个 Listener
最初尝试让一个类同时实现 AiServiceResponseReceivedListener 和 AiServiceErrorListener:
java
// 编译报错!
public class TokenUsageTracker implements
AiServiceResponseReceivedListener, // getEventClass() → Class<AiServiceResponseReceivedEvent>
AiServiceErrorListener { // getEventClass() → Class<AiServiceErrorEvent>
// 错误:两者都定义了 getEventClass(),但返回类型不兼容
}
原因:两个接口都从 AiServiceListener<T> 继承了 getEventClass() 方法,但返回类型分别是 Class<AiServiceResponseReceivedEvent> 和 Class<AiServiceErrorEvent>------Java 泛型不允许这种"不相关的返回类型"共存。
解决方案:拆成两个独立监听器 + 一个核心追踪器。
核心追踪器 TokenUsageTracker
java
@Component
public class TokenUsageTracker {
// 按 sessionId 追踪
public static class SessionUsage {
public final AtomicInteger totalCalls = new AtomicInteger(0);
public final AtomicInteger successCalls = new AtomicInteger(0);
public final AtomicInteger failedCalls = new AtomicInteger(0);
public final AtomicLong inputTokens = new AtomicLong(0);
public final AtomicLong outputTokens = new AtomicLong(0);
public final AtomicLong totalTokens = new AtomicLong(0);
public volatile LocalDateTime lastCallTime;
}
private final ConcurrentHashMap<String, SessionUsage> sessionUsageMap = new ConcurrentHashMap<>();
// 全局统计
private final AtomicInteger globalTotalCalls = new AtomicInteger(0);
private final AtomicLong globalTotalTokens = new AtomicLong(0);
// ... 其他统计字段
/**
* 处理 LLM 成功响应(由 TokenResponseListener 委托调用)
*/
public void onResponseReceived(AiServiceResponseReceivedEvent event) {
globalTotalCalls.incrementAndGet();
// 提取 sessionId
String sessionId = "unknown";
if (event.invocationContext() != null
&& event.invocationContext().chatMemoryId() != null) {
sessionId = event.invocationContext().chatMemoryId().toString();
}
// 提取 Token 用量
int inputTokens = 0, outputTokens = 0, totalTokens = 0;
if (event.response() != null) {
TokenUsage usage = event.response().tokenUsage();
if (usage != null) {
inputTokens = usage.inputTokenCount() != null ? usage.inputTokenCount() : 0;
outputTokens = usage.outputTokenCount() != null ? usage.outputTokenCount() : 0;
totalTokens = usage.totalTokenCount() != null ? usage.totalTokenCount() : 0;
}
}
// 更新会话级 + 全局统计
SessionUsage sessionUsage = sessionUsageMap.computeIfAbsent(sessionId, k -> new SessionUsage());
sessionUsage.inputTokens.addAndGet(inputTokens);
sessionUsage.outputTokens.addAndGet(outputTokens);
// ...
// 缓存最近一次 token 用量(供 Controller 返回给前端)
lastTokenUsage = new ApiResponse.TokenUsageInfo(inputTokens, outputTokens, totalTokens);
}
/**
* 处理 LLM 调用失败(由 TokenErrorListener 委托调用)
*/
public void onError(AiServiceErrorEvent event) {
globalTotalCalls.incrementAndGet();
globalFailedCalls.incrementAndGet();
// ... 记录错误信息
}
}
两个监听器
java
// 监听成功响应
@Component
public class TokenResponseListener implements AiServiceResponseReceivedListener {
private final TokenUsageTracker tokenUsageTracker;
@Override
public void onEvent(AiServiceResponseReceivedEvent event) {
tokenUsageTracker.onResponseReceived(event);
}
}
// 监听调用失败
@Component
public class TokenErrorListener implements AiServiceErrorListener {
private final TokenUsageTracker tokenUsageTracker;
@Override
public void onEvent(AiServiceErrorEvent event) {
tokenUsageTracker.onError(event);
}
}
在 ProductionConfig 中注册
java
@Bean("productionMallCustomerService")
public MallCustomerService productionMallCustomerService(
ChatModel chatModel,
MallToolService mallToolService,
ChatMemoryProvider chatMemoryProvider,
EmbeddingStoreContentRetriever contentRetriever,
TokenResponseListener tokenResponseListener,
TokenErrorListener tokenErrorListener) {
return AiServices.builder(MallCustomerService.class)
.chatModel(chatModel)
.tools(mallToolService)
.chatMemoryProvider(chatMemoryProvider)
.contentRetriever(contentRetriever)
.registerListeners(tokenResponseListener, tokenErrorListener) // ← 注册监听器
.build();
}
效果
调用 GET /api/production/mall/chat?sessionId=user-001&message=你好 后返回:
json
{
"code": 200,
"message": "success",
"data": "您好!我是XX商城智能客服,请问有什么可以帮您?",
"timestamp": "2026-08-18T14:30:00",
"tokenUsage": {
"inputTokens": 150,
"outputTokens": 80,
"totalTokens": 230
}
}
调用 GET /api/production/mall/token-stats 查看全局统计:
json
{
"code": 200,
"data": {
"totalCalls": 156,
"successCalls": 153,
"failedCalls": 3,
"inputTokens": 23400,
"outputTokens": 12400,
"totalTokens": 35800,
"estimatedCost": "¥0.0447",
"activeSessions": 12
}
}
零侵入 ------业务代码(MallCustomerService 接口、MallToolService 工具类)完全不需要改动,监听器在 AiServices.builder() 时自动织入。
四、健康检查与监控
问题
服务部署后,负载均衡器/K8s 需要一个健康检查端点来探活。如果 LLM 服务挂了,需要及时发现并报警。
解决方案
Spring Boot Actuator + 自定义健康指标
添加依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
自定义健康指标
java
@Component("llmServiceHealth")
public class HealthCheckController implements HealthIndicator {
private final TokenUsageTracker tokenUsageTracker;
private final Instant startTime;
@Override
public Health health() {
Health.Builder builder = Health.up();
// 1. JVM 内存检查
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
long heapUsed = memoryBean.getHeapMemoryUsage().getUsed();
long heapMax = memoryBean.getHeapMemoryUsage().getMax();
double heapUsagePercent = (double) heapUsed / heapMax * 100;
builder.withDetail("heapUsage", String.format("%.1f%%", heapUsagePercent));
// 2. LLM 调用统计
int totalCalls = tokenUsageTracker.getGlobalTotalCalls();
int failedCalls = tokenUsageTracker.getGlobalFailedCalls();
double failureRate = totalCalls > 0 ? (double) failedCalls / totalCalls * 100 : 0;
// 3. 健康判定
if (heapUsagePercent > 90) {
builder = Health.down().withDetail("reason", "heap usage > 90%");
} else if (totalCalls > 10 && failureRate > 50) {
builder = Health.down().withDetail("reason", "LLM failure rate > 50%");
}
return builder.build();
}
}
配置(application.yml)
yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
访问效果
bash
GET /actuator/health
json
{
"status": "UP",
"components": {
"llmServiceHealth": {
"status": "UP",
"details": {
"heapUsed": "128.3 MB",
"heapMax": "512.0 MB",
"heapUsage": "25.1%",
"uptime": "0d 2h 15m",
"llmTotalCalls": 156,
"llmFailedCalls": 3,
"llmFailureRate": "1.9%"
}
}
}
}
健康判定逻辑:
| 条件 | 状态 | 场景 |
|---|---|---|
| 内存 < 90% 且 LLM 失败率 < 50% | UP | 正常运行 |
| 内存 > 90% | DOWN | 内存泄漏/OOM 前兆 |
| LLM 失败率 > 50%(且调用 > 10 次) | DOWN | API Key 失效/网络中断 |
五、Docker 容器化部署
多阶段 Dockerfile
dockerfile
# 阶段1:构建
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
# 阶段2:运行时(精简镜像)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
RUN mkdir -p /app/data/chat-memory
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
ENV DASHSCOPE_API_KEY=""
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Ddashscope.api-key=$DASHSCOPE_API_KEY -jar app.jar"]
关键设计:
| 设计 | 说明 |
|---|---|
| 多阶段构建 | builder 阶段用 Maven+JDK17,运行时只需 JRE,镜像从 ~800MB 降到 ~200MB |
| 依赖缓存 | 先 COPY pom.xml 再下载依赖,利用 Docker 层缓存加速构建 |
| 持久化卷 | /app/data/chat-memory 挂载到宿主机,ChatMemory 重启不丢 |
| JVM 参数 | G1GC + 256-512MB 堆,适合中小流量 |
| HEALTHCHECK | 容器自带探活,配合 K8s livenessProbe |
构建与运行
bash
# 构建镜像
docker build -t youning-mall-ai:1.0 .
# 运行容器
docker run -d --name mall-ai \
-p 8080:8080 \
-e DASHSCOPE_API_KEY=sk-xxxxx \
-v ./data/chat-memory:/app/data/chat-memory \
youning-mall-ai:1.0
# 查看健康状态
curl http://localhost:8080/actuator/health
生产级 JVM 参数说明
bash
JAVA_OPTS="-Xms256m -Xmx512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/app/dumps/ \
-Dserver.shutdown=graceful \
-Dspring.lifecycle.timeout-per-shutdown-phase=30s"
| 参数 | 作用 |
|---|---|
-Xms256m -Xmx512m |
初始/最大堆内存,避免动态扩容抖动 |
UseG1GC |
G1 垃圾回收器,适合多核服务端 |
MaxGCPauseMillis=200 |
GC 停顿目标 200ms |
HeapDumpOnOutOfMemoryError |
OOM 时自动 dump,方便事后分析 |
server.shutdown=graceful |
优雅停机,等待正在处理的请求完成 |
六、对比总结:Demo 接口 vs 生产接口
接口对比
| 维度 | Demo (/mall/chat) |
生产 (/api/production/mall/chat) |
|---|---|---|
| 响应格式 | 裸 String |
ApiResponse<T> 统一格式 |
| 异常处理 | Spring 默认 500 | 全局异常处理器 + 降级文案 |
| 限流 | 无 | 60 秒内最多 20 次 |
| Token 追踪 | 无 | 每次 API 返回 token 用量 |
| 健康检查 | 无 | /actuator/health + 自定义指标 |
| 部署 | java -jar |
Docker 容器化 |
响应对比
Demo 版响应(成功时):
您好!我是XX商城智能客服,请问有什么可以帮您?
Demo 版响应(LLM 超时时):
json
{
"timestamp": "2026-08-18T14:30:00.000+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "I/O error on POST request for \"https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation\": timeout",
"path": "/mall/chat"
}
生产版响应(成功时):
json
{
"code": 200,
"message": "success",
"data": "您好!我是XX商城智能客服,请问有什么可以帮您?",
"timestamp": "2026-08-18T14:30:00",
"tokenUsage": {
"inputTokens": 150,
"outputTokens": 80,
"totalTokens": 230
}
}
生产版响应(LLM 超时时):
json
{
"code": 200,
"message": "success",
"data": "抱歉,客服系统暂时繁忙,请稍后再试。如需紧急帮助,请拨打客服热线 021-88889999。",
"timestamp": "2026-08-18T14:30:00",
"tokenUsage": null
}
注意生产版超时时不返回 500------因为 LLM 不可用不等于整个服务不可用。客服降级为电话引导,用户仍然有出口。
七、完整接口清单
sql
=== 生产级接口 ===
GET /api/production/mall
→ 接口总览
GET /api/production/mall/chat?sessionId=user-001&message=你好
→ 生产级客服对话(GET,便于测试)
POST /api/production/mall/chat
→ 生产级客服对话(POST,前端调用)
Body: {"sessionId":"user-001","message":"你好"}
GET /api/production/mall/token-stats
→ 全局 Token 消耗统计
GET /api/production/mall/token-stats/session?sessionId=user-001
→ 指定会话的 Token 统计
GET /api/production/mall/rate-limit-info?sessionId=user-001
→ 限流配置和剩余可用次数
GET /api/production/mall/health
→ 应用健康状态
GET /actuator/health
→ Spring Boot Actuator 健康检查(含自定义 LLM 指标)
八、踩坑记录
坑1:AiServiceListener 接口冲突
现象 :一个类同时实现 AiServiceResponseReceivedListener 和 AiServiceErrorListener,编译报错。
scss
类型 AiServiceListener<AiServiceErrorEvent> 和 AiServiceResponseReceivedListener 不兼容;
两者都定义了 getEventClass(),但却带有不相关的返回类型
原因 :两个接口都继承了 AiServiceListener<T> 的 getEventClass() 方法,返回类型分别是 Class<AiServiceResponseReceivedEvent> 和 Class<AiServiceErrorEvent>。Java 泛型不允许协变返回类型在这种场景下共存。
解法:拆成两个独立的监听器类,各自实现一个接口,委托给同一个 TokenUsageTracker。
坑2:TokenUsage 可能为 null
现象 :从 ChatResponse.tokenUsage() 取 TokenUsage 时,偶发 NPE。
原因:不是所有 LLM Provider 都会返回 token 用量。DashScope 在某些异常路径下可能不填充 tokenUsage。
解法:每个字段都做空判断:
java
TokenUsage usage = event.response().tokenUsage();
if (usage != null) {
inputTokens = usage.inputTokenCount() != null ? usage.inputTokenCount() : 0;
outputTokens = usage.outputTokenCount() != null ? usage.outputTokenCount() : 0;
totalTokens = usage.totalTokenCount() != null ? usage.totalTokenCount() : 0;
}
坑3:HealthIndicator 需要 Actuator 依赖
现象 :实现 HealthIndicator 接口时编译找不到类。
原因 :org.springframework.boot.actuate.health.Health 和 HealthIndicator 在 spring-boot-starter-actuator 包中,默认的 spring-boot-starter-web 不包含。
解法 :在 pom.xml 中添加 spring-boot-starter-actuator 依赖。
坑4:ChatMemoryProvider Bean 冲突
现象 :项目中有多个 ChatMemoryProvider Bean(ChatMemoryConfig 一个,AdvancedMemoryConfig 一个 @Primary),生产版 Bean 需要正确注入。
解法 :AdvancedMemoryConfig 中用 @Primary 标注了文件持久化版本,Spring 自动注入这个。如果需要切换策略,用 @Qualifier 指定。
九、LangChain4j 1.17.2 工程化 API 速查表
AiService Listener
| 类/接口 | 包路径 | 用途 |
|---|---|---|
AiServices.builder() |
dev.langchain4j.service |
构建 AI Service |
.registerListeners(listener...) |
dev.langchain4j.service.AiServices |
注册事件监听器 |
AiServiceListener<T> |
dev.langchain4j.observability.api.listener |
监听器根接口 |
AiServiceResponseReceivedListener |
dev.langchain4j.observability.api.listener |
监听成功响应 |
AiServiceErrorListener |
dev.langchain4j.observability.api.listener |
监听调用失败 |
AiServiceCompletedListener |
dev.langchain4j.observability.api.listener |
监听服务完成 |
事件类
| 事件 | 包路径 | 关键方法 |
|---|---|---|
AiServiceResponseReceivedEvent |
dev.langchain4j.observability.api.event |
response(), request(), invocationContext() |
AiServiceErrorEvent |
dev.langchain4j.observability.api.event |
error(), invocationContext() |
AiServiceCompletedEvent |
dev.langchain4j.observability.api.event |
result() (Optional) |
Token 用量
| 类 | 包路径 | 方法 |
|---|---|---|
ChatResponse |
dev.langchain4j.model.chat.response |
tokenUsage(), aiMessage(), finishReason() |
TokenUsage |
dev.langchain4j.model.output |
inputTokenCount(), outputTokenCount(), totalTokenCount() |
InvocationContext |
dev.langchain4j.invocation |
chatMemoryId(), methodName(), methodArguments(), timestamp() |
Spring Boot Actuator
| 端点 | 路径 | 用途 |
|---|---|---|
| health | /actuator/health |
健康检查 |
| info | /actuator/info |
应用信息 |
| metrics | /actuator/metrics |
指标监控 |
十、从 Demo 到生产 Checklist
最后给一个可执行的 Checklist,对照着逐项检查:
less
□ 异常处理
□ 统一响应格式 (ApiResponse)
□ 全局异常处理器 (@RestControllerAdvice)
□ LLM 降级策略 (catch + 友好提示)
□ 不暴露堆栈给前端
□ 限流保护
□ 按 sessionId 限流
□ 按 IP 限流(可选)
□ 全局限流(可选,保护 Token 额度)
□ 返回 429 + retry-after 提示
□ Token 监控
□ AiService Listener 注册
□ 每次响应返回 token 用量
□ 按会话/全局统计
□ 估算费用展示
□ Token 额度预警(可选)
□ 健康检查
□ Spring Boot Actuator
□ 自定义 LLM 健康指标
□ JVM 内存监控
□ K8s/LB 探活配置
□ 部署
□ Dockerfile (多阶段构建)
□ 数据持久化卷挂载
□ JVM 参数优化 (G1GC, OOM dump)
□ 优雅停机 (graceful shutdown)
□ 日志收集 (可选)
总结
Demo 和生产之间的差距,不是"再加几个功能"的差距,是工程思维的差距。
Demo 追求能跑 ------一条 chatModel.chat(message) 足够了。生产追求稳跑------异常要兜住、成本要可控、服务要可观测、部署要可复现。
本文实现的五个生产级能力,每一个都基于 LangChain4j 1.17.2 的真实 API。
bash
# 启动
java -DDASHSCOPE_API_KEY=sk-xxx -jar target/langchain4j-demo-1.0.0-SNAPSHOT.jar
# 测试
curl "http://localhost:8080/api/production/mall/chat?sessionId=user-001&message=你好"
curl "http://localhost:8080/api/production/mall/token-stats"
curl "http://localhost:8080/actuator/health"