返回

强制 ChatGPT API 输出纯净 JSON:告别 ```json 烦恼

Ai

强制 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:


结果呢?有时候它规规矩矩,有时候又“故态复萌”,就像下图展示的那样,时不时给你“惊喜”:

[![Outputs from ChatGPT with correct and wrong outputs](https://i.sstatic.net/mdSFU.png)](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)

代码解释:

  1. strip(): 去掉首尾可能存在的空格或换行符。
  2. re.search: 使用正则表达式查找被 json ... ... 包裹的内容。
    • r'```(?:json)?\s*(\{.*\}|\[.*\])\s*```': 这个正则尝试匹配 后可选的 `json` ,然后是任意空白符 (`\s*`),接着是捕获组 `(\{.*\}|\[.*\])`,这个组匹配以 `{` 或 `[` 开始,并尽可能多地 (`.*`) 匹配任意字符,直到遇到 `}` 或 `]`。最后匹配结束的
    • re.DOTALL: 让 . 能匹配包括换行符在内的任意字符。
    • re.IGNORECASE: 忽略 json 的大小写。
  3. 如果正则匹配成功 (match 对象存在),match.group(1) 就是括号内捕获到的 JSON 字符串主体。
  4. 如果正则没匹配到,我们退一步,检查字符串是否直接以 {[ 开始,并以对应的 }] 结束。如果是,就认为整个字符串是目标 JSON。
  5. 再退一步,如果上述都不满足,尝试寻找第一个 {[ 以及最后一个 }],把它们之间的内容提取出来。这能处理一些 JSON 前后混杂了其他文本的情况,但风险较高。
  6. 最关键的一步 : 无论通过哪种方式提取到了 potential_json,都必须用 json.loads() 尝试解析一下。只有能成功解析,才确认它是有效的 JSON 字符串,然后返回它。否则,说明提取的内容仍然有问题,返回 None 表示失败。

安全建议:

  • 正则表达式要谨慎编写,避免过于宽泛导致错误提取。比如,如果 JSON 内部的字符串值恰好包含 `````,复杂的正则可能会出错。上面的例子相对保守。
  • 永远在代码里对提取出来的字符串执行 json.loads() 并包含 try...except 块。这是最终的验证手段。不要相信仅通过字符串操作或正则匹配就能保证得到的是合法 JSON。

方案四:更进一步 - 试试 Function Calling

如果你需要更稳定、更结构化的数据交换,可以研究一下 OpenAI 的 Function Calling 功能。

原理和作用:
你可以在 API 请求中定义一个或多个“函数”的结构(包括函数名、、参数及其类型和)。然后,你在 Prompt 里引导模型去“调用”这个函数,并提供所需的参数。模型的回应会包含一个结构化的 JSON 对象,告诉你它想调用哪个函数以及用什么参数。这几乎完全规避了让模型自由生成包含特定格式文本的问题,因为它被引导去填充一个预定义的结构。

简要概念:

  1. 定义函数 Schema: 像写 OpenAPI (Swagger) 规范一样,定义你的函数签名,本质上就是你期望的 JSON 结构。
  2. 发起 API 请求:chat.completions.create 调用中传入 tools 参数(包含你的函数定义)和 tool_choice 参数(可以强制模型必须调用某个函数)。
  3. 处理响应: API 的响应会指示模型是否决定调用函数。如果调用了,message.tool_calls[0].function 里会包含函数名和参数(一个 JSON 字符串)。
  4. 解析参数: 解析这个参数 JSON 字符串,你就得到了结构化的数据。

虽然设置起来比前几种方法复杂,但对于需要高度可靠的结构化输出场景,Function Calling 是非常强大的工具。


总的来说,推荐的优先级是:

  1. 优先使用 response_format={'type': 'json_object'} (方案二) ,因为它最直接,是官方推荐的方式,效果通常最好。前提是你的模型支持。
  2. 配合精简、明确的 Prompt (方案一) ,特别是要在 Prompt 里说明需要 JSON 输出,并最好通过 System Message 设定角色。
  3. 加上客户端的后处理逻辑 (方案三) 作为最后的保障,清理可能漏网的 ```` 标记,并进行严格的 JSON 校验。
  4. 如果场景复杂或对可靠性要求极高,考虑 Function Calling (方案四)

结合使用这些方法,应该能有效解决 ChatGPT API 输出多余字符的问题,让你获得干净、可用的 JSON 数据。