feat(diag): 加 glm.pro52 空响应定层探针(交生产跑)(bump 0.58.48)
失败面板 empty 簇(近7天15)主角从 opus48 换成 glm.pro52(11/15):assistant 轮 既无 tool_calls 又无正文,被 loop 当正常收尾静默 done。要判 glm 是①真吐空、 ②工具意图漏成正文(narrated)、还是③瞬态。 scripts/diag_glm_empty_probe.py:build_agent(resume=True) 复原真实 system prompt + 全工具 schema + 完整多轮上下文(空响应发生在深层,单条 user 消息复现不了 —— 与 2a1bc25d narrated 探针的关键差别),打 glm.pro52,stream/non-stream 各一发, 三态判定 EMPTY/NARRATED/STRUCTURED。默认锁 task 35744bea(近7天 empty 5 次 最可复现),argv 可换 task/user/档做对照。 本机 import litellm 卡 ~20min,只能交生产机跑: .venv/Scripts/python.exe scripts/diag_glm_empty_probe.py 输出写 scripts/_glm_empty_probe_*.txt。定层后再决处置。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
795161adcd
commit
9646f571f3
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||
|
||||
最后更新:2026-07-20(工具失败面板巡检:salvage 复盘=保持 + svg 质量门签名诚实化,bump 0.58.47)
|
||||
最后更新:2026-07-20(加 glm.pro52 空响应定层探针,交生产跑,bump 0.58.48)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
### 2026-07
|
||||
|
||||
- **07-20 / 0.58.48 / glm.pro52 空响应定层探针**:失败面板 empty 簇(15)主角从 opus48 换成 `glm.pro52`(11/15)。加 `scripts/diag_glm_empty_probe.py`——`build_agent(resume=True)` 复原真实 system prompt + 全工具 schema + **完整多轮上下文**(空响应发生在深层,单条 user 消息复现不了,这是与 2a1bc25d narrated 探针的关键差别),打 glm.pro52,三态判定 EMPTY(真吐空)/NARRATED(工具意图漏成正文)/STRUCTURED。默认锁 task 35744bea(近7天 empty 5 次最可复现)。本机 import litellm 卡 ~20min,交生产机 `.venv/Scripts/python.exe scripts/diag_glm_empty_probe.py` 跑,输出写 `scripts/_glm_empty_probe_*.txt`。定层后再决处置。
|
||||
- **07-20 / 0.58.47 / 工具失败面板 6 簇巡检 + svg 质量门签名诚实化**:扫近7天失败聚集逐簇定根因。①**write/edit malformed(45/32)**=100% DeepSeek v4 wire 把并行 tool_call 的 arguments 拼成 `{...}{...}`,salvage(0.58.24)复盘提前到 07-20:命中率稳 ~85%(post-launch salvaged 252/malformed 44)、残差涉事 task 全在跑无终态失败=落非流式重试自愈,**决策保持现状不再写代码**(面板绝对数被上线前 07-13/14 尾巴撑高,要看 salvaged/malformed 比)。②**shell/exit "Font issues"(20)**=`svg_quality_checker` 质量门按设计 exit-1,但 `toolfail._classify` 取 `[exit]` 前尾行当签名,而尾行永远是通用 tip「4. Font issues:…」→ 所有门失败(typography/alignment/spec_lock drift)误标成 Font issues 误导排查。修:checker 在 errors>0 时末尾多打确定性 `gate_verdict_line()`「[GATE FAIL] svg_quality_checker: N error file(s) / M total; top issues: …」作为最后一行 stdout(放 main() 里 export 之后),toolfail 现有取尾行逻辑自动拾取诚实签名、toolfail 零改动;`_categorize_issue` 加 `spec_lock/typography` 类避免真错落 "Other"。补 3 测试(GateVerdictLineTests),39+3 全绿。③**run_python 预检(31)**=pysyntax 按设计拦中文正文硬拼进 .py(引号/缩进崩),非平台 bug=提示词治理待办。④**empty(15)**=主角换成 `glm.pro52`(11/15,非旧 opus48 案),需生产跑 narrated 探针定层(本机 import litellm 卡,task e7c2f50b 备用)。⑤**web_fetch(6)**=外部 403 拒抓为主,非平台锅。
|
||||
- **07-20 / 0.58.45 / glob 绝对路径崩溃(工具失败聚集 #2)**:巡检近7天工具失败聚集,`glob` 排第2(3次/3task/3用户、近24h 仍在发)。根因:`tools/fs.py` GlobTool 直接 `base.glob(pattern)`,模型传绝对 pattern(沙箱 cwd 就是 `/workspace`,自然写 `/workspace/**/*.py`)时 pathlib 抛 `NotImplementedError: Non-relative patterns are unsupported`,整个调用崩返 `[Error executing glob]`。改:绝对 pattern 先 `_split_abs_glob` 拆成「存在的静态前缀目录 + 相对 glob 串」(左起吃掉不含 `*?[` 的组件当 base)再匹配;补首字符 `/`\`\\` 判定(POSIX 前导斜杠在 Windows 宿主 `is_absolute()` 为 False,沙箱是 Linux,两处对齐)。顺手删掉原 `if "**"`/`else` 两分支代码完全相同的死重构。`/sandbox/tools/` 是 Dockerfile `COPY tools/` 拷贝,**需重建沙箱镜像**才生效。
|
||||
- **07-20 / 0.58.45 / PPT 页脚漂移治本(工具失败聚集 #1,A+C)**:`shell/exit` 排第1(近7天16条 exit-1、近24h 6条,9task/7用户),实测拆解 15/16 是**逐页手写绝对坐标偏离 `spec_lock.layout_grid` 锁定值 2–16px 的「想对齐没对齐」**(footer_y 近失 27行为最大头,margin_x/content_top 各7行,content_bottom 溢出12行),字体/spec_lock 只是 WARN 不 fail。关键发现:`executor-base.md §180-185` 早有「HARD—checker-enforced」的 snap 纪律却仍被漂移,加 prose 无用,治本须机械化。**A**:spec_lock_reference 新增 `## footer` 锁定片段——x/y 用 `margin_x`/`footer_y` 预填好、页码 `text-anchor="end"` 在 `canvas_w−margin_x`、留 `{PAGE}` 占位,executor 每页**逐字粘贴只换页码**,把 footer_y 这个跨页漂移第一大头从「每页手算」收敛成「一次编写」(executor-base 加对应硬规则,SKILL.md 同步);冒烟验证锁定片段过质检 errors=0、无 footer_y/margin_x 报错。**C**:`svg_quality_checker._ALIGN_TOL` 2→3px(2px 对手写实测过严),同步刷 SKILL/spec_lock_reference/executor-base 的「2–15px/2–12px」表述与测试值(margin drift 用例 60/63/66→60/64/68,30 测试全绿)。content_bottom 溢出(装太多项,另一性质)与 grid-snap 后处理(B)本轮未做。
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.58.47"
|
||||
__version__ = "0.58.48"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
"""定层 glm.pro52 空响应(失败面板 kind=empty_response 主角,11/15/近7天)。
|
||||
|
||||
背景:assistant 轮既无 tool_calls 又无正文,被 run loop 当正常收尾静默 done。
|
||||
与 opus48 的「tool_use 漏成正文」(narrated,见 diag_narrated_toolcall_2a1bc25d)
|
||||
是不同现象——要判 glm 到底是①真吐空(tc=0 且 content=0)、②漏成正文(narrated)、
|
||||
还是③正常(能回结构化 tool_calls,线上那次只是瞬态)。
|
||||
|
||||
做法:build_agent(resume=True) 复原真实 system prompt + 全套工具 schema + **完整多轮
|
||||
上下文**(空响应发生在深层,单条 user 消息复现不了 —— 这是与 2a1bc25d 探针的关键差别),
|
||||
直接打 glm.pro52,stream / non-stream 各打一发。只读 DB,不跑 loop 不写库。
|
||||
输出 ASCII(Windows GBK 安全),完整正文写文件。
|
||||
|
||||
用法(生产机跑,本机 import litellm 卡 ~20min):
|
||||
.venv/Scripts/python.exe scripts/diag_glm_empty_probe.py [task_id] [user_id] [model_profile]
|
||||
默认 = 35744bea(近7天 empty 5 次、最可复现)。model_profile 缺省用 task 自身档(glm.pro52),
|
||||
传第三参可换档做对照(如 deepseek_v4.pro 看是否 glm 独有)。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
env = ROOT / ".env"
|
||||
if env.exists():
|
||||
for line in env.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
||||
os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "True")
|
||||
|
||||
sys.path.insert(0, str(ROOT))
|
||||
import litellm # noqa: E402
|
||||
from core.agent_builder import build_agent # noqa: E402
|
||||
|
||||
# 默认锁定 35744bea(近7天 empty 5 次、最可复现);可 argv 覆盖。
|
||||
TASK_ID = sys.argv[1] if len(sys.argv) > 1 else "35744bea-49c0-41c8-86be-8d0c5c05bd32"
|
||||
USER_ID = UUID(sys.argv[2]) if len(sys.argv) > 2 else UUID("3cc07a22-f667-47f3-9617-e7d8a1a60cf0")
|
||||
MODEL_OVERRIDE = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
|
||||
_tag = (MODEL_OVERRIDE or "self").replace(".", "_").replace("/", "_")
|
||||
OUT = open(ROOT / "scripts" / f"_glm_empty_probe_{TASK_ID[:8]}_{_tag}.txt", "w", encoding="utf-8")
|
||||
|
||||
|
||||
def w(*a):
|
||||
print(*a, file=OUT)
|
||||
|
||||
|
||||
agent, session, sid, tstate, wd = build_agent(
|
||||
user_id=USER_ID, session_id=TASK_ID, resume=True,
|
||||
model_name=MODEL_OVERRIDE,
|
||||
)
|
||||
schemas = agent.executor.schemas()
|
||||
caps = agent.caps
|
||||
llm = agent.llm
|
||||
|
||||
# 复原 loop 下一轮真会送的完整消息(system + 全部历史)—— 空响应就发生在这个上下文上。
|
||||
messages = list(session.messages)
|
||||
sys_txt = next((m["content"] for m in messages if m.get("role") == "system"), "")
|
||||
roles = [m.get("role") for m in messages]
|
||||
|
||||
w("[SETUP]")
|
||||
w(f" task={TASK_ID} model_id={caps.model_id} profile={caps.family}.{caps.variant}")
|
||||
w(f" api_base={llm.api_base} temp={caps.optimal_temperature} "
|
||||
f"parallel_tools={caps.parallel_tools}")
|
||||
w(f" system_prompt chars={len(sys_txt)} tools={len(schemas)}")
|
||||
w(f" messages count={len(messages)} last_role={roles[-1] if roles else None}")
|
||||
w(f" role tail={roles[-6:]}")
|
||||
w("")
|
||||
|
||||
kwargs_base = dict(
|
||||
model=caps.model_id,
|
||||
api_base=llm.api_base,
|
||||
api_key=llm.api_key,
|
||||
messages=messages,
|
||||
tools=schemas,
|
||||
temperature=caps.optimal_temperature,
|
||||
timeout=300,
|
||||
)
|
||||
if caps.parallel_tools:
|
||||
kwargs_base["parallel_tool_calls"] = True
|
||||
|
||||
stdout_lines = []
|
||||
|
||||
|
||||
def probe(tag, stream):
|
||||
w(f"========== {tag} ==========")
|
||||
try:
|
||||
if stream:
|
||||
chunks = list(litellm.completion(
|
||||
**kwargs_base, stream=True,
|
||||
stream_options={"include_usage": True}))
|
||||
resp = litellm.stream_chunk_builder(chunks)
|
||||
else:
|
||||
resp = litellm.completion(**kwargs_base)
|
||||
except Exception as e:
|
||||
w(f"[FAIL] {type(e).__name__}: {str(e)[:300]}")
|
||||
stdout_lines.append(f"{tag}: FAIL {type(e).__name__}")
|
||||
return
|
||||
msg = resp.choices[0].message
|
||||
tc = msg.tool_calls or []
|
||||
content = msg.content or ""
|
||||
fr = resp.choices[0].finish_reason
|
||||
u = resp.usage
|
||||
w(f" finish_reason={fr}")
|
||||
w(f" usage prompt={u.prompt_tokens} completion={u.completion_tokens}")
|
||||
w(f" tool_calls count={len(tc)}")
|
||||
for i, t in enumerate(tc):
|
||||
w(f" [{i}] {t.function.name}({(t.function.arguments or '')[:200]})")
|
||||
w(f" content chars={len(content)}")
|
||||
w(" content:")
|
||||
w(content[:2000])
|
||||
w("")
|
||||
# 三态判定:EMPTY(真吐空)/ NARRATED(工具意图漏成正文)/ STRUCTURED / plain-text
|
||||
narrated = (not tc) and ("`" in content) and ("\n- " in content or "No result received" in content)
|
||||
if tc:
|
||||
verdict = "STRUCTURED tool_calls"
|
||||
elif not content.strip():
|
||||
verdict = "EMPTY (tc=0 content=0) <-- 复现空响应"
|
||||
elif narrated:
|
||||
verdict = "NARRATED-as-text"
|
||||
else:
|
||||
verdict = "plain-text-no-tools"
|
||||
stdout_lines.append(f"{tag}: {verdict} (finish={fr}, tc={len(tc)}, content={len(content)}c)")
|
||||
|
||||
|
||||
probe("NON-STREAM", stream=False)
|
||||
probe("STREAM", stream=True)
|
||||
|
||||
OUT.close()
|
||||
for ln in stdout_lines:
|
||||
print(ln)
|
||||
print(f"full -> scripts/_glm_empty_probe_{TASK_ID[:8]}_{_tag}.txt")
|
||||
Loading…
Reference in New Issue