1. 概述

本项目演示如何通过 Spring AI Alibaba 调用 DashScope 大模型完成文本分类,并逐步展示从基础 Prompt 到结构化输出的演进过程。

分类类别 含义
BUSINESS 商业、金融、市场
SPORT 体育赛事、竞技
TECHNOLOGY 软件、人工智能、网络安全
OTHER 其他无法归入上述三类的内容

核心依赖: Spring Boot 3.x、Spring AI Alibaba、DashScope 模型服务。


2. 架构设计分析

2.1 整体架构

HTTP POST

构建 Prompt

API 调用

返回文本

解析/映射

JSON 响应

客户端 / curl

ClassificationController

Spring AI ChatClient

DashScope 大模型

  • ClassificationController 暴露多个端点,每个端点封装不同的 Prompt 策略。
  • ChatClient 是 Spring AI 提供的高级抽象,封装了与 DashScope 的通信、Prompt 组织、结果解析等功能。
  • 模型服务位于阿里云 DashScope 平台,通过 API Key 鉴权。

2.2 请求处理流程

以“结构化输出”端点为例,时序图如下:

DashScope ChatClient Controller 客户端 DashScope ChatClient Controller 客户端 POST /classify/structured { "text": "..." } 构建消息历史 + 用户文本 发送 Prompt(含指令、示例、用户输入) 返回 "TECHNOLOGY" content() 或 entity(ClassificationType.class) { "category": "TECHNOLOGY", ... }

Spring AI 的 entity() 方法会尝试将模型输出反序列化为指定的 Java 类型(此处为枚举),如果失败则抛出异常,此时控制器可实施降级策略。


3. 核心原理解析

3.1 ChatClient 工作机制

ChatClient 通过 Builder 创建,关键配置:

  • defaultOptions:设置全局默认参数,如 temperature(0.0) 令输出更具确定性。
  • prompt().system():添加系统消息,定义任务规则。
  • prompt().user():添加用户消息,传递待分类文本。
  • call().content()call().entity():同步获取结果。

内部流程:

  1. 将 system、user 以及可能存在的 messages 历史拼接为完整的消息列表。
  2. 通过 DashScope 模型发送请求。
  3. 接收原始文本响应。
  4. 若使用 entity(),则用 Jackson 等工具反序列化为目标类型。

3.2 Few-Shot 与消息历史原理

Few-shot learning 通过在 prompt 中提供若干“输入→期望输出”的示例,让模型理解分类模式。在代码中有两种实现方式:

  1. Prompt 内嵌示例:将示例直接写入 system 或 user 消息。
  2. 消息历史:使用 List<Message> 包含 SystemMessage、UserMessage、AssistantMessage 组成对话上下文,模型会根据历史对话推断当前任务。

使用消息历史的优势:结构清晰,可重用,易于扩展示例。

3.3 结构化输出原理

调用 call().entity(ClassificationType.class) 时,Spring AI 会:

  • 获取模型返回的文本内容。
  • 尝试将内容转为大写,匹配枚举常量名。
  • 如果文本包含额外空白(如 " technology "),需要添加 trim 逻辑,否则可能解析失败。
  • 如果没有匹配值,抛出 ConversionException,故需在控制器中捕获并降级。

4. 代码问题深度诊断

4.1 请求体设计不当

原设计:

@PostMapping("/classify/class-names")
String classifyClassNames(@RequestBody String text) { ... }
  • 使用裸 String 作为请求体,不符合 RESTful 习惯,无法扩展字段(如后续需要语言、置信度阈值等)。
  • 客户端必须发送 Content-Type: application/json 以及一个裸字符串,容易引起误解。

4.2 返回类型不一致

  • /classify/class-names 等端点返回 String(模型原始输出)。
  • /classify/structured-output 返回 ClassificationType 枚举(直接序列化为字符串)。
    这种不一致导致前端适配困难,且无法返回辅助信息(如置信度、原始响应)。

4.3 硬编码 Prompt

每个方法内直接编写系统提示,维护成本高,扩展不便,且不易进行多语言或 A/B 测试。

4.4 异常处理缺失

call().content() 可能因为网络异常返回 null,或因模型输出无法解析为枚举时抛出异常,但原代码未做任何 try-catch 或降级处理。

4.5 部署指南与代码不一致

部署指南中的 curl 命令仍使用旧端点 /classify/class-names,而优化后的代码已将端点改为 /simple。若按指南部署,将返回 404。


5. 错误修正与代码优化

5.1 统一请求/响应模型

改进:

public record ClassificationRequest(String text) { }

public record ClassificationResponse(
    ClassificationType category,
    String confidence,
    String rawResponse
) { }

所有端点均接收 ClassificationRequest JSON 对象,返回 ClassificationResponse

5.2 强化枚举设计

public enum ClassificationType {
    BUSINESS("商业", "Commerce, finance, markets"),
    SPORT("体育", "Athletic events, tournaments"),
    TECHNOLOGY("技术", "Software, AI, cybersecurity"),
    OTHER("其他", "Anything else");

    private final String displayName;
    private final String description;

    // 构造函数、getter 省略
}

5.3 增强异常处理与容错

@PostMapping("/structured")
public ClassificationResponse classifyStructured(@RequestBody ClassificationRequest request) {
    try {
        ClassificationType result = chatClient.prompt()
            .messages(getFewShotsHistory())
            .user(request.text())
            .call()
            .entity(ClassificationType.class);
        return new ClassificationResponse(result, "HIGH", result.name());
    } catch (Exception e) {
        // 降级:使用字符串解析
        log.warn("Structured output failed, fallback to text parsing", e);
        return classifyWithPrompt(request.text(), buildDescriptionsPrompt());
    }
}

同时实现全局异常处理,避免直接抛出堆栈信息。

5.4 统一端点设计(新旧对照)

为避免混淆,新设计使用更清晰的路径:

旧端点 新端点 说明
/classify/class-names /classify/simple 基础分类
/classify/class-descriptions /classify/descriptions 带描述分类
/classify/few-shots-prompt /classify/few-shots Few-shot 内嵌
/classify/few-shots-history (合并到 /few-shots 统一使用历史消息
/classify/structured-output /classify/structured 结构化输出
/classify /classify/default 默认策略

部署指南应全部更新为新端点。

5.5 优化后的核心控制器片段

@RestController
@RequestMapping("/classify")
public class ClassificationController {

    private final ChatClient chatClient;

    public ClassificationController(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultOptions(ChatOptions.builder().temperature(0.0).build())
            .build();
    }

    @PostMapping("/simple")
    public ClassificationResponse classifySimple(@RequestBody ClassificationRequest request) {
        return classifyWithPrompt(request.text(), "Classify into: BUSINESS, SPORT, TECHNOLOGY, OTHER. Return only category name.");
    }

    // 其他端点类似...

    private ClassificationResponse classifyWithPrompt(String text, String systemPrompt) {
        String content = chatClient.prompt()
            .system(systemPrompt)
            .user(text)
            .call()
            .content();
        return new ClassificationResponse(parseType(content), "MEDIUM", content);
    }

    private ClassificationType parseType(String raw) {
        if (raw == null) return ClassificationType.OTHER;
        String upper = raw.trim().toUpperCase();
        for (ClassificationType type : ClassificationType.values()) {
            if (upper.contains(type.name())) return type;
        }
        return ClassificationType.OTHER;
    }
}

6. 优化替代方案详细分析

6.1 策略模式重构

动机: 当前每种分类方法都在 Controller 中实现,职责混杂。引入策略模式后,每种 Prompt 技术成为独立组件,方便替换和测试。

«interface»

ClassificationStrategy

+classify(String text) : ClassificationResponse

SimpleStrategy

+classify(text)

DescriptionStrategy

+classify(text)

FewShotStrategy

+classify(text)

StructuredStrategy

+classify(text)

StrategyContext

-ClassificationStrategy strategy

+setStrategy(ClassificationStrategy)

+execute(String text) : ClassificationResponse

Controller 根据请求参数动态选择策略:

@RestController
public class StrategyController {
    private final StrategyContext context;

    @PostMapping("/classify/{strategy}")
    public ClassificationResponse classify(
            @PathVariable String strategy,
            @RequestBody ClassificationRequest request) {
        context.setStrategy(strategy); // 从 Map 中获取对应 bean
        return context.execute(request.text());
    }
}

优势:

  • 符合开闭原则,新增策略无需改动 Controller。
  • 便于进行策略编排、日志记录、性能监控。

6.2 Prompt 配置化与模板引擎

将 Prompt 模板外置到 application.yml 或数据库,支持动态加载:

classification:
  prompts:
    simple: "Classify the text into BUSINESS, SPORT, TECHNOLOGY, OTHER. Return only category name."
    detailed: |
      Classification rules:
      BUSINESS - ...
      SPORT - ...
      ...

使用 Spring 的 @ConfigurationProperties 注入,结合 String.format 或模板引擎(如 Mustache)动态生成 Prompt。这样可实现多语言、多版本的 A/B 测试。

6.3 结果缓存与批量处理

  • 缓存: 对于重复的文本,使用 Caffeine 缓存分类结果,减少 API 调用成本。
  • 批量: 提供 /classify/batch 端点,接收 List<ClassificationRequest>,内部使用并行流调用 ChatClient,显著提升吞吐量。

批量处理注意: DashScope 可能有并发限制,需结合限流器(如 Resilience4j)平滑请求。

代码示例:

@PostMapping("/batch")
public List<ClassificationResponse> classifyBatch(@RequestBody List<ClassificationRequest> requests) {
    return requests.parallelStream()
        .map(req -> classifyWithPrompt(req.text(), buildDescriptionsPrompt()))
        .toList();
}

7. 修正后的部署指南

7.1 环境要求

  • JDK 17+
  • Maven 3.8+(可使用项目自带的 mvnw
  • 有效的 DashScope API Key(以 sk- 开头)

7.2 获取 API Key

  1. 访问 DashScope 控制台
  2. 登录阿里云账号
  3. 在“API-KEY 管理”页面创建新的 Key
  4. 保存备用

7.3 配置与启动

方式一:开发模式

export AI_DASHSCOPE_API_KEY="sk-your-key-here"
cd spring-ai-alibaba-text-classification-example
./mvnw spring-boot:run

方式二:Jar 运行

./mvnw clean package -DskipTests
java -jar target/spring-ai-alibaba-text-classification-example-0.0.1-SNAPSHOT.jar \
    --ai.dashscope.api-key=$AI_DASHSCOPE_API_KEY

服务默认端口为 10093,启动后可通过 http://localhost:10093/actuator/health 检查状态。

7.4 修正后的 API 测试示例

以下命令已替换为新端点,请求体统一使用 JSON 对象:

# 简单分类
curl -X POST http://localhost:10093/classify/simple \
  -H "Content-Type: application/json" \
  -d '{"text": "Apple releases new iPhone with AI features."}'

# 带描述分类
curl -X POST http://localhost:10093/classify/descriptions \
  -H "Content-Type: application/json" \
  -d '{"text": "Stock market hits all-time high."}'

# Few-shot 分类
curl -X POST http://localhost:10093/classify/few-shots \
  -H "Content-Type: application/json" \
  -d '{"text": "Basketball finals conclude with exciting finish."}'

# 结构化输出
curl -X POST http://localhost:10093/classify/structured \
  -H "Content-Type: application/json" \
  -d '{"text": "New AI breakthrough in NLP."}'

响应示例:

{
  "category": "TECHNOLOGY",
  "confidence": "HIGH",
  "rawResponse": "TECHNOLOGY"
}

7.5 Docker 部署

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 10093
ENTRYPOINT ["java", "-jar", "app.jar"]
docker build -t text-classification .
docker run -d -p 10093:10093 \
  -e AI_DASHSCOPE_API_KEY=$AI_DASHSCOPE_API_KEY \
  text-classification

生产环境建议使用 docker-compose 结合环境变量文件管理密钥。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐