1)先立规矩:你要的不是"像 JSON",而是"只输出 JSON"
最容易失败的提示词长这样:
请用 JSON 输出,并解释一下原因
这会让模型同时做两件事:结构化 + 自由文本。线上一定会有人撞坑。
建议你把底线写死(短、硬、可检查):
- 只允许输出一个 JSON 对象(或数组)
- 不允许 Markdown 代码块
- 不允许额外文本
- 信息不足时输出
{ "error": "...", "need": [...] }
2)提示词模板:schema + 规则 + 唯一出口
2.1 最小模板(低风险任务)
text
你必须只输出一个 JSON 对象,不要输出任何额外文本,也不要使用 Markdown。
信息不足的字段写 null。
schema:
{
"intent": "string",
"priority": "low|medium|high",
"summary": "string",
"tags": ["string"]
}
2.2 更稳模板(推荐):加规则 + 唯一出口
text
你必须只输出 JSON(不要 Markdown,不要解释)。
必须满足:
1) 必须包含字段:intent, priority, summary, tags
2) priority 只能是 low/medium/high
3) tags 必须是数组,最多 5 个
4) summary <= 60 字
如果无法满足规则,请只输出:
{ "error": "原因", "need": ["缺少的信息"] }
你会发现:"唯一出口"非常重要。它能把模型想说的废话收敛到结构里。
3)修复回路:解析失败别硬扛,给自己一个稳定的 fallback
生产里我一般按三层兜底:
- 直接解析(成功就返回)
- 轻量净化 (截取第一个
{到最后一个}) - 二次修复(把失败样本喂回模型,让它只输出最终 JSON)
如果你没有第 3 步,线上总会出现"某些输入必炸"的角落案例。
4)Kotlin 实战:kotlinx.serialization + 规则校验 + 净化截取
4.1 Gradle 依赖
kotlin
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
4.2 数据模型(包含 error/need 兜底字段)
kotlin
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@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
}
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
explicitNulls = false
}
4.3 净化:截取 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()
}
4.4 解析 + 规则校验
kotlin
data class Validation(val ok: Boolean, val message: String)
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")
}
fun parseTicket(raw: String): Ticket? =
runCatching { json.decodeFromString(Ticket.serializer(), raw.trim()) }.getOrNull()
fun parseWithFallback(raw: String): Ticket? {
parseTicket(raw)?.let { return it }
val sanitized = sanitizeToJsonObject(raw)
return parseTicket(sanitized)
}
你现在已经能处理 80% 的"夹文本/代码块"问题。剩下那 20%,需要二次修复回路。
5)二次修复回路(推荐):把失败样本再喂回 Claude
思路很简单:当 parseWithFallback() 仍失败时,触发一次修复调用:
修复提示词(示例):
text
下面是一段"可能包含多余文字"的输出。请你只输出一个合法 JSON,并且必须符合 schema:
{ ... }
禁止 Markdown,禁止解释。
原始输出:
<<<
{原文粘贴}
>>>
这一轮通常用 Sonnet 就够了,不要用 Opus 硬抬成本。
6)把 schema/校验/修复策略集中管理(147api)
结构化输出最怕的是:每个服务都写一套 schema,每次改字段都要全量发版。
更合理的做法是把这些能力集中到统一接入层:
- schema 版本化(v1/v2)
- 校验与修复回路统一实现
- 按项目/用户做限流与配额,避免修复回路把 429 撞爆
如果你不想自建网关,可以了解下 147api:按其介绍,它提供 OpenAI 风格的聚合管理与 key 管理能力。把接入层工程收口后,业务侧就只需要调用一个稳定接口。
一页清单
- 提示词:只输出 JSON,禁止 Markdown/解释
- schema:字段/类型/枚举写清楚
- 规则:写成可检查项(长度、必填、数组上限)
- 唯一出口:
{error, need}兜底 - 解析:反序列化失败要有 fallback(净化/修复回路)
- 观测:记录失败样本,反推 schema 与规则