【第34篇】文本分类示例
1. 概述
本项目演示如何通过 Spring AI Alibaba 调用 DashScope 大模型完成文本分类,并逐步展示从基础 Prompt 到结构化输出的演进过程。
| 分类类别 | 含义 |
|---|---|
| BUSINESS | 商业、金融、市场 |
| SPORT | 体育赛事、竞技 |
| TECHNOLOGY | 软件、人工智能、网络安全 |
| OTHER | 其他无法归入上述三类的内容 |
核心依赖: Spring Boot 3.x、Spring AI Alibaba、DashScope 模型服务。
2. 架构设计分析
2.1 整体架构
- ClassificationController 暴露多个端点,每个端点封装不同的 Prompt 策略。
- ChatClient 是 Spring AI 提供的高级抽象,封装了与 DashScope 的通信、Prompt 组织、结果解析等功能。
- 模型服务位于阿里云 DashScope 平台,通过 API Key 鉴权。
2.2 请求处理流程
以“结构化输出”端点为例,时序图如下:
Spring AI 的 entity() 方法会尝试将模型输出反序列化为指定的 Java 类型(此处为枚举),如果失败则抛出异常,此时控制器可实施降级策略。
3. 核心原理解析
3.1 ChatClient 工作机制
ChatClient 通过 Builder 创建,关键配置:
- defaultOptions:设置全局默认参数,如
temperature(0.0)令输出更具确定性。 - prompt().system():添加系统消息,定义任务规则。
- prompt().user():添加用户消息,传递待分类文本。
- call().content() 或 call().entity():同步获取结果。
内部流程:
- 将 system、user 以及可能存在的 messages 历史拼接为完整的消息列表。
- 通过 DashScope 模型发送请求。
- 接收原始文本响应。
- 若使用
entity(),则用 Jackson 等工具反序列化为目标类型。
3.2 Few-Shot 与消息历史原理
Few-shot learning 通过在 prompt 中提供若干“输入→期望输出”的示例,让模型理解分类模式。在代码中有两种实现方式:
- Prompt 内嵌示例:将示例直接写入 system 或 user 消息。
- 消息历史:使用
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 技术成为独立组件,方便替换和测试。
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
- 访问 DashScope 控制台
- 登录阿里云账号
- 在“API-KEY 管理”页面创建新的 Key
- 保存备用
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 结合环境变量文件管理密钥。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)