我的AI开发之旅:从零开始用Spring AI Alibaba构建智能应用

作为一名有着3年Spring Boot开发经验的Java程序员,当领导安排我负责给我们的电商系统加上AI客服功能时,说不慌是假的。毕竟之前我最多就是调用过几个REST API,对AI这块完全是小白。经过两个多月的摸爬滚打,我想和大家分享一下使用Spring AI Alibaba的真实体验,以及踩过的那些坑。

为什么我最终选择了Spring AI Alibaba?

第一印象:这不就是Spring吗?

记得当时我在GitHub上找Java AI框架,看到LangChain4j、Spring AI、Spring AI Alibaba这几个选择时,脑袋都大了。但是当我看到Spring AI Alibaba的第一个示例代码时,瞬间有了亲切感:

@RestController
public class ChatController {
    
    @Autowired
    private ChatClient chatClient;
    
    @PostMapping("/chat")
    public String chat(@RequestBody ChatRequest request) {
        return chatClient.prompt()
                .user(request.getMessage())
                .call()
                .content();
    }
}

这不就是我平时写的Spring Boot代码吗?@Autowired@RestController这些注解都是老朋友了,完全不需要重新学习什么架构设计。

配置简单到让人怀疑

我最怕的就是复杂的配置,还记得第一次配置Spring Security时被各种XML配置搞得头晕脑胀。但Spring AI Alibaba的配置简单得让我怀疑:

spring:
  ai:
    alibaba:
      dashscope:
        api-key: ${DASHSCOPE_API_KEY}
        chat:
          options:
            model: qwen-plus
            temperature: 0.7

就这样?对,就这样!添加依赖、写个配置文件、注入个Bean,5分钟就能跑起来一个AI聊天接口。

我的第一个AI应用:智能客服机器人

需求背景

我们的电商平台每天有大量的用户咨询,主要是订单查询、退换货政策、商品信息等重复性问题。老板希望先用AI处理这些标准问题,复杂问题再转人工。

第一版:最基础的问答

@Service
public class CustomerServiceBot {
    
    @Autowired
    private ChatClient chatClient;
    
    public String handleQuestion(String question) {
        String systemPrompt = """
            你是一个专业的电商客服助手。
            请礼貌、准确地回答用户问题。
            如果遇到不确定的问题,请建议联系人工客服。
            """;
            
        return chatClient.prompt()
                .system(systemPrompt)
                .user(question)
                .call()
                .content();
    }
}

这个版本跑起来后,效果还不错,但很快就发现问题了:每次对话都是独立的,AI记不住用户之前说了什么。

第二版:加上对话记忆

@Service
public class CustomerServiceBot {
    
    @Autowired
    private ChatClient chatClient;
    
    // 简单的内存存储,生产环境建议用Redis
    private final Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();
    
    public String handleQuestion(String userId, String question) {
        List<Message> history = conversationHistory.computeIfAbsent(userId, k -> new ArrayList<>());
        
        // 添加用户消息到历史
        history.add(new UserMessage(question));
        
        // 构建完整的对话上下文
        String response = chatClient.prompt()
                .system(getSystemPrompt())
                .messages(history)
                .call()
                .content();
        
        // 添加AI回复到历史
        history.add(new AssistantMessage(response));
        
        // 保持历史记录不要太长,避免token消耗过多
        if (history.size() > 20) {
            history.subList(0, history.size() - 20).clear();
        }
        
        return response;
    }
    
    private String getSystemPrompt() {
        return """
            你是一个专业的电商客服助手。
            用户信息:
            - 平台:XX电商
            - 服务范围:订单查询、退换货、商品咨询、物流信息
            
            回答规则:
            1. 保持礼貌和专业
            2. 答案要准确简洁
            3. 不确定的问题建议转人工
            4. 记住之前的对话内容
            """;
    }
}

这个版本好多了,但我发现一个严重问题:所有数据都存在内存里,应用重启就丢了,而且多实例部署时会有问题。

第三版:引入Redis持久化

@Service
public class CustomerServiceBot {
    
    @Autowired
    private ChatClient chatClient;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String CONVERSATION_KEY_PREFIX = "conversation:";
    private static final int CONVERSATION_TTL = 3600; // 1小时过期
    
    public String handleQuestion(String userId, String question) {
        String conversationKey = CONVERSATION_KEY_PREFIX + userId;
        
        // 从Redis获取历史对话
        List<Map<String, Object>> historyData = (List<Map<String, Object>>) 
            redisTemplate.opsForValue().get(conversationKey);
        
        List<Message> history = historyData != null ? 
            convertToMessages(historyData) : new ArrayList<>();
        
        // 添加用户消息
        history.add(new UserMessage(question));
        
        try {
            String response = chatClient.prompt()
                    .system(getSystemPrompt())
                    .messages(history)
                    .call()
                    .content();
            
            // 添加AI回复
            history.add(new AssistantMessage(response));
            
            // 保存到Redis
            saveConversationToRedis(conversationKey, history);
            
            return response;
            
        } catch (Exception e) {
            log.error("AI调用失败", e);
            return "抱歉,我暂时无法回答您的问题,请稍后再试或联系人工客服。";
        }
    }
    
    private void saveConversationToRedis(String key, List<Message> messages) {
        // 只保留最近10轮对话
        if (messages.size() > 20) {
            messages = messages.subList(messages.size() - 20, messages.size());
        }
        
        List<Map<String, Object>> historyData = convertToMapList(messages);
        redisTemplate.opsForValue().set(key, historyData, CONVERSATION_TTL, TimeUnit.SECONDS);
    }
}

进阶功能:工具调用(Function Calling)

客服机器人能聊天了,但用户问订单信息时,它只能说”请提供订单号”,不能真正查询数据库。这时候我发现了Spring AI Alibaba的一个强大功能:Function Calling。

实现订单查询功能

@Component
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Description("根据订单号查询订单详细信息")
    public OrderInfo getOrderInfo(
            @Description("订单号") String orderNumber) {
        
        Order order = orderRepository.findByOrderNumber(orderNumber);
        if (order == null) {
            return new OrderInfo("未找到该订单", null, null, null);
        }
        
        return new OrderInfo(
            order.getStatus(),
            order.getCreateTime(),
            order.getTotalAmount(),
            order.getItems().stream()
                .map(item -> item.getProductName() + " x " + item.getQuantity())
                .collect(Collectors.joining(", "))
        );
    }
    
    @Description("查询物流信息")
    public String getLogisticsInfo(@Description("订单号") String orderNumber) {
        // 调用物流API
        return logisticsService.getTrackingInfo(orderNumber);
    }
}

@Service
public class SmartCustomerService {
    
    @Autowired
    private ChatClient chatClient;
    
    public String handleQuestionWithTools(String userId, String question) {
        return chatClient.prompt()
                .system(getSystemPrompt())
                .user(question)
                .functions("getOrderInfo", "getLogisticsInfo") // 注册可用的工具
                .call()
                .content();
    }
}

现在用户说”我想查一下订单123456的情况”,AI会自动调用getOrderInfo函数查询数据库,然后组织语言回复用户。太神奇了!

添加退换货处理

@Component
public class RefundService {
    
    @Description("检查订单是否可以退换货")
    public RefundEligibility checkRefundEligibility(@Description("订单号") String orderNumber) {
        Order order = orderRepository.findByOrderNumber(orderNumber);
        if (order == null) {
            return new RefundEligibility(false, "订单不存在");
        }
        
        // 检查是否在退换货期限内
        LocalDateTime orderTime = order.getCreateTime();
        LocalDateTime now = LocalDateTime.now();
        long days = ChronoUnit.DAYS.between(orderTime, now);
        
        if (days > 7) {
            return new RefundEligibility(false, "已超过7天退换货期限");
        }
        
        if (order.getStatus().equals("DELIVERED")) {
            return new RefundEligibility(true, "可以申请退换货");
        }
        
        return new RefundEligibility(false, "订单状态不允许退换货");
    }
    
    @Description("创建退换货申请")
    public String createRefundRequest(
            @Description("订单号") String orderNumber,
            @Description("退换货原因") String reason) {
        
        // 创建退换货申请逻辑
        RefundRequest request = new RefundRequest();
        request.setOrderNumber(orderNumber);
        request.setReason(reason);
        request.setStatus("PENDING");
        request.setCreateTime(LocalDateTime.now());
        
        refundRepository.save(request);
        
        return "退换货申请已提交,申请单号:" + request.getId() + ",我们会在24小时内处理。";
    }
}

与其他框架的真实对比

经过实际使用,我也尝试了LangChain4j和Spring AI官方版本。让我客观地分享一下对比体验:

Spring AI Alibaba vs LangChain4j

LangChain4j的优势:

1. 文档和社区更成熟 LangChain4j的文档确实更详细,社区也更活跃。我遇到问题时,在GitHub和Stack Overflow上更容易找到答案。

2. 模型支持更广泛 支持的AI服务提供商更多,包括Azure OpenAI、Anthropic Claude、Google PaLM等。如果你需要用多种不同的模型,LangChain4j更有优势。

3. 工具链更完整 提供了更多开箱即用的工具,比如PDF解析、Web搜索、数据库查询等。

// LangChain4j的工具定义方式
@Tool("搜索网络信息")
public String searchWeb(String query) {
    return webSearchService.search(query);
}

4. 灵活性更高 可以更细粒度地控制AI的行为,比如自定义token计算、重试策略等。

Spring AI Alibaba的优势:

1. Spring生态集成度 如果你的项目已经是Spring技术栈,集成成本几乎为零。所有的Spring特性(事务、缓存、安全、监控)都能无缝使用。

// 可以直接使用Spring的声明式事务
@Transactional
public String processUserQuery(String userId, String query) {
    // AI处理 + 数据库操作都在同一个事务中
    String response = chatClient.prompt().user(query).call().content();
    userInteractionService.saveInteraction(userId, query, response);
    return response;
}

2. 中文支持更好 阿里云的通义模型对中文的理解确实更准确,特别是在处理中文的语境、习惯用语方面。

3. 企业级特性 内置了很多企业需要的功能,比如API限流、监控埋点、错误处理等。

4. 部署和运维 如果你在用阿里云,部署和监控都更方便。而且有企业级支持。

性能对比(基于我的测试):

// 我做的简单性能测试
@Test
public void performanceTest() {
    int requestCount = 100;
    String testMessage = "请介绍一下你的功能";
    
    // Spring AI Alibaba
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < requestCount; i++) {
        springAiService.chat(testMessage);
    }
    long springAiTime = System.currentTimeMillis() - startTime;
    
    // LangChain4j
    startTime = System.currentTimeMillis();
    for (int i = 0; i < requestCount; i++) {
        langChain4jService.chat(testMessage);
    }
    long langChainTime = System.currentTimeMillis() - startTime;
    
    System.out.println("Spring AI Alibaba: " + springAiTime + "ms");
    System.out.println("LangChain4j: " + langChainTime + "ms");
}

在我的测试中,两者性能差距不大,主要瓶颈都在网络调用上。

Spring AI Alibaba vs Spring AI 官方版

Spring AI 官方版的优势:

1. 官方维护 这是Spring官方项目,长期维护有保障,不用担心突然停止更新。

2. 国际化支持更好 如果需要支持多种语言和地区,官方版本的国际化做得更好。

3. 文档标准化 文档风格和Spring其他项目保持一致,学习体验更连贯。

Spring AI Alibaba的优势:

1. 本土化优势

  • 网络访问更稳定(不用翻墙)
  • 中文处理能力更强
  • 符合国内的合规要求

2. 阿里云生态 如果你已经在使用阿里云的其他服务,集成度会更好。

3. 企业级功能 提供了更多企业需要的功能,比如多租户支持、详细的使用统计等。

实际项目中的最佳实践

经过几个月的使用,我总结了一些实用的经验:

1. 错误处理和降级策略

@Service
public class RobustChatService {
    
    @Autowired
    private ChatClient chatClient;
    
    @Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public String chat(String message) {
        try {
            return chatClient.prompt()
                    .user(message)
                    .call()
                    .content();
        } catch (Exception e) {
            log.error("AI调用失败: ", e);
            throw e;
        }
    }
    
    @Recover
    public String recover(Exception e, String message) {
        // 降级处理:返回预设回复或转人工
        return "抱歉,AI助手暂时不可用,已为您转接人工客服。";
    }
}

2. 成本控制

AI调用是要花钱的,必须做好成本控制:

@Component
public class AIUsageController {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String DAILY_USAGE_KEY = "ai_usage:daily:";
    private static final int DAILY_LIMIT = 1000; // 每天1000次调用限制
    
    public boolean canMakeRequest(String userId) {
        String key = DAILY_USAGE_KEY + LocalDate.now().toString() + ":" + userId;
        Long count = redisTemplate.opsForValue().increment(key);
        
        if (count == 1) {
            // 第一次调用,设置过期时间
            redisTemplate.expire(key, Duration.ofDays(1));
        }
        
        return count <= DAILY_LIMIT;
    }
}

3. 监控和日志

@Component
@Slf4j
public class AIMetrics {
    
    private final MeterRegistry meterRegistry;
    private final Counter aiCallCounter;
    private final Timer aiCallTimer;
    
    public AIMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.aiCallCounter = Counter.builder("ai.calls.total")
                .description("Total AI calls")
                .register(meterRegistry);
        this.aiCallTimer = Timer.builder("ai.calls.duration")
                .description("AI call duration")
                .register(meterRegistry);
    }
    
    public String monitoredChat(String message) {
        return Timer.Sample.start(meterRegistry)
                .stop(aiCallTimer.recordCallable(() -> {
                    aiCallCounter.increment();
                    return chatClient.prompt().user(message).call().content();
                }));
    }
}

4. 内容安全

@Service
public class ContentSafetyService {
    
    public boolean isContentSafe(String content) {
        // 集成阿里云内容安全服务
        try {
            ContentScanResponse response = contentScanClient.scanText(content);
            return response.getResult().equals("PASS");
        } catch (Exception e) {
            log.error("内容安全检查失败", e);
            // 安全起见,异常时认为不安全
            return false;
        }
    }
    
    public String safeChatResponse(String userMessage) {
        if (!isContentSafe(userMessage)) {
            return "您的消息包含不当内容,请重新输入。";
        }
        
        String response = chatClient.prompt().user(userMessage).call().content();
        
        if (!isContentSafe(response)) {
            return "抱歉,我无法回答这个问题。";
        }
        
        return response;
    }
}

遇到的坑和解决方案

1. Token限制问题

最开始我没注意到token限制,对话历史越来越长,最后超出了模型的上下文窗口:

// 错误的做法:无限制保存历史
public String chat(String message) {
    allHistory.add(new UserMessage(message)); // 这里会越来越大
    return chatClient.prompt().messages(allHistory).call().content();
}

// 正确的做法:智能截断
public String chat(String message) {
    history.add(new UserMessage(message));
    
    // 简单截断:只保留最近的对话
    if (history.size() > 20) {
        history = history.subList(history.size() - 20, history.size());
    }
    
    return chatClient.prompt().messages(history).call().content();
}

2. 并发问题

多个用户同时聊天时,如果处理不当会互相影响:

// 错误的做法:共享状态
@Service
public class ChatService {
    private List<Message> sharedHistory = new ArrayList<>(); // 危险!
}

// 正确的做法:按用户隔离
@Service
public class ChatService {
    private final Map<String, List<Message>> userHistories = new ConcurrentHashMap<>();
    
    public String chat(String userId, String message) {
        List<Message> userHistory = userHistories.computeIfAbsent(userId, k -> new ArrayList<>());
        // 后续处理...
    }
}

3. 内存泄漏

对话历史如果只增不减,很容易造成内存泄漏:

@Scheduled(fixedRate = 3600000) // 每小时清理一次
public void cleanupOldConversations() {
    LocalDateTime cutoff = LocalDateTime.now().minusHours(2);
    userHistories.entrySet().removeIf(entry -> {
        // 根据最后活跃时间判断是否清理
        return getLastActiveTime(entry.getKey()).isBefore(cutoff);
    });
}

总结:选择建议

经过几个月的实践,我的建议是:

选择Spring AI Alibaba的场景:

  • 已有Spring Boot项目,需要快速集成AI功能
  • 主要服务中文用户
  • 使用阿里云基础设施
  • 需要企业级支持和服务保障
  • 团队对Spring生态比较熟悉

选择LangChain4j的场景:

  • 需要支持多种AI模型和服务商
  • 对灵活性要求较高
  • 有复杂的AI工作流需求
  • 团队有较强的自研能力

选择Spring AI官方版的场景:

  • 国际化项目
  • 长期维护考虑
  • 需要与Spring其他项目深度集成

对于我们这种中小型项目,Spring AI Alibaba确实是一个很好的选择。它让我这个AI小白也能快速上手,而且生产环境运行也很稳定。当然,技术选型还是要根据具体业务需求来定,没有银弹。

希望我的这些经验能帮到正在选择Java AI框架的朋友们。如果你也在使用这些框架,欢迎分享你的经验!

 

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注