第1703篇:模式匹配(Pattern Matching)在AI结果处理中的实战
第1703篇:模式匹配(Pattern Matching)在AI结果处理中的实战
有一句话我越来越认同:好的代码读起来接近自然语言。
模式匹配(Pattern Matching)就是在推动Java朝这个方向走。Java 16引入了 instanceof 的模式匹配,Java 21正式稳定了switch的模式匹配。这两个加起来,让处理AI响应的代码从"一堆if-else加强制转型"变成了"清晰声明式的逻辑描述"。
今天我们系统地把这块讲清楚。结合前两篇的Records和Sealed Classes,你会发现这三个特性组合起来,在AI工程里能碰出很多火花。
一、老写法的痛点回顾
先看看没有模式匹配之前,处理AI响应变体的代码有多难受:
// 假设LLMResult是个基类,有各种子类
public class OldStyleHandler {
public String handleResult(Object result) {
if (result instanceof LLMSuccessResult) {
LLMSuccessResult success = (LLMSuccessResult) result; // 先判断再转型,重复
return success.getContent();
} else if (result instanceof ContentFilteredResult) {
ContentFilteredResult filtered = (ContentFilteredResult) result;
log.warn("内容被过滤: {}", filtered.getReason());
return "抱歉,无法回答这个问题";
} else if (result instanceof TokenLimitResult) {
TokenLimitResult limit = (TokenLimitResult) result;
if (limit.getSide().equals("input")) {
return "输入太长,请缩短";
} else {
return "生成内容被截断";
}
} else {
return "未知错误";
}
}
}问题是:
- 每次都要先
instanceof判断,再强制转型,啰嗦 - if-else链不穷举,加了新的子类编译器不提醒你
- 嵌套逻辑进一步降低可读性
- 测试覆盖率难保证
二、instanceof 模式匹配:最常用的入门特性
Java 16开始,instanceof 支持同时做判断和绑定变量:
// 旧写法
if (result instanceof LLMSuccessResult) {
LLMSuccessResult success = (LLMSuccessResult) result; // 多此一举的转型
process(success);
}
// 新写法
if (result instanceof LLMSuccessResult success) {
process(success); // 直接用,不需要单独声明变量
}表面上只是少写了一行,但有个重要的隐含变化:绑定变量 success 的作用域由编译器严格控制。它只在条件为真的分支里有效,不可能在else分支里误用。
在AI服务里,这个特性最常用在工具调用的参数处理上:
@Service
public class ToolCallDispatcher {
// 工具调用参数是个多类型的联合
public ToolResult dispatch(ToolCall toolCall) {
Object params = toolCall.parameters();
if (params instanceof SearchParams search) {
return executeSearch(search.query(), search.maxResults());
}
if (params instanceof DatabaseQueryParams dbQuery) {
return executeDBQuery(dbQuery.sql(), dbQuery.timeout());
}
if (params instanceof HttpRequestParams httpReq) {
return executeHttpRequest(httpReq.url(), httpReq.method(), httpReq.headers());
}
if (params instanceof CodeExecutionParams codeExec) {
// 代码执行有额外的安全检查
if (!codeExec.isSandboxed()) {
log.warn("不安全的代码执行请求: {}", toolCall.toolId());
return ToolResult.error("代码执行必须在沙箱环境中");
}
return executeCode(codeExec.code(), codeExec.language());
}
return ToolResult.error("未知的工具参数类型: " + params.getClass().getName());
}
}这里还用了一个小技巧:在instanceof检查后面可以加条件(需要Java 21+):
// 带条件的instanceof模式匹配
if (params instanceof CodeExecutionParams codeExec && codeExec.isSandboxed()) {
// 只有类型匹配且条件满足才进入这个分支
return executeCode(codeExec.code(), codeExec.language());
}三、switch 模式匹配:真正的杀手锏
switch的模式匹配才是重头戏。配合前面说的密封类,可以把AI结果处理写得非常优雅。
先看基础用法:
public class AIResultProcessor {
public ProcessResult process(LLMResult result) {
return switch (result) {
case LLMResult.Success success -> {
// 成功处理逻辑
String content = success.content();
int tokens = success.completionTokens();
log.info("AI响应成功,tokens: {}", tokens);
yield ProcessResult.ok(content);
}
case LLMResult.ContentFiltered filtered -> {
log.warn("内容被过滤: {}", filtered.reason());
yield ProcessResult.rejected(
"您的请求触发了内容安全规则,类别:" + filtered.filterCategory()
);
}
case LLMResult.TokenLimitExceeded limit -> {
int overBy = limit.requestedTokens() - limit.maxAllowed();
log.info("Token超限{}个, 侧: {}", overBy, limit.side());
yield ProcessResult.needsTruncation(overBy);
}
case LLMResult.Timeout timeout -> {
log.error("AI请求超时,等待了{}ms, endpoint: {}",
timeout.waitedMs(), timeout.endpoint());
yield ProcessResult.retryable("服务超时,请稍后重试");
}
case LLMResult.ParseFailure failure -> {
log.error("AI响应解析失败: {}", failure.parseError());
alertService.sendAlert("AI响应解析失败", failure.rawResponse());
yield ProcessResult.error("服务内部错误");
}
};
// 如果漏掉了任何一个case,这里编译器会报错
}
}yield 关键字用于在switch的代码块里返回值,这个语法细节经常有人忘记。
四、带守卫条件(Guard)的模式匹配
Java 21的switch模式匹配支持 when 守卫,可以在类型匹配的同时加额外条件:
public String classifyAIResponse(LLMResult result) {
return switch (result) {
// 带守卫:成功且token消耗少(轻量响应)
case LLMResult.Success s when s.completionTokens() < 100 ->
"轻量响应(" + s.completionTokens() + " tokens)";
// 带守卫:成功但token消耗多(重量响应)
case LLMResult.Success s when s.completionTokens() >= 100 ->
"详细响应(" + s.completionTokens() + " tokens)";
// 不加守卫的Success兜底(其实上面两个已经穷举了,但加上更安全)
case LLMResult.Success s ->
"标准响应";
// Token超限,分输入侧和输出侧
case LLMResult.TokenLimitExceeded t when "input".equals(t.side()) ->
"输入过长,请缩短问题";
case LLMResult.TokenLimitExceeded t when "output".equals(t.side()) ->
"回答被截断,请分段获取";
case LLMResult.TokenLimitExceeded t ->
"Token超限(" + t.side() + ")";
// 超时,分快超时和慢超时
case LLMResult.Timeout t when t.waitedMs() > 10000 ->
"严重超时(超过10秒)";
case LLMResult.Timeout t ->
"一般超时(" + t.waitedMs() + "ms)";
// 其余情况
case LLMResult.ContentFiltered f -> "内容违规";
case LLMResult.ParseFailure p -> "解析失败";
};
}这段代码有个细节值得注意:switch的case是按顺序匹配的,更具体的条件要放在前面。比如我先放了 tokens < 100 的守卫,再放 tokens >= 100 的守卫,最后才是不带守卫的兜底。如果顺序反了,不带守卫的会优先匹配,带守卫的永远走不到。
编译器会对不可达的case发出警告,但不一定是错误,所以这里需要自己注意顺序。
五、解构模式(Deconstruction Patterns)
这是Java 21引入的预览特性,Java 23开始进入正式版。它允许在模式匹配里直接解构Record的字段:
// 假设Java版本支持解构模式
public void processWithDeconstruction(LLMResult result) {
switch (result) {
// 直接解构Success的字段
case LLMResult.Success(String content, int promptTokens, int completionTokens) -> {
System.out.println("内容: " + content);
System.out.println("总tokens: " + (promptTokens + completionTokens));
}
// 解构并添加守卫
case LLMResult.TokenLimitExceeded(int requested, int max, String side)
when "input".equals(side) -> {
System.out.println("输入超出" + (requested - max) + "个token");
}
default -> System.out.println("其他情况");
}
}这个特性目前(Java 21)还是预览,用在生产代码里需要谨慎。但了解它的方向是有价值的——Java正在朝着函数式语言的模式匹配方向靠拢。
六、实战案例:AI聊天消息路由器
来看一个比较完整的实际案例:在一个多功能AI助手里,根据用户消息类型做路由。
// 用户消息的密封类体系
public sealed interface UserMessage
permits UserMessage.TextMessage, UserMessage.ImageMessage,
UserMessage.AudioMessage, UserMessage.FileMessage,
UserMessage.SystemCommand {
record TextMessage(String text, String userId, Instant sentAt)
implements UserMessage {}
record ImageMessage(String imageUrl, String caption, String userId, Instant sentAt)
implements UserMessage {}
record AudioMessage(byte[] audioData, String format, int durationSeconds, String userId, Instant sentAt)
implements UserMessage {}
record FileMessage(String fileUrl, String fileName, long fileSizeBytes, String mimeType, String userId, Instant sentAt)
implements UserMessage {}
// 系统命令:清空历史、切换模型等
record SystemCommand(String command, Map<String, String> params, String userId)
implements UserMessage {}
}
// AI响应路由器
@Service
public class AIMessageRouter {
private final TextChatService textChatService;
private final VisionService visionService;
private final AudioTranscriptionService audioService;
private final DocumentAnalysisService documentService;
private final SessionManagementService sessionService;
public Mono<String> route(UserMessage message) {
return switch (message) {
// 纯文字消息:直接发给Chat API
case UserMessage.TextMessage text when !text.text().isBlank() ->
textChatService.chat(text.userId(), text.text());
// 空文本:给个提示
case UserMessage.TextMessage text ->
Mono.just("您发送了空消息,请输入您的问题");
// 图片消息:走Vision API
case UserMessage.ImageMessage img when img.caption() != null ->
visionService.analyzeWithCaption(img.imageUrl(), img.caption(), img.userId());
case UserMessage.ImageMessage img ->
visionService.analyze(img.imageUrl(), img.userId());
// 音频消息:先转文字再Chat
case UserMessage.AudioMessage audio when audio.durationSeconds() <= 300 ->
audioService.transcribe(audio.audioData(), audio.format())
.flatMap(text -> textChatService.chat(audio.userId(), text));
// 音频太长(超过5分钟):拒绝处理
case UserMessage.AudioMessage audio ->
Mono.just("音频时长超过5分钟,暂不支持处理");
// 文件消息:按MIME类型分别处理
case UserMessage.FileMessage file when file.mimeType().startsWith("application/pdf") ->
documentService.analyzePdf(file.fileUrl(), file.userId());
case UserMessage.FileMessage file when file.mimeType().startsWith("text/") ->
documentService.analyzeText(file.fileUrl(), file.userId());
case UserMessage.FileMessage file when file.fileSizeBytes() > 10 * 1024 * 1024 ->
Mono.just("文件大小超过10MB限制");
case UserMessage.FileMessage file ->
Mono.just("不支持的文件类型: " + file.mimeType());
// 系统命令
case UserMessage.SystemCommand cmd when "CLEAR_HISTORY".equals(cmd.command()) ->
sessionService.clearHistory(cmd.userId())
.thenReturn("已清空对话历史");
case UserMessage.SystemCommand cmd when "SWITCH_MODEL".equals(cmd.command()) ->
sessionService.switchModel(
cmd.userId(),
cmd.params().getOrDefault("model", "gpt-4o")
).thenReturn("已切换到模型: " + cmd.params().get("model"));
case UserMessage.SystemCommand cmd ->
Mono.just("未知命令: " + cmd.command());
};
}
}这段代码的可读性我觉得非常好。每个case清楚地说明了"什么情况下做什么",逻辑层次一目了然。如果后来新加了 VideoMessage,编译器会在这里提醒你处理它。
七、处理null:密封类+模式匹配的注意事项
模式匹配switch对null有特殊处理。在Java 21里,如果switch的输入是null,而你没有显式的 case null,会抛出 NullPointerException。这是个容易忽略的坑:
// 危险写法:如果result为null会NPE
public String process(LLMResult result) {
return switch (result) {
case LLMResult.Success s -> s.content();
case LLMResult.ContentFiltered f -> "被过滤";
// ... 其他case
};
}
// 安全写法1:加null case
public String process(LLMResult result) {
return switch (result) {
case null -> "结果为null,请检查服务调用";
case LLMResult.Success s -> s.content();
// ...
};
}
// 安全写法2:进入switch前先检查
public String process(LLMResult result) {
Objects.requireNonNull(result, "LLMResult不能为null");
return switch (result) {
case LLMResult.Success s -> s.content();
// ...
};
}在AI服务里,我更倾向于写法2:在服务边界明确要求非null,而不是在每个处理器里都处理null。用Optional也是个好选择:
public Optional<String> processOptional(LLMResult result) {
if (result == null) return Optional.empty();
return Optional.of(switch (result) {
case LLMResult.Success s -> s.content();
case LLMResult.ContentFiltered f -> "被过滤: " + f.reason();
// ...
});
}八、模式匹配与流水线组合
模式匹配可以和Stream API组合,处理批量AI结果特别方便:
@Service
public class BatchResultProcessor {
public BatchSummary processBatch(List<LLMResult> results) {
// 分组统计
var grouped = results.stream()
.collect(Collectors.groupingBy(result -> switch (result) {
case LLMResult.Success ignored -> "SUCCESS";
case LLMResult.ContentFiltered ignored -> "FILTERED";
case LLMResult.TokenLimitExceeded ignored -> "TOKEN_LIMIT";
case LLMResult.Timeout ignored -> "TIMEOUT";
case LLMResult.ParseFailure ignored -> "PARSE_FAILURE";
}));
// 提取所有成功结果的内容
List<String> successContents = results.stream()
.filter(r -> r instanceof LLMResult.Success)
.map(r -> ((LLMResult.Success) r).content())
.toList();
// 更优雅的写法(Java 21 instanceOf + filter没有直接的mapToType,用flatMap)
List<String> successContentsElegant = results.stream()
.flatMap(r -> r instanceof LLMResult.Success s
? Stream.of(s.content())
: Stream.empty())
.toList();
// 计算总token消耗
int totalTokens = results.stream()
.mapToInt(r -> switch (r) {
case LLMResult.Success s -> s.promptTokens() + s.completionTokens();
default -> 0;
})
.sum();
return new BatchSummary(
results.size(),
grouped.getOrDefault("SUCCESS", List.of()).size(),
grouped.getOrDefault("FILTERED", List.of()).size(),
grouped.getOrDefault("TIMEOUT", List.of()).size(),
totalTokens,
successContentsElegant
);
}
public record BatchSummary(
int total,
int successCount,
int filteredCount,
int timeoutCount,
int totalTokens,
List<String> successContents
) {
public double successRate() {
return total == 0 ? 0.0 : (double) successCount / total;
}
}
}九、完整的提示词路由系统
把这章所有内容整合成一个完整的例子:一个根据用户意图路由到不同AI策略的系统。
// 用户意图(密封类)
public sealed interface UserIntent
permits UserIntent.Question, UserIntent.CodeRequest,
UserIntent.CreativeTask, UserIntent.DataAnalysis,
UserIntent.Unknown {
record Question(String topic, boolean needsCitation, String questionText)
implements UserIntent {}
record CodeRequest(String language, TaskType taskType, String description)
implements UserIntent {
public enum TaskType { GENERATE, REVIEW, DEBUG, EXPLAIN }
}
record CreativeTask(ContentType contentType, String brief, String style)
implements UserIntent {
public enum ContentType { ARTICLE, POEM, EMAIL, STORY }
}
record DataAnalysis(String dataDescription, AnalysisType analysisType)
implements UserIntent {
public enum AnalysisType { SUMMARY, COMPARISON, TREND, ANOMALY }
}
record Unknown(String rawInput) implements UserIntent {}
}
// AI策略路由器
@Service
public class AIStrategyRouter {
private final ChatClient chatClient;
private final IntentClassifier intentClassifier;
public String route(String userInput) {
UserIntent intent = intentClassifier.classify(userInput);
// 根据意图选择不同的策略
AIRequestConfig config = switch (intent) {
// 知识型问答:低temperature,更精确
case UserIntent.Question q when q.needsCitation() ->
AIRequestConfig.defaults()
.withTemperature(0.1)
.withModel("gpt-4o");
case UserIntent.Question q ->
AIRequestConfig.defaults().withTemperature(0.3);
// 代码生成:极低temperature,确定性更高
case UserIntent.CodeRequest code when
code.taskType() == UserIntent.CodeRequest.TaskType.GENERATE ->
AIRequestConfig.forCodeGeneration().withTemperature(0.1);
// 代码审查:稍高一点,允许一些创意建议
case UserIntent.CodeRequest code ->
AIRequestConfig.forCodeGeneration().withTemperature(0.3);
// 创意任务:高temperature
case UserIntent.CreativeTask creative ->
AIRequestConfig.defaults().withTemperature(1.0);
// 数据分析:低temperature,需要准确
case UserIntent.DataAnalysis analysis ->
AIRequestConfig.defaults().withTemperature(0.1);
// 未知意图:使用默认配置
case UserIntent.Unknown ignored ->
AIRequestConfig.defaults();
};
// 根据意图构建系统提示词
String systemPrompt = switch (intent) {
case UserIntent.Question q ->
q.needsCitation()
? "你是一个知识专家,回答问题时请引用来源,标注[来源]"
: "你是一个知识渊博的助手,给出准确的回答";
case UserIntent.CodeRequest code ->
"你是一位专业的" + code.language() + "工程师," +
switch (code.taskType()) {
case GENERATE -> "请生成高质量、有注释的代码";
case REVIEW -> "请审查代码并指出潜在问题";
case DEBUG -> "请帮助定位和修复bug";
case EXPLAIN -> "请详细解释代码的逻辑";
};
case UserIntent.CreativeTask creative ->
switch (creative.contentType()) {
case ARTICLE -> "你是一位优秀的文章写作者," + creative.style();
case POEM -> "你是一位有才华的诗人," + creative.style();
case EMAIL -> "你是一位商务写作专家," + creative.style();
case STORY -> "你是一位优秀的故事创作者," + creative.style();
};
case UserIntent.DataAnalysis analysis ->
"你是一位数据分析专家,专注于" +
switch (analysis.analysisType()) {
case SUMMARY -> "数据汇总和关键指标提取";
case COMPARISON -> "多维度对比分析";
case TREND -> "趋势识别和预测";
case ANOMALY -> "异常检测和根因分析";
};
case UserIntent.Unknown ignored ->
"你是一个全能的AI助手,请理解用户的需求并给予帮助";
};
// 调用AI
return chatClient.prompt()
.system(systemPrompt)
.user(userInput)
.options(ChatOptionsBuilder.builder()
.withModel(config.model())
.withTemperature(config.temperature())
.withMaxTokens(config.maxTokens())
.build())
.call()
.content();
}
}十、性能小贴士
有人担心模式匹配switch的性能。我的实测经验是:和if-else链差不多,不需要特别担心。
JVM对switch有专门的优化,但模式匹配switch本质上还是条件判断,对于AI服务这种IO密集型应用,这点CPU差距完全不在关键路径上。一次AI API调用的延迟是秒级的,switch匹配是纳秒级的,数量级完全不同。
真正需要注意的是:不要在热路径上做大量的类型判断。比如流式输出时,每个token都做一次复杂的switch,这个累积下来可能有影响。这种场景下,考虑把类型判断提前,只做一次。
小结
模式匹配在AI结果处理中的价值总结:
instanceof模式匹配消除了"先判断再转型"的冗余,代码更简洁- switch 模式匹配配合密封类,实现穷举检查,避免漏处理情况
when守卫允许在类型匹配的基础上加条件,表达力大幅提升- 配合Stream API处理批量结果,代码简洁且高效
- 实战中要注意null处理、case顺序、守卫条件的覆盖性
这三篇——Records、Sealed Classes、Pattern Matching——合起来是Java现代化特性在AI工程里的完整组合拳。接下来我们转到函数式编程,聊聊如何用Function Composition构建Prompt处理流水线。
