feat(runner): 注入 tool_dispatch_factory/prompt_builder_factory + fail-fast
Runner.__init__ 新增 2 个可选参数: - tool_dispatch_factory: 工具调度工厂 - prompt_builder_factory: prompt 构建工厂 infer/eval/train 模式缺少工厂时 fail-fast 抛 ValueError。 _make_tool_dispatch_fn/_make_prompt_builder 优先使用注入工厂。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+28
-6
@@ -449,6 +449,8 @@ class Runner:
|
|||||||
evolve_llm: 进化用 LLMProvider(thinking=True)。
|
evolve_llm: 进化用 LLMProvider(thinking=True)。
|
||||||
vlm: VLMProvider。
|
vlm: VLMProvider。
|
||||||
telemetry: 遥测记录端口。
|
telemetry: 遥测记录端口。
|
||||||
|
tool_dispatch_factory: 工具调度工厂(infer/eval/train 模式必传)。
|
||||||
|
prompt_builder_factory: prompt 构建工厂(infer/eval/train 模式必传)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -459,12 +461,28 @@ class Runner:
|
|||||||
evolve_llm: LLMProvider,
|
evolve_llm: LLMProvider,
|
||||||
vlm: VLMProvider,
|
vlm: VLMProvider,
|
||||||
telemetry: TelemetryRecorder,
|
telemetry: TelemetryRecorder,
|
||||||
|
tool_dispatch_factory: Any | None = None,
|
||||||
|
prompt_builder_factory: Any | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._config = config
|
self._config = config
|
||||||
self._llm = llm
|
self._llm = llm
|
||||||
self._evolve_llm = evolve_llm
|
self._evolve_llm = evolve_llm
|
||||||
self._vlm = vlm
|
self._vlm = vlm
|
||||||
self._telemetry = telemetry
|
self._telemetry = telemetry
|
||||||
|
self._tool_dispatch_factory = tool_dispatch_factory
|
||||||
|
self._prompt_builder_factory = prompt_builder_factory
|
||||||
|
|
||||||
|
# fail-fast: 需要推理的模式必须注入工厂
|
||||||
|
if config.mode in {"infer", "eval", "train"} and (
|
||||||
|
tool_dispatch_factory is None or prompt_builder_factory is None
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"mode={config.mode!r} 需要 tool_dispatch_factory 和 "
|
||||||
|
f"prompt_builder_factory,但收到 "
|
||||||
|
f"tool_dispatch_factory={tool_dispatch_factory!r}, "
|
||||||
|
f"prompt_builder_factory={prompt_builder_factory!r}"
|
||||||
|
)
|
||||||
|
|
||||||
self._ensure_workspace()
|
self._ensure_workspace()
|
||||||
self._paths: ResolvedPaths = resolve_paths(config.workspace_dir)
|
self._paths: ResolvedPaths = resolve_paths(config.workspace_dir)
|
||||||
|
|
||||||
@@ -2062,22 +2080,26 @@ class Runner:
|
|||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|
||||||
def _make_tool_dispatch_fn(self, *, skills_dir: Path | None = None):
|
def _make_tool_dispatch_fn(self, *, skills_dir: Path | None = None):
|
||||||
"""构造工具调度函数(由子类或 main.py 覆盖)。"""
|
"""构造工具调度函数(优先使用注入的工厂,否则 noop 降级)。"""
|
||||||
|
if self._tool_dispatch_factory is not None:
|
||||||
|
return self._tool_dispatch_factory(skills_dir=skills_dir)
|
||||||
|
|
||||||
|
# noop fallback:diagnose/promote 等不需要推理的模式
|
||||||
async def _noop_dispatch(tool_name: str, args: dict, *, context: dict) -> str:
|
async def _noop_dispatch(tool_name: str, args: dict, *, context: dict) -> str:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(f"工具 {tool_name} 调度未配置")
|
||||||
f"工具 {tool_name} 调度未配置(需由 main.py 注入 tool_dispatch_fn)"
|
|
||||||
)
|
|
||||||
|
|
||||||
return _noop_dispatch
|
return _noop_dispatch
|
||||||
|
|
||||||
def _make_prompt_builder(
|
def _make_prompt_builder(
|
||||||
self, *, skills_dir: Path | None = None, prompts_dir: Path | None = None
|
self, *, skills_dir: Path | None = None, prompts_dir: Path | None = None
|
||||||
):
|
):
|
||||||
"""构造 prompt 构建函数(由子类或 main.py 覆盖)。"""
|
"""构造 prompt 构建函数(优先使用注入的工厂,否则 noop 降级)。"""
|
||||||
|
if self._prompt_builder_factory is not None:
|
||||||
|
return self._prompt_builder_factory(skills_dir=skills_dir, prompts_dir=prompts_dir)
|
||||||
|
|
||||||
|
# noop fallback:diagnose/promote 等不需要推理的模式
|
||||||
def _noop_builder(qa: GeneratedQuestion) -> tuple[str, str]:
|
def _noop_builder(qa: GeneratedQuestion) -> tuple[str, str]:
|
||||||
raise NotImplementedError("prompt_builder 未配置(需由 main.py 注入)")
|
raise NotImplementedError("prompt_builder 未配置")
|
||||||
|
|
||||||
return _noop_builder
|
return _noop_builder
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.harness.runner import (
|
from app.harness.runner import (
|
||||||
|
Runner,
|
||||||
_apply_batch_correctness,
|
_apply_batch_correctness,
|
||||||
_batch_from_ids,
|
_batch_from_ids,
|
||||||
_build_comparison_pairs,
|
_build_comparison_pairs,
|
||||||
@@ -680,3 +681,94 @@ class TestCooldownDecrement:
|
|||||||
|
|
||||||
cooldown = {t: n - 1 for t, n in cooldown.items() if n - 1 > 0}
|
cooldown = {t: n - 1 for t, n in cooldown.items() if n - 1 > 0}
|
||||||
assert cooldown == {}
|
assert cooldown == {}
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# factory 注入(Task 4)
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunnerFactoryInjection:
|
||||||
|
"""Runner 构造时 tool_dispatch_factory / prompt_builder_factory 注入检查。"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _base_config(tmp_path: Path, *, mode: str = "infer", **overrides):
|
||||||
|
"""构造 RunConfig,所有必填字段都给默认值。"""
|
||||||
|
from app.harness.config import RunConfig
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
"workspace_dir": tmp_path,
|
||||||
|
"store_dir": tmp_path,
|
||||||
|
"mode": mode,
|
||||||
|
"concurrency": 1,
|
||||||
|
"max_steps": 5,
|
||||||
|
"skill_mode": "none",
|
||||||
|
"n_samples": 0,
|
||||||
|
"questions": "benchmarks/Video-MME",
|
||||||
|
"skills_version": "v1",
|
||||||
|
"prompts_version": "v1",
|
||||||
|
"epochs": 1,
|
||||||
|
"diag_size": 10,
|
||||||
|
"diag_correct_ratio": 0.5,
|
||||||
|
"val_size": 24,
|
||||||
|
"val_correct_ratio": 0.5,
|
||||||
|
"edit_budget_start": 5,
|
||||||
|
"edit_budget_end": 2,
|
||||||
|
"batch_size": 5,
|
||||||
|
"min_class_per_batch": 2,
|
||||||
|
"eval_min_per_class": 2,
|
||||||
|
"early_stop_patience": 3,
|
||||||
|
"test_size": 10,
|
||||||
|
"use_slow_momentum": False,
|
||||||
|
"gate_e_confirm": 20.0,
|
||||||
|
"gate_e_provisional": 3.0,
|
||||||
|
"gate_w_net_min": 2,
|
||||||
|
"gate_delta_min": 0.02,
|
||||||
|
"gate_lambda_dir": -0.642,
|
||||||
|
"gate_e_rollback": 10.0,
|
||||||
|
"gate_block": 8,
|
||||||
|
"gate_n_max": 40,
|
||||||
|
"gate_p_low": 0.05,
|
||||||
|
"gate_p_high": 0.95,
|
||||||
|
"gate_probe_quota": 0.2,
|
||||||
|
"gate_gamma_decay": 0.9,
|
||||||
|
"gate_cooldown_steps": 2,
|
||||||
|
"gate_guard_err": 0.10,
|
||||||
|
"skill_update_mode": "patch",
|
||||||
|
"appendix_consolidate_threshold": 6,
|
||||||
|
}
|
||||||
|
defaults.update(overrides)
|
||||||
|
return RunConfig(**defaults)
|
||||||
|
|
||||||
|
def test_infer_mode_missing_factory_raises(self, tmp_path: Path) -> None:
|
||||||
|
"""infer 模式缺少工厂时抛出 ValueError。"""
|
||||||
|
config = self._base_config(tmp_path, mode="infer")
|
||||||
|
with pytest.raises(ValueError, match="tool_dispatch_factory"):
|
||||||
|
Runner(
|
||||||
|
config,
|
||||||
|
llm=MagicMock(),
|
||||||
|
evolve_llm=MagicMock(),
|
||||||
|
vlm=MagicMock(),
|
||||||
|
telemetry=MagicMock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_diagnose_mode_allows_none_factory(self, tmp_path: Path) -> None:
|
||||||
|
"""diagnose 模式不需要工厂,允许 None。"""
|
||||||
|
ws = tmp_path / "ws"
|
||||||
|
ws.mkdir()
|
||||||
|
(ws / "manifest.json").write_text(
|
||||||
|
'{"name":"ws","created_at":"","store":"../store",'
|
||||||
|
'"current":{"videos":"v","questions":"q","skills":"s","prompts":"p"},'
|
||||||
|
'"history":[]}'
|
||||||
|
)
|
||||||
|
config = self._base_config(tmp_path, mode="diagnose", workspace_dir=ws, run_id="test_run")
|
||||||
|
# 不应抛出 ValueError
|
||||||
|
runner = Runner(
|
||||||
|
config,
|
||||||
|
llm=MagicMock(),
|
||||||
|
evolve_llm=MagicMock(),
|
||||||
|
vlm=MagicMock(),
|
||||||
|
telemetry=MagicMock(),
|
||||||
|
)
|
||||||
|
assert runner._tool_dispatch_factory is None
|
||||||
|
assert runner._prompt_builder_factory is None
|
||||||
|
|||||||
Reference in New Issue
Block a user