harden(extract): 提取加写死的 JSON 输出契约兜底,消除"过度依赖 skill 文件"脆点
承上次定位:脚本生成空 skill 也能出 JSON,是因其 system 带写死的 _OUTPUT_PROTOCOL(进镜像、 永远在);提取没有这层兜底,只靠 skill 文件 → skill 一空就吐散文解析失败。本次对齐脚本做法: 给提取也加 _EXTRACT_OUTPUT_CONTRACT(写死的 entities/segments JSON 契约 + 不提商品等铁律), 拼在 skill 正文之后。即便将来 skill 再丢,提取也仍收到「只输出 JSON」硬指令,不会再退化。 真验证:用 dev 库里那条真实失败脚本,system 仅留写死契约(模拟 skill 全空)真调豆包 → 仍稳定出 JSON(角色/场景齐、商品不提)。 测试:152/152 全绿(新增契约兜底单测)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8dab8b4921
commit
66778fa2d3
@@ -374,6 +374,28 @@ _EXTRACT_INFLIGHT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 写死的提取输出契约兜底(对齐脚本 agent 的 _OUTPUT_PROTOCOL):随代码进镜像、永远在。
|
||||||
|
# 即便 skill 文件丢失/为空,模型也仍收到「只输出这套 JSON」的硬指令 → 不会再退化成吐散文 → 解析失败。
|
||||||
|
# 这是历史事故(skills 没打进镜像 → 提取 system 全空 → 解析失败)的根治性兜底,与 skill 正文不冲突(互相强化)。
|
||||||
|
_EXTRACT_OUTPUT_CONTRACT = """
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 输出契约(硬性 · 优先级最高)
|
||||||
|
你是电商短视频「实体提取」agent。读【商品信息】+【分镜脚本】,认出**角色(出镜的人)**和**场景(地点/环境)**,
|
||||||
|
**只输出且仅输出一个 JSON 对象**(UTF-8,无注释,不要 ```json 代码块外的任何散文/解释/markdown):
|
||||||
|
{
|
||||||
|
"entities": [
|
||||||
|
{"id": "c1", "type": "character", "name": "女主", "visual_prompt": "一句生图描述(20-45字,只写人物本身:年龄/外形/气质/整套穿搭;空手、不带与商品同类的物件)"},
|
||||||
|
{"id": "s1", "type": "scene", "name": "地点名", "visual_prompt": "一句空镜环境描述(20-40字,不写人)"}
|
||||||
|
],
|
||||||
|
"segments": [{"index": 0, "entity_refs": ["c1", "s1"]}]
|
||||||
|
}
|
||||||
|
硬性规则:① **绝不输出商品/product 实体**(商品由系统用真实主图注入);② 角色 id 用 c1/c2…、场景 id 用 s1/s2…,各最多 6 个、严格去重;
|
||||||
|
③ segments 必须覆盖输入每一镜,index 从 0 开始,每镜列出该镜出现的角色/场景的 id;④ 整个回复就是那个 JSON,不要任何前后缀。
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_inflight_extraction(project):
|
def get_inflight_extraction(project):
|
||||||
"""本项目正在跑的实体提取任务(最近一条优先);无则 None。"""
|
"""本项目正在跑的实体提取任务(最近一条优先);无则 None。"""
|
||||||
return (
|
return (
|
||||||
@@ -428,7 +450,8 @@ def submit_extract_entities(*, project, user) -> AITask:
|
|||||||
user_msg = (
|
user_msg = (
|
||||||
f"商品信息:\n{prod_line}\n\n分镜脚本(共 {len(segments)} 镜,index 从 0 开始):\n" + "\n".join(seg_lines)
|
f"商品信息:\n{prod_line}\n\n分镜脚本(共 {len(segments)} 镜,index 从 0 开始):\n" + "\n".join(seg_lines)
|
||||||
)
|
)
|
||||||
system = _load_skill_system_prompt("ecommerce-entity-extract")
|
# skill 正文(领域功力)+ 写死的输出契约兜底(保证「只输出 JSON」永远在,即便 skill 丢失)
|
||||||
|
system = _load_skill_system_prompt("ecommerce-entity-extract") + _EXTRACT_OUTPUT_CONTRACT
|
||||||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user_msg}]
|
messages = [{"role": "system", "content": system}, {"role": "user", "content": user_msg}]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ class SkillPromptBundlingTests(SimpleTestCase):
|
|||||||
self.assertIn("JSON", prompt)
|
self.assertIn("JSON", prompt)
|
||||||
self.assertIn("entities", prompt)
|
self.assertIn("entities", prompt)
|
||||||
|
|
||||||
|
def test_extract_output_contract_is_hardcoded_safety_net(self):
|
||||||
|
"""写死的提取输出契约(随代码进镜像)必须存在且含 JSON 铁律 —— 这是 skill 丢失时的兜底,
|
||||||
|
保证提取永远收到「只输出 JSON」指令(对齐脚本 agent 的 _OUTPUT_PROTOCOL,根治"系统提示词为空"事故)。"""
|
||||||
|
from apps.ai.services import _EXTRACT_OUTPUT_CONTRACT
|
||||||
|
|
||||||
|
self.assertGreater(len(_EXTRACT_OUTPUT_CONTRACT), 200)
|
||||||
|
for marker in ("entities", "segments", "character", "scene", "JSON"):
|
||||||
|
self.assertIn(marker, _EXTRACT_OUTPUT_CONTRACT)
|
||||||
|
|
||||||
|
|
||||||
class NormalizeDraftTests(SimpleTestCase):
|
class NormalizeDraftTests(SimpleTestCase):
|
||||||
"""normalize_draft 对模型不按契约输出的容错(防「旁白/画面全空」回归)。"""
|
"""normalize_draft 对模型不按契约输出的容错(防「旁白/画面全空」回归)。"""
|
||||||
|
|||||||
Reference in New Issue
Block a user