为什么 Workflow 需要 CI
代码改了,CI 跑测试发现问题。Workflow 的"代码"是 Markdown + YAML 文件,改了之后怎么知道没有破坏什么?
常见的三种漏网问题:
- 修改了
templates/analyze.md,新增了一个输出字段,忘记在workflow.md的context_inputs里声明。下游 Phase 收不到这个字段,悄悄失败 - 把路由的置信度阈值从 0.95 改成 0.9,路由逻辑测试没有更新,实际运行才发现阈值边界行为变了
- 删除了一个 template 文件,但
workflow.md还在引用它
这三类问题都能用自动化检查在提交阶段发现,不用等到运行时。
三级 CI 门控
markdown
Gate 1:静态校验(秒级,每次提交自动跑)
- 所有被引用的 template 文件存在
- config.yaml 引用的 Skill 在注册表中
- 每个 Phase 的 on_success / on_failure 目标存在
Gate 2:Schema 测试(分钟级,每次提交自动跑)
- 验证 context_inputs 声明与实际输出字段的对齐
- 不调用真实 LLM,只验证数据契约
- 对应 W5 评测篇里的 Layer 1 + Layer 2 测试
Gate 3:端到端回归(小时级,合并前跑)
- 用 eval/cases.yaml 中的 happy path 跑完整工作流
- 对比结果与基线指标
- 对应 W5 评测篇里的 Layer 3 测试
Gate 1:静态校验脚本
Gate 1 不调用 LLM,纯文件系统检查,秒级完成:
python
#!/usr/bin/env python3
# tools/validate_workflow.py
import sys
import yaml
from pathlib import Path
SKILL_DIR = Path("skills/wf-bug-e2e")
TEMPLATES_DIR = SKILL_DIR / "templates"
ERRORS = []
def check_template_references():
"""workflow.md 里引用的所有 template 文件必须存在"""
workflow_file = SKILL_DIR / "workflow.md"
content = workflow_file.read_text()
# 提取所有 template: xxx.md 引用
import re
refs = re.findall(r"template:\s*(\S+\.md)", content)
for ref in refs:
if not (TEMPLATES_DIR / ref).exists():
ERRORS.append(f"Template not found: templates/{ref} (referenced in workflow.md)")
def check_phase_routing():
"""workflow.md 里每个 Phase 的 on_success / on_failure 目标必须存在"""
workflow_file = SKILL_DIR / "workflow.md"
content = workflow_file.read_text()
import re
# 提取所有 phase_id 定义
phases = set(re.findall(r"^phase_(\w+):", content, re.MULTILINE))
# 提取所有路由目标
targets = re.findall(r"(?:on_success|on_failure|continue_to):\s*(\S+)", content)
reserved = {"END", "human_escalation", "gate_A", "gate_B", "gate_C"}
for target in targets:
phase_name = target.replace("phase_", "")
if target not in reserved and phase_name not in phases:
ERRORS.append(f"Routing target not found: '{target}' (check on_success/on_failure)")
def check_config_skills():
"""config.yaml 引用的所有 Skill 必须在 Skill 注册表中"""
config_file = SKILL_DIR / "config.yaml"
if not config_file.exists():
return
config = yaml.safe_load(config_file.read_text())
skill_registry_file = Path("skills/registry.yaml")
if not skill_registry_file.exists():
return # 没有注册表时跳过此检查
registry = yaml.safe_load(skill_registry_file.read_text())
registered_ids = {s["id"] for s in registry.get("skills", [])}
for phase_config in config.get("phases", {}).values():
skill_id = phase_config.get("skill")
if skill_id and skill_id not in registered_ids:
ERRORS.append(f"Skill not in registry: '{skill_id}' (check config.yaml)")
def main():
check_template_references()
check_phase_routing()
check_config_skills()
if ERRORS:
print("❌ Workflow validation failed:")
for e in ERRORS:
print(f" - {e}")
sys.exit(1)
print("✅ Workflow validation passed")
if __name__ == "__main__":
main()
接入 CI(GitHub Actions 示例):
yaml
# .github/workflows/workflow-ci.yml
name: Workflow CI
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pyyaml
- name: Gate 1 - Static validation
run: python tools/validate_workflow.py
schema-tests:
runs-on: ubuntu-latest
needs: validate
steps:
- uses: actions/checkout@v4
- run: pip install pytest
- name: Gate 2 - Schema tests
run: pytest tests/unit/ tests/integration/ -v
Gate 2:数据契约验证
Gate 2 的核心:验证每个 Phase 声明的 context_inputs 与上游 Phase 实际输出字段是否对齐。
python
# tests/integration/test_context_alignment.py
import yaml
from pathlib import Path
def load_context_inputs(phase_id: str) -> list[str]:
"""从 workflow.md 解析某个 Phase 声明的 context_inputs"""
# 实际实现依赖 workflow 文件的格式
# 这里用 YAML 配置作为示例
config = yaml.safe_load(Path("skills/wf-bug-e2e/config.yaml").read_text())
return config["phases"][phase_id].get("context_inputs", [])
def load_output_fields(phase_id: str) -> set[str]:
"""从 templates/ 解析某个 Phase 的输出 Schema 字段"""
import json, re
template = Path(f"skills/wf-bug-e2e/templates/{phase_id}.md").read_text()
# 从 Output Contract 部分提取字段名
schema_match = re.search(r"```json\n({.*?})\n```", template, re.DOTALL)
if schema_match:
schema = json.loads(schema_match.group(1))
return set(schema.keys())
return set()
def test_phase3_context_alignment():
"""Phase 3 声明需要的字段,Phase 1 和 Phase 2 都必须有输出"""
phase3_inputs = load_context_inputs("phase_3")
# phase3 声明从 phase1 获取 bug_info.summary, bug_info.stack_trace
phase1_outputs = load_output_fields("phase_1")
for input_decl in phase3_inputs:
if input_decl.startswith("phases.phase1."):
field = input_decl.replace("phases.phase1.", "")
assert field in phase1_outputs, \
f"Phase 3 需要 '{field}',但 Phase 1 的输出 Schema 中没有这个字段"
版本号规范
Workflow 文件本身就是"代码",每次修改都应该版本化。
markdown
版本号格式:MAJOR.MINOR.PATCH
MAJOR:Phase 结构变化
- 新增或删除 Phase
- 路由逻辑重大变更(影响主链路的条件)
- 子 Agent 输出 Schema 破坏性变更
→ 对正在运行的工作流有断裂风险
→ 续接时必须检查版本兼容性(见 W3 篇)
MINOR:功能扩展,向后兼容
- 新增 Step(在现有 Phase 内)
- 新增确认门选项
- 模板优化(不改变字段)
→ 正在运行的工作流可以继续用旧版本完成
→ 新触发的工作流用新版本
PATCH:措辞和配置调整
- 提示词措辞优化
- 超时时间调整
- 注释修改
→ 安全更新,不影响行为
→ 旧状态文件可以无感升级
版本号写在哪里:
markdown
# SKILL.md(工作流的入口文件)
---
name: wf-bug-e2e
version: 1.3.0 ← 每次发布前更新
last_updated: 2026-06-01
---
json
// workflow_state.json(每次运行时记录)
{
"workflow_version": "1.3.0", ← 运行时绑定,续接时校验
...
}
发布流程
bash
Step 1:描述修改目的
在 CHANGELOG.md 里写清楚:为什么改?改了什么?
不写"优化了一些逻辑",写"将 Phase 3 的置信度阈值从 0.95 改为 0.9,
原因:历史数据显示 0.95 触发 Gate A 的频率过高(18%)"
Step 2:运行 Gate 1 + Gate 2
python tools/validate_workflow.py
pytest tests/unit/ tests/integration/
Step 3:(MAJOR 版本)运行 Gate 3
python run_eval.py --cases eval/cases.yaml --output baseline_new.json
python compare_eval.py baseline_current.json baseline_new.json
Step 4:更新版本号
编辑 SKILL.md 的 version 字段
编辑 CHANGELOG.md 添加新版本条目
Step 5:发布
合并变更,旧版本进入 deprecated 状态
如果有正在运行的工作流用旧版本,记录在 deprecated_notes 里
CHANGELOG 模板
markdown
# CHANGELOG
## v1.3.0 (2026-06-01)
### Changed
- Phase 3 置信度阈值从 0.95 降至 0.90
- 原因:历史数据中 Gate A 触发率达到 18%,超过 < 20% 的阈值上限
- 影响:约 5% 的案例从触发 Gate A 改为直接进入 Phase 4
### Added
- Phase 4 新增 collect-all 策略声明
- 之前行为未明确,现在显式声明为 collect-all
## v1.2.1 (2026-05-15)
### Fixed
- 修复 Phase 7 的 Jira 评论幂等性检测
- 问题:run_id 标记格式不一致,导致部分情况下重复写评论
- 修复:统一 run_id 格式为 "wf-{jira_key}-{date}"
设计 Checklist
文件结构
- Policy / Workflow / TaskSpec / Tool 四层分离
-
config.yaml集中管理超时、重试次数、模型选择等可变参数 -
SKILL.md包含版本号字段
Gate 1(静态校验)
- 所有 template 引用在文件系统中存在
- 所有路由目标(on_success/on_failure)指向已知 Phase 或保留关键词
- CI 中每次提交自动运行
Gate 2(Schema 测试)
- context_inputs 与上游 Phase 输出字段对齐测试
- 所有路由条件的边界情况有测试覆盖
- CI 中每次提交自动运行
Gate 3(端到端回归)
- MAJOR 版本变更必须运行
- 结果与基线对比,Delta 超阈值不发布
版本管理
- 每次发布更新 SKILL.md 的版本号
- CHANGELOG.md 记录修改原因,不只是修改内容
总结
- Workflow 的 CI 分三级:静态校验(秒级,检查文件引用)→ Schema 测试(分钟级,验数据契约)→ 端到端回归(小时级,主链路改动才跑)------前两级覆盖大多数错误,成本低
- 版本号区分行为变更和安全更新:MAJOR 改了路由或 Schema,必须处理正在运行的工作流;PATCH 只改措辞,旧状态文件直接升级,不用担心兼容性
- CHANGELOG 写原因,不只写内容:"将阈值从 0.95 改为 0.9"是内容,"因为历史数据中 Gate A 触发率达到 18%,超过阈值"是原因,6 个月后的自己只需要原因
欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页