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

105 阅读14分钟

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

前几篇我们分别深入了 RAG、Agent/Tool Calling、Chat Memory 三个核心能力。但说实话——能跑通 Demo 和能上线生产,中间还差着十万八千里。

这篇就来填这个坑:异常处理、限流保护、Token 追踪、健康检查、容器化部署。

为什么需要这篇

先看一个真实的 Demo 代码:

@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>
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
@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 调用异常,返回预设的降级文案:

@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 missing400 + {"message":"缺少必填参数: message"}
限流触发无限流429 + {"message":"请求过于频繁,60秒内最多20次请求"}

二、滑动窗口限流

问题

没有限流意味着:一个恶意用户可以用脚本每秒发 100 个请求,直接快速把你的 Token 额度打光。

解决方案

纯内存滑动窗口限流器,不依赖 Redis,适合单机部署:

@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):

production:
  rate-limit:
    window-seconds: 60    # 时间窗口(秒)
    max-requests: 20      # 窗口内最大请求数

原理图解

时间轴 →

|--60s窗口--|
              ^-- 清理过期 --^
                            |-- 当前请求队列(最多20个)--|
                            
用户 A 第 21 次请求 → timestamps.size()=20 → 触发限流
返回:429 "请求过于频繁,60秒内最多20次请求,请3秒后重试"

多级限流策略

实际生产中可以按不同维度限流:

// 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 中的关键接口:

# 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

// 编译报错!
public class TokenUsageTracker implements
        AiServiceResponseReceivedListener,    // getEventClass() → Class<AiServiceResponseReceivedEvent>
        AiServiceErrorListener {               // getEventClass() → Class<AiServiceErrorEvent>
    // 错误:两者都定义了 getEventClass(),但返回类型不兼容
}

原因:两个接口都从 AiServiceListener<T> 继承了 getEventClass() 方法,但返回类型分别是 Class<AiServiceResponseReceivedEvent>Class<AiServiceErrorEvent>——Java 泛型不允许这种"不相关的返回类型"共存。

解决方案:拆成两个独立监听器 + 一个核心追踪器。

核心追踪器 TokenUsageTracker
@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();
        // ... 记录错误信息
    }
}
两个监听器
// 监听成功响应
@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 中注册
@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=你好 后返回:

{
  "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 查看全局统计:

{
  "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 + 自定义健康指标

添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
自定义健康指标
@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
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: always
访问效果
GET /actuator/health
{
  "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 次)DOWNAPI Key 失效/网络中断

五、Docker 容器化部署

多阶段 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

构建与运行

# 构建镜像
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 参数说明

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初始/最大堆内存,避免动态扩容抖动
UseG1GCG1 垃圾回收器,适合多核服务端
MaxGCPauseMillis=200GC 停顿目标 200ms
HeapDumpOnOutOfMemoryErrorOOM 时自动 dump,方便事后分析
server.shutdown=graceful优雅停机,等待正在处理的请求完成

六、对比总结:Demo 接口 vs 生产接口

接口对比

维度Demo (/mall/chat)生产 (/api/production/mall/chat)
响应格式StringApiResponse<T> 统一格式
异常处理Spring 默认 500全局异常处理器 + 降级文案
限流60 秒内最多 20 次
Token 追踪每次 API 返回 token 用量
健康检查/actuator/health + 自定义指标
部署java -jarDocker 容器化

响应对比

Demo 版响应(成功时):

您好!我是XX商城智能客服,请问有什么可以帮您?

Demo 版响应(LLM 超时时):

{
  "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"
}

生产版响应(成功时):

{
  "code": 200,
  "message": "success",
  "data": "您好!我是XX商城智能客服,请问有什么可以帮您?",
  "timestamp": "2026-08-18T14:30:00",
  "tokenUsage": {
    "inputTokens": 150,
    "outputTokens": 80,
    "totalTokens": 230
  }
}

生产版响应(LLM 超时时):

{
  "code": 200,
  "message": "success",
  "data": "抱歉,客服系统暂时繁忙,请稍后再试。如需紧急帮助,请拨打客服热线 021-88889999。",
  "timestamp": "2026-08-18T14:30:00",
  "tokenUsage": null
}

注意生产版超时时不返回 500——因为 LLM 不可用不等于整个服务不可用。客服降级为电话引导,用户仍然有出口。


七、完整接口清单

=== 生产级接口 ===

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,编译报错。

类型 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。

解法:每个字段都做空判断:

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监听器根接口
AiServiceResponseReceivedListenerdev.langchain4j.observability.api.listener监听成功响应
AiServiceErrorListenerdev.langchain4j.observability.api.listener监听调用失败
AiServiceCompletedListenerdev.langchain4j.observability.api.listener监听服务完成

事件类

事件包路径关键方法
AiServiceResponseReceivedEventdev.langchain4j.observability.api.eventresponse(), request(), invocationContext()
AiServiceErrorEventdev.langchain4j.observability.api.eventerror(), invocationContext()
AiServiceCompletedEventdev.langchain4j.observability.api.eventresult() (Optional)

Token 用量

包路径方法
ChatResponsedev.langchain4j.model.chat.responsetokenUsage(), aiMessage(), finishReason()
TokenUsagedev.langchain4j.model.outputinputTokenCount(), outputTokenCount(), totalTokenCount()
InvocationContextdev.langchain4j.invocationchatMemoryId(), methodName(), methodArguments(), timestamp()

Spring Boot Actuator

端点路径用途
health/actuator/health健康检查
info/actuator/info应用信息
metrics/actuator/metrics指标监控

十、从 Demo 到生产 Checklist

最后给一个可执行的 Checklist,对照着逐项检查:

□ 异常处理
  □ 统一响应格式 (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。

# 启动
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"