From 3c0c105aeb1b5fadcb4f564af992dda3e8390b14 Mon Sep 17 00:00:00 2001 From: lili Date: Wed, 8 Jul 2026 10:56:03 -0700 Subject: [PATCH] =?UTF-8?q?fix(cheap-gen):=20=E6=AD=BB=E5=9C=88=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20fix=E2=91=A0-B=20=E4=BE=BF=E5=AE=9C=E6=A1=A3?= =?UTF-8?q?=E7=95=B8=E5=BD=A2=E5=8F=82=E6=95=B0=E7=BA=A0=E5=81=8F=E2=80=94?= =?UTF-8?q?=E2=80=94n=3D5=20=E6=94=B6=E6=95=9B=E7=8E=AF=E5=9D=90=E5=AE=9E?= =?UTF-8?q?=20heritage=20=E6=AD=BB=E5=9C=88=E5=A4=8D=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit n=5 收敛 loop iteration 2(难品类)复现死圈:heritage 局 stuck 终杀,签名=连续 4 次 write_file:'path' is a required property——MiniMax 长参数下漏 write_file 必填 path,agent 收默认校验错不理会、反复漏同参 → 撞满 stuck 阈值(4)→ 九门从未跑 ok=False。fix①-A 给了 edit 替代但没阻止 agent 选 write_file 漏 path,故 fix①-B(此前押"证据门")现证据到、实现。 修:CircuitBreakerMiddleware.on_acting 非软停态给「漏必填参」的 write 类工具结果追加一条比默认 校验错更贴脸的补参提示(催补 path / 改 edit_file 短载荷),让 agent 撞满 stuck 前出圈。 经既有 _augment_toolresponse( 追加,agent 下轮读得到)+ on_acting 洋葱钩子, 符合设计不自造机制。cheap-only(enable_malformed_correction,cheap_budget 工厂置 True;tier2 默认 False、逐字节零行为变化);不重置 stuck 计数(仍漏照常熔断,上界由现有阈值封死、不新增无界重试)。 测:test_malformed_correction 4(cheap追加/tier2零变化/非漏参不追加/非write工具不进)+ tier2 回归 test_circuit_stuck_signature 5 + test_repair_middleware 5 = 14 passed。整环验证随 n=5 re-run。 Co-Authored-By: Claude Opus 4.8 (1M context) --- cheap-worker/cheap_budget.py | 2 + .../tests/test_malformed_correction.py | 81 +++++++++++++++++++ tier2/gen-worker/worker/middleware.py | 48 +++++++++++ 3 files changed, 131 insertions(+) create mode 100644 tier2/gen-worker/tests/test_malformed_correction.py diff --git a/cheap-worker/cheap_budget.py b/cheap-worker/cheap_budget.py index c84f6bf8..b14bfd4c 100644 --- a/cheap-worker/cheap_budget.py +++ b/cheap-worker/cheap_budget.py @@ -30,9 +30,11 @@ def build_cheap_breaker(**overrides) -> CircuitBreakerMiddleware: """ overrides.pop("rmb_hard_limit", None) overrides.pop("soft_budget", None) + overrides.pop("enable_malformed_correction", None) return CircuitBreakerMiddleware( enable_rmb_gate=True, rmb_hard_limit=cheap_soft_limit(), # 软停线 ¥10(soft 档语义;硬地板 = ×1.5 middleware 内派生) soft_budget=True, # 两段式:越软停线只许收尾、越硬地板 fail-closed(裁决) + enable_malformed_correction=True, # fix①-B:便宜档畸形参数纠偏(漏 path 撞满 stuck 前追加补参提示;tier2 不开) **overrides, ) diff --git a/tier2/gen-worker/tests/test_malformed_correction.py b/tier2/gen-worker/tests/test_malformed_correction.py new file mode 100644 index 00000000..d3d90803 --- /dev/null +++ b/tier2/gen-worker/tests/test_malformed_correction.py @@ -0,0 +1,81 @@ +"""test_malformed_correction.py — fix①-B 便宜档畸形参数纠偏(2026-07-08 n=5 收敛环坐实 heritage 死圈复现)。 + +守的不变量: + · cheap(enable_malformed_correction=True):write 类工具的「漏必填参」结果被追加补参纠偏提示(催补 path / 改 edit_file); + · tier2(默认 False):on_acting 该分支不进,工具结果【逐字节不变】(共享中间件对 tier2 零行为变化); + · 非「漏参」的正常结果 / 非 write 类工具:不追加(纠偏只打漏参、不误伤); + · 不重置 stuck 计数(纠偏不动 _fail_repeat,仍漏就照常撞熔断——本测只验追加行为,计数不变由代码保证)。 + +跑:cheap-worker/.venv/bin/python -m pytest tier2/gen-worker/tests/test_malformed_correction.py -q(cwd=tier2/gen-worker) +""" + +import asyncio +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → tier2/gen-worker/ +from worker.middleware import CircuitBreakerMiddleware # noqa: E402 +from agentscope.tool import ToolResponse # noqa: E402 +from agentscope.message import TextBlock # noqa: E402 + +_MISSING = "Input validation failed for tool 'write_file': 'path' is a required property" + + +class _TC: # mock tool_call(只需 .name) + def __init__(self, name): + self.name = name + + +def _resp(text): + return ToolResponse(content=[TextBlock(text=text)]) + + +def _text_of(resp): + return "".join(str(getattr(b, "text", "") or "") for b in (resp.content or [])) + + +def _drive(mw, tool_name, resp): + """跑 on_acting(mock tool_call + next_handler 吐 resp),收集 yield 的 items。soft_budget 默认 False → 直进 fix①-B 分支。""" + async def next_handler(**kwargs): + yield resp + + async def run(): + out = [] + async for item in mw.on_acting(None, {"tool_call": _TC(tool_name)}, next_handler): + out.append(item) + return out + + return asyncio.run(run()) + + +def test_cheap_augments_missing_param_error(): + mw = CircuitBreakerMiddleware(enable_malformed_correction=True) + out = _drive(mw, "write_file", _resp(_MISSING)) + assert len(out) == 1 + txt = _text_of(out[0]) + assert _MISSING in txt # 原错误保留 + assert "path" in txt and "edit_file" in txt # 追加了补参 + 催 edit_file 短载荷 + assert "重发本次调用" in txt + + +def test_tier2_does_not_augment(): + mw = CircuitBreakerMiddleware() # 默认 enable_malformed_correction=False + out = _drive(mw, "write_file", _resp(_MISSING)) + assert _text_of(out[0]) == _MISSING # 逐字节不变(tier2 零行为变化) + + +def test_cheap_no_augment_on_normal_result(): + mw = CircuitBreakerMiddleware(enable_malformed_correction=True) + out = _drive(mw, "write_file", _resp("write ok bytes=123")) + assert _text_of(out[0]) == "write ok bytes=123" # 非漏参不追加 + + +def test_cheap_no_augment_on_non_write_tool(): + mw = CircuitBreakerMiddleware(enable_malformed_correction=True) + out = _drive(mw, "read_file", _resp(_MISSING)) + assert _text_of(out[0]) == _MISSING # 非 write 类工具不进纠偏分支 + + +if __name__ == "__main__": + import pytest + sys.exit(pytest.main([__file__, "-q"])) diff --git a/tier2/gen-worker/worker/middleware.py b/tier2/gen-worker/worker/middleware.py index 992f0bd3..15bdd67a 100644 --- a/tier2/gen-worker/worker/middleware.py +++ b/tier2/gen-worker/worker/middleware.py @@ -177,6 +177,38 @@ DEFAULT_RMB_HARD_LIMIT = genconfig.get("budget", "rmb_hard_limit", 50.0) # 富 # scaffold_init(重开工程)。read/list 零成本只读、check/build/run_gates/finish 是收尾链本体,均放行。 DEFAULT_SOFT_STOP_BLOCKED_TOOLS = frozenset({"write_file", "edit_file", "write_source", "scaffold_init"}) +# ── fix①-B:便宜档畸形参数纠偏(2026-07-08 n=5 收敛环坐实 heritage 死圈复现)── +# 根因:MiniMax 在长工具调用参数下会漏 write_file/edit_file 的必填 path,AgentScope schema 校验拒 → +# 「Input validation failed: 'path' is a required property」;agent 收到默认校验错常不理会、反复漏同参 → +# 连撞 stuck_repeat_threshold(默认4)→ stuck 终态杀,九门从未跑、ok=False(fix①-A 给了 edit 替代但没阻止 +# agent 选 write_file 漏 path)。本纠偏在【非软停态】给这类「漏必填参」的工具结果追加一条比默认校验错更贴脸的 +# 补参提示(并催改用 edit_file 短载荷),让 agent 在撞满 stuck 阈值前补齐参数出圈。cheap-only +# (enable_malformed_correction=True 才开;tier2 默认关、零行为变化);不重置 stuck 计数(仍漏就照常熔断, +# 上界由现有 stuck 阈值天然封死、不新增无界重试面,与 fix①-A edit 工具叠加)。 +_MALFORMED_PARAM_MARKERS = ("is a required property", "input validation failed") +_MALFORMED_CORRECTION_TEXT = ( + "你这次工具调用漏了必填参数(通常是 path)。工具参数必须一次给全:write_file 必须同时给 {path, content}、" + "edit_file 必须给 {path, old, new}。请【立即重发本次调用、带齐全部参数】。" + "小改优先用 edit_file(只发变化的几行、载荷短、最不易漏参),别对整文件 write_file(长内容最容易把 path 漏掉)。" +) + + +def _is_missing_param_error(resp) -> bool: + """判 ToolResponse 是否是「漏必填参数」的 schema 校验错(fix①-B 纠偏触发条件)。best-effort:抽不到文本返 False。""" + try: + content = getattr(resp, "content", None) + text = "" + if isinstance(content, list): + for b in content: + text += str(getattr(b, "text", "") or "") + elif isinstance(content, str): + text = content + low = text.lower() + return any(m in low for m in _MALFORMED_PARAM_MARKERS) + except Exception: # noqa: BLE001 —— 判定失败按「非漏参」放行,绝不因此中断工具结果 + return False + + # ── 软停后「finish 逼近」递进升压(工单 c)── # 软停(budget_soft_tripped)已由 on_system_prompt 注入「立即 finish」强提醒 + on_acting 拦新增生成类工具, # 但这是被动系统提示;A1 T8 实测 M3 可能烧推理不理会,80011 生产实录也是「软停触发后仍烧到 step_cap/硬地板才停」。 @@ -235,6 +267,7 @@ class CircuitBreakerMiddleware(MiddlewareBase): group_ratio: float | None = None, soft_budget: bool = False, # ¥ 越限行为档位(False=hard 单段 fail-closed;True=soft 两段式:软停+硬地板) soft_stop_blocked_tools: Optional[frozenset] = None, # 软停后拦截的「新增生成面」工具名集(None=默认集) + enable_malformed_correction: bool = False, # fix①-B:便宜档畸形参数纠偏(True 才开;tier2 默认关、零变化) ) -> None: """ Args: @@ -305,6 +338,8 @@ class CircuitBreakerMiddleware(MiddlewareBase): self._soft_stop_blocked_tools = ( soft_stop_blocked_tools if soft_stop_blocked_tools is not None else DEFAULT_SOFT_STOP_BLOCKED_TOOLS) + # fix①-B:便宜档畸形参数纠偏总开关(cheap 熔断工厂置 True;tier2 默认 False = on_acting 该分支不进、零行为变化)。 + self._malformed_correction = enable_malformed_correction self.group_ratio = group_ratio self._pricing_params = pricing_params # {pricing, qpu, usd_rate} 或 None(惰性取价后填) self._pricing_fetch_tried = pricing_params is not None # 已有则不再去取 @@ -595,6 +630,19 @@ class CircuitBreakerMiddleware(MiddlewareBase): escalate = None # _acting 仅一个终态 ToolResponse;附加一次即可,防御性置空避免重复 yield item return + # fix①-B(cheap-only):非软停态下,给「漏必填参」的 write 类工具结果追加补参纠偏提示(催补 path / 改 edit_file + # 短载荷),让 agent 在撞满 stuck 阈值前出圈。tier2(_malformed_correction=False)不进本分支、逐字节零行为变化。 + if self._malformed_correction: + tool_call = input_kwargs.get("tool_call") if isinstance(input_kwargs, dict) else None + name = getattr(tool_call, "name", None) + if name in self._soft_stop_blocked_tools: + async for item in next_handler(**input_kwargs): + if isinstance(item, ToolResponse) and _is_missing_param_error(item): + self._augment_toolresponse(item, _MALFORMED_CORRECTION_TEXT) + print(f"[tier2-circuit] 便宜档畸形参数纠偏:{name} 漏必填参 → 追加补参提示" + f"(催补 path / 改 edit_file 短载荷,趁 stuck 阈值前出圈)。", flush=True) + yield item + return async for item in next_handler(**input_kwargs): yield item