diff --git a/core/backend/apps/ai/services.py b/core/backend/apps/ai/services.py index 1f6977b..062a9e2 100644 --- a/core/backend/apps/ai/services.py +++ b/core/backend/apps/ai/services.py @@ -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): """本项目正在跑的实体提取任务(最近一条优先);无则 None。""" return ( @@ -428,7 +450,8 @@ def submit_extract_entities(*, project, user) -> AITask: user_msg = ( 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}] try: diff --git a/core/backend/apps/ai/tests.py b/core/backend/apps/ai/tests.py index 5f2375f..e939b70 100644 --- a/core/backend/apps/ai/tests.py +++ b/core/backend/apps/ai/tests.py @@ -29,6 +29,15 @@ class SkillPromptBundlingTests(SimpleTestCase): self.assertIn("JSON", 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): """normalize_draft 对模型不按契约输出的容错(防「旁白/画面全空」回归)。"""