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
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)
根本原因
-
GPT-4o Mini 的行为不一致:
- 有时直接返回 JSON 对象(dict)
- 有时返回 Markdown 格式的字符串(包裹在代码块中)
-
原有代码只处理了纯 JSON 字符串,没有处理 Markdown 代码块格式
-
AIHubMix Provider 的
process_text方法中,response_format参数对 GPT-4o Mini 的约束不够强
解决方案
1. 增强 JSON 解析逻辑
在 server/app/tasks/ai_tasks.py 的 parse_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. 处理流程
-
检测 Markdown 代码块:
- 检查字符串是否以
```开头 - 使用正则表达式匹配
```json\n...\n```或```\n...\n```格式
- 检查字符串是否以
-
移除代码块标记:
- 提取代码块内的 JSON 内容
- 记录日志以便追踪
-
降级处理:
- 如果仍然解析失败,尝试提取第一个
{到最后一个}之间的内容 - 这可以处理 AI 在 JSON 前后添加额外文本的情况
- 如果仍然解析失败,尝试提取第一个
3. 重启服务
docker restart jointo-server-celery-ai
测试验证
测试场景
-
场景 1:AI 直接返回 JSON 对象(dict)
- ✅ 无需处理,直接使用
-
场景 2:AI 返回 Markdown 代码块格式
```json { "characters": [...], ... }- ✅ 移除代码块标记后解析成功 -
场景 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 行为不一致?
-
模型特性:
- GPT-4o Mini 在不同的请求中可能采用不同的输出格式
- 即使设置了
response_format={'type': 'json_object'},也不能 100% 保证格式
-
System Prompt 影响:
- 如果 Prompt 中包含"输出 JSON"等自然语言描述
- AI 可能理解为"输出 Markdown 格式的 JSON 代码块"
-
温度参数:
- 较高的 temperature 会增加输出的随机性
- 可能导致格式不一致
长期优化方向
-
优化 System Prompt:
- 明确要求"只返回纯 JSON,不要 Markdown 代码块"
- 提供 JSON 示例
-
使用 Function Calling:
- 对于支持的模型,使用 Function Calling 强制 JSON 输出
- 更可靠的结构化输出
-
添加输出验证:
- 在 AI Provider 层面验证输出格式
- 自动重试格式错误的响应
相关文档
技术债务
- 优化 System Prompt 以减少格式不一致
- 研究 Function Calling 在剧本解析中的应用
- 添加 AI 输出格式监控和告警
- 实现自动重试机制(格式错误时)