作者:互联网 时间: 2026-07-22 08:53:02
适合谁看: 测试开发、后端开发、平台工程师、正在做 AI Agent 工具工程化的人。
核心问题: 复杂测试造数流程,如何从脚本自动化升级成配置驱动的编排系统?
核心结论: YAML 不是简单参数表,而是一个小型流程 DSL;Action 数量要少,但表达力要够。核心 6 种 Action 加扩展 3 种 Action,已经能覆盖大多数企业造数链路。
一句话答案: 把稳定能力抽成 Action,把业务差异放进 YAML,把复杂请求体交给模板,把执行通道留给 Dify。
系列定位: 这是执行骨架篇。第 3 篇讲多模态如何把截图变成参数;本篇讲这些参数进入引擎后,如何被 YAML 和 Action 稳定编排。
关键边界: 多模态不是第 10 种 Action,它发生在 Action 之前;Action 只消费已经结构化、已校验的参数。
在做造数 Skill 之前,最直接的方案是写脚本。
造数脚本最麻烦的地方,不是第一条链路难写,而是第五条、第十条链路开始出现时,你会发现每个脚本都长得很像,但谁也不敢合并。
它们都有 HTTP、SQL、审批、等待、重试、结果查询;又都有一点点业务差异。继续复制,短期最快,长期最难维护。
我在这个项目里做的关键工作,不是多接了几个接口,而是把“造数流程里反复出现的动作”抽象成 9 种 Action,让业务差异留在 YAML,让稳定能力沉淀到引擎。
比如一条采购订单测试数据,可以写成这样:
check_material()create_request()query_request_no()approve_request()query_contract()create_order()approve_order()query_final_no()
这个脚本能跑通一个场景。
但问题是,企业造数很少只有一个场景。
真实系统里会有很多变化:
如果继续复制脚本,最后会变成:
场景 A 脚本场景 B 脚本场景 C 脚本场景 D 脚本...
每个脚本里都有相似的 HTTP、SQL、模板、审批、等待、重试逻辑,但又夹杂着少量业务差异。
这就是最难维护的状态:大量相似,但没有抽象。
所以我没有继续堆脚本,而是做了一个轻量编排引擎。
这个项目里,我真正投入最多的不是某一个接口调用,而是抽象流程语言:哪些能力应该成为 Action,哪些应该留在配置里,哪些应该沉淀为共享步骤。这个抽象决定了系统能不能从“跑通一个场景”扩展到“维护一类场景”。
很多人一听 YAML,会觉得它只是配置文件。
但在这个造数 Skill 里,YAML 承担的是流程描述能力。
它要表达:
一个脱敏后的简化示例:
name: sample-order-flowinputs:orderType:required: truesupplierCode:required: truematerials:required: truetype: liststeps:- id: build_requestaction: templatetemplate: templates/order-request.json.j2context:orderType: "{{ inputs.orderType }}"supplierCode: "{{ inputs.supplierCode }}"materials: "{{ inputs.materials }}"- id: create_requestaction: http_callparams:url: "{{ config.api_base }}/purchase/request"method: POSTbody: "{{ steps.build_request.output }}"extract:code: "$.body.code"request_no: "$.body.data.requestNo"- id: check_resultaction: conditionif: "{{ steps.create_request.extracted.code }} == '0'"then: query_requestelse: END_CREATE_FAIL
这里的关键不是 YAML 语法,而是这套结构背后的模型。
我把每一步都抽象成:
step = id + action + params/context + extract + goto
也就是说,一个步骤至少要回答四个问题:
| 问题 | 对应字段 |
|---|---|
| 这一步叫什么 | id |
| 这一步做什么 | action |
| 这一步需要什么输入 | params / context |
| 这一步产出什么结果 | extract / output |
| 执行完去哪 | goto / then / else |
这样做之后,流程不再藏在 Python 函数调用顺序里,而是变成了可以被阅读、校验、复用的配置。
我没有设计很多 Action。
Action 太多,配置会变得难学;Action 太少,又表达不了真实业务。
最后沉淀下来的是“核心 6 种 + 扩展 3 种”。
| 类型 | Action | 作用 | 是否调用 Dify |
|---|---|---|---|
| 核心 | http_call | 调业务接口 | 是 |
| 核心 | sql_query | 查询数据库状态 | 是 |
| 核心 | template | 渲染复杂请求体 | 否 |
| 核心 | condition | 条件分支 | 否 |
| 核心 | switch | 多路分支 | 否 |
| 核心 | loop | 遍历集合并执行子步骤 | 间接调用 |
| 扩展 | route | 按业务类型路由到子流程 | 否 |
| 扩展 | sql_execute | 必要时做测试数据修正 | 是 / 可配置 |
| 扩展 | filter_collection | 对查询结果做集合过滤 | 否 |
这 9 种 Action 的边界很重要。
http_call 和 sql_query 是执行动作,负责和外部系统交互。template、condition、switch、loop 是流程表达动作。route、sql_execute、filter_collection 是复杂企业场景里补出来的扩展动作。
我不希望 Action 变成“一个业务接口一个 Action”。
例如不要设计成:
create_purchase_requestapprove_purchase_requestquery_contractcreate_purchase_orderapprove_purchase_orderquery_final_no
这种设计看起来语义更强,但扩展性会变差。
更稳的抽象应该是:
http_call + sql_query + template + condition + loop + route
业务语义放在 YAML 和模板里,底层动作保持通用。
http_call:业务接口统一入口http_call 负责调业务接口。
它不关心这是创建申请、触发审批,还是重新推送下游。它只关心:
示例:
- id: create_orderaction: http_callparams:url: "{{ config.api_base }}/purchase/order"method: POSTheaders:Authorization: "{{ context.auth_header }}"body: "{{ steps.build_order_body.output }}"extract:code: "$.body.code"message: "$.body.message"order_no: "$.body.data.orderNo"
这里要注意:复杂请求体不在 http_call 里拼,而是前一步由 template 生成。
sql_query:查询中间态和结果sql_query 负责查流程状态。
在造数链路里,很多关键结果不是接口直接返回的,而是需要查数据库确认:
示例:
- id: query_orderaction: sql_querywait: 5retry:max: 3interval_ms: 5000params:db_uri: "{{ config.db_uri }}"query: >SELECT order_noFROM purchase_orderWHERE request_no = '{{ steps.create_request.extracted.request_no }}'ORDER BY id DESCLIMIT 1extract:order_no: "$.rows[0].order_no"
SQL 查询的关键不是“查到一条”,而是“查到本次流程生成的那条”。
所以我会强调:
LIMIT。wait 和 retry 判断是异步延迟还是业务失败。template:复杂 JSON 的治理层企业接口请求体一复杂,就不适合直接写在 Python 里,也不适合塞在低代码节点里。
template 负责调用 Jinja2 模板生成请求体:
- id: build_order_bodyaction: templatetemplate: templates/order-body.json.j2context:supplierCode: "{{ inputs.supplierCode }}"materials: "{{ inputs.materials }}"orderType: "{{ inputs.orderType }}"
模板里可以处理:
template 的价值是把请求体结构从流程逻辑里分离出来。
condition:二选一分支condition 用于简单判断。
例如创建成功继续,创建失败终止:
- id: check_create_resultaction: conditionif: "{{ steps.create_order.extracted.code }} == '0'"then: query_orderelse: END_CREATE_FAIL
我不建议把特别复杂的业务表达式都塞进 condition。
如果判断越来越复杂,应该拆成多个步骤,或者沉淀成更明确的校验 Action。
switch:多路分支switch 适合处理枚举型分支。
例如不同订单类型走不同后续步骤:
- id: choose_flowaction: switchswitch_on: "{{ inputs.orderType }}"cases:- match: STANDARDgoto: build_standard_body- match: SPECIALgoto: build_special_bodydefault: END_UNSUPPORTED_TYPE
condition 适合二选一,switch 适合多选一。
这个边界清楚后,配置会更容易读。
loop:多物料、多记录的循环处理造数场景里经常会有集合:
loop 用来遍历集合并执行子步骤:
- id: check_materialsaction: loopitems: "{{ inputs.materials }}"loop_var: materialcollect: material_check_resultssteps:- id: check_one_materialaction: http_callparams:url: "{{ config.api_base }}/material/check"method: POSTbody:materialCode: "{{ context.material.code }}"extract:code: "$.body.code"message: "$.body.message"
这里有一个小但重要的经验:collect 不要都叫 results。
如果多个循环都叫 results,后面会覆盖前面的结果。语义化命名可以减少排障成本:
material_check_resultscontract_query_resultsstatus_update_resultsroute:按业务类型进入子流程当场景差异很大时,不适合在一个 YAML 里写满 switch。
这时可以用 route 路由到子流程:
- id: route_by_order_typeaction: routeswitch_on: "{{ inputs.orderType }}"routes:STANDARD: configs/standard-order.yamlSPECIAL: configs/special-order.yamlSEMI: configs/semi-finished-order.yamlgoto_fail: END_UNSUPPORTED_TYPE
这样主流程只负责分发,子流程负责各自细节。
这能避免一个 YAML 文件无限膨胀。
sql_execute:必要的数据修正正常情况下,造数工具应该尽量通过业务接口完成数据流转。
但在测试环境里,有些链路会遇到需要补齐测试状态或修正测试数据的情况。
这时可以提供受控的 sql_execute:
- id: fix_test_statusaction: sql_executeparams:db_uri: "{{ config.db_uri }}"query: >UPDATE test_orderSET status = 'READY'WHERE request_no = '{{ steps.create_request.extracted.request_no }}'
这个 Action 要谨慎使用。
我的建议是:
filter_collection:把大结果收敛成当前需要的数据有时接口或 SQL 会返回一批候选数据,但流程只需要其中几条。
例如用户输入了物料列表,接口返回了更多物料信息。此时需要按用户输入过滤:
- id: filter_materialsaction: filter_collectionsource: "steps.fetch_materials.extracted.materials_data"match_field: "materialCode"input_ref: "inputs.materials"output: "matched_materials"
filter_collection 的价值是让集合筛选显式化。
否则这类逻辑很容易散落在模板、SQL 或 Python 临时代码里。
编排引擎最重要的能力之一,是让步骤之间可以传递结果。
我把上下文分成几类:
| 上下文 | 作用 |
|---|---|
inputs | 用户输入的原始参数 |
config | 环境配置,如接口基址、数据库连接占位 |
steps | 每个步骤的执行结果和提取字段 |
context | 临时上下文,常用于 loop 当前项 |
collected | loop 收集到的批量结果 |
一个典型引用长这样:
body: "{{ steps.build_order_body.output }}"
或者:
query: >SELECT order_noFROM purchase_orderWHERE request_no = '{{ steps.create_request.extracted.request_no }}'LIMIT 1
循环里会用到 context:
body:materialCode: "{{ context.material.code }}"
循环结果会进入 collected:
items: "{{ collected.material_check_results }}"
这个上下文设计让步骤之间不需要硬编码 Python 变量,而是通过统一引用规则传递数据。
当场景变多后,会出现很多重复步骤:
如果每个场景 YAML 都复制这些步骤,维护成本会很高。
所以我引入了共享步骤,通过 use 引用。
示例:
use:- common-steps.get_authsteps:- use: common-steps.check_materials- use: common-steps.approve_request- id: custom_step_for_this_sceneaction: http_callparams:url: "{{ config.api_base }}/scene/custom"method: POST
use 有两个价值:
把通用能力沉淀到共享步骤。
场景文件只保留业务差异。
让变更集中。
如果审批逻辑调整,只需要改共享步骤,不需要改所有场景文件。
这也是配置化能不能长期维护的关键。
配置化不是把所有内容摊平到 YAML 里,而是要继续分层:公共步骤归公共步骤,场景差异归场景文件。
造数链路里,失败是常态。
常见失败包括:
所以编排引擎不能只设计成功路径。
对于异步链路,可以在步骤上声明等待和重试:
- id: query_final_noaction: sql_querywait: 5retry:max: 3interval_ms: 5000
这样流程不会因为下游慢几秒就立即失败。
失败时不要只抛异常,最好能给出明确终止原因:
terminals:END_CREATE_FAIL:type: failmessage: "创建申请失败,请检查参数或接口返回"END_SUCCESS:type: successmessage: "造数完成,订单号:{{ steps.query_order.extracted.order_no }}"
终止状态让用户看到的是业务语义,而不是一串堆栈。
长链路最怕跑到第 8 步失败,然后不知道前面发生了什么。
所以执行过程中要保存状态快照,包括:
状态快照的价值是:失败后可以复盘,不需要靠记忆排查。
当 YAML 变成流程 DSL 后,它就必须能被校验。
否则配置化会变成另一种不稳定。
我补了一层静态校验,用来检查:
| 校验项 | 目的 |
|---|---|
| YAML 能否加载 | 避免格式错误 |
| action 是否合法 | 避免写错动作 |
| step id 是否重复 | 避免结果覆盖 |
goto / then / else 是否存在 | 避免跳到不存在的步骤 |
route 子流程是否存在 | 避免运行中才发现配置缺失 |
| template 文件是否存在 | 避免执行到一半才失败 |
loop 是否使用默认 collect=results | 提醒改成语义化名称 |
| 是否存在明显敏感信息 | 避免 Token、连接串、内部地址泄露 |
这层校验的意义很直接:能在执行前发现的问题,不要拖到执行中。
对于企业内部工具来说,静态校验不是锦上添花。只要工具开始给别人用,配置错误就会成为稳定性问题。
做编排引擎之前,造数能力更像脚本集合。
做完之后,它变成了一个可扩展的流程系统。
| 原来 | 现在 |
|---|---|
| 每个场景一份脚本 | 每个场景一份 YAML |
| 请求体写在代码里 | 请求体放在 Jinja2 模板 |
| 流程顺序靠函数调用 | 流程顺序由 steps 描述 |
| 分支逻辑散落在 if 里 | condition / switch 显式表达 |
| 多物料处理靠临时代码 | loop + collect 统一表达 |
| 公共步骤复制粘贴 | use 引用共享步骤 |
| 执行失败靠人工猜 | 状态快照和终止状态辅助排障 |
| 配置错误运行时才发现 | 静态校验提前暴露 |
最重要的变化是:新增场景的方式变了。
以前是复制一份脚本,然后小心翼翼地改。
现在是组合已有 Action、复用共享步骤、补充场景 YAML 和模板。
这就是从“自动化脚本”到“编排引擎”的区别。
如果你也在做测试造数、企业流程自动化或 Agent 工具编排,我建议按这个顺序设计。
不要一上来就设计很多业务专用 Action。
先抽稳定动作:
业务差异尽量放到配置和模板里。
Action 少,学习成本低。
上下文清楚,组合能力才强。
至少要定义清楚:
只要第二个场景开始复制同样的步骤,就应该考虑共享。
共享步骤能把团队经验沉淀下来,也能减少场景文件里的噪音。
不要等配置变多之后才补校验。
一开始就校验 action、step id、goto、route、template、敏感信息,会少掉很多低级故障。
这是最容易走偏的地方。
如果 YAML 里出现越来越复杂的表达式、越来越长的业务判断、越来越多的临时字段,说明抽象可能需要调整。
YAML 应该描述流程,不应该承载所有业务计算。
这个造数 Skill 的编排引擎,本质上不是为了“让 YAML 看起来高级”。
它解决的是一个很实际的问题:如何把复杂业务造数流程,从个人脚本升级成团队可维护的工程资产。
核心设计可以概括成几句话:
inputs、config、steps、context、collected 传递上下文。use 沉淀共享步骤。对我来说,这个项目最有价值的地方,不是跑通了某一条链路,而是把“怎么造数”这件事抽象成了一套可描述、可复用、可校验的流程语言。
Action 太多会增加学习和维护成本。当前核心 6 种加扩展 3 种,已经能覆盖 HTTP、SQL、模板、条件、分支、循环、路由、数据修正和集合过滤,大多数造数链路可以组合出来。
会有这个风险。所以需要共享步骤、模板拆分和静态校验。YAML 应该描述流程,不应该承载所有业务计算。
短流程可以直接画。复杂企业造数链路涉及多分支、多模板、多 SQL、多异步状态和失败恢复,全部放进低代码平台会变成难维护的节点巨图。
condition 和 switch 怎么区分?condition 适合二选一判断,例如成功或失败。switch 适合枚举型多路分支,例如不同订单类型进入不同步骤。
route 和 switch 有什么区别?switch 通常在当前流程内跳转。route 更适合把不同业务类型分发到不同子流程文件,避免一个 YAML 文件无限膨胀。
共享步骤能把通用的鉴权、查询、审批、结果获取等能力沉淀下来。场景文件只保留差异,后续修改公共逻辑也更集中。
价值在于抽象能力:识别哪些能力应该成为通用 Action,哪些差异应该留在 YAML,哪些步骤应该共享,哪些错误应该由静态校验提前发现。这个抽象决定系统能不能从一个场景扩展到一类场景。