Java工程师集成大模型:六个我踩过的坑

作为 Java 后端工程师,这两年接了不少把 AI 大模型集成到现有系统的需求。从 Spring Boot 调通义千问、DeepSeek,到用 LangChain4j 做 RAG 检索增强,踩的坑不少。今天整理几个最常见的,帮大家少走弯路。

坑一:API Key 直接写在配置文件里

这是最低级也最危险的坑。很多人图方便,把 API Key 写进 application.yml 然后提交到 GitHub。

# ❌ 错误示例:Key 直接明文写在配置里
spring:
  ai:
    api-key: sk-xxxxxxxxxxxxxxxxxxxx

# ✅ 正确做法:从环境变量读取
spring:
  ai:
    api-key: ${DEEPSEEK_API_KEY}

# 生产环境用配置中心(如 Nacos/Apollo)
spring:
  ai:
    api-key: ${AI_API_KEY:默认值}  # 带默认值,方便本地测试

另外,API Key 要定期轮换,不要在代码里 hardcode。推荐用公司的配置中心统一管理,敏感信息和代码彻底分离。

坑二:同步调用大模型把接口堵死

Spring Boot 默认是同步处理请求,当大模型响应时间在 5-30 秒时,整个线程就被堵住了。并发量一上来,Tomcat 线程池直接耗尽。

// ❌ 错误示例:同步调用,阻塞线程
@RestController
public class AIController {
    @Autowired private ChatClient chatClient;

    @GetMapping("/ai/chat")
    public String chat(String question) {
        // 大模型响应耗时 10 秒?整个请求就卡 10 秒
        return chatClient.prompt().user(question).call().content();
    }
}

// ✅ 正确做法:用 WebClient 或 @Async 做异步化
@RestController
public class AIController {
    @GetMapping("/ai/chat")
    public CompletableFuture<String> chatAsync(String question) {
        return CompletableFuture.supplyAsync(() ->
            chatClient.prompt().user(question).call().content()
        );
    }
}

// 或者更优雅:用 Spring WebFlux 响应式编程
// 配置独立的 AI 调用线程池,不占用业务线程池
@Bean
public ExecutorService aiExecutor() {
    return new ThreadPoolExecutor(
        10, 50, 60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(100),
        new ThreadFactoryBuilder().setNameFormat("ai-call-%d").build()
    );
}

坑三:大模型回复格式不稳定——直接当 JSON 用

大多数项目希望 AI 返回结构化 JSON,但大模型的输出本身是非确定性的,直接解析很容易报错。

// ❌ 危险示例:假设大模型100%返回合法JSON
String raw = chatClient.prompt().user("返回用户信息").call().content();
User user = objectMapper.readValue(raw, User.class); // 随时爆炸

// ✅ 正确做法:用结构化输出约束 + 异常兜底
@RestController
public class AIController {

    // Spring AI 支持结构化输出
    @GetMapping("/ai/user")
    public User chatUser(String name) {
        return chatClient.prompt()
            .user("请返回用户信息,格式为JSON,包含name和age字段")
            .call()
            .entity(User.class);  // Spring AI 自动解析,失败抛异常
    }

    // 同时加 try-catch 兜底,防止解析失败导致整次调用500
    public User safeGetUser(String name) {
        try {
            return chatUser(name);
        } catch (Exception e) {
            log.error("AI返回格式异常,原始内容:{}", raw, e);
            return User.builder().name(name).age(0).build(); // 返回空对象而非崩溃
        }
    }
}

坑四:Token 无限增长导致费用爆炸

大模型按 Token 计费,很多项目上线前没做 Token 限制,上线后一查账单——一个月烧了几万块。

# application.yml 配置 Token 上限
spring:
  ai:
    chat:
      options:
        maxTokens: 500          # 单次回复上限
        temperature: 0.7         # 控制随机性,降低幻觉

# 或者代码层面硬限制
public String safeChat(String question) {
    // 先截断用户输入,控制 Token 总数
    String truncated = question.length() > 1000
        ? question.substring(0, 1000)
        : question;

    // 只取最近 N 条历史消息,防止上下文无限膨胀
    List<String> recent = history.subList(
        Math.max(0, history.size() - 6), history.size()
    );

    return chatClient.prompt()
        .messages(recent)
        .user(truncated)
        .call()
        .content();
}

坑五:没有 Prompt 缓存意识,反复发送相同上下文

每个对话都把系统 Prompt 重复发送,白花花的银子就这么浪费了。

// ❌ 低效:每次请求都发完整系统 Prompt(每个 Token 都是钱)
user(question) {
    return chatClient.prompt()
        .system("你是一个专业的Java后端助手,负责代码审查...")
        .user(question)
        .call().content();
}

// ✅ 正确:只发送变更部分,用 session 机制维护上下文
@Autowired
private ChatMemory chatMemory;  // Spring AI 的 ChatMemory

@GetMapping("/ai/chat")
public String chat(String sessionId, String question) {
    // ChatMemory 自动管理会话上下文,只传输必要信息
    ChatResponse response = chatClient.prompt()
        .advisors(new MessageChatMemoryAdvisor(
            chatMemory, sessionId, 10  // 保留最近10条
        ))
        .user(question)
        .call();
    return response.getResult().getOutput().getText();
}

坑六:生产环境没有熔断降级

大模型 API 不是 100% 可用的,网络抖动、API 维护、Token 超限都会导致接口超时。如果没做降级,大模型挂了你的业务也跟着挂。

// Spring Cloud Resilience4j 熔断器
@Bean
public CircuitBreakerFactory circuitBreakerFactory() {
    return new Resilience4JCircuitBreakerFactory();
}

@GetMapping("/ai/chat")
@CircuitBreaker(name = "aiService", fallbackMethod = "chatFallback")
public String chat(String question) {
    return chatClient.prompt().user(question).call().content();
}

// 降级方法:大模型不可用时返回兜底内容
public String chatFallback(String question, Exception e) {
    log.warn("AI服务不可用,启动降级:{}", e.getMessage());
    return "当前AI服务繁忙,请稍后再试或联系管理员";
}

总结

坑点 风险等级 解决方案
API Key 明文配置 🔴 高 环境变量 / 配置中心
同步阻塞调用 🔴 高 异步化 / WebFlux
直接解析 JSON 🟡 中 结构化输出 + try-catch
Token 无限增长 🟡 中 maxTokens 限制 + 截断
重复发送上下文 🟡 中 ChatMemory 会话管理
无熔断降级 🔴 高 Resilience4j 熔断器

集成大模型不是把 API 调通就完事了,高并发、低延迟、高可用每个环节都要考虑。上线前做好压测,算清楚 Token 成本,这才是正经事。

⚠️ 声明:本文相关内容仅供参考。

发表评论

苏ICP备18039580号-2