裁定三落地:accepted = 机械预筛 ∧ 独立模型玩法判定,九门 pass 降为「未见明显死」预筛。 - 波2a 玩法判定器(cheap_verify.py):读 brief+run-summary+全程截图(含新增局中连拍 midplay-1..N,play.cdp.cjs 只读并发拍、不进任何门判据),布尔裁 broken/hollow/off_brief, fail-closed;判定真相层落 evidence/judge.json + verdict.json 写回 accepted/judge 段; 金标夹具 fixtures/judge-golden/ + judge_golden.py 校准跑批 - 波2b 拆残笼:check 词法 tokenizer 替换 stripCode 正则(字符串内 // 假阴根修,292 在册 源零差异);prompt v1.6.3 外科清洗(两层奖励→纯设计语/删盲驱动器围设计/删熔断恐吓), registry 热取==内置逐字节一致(cheap_roles 内置回退同步);edit_file 空白归一+近似片段提示 - 判定器补修:空输出有界重试一次(glm 思考吃光 max_tokens 时 content 空)+重试放大公式 反缩 bug 修(旧 min(×2,6000) 大 base 反缩)+finishReason 落盘;imagesSeen 模型自报+ 盲检微扰重掷、两掷均盲=仪器故障 degraded(闲鱼中转静默丢图实锤,不再错杀 hollow) - 判定档切 MiniMax-M3(创始人 2026-07-10 拍板,裁定三①边界随 v2 plan 修订): generation.yaml judge.model+max_tokens=202752(512K 网关实探 400 拒,取实探上限); 3 例盲案 M3 重判全 accept、¥0.026/局 - 三路消费对齐:CLI 预筛语义修(cheap_studio 喂 verdict.pass 而非 finished,违裁定三的 放行已纠)/Service 路 apply_gameplay_judge 显式 prefilter 参数/result_out 权威改读 accepted(无键回落预筛,旧产物兼容)+trace.gameplayJudge additive 透传; tier2 gate_judge/genconfig 同步判定配置与消费 测试:test_gameplay_judge 33 项新增,全仓 pytest 480 绿;真判定闭环 accept ¥0.061。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
484 lines
30 KiB
Python
484 lines
30 KiB
Python
"""genconfig.py —— tier2 生成配置层加载器(把散在 Python 里的生成器旋钮收敛成一份外部 YAML,运行时读)。
|
||
|
||
【这份解决什么问题】
|
||
"调生成、优化过门率"是 tier2 当前最高频的迭代循环:改一个 max_iters、抬一档 thinking_budget、调一条
|
||
退路树阈值,跑一批看过门率有没有涨 20%。在此之前这些旋钮散在 6+ 个 Python 文件里写死,改一个值要
|
||
改码、再同步到 mini-desktop 重跑。本模块把它们集中成 tier2/config/generation.yaml,**运行时读**:
|
||
调生成 = 改 YAML 重跑,不改码、不重同步。这正是图说 B 族「配置注册表(唯一事实源 / 版本化 / 运行时读)」
|
||
在生成器旋钮这一面的最小落地——配置是真相,代码只是按 id 取值的消费方。
|
||
|
||
【取值优先级(运行时读,失败绝不中断主链)】
|
||
get(area, key, default) 每次调用按以下顺序取值,任一级取到即用:
|
||
⓪ Nacos 热源(配置控制面阶段二接入,默认不启用):worker 启动期若由工厂 genconfig_nacos
|
||
.build_and_attach_hot_source() attach 了一个 NacosHotConfig(env TIER2_GENCONFIG_NACOS 启用),
|
||
则最先查它——配置中心激活的预算/门阈值经 Nacos 长轮询推送热读进来,改一次配置中心、下一次生成
|
||
即读到新值,不重启。未启用(默认)时本级不存在,取值链 = 原三级,行为与阶段二前逐字节一致。
|
||
热源以复合键 "area.key" 存值(genconfig 是 (area,key) 二维,复合键无歧义扁平化);缺该键则继续
|
||
往下回落,绝不引入第二默认值(热源只给「有就用」的值,缺失全交下面 env/YAML/内置默认)。
|
||
① env 单点覆盖:环境变量 TIER2_GEN__<AREA>__<KEY>(全大写、双下划线分隔)——临时压一个值不必改文件,
|
||
适合「这一批我想试 max_iters=60」这种一次性实验(改完 unset 即恢复)。
|
||
② 外部 YAML:tier2/config/generation.yaml(路径可被 env TIER2_GENCONFIG 指向别处)的 area.key。
|
||
③ 内置默认 _BUILTIN_DEFAULTS[area][key]:本模块自带的一份与现行硬编码值字节一致的副本——YAML 缺失 /
|
||
读不出 / 脏值时的兜底,保证「配置文件没了也能按现行行为跑」。
|
||
④ 调用方传入的 default:连内置默认都没登记这个 key 时的最后防线(调用方按惯例传现行硬编码值)。
|
||
|
||
【默认行为字节不变(本相红线)】
|
||
外置后,只要没人去改 YAML / 没设 env,每个旋钮取到的值必须 == 改造前的硬编码值。为此:
|
||
- generation.yaml 里每个值 = 当前硬编码值(逐项对齐);
|
||
- _BUILTIN_DEFAULTS 里每个值 = 当前硬编码值(逐项对齐,作 YAML 失效兜底);
|
||
- 各调用点 get(..., default=现值):default 也填现值。
|
||
三处同值 ⇒ 无论哪一级取到,结果都是现值。改 YAML 一个值即可让它流通到取值点(运行时读)。
|
||
|
||
【best-effort 铁律(对齐项目 client.py / newapi_pricing.py 同款纪律)】
|
||
读文件 / 解析 YAML / env 转型的任何异常都不抛、不中断生成主链:读不到或脏 → 落内置默认 + 一次性告警。
|
||
YAML 只在「首次访问 + 文件 mtime 变化」时重读(运行时读但不必每次 get 都打开文件;改了文件下一次 get 自动生效)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import threading
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
# YAML 解析:pyyaml 是声明依赖(requirements.txt),但仍 best-effort 兜底——import 失败则整层降级为
|
||
# 「只用内置默认 + env 覆盖」,绝不让一个缺失的解析库阻断生成(读不到 = 用默认,与脏文件同处置)。
|
||
try:
|
||
import yaml as _yaml # type: ignore
|
||
except Exception: # pragma: no cover —— 极端环境缺 pyyaml:降级为内置默认 + env(不抛)
|
||
_yaml = None
|
||
|
||
|
||
# ── 配置文件默认路径(可被 env TIER2_GENCONFIG 指向别处)─────────────────────────────
|
||
# 本文件在 tier2/gen-worker/worker/genconfig.py → 上溯 2 级到 gen-worker/,再上 1 级到 tier2/,
|
||
# 配置在 tier2/config/generation.yaml。与 archetypes.py / run.py 同源的「从 __file__ 推 tier2/ 根」口径。
|
||
_TIER2_DIR = Path(__file__).resolve().parents[2]
|
||
_DEFAULT_CONFIG_PATH = _TIER2_DIR / "config" / "generation.yaml"
|
||
|
||
# env 覆盖前缀:TIER2_GEN__<AREA>__<KEY>(双下划线分隔 area / key;全大写)。
|
||
# 例:压一档单写轮数 → export TIER2_GEN__ITERATION__WRITER_MAX_ITERS=60。
|
||
_ENV_PREFIX = "TIER2_GEN__"
|
||
|
||
|
||
# ════════════════════════════════════════════════════════════════════════════
|
||
# 内置默认(_BUILTIN_DEFAULTS)—— 与现行硬编码值【字节一致】的兜底副本
|
||
# 作用:generation.yaml 读不到 / 脏 / 缺某 key 时,按这里的值兜底,保证默认行为不漂移。
|
||
# ⚠️ 维护纪律:这里每个值必须与对应源文件里改造前的硬编码值严格相等;改默认值优先改 YAML,
|
||
# 这份兜底只在 YAML 失效时生效,不应与 YAML 的「现值」语义分叉。
|
||
# 分区(area)与各源文件一一对应,便于查改:
|
||
# model ← worker/config.py(模型名 / max_tokens / thinking / 历史压缩 / 取价估算)
|
||
# budget ← worker/middleware.py(¥ 硬闸 / 四道熔断 / 软刹 / 折价倍率 / 首调估算 token)
|
||
# iteration ← worker/agent_loop/studio.py(单写轮数 / 外层 resume 上限)
|
||
# design_team ← worker/agent_loop/design_team.py(leader 轮数 / 单专家上限 / 团队超时)
|
||
# gates ← worker/fallback_tree.py(退路树四阈值)+ harness 门阈值(人可玩 / 渲染反映)
|
||
# ════════════════════════════════════════════════════════════════════════════
|
||
_BUILTIN_DEFAULTS: dict[str, dict[str, Any]] = {
|
||
# ── model:模型客户端构建旋钮(worker/config.py)──
|
||
"model": {
|
||
"default_model_name": "MiniMax-M3", # model_name_from_env 默认(M3 路)
|
||
"deepseek_flash": "deepseek-v4-flash", # DEEPSEEK_FLASH 便宜主力档
|
||
"deepseek_pro": "deepseek-v4-pro", # DEEPSEEK_PRO 强便宜救场档
|
||
# ── 强基线档(C3;A 门「M3 证路(软)」用强模型跑同一条 studio/控制面线,把「路不通」与「便宜模型太弱」分开)──
|
||
# Opus = Anthropic 原生经 new-api(与 M3 同路);Fable 同协议但 **不假设网关一定可用**
|
||
# (build_baseline_model 取不到即诚实报错,不静默)。退路树 fallback_tree.STRONG_BASELINE_KEYS 读 "Opus"/"Fable" 当 key,
|
||
# 这里两键存的是 new-api 上的真实模型名(消费方:worker/config.py build_baseline_model 的 baseline_name → 真名映射)。
|
||
"baseline_opus": "claude-opus-4-8", # 强基线 Opus(Anthropic 原生路;A 门软证路主用)
|
||
"baseline_fable": "claude-fable-5", # 强基线 Fable(列出但不假设网关可用;取不到诚实报错)
|
||
"max_tokens": 16000, # build_model / build_model_openai 默认输出上限
|
||
"thinking_enable": True, # build_model 默认开 thinking 分离
|
||
"thinking_budget": 8000, # build_model 默认 thinking 预算(须 < max_tokens)
|
||
"max_retries": 2, # 网关偶发 502 突发兜底重试次数
|
||
"ctx_trigger_ratio": 0.8, # build_context_config 触发压缩比例
|
||
"ctx_reserve_ratio": 0.1, # build_context_config 保留最近上下文比例
|
||
},
|
||
# ── budget:¥ 累进硬闸 + 四道熔断 + 软刹(worker/middleware.py)──
|
||
"budget": {
|
||
# 富档单局 ¥ 软停线(soft 档语义;hard 档=单段硬线)。创始人 2026-07-03 一次性裁决「预算两段式」:
|
||
# 软停线维持 ¥50(富档)/¥10(便宜档,见 cheap_rmb_hard_limit),越线只许收尾类动作;
|
||
# 整局 RMB 硬地板 = 软停线 × rmb_hard_floor_ratio(1.5 → 富档 ¥75 / 便宜档 ¥15),middleware 强制。
|
||
# 单位经济或模型单价大变即复审(裁决记录 = 2026-07-03 重推演档 §5)。
|
||
"rmb_hard_limit": 50.0,
|
||
# 两段式硬地板倍率:硬地板 = 软停线 × 此值(soft 档 fail-closed 封上界;hard 档不用它)。
|
||
"rmb_hard_floor_ratio": 1.5,
|
||
# 便宜档单局 ¥ 软停线(创始人裁决 ¥10;硬地板 = ×1.5 = ¥15)。
|
||
# 生产 Service(cheap_service_app)与 CLI(cheap_studio)经 cheap_budget.build_cheap_breaker 同源读此键。
|
||
"cheap_rmb_hard_limit": 10.0,
|
||
"max_tool_calls": 100, # step_cap 步数硬顶(2026-07-04 创始人令 60→100,详注见 generation.yaml)
|
||
"max_model_calls": 120, # budget 预算闸(随步顶等比 80→120)
|
||
"wall_timeout_s": 2700.0, # timeout 整 reply 墙钟超时(随步顶 1800→2700)
|
||
"step_timeout_s": 420.0, # timeout 单步静默超时(秒)
|
||
"stuck_repeat_threshold": 4, # stuck 连续同一失败签名达此次数判死圈
|
||
"soft_ratio": 0.8, # 软刹触发比例(达 soft_ratio×硬顶注入收敛提醒)
|
||
"group_ratio": 1.0, # new-api 分组倍率(成本折算用)
|
||
"est_prompt_tokens": 12000, # _estimate_call_rmb 首调保守估:prompt token 量级
|
||
"est_completion_tokens": 2000, # _estimate_call_rmb 首调保守估:补全 token 量级
|
||
},
|
||
# ── iteration:单写 ReAct 轮数 + 外层有界 resume(worker/agent_loop/studio.py)──
|
||
"iteration": {
|
||
"writer_max_iters": 60, # run_studio 单写 ReAct 最大轮数(2026-07-04 创始人令 40→60,详注见 generation.yaml)
|
||
"max_resumes": 6, # 外层有界自纠 resume 上限(agent 过早停下再踹回去续修)
|
||
},
|
||
# ── design_team:阶段 1 工作室星形多 agent 设计预算(worker/agent_loop/design_team.py)──
|
||
"design_team": {
|
||
"leader_max_iters": 12, # leader ReAct 总轮数上限(调专家 + 汇总天花板)
|
||
"per_expert_cap": 2, # 单专家被 leader 调用次数上限(防反复刷同一专家)
|
||
"timeout_s": 420.0, # 整团队墙钟硬超时(秒;2026-07-04 F-2 R1 实证 240→420,详注见 generation.yaml)
|
||
},
|
||
# ── gates:退路树四阈值(fallback_tree.py)+ harness 门阈值(play-phaser.cdp.cjs)──
|
||
# 注:harness 是 Node,这些值经导出 JSON / env 注入到 .cjs(见下方 export_for_harness 与 .cjs 注释);
|
||
# Python 侧 fallback_tree 直接 import genconfig 读。
|
||
"gates": {
|
||
"qpass_go_min": 0.40, # 退路树 Q1:v4-pro 过门率 ≥ 此值 → GO(QPASS_GO_MIN)
|
||
"qpass_floor_max": 0.20, # 退路树 Q4:便宜档全线 < 此值 → 全线崩(QPASS_FLOOR_MAX)
|
||
"qpass_strong_baseline_min": 0.0, # 退路树 Q4:强基线过门率 > 此值 → 能过(QPASS_STRONG_BASELINE_MIN)
|
||
"concentration_ratio": 0.50, # 退路树 Q2/Q3:某桶失败占比 ≥ 此值 → 集中(CONCENTRATION_RATIO)
|
||
"human_min_patience_ms": 12000, # harness 人可玩 advisory:订单耐心人类最小窗口(HUMAN_MIN_PATIENCE_MS)
|
||
"color_dist_min": 18, # harness render-reflects-state:色差判反映状态阈值(COLOR_DIST_MIN)
|
||
"var_delta_min": 120, # harness render-reflects-state:方差增量判反映状态阈值(VAR_DELTA_MIN)
|
||
},
|
||
# ── judge:独立模型玩法地板判定(cheap-worker/cheap_verify.apply_gameplay_judge;W-AXIS 波2 裁定三)──
|
||
# 机械九门降位为预筛,玩法地板由独立多模态档看真玩证据裁 broken/hollow/off-brief,阻断放行(ok=预筛∧判定)。
|
||
"judge": {
|
||
"blocking": True, # 判定是否阻断放行(整体回退位:false=只观测、ok 仍=预筛)
|
||
"model": "glm-5.2", # 判定模型(new-api 唯一实测多模态档;2026-07-09 probe 坐实 M3/MiniMax/deepseek 不吃图)
|
||
"max_tokens": 1500, # 判定输出上限
|
||
"timeout_s": 120.0, # 判定 LLM 超时(秒);超时 → fail-closed
|
||
"max_frames": 6, # 最多喂几帧证据截图(首帧+局中连拍+局末)
|
||
"cost_target_rmb": 0.3, # 单局判定成本目标(¥;仅记账对照,不阻断)
|
||
},
|
||
# 注:business-sim driver 参数(steps/stepMs/winThreshold/bankruptSteps/streakLose)不在本配置层——
|
||
# 曾登记过一个 "archetype" 区声明这五个键,但生成路(archetypes._business_sim_gate_spec /
|
||
# run.DEFAULT_BUSINESS_SIM_PLAY_SPEC)从不读它、真值一直硬编码在那两处 = 死旋钮(改配置无效、误导)。
|
||
# 2026-07-05 清账删除该假声明,两处硬编码为单一权威源。若放量期确需热调这几个 driver 数,再按 D11 的
|
||
# 「读 genconfig、硬编码值作回落」范式补接线(那时这里重新加区、生成路加 genconfig.get 读取)。
|
||
}
|
||
|
||
|
||
# ── 运行时读状态(缓存 + mtime 守门;线程安全)──────────────────────────────────────
|
||
# _cache 存解析后的 YAML dict;_cache_mtime / _cache_path 记上次读的文件特征,用于判定是否需重读
|
||
# (运行时读 = 改了文件下次 get 自动重读,但不必每次 get 都开文件)。_warned 防重复告警刷屏。
|
||
_lock = threading.Lock()
|
||
_cache: dict[str, dict[str, Any]] | None = None
|
||
_cache_mtime: float | None = None
|
||
_cache_path: str | None = None
|
||
_warned: set[str] = set()
|
||
|
||
|
||
# ── Nacos 热源(配置控制面阶段二步骤4:genconfig 读取后端提升到 Nacos)────────────────────
|
||
# 工厂 genconfig_nacos.build_and_attach_hot_source() 在 worker 启动期把一个 NacosHotConfig 实例
|
||
# attach 进这个进程级单例槽;get() 取值链最前端先查它(见 get() 的 ⓪ 层)。未 attach(默认)→ get()
|
||
# 完全走原链(env→YAML→内置默认→调用方 default),行为与阶段二前逐字节一致。
|
||
# 为什么是进程级单例、且 genconfig 不认档:cheap Service 与 tier2 Service 是两个独立进程,却都
|
||
# `from worker import genconfig` 共用本模块。两档隔离不靠 genconfig 区分,而靠【各档进程 attach 各档
|
||
# dataId 的热源】——cheap 进程 attach cheap dataId 的 NacosHotConfig(store 里是 budget.cheap_* 键)、
|
||
# tier2 进程 attach tier2 dataId 的(store 里是 budget.rmb_hard_limit 等)。genconfig 只查「当前进程
|
||
# attach 的那个热源」,故同一份 get() 代码在两档进程里各取各的 dataId,天然不串档。
|
||
_hot_source: Any = None
|
||
|
||
# sentinel:区分「热源里没有这个键」与「热源里键值恰好是 None」。热源缺键必须继续回落到 env/YAML/
|
||
# 内置默认,不能被当成一个 None 值截断(否则会吞掉下面所有回落级)。
|
||
_MISSING: Any = object()
|
||
|
||
|
||
def attach_hot_source(source: Any) -> None:
|
||
"""worker 启动期由工厂注入 Nacos 热源(NacosHotConfig 实例);此后 get() 最先查它。
|
||
|
||
幂等可重入:重复 attach 覆盖为最新源。source 只需满足 get(key, default)->value 契约
|
||
(NacosHotConfig 即是),genconfig 不依赖其内部实现——故单测可注入任意 fake 源。
|
||
"""
|
||
global _hot_source
|
||
_hot_source = source
|
||
|
||
|
||
def detach_hot_source() -> None:
|
||
"""卸载热源,get() 回落纯本地链(env→YAML→内置默认→调用方 default)。
|
||
|
||
这是设计 §3.6/§6「回退开关」在进程内的一处落点:热源出问题时切回纯本地,调用面一行不改。
|
||
"""
|
||
global _hot_source
|
||
_hot_source = None
|
||
|
||
|
||
def _warn_once(tag: str, msg: str) -> None:
|
||
"""同一 tag 只告警一次(防 best-effort 路径在循环里刷屏);可追溯日志,带 [tier2-genconfig] 前缀。"""
|
||
if tag in _warned:
|
||
return
|
||
_warned.add(tag)
|
||
print(f"[tier2-genconfig] {msg}", flush=True)
|
||
|
||
|
||
def config_path() -> Path:
|
||
"""当前生效的配置文件路径:env TIER2_GENCONFIG 指定 > 默认 tier2/config/generation.yaml。"""
|
||
p = os.environ.get("TIER2_GENCONFIG")
|
||
return Path(p) if p else _DEFAULT_CONFIG_PATH
|
||
|
||
|
||
def _load_yaml() -> dict[str, dict[str, Any]]:
|
||
"""运行时读 generation.yaml(带 mtime 缓存 + best-effort)。返回解析后的 dict(读不到/脏 → 空 dict)。
|
||
|
||
线程安全:首次读 / 文件 mtime 变化时重读并刷新缓存;否则返回缓存(避免每次 get 都开文件)。
|
||
任何异常(文件缺失 / YAML 语法错 / 顶层非 dict / 缺 pyyaml)都不抛——返回空 dict(由上层落内置默认)。
|
||
"""
|
||
global _cache, _cache_mtime, _cache_path
|
||
|
||
path = config_path()
|
||
path_str = str(path)
|
||
|
||
# pyyaml 缺失:整层降级(只用内置默认 + env)。一次性告警后返回空 dict。
|
||
if _yaml is None:
|
||
_warn_once("no-yaml", "pyyaml 不可用 → 配置层降级为「内置默认 + env 覆盖」(行为仍 = 现值)。")
|
||
return {}
|
||
|
||
with _lock:
|
||
# 取文件 mtime;文件不存在 → mtime=None。
|
||
try:
|
||
mtime = path.stat().st_mtime if path.exists() else None
|
||
except OSError:
|
||
mtime = None
|
||
|
||
# 命中缓存:路径与 mtime 都没变 → 直接返回上次解析结果(运行时读但不重复开文件)。
|
||
if _cache is not None and _cache_path == path_str and _cache_mtime == mtime:
|
||
return _cache
|
||
|
||
# 文件不存在:缓存空 dict(按内置默认跑);一次性告警「用内置默认」(非错误,可能就是没建文件)。
|
||
if mtime is None:
|
||
_warn_once(
|
||
f"missing:{path_str}",
|
||
f"配置文件不存在({path_str})→ 全部用内置默认(行为 = 现值);"
|
||
"如需调生成,建该文件或 export TIER2_GENCONFIG 指向它。",
|
||
)
|
||
_cache, _cache_mtime, _cache_path = {}, None, path_str
|
||
return _cache
|
||
|
||
# 读 + 解析(best-effort:任何异常都落空 dict + 告警,绝不抛)。
|
||
try:
|
||
raw = path.read_text(encoding="utf-8")
|
||
data = _yaml.safe_load(raw)
|
||
if data is None:
|
||
data = {} # 空文件 → 空配置(按内置默认跑)
|
||
if not isinstance(data, dict):
|
||
# 顶层不是映射(写错了)→ 视为脏文件,落空 dict 用内置默认。
|
||
_warn_once(
|
||
f"nonmap:{path_str}",
|
||
f"配置文件顶层不是 YAML 映射({path_str})→ 视为脏,全部用内置默认(行为 = 现值)。",
|
||
)
|
||
data = {}
|
||
except Exception as exc: # noqa: BLE001 —— 脏 YAML / IO 异常都降级,绝不中断生成
|
||
_warn_once(
|
||
f"parse:{path_str}",
|
||
f"配置文件解析失败({path_str}: {type(exc).__name__}: {exc})→ 全部用内置默认(行为 = 现值)。",
|
||
)
|
||
data = {}
|
||
|
||
_cache, _cache_mtime, _cache_path = data, mtime, path_str
|
||
return _cache
|
||
|
||
|
||
def _coerce(value: Any, like: Any) -> Any:
|
||
"""把从 env / YAML 取到的原始值按「内置默认 like 的类型」做最小转型(best-effort,失败回 None)。
|
||
|
||
为什么要转型:env 取出来全是字符串,YAML 也可能把 0.8 读成字符串;转型按现值的类型对齐,避免
|
||
把 "60" 当字符串塞进期望 int 的旋钮。bool 特殊处理(避免 bool('False') == True 的经典坑)。
|
||
转型失败(如把 'abc' 转 int)→ 返回 None,由上层视为「这一级没取到」继续往下回落。
|
||
"""
|
||
if value is None:
|
||
return None
|
||
try:
|
||
if isinstance(like, bool):
|
||
# bool 优先:像 like 是 True/False 时,把常见真假字面量与数值都正确映射。
|
||
if isinstance(value, bool):
|
||
return value
|
||
s = str(value).strip().lower()
|
||
if s in ("1", "true", "yes", "on"):
|
||
return True
|
||
if s in ("0", "false", "no", "off"):
|
||
return False
|
||
return None # 不认识的 bool 字面量 → 视为没取到
|
||
if isinstance(like, int) and not isinstance(like, bool):
|
||
return int(value)
|
||
if isinstance(like, float):
|
||
return float(value)
|
||
if isinstance(like, str):
|
||
return str(value)
|
||
except (TypeError, ValueError):
|
||
return None # 转型失败 → 当作没取到,回落下一级
|
||
# like 是 None / 其它类型:原样返回(不强转)。
|
||
return value
|
||
|
||
|
||
def get(area: str, key: str, default: Any = None) -> Any:
|
||
"""取一个生成器旋钮的当前值:env 覆盖 > 外部 YAML > 内置默认 > 调用方 default。
|
||
|
||
Args:
|
||
area: 分区名(model / budget / iteration / design_team / gates / archetype)。
|
||
key: 分区内的旋钮名(见 _BUILTIN_DEFAULTS 与 generation.yaml)。
|
||
default: 调用方传入的兜底值——**按惯例传该旋钮当前硬编码值**;它是连内置默认都没登记该 key
|
||
时的最后防线,保证取值结果在三处同值时 == 现值(默认行为字节不变)。
|
||
|
||
Returns:
|
||
旋钮值(类型按内置默认对齐转型;全级都取不到 → 返回 default)。绝不抛(best-effort)。
|
||
|
||
取值口径(任一级取到即用):
|
||
① env TIER2_GEN__<AREA>__<KEY>(全大写、双下划线分隔);
|
||
② YAML 的 area.key;
|
||
③ _BUILTIN_DEFAULTS[area][key];
|
||
④ 调用方 default。
|
||
转型基准 `like`:优先用内置默认的类型,缺则用 default 的类型(让 env/YAML 的字符串值按现值类型落位)。
|
||
"""
|
||
builtin = _BUILTIN_DEFAULTS.get(area, {})
|
||
# 转型基准:内置默认有该 key 用它的类型,否则用 default 的类型(都为 None 则不强转)。
|
||
like = builtin.get(key, default)
|
||
|
||
# ⓪ Nacos 热源(配置控制面阶段二:最高优先级)。未 attach(默认)/ 热源无此键 / 取值异常 → 静默
|
||
# 继续回落,绝不引入第二默认值(热源只给「有就用」的值,缺失一律交下面 env/YAML/内置默认)。
|
||
# 复合键 area.key:把 genconfig 的 (area,key) 二维无歧义扁平化成热源 store 的一个键;激活编排 /
|
||
# 演示脚本按同一 "area.key" 组 dataId JSON。热源命中即返回,根本不读 YAML —— mtime 缓存无从盖住热值。
|
||
src = _hot_source
|
||
if src is not None:
|
||
try:
|
||
hot_val = src.get(f"{area}.{key}", _MISSING)
|
||
except Exception as exc: # noqa: BLE001 —— 热源取值绝不连累主链:异常即视为「没取到」回落
|
||
hot_val = _MISSING
|
||
_warn_once(
|
||
f"hot-err:{area}.{key}",
|
||
f"Nacos 热源取 {area}.{key} 异常({type(exc).__name__}: {exc})→ 回落 env/YAML/默认。",
|
||
)
|
||
if hot_val is not _MISSING:
|
||
coerced = _coerce(hot_val, like)
|
||
if coerced is not None:
|
||
return coerced
|
||
# 热源有该键但值转不动(类型脏)→ 告警回落(与 env/YAML 脏值同处置,不让脏热值穿透)。
|
||
_warn_once(
|
||
f"hot-bad:{area}.{key}",
|
||
f"Nacos 热源 {area}.{key}={hot_val!r} 无法转成 {type(like).__name__} → 忽略,回落 env/YAML/默认。",
|
||
)
|
||
|
||
# ① env 单点覆盖(Nacos 热源之下、YAML 之上;一次性实验逃生舱,不必改文件)。
|
||
env_name = f"{_ENV_PREFIX}{area.upper()}__{key.upper()}"
|
||
env_raw = os.environ.get(env_name)
|
||
if env_raw is not None:
|
||
coerced = _coerce(env_raw, like)
|
||
if coerced is not None:
|
||
return coerced
|
||
# env 设了但转型失败(写了非法值)→ 告警并继续回落(不让一个手滑的 env 把旋钮变 None)。
|
||
_warn_once(
|
||
f"env-bad:{env_name}",
|
||
f"env {env_name}={env_raw!r} 无法转成 {type(like).__name__} → 忽略,回落 YAML/默认。",
|
||
)
|
||
|
||
# ② 外部 YAML 的 area.key。
|
||
data = _load_yaml()
|
||
section = data.get(area) if isinstance(data, dict) else None
|
||
if isinstance(section, dict) and key in section:
|
||
coerced = _coerce(section.get(key), like)
|
||
if coerced is not None:
|
||
return coerced
|
||
# YAML 写了该 key 但值脏(类型转不动)→ 告警并回落内置默认(不让脏值穿透)。
|
||
_warn_once(
|
||
f"yaml-bad:{area}.{key}",
|
||
f"配置 {area}.{key}={section.get(key)!r} 无法转成 {type(like).__name__} → 忽略,回落内置默认。",
|
||
)
|
||
|
||
# ③ 内置默认。
|
||
if key in builtin:
|
||
return builtin[key]
|
||
|
||
# ④ 调用方 default(连内置默认都没登记该 key 的最后防线)。
|
||
return default
|
||
|
||
|
||
def export_for_harness() -> dict[str, Any]:
|
||
"""导出 harness(Node)需要的门阈值为一个扁平 dict(供 .cjs 读一份 JSON,或编排器写临时 JSON)。
|
||
|
||
harness 是 .cjs,不便直接读本 Python 加载器;最小改动法是让它读一份导出的 JSON 或经 env 注入。
|
||
本函数把 gates 区里 harness 关心的几个阈值按 .cjs 端约定的 env 名导出成 dict,值仍走 get() 三级回落
|
||
(故默认 = 现值)。.cjs 端按「process.env.<同名> > 内置默认」消费(见 play-phaser.cdp.cjs 顶部注释)。
|
||
|
||
返回的键 = .cjs 端读取的 env 变量名,值 = 当前生效值(已转型)。
|
||
"""
|
||
return {
|
||
# 键名 = play-phaser.cdp.cjs 读的 env 名(TIER2_GEN__GATES__* 同 get() 口径,.cjs 也认这个前缀)。
|
||
"TIER2_GEN__GATES__HUMAN_MIN_PATIENCE_MS": get("gates", "human_min_patience_ms", 12000),
|
||
"TIER2_GEN__GATES__COLOR_DIST_MIN": get("gates", "color_dist_min", 18),
|
||
"TIER2_GEN__GATES__VAR_DELTA_MIN": get("gates", "var_delta_min", 120),
|
||
}
|
||
|
||
|
||
def reload() -> None:
|
||
"""强制丢弃缓存,下次 get 重新读文件(测试 / 手动热更用;运行时 mtime 已自动重读,一般不必显式调)。"""
|
||
global _cache, _cache_mtime, _cache_path
|
||
with _lock:
|
||
_cache, _cache_mtime, _cache_path = None, None, None
|
||
|
||
|
||
# ── __main__ 自测块:不依赖 agentscope / 网络,只校验三级回落与转型(6c6g 可直接跑)────────────
|
||
# 校验:python3 -c 'from worker import genconfig'(import 干净)+ python3 worker/genconfig.py(自测)。
|
||
if __name__ == "__main__": # pragma: no cover —— 本地自测
|
||
import tempfile
|
||
|
||
def _check(label: str, got: Any, expect: Any) -> None:
|
||
ok = "OK" if got == expect and type(got) is type(expect) else "FAIL"
|
||
print(f"[{ok}] {label}: got={got!r}(期望 {expect!r})")
|
||
assert got == expect and type(got) is type(expect), f"{label}: 期望 {expect!r},实得 {got!r}"
|
||
|
||
# ① 无 YAML、无 env:取内置默认(= 现值),类型正确。
|
||
reload()
|
||
os.environ.pop("TIER2_GENCONFIG", None)
|
||
_check("内置默认 model.max_tokens", get("model", "max_tokens", 16000), 16000)
|
||
# 期望值锚当前单源现值(YAML/内置):rmb_hard_limit 裁决 ¥50(0号 spike 占位 3.0 已废)、
|
||
# writer_max_iters 60(2026-07-04 创始人阈值批 40→60)。改单源必须同步这里,否则自测块先失真。
|
||
_check("内置默认 budget.rmb_hard_limit", get("budget", "rmb_hard_limit", 3.0), 50.0)
|
||
_check("内置默认 model.thinking_enable(bool)", get("model", "thinking_enable", True), True)
|
||
_check("内置默认 iteration.writer_max_iters", get("iteration", "writer_max_iters", 40), 60)
|
||
|
||
# ② 调用方 default 兜底:内置默认未登记的 key → 返回 default。
|
||
_check("未登记 key 回落 default", get("model", "不存在的key", 999), 999)
|
||
|
||
# ③ env 覆盖 + 转型(字符串 → int / float / bool)。
|
||
os.environ["TIER2_GEN__ITERATION__WRITER_MAX_ITERS"] = "60"
|
||
os.environ["TIER2_GEN__BUDGET__RMB_HARD_LIMIT"] = "5.5"
|
||
os.environ["TIER2_GEN__MODEL__THINKING_ENABLE"] = "false"
|
||
_check("env 覆盖 int", get("iteration", "writer_max_iters", 40), 60)
|
||
_check("env 覆盖 float", get("budget", "rmb_hard_limit", 3.0), 5.5)
|
||
_check("env 覆盖 bool=false", get("model", "thinking_enable", True), False)
|
||
# env 非法值 → 忽略、回落内置默认(不变 None)。
|
||
os.environ["TIER2_GEN__ITERATION__WRITER_MAX_ITERS"] = "abc"
|
||
_check("env 非法值回落默认", get("iteration", "writer_max_iters", 40), 60)
|
||
for k in ("TIER2_GEN__ITERATION__WRITER_MAX_ITERS", "TIER2_GEN__BUDGET__RMB_HARD_LIMIT",
|
||
"TIER2_GEN__MODEL__THINKING_ENABLE"):
|
||
os.environ.pop(k, None)
|
||
|
||
# ④ 外部 YAML 流通:写一个临时 YAML 改一个值 → get 取到改后值(运行时读)。
|
||
if _yaml is not None:
|
||
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False, encoding="utf-8") as f:
|
||
f.write("model:\n max_tokens: 24000\nbudget:\n rmb_hard_limit: 2.0\n")
|
||
tmp_path = f.name
|
||
os.environ["TIER2_GENCONFIG"] = tmp_path
|
||
reload()
|
||
_check("YAML 改值流通 model.max_tokens", get("model", "max_tokens", 16000), 24000)
|
||
_check("YAML 改值流通 budget.rmb_hard_limit", get("budget", "rmb_hard_limit", 3.0), 2.0)
|
||
# YAML 没写的 key 仍回落内置默认(= 现值)。
|
||
_check("YAML 未写 key 回落内置默认", get("iteration", "writer_max_iters", 40), 60)
|
||
os.environ.pop("TIER2_GENCONFIG", None)
|
||
os.unlink(tmp_path)
|
||
reload()
|
||
|
||
# ⑤ 脏 YAML(顶层非映射 / 语法错):降级内置默认,不抛。
|
||
if _yaml is not None:
|
||
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False, encoding="utf-8") as f:
|
||
f.write("- 这是个列表不是映射\n- 顶层写错了\n")
|
||
bad_path = f.name
|
||
os.environ["TIER2_GENCONFIG"] = bad_path
|
||
reload()
|
||
_check("脏 YAML 降级内置默认", get("model", "max_tokens", 16000), 16000)
|
||
os.environ.pop("TIER2_GENCONFIG", None)
|
||
os.unlink(bad_path)
|
||
reload()
|
||
|
||
# ⑥ export_for_harness:默认导出 = 现值。
|
||
h = export_for_harness()
|
||
_check("harness 导出 HUMAN_MIN_PATIENCE_MS", h["TIER2_GEN__GATES__HUMAN_MIN_PATIENCE_MS"], 12000)
|
||
_check("harness 导出 COLOR_DIST_MIN", h["TIER2_GEN__GATES__COLOR_DIST_MIN"], 18)
|
||
|
||
print("\n[genconfig] 三级回落 + 转型 + YAML 流通 + 脏值降级 + harness 导出自测全部通过。")
|