强制 ChatGPT API 输出纯净 JSON:告别 ```json 烦恼
2025-04-01 08:52:54
强制 ChatGPT API 输出纯净 JSON:告别恼人的 ````json`
用 ChatGPT API 时,明明想让它只吐 JSON,结果它老给你画蛇添足,在前后加上 json` 和
这样的标记,是不是挺烦人?
就像下面这样:
你想要的输出:
{
"name": "Jon",
"last_nane": "Doe"
}
结果 API 可能给的是:
```json
{
"name": "Jon",
"last_name": "Doe"
}
尤其是当你把这个输出直接喂给下一个程序处理时,这点“小聪明”就成了大麻烦,解析直接报错。就算你在 Prompt 里三令五申,告诉它:
* 别用反引号!
* 别加 ````json`!
* 别加 ````!
* 别解释!
* 完全照搬我给的输出格式!
比如下面这个精心设计的 Prompt:
Translate the following text to the languages matching the language codes and output them in JSON format.
Important:
- Do NOT use apostrophes/backticks in the output
- Do NOT start with ```json
- Do NOT end with ```
- Do NOT explain anything in the output
- If there are any exceptions, then do NOT translate them
- If the input is empty, then the output should be empty
Use exactly the same OUTPUT format as the below:
INPUT: Dette er en Dansk oversættelse
LANGUAGECODES: da-DK, en-US, nl-NL
EXCEPTIONS: Dansk
OUTPUT: [{"da-DK":"Dette er en Dansk oversættelse"},{"en-US":"This is an Dansk translation"},{"nl-NL":"Dit is een Dansk vertaling"}]
INPUT: {{$input}}
LANGUAGECODES: {{$languagecodes}}
EXCEPTIONS: {{$exceptions}}
OUTPUT:
结果呢?有时候它规规矩矩,有时候又“故态复萌”,就像下图展示的那样,时不时给你“惊喜”:
[](https://i.sstatic.net/mdSFU.png)
这随机性让人头疼。怎么才能让它彻底老实,只输出干干净净的 JSON 对象呢?
## 刨根问底:这“随机性”哪来的?
这事儿吧,不能全怪 API。大语言模型(LLM)本身的工作方式就有点“随性”。
1. **训练数据的“锅”** : 模型训练时看了海量的文本,其中包含大量用 Markdown 格式写的代码片段。在 Markdown 里,用 ````json ... ```` 来标记 JSON 代码块是标准操作。模型可能觉得:“哦,你想让我生成 JSON 啊,那得按标准格式来,加上标记才专业!”—— 它把格式标记也当成了内容的一部分。
2. **概率的游戏** : LLM 生成内容是基于概率的。它不是严格按照指令执行代码,而是在预测下一个最可能的词(token)。即使你明确禁止,它在某些上下文里,生成 ````json` 的概率还是会略高,就可能“顺手”加上了。指令再多,也只是提高了不加标记的概率,没法做到 100% 保证。
3. **指令理解的模糊地带** : 你的 Prompt 虽然看起来很详细,但对模型来说,理解长篇幅、多条否定指令("Do NOT...")本身就有挑战。它可能抓住了核心——“生成 JSON”,但忽略了某些格式限制,或者在处理复杂指令时“短路”了。
## 出招应对:几种强制输出纯净 JSON 的方案
别灰心,虽然不能保证 100% 绝对杜绝,但我们有几种方法能大大提高成功率,甚至接近完美。
### 方案一:优化 Prompt - 精准指令是关键
跟人沟通一样,话说不清楚,对方就容易误会。改进 Prompt 是最直接、成本最低的方法。
**原理和作用:**
通过调整措辞、强调重点、减少歧义,让模型更准确地理解你的核心需求——只要纯 JSON,不要任何附加物。
**具体操作和示例:**
1. **强调核心格式要求,放在最显眼的位置** : 把否定指令(Do NOT...)提前,或者用更醒目的方式强调。试试把最重要的规则放在 `OUTPUT:` 指令之前或之后,并再次重申。
```
Translate the following text... (前面部分省略) ...
Important Rules for the Output:
* The output MUST be a valid JSON object or JSON array ONLY.
* ABSOLUTELY NO backticks (`) before or after the JSON.
* ABSOLUTELY NO ```json markdown tags.
* ABSOLUTELY NO explanations or any other text outside the JSON itself.
Use exactly the same OUTPUT format as the below example:
(示例部分省略)
INPUT: {{$input}}
LANGUAGECODES: {{$languagecodes}}
EXCEPTIONS: {{$exceptions}}
OUTPUT: (The model should generate the pure JSON here)
```
2. **使用更强烈的语气** : 将 "Do NOT" 改为 "ABSOLUTELY NO" 或者 "MUST NOT contain"。虽然听起来有点傻,但对模型来说可能信号更强。
3. **简化指令** : 有时候指令太多反而让模型困惑。尝试去掉一些可能非必要的指令,比如关于空输入的处理(如果你的场景里基本不会遇到空输入),看看会不会改善。
4. **利用系统消息(System Message)** : 如果你使用的是 Chat Completion API,可以把一些全局性的、固定不变的指令(比如“你是一个只输出纯净 JSON 的助手”)放在 System Message 里,让 User Message 更聚焦于当次任务。
*System Message:*
```
You are an API assistant that ONLY outputs raw, valid JSON strings according to the user's request. You NEVER wrap the JSON in markdown code blocks (like ```json ... ```). You NEVER add any explanations or conversational text. Respond ONLY with the JSON object or array itself.
```
*User Message:*
```
Translate the following text... (只需要包含任务相关的部分) ...
INPUT: Dette er en Dansk oversættelse
LANGUAGECODES: da-DK, en-US, nl-NL
EXCEPTIONS: Dansk
OUTPUT: [{"da-DK":"Dette er en Dansk oversættelse"},{"en-US":"This is an Dansk translation"},{"nl-NL":"Dit is een Dansk vertaling"}]
INPUT: {{$input}}
LANGUAGECODES: {{$languagecodes}}
EXCEPTIONS: {{$exceptions}}
OUTPUT:
```
**进阶技巧: Few-Shot 示例强化**
你的 Prompt 已经用了一个示例(Few-Shot),这是个好方法。可以考虑再增加一两个不同场景但遵循相同规则的示例,进一步强化模型对格式的理解。确保你的示例 *完全* 符合你最终期望的纯净 JSON 格式。
### 方案二:API 参数调优 - 利用模型特性
OpenAI 其实提供了“官方外挂”来解决这个问题,那就是 `response_format` 参数。
**原理和作用:**
这个参数直接告诉 API:“我就是要 JSON!” API 会在内部进行优化,强制模型输出符合 JSON 语法的字符串。这通常比单靠 Prompt 指令更可靠。
**具体操作和代码示例 (Python):**
你需要在使用 Chat Completion API 时,将 `response_format` 设置为 `{'type': 'json_object'}`。
```python
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY") # 替换成你的 API Key
try:
response = client.chat.completions.create(
model="gpt-4-turbo", # 或者支持 JSON 模式的其他模型, 比如 gpt-3.5-turbo-1106 及更新版本
messages=[
{"role": "system", "content": "You are a helpful assistant designed to output JSON."}, # 最好还是加一句 system message
{"role": "user", "content": """
Translate the following text to the languages matching the language codes and output them in JSON format.
Important Rules for the Output:
* The output MUST be a valid JSON object or JSON array ONLY.
Use exactly the same OUTPUT format as the below example:
----------------
INPUT: Dette er en Dansk oversættelse
LANGUAGECODES: da-DK, en-US, nl-NL
EXCEPTIONS: Dansk
OUTPUT: [{"da-DK":"Dette er en Dansk oversættelse"},{"en-US":"This is an Dansk translation"},{"nl-NL":"Dit is een Dansk vertaling"}]
INPUT: Hello world
LANGUAGECODES: es-ES, fr-FR
EXCEPTIONS: world
OUTPUT:
"""}
],
response_format={ "type": "json_object" } # 关键在这里!
)
# response.choices[0].message.content 现在大概率就是纯净的 JSON 字符串了
json_output_string = response.choices[0].message.content
print(json_output_string)
# 你可以尝试解析它
import json
try:
parsed_json = json.loads(json_output_string)
print("JSON parsed successfully!")
# 后续处理...
except json.JSONDecodeError as e:
print(f"Failed to parse JSON: {e}")
# 记录错误或进行其他处理
except Exception as e:
print(f"An API error occurred: {e}")
注意事项:
- 模型支持 :
json_object
模式需要特定的模型版本支持,例如gpt-4-turbo
,gpt-4-0125-preview
,gpt-3.5-turbo-1106
及更新的版本。使用旧模型会报错。 - Prompt 要求 : 即便用了
json_object
模式,你仍然需要在 Prompt (通常是 System Message 或最后一个 User Message) 里明确指示模型输出 JSON。否则 API 也会报错。你的 Prompt 已经满足了这一点。 - 不保证 100% : 虽然官方大力推荐,但在极罕见的情况下,模型输出的 JSON 仍然可能存在语法错误(比如忘记逗号)。所以,客户端的 JSON 解析和错误处理还是必要的。但它几乎总能解决 ````json` 包裹的问题。
- 输出类型 :
json_object
顾名思义,主要保证输出是个 JSON 对象{...}
。如果你期望的是 JSON 数组[...]
作为根元素(就像你的翻译例子),理论上它也能处理,但确保 Prompt 里的示例清晰指向数组格式。如果遇到问题,可以尝试让模型输出一个包含该数组的对象,例如{"translations": [...]}
,然后再提取。
方案三:后处理 - 输出端兜底
有时候,无论你怎么优化,API 它就是偶尔“调皮”一下。这时,与其和它较劲,不如在接收端加一道“保险杠”——后处理。
原理和作用:
不管 API 返回什么,我们都假设它可能混入了多余的字符,用代码自动把这些“杂质”清理掉,提取出真正的 JSON 部分。
具体操作和代码示例 (Python):
import re
import json
def extract_pure_json(raw_output: str) -> str | None:
"""
尝试从可能包含 ```json ... ``` 标记的字符串中提取纯净的 JSON 字符串。
"""
raw_output = raw_output.strip() # 先去除首尾空白
# 常见模式: ```json ... ``` 或 ``` ... ```
match = re.search(r'```(?:json)?\s*(\{.*\}|\[.*\])\s*```', raw_output, re.DOTALL | re.IGNORECASE)
if match:
# 如果匹配到 ``` 包裹的模式,提取括号内的内容
potential_json = match.group(1).strip()
else:
# 如果没有匹配到 ``` 包裹,假设整个字符串就是 JSON (或者至少应该尝试解析)
# 检查是否直接以 { 或 [ 开头,以 } 或 ] 结尾
if (raw_output.startswith('{') and raw_output.endswith('}')) or \
(raw_output.startswith('[') and raw_output.endswith(']')):
potential_json = raw_output
else:
# 可能根本不是有效的JSON,或者混杂了其他文本
# 尝试寻找第一个 { 或 [ 和最后一个 } 或 ]
first_brace = raw_output.find('{')
first_bracket = raw_output.find('[')
start_index = -1
if first_brace != -1 and first_bracket != -1:
start_index = min(first_brace, first_bracket)
elif first_brace != -1:
start_index = first_brace
elif first_bracket != -1:
start_index = first_bracket
if start_index != -1:
last_brace = raw_output.rfind('}')
last_bracket = raw_output.rfind(']')
end_index = max(last_brace, last_bracket)
if end_index > start_index:
potential_json = raw_output[start_index:end_index+1].strip()
else:
return None # 没找到合理的结束符
else:
return None # 连开始符都没找到
# 最后一步:尝试解析,确认是有效的 JSON
try:
json.loads(potential_json) # 只验证,不返回解析结果
return potential_json # 返回清理后的纯净 JSON 字符串
except json.JSONDecodeError:
# 即使清理了,也不是有效的 JSON
# 可以选择在这里打印日志或抛出更具体的异常
print(f"Warning: Extracted string is not valid JSON: {potential_json[:100]}...") # 只显示前100个字符
return None # 或者返回原始 potential_json,让调用者决定如何处理
# --- 使用示例 ---
outputs = [
'```json\n{\n "name": "Jon",\n "last_name": "Doe"\n}\n```',
'```\n{\n "name": "Jane",\n "last_name": "Smith"\n}\n```',
'{\n "name": "Peter",\n "last_name": "Jones"\n}',
'Here is the JSON you requested:\n```json\n[{"item": "apple"}, {"item": "banana"}]\n```\nHope this helps!',
'[{"lang":"en","text":"Hello"}]',
'{"message": "Done"} but maybe with some extra text here.',
'Invalid stuff'
]
for output in outputs:
print(f"Raw output: {output[:50]}...") # 打印部分原始输出
cleaned_json = extract_pure_json(output)
if cleaned_json:
print(f"Cleaned JSON: {cleaned_json}")
# 在这里可以安全地调用 json.loads(cleaned_json)
try:
parsed = json.loads(cleaned_json)
print("Successfully parsed!")
except json.JSONDecodeError:
print("Error: Parsing failed even after cleaning!") # 理论上不应该发生,除非extract_pure_json逻辑有误
else:
print("Failed to extract valid JSON.")
print("-" * 20)
代码解释:
strip()
: 去掉首尾可能存在的空格或换行符。re.search
: 使用正则表达式查找被json ...
或...
包裹的内容。r'```(?:json)?\s*(\{.*\}|\[.*\])\s*```'
: 这个正则尝试匹配后可选的 `json` ,然后是任意空白符 (`\s*`),接着是捕获组 `(\{.*\}|\[.*\])`,这个组匹配以 `{` 或 `[` 开始,并尽可能多地 (`.*`) 匹配任意字符,直到遇到 `}` 或 `]`。最后匹配结束的
。re.DOTALL
: 让.
能匹配包括换行符在内的任意字符。re.IGNORECASE
: 忽略json
的大小写。
- 如果正则匹配成功 (
match
对象存在),match.group(1)
就是括号内捕获到的 JSON 字符串主体。 - 如果正则没匹配到,我们退一步,检查字符串是否直接以
{
或[
开始,并以对应的}
或]
结束。如果是,就认为整个字符串是目标 JSON。 - 再退一步,如果上述都不满足,尝试寻找第一个
{
或[
以及最后一个}
或]
,把它们之间的内容提取出来。这能处理一些 JSON 前后混杂了其他文本的情况,但风险较高。 - 最关键的一步 : 无论通过哪种方式提取到了
potential_json
,都必须用json.loads()
尝试解析一下。只有能成功解析,才确认它是有效的 JSON 字符串,然后返回它。否则,说明提取的内容仍然有问题,返回None
表示失败。
安全建议:
- 正则表达式要谨慎编写,避免过于宽泛导致错误提取。比如,如果 JSON 内部的字符串值恰好包含 `````,复杂的正则可能会出错。上面的例子相对保守。
- 永远在代码里对提取出来的字符串执行
json.loads()
并包含try...except
块。这是最终的验证手段。不要相信仅通过字符串操作或正则匹配就能保证得到的是合法 JSON。
方案四:更进一步 - 试试 Function Calling
如果你需要更稳定、更结构化的数据交换,可以研究一下 OpenAI 的 Function Calling 功能。
原理和作用:
你可以在 API 请求中定义一个或多个“函数”的结构(包括函数名、、参数及其类型和)。然后,你在 Prompt 里引导模型去“调用”这个函数,并提供所需的参数。模型的回应会包含一个结构化的 JSON 对象,告诉你它想调用哪个函数以及用什么参数。这几乎完全规避了让模型自由生成包含特定格式文本的问题,因为它被引导去填充一个预定义的结构。
简要概念:
- 定义函数 Schema: 像写 OpenAPI (Swagger) 规范一样,定义你的函数签名,本质上就是你期望的 JSON 结构。
- 发起 API 请求: 在
chat.completions.create
调用中传入tools
参数(包含你的函数定义)和tool_choice
参数(可以强制模型必须调用某个函数)。 - 处理响应: API 的响应会指示模型是否决定调用函数。如果调用了,
message.tool_calls[0].function
里会包含函数名和参数(一个 JSON 字符串)。 - 解析参数: 解析这个参数 JSON 字符串,你就得到了结构化的数据。
虽然设置起来比前几种方法复杂,但对于需要高度可靠的结构化输出场景,Function Calling 是非常强大的工具。
总的来说,推荐的优先级是:
- 优先使用
response_format={'type': 'json_object'}
(方案二) ,因为它最直接,是官方推荐的方式,效果通常最好。前提是你的模型支持。 - 配合精简、明确的 Prompt (方案一) ,特别是要在 Prompt 里说明需要 JSON 输出,并最好通过 System Message 设定角色。
- 加上客户端的后处理逻辑 (方案三) 作为最后的保障,清理可能漏网的 ```` 标记,并进行严格的 JSON 校验。
- 如果场景复杂或对可靠性要求极高,考虑 Function Calling (方案四) 。
结合使用这些方法,应该能有效解决 ChatGPT API 输出多余字符的问题,让你获得干净、可用的 JSON 数据。