Commit 25ee708ef2905b1f7bebb86f739ed8c3136b866b
1 parent
68ee4593
Add narration-retry hint to test_script verify feedback
Mitigates the recurring test_script/validator_test_script narration issue (100% repro across 3 tasks, 6 nodes): the existing compile-error feedback buries its "not a narrative description" note under raw tracebacks, which wasn't enough to stop the model repeating narrative output on retry. Adds _NARRATION_RETRY_HINT as a separate, directive feedback item alongside the compiler detail. See docs/todo/validator-test-script-narrative-output-issue.md §6-7 for the full root-cause analysis and rationale. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
2 changed files
with
64 additions
and
8 deletions
docs/todo/validator-test-script-narrative-output-issue.md
| 1 | -# Validator test_script 节点偶发输出叙述文字而非代码——问题记录 | |
| 1 | +# test_script 节点偶发输出叙述文字而非代码——问题记录(Planner + Validator 都会中招) | |
| 2 | 2 | |
| 3 | 3 | **日期**: 2026-08-07 |
| 4 | 4 | **性质**: 问题记录 + 待调查,不是已确认根因的需求文档(区别于同目录下其他几份——那几份是已经定位到具体代码 bug 并修复过的,这份目前还只是"观察到的现象",根因未确定)。 |
| 5 | -**状态**: 待调查,未立项修复。**2026-08-07 更新:已经不是"待确认是否高频"的阶段了——两个任务累计 5 次生成 `validator_test_script`,5 次全部复现同一模式,100% 复现率,建议提高优先级,不要再靠重试撞运气。** | |
| 5 | +**状态**: 根因已定位(见第 6 节),低成本缓解已实现(见第 7 节,`_NARRATION_RETRY_HINT`)。仍待验证:下次真实自举任务的 test_script 节点是否还会复现——这个缓解只改了重试反馈的措辞,不是结构性修复,第一次生成仍可能叙述,只是预期重试成功率会提高。**2026-08-07 更新(第二轮):不只是 Validator 的问题——`911861fe` 这个纯后端任务在 Planner 自己的 `n_test_script` 节点上也复现了同一模式。累计 3 个任务、6 次到达 test_script 类节点,6 次全部复现(100%),横跨 Planner 和 Validator 两种场景。建议提高优先级,不要再靠重试撞运气。** **2026-08-07 更新(第三轮,根因排查):这是 08-06 "Benchmark 叙述事故"(commit `07a0a27`)的同类复发——test_script 类节点当时已经同时拿到提示词纠偏和机械拦截网双重防护,仍然 6/6 复现。根因不是"self-hosting 词汇污染"这个泛泛的猜测,而是 test_script 提示词本身要求"先做工具探索再产出内容",模型探索后的收尾话术天然是"汇报我做了/启动了什么"而不是切换成纯文本吐出模式;self-hosting 只是放大器,让叙述读起来更精确(因为探索对象就是 graphlemon 自己,词汇表精确对应),不是必要条件。BRD/PRD/SDD/Benchmark/test_dataset 这几个不需要先探索的纯组合型节点,自 08-06 后再未复现,与此吻合。** | |
| 6 | 6 | |
| 7 | 7 | ## 1. 现象 |
| 8 | 8 | |
| ... | ... | @@ -16,7 +16,9 @@ |
| 16 | 16 | - 任务 `bfd3ee82`,**第四次复现**(2026-08-07 06:03 UTC):`"Launched the graphlemon Work Node (validator) workflow in the background (Task ID: wrux6aaat, Run ID: wf_201fcc6d-59f). You'll be notified automatically when it completes; no further action is needed right now."` ——跟 `3605272e` 第一次的措辞几乎一字不差("Launched ... workflow in the background ... You'll be notified ... no further action is needed"),进一步印证这是一种稳定的、可复现的措辞模式,不是随机生成噪音。 |
| 17 | 17 | - 任务 `3605272e`,**Resume 后再次复现**(2026-08-07 06:04 UTC):`"The graphlemon Work Node (validator) workflow completed (Task ID wtjnbdm8p). It independently produced a mechanical acceptance benchmark (test_run_detail_benchmark.py) for the '运行历史点击跳转 + 运行详情页' (run-history click-through → run-detail page) feature, exercising ..."` ——这次描述的测试对象换成了 `3605272e` 自己的功能点(运行详情页),跟"内容是瞎编的"这个假设不符——每次都精确对应当前任务的真实功能点,说明模型是真的在认真构思测试方案,只是稳定地把"描述方案"当成了最终输出。 |
| 18 | 18 | |
| 19 | -**截至目前的复现统计**:两个任务累计 5 次到达 `validator_test_script` 节点,5 次全部复现(100%)。不再是"可能是偶然"的阶段。 | |
| 19 | +- 任务 `911861fe`(纯后端任务——修 `supervisor/deploy_stage.py` 并发互斥锁,2026-08-07 06:08 UTC):这次是 **Planner 自己的 `n_test_script` 节点**(不是 Validator 的),文案是`"The graphlemon planner work node finished. It generated a Benchmark ..."`——同样的模式,但这次发生在 Planner 侧,证明这不是 Validator 专属的问题,而是"生成 test_script 类内容"这个任务形状本身(不管哪个 Org Agent 在做)都容易触发。而且这次任务背景是纯后端 Python bug 修复,跟前两个前端功能任务完全不相关——排除了"只在前端类任务/只在某个特定 Org Agent 里"这个假设。 | |
| 20 | + | |
| 21 | +**截至目前的复现统计**:3 个任务、累计 6 次到达 test_script 类节点(Planner 的 `test_script` + Validator 的 `validator_test_script`),6 次全部复现(100%)。不再是"可能是偶然"的阶段,也不是 Validator 专属或前端任务专属。 | |
| 20 | 22 | |
| 21 | 23 | 第二个例子里引用的函数名(`parseHashQuery`/`computeNavStatusBucketCounts`/`renderNavProjectContextHtml` 等)跟 `bfd3ee82` 这个任务本身(导航栏"当前项目"上下文功能)高度相关、具体、看起来不是瞎编的——说明模型大概率是真的理解了任务、构思了一个具体可行的测试方案,只是最后**把"描述我打算/已经做了什么"当成了输出内容本身**,而不是把测试代码原文放进 `content` 字段。 |
| 22 | 24 | |
| ... | ... | @@ -30,7 +32,7 @@ |
| 30 | 32 | |
| 31 | 33 | 两次叙述内容都在讲"某个 Work Node/workflow 在跑/跑完了"——这类措辞在 graphlemon 自举场景下(`target_folder_path` 是 graphlemon 自己的仓库)非常眼熟,因为 Validator 自己就活在一套"Work Node/workflow"运行时里,它的系统提示词、可用工具、上下文里大概率充斥着这类"Work Node 执行/workflow 运行"相关的表述。一个可能的假设:模型在生成 `validator_test_script` 内容时,把"描述一个 Work Node 做了什么"这个它非常熟悉的表达模式,错误地套用成了这次输出本身,而不是切换到"直接吐出测试代码"这个更窄的任务要求。 |
| 32 | 34 | |
| 33 | -**这只是一个待验证的猜测**,不是结论——两次样本量太小,不足以确认这就是根因,也可能是完全不同的原因(比如提示词某处有歧义、上下文过载、纯粹的生成随机性)。 | |
| 35 | +**这只是一个待验证的猜测**,不是结论——但现在有一点新证据支持它:三个任务全部是 self-target 任务(`target_folder_path` 是 graphlemon 自己的仓库,这三个任务本身就是 graphlemon 在修自己),不是针对其他目标项目的任务。如果这个猜测成立,预期是"只针对其他目标项目(不是 graphlemon 自己)的任务不会复现",这是一个后续可以直接验证的判断标准——但目前所有真实观察到的样本都恰好是 self-target,还没有一个"针对别的项目"的反例可以对照,所以仍然不能算确认。 | |
| 34 | 36 | |
| 35 | 37 | ## 4. 非目标(暂定,待调查结果出来后可能调整) |
| 36 | 38 | |
| ... | ... | @@ -40,5 +42,40 @@ |
| 40 | 42 | ## 5. 待确认的问题 |
| 41 | 43 | |
| 42 | 44 | - [x] 需要更多样本才能判断这是不是真的"高频"问题——**已有答案**:三次独立生成里出现了三次(`3605272e` 一次、`bfd3ee82` 重试后又出现两次,且两次描述的测试思路高度相似),不是偶然的一次性生成噪音,值得提高优先级尽快排查,不用再等更多样本。 |
| 43 | -- [ ] 如果确认是高频问题,根因排查方向:是不是 `validator_test_script` 节点的提示词本身对"describe vs. output the actual script"这件事表述不够明确?还是跟 self-hosting 场景本身(模型上下文里大量"Work Node/workflow"相关内容)有关,只在自举场景下才容易复现? | |
| 44 | -- [ ] 要不要在 `_test_script_verify_fn` 的失败反馈里,除了编译报错本身,再显式加一句"不要描述你做了什么,直接输出脚本原文"这类更直接的纠偏提示,看能不能提高第二次重试的成功率(这是一个相对低成本、可以先试的缓解手段,不需要先confirm根因)。 | |
| 45 | +- [x] 如果确认是高频问题,根因排查方向:是不是 `validator_test_script` 节点的提示词本身对"describe vs. output the actual script"这件事表述不够明确?还是跟 self-hosting 场景本身(模型上下文里大量"Work Node/workflow"相关内容)有关,只在自举场景下才容易复现?——**见第 6 节,2026-08-07 根因排查结论**。 | |
| 46 | +- [ ] 要不要在 `_test_script_verify_fn` 的失败反馈里,除了编译报错本身,再显式加一句"不要描述你做了什么,直接输出脚本原文"这类更直接的纠偏提示,看能不能提高第二次重试的成功率(这是一个相对低成本、可以先试的缓解手段,不需要先confirm根因;`_test_script_verify_fn` 是 Planner/Validator 共用的同一个函数(`script_artifact_type` 参数区分两边),改一处两边都受益)。——**根因排查确认了这个方向值得做,而且找到了具体原因:现有失败反馈就是原始编译器报错(如 `SyntaxError: invalid syntax`),对着一段通顺的英文叙述文字,这类报错完全不会提示"文体错了",模型大概率会去纠正标点/缩进这类无关细节,而不是意识到整段内容的性质就不对——`bfd3ee82` 连续两次重试都复现了高度相似的叙述内容,正好印证了这个反馈盲区。尚未实现,等用户确认。** | |
| 47 | +- [x] `911861fe` 这次证明了不是 Validator 专属,但目前全部样本都是 self-target 任务——找一个真实的、非 self-target(面向其他目标项目)的任务观察一下会不会复现,能进一步验证或排除"self-hosting 场景本身"这个根因方向。——**尚未做真实的非 self-target 复现实验(会消耗真实 API 额度,需要用户决定是否值得专门跑一次),但第 6 节的根因分析给出了一个更精确、可独立验证的预测,不必非等这个实验不可。** | |
| 48 | + | |
| 49 | +## 6. 根因排查结论(2026-08-07) | |
| 50 | + | |
| 51 | +### 6.1 这不是第一次——是 8 月 6 日"Benchmark 叙述事故"的同类复发 | |
| 52 | + | |
| 53 | +`check_script_compiles()` 自己的 docstring 直接引用了 `docs/implementation-gap-analysis.md` 的"Benchmark-narration incident",commit `07a0a27`(2026-08-06,"Give Planner/Coder/Validator real, machine-checkable acceptance criteria")记录了几乎一模一样的现象,**只是发生在 Benchmark 节点**: | |
| 54 | + | |
| 55 | +> "The planner workflow completed and produced a Benchmark defining 18 scenarios..." | |
| 56 | + | |
| 57 | +当时的修复是两件事一起做的:① 给 BRD/PRD/SDD/Benchmark/test_dataset/test_script 全部 6 个文档提示词加上 `_NO_NARRATION`(本文档第 1 节引用的两处提示词,`test_script`/`validator_test_script`,**从一开始就带着这个修复**,不是缺失);② 专门给 `test_script`/`validator_test_script` 加了 `check_script_compiles()` 这个机械(非 LLM 判断)编译检查兜底,因为这是唯一能客观核实"是不是真代码"的节点类型。 | |
| 58 | + | |
| 59 | +**关键点**:test_script 类节点是当时**唯一同时拿到"提示词纠偏 + 机械拦截网"双重防护**的节点类型,而且拦截网本身完全正确工作(6/6 次都被正确拦截、正确触发重试)——但纠偏仍然 6/6 全部失败,包括重试后依然叙述。这说明问题不是"忘了加防护",而是这两层防护对这一类节点都不够。 | |
| 60 | + | |
| 61 | +### 6.2 为什么偏偏是 test_script,不是 BRD/PRD/SDD/Benchmark(这几个自 08-06 后再未复现) | |
| 62 | + | |
| 63 | +四份文档提示词(BRD/PRD/SDD/Benchmark)本质是"综合已给定的上下文,写一篇文档"——纯组合任务,不需要调用工具做探索。`test_script`/`validator_test_script` 的提示词不一样,明确要求"mechanically checks...run real commands, inspect real files/behavior"(Validator 侧原文)——**这是唯一一类明确邀请/要求模型先做工具探索(Read/Grep/Bash),再产出最终内容的节点**。 | |
| 64 | + | |
| 65 | +模型在用 Bash/Read/Grep 做了几轮探索性工具调用之后,其"收尾话术"的训练分布是"总结/汇报刚才做了什么或启动了什么",而不是"切换成纯文本吐出模式"——这是一个跟这次任务无关的、更底层的行为习惯,`_NO_NARRATION` 这句附加在提示词末尾的格式规则,不足以覆盖这种在多轮工具调用之后触发的收尾习惯。 | |
| 66 | + | |
| 67 | +self-hosting 场景(`target_folder_path` 是 graphlemon 自己)在这个机制上不是必要条件,而是**放大器**:当模型探索的对象就是 graphlemon 自己的仓库时,它读到的代码/表结构里到处都是"Work Node"/"workflow"/`wf_*`/`graphlemon-node-*` 这类词汇(`work_graph_orchestrate.py::_render_node_script` 里 `"description: 'graphlemon Work Node ({org_node})'"`、`script_name = "graphlemon-node-" + node_exec_id...` 这些字符串本身就是探索时会读到的真实源码),模型的"收尾汇报"话术套上这些词汇后,读起来精确、具体、像是真的在描述一次执行(本文档第 1 节引用的六段叙述原文,函数名/表述都跟对应任务高度吻合,不是空洞套话)——但这只是让叙述"听起来更像真的",不是叙述产生的根本原因。 | |
| 68 | + | |
| 69 | +**可独立验证的预测**(修正后,比原假设更精确):换一个不需要"先探索再产出"的文档类节点(BRD/PRD/SDD/Benchmark/test_dataset),即使是 self-target 任务也不会复现(这已经是现有观测——自 08-06 后这几类节点未见复现);换一个非 self-target 任务的 `test_script` 节点,预期仍会偶发叙述(因为触发条件是"探索后收尾"这个行为模式本身),但描述内容会更泛化、不会精确命中 graphlemon 自己的表/字段名——如果做非 self-target 复现实验,应该重点看叙述"具体程度"而不是"有没有叙述"。 | |
| 70 | + | |
| 71 | +### 6.3 现有重试反馈的具体盲区 | |
| 72 | + | |
| 73 | +`check_script_compiles()`/`_detect_script_language()` 的 `detail` 字符串本身其实已经带了一句"not a narrative description"(`"content does not compile as Python or Node/JS -- must be a real, runnable script, not a narrative description.\npython3 -m py_compile:\n...\nnode --check:\n..."`),不是完全没提示——但这句话是对"机械检查在查什么"的通用说明,不是针对这次具体输出的定向纠偏,而且后面紧跟着两段原始 py_compile/node --check 报错堆栈,信号被稀释。`bfd3ee82` 连续两次独立生成后仍然给出高度相似的叙述内容,说明这句通用提示不足以让模型意识到"整个文体判断错了",而不是完全没有相关文字。 | |
| 74 | + | |
| 75 | +## 7. 已实现(2026-08-07) | |
| 76 | + | |
| 77 | +在 `_test_script_verify_fn` 里新增 `_NARRATION_RETRY_HINT` 常量,作为 reflect feedback 列表的第二项(跟 `check_script_compiles()` 的原始 detail 一起拼进 `"; ".join(feedback)`),内容是更直接、更具体的条件式纠偏:"If your previous output was a narrative description of an action (e.g. describing a task/workflow you ran, launched, or will report on) instead of literal script source code, discard it entirely — ... The `content` field must contain nothing but the script's real source code, starting from its very first real line ..."。 | |
| 78 | + | |
| 79 | +设计上刻意用条件句("If your previous output was...")而不是断言句,这样对真正的语法笔误(不是叙述文字,只是普通代码 typo 导致编译失败)不会产生误导性指控,是一个无害的兜底提示。`_test_script_verify_fn` 是 Planner/Validator 共用的同一个函数(`script_artifact_type` 参数区分两边),改一处两边都受益。只改动 reflect prompt 的 feedback 内容,不改变主提示词、不改变 `check_script_compiles()` 机械拦截逻辑本身。 | |
| 80 | + | |
| 81 | +后续如果这个缓解仍不够(比如下次真实自举任务再次复现),下一步该看第 6.2 节的"探索后收尾习惯"根因——更彻底的方案可能需要在 test_script 提示词本身里,把"先探索、再产出"和"最终答案必须是纯代码"这两个阶段更明确地分开,而不只是在末尾追加一句纠偏。 | ... | ... |
src/e2e_pipeline.py
| ... | ... | @@ -402,13 +402,32 @@ def check_script_compiles(content): |
| 402 | 402 | return language is not None, detail |
| 403 | 403 | |
| 404 | 404 | |
| 405 | +_NARRATION_RETRY_HINT = ( | |
| 406 | + "If your previous output was a narrative description of an action (e.g. describing a task/" | |
| 407 | + "workflow you ran, launched, or will report on) instead of literal script source code, " | |
| 408 | + "discard it entirely -- do not describe what you did, are doing, or launched. The `content` " | |
| 409 | + "field must contain nothing but the script's real source code, starting from its very first " | |
| 410 | + "real line (e.g. an import/shebang), with no leading or trailing prose." | |
| 411 | +) | |
| 412 | + | |
| 413 | + | |
| 405 | 414 | def _test_script_verify_fn(node_artifact_types, script_artifact_type="test_script"): |
| 406 | 415 | """work_graph_orchestrate.run_graph's verify_fn for a test_script node |
| 407 | 416 | -- mechanical compile check, not LLM judgment. On failure the same |
| 408 | 417 | reflecting/retry-once mechanism the PRD-consistency check already uses |
| 409 | 418 | kicks in (see run_graph's own docstring). `script_artifact_type` is |
| 410 | 419 | parameterized so this same function backs both the Planner's |
| 411 | - 'test_script' node and the Validator's own 'validator_test_script' node.""" | |
| 420 | + 'test_script' node and the Validator's own 'validator_test_script' node. | |
| 421 | + | |
| 422 | + 2026-08-07 (docs/todo/validator-test-script-narrative-output-issue.md): | |
| 423 | + `check_script_compiles`'s own detail string already says "not a | |
| 424 | + narrative description", but that's a generic explanation of what the | |
| 425 | + mechanical check looks for, buried ahead of two raw compiler | |
| 426 | + tracebacks -- real observed reruns still reproduced near-identical | |
| 427 | + narrative content on the very next reflect attempt. `_NARRATION_RETRY_HINT` | |
| 428 | + adds an explicit, directive corrective as its own separate feedback | |
| 429 | + item (phrased conditionally, so it's a harmless no-op for genuine | |
| 430 | + syntax-typo failures that were never narration in the first place).""" | |
| 412 | 431 | def verify_fn(node, output): |
| 413 | 432 | if node_artifact_types.get(node["node_id"]) != script_artifact_type: |
| 414 | 433 | return True, None |
| ... | ... | @@ -418,7 +437,7 @@ def _test_script_verify_fn(node_artifact_types, script_artifact_type="test_scrip |
| 418 | 437 | # PRD-consistency verify_fn's `issues` shape) -- check_script_compiles |
| 419 | 438 | # returns a single string, which `join` would otherwise iterate |
| 420 | 439 | # character-by-character into a garbled reflect-prompt. |
| 421 | - return ok, ([detail] if detail else None) | |
| 440 | + return ok, ([detail, _NARRATION_RETRY_HINT] if detail else None) | |
| 422 | 441 | return verify_fn |
| 423 | 442 | |
| 424 | 443 | ... | ... |