欢迎阅读慕雪撰写的AI Agent专栏,本专栏目录如下
- 【MCP】详细了解MCP协议:和function call的区别何在?如何使用MCP?
- 【AI】AI对26届及今后计算机校招的影响
- 【Agent.01】AI Agent智能体开发专题引言
- 【Agent.02】市面上常见的大模型有哪些?
- 【Agent.03】带你学会写一个基础的Prompt
- 【Agent.04】AI时代的hello world:调用OpenAI接口,与大模型交互
- 【Agent.05】OpenAI接口Function Calling工具调用详解
- 【Agent.06】使用openai sdk实现多轮对话
- 【Agent.07】什么是Agent?从Chat到ReAct的AI进化之路
- 【Agent.08】LangChain的第一个Demo:从零开始构建Agent
- 【Agent.09】LangChain里面使用MCP工具
- 【Agent.10】OpenAI接口输出格式约束(response_format)
本专栏所有代码都会归档至 musnows/agent-blog 开源仓库。
1. 引言
Agent专栏已经写到LangChain部分了,突然想起来,还遗漏了一个重要的OpenAI接口提供的特性没有使用:结果response_format的格式化输出。
在一般情况下,AI输出的都是自然语言,和我们人类输入的信息一样。在一般的问答Agent场景中,输出自然语言是OK的,但当我们希望使用AI来生成测试用例、分析报告、数据总结等等信息的时候,就会需要AI输出结构化的数据,这样我们才能进行有效的解析和后处理。
在继续阅读本文之前,你需要对序列化、反序列化概念有所了解,并知晓json序列化协议的基本结构。
举个最简单的例子:当我们需要AI输出针对需求的测试用例时,如果AI使用的是自然语言输出,如“链接网络,打开手机APP,点击播放视频的按钮,确认视频能正常播放”,我们就没有办法对这个测试用例进行有效拆分,从而提取出前置条件、测试步骤、预期结果。
但如果我们要求AI以json格式输出这些信息,解析这个测试用例就很容易了,比如要求AI按如下格式输出:
1 2 3 4 5
| { "preStep": ["链接网络"], "testStep":["打开手机APP","点击播放视频的按钮"], "expectation":["视频能正常播放"] }
|
有了这个json,我们想解析前置步骤、测试步骤、预期结果就非常简单了,直接对json进行反序列化(如python里面的json.loads)就可以加载到这串结构化的数据,进行后续的其他处理了。
2. 怎么约束输出格式?
理解了这个背景后,想必你已经知道为啥需要让AI结构化输出信息了。那要怎么做呢?
最简单的做法,就是在Prompt里面新增输出格式的要求,让AI遵循我们的要求,直接在回答里面输出json或其他可序列化的格式(xml、yaml),然后我们对返回的string进行解析,得到最终的结构化数据。
但是,这样做有非常大的弊端:
- AI可能因为幻觉,不按我们预定的格式进行输出(现在的AI对Prompt遵循性相比半年之前有显著提升,这个问题出现次数减少了。
- AI可能会在输出中包含其他说明文字(这个问题至今依旧没有解决,AI总是喜欢给你加点其他说明,即便Prompt里面多次说明“禁止包含其他信息”)
- AI可能使用markdown代码块包裹信息输出,而不是只输出结构化数据(需要我们处理返回值里面的代码块)
所以,OpenAI提供了json_schema格式化输出的约定字段,可以要求AI依照预定的response_format进行输出。
注意,这个字段依赖于OpenAI服务提供商对response_format支持,如果你使用的是第三方服务商提供的OpenAI兼容API,需要查看该服务商的文档,确认其支持response_format字段。目前轨迹流动是支持的,而美团的LongCat就不支持(不会遵循response_format)。
测试方式也比较简单,用本文给出的response_format设置代码去测试请求一次就能看出来AI是否有遵循response_format了。

3. 使用json_schema限定AI输出格式
使用openai的python sdk,我们可以直接在创建会话的时候,传入response_format字段对返回值进行格式控制。
代码如下所示,我们希望AI格式化解析用户提供的购物清单,精准输出购买商品的名字name、数量quantity、单位unit。
在response_format的设置中,"type": "array"代表items是一个json的list,"type": "object"则代表是一个json的对象(对应python的dict)。给定的name/quantity/unit这三个字段都是required必填字段,"strict": True则是要求AI必须严格遵循这个结构进行输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import os from openai import OpenAI from dotenv import load_dotenv
load_dotenv(override=True)
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.siliconflow.cn/v1") OPENAI_MODEL = os.getenv("OPENAI_MODEL", "Qwen/Qwen3-8B")
client = OpenAI( api_key=OPENAI_API_KEY, base_url=OPENAI_BASE_URL )
def parse_shopping_list_with_schema(user_input: str): """使用JSON Schema解析购物清单""" response = client.chat.completions.create( model=OPENAI_MODEL, messages=[ {"role": "system", "content": "解析用户输入的购物清单,生成结构化数据"}, {"role": "user", "content": user_input} ], response_format={ "type": "json_schema", "json_schema": { "name": "shopping_list", "schema": { "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string"}, "quantity": {"type": "integer"}, "unit": {"type": "string"} }, "required": ["name", "quantity", "unit"] } } }, "required": ["items"], "strict": True } } } ) return response.choices[0].message.content
|
其他更复杂的格式都是在这套的基础上进行扩展,可把你的需要直接发送给编程AI助手,让他根据你的需要生成对应的json格式要求就可以了。
4. 效果对比
4.1. 使用Prompt限定代码
作为对比,这里提供了一份使用Prompt限定输出格式的OpenAI调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| def parse_shopping_list_with_prompt(user_input: str): """使用prompt限定格式解析购物清单""" system_prompt = """你是一个购物清单解析助手。请解析用户的购物清单,并严格按照以下JSON格式返回:
{ "items": [ { "name": "商品名称", "quantity": 数量, "unit": "单位" } ] }
要求: 1. 必须返回有效的JSON格式 2. name字段为字符串类型 3. quantity字段为整数类型 4. unit字段为字符串类型 5. 所有字段都是必需的 6. 不要添加任何额外的文字说明,只返回JSON 7. 不要使用markdown代码块格式 """
response = client.chat.completions.create( model=OPENAI_MODEL, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input} ], temperature=0.1 ) return response.choices[0].message.content
|
4.2. 测试结果
测试使用硅基流动的Qwen/Qwen3-8B模型
使用“我买了苹果5斤,牛奶2箱,面包3个”进行测试,可以看到,Qwen/Qwen3-8B对这种简单任务的Prompt遵循性还不错,不管是使用response_format还是使用Prompt的方式进行指定,都按照我们的要求进行输出了,且没有提供任何的说明文字。但Qwen/Qwen3-8B还是输出了markdown代码块包裹了这个json(Prompt里面要求不要使用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 测试用例 1: 用户输入: 我买了苹果5斤,牛奶2箱,面包3个
================================================== 1. 使用JSON Schema方法: {"items": [{"name": "苹果", "quantity": 5, "unit": "斤"}, {"name": "牛奶", "quantity": 2, "unit": "箱"}, {"name": "面包", "quantity": 3, "unit": "个"}]}
------------------------------ 2. 使用prompt限定格式方法: ```json { "items": [ { "name": "苹果", "quantity": 5, "unit": "斤" }, { "name": "牛奶", "quantity": 2, "unit": "箱" }, { "name": "面包", "quantity": 3, "unit": "个" } ] } ```
|
相同一次运行里面的其他输入,他又可能不会输出markdown代码块(AI幻觉导致)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| 测试用例 3: 用户输入: 可乐2瓶,薯片5包,巧克力4块
================================================== 1. 使用JSON Schema方法: { "items": [ { "name": "可乐", "quantity": 2, "unit": "瓶" }, { "name": "薯片", "quantity": 5, "unit": "包" }, { "name": "巧克力", "quantity": 4, "unit": "块" } ] }
------------------------------ 2. 使用prompt限定格式方法: { "items": [ { "name": "可乐", "quantity": 2, "unit": "瓶" }, { "name": "薯片", "quantity": 5, "unit": "包" }, { "name": "巧克力", "quantity": 4, "unit": "块" } ] }
|
因此可见,如果使用的OpenAI服务提供商支持response_format,使用response_format来控制AI输出结构是更好的方式,同时也避免我们去过多调试“约束AI生成数据结构”的Prompt了。
5. 从AI回答里面精准提取json字符串
不过呢,输出markdown代码块是一个小问题了,我们可以很轻松地编写一个json提取函数,从AI的输出里面精准提取出完整的json来,只要AI输出的json没有断。
如果你不想自己实现,可以使用pypi上已有的解析器:JsonExtractor
在很多场景下都可以使用这个json对AI的输出进行处理(即便提供了response_format也可以使用这个函数先处理一下),保证我们后续节点一定能得到一个有效的json结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| import json import re from typing import Union
def extract_json_with_regex(response_str: str) -> Union[dict,list]: """ 使用算法从字符串中精准提取唯一的完整JSON Args: response_str: 包含JSON的原始回答字符串 Returns: dict/list: 解析后的JSON对象或数组,无法解析时返回None """ try: return json.loads(response_str) except json.JSONDecodeError as e: pass response_str = re.sub(r'```(?:json)?\s*|\s*```', '', response_str, flags=re.IGNORECASE) matches = [] for i, char in enumerate(response_str): if char == '{': json_str = _extract_balanced(response_str, i, '{', '}') if json_str: matches.append(json_str) elif char == '[': json_str = _extract_balanced(response_str, i, '[', ']') if json_str: matches.append(json_str) if not matches: return None json_str = max(matches, key=len).strip() while json_str and json_str[-1] not in '}]': json_str = json_str[:-1].strip() if json_str and json_str[-1] in ',;': json_str = json_str[:-1].strip() try: return json.loads(json_str) except json.JSONDecodeError as e: print(f"JSON解析失败:{str(e)}\n提取的内容:{json_str}") return None
def _extract_balanced(text: str, start_idx: int, open_char: str, close_char: str) -> str: """ 提取平衡的括号内容 Args: text: 源文本 start_idx: 起始位置(必须是开括号) open_char: 开括号字符 close_char: 闭括号字符 Returns: str: 平衡的括号内容,如果无法平衡则返回None """ if text[start_idx] != open_char: return None stack = 1 in_string = False escape_next = False for i in range(start_idx + 1, len(text)): char = text[i] if escape_next: escape_next = False continue if char == '\\' and in_string: escape_next = True continue if char == '"' and not escape_next: in_string = not in_string continue if not in_string: if char == open_char: stack += 1 elif char == close_char: stack -= 1 if stack == 0: return text[start_idx:i+1] return None
|
当然,这个函数没办法处理AI输出的json的结构不对的情况(比如缺key、key的名字不对、结构错乱等问题),只能保证剔除回答里面的其他无效信息,解析出一个有效的json。
同时,这个函数也不能支持回答里面有多个独立的json的情况。不推荐让AI输出多个独立的json,如果有多个独立json输出的要求,请使用一个大的json,用key包含这些json进行输出,比如指定多个大key来保存独立的json
1 2 3 4 5
| { "key1": {}, "key2": {}, ... }
|
6. The end
本文介绍了如何在调用OpenAI接口的时候约束AI的输出结构。这个场景几乎是AI Agent开发里面最常见的场景。即便我们后续使用LangChain SDK,也一样会遇到需要要求AI输出结构化数据的场景。