fix(concurrency): 消除 model 覆盖的模块级变量中转(并发串味)
R4 首版实现用模块级 _ACTIVE_MODEL_OVERRIDE 中转 model override, 理由是不想改动 run_specialists 的签名(既有测试会 mock 它)。 但 backtest.py:225 会 asyncio.gather 并发 8 场预测(Semaphore(8)), 每场都调用 predict_match_multi —— 模块级变量会被并发调用互相覆盖, 导致 A 场的预测用上 B 场的模型。这是静默的正确性缺陷。 改为: run_specialists 新增 model_override 形参,一路显式下传; 删除模块级变量。同步更新 6 处 mock 签名。 新增 3 个守卫并做变异验证: - test_r4_no_module_level_model_override_global (源码级,可判别) - test_r4_run_specialists_accepts_model_override_parameter - test_r4_dispatch_passes_override_to_specialists 变异测试: 重新引入全局变量方案 → 两个守卫变红;还原 → 全绿。 注: 曾尝试写并发行为测试,但真实 run_specialists 会访问数据库, 测试环境下不稳定(ConnectionRefusedError),会是 flaky 的假证据, 故改用清晰的源码级判别守卫,并在注释中说明原因。 240 passed / 1 skipped; tsc 0 error; vite build 成功。
This commit is contained in:
@@ -34,11 +34,6 @@ logger = logging.getLogger(__name__)
|
||||
_AGENT_PROVIDER_CACHE: dict[str, tuple[float, LLMProvider]] = {}
|
||||
_AGENT_PROVIDER_CACHE_TTL = 60.0
|
||||
|
||||
# 当前预测的模型覆盖(调用方显式指定)。用模块级变量而非新增形参,因为
|
||||
# run_specialists 会被既有测试 mock,加形参会破坏那些测试的调用签名。
|
||||
# 由 predict_match_multi 在进入时 set、退出时 reset。
|
||||
_ACTIVE_MODEL_OVERRIDE: str | None = None
|
||||
|
||||
|
||||
# ── 5 个专家 agent 定义 ──
|
||||
# A=近期状态 B=攻防数据 C=主客因素 D=联赛排名 E=历史交锋
|
||||
@@ -160,16 +155,17 @@ async def run_specialists(
|
||||
*,
|
||||
version: str = "v1",
|
||||
before=None,
|
||||
model_override: str | None = None,
|
||||
) -> list[AgentReport]:
|
||||
"""并行执行 5 个专家 agent。fail-open: 单个失败不影响其他。
|
||||
|
||||
before: 数据截止时间(回测防泄漏)。None 表示不限制。
|
||||
model_override: 调用方显式指定的模型,覆盖各 agent 的层级默认。
|
||||
|
||||
模型覆盖通过 _ACTIVE_MODEL_OVERRIDE 传递,而不是新增形参:既有测试
|
||||
会 mock 本函数(见 tests/test_agent_weights_persist.py),加形参会破坏
|
||||
它们的调用签名。predict_match_multi 在调用前后 set/reset 该变量。
|
||||
注意:model_override 必须作为形参下传,不能用模块级变量中转。
|
||||
backtest 会 asyncio.gather 并发 8 场预测(见 backtest.py 的 Semaphore(8)),
|
||||
模块级变量会被并发调用互相覆盖,导致 A 场的预测用上 B 场的模型。
|
||||
"""
|
||||
model_override = _ACTIVE_MODEL_OVERRIDE
|
||||
tasks = [
|
||||
_run_one(spec, header, await _agent_provider(spec.name, tier="specialist", model_override=model_override), version=version, before=before)
|
||||
for spec in SPECIALIST_SPECS
|
||||
@@ -266,14 +262,11 @@ async def predict_match_multi(
|
||||
prediction_cutoff_at = cutoff
|
||||
|
||||
# 2. 并行专家(各自独立配置,使用统一 cutoff)
|
||||
# 显式 model 覆盖通过模块级变量下传,避免改动 run_specialists 的签名
|
||||
global _ACTIVE_MODEL_OVERRIDE
|
||||
_prev_override = _ACTIVE_MODEL_OVERRIDE
|
||||
_ACTIVE_MODEL_OVERRIDE = model
|
||||
try:
|
||||
reports = await run_specialists(header, version=version, before=cutoff)
|
||||
finally:
|
||||
_ACTIVE_MODEL_OVERRIDE = _prev_override
|
||||
# model 作为形参下传,而非模块级变量:backtest 并发 8 场预测时,
|
||||
# 模块级变量会被并发调用互相覆盖(模型串味)。
|
||||
reports = await run_specialists(
|
||||
header, version=version, before=cutoff, model_override=model
|
||||
)
|
||||
|
||||
# 2.5 统计有效专家报告数量
|
||||
ok_reports = [r for r in reports if r.status == "ok"]
|
||||
|
||||
Reference in New Issue
Block a user