feat: QA 护栏 + fallback 路由(T15 架构审查整改收官,OV6)

- src/genesis/qa/guardrails.py: resolve_qa_model() 强制 QA 走 fallback 模型(防
  DeepSeek 校验 DeepSeek 自校验盲区);QALoopController(max_rounds=3) 约束
  「QA→Writer修正→重校验」循环边界防无限
- 集成测试验证 QA 调用实际落到 fallback 而非 primary(FakeLLMClient 记录 model)
- design.md §7.4 补 QA 护栏说明(循环边界 + 独立校验模型)
- TDD:257 passed / 100.00% 覆盖,17 项架构审查整改全部完成
This commit is contained in:
lhl
2026-08-12 23:24:15 +08:00
parent a1336f6dd3
commit 0b704fb07e
5 changed files with 182 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""QA Agent 模块:质量校验与护栏(T15,OV6)。"""
+56
View File
@@ -0,0 +1,56 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
# QA 校验反馈循环的最大轮次上限(OV6:防止「QA→Writer修正→重新QA」无限循环)
DEFAULT_MAX_QA_ROUNDS = 3
def resolve_qa_model(models: Any | None) -> str | None:
"""解析 QA 校验应使用的模型名。
OV6 修复:QA 校验必须走 fallback 模型(独立模型族),避免「主模型自校验盲区」
(如 DeepSeek 生成、DeepSeek 校验 → 同族模型盲点,难以发现自身偏误)。
返回 fallback 模型名;若无 fallback 配置则返回 None,迫使调用方显式指定
一个独立于生成模型的校验模型(而非静默回退到 primary)。
"""
if models is None:
return None
fallback = getattr(models, "fallback", None)
if fallback is not None:
name = getattr(fallback, "name", None)
if name:
return name
return None
@dataclass
class QALoopController:
"""QA 反馈循环护栏:约束「QA校验→Writer修正→重新校验」的最大轮次。
防止 OV6 指出的无边界循环风险:超过 max_rounds 后必须停止循环,
转为上报人工或降级处理,不得无限重试。
"""
max_rounds: int = DEFAULT_MAX_QA_ROUNDS
_round: int = field(default=0, init=False)
@property
def round(self) -> int:
"""当前已执行的轮次(0 表示尚未开始)。"""
return self._round
def can_continue(self) -> bool:
"""是否还能进入下一轮 QA 校验。"""
return self._round < self.max_rounds
def advance(self) -> int:
"""推进一轮,返回推进后的当前轮次(从 1 开始计数)。"""
self._round += 1
return self._round
def is_exhausted(self) -> bool:
"""已达上限:必须停止循环并上报,禁止继续重试。"""
return self._round >= self.max_rounds