Workflow 系列(07):工程化与版本管理——Workflow 的 CI/CD

为什么 Workflow 需要 CI

代码改了,CI 跑测试发现问题。Workflow 的"代码"是 Markdown + YAML 文件,改了之后怎么知道没有破坏什么?

常见的三种漏网问题:

  • 修改了 templates/analyze.md,新增了一个输出字段,忘记在 workflow.mdcontext_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 超阈值不发布

版本管理


总结

  1. Workflow 的 CI 分三级:静态校验(秒级,检查文件引用)→ Schema 测试(分钟级,验数据契约)→ 端到端回归(小时级,主链路改动才跑)------前两级覆盖大多数错误,成本低
  2. 版本号区分行为变更和安全更新:MAJOR 改了路由或 Schema,必须处理正在运行的工作流;PATCH 只改措辞,旧状态文件直接升级,不用担心兼容性
  3. CHANGELOG 写原因,不只写内容:"将阈值从 0.95 改为 0.9"是内容,"因为历史数据中 Gate A 触发率达到 18%,超过阈值"是原因,6 个月后的自己只需要原因

欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。

更多实用知识和有趣产品,欢迎访问我的个人主页

相关推荐
两万五千个小时1 小时前
Claude Code 上下文管理(一):为什么 Agent 会"失忆"?
人工智能·架构·开源
两万五千个小时1 小时前
Claude Code 上下文管理(二):零 Token 消耗的压缩三板斧
人工智能·程序员·开源
冬奇Lab1 小时前
每日一个开源项目(第150篇):caveman - 为什么用很多 token,少 token 也行——给 AI Agent 装上穴居人嘴巴
人工智能·开源·资讯
贵慜_Derek1 小时前
MAI-04|干净数据在工程上意味着什么:MAI 预训练数据治理
人工智能·算法·llm
feelmylife591 小时前
Agent 记忆设计架构 — 分层记忆:什么时候该记住,什么时候该忘记
人工智能
阿黎梨梨2 小时前
揭秘大语言模型的底层逻辑:从文本分词到高维向量的计算之旅
javascript·人工智能
moMo2 小时前
AI工程化 03:给模型喂上下文
人工智能
火山引擎开发者社区11 小时前
没有长期记忆,Agent 谈何持续进化?一图看懂火山 Mem0:解锁 Agent 持续学习与进化之路
人工智能