Files
lhl 0b704fb07e 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 项架构审查整改全部完成
2026-08-12 23:24:15 +08:00

115 lines
3.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import types as t
import pytest
from genesis.inference.engine import InferenceEngine
from genesis.inference.token import approximate_token_count
from genesis.qa.guardrails import (
DEFAULT_MAX_QA_ROUNDS,
QALoopController,
resolve_qa_model,
)
from tests.inference_helpers import FakeLLMClient
class QAModels:
"""模拟 config.InferenceModelsprimary=deepseek / fallback=qwen。"""
def __init__(self):
self.primary = t.SimpleNamespace(name="deepseek-chat", provider="deepseek")
self.fallback = t.SimpleNamespace(name="qwen-max", provider="qwen")
class QAModelsNoFallback:
"""无 fallback 配置(覆盖 None 分支)。"""
def __init__(self):
self.primary = t.SimpleNamespace(name="deepseek-chat")
self.fallback = None
class QAModelsFallbackEmptyName:
"""fallback 存在但 name 为空字符串(覆盖 name 假值分支)。"""
def __init__(self):
self.primary = t.SimpleNamespace(name="deepseek-chat")
self.fallback = t.SimpleNamespace(name="")
# ---------- resolve_qa_modelOV6:强制 fallback,防自校验盲区) ----------
def test_resolve_qa_model_returns_fallback_name():
assert resolve_qa_model(QAModels()) == "qwen-max"
def test_resolve_qa_model_never_returns_primary():
"""关键护栏:QA 不得返回主模型名(否则 DeepSeek 校验 DeepSeek 盲区)。"""
models = QAModels()
assert resolve_qa_model(models) != getattr(models.primary, "name", None)
def test_resolve_qa_model_no_fallback_returns_none():
"""无 fallback 时返回 None,迫使调用方显式指定独立校验模型。"""
assert resolve_qa_model(QAModelsNoFallback()) is None
def test_resolve_qa_model_fallback_empty_name_returns_none():
"""fallback 存在但 name 为空时返回 None(不静默回退到 primary)。"""
assert resolve_qa_model(QAModelsFallbackEmptyName()) is None
def test_resolve_qa_model_none_models_returns_none():
assert resolve_qa_model(None) is None
# ---------- QALoopControllerOV6:循环边界防无限) ----------
def test_loop_default_max_rounds():
ctrl = QALoopController()
assert ctrl.max_rounds == DEFAULT_MAX_QA_ROUNDS
def test_loop_stops_at_max_rounds():
ctrl = QALoopController(max_rounds=3)
assert ctrl.can_continue()
ctrl.advance()
ctrl.advance()
ctrl.advance()
assert ctrl.round == 3
assert ctrl.is_exhausted()
assert not ctrl.can_continue()
def test_loop_exhausted_cannot_continue():
ctrl = QALoopController(max_rounds=1)
ctrl.advance()
assert ctrl.is_exhausted()
assert not ctrl.can_continue()
# ---------- 集成:QA 调用强制走 fallback 模型 ----------
@pytest.mark.anyio
async def test_qa_call_uses_fallback_not_primary():
client = FakeLLMClient([("ok", "QA 通过")])
models = QAModels()
eng = InferenceEngine(
client=client,
models=models,
estimator=approximate_token_count,
)
qa_model = resolve_qa_model(models)
assert qa_model == "qwen-max"
await eng.chat(
session_id="qa1",
prompt="请校验以下章节",
variables={},
model=qa_model,
)
called_models = [c["model"] for c in client.calls]
# 实际 LLM 调用必须用 fallback,绝不可落到 primary
assert called_models == ["qwen-max"]
assert "deepseek-chat" not in called_models