跳转至

第3章 CI/CD 实践

学习目标

  • 理解 CI/CD 的核心理念与价值,区分持续集成、持续交付、持续部署
  • 掌握 GitHub Actions 进阶用法,能编写复杂工作流
  • 了解 Jenkins 的基本配置与适用场景
  • 学会设计自动化测试与自动化部署流水线
  • 掌握蓝绿部署与金丝雀发布等渐进式发布策略

问题导入

某团队 Agent 服务上线后,每次发版都是一场"深夜惊魂":①开发合代码后,运维手动拉代码、装依赖、构建镜像、SSH 上传、重启服务,全程 2 小时;②没有自动化测试,几次发版把回归 bug 带上线,凌晨回滚;③一次发版直接替换所有实例,新版本有内存泄漏导致全站崩溃 15 分钟;④团队不敢频繁发版,迭代周期被拉长到两周一次。

这些痛点的根因是:缺少自动化交付流水线。CI/CD(持续集成/持续部署)把代码提交后的构建、测试、部署全部自动化,让发版从"手工冒险"变成"流水线作业"。本章讲解如何用 GitHub Actions 与 Jenkins 搭建 Agent 项目的 CI/CD 流水线,并实现安全的渐进式发布。

核心概念

1. CI/CD 理念

  • 持续集成(CI):开发人员频繁把代码合并到主干,每次合并自动触发构建与测试,尽早发现集成问题
  • 持续交付(CD, Delivery):CI 通过后自动打包成可部署制品,随时可一键发布到生产
  • 持续部署(CD, Deployment):制品通过后自动部署到生产,无需人工干预

三者关系层层递进。CI/CD 的核心价值:缩短从代码到上线的反馈周期,降低发版风险,提升迭代速度。

2. GitHub Actions 进阶

GitHub Actions 是 GitHub 内置的 CI/CD 平台,核心概念:

  • Workflow(工作流):YAML 文件定义,放在 .github/workflows/
  • Job(任务):工作流由多个 job 组成,可并行或串行
  • Step(步骤):job 内的执行单元,可以是 shell 命令或预定义 Action
  • Matrix(矩阵):在多个 Python 版本/操作系统上并行测试
  • Secrets:加密存储 API Key 等敏感信息
  • Artifact:构建产物上传保存,供后续 job 使用
  • Cache:缓存依赖,加速构建

3. Jenkins 配置

Jenkins 是老牌开源 CI/CD 工具,适合企业内网与复杂流程:

  • Pipeline as Code:用 Jenkinsfile 定义流水线,与代码同仓库
  • 插件生态:支持 Docker、K8s、Slack 通知等数百插件
  • Master-Agent 架构:Master 调度,Agent 执行,支持横向扩展
  • 凭据管理:内置 Credentials 存储密钥

Jenkins 与 GitHub Actions 的选择:开源项目与云原生场景优先 Actions;企业内网、强合规、复杂审批流选 Jenkins。

4. 自动化测试

AI Agent 的测试金字塔:

  • 单元测试:工具函数、Prompt 模板、解析逻辑(pytest)
  • 集成测试:Agent 与外部服务的交互(mock LLM、真实 Redis)
  • 端到端测试:完整对话流程验证(用录制回放避免真实 API 消耗)
  • Prompt 回归测试:固定输入对比输出,检测 Prompt 改动导致的语义漂移

CI 中必须跑的测试:lint(ruff)+ 类型检查(mypy)+ 单元测试 + 集成测试。端到端测试因 LLM 调用成本高,可用 mock 或仅在发版前跑。

5. 自动化部署

部署自动化的关键:

  • 制品优先:部署的是构建好的镜像,不是源码
  • 环境隔离:staging 与 production 配置严格分离
  • 回滚机制:每次部署保留前一版本,可一键回滚
  • 健康检查:部署后自动验证服务健康,失败则回滚
  • 蓝绿与金丝雀:渐进式发布降低风险

6. 蓝绿部署与金丝雀发布

  • 蓝绿部署:维护两套完全相同的环境(蓝、绿),新版本部署到备用环境,验证通过后流量切换。优点:切换瞬间完成、回滚极快(切回去即可)。缺点:资源翻倍。

  • 金丝雀发布:新版本先只承接一小部分流量(如 5%),观察指标无异常后逐步扩大(5% → 20% → 50% → 100%)。优点:风险可控、资源渐进。缺点:需要流量分发能力。

AI Agent 场景下金丝雀发布特别重要:新 Prompt 或新模型可能影响输出质量,需逐步放量观察用户反馈与成本指标。

原理图

flowchart LR
    Dev[开发提交代码] --> PR[Pull Request]

    subgraph CI["持续集成 CI"
        Lint[代码检查 Lint]
        Test[单元/集成测试]
        Build[构建镜像]
        Push[推送镜像仓库]
    end]

    PR --> Lint --> Test --> Build --> Push

    subgraph CD["持续部署 CD"
        DeployStaging[部署 Staging]
        E2E[端到端测试]
        Approve[人工审批]
        Canary[金丝雀发布 5%]
        Observe[观察指标]
        Full[全量发布]
    end]

    Push --> DeployStaging --> E2E --> Approve --> Canary --> Observe
    Observe -->|正常| Full
    Observe -->|异常| Rollback[自动回滚]

图:CI/CD 流水线——代码提交触发 CI(lint→测试→构建→推送),CD 经 staging 验证与审批后金丝雀渐进发布,异常自动回滚

示例代码

1. GitHub Actions 部署工作流

# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:  # 支持手动触发

env:
  IMAGE_NAME: mycorp/agent-api
  REGISTRY: registry.mycorp.com

jobs:
  # ---------- CI 阶段 ----------
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11"]
    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip

      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install ruff mypy pytest pytest-asyncio

      - name: Lint
        run: ruff check app/

      - name: Type check
        run: mypy app/

      - name: Run tests
        run: pytest tests/ --cov=app --cov-report=xml

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          file: ./coverage.xml

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_TOKEN }}

      - name: Extract version
        id: version
        run: echo "tag=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
          cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max

  # ---------- CD 阶段 ----------
  deploy-staging:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Deploy to staging
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /opt/agent
            docker compose pull
            docker compose up -d
            sleep 10
            curl -f http://localhost:8000/health || exit 1

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production  # 需要人工审批
    steps:
      - name: Canary deploy
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /opt/agent
            # 金丝雀:先更新 1 个实例
            docker compose up -d --no-deps --scale agent-api=3 agent-api
            sleep 15
            # 健康检查
            curl -f http://localhost:8000/health || (echo "健康检查失败,回滚" && docker compose rollback && exit 1)
            echo "金丝雀实例健康,等待观察 5 分钟后全量"

2. Jenkinsfile 流水线

pipeline {
    agent any
    environment {
        IMAGE = "registry.mycorp.com/agent-api:${env.BUILD_NUMBER}"
    }
    stages {
        stage('Checkout') {
            steps { checkout scm }
        }
        stage('Test') {
            steps {
                sh 'pip install -r requirements.txt'
                sh 'ruff check app/'
                sh 'pytest tests/ --cov=app'
            }
        }
        stage('Build') {
            steps {
                sh "docker build -t ${IMAGE} ."
                sh "docker push ${IMAGE}"
            }
        }
        stage('Deploy Staging') {
            steps {
                sh "ssh deploy@staging 'cd /opt/agent && docker compose pull && docker compose up -d'"
            }
        }
        stage('Approval') {
            steps { input message: '批准部署到生产?', ok: '部署' }
        }
        stage('Deploy Production') {
            steps {
                sh "ssh deploy@prod 'cd /opt/agent && docker compose pull && docker compose up -d'"
            }
        }
    }
    post {
        failure {
            slackSend channel: '#alerts', message: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
    }
}

3. Prompt 回归测试示例

import pytest
from app.agent import Agent

# 录制的固定输入与期望关键词
REGRESSION_CASES = [
    {"input": "查询订单 O12345 状态", "expect_keywords": ["订单", "状态"]},
    {"input": "我想退款", "expect_keywords": ["退款", "流程"]},
]

@pytest.mark.parametrize("case", REGRESSION_CASES)
def test_prompt_regression(case, mock_llm_client):
    """Prompt 改动后验证输出仍包含关键语义"""
    agent = Agent(client=mock_llm_client)
    output = agent.run(case["input"])
    for kw in case["expect_keywords"]:
        assert kw in output, f"输出缺失关键语义: {kw}"

企业案例

案例 1:Agent 项目 CI/CD 全流程

某团队为客服 Agent 搭建完整 CI/CD 流水线。CI 阶段:PR 触发 ruff lint、mypy 类型检查、pytest 单元测试、Prompt 回归测试(mock LLM),全部通过才允许合并。CD 阶段:合并到 main 后自动构建镜像推送 Harbor,部署到 staging 跑端到端测试,人工审批后金丝雀发布——先更新 1 个实例承接 10% 流量,观察 30 分钟的延迟、错误率、Token 成本,正常则全量。

改造效果:①发版时间从 2 小时降到 15 分钟;②回归 bug 在 CI 阶段拦截,上线事故下降 80%;③金丝雀发布让一次 Prompt 缺陷在 10% 流量时被发现,5 分钟回滚,影响面极小;④团队敢于每日发版,迭代速度提升 3 倍。关键经验:Prompt 回归测试是 AI Agent CI 的特色,能防止 Prompt 改动引发的语义漂移。

案例 2:蓝绿部署保障零停机

某金融场景 Agent 对可用性要求极高,团队用蓝绿部署:维护 blue、green 两套 K8s 部署,新版本部署到备用环境,运行 10 分钟健康检查与流量回放验证,通过后切换 Service 选择器把流量切到新版本。回滚只需把选择器切回旧版本,秒级完成。代价是资源翻倍,但对金融场景值得。

常见问题

Q1:CI 与 CD 的边界在哪?

A:CI 关注"代码能否正确集成",产出是经过测试的制品(镜像);CD 关注"制品能否安全上线",产出是运行中的服务。分界线是镜像仓库:CI 把镜像推上去,CD 把镜像拉下来部署。

Q2:GitHub Actions 的 secrets 安全吗?

A:secrets 加密存储,日志中自动脱敏。但要注意:①PR 来自 fork 时默认不注入 secrets,防止泄露;②不要把 secret 直接 echo 到日志;③定期轮换密钥;④最小权限原则,每个 secret 只赋必要权限。

Q3:金丝雀发布如何判断"正常"?

A:监控四类指标:①技术指标(错误率、延迟、CPU/内存);②业务指标(对话完成率、用户满意度);③AI 指标(Token 成本、幻觉率);④用户反馈(差评、投诉)。任一异常即回滚,避免只看技术指标而忽略 AI 质量退化。

Q4:Agent 的端到端测试太贵(真实调 LLM),怎么办?

A:①用录制回放(VCR.py 录制 LLM 响应,测试时回放);②CI 用 mock LLM,发版前手动跑真实端到端;③用便宜的小模型(如 GPT-4o-mini)做冒烟测试,生产用大模型;④测试用例聚焦流程逻辑而非具体文案。

Q5:蓝绿部署与金丝雀怎么选?

A:蓝绿适合"需要瞬间切换+快速回滚"的场景(如金融、强一致);金丝雀适合"需要观察质量指标"的场景(如 AI Agent,新 Prompt 可能影响输出质量)。AI Agent 一般优先金丝雀。

面试题

  1. 简答题:解释 CI、持续交付、持续部署的区别。

参考要点:CI 自动构建测试;持续交付自动打包制品可随时一键发布;持续部署自动部署到生产无需人工。三者层层递进,自动化程度递增。

  1. 实践题:设计一个 Agent 项目的 GitHub Actions 工作流,包含测试、构建、金丝雀部署。

参考要点:test job 跑 lint+pytest+Prompt 回归;build job 构建推送镜像;deploy-staging 部署 staging;deploy-production 用 environment 审批 + 金丝雀策略。

  1. 设计题:金丝雀发布中如何决定"继续放量"还是"回滚"?

参考要点:设定 SLO 阈值(错误率<1%、P99 延迟<3s、Token 成本波动<20%);观察期至少 30 分钟;技术+业务+AI 指标综合判断;任一异常自动回滚。

  1. 对比题:蓝绿部署与金丝雀发布的优缺点对比。

参考要点:蓝绿切换快、回滚快,但资源翻倍、无法渐进观察;金丝雀资源渐进、风险可控,但需要流量分发与监控能力、发布周期长。

  1. 原理题:为什么 AI Agent 的 CI 要专门做 Prompt 回归测试?

参考要点:Prompt 改动可能改变输出语义,传统单元测试无法捕获;固定输入对比输出关键词,能检测语义漂移;mock LLM 避免真实调用成本。

练习题

  1. 工作流编写:为你的 Agent 项目编写 GitHub Actions 工作流,包含 lint、单元测试、镜像构建推送,并在 PR 上触发。

  2. 金丝雀模拟:用 Docker Compose 启动 3 个 Agent 实例,编写脚本模拟金丝雀发布:更新 1 个实例后做健康检查,通过则更新其余实例,失败则回滚。

  3. Prompt 回归:为你的 Agent 编写 5 个 Prompt 回归测试用例,用 mock LLM 客户端验证输出包含关键语义。

小结

CI/CD 是现代软件交付的发动机。本章核心:CI 自动化构建测试保障代码质量,CD 自动化部署加速交付;GitHub Actions 适合云原生项目,Jenkins 适合企业内网复杂流程;AI Agent 的 CI 要加入 Prompt 回归测试;金丝雀发布是 AI 应用的首选策略,能渐进观察质量指标。流水线就绪后,如何控制 LLM 成本与加固安全,是第 4 章的主题。