测试、调试、修改已存在的 mini_agent workflow(`.agent/workflows/<name>/`)——沙箱跑单个 step、查一次执行失败在哪、只改坏掉的那个 step、从断点续跑、看长期执行统计,包括 foreach/wait/merge 批处理与汇聚 step、workflow 级熔断、tool_call/result_file 占位符相关问题的排查。当用户说"这个workflow跑失败了"、"帮我测一下这个step"、"这一步为什么一直失败"、"改一下xxx workflow的这个字段"、"从哪步继续跑"、"这个workflow靠谱吗/成功率怎么样"、"foreach里某个元素失败了"、"整个workflow好像熔断了"时使用。**不**用于从零生成新 workflow,那是 workflow-generator skill 的场景。
Install
npx skillscat add onewaymyway/mini-agent/workflow-debugger Install via the SkillsCat registry.
Workflow Debugger(测试 / 调试 / 修改已有 workflow)
用于已经用 save_workflow(或 workflow-generator skill)保存过的 workflow。
覆盖三类场景:跑之前先验证、跑失败之后排查、只改坏掉的那一小块再续跑。
不生成新 workflow——需要从零写一个新的,切到 workflow-generator skill;
不确定 YAML 字段语义时也去看 workflow-generator skill 里的字段表,这里不
重复。核心工具在 src/mini_agent/workflow/tools.py,纯逻辑在workflow/api_helpers.py,状态机定义在 workflow/schema.py。
先判断用户想要哪一种
| 用户诉求 | 用哪个工具 | 不要用 |
|---|---|---|
| "这个 workflow 整体表现怎么样/长期靠不靠谱" | get_workflow_stats(name) |
逐次翻 list_workflow_runs 人工数 |
| "这次跑的具体哪步失败了/为什么" | get_workflow_run_status(workflow_session_id, verbose=true) |
直接猜、或让用户去翻文件 |
| "改完 prompt/timeout 之后不确定对不对,想先验证一下这一步" | test_workflow_step(沙箱,不落盘、不影响正式执行记录) |
直接 run_workflow 整个跑一遍(浪费 token,还会重跑已经成功的前面几步) |
| "确认要正式修 workflow 定义里的某个字段" | patch_workflow_step(只改一个 step 的字段,会校验、会持久化) |
让用户手工改 YAML 文件、或重新贴一份完整 YAML |
| "改完了,接着跑" | resume_workflow_run(force_rerun_from=<step_id>)(只重跑这步及下游,前面成功的不重来) |
重新 run_workflow 从头跑一遍 |
| "只是临时想放宽一下 timeout 试试是不是单纯超时问题,不想动定义" | resume_workflow_run(step_overrides={...})(一次性覆盖,不写回定义) |
patch_workflow_step(那是永久改动) |
| "跑之前想看看这次大概会怎么分批/condition 会不会命中/参数是不是传全了" | preview_workflow(name, inputs)(dry-run,不真的执行,会给出 unresolved_placeholders 清单,见下文) |
直接跑一遍看结果 |
| "这一步到底实际收到了什么 prompt/子进程打印了什么/是不是真并发跑的" | 开启 debug_log_enabled 后看 get_workflow_run_status(verbose=true) 摘要,或 /workflow debug <run_id> <step_id> 看完整 debug_log(见下文"运行时调试日志") |
凭 inputs+上游输出在脑子里重算一遍占位符替换 |
| "正在跑,想先停一下 / 不想跑了" | pause_workflow_run / cancel_workflow_run |
干等或杀进程 |
"设了 output_export_dir 但目标目录里没看到文件/文件不全" |
看 events.jsonl 里的 output_exported 事件(含 copied_files/error),或 get_workflow_run_status(verbose=true);先确认产出文件当初写进了本次 workflow 的 output/ 目录而不是别处 |
怀疑是导出功能本身坏了就去改 runner 代码——复制失败不影响 workflow 执行状态,多数情况是产出文件路径写错了 |
| "有个 human_input/审批门卡着" | provide_workflow_step_input / approve_workflow_step / reject_workflow_step |
让用户去改 workflow 定义绕过它 |
"foreach 里某个/某几个元素失败了,整体状态却是 done" |
这是 foreach_stop_on_error=false(默认)的预期行为,看聚合结果 JSON 里对应 item_index 的 {"error": "..."} |
以为整体 done 就代表所有元素都成功 |
"condition 这一步判成了 needs_fix 而不是 skipped" |
先确认是不是表达式本身写错(引用不存在的 step / 字段类型不对),patch_workflow_step 改表达式,而不是当成"业务上不满足"去调整上游逻辑 |
把 needs_fix 误当成"分数没达标"去改上游 |
| "整个 workflow 好像被提前取消了,好几个不同 step 都报同一种错" | 看 get_workflow_run_status(verbose=true) 有没有 circuit_breaker_tripped/熔断原因,见下文"workflow 级熔断"一节 |
逐个 step 分别排查、以为是巧合 |
"script 声明了 result_file 但一直报失败,returncode 明明是 0" |
检查脚本是否真的把 JSON 写到了 $WORKFLOW_RESULT_FILE_PATH,script 的 structured 模式下 returncode==0 不等于整体成功,见下文"script 的 result_file structured 模式"一节 |
只看 returncode 就断定是 runner 的 bug |
"这个 agent/skill_agent step 老是不稳定/产出格式不对/偶尔跑偏" |
先判断这一步是不是本该是 python_step(见下文"用选型规则诊断不稳定的 agent 类型 step"),是的话 patch_workflow_step 把 type 改成 python_step 并配 script_path |
一味调大 retry_on_error/max_turns 掩盖问题 |
排查一次失败执行的标准流程
- 拿到
workflow_session_id:run_workflow/resume_workflow_run后台
执行时会直接返回;忘了的话用list_workflow_runs(name)找最近一次。 - 看详细状态:
get_workflow_run_status(workflow_session_id, verbose=true)。verbose=true才会带上失败步骤的traceback和出错时的上下文快照
(prompt 预览、step 配置),排查时基本必开;默认false只是省 token
的快速查看模式。- 想连续盯着一个后台执行直到跑完/卡住,用
wait=true(内部轮询,
一次工具调用等到终态或超时为止),不要自己写"查一次、决定要不要
再查"这种反复调用的循环。
- 想连续盯着一个后台执行直到跑完/卡住,用
- 区分两类失败状态(这一步决定你接下来该做什么):
failed/timeout:可能是瞬时故障(网络抖动、外部服务慢、这次
token 超时),可以直接resume_workflow_run不带force_rerun_from简单重跑,或用step_overrides临时调大timeout/retry_on_error再试。needs_fix:runner 已经判定这是结构性/配置性错误(prompt 占位符
写错不存在的 step、tool_name没注册、prompt_file路径不存在,
或者同一 step 连续多次同类失败被escalate_after_n_same_failures
提前拦下,或者(P12)condition表达式求值本身抛异常——引用了
不存在的 step、或此刻字段类型不对导致AttributeError/TypeError,
这跟"表达式写对了、只是求值结果确实是False"的skipped是两回事,
不要混淆),重跑没有用,get_workflow_run_status的输出会显式
提示"这是定义/配置问题,重跑无效"。这种必须先patch_workflow_step
改定义,不要在没改动的情况下反复resume_workflow_run。
- 定位到具体字段该怎么改:结合
verbose=true里的错误信息和context(一般包含解析后的 prompt、该 step 当时生效的 model/timeout
等配置)判断问题出在哪个字段。不确定字段语义/合法取值时参考
workflow-generator skill 里的字段表,或show_workflow(name)看完整
当前定义。 - 改动前先用沙箱验证(尤其是改了 prompt 措辞、想确认新写法是否真的
解决问题时):
这一步会真实调用 LLM/工具(不是假执行),但不接入 DAG、不落盘进test_workflow_step( name="<workflow>", step_id="<出问题的step>", mock_step_results='{"上游step id": {"output": "...", "score": 0.9}}', mock_inputs='{"参数名": "..."}' )workflow_runs历史、不影响任何正式执行——可以反复试而不产生垃圾记录。mock_step_results要覆盖该 step prompt 里用到的所有{xxx.output}/{xxx.score}占位符,缺了会报错并提示需要补哪些;human_input/require_approval类型的 step 不支持沙箱测试,工具会直接提示跳过,
这种只能走真实run_workflow/resume_workflow_run验证。 - 确认沙箱结果符合预期后再正式修改:
patch_workflow_step(name="<workflow>", step_id="<step>", patch='{"prompt": "改好的新 prompt", "timeout": 120}')patch只需要传变化的字段,其余字段保持不变;字段名与 workflow-generator
字段表一致(prompt/model/timeout/retry_on_error/retry_on_gate_fail/allow_parallel/require_approval/tool_name/tool_args/workflow_name/script/skill_name/input_key/input_prompt/condition/depends_on等)。改动后会自动跑一遍wf.validate(),校验不通过不会保存,会把错误列表直接返回——按
报错逐条修正,不要绕过校验手工改磁盘上的 YAML。 - 续跑:
resume_workflow_run(workflow_session_id="<之前那次的id>", force_rerun_from="<step_id>")force_rerun_from会让这个 step 及其所有下游重新执行,之前已经成功、
消耗过 token 的步骤不会重来。不传force_rerun_from时是"从断点继续
跑未完成的部分",语义不同,改完定义后要修某一步通常需要显式传force_rerun_from,否则如果该 step 之前状态不是"未完成"可能不会被
重新纳入执行计划。
用选型规则诊断不稳定的 agent/skill_agent step:能不能改成 python_step
workflow-generator skill 里的强制排序是:能用 python_step(配合ctx.llm/ctx.llm.ask_json)解决的步骤应该优先用 python_step,agent/role_agent 是其它方案都覆盖不了时才用的兜底方案(skill_agent
介于两者之间)。调试时凡是遇到下面这几类症状,第一反应不应该是调大retry_on_error/max_turns/timeout,而应该先判断这一步是不是当初生成
时选型选错了、本该是 python_step:
- 产出格式不稳定:这一步要的是结构化结果(下游
python_step消费的
JSON),但agent/skill_agent没配result_file,靠对话原文解析,
时好时坏——如果这一步本身是"给定输入、按固定规则产出判断"(没有真正的
临场应变需求),正确修法不是补result_file,而是直接改成python_step+ctx.llm.ask_json,从根上消除"模型要不要按格式说话"这个
不确定性。 - 同一类失败反复出现、被
escalate_after_n_same_failures升级成needs_fix:先用get_workflow_run_status(verbose=true)看错误内容,
如果是"模型输出解析失败"“漏判/多判”这类而不是外部依赖报错,大概率是
选型问题而不是 prompt 措辞问题。 get_workflow_stats(name)里某个agent/skill_agentstep 的平均
重试次数/失败率明显偏高,且它的职责描述读起来像"过滤/打分/重排/解析":
这是选型错误的强信号。
诊断步骤:
show_workflow(name)看这一步当前的prompt/prompt_file,判断它是不是
纯粹在要求模型"读取一段结构化输入、按规则给出结构化判断"——如果 prompt
里完全没有要求模型自主决定调用哪些工具/自主探索下一步该干嘛,这是可以
转成python_step的强信号。- 确认转换后仍能满足需求:
python_step脚本里用ctx.llm.ask_json()
把原来 prompt 的判断要求原样喂给模型,ctx.inputs/ctx.input_json()
替代原来的{step_id.output}占位符读取上游数据。 - 写好
steps/<step_id>.py(暴露def run(ctx) -> str|dict),用test_workflow_step沙箱验证新脚本产出是否符合下游预期(沙箱同样会
真实拉起子进程、真实调用 LLM,见下文"沙箱测试python_step"一节)。 - 验证通过后
patch_workflow_step(name, step_id, patch='{"type": ' '"python_step", "script_path": "steps/xx.py"}')——同时清掉这一步不再
需要的agent专属字段(比如skill_name/max_turns),patch只需要
传变化的字段,多余的旧字段留着通常不影响校验,但会造成阅读混淆,建议
一并清理。 - 提醒用户确认
agent_config.json里python_step_enabled已开启,否则
转换后这一步会被直接拦截,反而制造新的失败。
什么时候不该转换:如果这一步的失败原因明确是"需要临场应变"(比如
浏览器页面结构变了、需要根据中间结果动态决定下一步调用哪个工具),说明
选型本身没错,应该按原有的 skill_agent/result_file 排查路径处理(见
下一节),硬转成 python_step 只会让这类步骤更脆弱。
skill_agent 步骤失败或"文件已生成但迟迟不结束":先看 result_file 契约
skill_agent(声明了 result_file)是当前 workflow 里最容易出现"看起来卡住"
或"重试多次仍失败"的 step 类型,排查前先确认这一点,再决定怎么改:
- 先看
result_file是否声明、result_file_required_keys是否列全:show_workflow(name)里没有result_file字段的skill_agentstep,
意味着下游只能靠对话原文解析——如果失败信息看起来像"下游 JSON 解析出
错"而不是这个 step 本身报错,很可能是漏配了result_file,应该先给patch_workflow_step补上,而不是一味调大retry_on_error。 verbose=true里如果错误信息形如"resume×3 + 重开×3 次尝试后仍未产出
合法的 result_file":说明result_file契约本身校验失败了 3+3=6 次,
通常是 prompt 指令不够明确(模型没意识到必须写文件)或result_file_required_keys列的字段模型理解有偏差。先用test_workflow_step单独跑这一步,观察它对话里到底有没有尝试写文件、
写的字段跟要求的是否对得上,再决定改 prompt 还是改result_file_required_keys。- "result_file 已经生成但这一步迟迟不结束"不是 bug,是预期行为:
result_file只在 agent 这一轮自然结束后才会被校验——如果 agent
写完文件后还在继续浏览/反复自我确认,step 就会一直等到它自己收尾或
撞到max_turns/timeout。这种情况改法是在 prompt 里补一句"写完文件
并自检通过后立即收尾,不要再做任何其它操作",或者适当调低max_turns
逼它更早收敛,而不是去调timeout(调大timeout只会让"卡住的感觉"
变得更久,治标不治本)。 ctx.input_json(step_id)/ctx.input_output(step_id)优先读result_file:如果下游python_step报"字段缺失"一类的错,先确认
上游result_file那次校验到底有没有通过(get_workflow_run_status
里能看到该 step 的status),通过了才会读文件,没通过会退回读对话
原文——这两种数据源的字段完整性可能完全不同,排查时不要想当然。
排查 foreach / wait / merge(P13/P14)
这三种类型都是纯 StepExecutor 插件,不改变外层拓扑调度语义,排查思路
跟其它 step 类型一致(get_workflow_run_status(verbose=true)/test_workflow_step 都适用),但各有一个需要单独注意的点:
foreach:- 整体状态是
done不代表每个元素都成功——foreach_stop_on_error
默认false,失败元素会以{"item_index": i, "error": "..."}的形式
记入聚合结果的 JSON 数组,需要看聚合结果本身才能知道哪些元素失败了、
为什么失败。如果用户希望"只要有一个元素失败就应该整体失败",检查show_workflow里这一步的foreach_stop_on_error是不是漏设成true,而不是去改内层foreach_step的重试参数。 test_workflow_step对foreach同样适用,但沙箱测的是整个foreachstep(会真实对items里所有元素跑一遍内层逻辑),想单独
验证"某一个元素的处理逻辑对不对",更好的办法是把foreach_step里的
定义单独抽出来构造一个临时的普通 step(同样的type/prompt等
字段,把{item}/{item_index}换成具体值)走一次test_workflow_step,而不是直接对整个foreach跑沙箱、每次都要等
全部元素跑完。items解析失败(不是合法 list)或引用的占位符解析不出值,会在
执行期报错,先用show_workflow确认items引用的上游 step 是否
真的产出了预期的列表(尤其是{step.result_file:field}这种写法,
先确认该字段在result_fileJSON 里确实存在)。foreach_max_concurrency调大后如果发现结果乱序或互相干扰,注意这
是独立于外层allow_parallel的并发维度,排查并发问题时两者要分开看,
不要混在一起分析。
- 整体状态是
wait:如果一个waitstep 看起来"卡住不动",先确认这是不是预期
行为(wait_seconds设置过大),需要提前结束用cancel_workflow_run/pause_workflow_run,wait会在 0.5 秒粒度内
响应这些控制信号,不需要等到wait_seconds跑完;如果响应明显滞后,
说明控制信号没有正确传递,检查这次执行是不是走了绕过runner._current_ control的自定义调用路径(比如自己拼装WorkflowRunner没走标准run_workflow/resume_workflow_run)。merge:merge_strategy: json_merge报错"非 dict",说明merge_sources
里某个来源的output(或result_file,取决于merge_use_result_file)
解析出来不是 JSON object——先用get_workflow_run_status(verbose=true)
看具体是哪个来源,再决定是改那个上游 step 的产出格式,还是把merge_strategy换成更宽松的json_array。merge_strategy: json_array且某个来源解析失败时会退化为原始文本
元素而不是报错,如果发现数组里混入了不该有的原始文本,说明某个
上游来源的输出不是合法 JSON,检查是不是merge_use_result_file
设置和该来源是否声明了result_file不匹配。merge_sources引用的 id 没有出现在depends_on里会在patch_workflow_step/save_workflow阶段直接报错,不会等到运行期
才发现,遇到这类报错直接照提示把缺的 id 加进depends_on。
workflow 级熔断(P14):circuit_breaker_distinct_step_threshold
如果好几个不同 step 在同一次运行里因为同一个 error_type(比如
某个外部 API 挂了)先后失败,且 agent_config.json 里配置了{"workflow": {"circuit_breaker_distinct_step_threshold": N}},达到阈值
后 watchdog 会主动 request_cancel() 提前结束整次运行,而不是让每个
step 各自耗尽 retry_on_error 预算再各自报错——这跟"同一个 step 内部连续
失败"的 escalate_after_n_same_failures 是两回事,即使每个 step 都只失败
了一次(没有触发各自的 escalate_after_n_same_failures),也可能因为熔断
被提前取消。
排查方式:
get_workflow_run_status(workflow_session_id, verbose=true)看有没有circuit_breaker_tripped=true和熔断原因(记录了触发的error_type
以及哪些 step 因此被判定为系统性问题)。- 看到熔断触发时,不要逐个 step 分别调大
retry_on_error/timeout
去掩盖——先定位这个共同的error_type到底是什么外部依赖出的问题
(网络、某个第三方 API、某个共享资源),修好那个根因,或者在确认这次
确实是巧合、不是系统性故障时,用resume_workflow_run正常续跑。 - 如果发现熔断阈值设得太敏感(正常的偶发失败也会触发),提示用户去调整
agent_config.json里的circuit_breaker_distinct_step_threshold
(调大阈值,或设为null关闭),这是全局配置,不是某个 workflow 的patch_workflow_step能改的字段。 - 触发熔断后已经在跑/待跑的 step 会在下一次批次边界被取消,
resume_ workflow_run续跑前建议先确认根因是否已经解决,否则大概率会再次触发
熔断。
script 的 result_file structured 模式排查(P15)
script 类型声明了 result_file 后,即使子进程 returncode == 0
(脚本自己判定的"成功"),runner 仍会额外校验 result_file 是否存在、
是否合法 JSON、是否包含 result_file_required_keys 列出的字段——校验
不通过时整个 step 会被判定失败,即使脚本本身跑得很"顺利"。排查时:
- 先确认脚本是否真的把结构化结果写到了
$WORKFLOW_RESULT_FILE_PATH
这个环境变量指向的路径——这是script唯一被告知目标路径的方式(不像skill_agent是在 prompt 文字里提示),脚本如果没有读这个环境变量、
还是按老路径写文件或者只print到 stdout,result_file校验必然
失败。 get_workflow_run_status(verbose=true)的报错信息里会附带脚本的stdout/stderr,先看这两个字段确认脚本执行本身是否符合预期,再判断
是"脚本逻辑错"还是"忘了写result_file"。- 与
skill_agent的关键区别:script是一次性子进程,没有可resume的会话上下文,校验失败不会像skill_agent那样原地resume补写文件重试,只能靠retry_on_error整个重跑该 step——如果
脚本逻辑本身没问题、只是偶发写文件失败,调大retry_on_error是合理
的;如果是脚本逻辑本身没有实现写文件这一步,重试多少次都没用,需要patch_workflow_step改script内容。 - 未声明
result_file的既有scriptstep 行为完全不受影响(stdout
即结果),如果排查的是一个没有result_file字段的scriptstep,
本节内容不适用,回到常规的"看 stdout/stderr"排查路径。
用 events.jsonl 排查"卡在哪一步"/耗时异常
.agent/workflow_sessions/<id>/events.jsonl 里每个 step 正常应该有一对step_start(开始执行前)+ step_end(执行完毕,带 duration_seconds/status/retries_used)。怀疑某次执行"卡住了"但还没到能看get_workflow_run_status 终态的地步时,直接读这个文件比反复轮询状态更
直接:
cat .agent/workflow_sessions/<id>/events.jsonl | python3 -c \
"import json,sys; [print(json.loads(l)['event'], json.loads(l).get('step_id'), json.loads(l).get('status','')) for l in sys.stdin]"- 只有
step_start没有对应step_end的 step,就是当前正卡着的那一步——
再结合它的type判断:skill_agent/agent大概率是还在跑多轮工具
调用(参考上一节),python_step卡住通常是脚本本身死循环/外部调用
没超时,可以直接去看script_path对应脚本有没有网络调用忘了设
timeout。 - 如果同一个
step_id出现了多组step_start/step_end,说明这一步被
重跑过(retry_on_error内部重试,或多次resume_workflow_run),按
时间顺序看最后一组的status才是最终结果,前面几组只是历史记录。 - 只有
step_end没有step_start的记录,是旧版本(step_start事件是
后补的)留下的历史 session,不代表当前这次执行有问题。
只是想临时调参数、不想动定义(step_overrides)
resume_workflow_run 的 step_overrides 参数用于"这次先试试放宽点限制,
但不确定要不要永久改"这类场景:
resume_workflow_run(
workflow_session_id="<id>",
step_overrides='{"analyze": {"timeout": 300, "retry_on_error": 2}}'
)- 只影响这一次 resume 执行,不写回
workflow.yaml,下次run_workflow/resume_workflow_run不会带上这次的覆盖;get_workflow_run_status的输出会带一行提示标注"本次使用了临时覆盖",
避免误以为这是 workflow 定义本身的行为。 - 只允许覆盖执行参数类字段的白名单:
timeout/retry_on_error/allow_parallel/model/escalate_after_n_same_failures。不允许
覆盖prompt/condition/tool_name等会改变 step 语义的字段——那类改动
本质是"改逻辑",必须走patch_workflow_step留痕,不能用"临时覆盖"的
名义绕过;传了白名单外的字段会被拒绝。 - 想验证"是不是单纯超时"、"并发是不是导致了资源竞争"这类假设时,这个
参数比patch_workflow_step更合适,因为不需要事后再 patch 回去。force_serial=true(run_workflow/等价场景)同理,是"这一次强制串行"
而不改allow_parallel定义本身。
常见坑
调大
retry_on_error/max_turns不是万能修法:如果失败原因是"这一步
本该是python_step却生成成了agent/skill_agent",调重试次数只会
多花 token、不会提升成功率,见上文"用选型规则诊断不稳定的 agent 类型
step",先判断选型对不对,再决定要不要调参数。needs_fix状态重跑没有用:这是设计上的"保存期/运行期区分"——
结构性错误(占位符写错、tool_name不存在等)无论重试多少次结果都一样,
runner 会直接跳过retry_on_error预算改判needs_fix。看到这个状态
别再无脑resume_workflow_run,先改定义。test_workflow_step不是"假执行":它仍然会真的调用 LLM/工具,
只是不接入 DAG、不落盘历史——如果 step 里有副作用(写文件、调用外部
API),沙箱测试一样会触发这些副作用,只是"不算作正式一次 workflow
执行"。有强副作用的 step 沙箱测试前要想清楚。mock_step_results结构:key 是上游 step 的 id,value 是一个包含output/score等字段的 dict,不是直接给字符串;condition用到的.passed之类自定义字段如果 prompt 没用到可以不用补。force_rerun_fromvs 不传:不传时是"接着跑断点之后没完成的部分",
如果目标 step 之前已经是done状态,不会被重新执行;确实要重跑某个
已经"成功过"的 step(比如它的输出其实是错的,只是没被判定为失败),
必须显式传force_rerun_from=<该step_id>。step_overrides不持久化:改完发现确实需要长期生效,要额外补一次patch_workflow_step把同样的值写进定义,step_overrides只管这一次。get_workflow_run_status默认verbose=false:调试时几乎总是要开verbose=true,否则看不到traceback/context,只能看到状态和粗略
错误信息。patch_workflow_step的patch字段名必须是WorkflowStep已有属性:
传入未知字段名会被直接拒绝并报出具体是哪个字段名不认识,不会静默忽略;
不确定某个字段是不是这个 step 类型该有的(比如给agent类型 step 传了tool_name),可以传,patch_workflow_step本身不按type过滤,但wf.validate()未必会报错——这种"类型不匹配但字段合法"的情况不会被
自动拦下来,改完最好用show_workflow/test_workflow_step确认真的
生效了预期的效果。文件夹模式 workflow 报
workflow_dir 未设置:如果是在resume_workflow_run
之后才出现(首次run_workflow没问题),说明 resume 走的是workflow_def.yaml快照重新解析,而不是重新WorkflowStore.load(name);
当前实现已经会在 resume 时按 workflow 名字重新定位一次原始目录、补齐source_dir/展开prompt_file/script_path。如果你在自己的插件或二次
开发里绕过了resume_workflow_run、自己拼装WorkflowDef去调WorkflowRunner.run(),要记得这一步不能省,否则python_step里ctx.load_prompt_file()会直接报错。改动前后都可以
preview_workflow:不确定这次改动会不会影响并发
分批、condition 求值结果,patch_workflow_step保存成功后可以再preview_workflow(name, inputs)看一眼 dry-run 结果,比直接跑一遍正式
执行更省成本。tool_call的tool_args里占位符没生效:先确认引用的 step 是否
在depends_on里声明过(P12 起tool_args占位符和 prompt 占位符走
同一套depends_on校验),字面量值(不含{...})本身不会被替换,
这是设计如此、不是 bug。{step_id.result_file:a.b[0].c}占位符原样保留、没被替换:这是
容错设计,不是报错——路径取不到值、或结果文件不是合法 JSON 时占位符会
原样保留而不是抛异常中断整个 step。排查时先确认result_file那次
校验是否真的通过(没通过就不会有文件可读),再确认路径写法(a.b[0].c
这种"属性 + 数字下标"链式路径,不支持完整 JSONPath 语法)是否跟 JSON
实际结构对得上。foreach整体done但用户反馈"有几条数据处理错了":不是 bug,是foreach_stop_on_error=false的默认行为,去看聚合结果 JSON 数组里对应item_index的error字段,不要先怀疑 runner 出问题。workflow 熔断触发后逐个 step 加
retry_on_error:熔断是跨 step 的
系统性信号(同一error_type导致多个不同 step 失败),根因通常是某个
共享的外部依赖,逐个调大重试次数解决不了问题,反而会让下次熔断触发得
更慢、浪费更多 token 才发现同样的根因。script声明了result_file却还在只看returncode:structured
模式下returncode == 0不等于整体成功,result_file校验失败会让
runner 判定整个 step 失败,即使 stdout 看起来一切正常。output_export_dir复制失败不算 workflow 失败:跑到终态(paused
不算终态)后才会尝试复制,复制本身出错(权限/磁盘/路径不存在等)只会在events.jsonl的output_exported事件里带error字段、并打印一行
警告,status仍然按 workflow 本身的执行结果判定——排查"文件没导出"时
第一步永远是先看output_exported事件的error字段,而不是去怀疑
workflow 本身跑失败了。
长期健康度:get_workflow_stats
用户问"这个 workflow 稳不稳"、"哪一步经常出问题"、"要不要调整某个 step
的配置"时,不要靠翻 list_workflow_runs 人工数,直接:
get_workflow_stats(name="<workflow>")返回整体成功率、每个 step 的出现次数/失败率/平均耗时/平均评分/平均重试
次数,以及有 condition 的 step 的命中率(未被跳过的比例)。这是纯粹对
已落盘的历史执行记录做聚合,不会触发任何新执行。看到某个 step 失败率/
平均重试次数明显偏高,是"该考虑给它调 timeout/retry_on_error/拆分
这一步"的信号——但具体怎么改还是要结合 get_workflow_run_status(verbose= true) 里的具体错误原因,不要只凭统计数字瞎猜。
运行时调试日志:debug_log 与 /workflow debug(P11 §6)
failed/needs_fix 之外,还有一类排查诉求是"这一步执行成功了,但结果
不对/看着奇怪,想知道它到底实际收到了什么"——StepResult 默认只在出错时
留痕上下文,正常执行完的 step 不会保留替换占位符后的最终 prompt。
开启方式:agent_config.json 里 {"workflow": {"debug_log_enabled": true}}(默认关闭,避免长期运行的 workflow session 目录体积膨胀)。开启后
每个 step 执行完(无论成功失败)都会在 StepResult.debug_log 里记录:
resolved_prompt:_resolve_prompt替换占位符后的最终文本(仅
prompt 驱动的 step 类型有值),事后复盘"为什么这一步给出了奇怪的回答"
不再需要人工重算占位符替换。unresolved_placeholders:这次执行里"形如{xxx}、在inputs里没找到
对应 key"的占位符列表——即使_resolve_prompt本身会静默保留原样不报错,
这里也能看到确实发生过。upstream_step_ids_used:实际引用/读取到的上游 step_id,可以直接跟depends_on声明 diff,判断是不是"用到了没声明的依赖"。started_at/finished_at/thread_id/batch_index:并发排查"这两个
step 是不是真的并发跑了/是不是被串行化了"的直接证据,不用再靠肉眼估算duration_seconds时间戳是否重叠。subprocess_stdout/subprocess_stderr:仅python_step/script类型,
成功时也会保留(此前只有失败时才会把子进程输出塞进错误消息里),脚本
里用print()打的调试信息现在不需要失败才能看到。
查看方式:
get_workflow_run_status(workflow_session_id, verbose=true)会展示debug_log的关键字段摘要(按debug_log_max_chars,默认 4000 字符
截断)。- 想看某一步完整的
debug_log(不受 verbose 摘要长度限制),用/workflow debug <workflow_session_id> <step_id>CLI 子命令(REPL 内
或mini-agent workflow debug ...独立命令行都可以)。
排查 python_step 脚本问题时特别有用:升级到 P11 之后,python_step
的 ctx.inputs 默认按 depends_on 过滤(不再是全部已跑完的 step),如果
脚本报"读不到预期的上游数据",先看 debug_log.upstream_step_ids_used 和
该 step 的 depends_on 是否一致——大概率是脚本引用了一个没写进depends_on 的 step_id,应该先 patch_workflow_step 把它加进depends_on,而不是关掉 python_step_inputs_filtered_by_depends_on 开关
绕过(那是给排查用的临时回退,不建议作为正式修复手段)。
与 undeclared_dependency_usage 的关系:保存阶段的静态校验(占位符depends_on 一致性检查、python_step 输入过滤)已经把大部分"用了未声明
依赖"的问题拦在运行之前;只有显式关闭对应开关时,运行期才会在debug_log["undeclared_dependency_usage"] 里留下兜底记录(同时上报给
watchdog),这条记录只做记录、不改变该 step 的执行结果。
沙箱测试 / 续跑时如何验证 python_step
test_workflow_step 对 python_step 同样适用(会真实拉起子进程执行脚本,
不是假执行),但要注意:
mock_step_results需要覆盖脚本depends_on里声明的所有上游 step,
key 要和depends_on列表完全对应——沙箱测试同样遵循"ctx.inputs按depends_on过滤"这条 P11 规则,mock 数据里多给的 key 不会被脚本看到。- 脚本如果调用了
ctx.llm,沙箱测试会真实触发 LLM 调用(子进程独立建一条LLMHelper,不跟随主 Agent 当前/model状态),产生真实 token 消耗,
这点和agent类型 step 的沙箱测试是一致的代价。 - 脚本内
print()输出想在沙箱测试时也能看到,需要先开启debug_log_enabled,否则子进程 stdout/stderr 默认不落进沙箱测试的返回
结果里(沙箱测试本身不写入workflow_runs历史,但仍然复用同一套debug_log填充逻辑)。
命令行直接跑(不进 Agent 对话)
以上所有工具在 Agent 对话之外还有一条等价路径:mini-agent workflow ...
独立命令行(src/mini_agent/cli/commands/workflow_cmd.py::run_workflow_cli,
在 cli/app.py 里与 daemon/user/self/eval 一样按 sys.argv[1]
短路,不进入交互 REPL、不构造 Agent,只 load_config())。子命令名和参数
与 REPL 里的 /workflow ... 完全一致,前缀换一下即可:
mini-agent workflow run <name> ['{"key":"value"}'] [--background] [--project <path>]
mini-agent workflow status <workflow_session_id> [--project <path>]
mini-agent workflow resume <workflow_session_id> [--background] [--project <path>]
mini-agent workflow stats <name> [--project <path>]用户想要"脚本里/cron/systemd 里直接跑一个已保存的 workflow,不想为此启动
一整个交互式 Agent 会话"时,这是该走的路径,不是让用户手写 Python 调WorkflowRunner,也不是建议他们进 REPL 敲 /workflow run。
不传
--project/-p时默认用当前工作目录作为项目根,找不到 workflow
时排查的第一件事是确认cwd/--project对不对。--background在独立 CLI 下的语义和 REPL 里不一样:REPL 里--background是"这个长期存活的进程内起一个后台线程";独立 CLI 一次性
命令跑完就退出,同样起线程的话线程会被一起杀掉、工作流根本跑不完——
所以这里改成 spawn 一个完全独立的 OS 子进程(start_new_session=True,
不继承父进程的进程组),父进程立刻返回打印workflow_session_id和
一个日志文件路径(子进程 stdout/stderr 重定向到<workflow_sessions目录>/<id>/cli_detached.log),即使触发它的 shell/
cron 已经退出,子进程也会独立跑完。用户要的是"挂后台,进程退出后还在
跑"时,一定要提醒用户加--background,不加的话是前台同步阻塞执行,
命令不会提前返回。后续查进度/续跑/审批,全部继续用同一套工具(
get_workflow_run_status/patch_workflow_step/resume_workflow_run等,走 Agent 对话)或对应的mini-agent workflow status|resume|...命令行子命令都可以,两条路径共享
同一份落盘的WorkflowSession状态,互相看得到彼此的执行记录,不是两套
隔离的东西。mode: autonomous的 workflow 天生适合配这条路径:所有参数在启动时
通过inputs_json一次性给全,配合--background挂到独立子进程里跑,
适合直接写进 crontab/systemd timer,不需要人在旁边盯着交互式会话。
注意:目前 CLI 子命令的run只暴露了inputs_json/--background
两个参数,run_workflow工具里的require_all_inputs_upfront/force_serial这两个开关暂时没有对应的 CLI flag——想用这两个开关,还是
要走 Agent 对话调工具,或者先确认这次真的需要它们再考虑要不要给 CLI
补上(工具层参数不会自动透传到命令行)。从自然语言描述从零生成一个新 workflow → workflow-generator skill。
已有 workflow 想测试某一步、排查一次失败、改某个字段、
换个方式续跑 → 本 skill。两者会共用同一份
WorkflowStep/WorkflowDef字段语义(见
workflow-generator skill 的字段表),本 skill 不重复维护字段说明,只
聚焦"已有 workflow 出问题/要验证时该调哪个工具、按什么顺序调"。