你是不是也有过这样的经历:让大模型帮忙整理一段简历信息,结果它给你输出一大段带小标题、加粗、列表混排的“精美文案”,你反而要花更多时间去字段里捞数据?如果大模型能直接返回一份干净的 JSON,任务就简单多了——程序可以直接读取、校验、落库,前端也能直接渲染。这正是 JSON 结构化输出的价值。
本文将围绕“JSON 结构化输出”这个核心主题展开,先讲清为什么需要结构化输出,再介绍 JSON 的基础语法和常见误区,接着剖析大模型产生杂乱输出的原因,最后用一个完整的“简历助手”项目,演示从定义数据结构、构造 Prompt、调用大模型,到解析校验和生成标准简历文案的全流程。
文章适合以下读者:
- 正在用大模型 API 做应用开发的工程师。
- 被大模型“自由发挥”式输出困扰的开发者。
- 想系统学习 JSON 与结构化数据处理的初学者。
- 准备做 AI 项目实战、毕设或简历项目的新手。
学完之后,你能掌握一套从“自由文本”到“标准 JSON”再到“业务数据”的完整处理思路,并且可以把简历助手扩展成招聘匹配、档案整理、信息抽取等更多工具。
1. 为什么需要结构化输出
1.1 非结构化输出的痛点
大模型默认的输出方式是“自然语言”。自然语言天生适合人类阅读,却不适合程序解析。假设你让大模型帮用户提取简历里的工作经历,模型可能返回这样的内容:
张三,2019年入职北京某科技公司,担任后端开发工程师,期间负责电商系统订单模块的设计与开发,使用Java、Spring Boot等技术栈,带领3人小组,项目上线后系统QPS提升约30%,2021年晋升为高级工程师,2022年离职。这段文本信息密度很高,但如果你的目标是“把这份工作经历存入数据库”,就会遇到几个麻烦:
- 没有统一的字段边界。“入职时间”“离职时间”“职责描述”都混在一句话里。
- 计算机无法直接读取。要提取“公司名称”,只能靠正则或再次调用模型。
- 格式不稳定。换个提问方式,模型输出的措辞和结构可能完全不同。
在业务系统中,我们更希望大模型返回这样的内容:
{ "name": "张三", "jobs": [ { "company": "北京某科技公司", "start_date": "2019-07", "end_date": "2022-06", "title": "高级后端开发工程师", "description": "负责电商系统订单模块设计与开发,使用Java、Spring Boot等技术栈,带领3人小组,项目上线后系统QPS提升约30%。" } ] }两相对比,结构化数据的优势一目了然:字段明确、程序可解析、前后端可复用、校验成本低。
1.2 结构化输出解决什么问题
结构化输出,本质上是给大模型的“自由发挥”套上一层约束。它解决的核心问题包括:
- 数据可解析性。程序收到 JSON 后,可以直接用
json.loads()等 API 转成对象,不需要写复杂的正则表达式。 - 数据一致性。相同的字段在不同记录中拥有相同的结构,便于汇总和比较。
- 数据可校验性。可以配合 JSON Schema 对模型输出进行自动化校验,不满足要求的字段直接重新生成。
- 业务集成效率。结构化数据可以直接存入数据库,或传给下游接口,完成更复杂的自动化流程。
1.3 JSON 不是唯一的结构化方案
常见的大模型结构化输出格式有 JSON、XML 和 Markdown。JSON 是目前最主流的选择,因为:
- 前端 JavaScript 原生支持 JSON 解析。
- 后端 Java、Python、Go 等语言都有成熟的 JSON 库。
- JSON 结构轻量,阅读性和机器可读性兼具。
- 大部分大模型 API 都提供了 JSON 模式或结构化输出能力。
XML 在文档型数据场景也有优势,但解析成本更高,信息密度更低。Markdown 更适合展示型文本,不适合做数据交换。因此,本文以 JSON 作为主要讨论对象。
2. JSON 基础速览
在进入实战前,先快速复习 JSON 的核心语法。如果你已经很熟悉,可以直接跳到第 3 节。
2.1 JSON 的六种数据类型
JSON(JavaScript Object Notation,JavaScript 对象简谱)是一种轻量级的数据交换格式。它一共支持六种数据类型:
| 数据类型 | 示例 | 说明 |
|---|---|---|
| 字符串 | "name": "张三" | 必须用双引号包裹 |
| 数字 | "age": 26 | 整数或浮点数 |
| 布尔值 | "is_active": true | 只能是小写 true/false |
| 数组 | "skills": ["Java", "Python"] | 有序列表,值类型可以混合 |
| 对象 | "address": {"city": "北京"} | 无序键值对集合 |
| 空值 | "summary": null | 表示空 |
一个完整的 JSON 对象可以嵌套多种数据类型,例如:
{ "basic_info": { "name": "张三", "age": 26, "email": "zhangsan@example.com", "is_full_time": true }, "skills": ["Java", "Python", "SQL"], "work_experience": [ { "company": "某科技有限公司", "years": 2 } ] }2.2 JSON 常见语法误区
新手在写 JSON 时最容易犯几个错误:
- 误用单引号。JSON 字符串必须使用双引号,不能写成
{'name': '张三'}。 - 多了尾逗号。数组或对象最后一个元素后面不能加逗号。
- 注释混入 JSON。标准 JSON 不支持注释,像
// 这是注释这样的内容会导致解析失败。 - 布尔值大小写错误。JSON 里布尔值必须是小写
true/false。 - 数字带前导零。例如
"age": 08是非法的。
如果你经常手写 JSON,建议在 IDE 中安装 JSON 格式化插件,或者用在线 JSON 校验工具检查语法。
2.3 为什么 JSON 适合与大模型搭配使用
大模型的训练数据中有大量代码和配置文件,JSON 是其中最常见的格式之一,因此模型对 JSON 的生成能力相对较强。加上 JSON 结构本身自带明确的键名,即使模型输出的内容稍微变化,程序也能通过键名提取对应值,容错率比纯文本高很多。
举个简单的例子,下面两段 JSON 虽然字段顺序不同,但解析结果完全等价:
{"name": "张三", "age": 26}{"age": 26, "name": "张三"}这种无序性让模型在生成时更容易命中“正确结构”,而不是必须记住固定的顺序。
3. 大模型非结构化输出的原因与风险
3.1 模型为什么总爱“自由发挥”
大模型的本质是“文本生成器”。给定一段输入(Prompt),它会根据历史数据分布预测下一个 token(文本单元)。这种设计使得模型天然倾向于生成流畅自然的文本,而不是严格符合结构的“数据”。如果你不约束它,它很有可能会:
- 在 JSON 前后添加解释性文字:“好的,以下是你要的 JSON:”。
- 使用 Markdown 代码块包裹 JSON。
- 漏掉某个字段,或把字段名改成近义词。
- 在描述型字段里混入大量“仅供参考”的废话。
这些行为对聊天场景很友好,但对程序化调用是灾难。
3.2 解析杂乱输出的成本
假设大模型返回了这样的内容:
好的,根据你的描述,我给你整理了一份简历: { "姓名": "张三", // 注意:这里用了中文键名 "age": 26, "skills": ["Java", "Python",], } 希望这份简历能帮到你!程序要把它变成标准 JSON,需要经过以下步骤:
- 从文本中截取 JSON 片段。
- 去掉 Markdown 代码块标记。
- 删除注释。
- 修复尾逗号。
- 把中文键名统一成英文键名。
每一步都可能出错。如果模型输出结构不稳定,你甚至需要写一套“解析容错层”,这会让项目复杂度急剧上升。
3.3 不安全解析会带来什么隐患
除了格式问题,如果直接把大模型输出的内容当 JSON 解析,还可能出现数据类型错误、字段缺失、注入风险等问题。例如:
- 年龄字段可能是字符串
"26",而不是数字26。 - 邮箱字段可能是
null,但程序没有做空值处理。 - 恶意 Prompt 可能诱导模型输出带有额外字段的 JSON,绕过下游的数据校验逻辑。
因此,实战中必须做到“先校验、再使用”,而不是盲目相信模型输出。
4. 结构化输出的主流实现方案
4.1 方案一:Prompt 约束 + JSON 示例
这是最基础的做法。在 Prompt 中明确告诉模型“只输出 JSON”,并在 Prompt 中给出期望的结构示例。适合没有平台级 JSON 模式支持的大模型接口。
请根据用户提供的简历信息,输出一个 JSON 对象。 JSON 结构如下: { "name": "string", "age": number, "skills": ["string"] } 不要输出任何解释或额外内容。这种方法的优点是通用,缺点是模型有时仍不听话,需要额外解析和重试。
4.2 方案二:平台 JSON 模式
OpenAI、DeepSeek、Ollama 等大模型服务通常提供response_format参数,允许用户在请求中声明json_object或传入 JSON Schema。
例如在 OpenAI 兼容接口中:
{ "response_format": {"type": "json_object"} }部分平台还支持:
{ "response_format": { "type": "json_schema", "json_schema": {"name": "Resume", "schema": {...}} } }这种方式能将“模型不输出 JSON”的概率降到很低,但仍需注意字段校验。
4.3 方案三:函数调用 / 工具调用
OpenAI 等平台支持 function calling。开发者可以定义一个“工具函数”,让模型把输出包装成该函数入参对象。模型在回答时会自动生成符合函数参数结构的 JSON。
{ "name": "extract_resume", "description": "提取简历信息", "parameters": { "type": "object", "properties": { "name": {"type": "string"}, "jobs": {"type": "array"} } } }这种方案更加工程化,适合构建 Agent 类应用。
4.4 方案四:本地后处理纠偏
如果模型服务不支持 JSON 模式,或者仍偶尔返回夹杂文本的内容,可以在代码里做一层后处理:
import json import re def extract_json(text: str) -> dict: """从模型输出中提取 JSON 对象。""" text = text.strip() # 去掉 Markdown 代码块标记 text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text).strip() start = text.find("{") end = text.rfind("}") if start == -1 or end == -1: raise ValueError("未找到 JSON 对象") return json.loads(text[start:end + 1])后处理也有局限,比如尾逗号、单引号、注释等问题仍然需要额外处理。建议用第三方库如json5或自定义修复逻辑。
5. 项目实战:简历助手——需求分析与项目准备
前面把概念讲清楚了,下面进入完整实战。我们的目标是:做一个“简历助手”,用户输入一段自由描述,程序调用大模型,输出标准的简历 JSON,并最终生成一份格式化的简历文案。
5.1 项目需求拆分
简历助手需要完成以下功能:
- 接收用户输入的原始简历描述。
- 调用大模型接口,要求模型输出符合指定 JSON Schema 的简历数据。
- 解析大模型返回的 JSON,并进行字段校验。
- 将解析后的数据保存到本地 JSON 文件。
- 根据数据生成一段 Markdown 格式的简历文案,方便直接复制使用。
从工程角度,这个项目可以拆成四个模块:
- 数据模型模块:定义简历结构。
- 大模型调用模块:拼接 Prompt、调用接口、接收结果。
- 解析校验模块:解析 JSON、检查字段。
- 文案生成模块:将结构化数据渲染成文案。
5.2 技术栈与运行环境
本文示例以 Python 为例,主要使用以下组件:
- Python 3.9+。
- requests 库,用于调用兼容 OpenAI 协议的大模型接口。
- jsonschema 库(可选),用于更规范的 JSON 校验。
- 大模型接口:可以使用 OpenAI、DeepSeek、Ollama 等支持 OpenAI 兼容格式的服务。
版本方面需要提醒一点:不同大模型服务的 API 参数可能有差异,比如response_format的支持程度不同。本文的代码会兼容“支持 JSON 模式”和“不支持 JSON 模式”两种场景,你可以根据实际服务调整。
如果你没有 API Key,可以用 Ollama 本地部署一个小模型,也可以先直接跳过调用步骤,用模拟数据进行项目流程调试。
5.3 创建项目结构
终端执行:
mkdir resume-assistant cd resume-assistant mkdir data output目录结构如下:
resume-assistant/ ├── data/ # 存放输入文本和中间 JSON ├── output/ # 存放最终生成的简历 JSON 和文案 ├── config.py # 配置项 ├── models.py # 数据结构定义 ├── llm_client.py # 大模型调用模块 ├── parser.py # JSON 解析校验模块 ├── renderer.py # 简历文案渲染模块 └── main.py # 主入口5.4 安装依赖
在项目根目录执行:
pip install requests jsonschemarequests用于发送 HTTP 请求,jsonschema用于校验 JSON 结构。如果你使用的是 OpenAI 官方 SDK,也可以换成openai库,但为了减少依赖和适配不同服务,本文使用requests直接调用 OpenAI 兼容接口,这样更通用。
6. 简历助手核心代码实现
下面逐模块编写代码。完整代码可以直接复制运行,但需要根据你的实际大模型服务地址和密钥进行配置。
6.1 配置模块 config.py
# config.py import os # 大模型服务配置 # 以 OpenAI 兼容接口为例。使用 Ollama 时,可以改为 http://localhost:11434/v1 LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://api.openai.com/v1") LLM_API_KEY = os.getenv("LLM_API_KEY", "your-api-key") LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini") # 是否使用 JSON 模式。部分服务不支持,请设置为 False LLM_JSON_MODE = os.getenv("LLM_JSON_MODE", "true").lower() == "true" # 输入输出目录 INPUT_DIR = "data" OUTPUT_DIR = "output"这里使用环境变量管理密钥,避免把 API Key 硬编码在代码里,也更符合安全实践。如果你用的是本地 Ollama,LLM_BASE_URL填http://localhost:11434/v1,API Key 填任意非空字符串即可。
6.2 数据模型 models.py
我们定义简历结构。这里采用类属性的方式编写,方便 IDE 自动补全和后续扩展:
# models.py from dataclasses import dataclass, field from typing import List, Optional @dataclass class Education: school: Optional[str] = None degree: Optional[str] = None major: Optional[str] = None start_date: Optional[str] = None end_date: Optional[str] = None @dataclass class WorkExperience: company: Optional[str] = None title: Optional[str] = None start_date: Optional[str] = None end_date: Optional[str] = None description: Optional[str] = None @dataclass class Resume: name: Optional[str] = None age: Optional[int] = None email: Optional[str] = None phone: Optional[str] = None skills: List[str] = field(default_factory=list) education: List[Education] = field(default_factory=list) work_experience: List[WorkExperience] = field(default_factory=list) summary: Optional[str] = None同时,我们把 JSON Schema 单独定义为常量,后续 Prompt 和校验都会用到:
# models.py 追加内容 RESUME_SCHEMA = { "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer"}, "email": {"type": "string"}, "phone": {"type": "string"}, "skills": { "type": "array", "items": {"type": "string"} }, "education": { "type": "array", "items": { "type": "object", "properties": { "school": {"type": "string"}, "degree": {"type": "string"}, "major": {"type": "string"}, "start_date": {"type": "string"}, "end_date": {"type": "string"} } } }, "work_experience": { "type": "array", "items": { "type": "object", "properties": { "company": {"type": "string"}, "title": {"type": "string"}, "start_date": {"type": "string"}, "end_date": {"type": "string"}, "description": {"type": "string"} } } }, "summary": {"type": "string"} }, "required": ["name", "skills"] }required字段表示模型输出必须包含name和skills,其他字段可为空。这样既能约束关键信息,又不会因为信息缺失导致整次生成失败。
6.3 大模型调用模块 llm_client.py
这一部分是核心。需要处理两种场景:使用 JSON 模式和不使用 JSON 模式。代码如下:
# llm_client.py import json import requests from config import LLM_BASE_URL, LLM_API_KEY, LLM_MODEL, LLM_JSON_MODE from models import RESUME_SCHEMA def build_prompt(user_input: str) -> str: """根据用户输入构造 Prompt。""" schema_str = json.dumps(RESUME_SCHEMA, ensure_ascii=False, indent=2) prompt = f""" 你是一个简历信息提取助手。请根据用户提供的原始简历描述,提取结构化信息。 具体要求: 1. 只输出一个 JSON 对象,不要输出任何解释、问候或 Markdown 代码块标记。 2. JSON 必须符合以下 JSON Schema: {schema_str} 字段说明: - name: 姓名 - age: 年龄(数字类型) - email: 邮箱 - phone: 电话 - skills: 技能列表,数组类型 - education: 教育经历列表,每个元素包含 school、degree、major、start_date、end_date - work_experience: 工作经历列表,每个元素包含 company、title、start_date、end_date、description - summary: 个人简介 如果原始描述中缺少某个字段,请将该字段设为 null(对象类型)或空数组(数组类型),不要编造。 用户原始描述: {user_input} """ return prompt def call_llm(user_input: str) -> str: """调用大模型接口,返回模型输出的原始文本。""" url = f"{LLM_BASE_URL}/chat/completions" headers = { "Authorization": f"Bearer {LLM_API_KEY}", "Content-Type": "application/json" } payload = { "model": LLM_MODEL, "messages": [ {"role": "system", "content": "你是一个严谨的数据提取助手,严格只输出 JSON。"}, {"role": "user", "content": build_prompt(user_input)} ], "temperature": 0.2 } # 如果服务支持 JSON 模式,则开启。 # 注意:不同服务对 response_format 的兼容性不同,需按实际服务调整。 if LLM_JSON_MODE: payload["response_format"] = {"type": "json_object"} resp = requests.post(url, headers=headers, json=payload, timeout=60) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]关于temperature:数值越小,模型输出越稳定,更适合提取型任务。这里设置为 0.2,不会过分随机,也不会呆板到失去概括能力。
6.4 解析校验模块 parser.py
在实际项目中,大模型返回的结果不一定“干干净净”,所以解析和校验这一层不能省略。代码如下:
# parser.py import json import re from typing import Dict, Any from jsonschema import validate, ValidationError from models import RESUME_SCHEMA def extract_json(text: str) -> Dict[str, Any]: """从模型输出中提取 JSON 对象。""" text = text.strip() # 去掉常见的 Markdown 代码块标记 text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text).strip() # 找到第一个 { 和最后一个 } start = text.find("{") end = text.rfind("}") if start == -1 or end == -1 or end < start: raise ValueError("模型输出中未找到 JSON 对象") json_str = text[start:end + 1] # 尝试直接解析 try: return json.loads(json_str) except json.JSONDecodeError: # 如果解析失败,尝试修复尾逗号 fixed = re.sub(r",\s*([}\]])", r"\1", json_str) return json.loads(fixed) def validate_resume(data: Dict[str, Any]) -> Dict[str, Any]: """校验 JSON 是否符合简历 Schema。""" try: validate(instance=data, schema=RESUME_SCHEMA) except ValidationError as e: raise ValueError(f"简历 JSON 校验失败: {e.message}") return data6.5 文案渲染模块 renderer.py
有了结构化数据,我们可以灵活生成各种格式的文案。这里实现一个将简历 JSON 转为 Markdown 文档的函数:
# renderer.py from typing import Dict, Any def render_markdown(resume: Dict[str, Any]) -> str: """将简历 JSON 渲染为 Markdown 文案。""" lines = [] # 基本信息 name = resume.get("name", "未知") lines.append(f"# {name} 的简历") lines.append("") # 联系方式 contact_parts = [] if resume.get("email"): contact_parts.append(f"邮箱:{resume['email']}") if resume.get("phone"): contact_parts.append(f"电话:{resume['phone']}") if contact_parts: lines.append("## 联系方式") lines.append("") for part in contact_parts: lines.append(f"- {part}") lines.append("") # 个人简介 if resume.get("summary"): lines.append("## 个人简介") lines.append("") lines.append(resume["summary"]) lines.append("") # 技能 skills = resume.get("skills", []) if skills: lines.append("## 技能") lines.append("") lines.append("、".join(skills)) lines.append("") # 工作经历 work_experience = resume.get("work_experience", []) if work_experience: lines.append("## 工作经历") lines.append("") for exp in work_experience: company = exp.get("company", "未知公司") title = exp.get("title", "未知职位") start = exp.get("start_date", "") end = exp.get("end_date", "至今") date_str = f"{start} ~ {end}" if start else "" lines.append(f"### {title} @ {company} ({date_str})") if exp.get("description"): lines.append("") lines.append(exp["description"]) lines.append("") # 教育经历 education = resume.get("education", []) if education: lines.append("## 教育经历") lines.append("") for edu in education: school = edu.get("school", "未知学校") degree = edu.get("degree", "") major = edu.get("major", "") start = edu.get("start_date", "") end = edu.get("end_date", "") date_str = f"{start} ~ {end}" if start else "" lines.append(f"- {school},{degree},{major}({date_str})") lines.append("") return "\n".join(lines)这种写法把“数据”和“展示”彻底分开,后续你想改成 HTML、PDF 或 DOCX,只需要新增一个渲染函数即可。
6.6 主流程 main.py
最后把各个模块串起来:
# main.py import json import os from config import INPUT_DIR, OUTPUT_DIR from llm_client import call_llm from parser import extract_json, validate_resume from renderer import render_markdown def load_input(filename: str) -> str: """加载用户输入的简历描述文本。""" path = os.path.join(INPUT_DIR, filename) with open(path, "r", encoding="utf-8") as f: return f.read().strip() def save_json(data: dict, filename: str): """保存 JSON 到输出目录。""" os.makedirs(OUTPUT_DIR, exist_ok=True) path = os.path.join(OUTPUT_DIR, filename) with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) print(f"[INFO] JSON 已保存: {path}") def save_markdown(text: str, filename: str): """保存 Markdown 文案到输出目录。""" os.makedirs(OUTPUT_DIR, exist_ok=True) path = os.path.join(OUTPUT_DIR, filename) with open(path, "w", encoding="utf-8") as f: f.write(text) print(f"[INFO] Markdown 已保存: {path}") def main(): # 1. 读取原始简历描述 user_input = load_input("input.txt") print("[INFO] 原始输入已读取。") # 2. 调用大模型,得到原始响应文本 raw_output = call_llm(user_input) print("[INFO] 大模型调用完成。") # 3. 提取并校验 JSON resume_data = extract_json(raw_output) validate_resume(resume_data) print("[INFO] JSON 解析与校验通过。") # 4. 保存结构化 JSON save_json(resume_data, "resume.json") # 5. 渲染 Markdown 文案 markdown_text = render_markdown(resume_data) save_markdown(markdown_text, "resume.md") print("[INFO] 流程结束。") if __name__ == "__main__": main()6.7 准备输入数据
在data目录下创建input.txt,内容写一段模拟的简历描述:
张三,26岁,邮箱 zhangsan@example.com,电话 13800138000。 2019年本科毕业于北京理工大学计算机科学专业。 2020年加入某科技公司担任后端开发工程师,负责订单系统开发,使用 Java 和 Spring Boot,2021年晋升为高级工程师。 技能包括 Java、Python、SQL、Docker。 个人简介:热爱技术,有良好的团队协作能力,熟悉高并发系统设计与开发。6.8 运行项目
在项目根目录执行:
python main.py如果一切正常,你会看到类似输出:
[INFO] 原始输入已读取。 [INFO] 大模型调用完成。 [INFO] JSON 解析与校验通过。 [INFO] JSON 已保存: output/resume.json [INFO] Markdown 已保存: output/resume.md [INFO] 流程结束。打开output/resume.json,你会看到一份标准结构化简历。打开output/resume.md,会看到渲染好的 Markdown 文案,可以直接复制到简历网站或文档里。
7. 常见问题与排查思路
在项目落地过程中,你可能会遇到下面这些问题。这里整理成表格,方便快速定位。
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
| 模型返回内容包含解释性文字 | 未启用 JSON 模式,或 Prompt 约束不够 | 在 Prompt 中明确“只输出 JSON”,或使用response_format |
JSON 解析报错JSONDecodeError | 模型输出了 Markdown 代码块、尾逗号或单引号 | 用extract_json()做后处理,必要时修复尾逗号 |
| 校验失败,缺少必填字段 | 原始输入信息不足,模型没有推填充 | 扩展现有字段为可选,或对缺失字段做二次追问 |
| 年龄被识别为字符串 | 模型对类型约束不敏感 | 在 Prompt 中强调字段类型,并用代码强制类型转换 |
| 接口返回 401/403 | API Key 错误或缺少权限 | 检查环境变量配置、API Key 和套餐权限 |
| 接口返回 404 | LLM_BASE_URL路径不对 | 确认接口地址是否以/v1结尾,并检查模型名是否存在 |
| 速度太慢 | 模型较大、网络延迟或输入过长 | 换更小模型,减少 Prompt 中不必要的示例,增加超时时间 |
如果你遇到“没有 API Key”的问题,可以先用本地模拟数据替代:写一个假的call_llm函数,返回你手工构造的 JSON 字符串。这样整个流程依然可以跑通,等接上真实模型时再替换调用模块。
8. 最佳实践与工程建议
8.1 Prompt 设计
- 明确“只输出 JSON”的指令,放在 Prompt 开头,避免模型遗忘。
- 给出 JSON Schema,让模型知道每个字段的类型和含义。
- 对缺失字段给出处理策略,例如:“缺失字段设为 null 或空数组,不要编造”。
- 控制
temperature在 0 到 0.3 之间,提升生成稳定性。
8.2 数据校验
无论模型服务多成熟,都不能跳过校验层。建议在代码中引入 JSON Schema 校验,并在生产环境增加异常重试机制:
MAX_RETRIES = 3 for attempt in range(MAX_RETRIES): try: raw_output = call_llm(user_input) resume_data = extract_json(raw_output) validate_resume(resume_data) break except Exception as e: print(f"[WARN] 第 {attempt + 1} 次尝试失败: {e}") if attempt == MAX_RETRIES - 1: raise8.3 日志与可观测性
记录每次调用的输入 Prompt、原始输出、解析结果和耗时。这不仅能帮你排查问题,还能为后续 Prompt 调优提供数据支撑。建议使用结构化的日志格式,例如:
{ "event": "llm_call", "model": "gpt-4o-mini", "status": "success", "latency_ms": 1200, "input_length": 320, "output_length": 280 }8.4 安全边界
- 不要在 Prompt 中拼接未经验证的用户输入,防止提示注入。
- 对模型输出做字段白名单校验,只允许 Schema 中声明的字段。
- API Key 一律通过环境变量或密钥管理服务读取,不要提交到代码仓库。
- 涉及生产数据时,遵循最小权限原则,模型服务只需读取本次任务所需数据。
8.5 性能优化
- 限制输入长度,避免把超长文档全部塞进 Prompt,必要时先做摘要。
- 考虑批量请求。如果有多份简历需要处理,可以异步并行调用,但要注意 API 速率限制。
- 对结果做缓存。相同输入不做重复调用,节省 Token 成本。
8.6 维护成本
结构化输出不是“一次写对,永远能用”。随着业务变化,你需要调整字段结构、补充枚举约束、更新 Prompt。建议把 JSON Schema 独立成版本化文件,用 Git 管理变更,必要时记录字段兼容性说明。
9. 总结与下一步学习路线
通过这篇文章,我们完成了一条完整的学习路径。先理解了为什么需要结构化输出,再快速掌握了 JSON 的基础语法,然后深入分析了大模型产生杂乱输出的原因,最后用“简历助手”项目串起了从 Prompt 构造、模型调用、JSON 解析校验到文案渲染的全流程。你现在应该已经掌握了几个关键能力:
- 用 JSON 约束大模型输出,让程序能稳定消费模型返回的数据。
- 用 JSON Schema 定义业务数据结构,并对模型输出做自动化校验。
- 写一个可以复用的“文本到结构化数据”的工程项目骨架。
- 针对常见解析失败、字段缺失、接口报错等问题做排查和修复。
下一步,你可以继续扩展这个简历助手,比如增加多轮对话,追问用户缺失的信息;接入前端页面,做成前后端分离的完整应用;或者把“简历提取”的思路迁移到“招聘信息抽取”“合同关键字段提取”等场景。结构化输出能力是 LLM 应用开发的基本功,掌握它之后,你会发现大模型集成业务系统的效率有了质的提升。
如果这篇教程对你有帮助,可以收藏备用,也欢迎在实际动手过程中记录自己的踩坑经验,形成你自己的实战笔记。