独立开发者的 CI/CD 选型:GitHub Actions 跑 AI 推理任务该怎么配

独立开发者的 CI/CD 选型:GitHub Actions 跑 AI 推理任务该怎么配

一、AI 推理跑在 CI 里?看似荒谬实则必要

CI(持续集成)的传统职责是跑测试、lint、构建。放在 AI 场景下,你可能会问:为什么要在 CI 里跑推理任务?

答案是评估(Evaluation)。你改了一行 Prompt,怎么知道这次修改让回答质量提升了还是下降了?你升级了模型版本,怎么确认旧用例的准确率没有退化?你新增了一个工具调用的逻辑,怎么验证边界 case 没有被破坏?

这些都依赖于 AI 推理的自动化评估。如果每次发布前都靠人肉测试——"把 50 个标准问题发给 AI,手动对比回答质量"——那你的发布节奏会被严重拖慢。

在 CI 中自动运行推理评估是最优解。但这里有三个挑战:API 调用的成本控制、推理时间的等待策略、以及评估结果的判读机制。

graph TB A[PR 提交] --> B[Lint + Unit Test] B -->|通过| C{AI Eval 触发?} C -->|diff 包含 Prompt 变更| D[启动 AI 评估] C -->|diff 不含 Prompt 变更| E[跳过评估] D --> F[运行标准用例集<br/>20-50 个测试] F --> G[收集结果 + Token 消耗] G --> H{评分 ≥ 阈值?} H -->|是| I[CI 通过] H -->|否| J[CI 失败 + 详细报告] I --> K[记录指标变化] style J fill:#ff6b6b,color:#fff style I fill:#51cf66,color:#fff style E fill:#868e96,color:#fff

本文将设计一套低成本、高效率的 AI 推理 CI 流程,覆盖触发策略、成本控制和结果判读。

二、触发策略:不是每次提交都值得跑 AI 评估

AI 推理的 CI 不能在每次 push 时都运行。原因很简单:API 调用花钱,推理需要时间。一次完整的评估可能需要 5-10 分钟和 $0.5-2 的成本。

触发策略应该是条件性的:

条件是否触发原因
prompts/目录有变更触发Prompt 变更直接影响输出质量
models/或模型配置变更触发模型切换可能引入回归
eval/或测试用例变更触发评估体系本身需要验证
只改了文档、注释、CSS跳过不影响推理质量
PR 是 Draft跳过WIP 不需要评估

GitHub Actions 的paths过滤可以实现:

on: pull_request: paths: - 'prompts/**' - 'src/models/**' - 'eval/**' - 'config/ai.json'

更进一步,可以在 workflow 中动态判断 diff 内容,决定是否运行全部用例还是抽样运行。

三、完整 CI 配置与评估脚本

GitHub Actions Workflow:

name: AI Eval on: pull_request: paths: - 'prompts/**' - 'src/models/**' - 'eval/**' jobs: eval: runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Run AI Eval env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: node scripts/ai-eval.js - name: Upload Eval Report if: always() uses: actions/upload-artifact@v4 with: name: eval-report path: eval-results.json

评估脚本:

// scripts/ai-eval.js const fs = require('fs'); const path = require('path'); const OpenAI = require('openai'); const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // 加载测试用例 function loadTestCases() { const evalDir = path.join(__dirname, '..', 'eval'); const cases = []; for (const file of fs.readdirSync(evalDir)) { if (!file.endsWith('.json')) continue; const content = JSON.parse(fs.readFileSync(path.join(evalDir, file), 'utf-8')); cases.push(...content); } return cases; } // 评分函数:比较 AI 输出与期望答案 function scoreResponse(expected, actual) { const lowerActual = actual.toLowerCase(); const lowerExpected = expected.toLowerCase(); // 精确匹配 if (lowerActual === lowerExpected) return 1.0; // 包含匹配 if (lowerActual.includes(lowerExpected) || lowerExpected.includes(lowerActual)) { return 0.8; } // 关键词匹配 const keywords = lowerExpected.split(/\s+/).filter(w => w.length > 3); const hitCount = keywords.filter(kw => lowerActual.includes(kw)).length; if (keywords.length === 0) return 0.5; return Math.min(hitCount / keywords.length, 0.7); } async function runEval() { const testCases = loadTestCases(); const results = { timestamp: new Date().toISOString(), total: testCases.length, passed: 0, failed: 0, totalTokens: 0, totalCost: 0, details: [], }; // 串行运行,避免触发 API 速率限制 for (const tc of testCases) { try { const start = Date.now(); const response = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: tc.messages, temperature: 0, max_tokens: 500, }); const actual = response.choices[0].message.content; const score = scoreResponse(tc.expected, actual); const usage = response.usage; results.totalTokens += usage.total_tokens; // gpt-4o-mini 定价: $0.15/1M input, $0.60/1M output const cost = (usage.prompt_tokens * 0.15 / 1_000_000) + (usage.completion_tokens * 0.60 / 1_000_000); results.totalCost += cost; const passed = score >= tc.threshold; if (passed) { results.passed++; } else { results.failed++; } results.details.push({ name: tc.name, passed, score, threshold: tc.threshold, expected: tc.expected.substring(0, 100), actual: actual.substring(0, 200), tokens: usage.total_tokens, cost: cost.toFixed(4), durationMs: Date.now() - start, }); console.log(`${passed ? '✓' : '✗'} ${tc.name}: ${score.toFixed(2)}/${tc.threshold} (${(Date.now() - start)}ms, $${cost.toFixed(4)})`); } catch (err) { results.failed++; results.details.push({ name: tc.name, passed: false, error: err.message, }); console.error(`✗ ${tc.name}: ERROR - ${err.message}`); } } // 写入结果 fs.writeFileSync('eval-results.json', JSON.stringify(results, null, 2)); // 总结 console.log(`\n=== AI Eval Summary ===`); console.log(`Total: ${results.total}, Passed: ${results.passed}, Failed: ${results.failed}`); console.log(`Total Tokens: ${results.totalTokens}, Cost: $${results.totalCost.toFixed(4)}`); // CI 失败条件:通过率 < 80% const passRate = results.total > 0 ? results.passed / results.total : 0; if (passRate < 0.8) { console.error(`Pass rate ${(passRate * 100).toFixed(1)}% is below threshold 80%`); process.exit(1); } } runEval().catch((err) => { console.error('Eval failed:', err); process.exit(1); });

测试用例格式(eval/basic_qa.json):

[ { "name": "天气查询-晴天", "messages": [ {"role": "system", "content": "你是天气助手。用中文回答。"}, {"role": "user", "content": "今天北京天气怎么样?"} ], "expected": "北京", "threshold": 0.5 } ]

四、成本优化与工程权衡

成本优化手段

  • 使用 gpt-4o-mini 而非 gpt-4o 跑评估。评估的目的是验证 Prompt 变更的效果,mini 模型的判断力和 full 模型高度相关
  • 对非 PR,只在 main 分支的定时任务中运行全量评估(如每天凌晨一次)
  • 使用temperature: 0保证输出的一致性,减少重复运行
  • 和 Prompt 无关的 PR,可以通过 paths filter 跳过 AI 评估

CI 超时风险:AI 推理可能因为 API 延迟而超时。设置timeout-minutes: 15,如果超时视为评估失败。

不适用场景

  • 评估需要人工判断的开放性问题(如创意写作质量)
  • LLM 调用量极小(< 100 次/天)的项目——人工检查更经济
  • 模型变更频率极低(月级)——不需要 CI 集成

五、总结

AI 推理 CI 的核心价值是:用自动化评估替代人工质检,让每次 Prompt 变更都经过客观验证。

落地路径:先建立 20-30 个标准测试用例覆盖核心场景;然后在 CI 中配置 paths 过滤,只在 Prompt 相关文件变更时触发;接着用 gpt-4o-mini 控制成本;最后设定通过率阈值(80%)作为 CI 的通过条件。

独立开发者最宝贵的资源是时间。让 CI 替你盯着质量,你才能专注于创造价值。少即是多——让 CI 做它擅长的事,你做你擅长的事。