查资料/server.py(_loads_json_loose 第 715-722 行、ai_summarize 第 725-765 行、local_summarize 第 768-777 行)\+ worry_debate_game/nodes.py(_clamp_int 第 305-309 行、evaluate_round 第 312-349 行)
返回字符串代替数组、给出越界数值。程序必须容错。
第一层:容错解析(查资料,去掉围栏、抠出花括号):
def _loads_json_loose(content):
"""从模型输出中稳健地提取 JSON 对象(容忍代码块围栏/前后缀文字)。"""
text = (content or "").strip()
text = re.sub(r"^```[a-zA-Z]*\s*|```\s*$", "", text).strip()
start, end = text.find("{"), text.rfind("}")
if start != -1 and end != -1 and end > start:
text = text[start : end + 1]
return json.loads(text)
第二层:字段消毒(类型宽容 + 截断 + 空值兜底):
tags = data.get("tags") or []
if isinstance(tags, str): # 模型可能返回字符串而不是数组
tags = re.split(r"[,,、\s]+", tags)
tags = [str(t).strip() for t in tags if str(t).strip()][:3]
return {
"title": str(data.get("title") or "").strip()[:30],
"body": str(data.get("body") or "").strip() or transcript, # 正文空就用原文
...
}
第三层:数值夹逼(worry,模型可能给 100 分或 -3 分):
def _clamp_int(value, lo: int, hi: int, default: int) -> int:
try:
return max(lo, min(hi, int(round(float(value)))))
except (TypeError, ValueError):
return default
第四层:降级链(AI 失败/未配置 → 本地规则,且告知原因):
if AI_API_KEY:
try:
result = ai_summarize(transcript)
except Exception as e:
result = local_summarize(question, transcript) # 降级
result["warning"] = str(e) # 但如实告知
else:
result = local_summarize(question, transcript)
warning 字段,用户知道发生了什么\{.*\} + 花括号截取双保险,处理"前言 + JSON + 后记"json/re)worry 有 evaluate_round 相关测试;查资料 的离线降级路径可手动验证llmjson 小库:loads_loose(text, schema_hint) -> dict,内置类型强制、范围夹逼、默认值表
pydantic + instructor:他们更强大(结构化输出 API/重试),你这个胜在零依赖、可直接嵌进任何项目
| 方案 | 特点 |
|---|---|
| Prompt 约束 + 容错解析(本功能) | 零依赖、通用;要自己写容错 |
| JSON Mode / 结构化输出 API | 更可靠;依赖模型支持 |
| Function Calling | 最可靠;概念更多 |
pydantic + instructor | Python 生态主流;较重 |
第 10 课(prompt 与链)、第 15 课(结构化输出实战)