独立开发者的 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 做它擅长的事,你做你擅长的事。