在上一篇文章《Java AI 实战:本地模型 JSON 结构化输出》中,我们介绍了如何使用 Ollama 生成 JSON 格式的结构化输出。然而,在实际应用中,确保 AI 生成的输出符合预期的数据结构是非常重要的。通过使用 JSON Schema,我们可以定义期望的输出格式,并验证 AI 生成的内容是否符合要求。本文将介绍如何在 Spring Boot 应用程序中实现这一功能,帮助你更好地处理和验证 AI 的结构化输出。

项目依赖

首先,我们需要添加 JSON Schema 验证器的依赖。在pom.xml中添加:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>json-schema-validator</artifactId>
    <version>1.4.0</version>
</dependency>

networknt/json-schema-validator 是一个轻量级、高性能的 JSON Schema 验证器,专门为 Java 应用程序设计 这个依赖对于我们验证 AI 的输出特别有用,因为它:

  • 支持复杂的嵌套 JSON 结构验证
  • 提供清晰的错误信息,便于调试
  • 性能优秀,适合处理大量的验证请求
  • 可以缓存 Schema 定义,提高验证效率

AI 结构化输出

AI 提供了 Function Calling 功能,允许我们定义期望的输出格式。通过指定 JSON Schema,我们可以让模型生成符合特定结构的输出。

定义输出 Schema

以下是一个用于生成产品评论的 Schema 示例:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "review": {
      "type": "object",
      "properties": {
        "rating": {
          "type": "integer",
          "minimum": 1,
          "maximum": 5,
          "description": "产品评分(1-5星)"
        },
        "summary": {
          "type": "string",
          "maxLength": 100,
          "description": "评论摘要"
        },
        ...
        "sentiment": {
          "type": "string",
          "enum": ["positive", "neutral", "negative"],
          "description": "整体情感倾向"
        }
      },
      "required": ["rating", "summary", "sentiment"]
    }
  }
}

实现 AI 服务

创建一个服务类来处理 OpenAI API 调用和输出验证:

@Service
public class AIReviewService {
    private final OpenAiClient openAiClient;
    private final JsonSchemaValidator schemaValidator;

    @Value("classpath:schemas/review-output-schema.json")
    private Resource schemaResource;

    public ReviewResponse generateStructuredReview(String productDescription) {
        var request = ChatCompletionRequest.builder()
            .model("gpt-4")
            .messages(List.of(
                new Message("system", "你是一个专业的产品评论分析师。请根据用户提供的产品描述生成结构化的评论。"),
                new Message("user", productDescription)
            ))
            .functions(List.of(
                new FunctionDefinition(
                    "generate_review",
                    "生成产品评论",
                    schemaResource // JSON Schema作为函数参数定义
                )
            ))
            .functionCall("generate_review") // 强制使用指定的函数
            .build();

        return openAiClient.createChatCompletion(request)
            .map(this::validateAndParseResponse)
            .orElseThrow(() -> new AIGenerationException("Failed to generate review"));
    }

    private ReviewResponse validateAndParseResponse(String jsonResponse) {
        // 验证生成的JSON是否符合Schema
        if (!schemaValidator.isValid(jsonResponse)) {
            throw new InvalidOutputException("AI generated invalid review format");
        }

        // 解析验证通过的JSON
        return objectMapper.readValue(jsonResponse, ReviewResponse.class);
    }
}

输出验证器

实现一个专门的验证器来处理 AI 生成的输出:

@Component
public class AIOutputValidator {
    private final Map<String, JsonSchema> schemaCache = new ConcurrentHashMap<>();
    private final ObjectMapper objectMapper;

    public AIOutputValidator(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    public ValidationResult validateOutput(String output, String schemaPath) {
        JsonSchema schema = getOrLoadSchema(schemaPath);
        try {
            JsonNode outputNode = objectMapper.readTree(output);
            Set<ValidationMessage> errors = schema.validate(outputNode);

            if (errors.isEmpty()) {
                return ValidationResult.success();
            }

            return ValidationResult.failure(errors.stream()
                .map(ValidationMessage::getMessage)
                .collect(Collectors.toList()));

        } catch (Exception e) {
            return ValidationResult.failure(List.of("Invalid JSON format: " + e.getMessage()));
        }
    }

    private JsonSchema getOrLoadSchema(String schemaPath) {
        return schemaCache.computeIfAbsent(schemaPath, this::loadSchema);
    }

    private JsonSchema loadSchema(String schemaPath) {
        try {
            Resource resource = new ClassPathResource(schemaPath);
            JsonNode schemaNode = objectMapper.readTree(resource.getInputStream());
            return JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)
                .getSchema(schemaNode);
        } catch (Exception e) {
            throw new SchemaLoadException("Failed to load schema: " + schemaPath, e);
        }
    }
}

错误处理

添加专门的异常处理来处理 AI 输出验证错误:

@ControllerAdvice
public class AIExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(InvalidOutputException.class)
    public ResponseEntity<ErrorResponse> handleInvalidOutput(InvalidOutputException ex) {
        ErrorResponse error = new ErrorResponse(
            "AI_OUTPUT_VALIDATION_ERROR",
            "AI生成的输出格式无效",
            ex.getValidationErrors()
        );
        return ResponseEntity.badRequest().body(error);
    }

    @ExceptionHandler(AIGenerationException.class)
    public ResponseEntity<ErrorResponse> handleAIGeneration(AIGenerationException ex) {
        ErrorResponse error = new ErrorResponse(
            "AI_GENERATION_ERROR",
            "AI生成内容失败",
            Collections.singletonList(ex.getMessage())
        );
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(error);
    }
}

总结

通过使用 JSON Schema 验证 AI 的输出,我们可以确保 AI 生成的内容符合预期的格式和质量要求。这不仅提高了 AI 推理的结果质量和应用的可靠性,还简化了后续的数据处理流程。以下是一些关键的实践建议:

  1. 在 Prompt 中包含 JSON Schema

    • 在提示词中直接包含 JSON Schema 的定义,可以显著提升模型输出符合预期格式的概率
    • 建议使用简化版的 Schema 描述,避免过于复杂的验证规则影响模型理解
  2. 结构化的提示模板

    请按照以下 JSON Schema 格式生成输出:
    {
      "type": "object",
      "properties": {
        "field1": {"type": "string"},
        "field2": {"type": "number"}
      },
      "required": ["field1", "field2"]
    }
    
  3. 输出质量优化

    • 通过示例展示期望的输出格式
    • 明确指出必填字段和数据类型要求
    • 在提示词中强调需要严格遵循 JSON 格式规范

这种方法不仅能提高首次生成的准确率,还能减少重试和错误处理的成本,是构建稳定可靠的 AI 应用的重要实践。

参考资料