You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

5.3 KiB

修复 AI 任务中的 Markdown JSON 解析问题

日期: 2026-02-09
类型: Bug 修复
影响范围: 剧本解析 AI 任务

背景

在使用 GPT-4o Mini 进行剧本解析时,发现 AI 有时会返回 Markdown 格式的 JSON(包裹在 ```json ... ``` 代码块中),导致 JSON 解析失败。

问题表现

第一次执行(成功)

AI 返回 result 类型: <class 'dict'>
# 直接返回 JSON 对象,解析成功

第二次执行(失败)

AI 返回 result 类型: <class 'str'>
AI 返回 result 前200字符: ```json
{
  "scenes": [
    {
      "scene_number": 1,
      ...

错误信息:

JSONDecodeError: Expecting value: line 1 column 1 (char 0)

根本原因

  1. GPT-4o Mini 的行为不一致:

    • 有时直接返回 JSON 对象(dict)
    • 有时返回 Markdown 格式的字符串(包裹在代码块中)
  2. 原有代码只处理了纯 JSON 字符串,没有处理 Markdown 代码块格式

  3. AIHubMix Provider 的 process_text 方法中,response_format 参数对 GPT-4o Mini 的约束不够强

解决方案

1. 增强 JSON 解析逻辑

server/app/tasks/ai_tasks.pyparse_screenplay_task 函数中,添加对 Markdown 代码块的处理:

if isinstance(parsed_data, str):
    import json
    import re
    
    # 移除 Markdown 代码块标记(```json ... ``` 或 ``` ... ```)
    parsed_data = parsed_data.strip()
    if parsed_data.startswith('```'):
        # 匹配 ```json 或 ``` 开头,``` 结尾
        match = re.match(r'^```(?:json)?\s*\n(.*)\n```\s*$', parsed_data, re.DOTALL)
        if match:
            parsed_data = match.group(1).strip()
            logger.info("✅ 移除 Markdown 代码块标记")
    
    # 解析 JSON
    try:
        parsed_data = json.loads(parsed_data)
    except json.JSONDecodeError as e:
        logger.error("JSON 解析失败,尝试提取 JSON 内容: %s", str(e))
        # 尝试提取 JSON 对象(从第一个 { 到最后一个 })
        json_match = re.search(r'\{.*\}', parsed_data, re.DOTALL)
        if json_match:
            parsed_data = json.loads(json_match.group())
            logger.info("✅ 成功提取并解析 JSON 内容")
        else:
            raise

2. 处理流程

  1. 检测 Markdown 代码块

    • 检查字符串是否以 ``` 开头
    • 使用正则表达式匹配 ```json\n...\n``````\n...\n``` 格式
  2. 移除代码块标记

    • 提取代码块内的 JSON 内容
    • 记录日志以便追踪
  3. 降级处理

    • 如果仍然解析失败,尝试提取第一个 { 到最后一个 } 之间的内容
    • 这可以处理 AI 在 JSON 前后添加额外文本的情况

3. 重启服务

docker restart jointo-server-celery-ai

测试验证

测试场景

  1. 场景 1:AI 直接返回 JSON 对象(dict)

    • 无需处理,直接使用
  2. 场景 2:AI 返回 Markdown 代码块格式

    ```json
    {
      "characters": [...],
      ...
    }
    
    - ✅ 移除代码块标记后解析成功
    
    
  3. 场景 3:AI 返回带额外文本的 JSON

    这是解析结果:
    {
      "characters": [...],
      ...
    }
    
    • 提取 JSON 内容后解析成功

预期结果

  • 所有格式的 AI 返回都能正确解析
  • 剧本元素正确提取和存储
  • 不再出现 JSONDecodeError

技术细节

正则表达式说明

# 匹配 Markdown 代码块
r'^```(?:json)?\s*\n(.*)\n```\s*$'
  • `^````: 以三个反引号开头
  • (?:json)?: 可选的 "json" 标识(非捕获组)
  • \s*\n: 可选的空白字符和换行
  • (.*): 捕获 JSON 内容(贪婪匹配)
  • \n```\s*$: 换行 + 三个反引号 + 可选空白 + 字符串结尾

降级策略

# 提取 JSON 对象
r'\{.*\}'
  • 从第一个 { 到最后一个 } 的所有内容
  • 使用 re.DOTALL 标志以匹配换行符

相关问题

为什么 GPT-4o Mini 行为不一致?

  1. 模型特性

    • GPT-4o Mini 在不同的请求中可能采用不同的输出格式
    • 即使设置了 response_format={'type': 'json_object'},也不能 100% 保证格式
  2. System Prompt 影响

    • 如果 Prompt 中包含"输出 JSON"等自然语言描述
    • AI 可能理解为"输出 Markdown 格式的 JSON 代码块"
  3. 温度参数

    • 较高的 temperature 会增加输出的随机性
    • 可能导致格式不一致

长期优化方向

  1. 优化 System Prompt

    • 明确要求"只返回纯 JSON,不要 Markdown 代码块"
    • 提供 JSON 示例
  2. 使用 Function Calling

    • 对于支持的模型,使用 Function Calling 强制 JSON 输出
    • 更可靠的结构化输出
  3. 添加输出验证

    • 在 AI Provider 层面验证输出格式
    • 自动重试格式错误的响应

相关文档

技术债务

  • 优化 System Prompt 以减少格式不一致
  • 研究 Function Calling 在剧本解析中的应用
  • 添加 AI 输出格式监控和告警
  • 实现自动重试机制(格式错误时)