从 Demo 到生产:LangChain4j 工程化落地全指南

从 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

最初尝试让一个类同时实现 AiServiceResponseReceivedListenerAiServiceErrorListener

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 接口冲突

现象 :一个类同时实现 AiServiceResponseReceivedListenerAiServiceErrorListener,编译报错。

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.HealthHealthIndicatorspring-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"
相关推荐
这张生成的图像能检测吗1 小时前
(论文速读)TADNet:心磁图信号去噪的时间分解和基于注意力的深度学习
人工智能·深度学习·信号处理·信号去噪
小小测试开发1 小时前
AI Agent 回归测试:Replay 录一次、CI 跑千遍,像 Jest 一样给非确定性系统写断言
人工智能·ci/cd
csdn_aspnet1 小时前
开源人工智能编码代理列表
人工智能·continue·cline·goose·aider·opencode·openhands
正经教主1 小时前
AI提示词工程(高阶)第20课:编程领域专项提示词设计
人工智能
程序员-李俞1 小时前
Mistral OCR 4真正改变的不是“识字”:文档AI正在变成Agent的数据入口
人工智能·windows·ai作画·aigc·ocr·ai编程·ai写作
新知图书1 小时前
7.1 User特质的周期提取与自进化(智能体工程)
人工智能·agent·ai agent·智能体
阿里云大数据AI技术1 小时前
免费领票!9月22日-24日,2026云栖大会杭州见
大数据·人工智能·agent
circuitsosk1 小时前
AI输出的“质检员”:构建智能体质量评估、异常检测与人工兜底的三层防线
人工智能·python·microsoft·正则表达式·langchain
IvanCodes1 小时前
我做了一个软著 Skill,可以一键生成申请材料
人工智能·agent