Promotion 判据太宽,劣质输出被放行

Agent 流水线的晋级(Promotion)判断逻辑标准过于宽松,让不完整、格式错误或语义错误的输出通过了质量关卡,流入下游系统或生产环境。本文分析判据失效的根因并给出量化指标和分层校验方案。

你的 CrewAI 流水线里,Quality Gate Agent 的判断标准是「输出是否包含 conclusion 字段」。结果一个 Agent 输出了 "conclusion": ""(空字符串),字段存在但内容为空,Quality Gate 放行了。或者在 LangGraph 里,Reviewer 节点的 prompt 说「如果代码看起来合理就通过」,LLM 把一段有 5 个 TODO 注释、3 个 hardcoded secret 的代码判定为「看起来合理」,直接进了 PR。Promotion 判据太宽的危害不是立即可见的——劣质输出会悄悄流过整个流水线,在生产环境里才暴露问题。

常见原因

1. 判据只检查字段存在性,不检查字段值的有效性

if "conclusion" in output: promote() 无法区分有意义的结论和空字符串。同理,if output.get("score") is not None 无法捕获 score=0score=-1 这类非法值。

怎么判断:审查所有 Promotion 判据的代码,统计用了多少 in 检查(存在性)vs 实际值校验(非空、范围、格式)。

2. 用 LLM 做质量评判,但 prompt 过于宽泛

「请评估这段输出的质量,如果合格则输出 PASS」这类 prompt 没有定义「合格」的具体标准,LLM 会根据其内部的质量感知来判断,通常比人类预期的要宽松——LLM 倾向于「给对方面子」。

怎么判断:用已知的劣质输出样本(空结论、有错误、格式不对)测试 Reviewer prompt,如果这些样本都得到了 PASS,判据就太宽松了。

3. 多维度评分时只要求平均分达标,不要求各维度都达标

输出在「格式完整性」维度得了 10 分,在「内容准确性」维度得了 2 分,平均 6 分,恰好超过了 5 分的 Promotion 阈值。但内容准确性 2 分的输出显然不应该被放行。

怎么判断:查看 Promotion 逻辑是否用了 avg_score >= threshold,如果是,把各维度的得分单独列出来,检查是否有维度低于最低可接受值。

4. 阈值是在小规模测试集上调出来的,实际分布不同

在 100 条测试数据上,阈值 7 分能过滤掉 95% 的劣质输出。但生产数据的分布和测试数据不同,阈值 7 分在生产环境只能过滤掉 70% 的劣质输出。

怎么判断:对比 Promotion 通过的输出样本(随机抽 50 条)与人工标注的质量,统计「系统认为合格但人工认为不合格」的比例。

5. 没有对「必须没有」的条件做检查

某些问题是绝对不能放行的(如硬编码 secret、SQL 注入风险、空 body),但 Promotion 判据只做了「必须有什么」的正向检查,没有做「不能有什么」的负向检查。

怎么判断:在 Promotion 判据代码里搜索 assert notif ... in output: rejectblocklist 等关键词,如果没有,就缺少负向检查。

6. 判据随任务类型变化,但代码里用了统一的阈值

代码生成任务的质量标准(无语法错误、无已知漏洞)和文档生成任务的质量标准(完整性、可读性)完全不同,但系统用了同一套判据,造成代码任务的判据太松,或者文档任务的判据太严。

怎么判断:列出所有任务类型和对应的 Promotion 判据,检查是否有任务类型用了不匹配的通用判据。

最短修复路径

Step 1:把 Promotion 判据从布尔检查升级为分层校验

from pydantic import BaseModel, field_validator
from enum import Enum

class PromotionVerdict(str, Enum):
    PASS = "pass"
    WARN = "warn"      # 通过但有警告
    FAIL = "fail"      # 阻断

class PromotionChecker:
    def check(self, output: dict, task_type: str) -> PromotionVerdict:
        failures = []
        warnings = []
        
        # 层级 1:硬性条件(任何一条失败就 FAIL)
        hard_failures = self._check_hard_requirements(output, task_type)
        if hard_failures:
            return PromotionVerdict.FAIL, hard_failures
        
        # 层级 2:软性条件(失败记 warning,累积到一定数量变 FAIL)
        soft_issues = self._check_soft_requirements(output, task_type)
        if len(soft_issues) >= 3:
            return PromotionVerdict.FAIL, soft_issues
        elif soft_issues:
            return PromotionVerdict.WARN, soft_issues
        
        return PromotionVerdict.PASS, []
    
    def _check_hard_requirements(self, output: dict, task_type: str) -> list[str]:
        failures = []
        # 通用硬性条件
        if not output.get("content") or len(str(output["content"]).strip()) < 10:
            failures.append("content 为空或过短(小于 10 字符)")
        if output.get("status") not in ("complete", "partial"):
            failures.append(f"无效的 status 值:{output.get('status')}")
        # 代码任务的专项硬性条件
        if task_type == "code_generation":
            code = output.get("code", "")
            if "TODO" in code and code.count("TODO") > 2:
                failures.append(f"代码包含 {code.count('TODO')} 个 TODO,超过允许上限 2 个")
            if any(pattern in code for pattern in ["password=", "secret=", "api_key="]):
                failures.append("代码包含硬编码的敏感信息")
        return failures

Step 2:用量化指标替代 LLM 的主观评判

def score_output_quantitatively(output: dict, task_type: str) -> dict[str, float]:
    """对输出做量化评分,每个维度独立评分,不用 LLM 主观判断。"""
    scores = {}
    
    content = str(output.get("content", ""))
    
    # 完整性:字符数是否达到最低要求
    min_length = {"code_generation": 100, "documentation": 200, "analysis": 150}
    scores["completeness"] = min(1.0, len(content) / min_length.get(task_type, 100))
    
    # 格式合规性:必填字段是否都有非空值
    required_fields = {"code_generation": ["code", "explanation"], "documentation": ["title", "body"]}
    fields = required_fields.get(task_type, [])
    present = sum(1 for f in fields if output.get(f) and str(output[f]).strip())
    scores["format_compliance"] = present / len(fields) if fields else 1.0
    
    # 各维度都必须达到最低分(不是平均分)
    MIN_SCORES = {"completeness": 0.8, "format_compliance": 1.0}
    for dim, min_score in MIN_SCORES.items():
        if scores.get(dim, 0) < min_score:
            raise PromotionFailed(f"维度 '{dim}' 得分 {scores[dim]:.2f} 低于最低要求 {min_score}")
    
    return scores

Step 3:维护一个「负向黑名单」

PROMOTION_BLOCKLIST = {
    "code_generation": [
        r'password\s*=\s*["\'][^"\']{3,}["\']',   # 硬编码密码
        r'TODO[:\s]',                               # 未完成的 TODO
        r'raise NotImplementedError',               # 未实现的函数
        r'# FIXME',                                 # 明确标记的问题
    ],
    "documentation": [
        r'Lorem ipsum',                             # 占位内容
        r'\[TODO\]',                                # 未填写的占位符
        r'<insert .+?>',                            # 模板未替换
    ]
}

def check_blocklist(output: dict, task_type: str) -> list[str]:
    content = str(output.get("content", "") or output.get("code", ""))
    violations = []
    for pattern in PROMOTION_BLOCKLIST.get(task_type, []):
        if re.search(pattern, content, re.IGNORECASE):
            violations.append(f"输出包含被禁止的模式:{pattern}")
    return violations

Step 4:对 Promotion 判据本身写测试

# 在 CI 里运行判据测试,确保已知劣质样本都被拦截
python -m pytest tests/test_promotion_criteria.py -v

# tests/test_promotion_criteria.py
def test_empty_conclusion_is_rejected():
    output = {"content": "", "status": "complete"}
    verdict, _ = checker.check(output, "analysis")
    assert verdict == PromotionVerdict.FAIL

def test_hardcoded_password_is_rejected():
    output = {"code": 'db_password = "mypassword123"', "status": "complete"}
    verdict, _ = checker.check(output, "code_generation")
    assert verdict == PromotionVerdict.FAIL

Step 5:建立 Promotion 准确率的持续监控

# 对每 100 次 Promotion 结果,抽取 10 条做人工复查
# 把「系统通过但人工标为劣质」的比例记录为 FPR (False Promotion Rate)
# FPR 超过 5% 时触发判据审查

预防建议

  • 用分层判据(硬性条件 + 软性条件 + 负向黑名单)替代单一阈值判断
  • 各评分维度独立设置最低分,任何维度低于最低分都 FAIL,不允许用平均分掩盖短板
  • 为每类任务维护独立的判据配置,不用统一的通用判据处理所有任务类型
  • 为 Promotion 判据维护「已知劣质样本」测试集,每次修改判据后跑回归测试
  • 对 LLM 评判的场景,提供带分数的 rubric 而不是开放式评估,并要求输出结构化评分
  • 定期(每周)抽样人工复查通过 Promotion 的输出,计算假阳性率
  • 新增任务类型时,必须同时定义该类型的 Promotion 判据,不允许复用已有的不匹配判据

常见问答 (FAQ)

Q: LLM 评判和规则评判哪个更准确? A: 对于有明确定义的质量标准(如字段非空、格式合规、无禁用模式),规则评判更可靠,因为它确定性、可测试、成本低。对于主观质量(如文章是否流畅、代码是否可维护),LLM 评判更擅长。实践中两者结合使用:规则做硬性条件检查,LLM 做软性质量评分。

Q: 怎么确定各维度的最低分阈值? A: 从小规模数据开始:收集 200 条有人工质量标注的输出,在上面测试不同阈值的精确率和召回率,选择「漏掉劣质输出率 小于 5%」且「误杀合格输出率 小于 10%」的阈值。阈值不是一次定好的,需要随着数据分布变化定期调整。

Q: Promotion 判据太严格导致大量输出被拦截,流水线吞吐量下降,怎么平衡? A: 先分析被拦截输出的失败原因分布。如果 80% 的失败都集中在同一个原因(如「代码包含 TODO」),可能是 Agent 的 prompt 问题,修复 prompt 通常比放松判据更有效。如果失败原因分散,说明任务本身的复杂度超出了 Agent 的能力范围,需要在任务设计层面优化,而不是降低质量标准。

Q: 如何处理「合格但非最优」的输出,例如可以工作但代码质量不高? A: 引入「WARN」状态(介于 PASS 和 FAIL 之间)。WARN 的输出可以被放行(不阻断流水线),但会触发告警,标记为「需要人工审查」。这样既不阻断流水线,又不让质量隐患完全被忽视。

相关阅读

标签: #AI 编程 #Agents #排查