fix(site-import): GEN2 嵌套分组展平容错 + 提示词钉死扁平输出(Task10 实测 glm-4-flash 常见形态)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-20 23:11:08 +08:00
parent 84aa302c8e
commit 9c71fd51d5
2 changed files with 55 additions and 3 deletions
+25 -3
View File
@@ -63,7 +63,7 @@ GEN1_SYSTEM = """你是 B2B 外贸网站的行业分析师。分析用户提供
GEN2_SYSTEM = """你是 B2B 外贸客服提示词的撰写专家。根据上一步的结构化分析结果(JSON)与参考范例,为该网站生成客服提示词的全部占位符内容与品牌文案。
## 规则(全部必须遵守)
- 严格只输出一个 JSON 对象:键名与「待生成键清单」逐字一致、一个不落;不要 markdown 代码栅栏、不要解释文字、不要新增清单外的键
- 严格只输出一个**扁平** JSON 对象:所有键平铺在同一层(禁止按模板分组嵌套成 {"system": {...}, "extractor": {...}} 这类结构);键名与「待生成键清单」逐字一致、一个不落;不要 markdown 代码栅栏、不要解释文字、不要新增清单外的键
- 逐字填槽:只生成清单所列键的值,不复述模板固定段、不改写骨架结构
- 语言钉死:描述性内容用中文写;产品统称EN、面向客户的对话示例句(固定追问句/零上下文首轮例句/好差示例组/正例/负例/浏览例句/产品追问示例)按其语境语言(通常英文)写——抓取文本是西语/英语不改变本规则
- 数字纪律:示例与描述中出现的数字必须来自分析结果(即源站出现过的数字),禁止推算、换算、编造
@@ -178,6 +178,26 @@ def _parse_json(raw: str) -> dict | None:
return obj if isinstance(obj, dict) else None
def _flatten_nested(obj: dict) -> dict:
"""嵌套分组容错:模型把键按 {"system": {...}, "extractor": {...}} 分组时
自动展平为单层(Task 10 实测 glm-4-flash 的常见形态——清单里
「system 模板占位符」措辞诱导分组)。顶层已可直接识别的键不动;
dict 值的键并入顶层(子键覆盖散键同名时以子键为准),非 dict 值原位保留;
无 dict 值时输出与输入等价。"""
recognizable = set(ALL_PLACEHOLDER_NAMES) | set(_BRAND_KEYS) | set(_TAIL_KEYS)
if any(k in recognizable for k in obj):
# 顶层已有可识别键——但混合形态(嵌套组+散键)仍需展开 dict 值
if not any(isinstance(v, dict) for v in obj.values()):
return obj
flat: dict = {}
for k, v in obj.items():
if isinstance(v, dict):
flat.update(v)
else:
flat[k] = v
return flat
def _analysis_shape_ok(d) -> bool:
"""调用 1 schema 下限:company_name 为串 + product_lines 为列表。
@@ -210,8 +230,10 @@ async def _chat_json(provider, system: str, user: str, what: str,
except asyncio.TimeoutError:
raise GenerateError(f"{what}:AI 调用超时({AI_TIMEOUT:.0f}s)")
parsed = _parse_json(raw)
if parsed is not None and shape_ok(parsed):
return parsed
if parsed is not None:
parsed = _flatten_nested(parsed)
if shape_ok(parsed):
return parsed
raise GenerateError(f"{what}:输出无法解析为 JSON 或 schema 不符(已自动重试一次)")
+30
View File
@@ -432,3 +432,33 @@ class TestCrawlWarningsMerge:
draft = generator.build(gen, ANALYSIS_JSON, crawl, SOURCE_TEXT)
assert "logo 为不支持的格式,已跳过" in draft["warnings"]
assert "子页过薄已跳过:/thin" in draft["warnings"]
class TestNestedFlatten:
def test_嵌套分组自动展平(self):
"""Task 10 实测:glm-4-flash 常把键按模板分组嵌套——展平容错。"""
from app.platform.site_import.generator import _flatten_nested
nested = {"system": {"身份描述": "x", "产品统称": "y"},
"extractor": {"采购域": "z"}, "brand_title": "T"}
flat = _flatten_nested(nested)
assert flat == {"身份描述": "x", "产品统称": "y", "采购域": "z", "brand_title": "T"}
def test_本来就扁平原样返回(self):
from app.platform.site_import.generator import _flatten_nested
flat_in = {"身份描述": "x", "brand_title": "T"}
assert _flatten_nested(dict(flat_in)) == flat_in
def test_非dict子值不展平(self):
from app.platform.site_import.generator import _flatten_nested
weird = {"a": "字符串", "b": {"身份描述": "x"}}
assert _flatten_nested(weird) is weird or _flatten_nested(weird) == weird
async def test_generate接受嵌套输出(self):
"""端到端:fake 回嵌套 JSON 也能过 generate。"""
import json as _json
nested = {"system": {"身份描述": "x"}, "brand_title": "T"}
class NestedP:
async def chat(self, m, s):
return "```json\n" + _json.dumps(nested, ensure_ascii=False) + "\n```"
out = await generator.generate(NestedP(), ANALYSIS_JSON)
assert out.get("身份描述") == "x" and out.get("brand_title") == "T"