1. 为什么需要严格控制AI的JSON输出格式
在开发AI应用时,我们经常需要模型输出结构化的JSON数据。但实际使用中,模型可能会输出包含解释文本、Markdown代码块等非纯JSON内容,导致下游系统解析失败。这种情况在线上服务中尤为致命。
我曾在电商客服系统中遇到过这样的问题:当用户询问"帮我查订单状态"时,理想输出应该是{"intent":"check_order","order_id":"12345"},但模型有时会输出:
code复制好的,这是您要的订单信息:
```json
{
"intent": "check_order",
"order_id": "12345"
}
这种"解释+Markdown"的输出格式会让JSON解析器直接崩溃。更糟的是,这种问题往往在测试阶段不易发现,直到上线后才在特定场景下暴露。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 设计可靠的JSON输出提示词
2.1 基础提示词模板
最简单的提示词只需要明确要求纯JSON输出:
code复制你必须只输出一个JSON对象,不要输出任何额外文本,也不要使用Markdown。
信息不足的字段写null。
但这样的提示词在实际应用中仍然不够可靠。模型可能会因为各种原因(如输入不完整、指令理解偏差)输出非JSON内容。
2.2 增强版提示词模板
更可靠的提示词应该包含以下几个关键部分:
- 格式要求:明确禁止Markdown和解释文本
- Schema定义:详细说明每个字段的类型和格式
- 校验规则:定义字段必填、枚举值、长度限制等
- 错误兜底:指定当输入不满足要求时的统一错误格式
示例:
code复制你必须只输出JSON(不要Markdown,不要解释)。
必须满足:
1) 必须包含字段:intent, priority, summary, tags
2) priority只能是low/medium/high
3) tags必须是数组,最多5个
4) summary <= 60字
如果无法满足规则,请只输出:
{ "error": "原因", "need": ["缺少的信息"] }
这种提示词通过"唯一出口"设计,将模型可能输出的各种非结构化内容收敛到一个统一的错误结构中。
3. 实现多层解析兜底策略
即使有严格的提示词,线上环境仍可能出现非标准JSON输出。我们需要在代码层面实现多层解析策略。
3.1 直接解析
首先尝试直接解析原始输出:
kotlin复制fun parseTicket(raw: String): Ticket? =
runCatching { json.decodeFromString(Ticket.serializer(), raw.trim()) }.getOrNull()
3.2 文本净化
如果直接解析失败,尝试从文本中提取可能的JSON部分:
kotlin复制fun sanitizeToJsonObject(raw: String): String {
val start = raw.indexOf('{')
val end = raw.lastIndexOf('}')
if (start < 0 || end <= start) return raw.trim()
return raw.substring(start, end + 1).trim()
}
这个方法会截取第一个{到最后一个}之间的内容,去除可能存在的解释文本。
3.3 二次修复回路
当上述方法都失败时,可以将原始输出再次喂给模型,要求它只输出纯JSON:
code复制下面是一段"可能包含多余文字"的输出。请你只输出一个合法JSON,并且必须符合schema:
{ ... }
禁止Markdown,禁止解释。
原始输出:
<<<
{原文粘贴}
>>>
这一层修复通常能解决90%以上的解析失败问题。需要注意的是,修复调用应该使用轻量级模型(如Claude Sonnet)以控制成本。
4. Kotlin实现细节
4.1 数据模型定义
使用kotlinx.serialization定义数据类:
kotlin复制@Serializable
data class Ticket(
val intent: String? = null,
val priority: Priority? = null,
val summary: String? = null,
val tags: List<String> = emptyList(),
val error: String? = null,
val need: List<String> = emptyList()
)
@Serializable
enum class Priority {
@SerialName("low") LOW,
@SerialName("medium") MEDIUM,
@SerialName("high") HIGH
}
4.2 JSON解析配置
配置解析器以容忍一些常见问题:
kotlin复制private val json = Json {
ignoreUnknownKeys = true // 忽略未定义的字段
isLenient = true // 允许非标准JSON格式
explicitNulls = false // 不序列化null值
}
4.3 校验逻辑实现
实现业务规则的校验:
kotlin复制fun validate(t: Ticket): Validation {
if (t.error != null) return Validation(true, "error-mode")
if (t.intent.isNullOrBlank()) return Validation(false, "intent missing")
if (t.priority == null) return Validation(false, "priority missing")
if (t.summary.isNullOrBlank()) return Validation(false, "summary missing")
if (t.tags.size > 5) return Validation(false, "too many tags")
if (t.summary.length > 60) return Validation(false, "summary too long")
return Validation(true, "ok")
}
5. 系统架构建议
5.1 集中管理Schema
将所有的输出Schema集中管理,避免每个服务单独实现:
- 使用版本控制(v1, v2等)
- 提供Schema注册和发现机制
- 实现自动化的Schema迁移工具
5.2 统一校验与修复
在API网关层实现统一的校验和修复逻辑:
- 拦截所有AI模型的输出
- 应用统一的解析和校验规则
- 自动触发修复回路当解析失败时
- 记录失败案例用于后续分析
5.3 监控与告警
建立完善的监控体系:
- 记录解析成功率、修复率等关键指标
- 设置合理的告警阈值
- 收集失败样本用于提示词优化
6. 实战经验与避坑指南
6.1 常见问题与解决方案
-
模型输出Markdown代码块
- 解决方案:在提示词中明确禁止Markdown
- 备选方案:实现代码块提取逻辑
-
模型添加解释文本
- 解决方案:使用"唯一出口"设计,强制错误情况也输出JSON
- 备选方案:文本净化提取JSON部分
-
字段缺失或格式不符
- 解决方案:在提示词中明确每个字段的要求
- 备选方案:实现严格的校验逻辑
6.2 性能优化技巧
-
修复回路优化
- 对简单问题使用轻量级模型
- 设置修复调用的超时和重试机制
- 实现本地缓存避免重复修复相同问题
-
解析性能
- 对高频Schema预编译解析器
- 实现流式解析处理大响应
- 使用更高效的JSON库如kotlinx.serialization
6.3 成本控制策略
-
修复回路成本
- 限制每个请求的最大修复次数
- 对不同重要度的请求使用不同规格的修复模型
- 实现配额和限流机制
-
监控成本
- 采样记录而非全量记录
- 实现智能过滤只记录异常案例
- 使用低成本存储方案
7. 高级应用场景
7.1 动态Schema支持
对于需要灵活Schema的场景,可以实现动态Schema:
- 在运行时提供Schema定义
- 动态生成提示词
- 实现通用的解析和校验逻辑
kotlin复制interface Schema {
fun validate(json: String): Boolean
fun parse(json: String): Any
}
class DynamicSchema(private val definition: String) : Schema {
// 实现基于定义的自校验和解析
}
7.2 多模型统一接口
当使用多个AI模型时,可以提供统一的输出接口:
- 定义通用的输出Schema
- 为每个模型实现适配器
- 统一错误处理和修复策略
kotlin复制interface AIModel {
fun generate(prompt: String): String
}
class UnifiedOutput(model: AIModel, private val schema: Schema) {
fun call(prompt: String): Result {
val raw = model.generate(prompt)
return schema.parse(raw)
}
}
7.3 自动化测试框架
构建专门的测试框架验证JSON输出的稳定性:
- 生成多样化的测试用例
- 自动化执行和验证
- 统计并报告稳定性指标
kotlin复制class StabilityTester(
private val model: AIModel,
private val schema: Schema
) {
fun runTests(cases: List<String>): TestReport {
// 执行测试并生成报告
}
}
在实际项目中,我发现最有效的做法是将这些策略组合使用。例如,我们可以在网关层实现统一的Schema管理和校验,同时在业务层根据具体需求实现特殊的处理逻辑。这种分层设计既保证了整体的一致性,又保留了局部的灵活性。
