fix(strategy): AI 生成 META: dict = {...} 时规范化报错「找不到 META 字典」

LLM 常给 META 加类型注解 (ast.AnnAssign 节点), 旧版 _find_meta_dict
只遍历 ast.Assign 漏掉注解形式 → 抛「找不到 META 字典」→ 上层包成
「规范化 META 失败」。校验 (_extract_meta) 与规范化 (_find_meta_dict)
用两套不一致逻辑, 导致「校验通过、规范化失败」。

两个函数统一增加 ast.AnnAssign 分支, 消除不一致。补充 4 个回归测试
覆盖注解形式 (含端到端 _normalize_build_result 路径)。

修复后无论哪台机器、哪个模型生成都不再触发该错误。
This commit is contained in:
shy3130
2026-07-13 18:39:17 +08:00
parent 570bcba8b9
commit 7062dcd94d
3 changed files with 91 additions and 8 deletions
+13 -3
View File
@@ -313,14 +313,24 @@ def _py_string(value: str) -> str:
def _find_meta_dict(code: str) -> ast.Dict:
# 兼容两种 LLM 常见写法:
# META = {...} → ast.Assign
# META: dict = {...} → ast.AnnAssign (类型注解, 合法但旧逻辑漏匹配)
tree = ast.parse(code)
for node in ast.walk(tree):
value = None
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "META":
if not isinstance(node.value, ast.Dict):
raise ValueError("META 必须是字面量字典")
return node.value
value = node.value
break
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) \
and node.target.id == "META":
value = node.value
if value is not None:
if not isinstance(value, ast.Dict):
raise ValueError("META 必须是字面量字典")
return value
raise ValueError("找不到 META 字典")
+16 -5
View File
@@ -144,14 +144,25 @@ class AIStrategyGenerator:
@staticmethod
def _extract_meta(code: str) -> dict:
"""从代码字符串中提取 META 字典(不执行代码, 仅接受字面量)"""
"""从代码字符串中提取 META 字典(不执行代码, 仅接受字面量)
兼容两种声明: META = {...} (Assign) 和 META: dict = {...} (AnnAssign)。
与 api.strategy._find_meta_dict 保持同一套匹配逻辑。
"""
tree = ast.parse(code)
for node in ast.walk(tree):
value = None
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "META":
try:
return ast.literal_eval(node.value)
except (ValueError, SyntaxError) as e:
raise ValueError(f"META 必须是纯字面量字典: {e}") from e
value = node.value
break
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) \
and node.target.id == "META":
value = node.value
if value is not None:
try:
return ast.literal_eval(value)
except (ValueError, SyntaxError) as e:
raise ValueError(f"META 必须是纯字面量字典: {e}") from e
return {}
@@ -76,3 +76,65 @@ def filter(df: pl.DataFrame, params: dict) -> pl.Expr:
assert '"id": "ai_inserted"' in code
assert '"name": "中文名"' in code
assert '"description": "描述"' in code
# --- 回归: LLM 偏移写法 -------------------------------------------------
# 模型常给 META 加类型注解 (META: dict = {...}, ast.AnnAssign 节点)。
# 旧版匹配器只遍历 ast.Assign, 漏掉注解形式 → 报「找不到 META 字典」。
ANNOTATED_CODE = '''"""模型返回的策略 (带类型注解的 META — LLM 常见偏移)"""
import polars as pl
META: dict = {
"id": "annotated_wrong_id",
"name": "Placeholder",
"description": "model desc",
"tags": ["AI"],
"params": [],
"scoring": {},
}
def filter(df: pl.DataFrame, params: dict) -> pl.Expr:
return pl.lit(True)
'''
def test_find_meta_dict_accepts_type_annotated_form():
"""META: dict = {...} 必须能被识别 (旧版会抛「找不到 META 字典」)。"""
from app.api.strategy import _find_meta_dict
node = _find_meta_dict(ANNOTATED_CODE)
assert node is not None # 能找到就说明没抛异常
def test_extract_meta_accepts_type_annotated_form():
from app.strategy.ai_generator import AIStrategyGenerator
meta = AIStrategyGenerator._extract_meta(ANNOTATED_CODE)
assert meta["id"] == "annotated_wrong_id"
assert meta["name"] == "Placeholder"
def test_normalize_strategy_meta_works_on_annotated_form():
"""端到端: AI 生成注解形式 META 时, 规范化不再报「规范化 META 失败」。"""
code = _normalize_strategy_meta(
ANNOTATED_CODE,
"ai_annotated_ok",
name="断板反包",
description="中文描述",
)
assert '"id": "ai_annotated_ok"' in code
assert '"name": "断板反包"' in code
assert '"description": "中文描述"' in code
assert "annotated_wrong_id" not in code
def test_normalize_build_result_succeeds_on_annotated_form():
"""模拟前端 /build/stream 的完整结果路径 (之前报错的入口)。"""
result = {"code": ANNOTATED_CODE, "meta": {}, "valid": True, "error": None}
normalized = _normalize_build_result(result, "ai_build_ok")
assert normalized["valid"] is True
assert normalized["error"] is None
assert normalized["meta"]["id"] == "ai_build_ok"