一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

造数脚本越堆越乱:稳定的沉淀成引擎 变化的留在配置

时间:2026-07-21 17:31:58 编辑:袖梨 来源:一聚教程网

写在前面

适合谁看: 测试开发、后端开发、平台工程师、正在做 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()

这个脚本能跑通一个场景。

但问题是,企业造数很少只有一个场景。

真实系统里会有很多变化:

  • 不同订单类型走不同创建接口。
  • 不同组织需要不同参数。
  • 有些场景要查合同,有些场景不用。
  • 有些物料要额外校验。
  • 有些字段为空时不能传。
  • 审批和下游推送需要等待。
  • SQL 查询必须避免查到历史数据。
  • 失败后要能知道卡在哪一步。

如果继续复制脚本,最后会变成:

场景 A 脚本场景 B 脚本场景 C 脚本场景 D 脚本...

每个脚本里都有相似的 HTTP、SQL、模板、审批、等待、重试逻辑,但又夹杂着少量业务差异。

这就是最难维护的状态:大量相似,但没有抽象。

所以我没有继续堆脚本,而是做了一个轻量编排引擎。

这个项目里,我真正投入最多的不是某一个接口调用,而是抽象流程语言:哪些能力应该成为 Action,哪些应该留在配置里,哪些应该沉淀为共享步骤。这个抽象决定了系统能不能从“跑通一个场景”扩展到“维护一类场景”。

二、整体设计:YAML 是流程 DSL,不是参数表

很多人一听 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 函数调用顺序里,而是变成了可以被阅读、校验、复用的配置。

三、9 种 Action:核心 6 种,扩展 3 种

我没有设计很多 Action。

Action 太多,配置会变得难学;Action 太少,又表达不了真实业务。

最后沉淀下来的是“核心 6 种 + 扩展 3 种”。

类型Action作用是否调用 Dify
核心http_call调业务接口
核心sql_query查询数据库状态
核心template渲染复杂请求体
核心condition条件分支
核心switch多路分支
核心loop遍历集合并执行子步骤间接调用
扩展route按业务类型路由到子流程
扩展sql_execute必要时做测试数据修正是 / 可配置
扩展filter_collection对查询结果做集合过滤

这 9 种 Action 的边界很重要。

http_callsql_query 是执行动作,负责和外部系统交互。
templateconditionswitchloop 是流程表达动作。
routesql_executefilter_collection 是复杂企业场景里补出来的扩展动作。

我不希望 Action 变成“一个业务接口一个 Action”。

例如不要设计成:

create_purchase_requestapprove_purchase_requestquery_contractcreate_purchase_orderapprove_purchase_orderquery_final_no

这种设计看起来语义更强,但扩展性会变差。

更稳的抽象应该是:

http_call + sql_query + template + condition + loop + route

业务语义放在 YAML 和模板里,底层动作保持通用。

四、Action 逐个拆开看

1. http_call:业务接口统一入口

http_call 负责调业务接口。

它不关心这是创建申请、触发审批,还是重新推送下游。它只关心:

  • URL 是什么。
  • Method 是什么。
  • Header 是什么。
  • Body 是什么。
  • 返回结果怎么提取。

示例:

- 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 生成。

2. 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
  • 查不到时要结合 waitretry 判断是异步延迟还是业务失败。

3. 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 的价值是把请求体结构从流程逻辑里分离出来。

4. condition:二选一分支

condition 用于简单判断。

例如创建成功继续,创建失败终止:

- id: check_create_resultaction: conditionif: "{{ steps.create_order.extracted.code }} == '0'"then: query_orderelse: END_CREATE_FAIL

我不建议把特别复杂的业务表达式都塞进 condition

如果判断越来越复杂,应该拆成多个步骤,或者沉淀成更明确的校验 Action。

5. 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 适合多选一。

这个边界清楚后,配置会更容易读。

6. 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_results
  • contract_query_results
  • status_update_results

7. route:按业务类型进入子流程

当场景差异很大时,不适合在一个 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 文件无限膨胀。

8. 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 要谨慎使用。

我的建议是:

  • 只用于测试环境。
  • 只用于明确的测试数据修正。
  • SQL 必须绑定当前流程上下文。
  • 必须有日志和审查。
  • 公开文章不展示真实库表和真实 SQL。

9. 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 当前项
collectedloop 收集到的批量结果

一个典型引用长这样:

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 有两个价值:

  1. 把通用能力沉淀到共享步骤。
    场景文件只保留业务差异。

  2. 让变更集中。
    如果审批逻辑调整,只需要改共享步骤,不需要改所有场景文件。

这也是配置化能不能长期维护的关键。

配置化不是把所有内容摊平到 YAML 里,而是要继续分层:公共步骤归公共步骤,场景差异归场景文件。

七、错误处理、等待和状态快照

造数链路里,失败是常态。

常见失败包括:

  • 参数缺失。
  • 接口返回业务错误。
  • 数据还没落库。
  • 审批状态还没更新。
  • SQL 查不到结果。
  • 路由目标写错。
  • 模板文件不存在。

所以编排引擎不能只设计成功路径。

1. 等待和重试

对于异步链路,可以在步骤上声明等待和重试:

- id: query_final_noaction: sql_querywait: 5retry:max: 3interval_ms: 5000

这样流程不会因为下游慢几秒就立即失败。

2. 终止状态

失败时不要只抛异常,最好能给出明确终止原因:

terminals:END_CREATE_FAIL:type: failmessage: "创建申请失败,请检查参数或接口返回"END_SUCCESS:type: successmessage: "造数完成,订单号:{{ steps.query_order.extracted.order_no }}"

终止状态让用户看到的是业务语义,而不是一串堆栈。

3. 状态快照

长链路最怕跑到第 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 工具编排,我建议按这个顺序设计。

1. 先抽动作,不要先抽业务名词

不要一上来就设计很多业务专用 Action。

先抽稳定动作:

  • HTTP。
  • SQL。
  • 模板。
  • 条件。
  • 分支。
  • 循环。
  • 路由。

业务差异尽量放到配置和模板里。

2. Action 要少,但上下文要清楚

Action 少,学习成本低。
上下文清楚,组合能力才强。

至少要定义清楚:

  • 用户输入在哪里。
  • 环境配置在哪里。
  • 上一步结果在哪里。
  • 循环当前项在哪里。
  • 批量结果在哪里。

3. 共享步骤要早做

只要第二个场景开始复制同样的步骤,就应该考虑共享。

共享步骤能把团队经验沉淀下来,也能减少场景文件里的噪音。

4. 静态校验要早做

不要等配置变多之后才补校验。

一开始就校验 action、step id、goto、route、template、敏感信息,会少掉很多低级故障。

5. 不要把 YAML 写成另一种代码

这是最容易走偏的地方。

如果 YAML 里出现越来越复杂的表达式、越来越长的业务判断、越来越多的临时字段,说明抽象可能需要调整。

YAML 应该描述流程,不应该承载所有业务计算。

十一、总结:Action 设计决定系统能走多远

这个造数 Skill 的编排引擎,本质上不是为了“让 YAML 看起来高级”。

它解决的是一个很实际的问题:如何把复杂业务造数流程,从个人脚本升级成团队可维护的工程资产。

核心设计可以概括成几句话:

  • 用 YAML 描述流程,而不是把流程写死在脚本里。
  • 用 9 种 Action 覆盖大多数造数动作。
  • 用 Jinja2 管理复杂请求体。
  • inputsconfigstepscontextcollected 传递上下文。
  • use 沉淀共享步骤。
  • 用等待、重试、终止状态、状态快照处理失败路径。
  • 用静态校验把低级错误挡在执行前。
  • Dify 只作为 HTTP / SQL 执行通道,不承载业务编排复杂度。

对我来说,这个项目最有价值的地方,不是跑通了某一条链路,而是把“怎么造数”这件事抽象成了一套可描述、可复用、可校验的流程语言。

FAQ

1. 为什么是 9 种 Action,不是更多?

Action 太多会增加学习和维护成本。当前核心 6 种加扩展 3 种,已经能覆盖 HTTP、SQL、模板、条件、分支、循环、路由、数据修正和集合过滤,大多数造数链路可以组合出来。

2. YAML 会不会变得很复杂?

会有这个风险。所以需要共享步骤、模板拆分和静态校验。YAML 应该描述流程,不应该承载所有业务计算。

3. 为什么不用低代码平台直接画完整流程?

短流程可以直接画。复杂企业造数链路涉及多分支、多模板、多 SQL、多异步状态和失败恢复,全部放进低代码平台会变成难维护的节点巨图。

4. conditionswitch 怎么区分?

condition 适合二选一判断,例如成功或失败。switch 适合枚举型多路分支,例如不同订单类型进入不同步骤。

5. routeswitch 有什么区别?

switch 通常在当前流程内跳转。route 更适合把不同业务类型分发到不同子流程文件,避免一个 YAML 文件无限膨胀。

6. 共享步骤的价值是什么?

共享步骤能把通用的鉴权、查询、审批、结果获取等能力沉淀下来。场景文件只保留差异,后续修改公共逻辑也更集中。

7. 这个设计最大的个人价值体现在哪里?

价值在于抽象能力:识别哪些能力应该成为通用 Action,哪些差异应该留在 YAML,哪些步骤应该共享,哪些错误应该由静态校验提前发现。这个抽象决定系统能不能从一个场景扩展到一类场景。

热门栏目