结构化输出与角色提示
把 LLM 接入实际应用时,结构化输出是核心需求。本篇讲如何让模型稳定输出 JSON、XML 等结构化格式,以及"角色提示"如何配合使用。
1. 为什么需要结构化输出
LLM 默认输出自然语言,但应用程序需要结构化数据:
| 场景 | 需要的格式 |
|---|---|
| 写入数据库 | JSON / 字典 |
| 调用 API | JSON / XML |
| 渲染 UI | JSON |
| 数据分析 | CSV / 表格 |
| 配置生成 | YAML / TOML |
如果模型输出"有时是 JSON,有时是带前言的 JSON,有时还多了 markdown 围栏",应用就不稳定。让输出 100% 可解析,是工程化的基础。
2. JSON 输出的基本写法
请提取下面文本中的人名和年龄,按 JSON 输出。
格式:
{
"people": [
{"name": "string", "age": number}
]
}
要求:
- 仅输出 JSON,不要任何额外文字
- 没找到时输出 {"people": []}
- 年龄缺失用 null
文本:
"小明今年 25 岁,他妈妈年纪比较大但没说几岁,弟弟 12 岁。"
输出:
{
"people": [
{"name": "小明", "age": 25},
{"name": "小明的妈妈", "age": null},
{"name": "小明的弟弟", "age": 12}
]
}
3. JSON 输出的关键技巧
技巧 1:给 schema 不给例子
直接写字段定义比给完整例子更不容易让模型"复制例子":
输出 schema:
- title: string,文章标题
- tags: string[],至少 3 个,全小写
- summary: string,不超过 100 字
- score: number,1-10
技巧 2:明确"只输出 JSON"
模型常常会加前言("以下是结果:")或 Markdown 围栏(```json)。
技巧 3:处理缺失字段
避免模型"觉得没必要就不返回"。
技巧 4:给一个完整示例
少量 Few-shot 能稳住格式:
4. JSON Schema 严格模式
OpenAI、Anthropic 都支持强制 JSON 输出模式:
OpenAI Function Calling
response = openai.chat.completions.create(
model="gpt-4",
messages=[...],
response_format={"type": "json_schema", "json_schema": {
"name": "extract_people",
"strict": True,
"schema": {
"type": "object",
"properties": {
"people": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": ["number", "null"]}
},
"required": ["name", "age"]
}
}
}
}
}}
)
Anthropic Tool Use
response = anthropic.messages.create(
model="claude-opus-4-7",
tools=[{
"name": "extract_people",
"input_schema": {...}
}],
tool_choice={"type": "tool", "name": "extract_people"},
messages=[...]
)
强制模式 vs 提示词约束:
| 维度 | Schema 模式 | 提示词约束 |
|---|---|---|
| 稳定性 | 100% 合法 JSON | 95-99% |
| 灵活性 | 字段固定 | 可临时调整 |
| 复杂度 | 需要写 schema | 写文字即可 |
| 适用场景 | 生产代码 | 原型、Notebook |
生产环境务必用 Schema 模式。
5. XML 输出(Claude 推荐)
Claude 对 XML 标签的处理特别稳定,尤其在嵌套场景:
请按以下 XML 输出:
<analysis>
<summary>简要总结</summary>
<issues>
<issue severity="high|medium|low">
<line>行号</line>
<description>问题描述</description>
<fix>修复建议</fix>
</issue>
...
</issues>
</analysis>
代码:
{code}
XML 在以下场景比 JSON 更好: - 内容包含大段自由文字(JSON 转义麻烦) - 嵌套层级深 - 给 Claude 用
6. 表格输出
适合给人看的、列数固定的数据:
按 markdown 表格输出,列固定:
| 序号 | 问题 | 严重 | 行号 | 建议 |
|---|---|---|---|---|
要求:
- 严重 用 高/中/低
- 行号 写具体数字
- 建议 不超过 30 字
表格输出便于人审,但不便于程序解析。需要程序处理时还是用 JSON。
7. CSV 输出
数据导出场景:
注意:CSV 解析在含特殊字符时容易出问题,复杂数据优先 JSON。
8. YAML / TOML 输出
适合输出配置文件:
模型对 YAML 的稳定性不如 JSON,复杂配置先生成 JSON 再转 YAML 更稳。
9. 角色提示(Role Prompting)
什么是角色提示
给模型设定一个身份,影响它的: - 知识激活范围 - 语言风格 - 输出深度 - 价值取向
角色提示对结构化输出的作用
听起来矛盾,但好的角色能让结构化输出更稳定。
❌ 没有角色:
✅ 有角色:
模型更"在角色里",会刻意保持纪律。
10. 角色提示的层次
层次 1:通用身份
激活 Python 相关知识。
层次 2:具体专长
更精准。
层次 3:经验 + 偏好
输出风格会接近你的偏好。
层次 4:完整人设
模型会"演"得更具体。
11. 角色 + 结构化输出 模板
把两者组合,是生产环境的常见模式:
你是一个生产级 JSON API。
行为约束:
- 始终返回符合 schema 的 JSON
- 永远不输出自然语言解释
- 遇到无法处理的输入,返回 {"error": "...", "code": "..."}
- 永远不偏离任务范围
Schema:
{
"result": "string",
"confidence": "number 0-1",
"metadata": {...}
}
任务:
{...}
效果好很多。
12. 处理"模型不听话"
哪怕用了所有技巧,偶尔还是会出问题。准备好兜底:
兜底 1:解析失败时重试
def call_with_retry(prompt, max_retry=3):
for i in range(max_retry):
response = call_model(prompt)
try:
return json.loads(response)
except json.JSONDecodeError:
prompt += f"\n\n上次输出无法解析为 JSON,请重新输出,仅 JSON。"
raise ValueError("model failed")
兜底 2:剥离 Markdown 围栏
def extract_json(text):
text = text.strip()
if text.startswith("```"):
text = text.split("```")[1]
if text.startswith("json"):
text = text[4:]
return json.loads(text.strip())
兜底 3:用 schema 校验
from pydantic import BaseModel
class Result(BaseModel):
title: str
score: int
result = Result.model_validate_json(response) # 严格校验
13. 结构化输出的反模式
反模式 1:字段太多
一次让模型输出 30 个字段,模型会偷懒漏掉。
解法:分多次提取,每次 5-10 个字段。
反模式 2:字段类型混乱
类型不固定,下游难处理。统一为可选 number + 用 null 表示未知。
反模式 3:嵌套过深
5 层嵌套的 JSON,模型容易在第 4 层出错。控制在 3 层以内。
反模式 4:自由文字混结构化
JSON 字符串里嵌 Markdown,转义复杂。长文用 XML,短文用 JSON。
14. 实战案例:客服意图识别
需求:把用户消息分类为预设意图,提取关键参数。
你是一个生产级意图识别系统。
行为约束:
- 始终返回 JSON
- 不要解释、不要前言
- 无法识别时 intent 设为 "unknown"
意图列表:
- query_order: 查订单状态
- request_refund: 退款申请
- product_question: 商品咨询
- complaint: 投诉
- unknown: 无法识别
Schema:
{
"intent": "上述意图之一",
"confidence": "0-1 的数字",
"params": {
"order_id": "string 或 null",
"product_name": "string 或 null"
},
"raw_message": "原始消息"
}
示例:
输入:"我那个订单 12345 怎么还没到?"
输出:
{
"intent": "query_order",
"confidence": 0.95,
"params": {"order_id": "12345", "product_name": null},
"raw_message": "我那个订单 12345 怎么还没到?"
}
输入:"你们家狗粮有几种?"
输出:
{
"intent": "product_question",
"confidence": 0.9,
"params": {"order_id": null, "product_name": "狗粮"},
"raw_message": "你们家狗粮有几种?"
}
现在:
输入:"{user_message}"
输出:
效果: - 输出 100% 可解析 - 字段稳定 - 拿 confidence 决定走人工兜底
总结
- 结构化输出是 LLM 工程化的基础,需要 100% 可解析
- JSON 最常用、最稳定,XML 适合 Claude + 长文混合,表格适合给人看
- 提示词级约束:明确 schema、明确"只输出 JSON"、给 1 个完整示例
- 生产环境强烈推荐用 Schema 模式(OpenAI Function Calling / Claude Tool Use)
- 角色提示配合结构化输出能显著提升稳定性
- 角色 4 个层次:身份 → 专长 → 经验+偏好 → 完整人设
- 兜底策略:重试、剥离 Markdown 围栏、Pydantic 校验
- 反模式:字段太多、类型混乱、嵌套过深、长文混 JSON
- 工程化模板:身份 + 行为约束 + Schema + 示例 + 错误处理