第2348篇:Java AI的Web层设计——RESTful API与AI功能的最佳实践
2026/4/30大约 4 分钟
第2348篇:Java AI的Web层设计——RESTful API与AI功能的最佳实践
适读人群:负责AI服务API设计的Java工程师,希望建立规范化AI接口设计的团队 | 阅读时长:约15分钟 | 核心价值:掌握AI功能的Web层设计规范,包括流式接口、异步接口和结果查询接口的标准化实践
AI功能的Web层设计有几个传统API没有的挑战:
流式响应:LLM的token流式输出,需要Server-Sent Events或WebSocket。
长时间处理:AI任务可能需要几十秒,直接用同步HTTP会超时,需要异步+轮询或Webhook回调模式。
不确定性的响应:AI可能拒绝回答、输出不符合预期格式、或者输出被截断,接口设计要处理这些情况。
接口类型分类
标准化的请求/响应结构
// 统一的AI请求基类
public record AiRequest(
String conversationId, // 对话ID(多轮对话时保持一致)
String message, // 用户输入
AiRequestOptions options // 可选参数
) {
public record AiRequestOptions(
String language, // 期望的回答语言(可选)
Boolean stream, // 是否流式输出(可选)
Integer maxTokens, // 限制输出长度(可选)
String model // 指定模型(可选,高级用户)
) {}
// 便捷构造
public static AiRequest simple(String message) {
return new AiRequest(null, message, null);
}
}
// 统一的AI响应结构
@JsonInclude(JsonInclude.Include.NON_NULL)
public record AiResponse(
String conversationId,
String messageId, // 本条消息的唯一ID
String reply, // AI回答内容
AiResponseMeta meta // 元数据
) {
public record AiResponseMeta(
String modelVersion, // 使用的模型版本
int promptTokens, // Prompt token数
int completionTokens, // 输出token数
long processingTimeMs, // 处理耗时
boolean truncated, // 是否被截断(达到maxTokens)
String finishReason // 结束原因:stop/length/content_filter
) {}
// 创建简单响应(不含meta)
public static AiResponse simple(String conversationId, String reply) {
return new AiResponse(conversationId, UUID.randomUUID().toString(), reply, null);
}
}
// 统一的错误响应
public record AiErrorResponse(
String code,
String message,
String conversationId, // 保留对话ID,方便客户端处理
boolean retryable, // 客户端是否可以重试
Integer retryAfterSeconds // 如果可以重试,多久后重试
) {}同步对话接口
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Slf4j
public class ChatController {
private final ChatService chatService;
/**
* 同步对话接口
* 适合简单问答,响应时间<5秒的场景
*/
@PostMapping("/chat")
@RateLimiter(name = "chat-api")
public ResponseEntity<AiResponse> chat(
@RequestBody @Valid AiRequest request,
@AuthenticationPrincipal UserDetails user) {
// 生成或使用现有会话ID
String conversationId = request.conversationId() != null
? request.conversationId()
: UUID.randomUUID().toString();
long start = System.currentTimeMillis();
ChatService.ChatResult result = chatService.chat(
user.getUsername(), conversationId, request.message());
long duration = System.currentTimeMillis() - start;
AiResponse response = new AiResponse(
conversationId,
UUID.randomUUID().toString(),
result.reply(),
new AiResponse.AiResponseMeta(
result.modelVersion(),
result.promptTokens(),
result.completionTokens(),
duration,
result.truncated(),
result.finishReason()
)
);
return ResponseEntity.ok()
.header("X-Conversation-Id", conversationId)
.header("X-Processing-Time-Ms", String.valueOf(duration))
.body(response);
}
}流式接口(SSE)
/**
* 流式对话接口(Server-Sent Events)
* 适合需要实时展示输出的场景
*
* 前端使用:const es = new EventSource('/api/v1/chat/stream?message=...')
*/
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<?>> chatStream(
@RequestParam String message,
@RequestParam(required = false) String conversationId,
@RequestParam(required = false, defaultValue = "true") boolean includeStats,
@AuthenticationPrincipal UserDetails user) {
final String finalConversationId = conversationId != null
? conversationId
: UUID.randomUUID().toString();
String userId = user.getUsername();
long[] startTime = {System.currentTimeMillis()};
int[] tokenCount = {0};
Flux<ServerSentEvent<?>> tokenStream = chatService.chatStream(userId, finalConversationId, message)
.map(token -> {
tokenCount[0] += token.length() / 4; // 粗略估算token数
return ServerSentEvent.builder()
.event("token")
.id(UUID.randomUUID().toString())
.data(token)
.build();
});
// 完成事件(带统计信息)
Flux<ServerSentEvent<?>> completionEvent = Mono.fromCallable(() -> {
long duration = System.currentTimeMillis() - startTime[0];
if (includeStats) {
Map<String, Object> stats = Map.of(
"conversationId", finalConversationId,
"processingTimeMs", duration,
"estimatedTokens", tokenCount[0]
);
return (ServerSentEvent<?>) ServerSentEvent.builder()
.event("complete")
.data(stats)
.build();
} else {
return (ServerSentEvent<?>) ServerSentEvent.builder()
.event("done")
.data("")
.build();
}
}).flux();
return tokenStream
.concatWith(completionEvent)
.onErrorResume(e -> {
log.error("流式输出错误:userId={}", userId, e);
return Flux.just(ServerSentEvent.builder()
.event("error")
.data(Map.of("message", "生成失败,请重试"))
.build());
});
}异步任务接口
/**
* 异步AI任务提交
* 适合复杂分析、批量处理等>30秒的任务
*/
@PostMapping("/tasks")
public ResponseEntity<TaskSubmitResponse> submitTask(
@RequestBody @Valid TaskSubmitRequest request,
@AuthenticationPrincipal UserDetails user) {
String taskId = aiTaskService.submitTask(
user.getUsername(),
request.taskType(),
request.content(),
request.options()
);
return ResponseEntity.accepted() // 202 Accepted
.header("Location", "/api/v1/tasks/" + taskId)
.body(new TaskSubmitResponse(taskId, "PENDING",
"任务已提交,请通过GET /api/v1/tasks/" + taskId + " 查询结果"));
}
/**
* 查询任务状态和结果
*/
@GetMapping("/tasks/{taskId}")
public ResponseEntity<?> getTaskResult(
@PathVariable String taskId,
@AuthenticationPrincipal UserDetails user) {
AiTaskResult result = aiTaskService.getResult(user.getUsername(), taskId);
return switch (result.status()) {
case PENDING, PROCESSING -> ResponseEntity.ok()
.header("Retry-After", "5") // 建议5秒后重试
.body(Map.of(
"taskId", taskId,
"status", result.status(),
"progress", result.progressPercentage()
));
case COMPLETED -> ResponseEntity.ok(Map.of(
"taskId", taskId,
"status", "COMPLETED",
"result", result.output(),
"processingTimeMs", result.processingTimeMs()
));
case FAILED -> ResponseEntity.ok(Map.of(
"taskId", taskId,
"status", "FAILED",
"error", result.errorMessage(),
"retryable", result.retryable()
));
};
}
public record TaskSubmitRequest(
String taskType,
String content,
Map<String, String> options) {}
public record TaskSubmitResponse(String taskId, String status, String message) {}OpenAPI文档注解
AI接口的文档比普通接口更重要——调用方需要知道延迟预期、Token限制、错误码含义:
@Operation(
summary = "AI对话(同步)",
description = """
发送消息并同步等待AI回复。
**适用场景**:简单问答,预期响应时间<10秒
**不适用场景**:复杂分析、文档处理等长时间任务,请使用 POST /tasks
**限流**:每个用户每分钟最多60次请求
""",
responses = {
@ApiResponse(responseCode = "200", description = "成功"),
@ApiResponse(responseCode = "400", description = "请求参数错误或内容不合规"),
@ApiResponse(responseCode = "429", description = "请求频率超限,响应头Retry-After说明等待时间"),
@ApiResponse(responseCode = "503", description = "AI服务暂时不可用")
}
)
@PostMapping("/chat")
public ResponseEntity<AiResponse> chat(/* ... */) {
// 实现
}良好的Web层设计是AI服务可用性的第一道屏障。接口设计清晰、错误码规范、文档完整,能大幅减少集成方的困惑和支持成本。
