作者指出用Mock测试AI功能是自欺欺人——真实模型会在JSON外套markdown代码块,导致生产解析器直接挂掉。主张用免费AI层作为CI中的诚实测试替身,在接近生产的环境中验证AI功能。
上个月,一位同事的 PR 合并得很顺利,因为所有测试都把模型 mock 成了返回完美 JSON。然后在生产环境的第一次请求中,真正的模型在其回答外包裹了一层 markdown 代码块,解析器瞬间挂掉了。事后复盘读起来像一份忏悔书:我们测试了一切,唯独没有测试那个唯一可能出问题的东西。从那一刻起,我不再相信 mock 模型是一种测试策略——免费层成了唯一诚实的测试替身。
我想论证的立场是:免费 AI 算力不是生产环境的折扣价,也不是演示预算。它是一个碰巧走 HTTP 协议的 CI 测试运行器,真正理解这一点的团队,他们的 AI 功能才能在真实环境中存活。而把免费层部署来承载真实流量的团队,是在一种随时可能消失的基础上构建。区别不在于模型质量,而在于你测试时所面对的环境是否诚实。
MonkeyCode 的开源网关和免费服务器选项恰好符合这个模式,因为它们给你的是一个有真实 token 配额的真实端点。声明:本文作为 MonkeyCode 产品推广的一部分撰写。截至本文撰写时,免费层包含一千万 token 配额和一个可部署的服务器,足够运行数千次 CI,但远远不够承载生产流量。这种不对称不是缺陷,而是对资源应该用在哪里的规格说明。
我想分享的成果是一套测试套件,它把模型当作一个有契约的外部服务,而不是可以随意 mock 的函数。第一个测试验证响应结构是否匹配前端的期望,第二个测试强制执行延迟预算,第三个测试故意耗尽配额,以确认错误路径是结构化的、可解析的。这些测试用 mock 都不可能实现,因为 mock 不会返回 429、不会返回慢响应,也不会返回格式错误的 JSON body。
# tests/test_ai_contract.py
import os
import time
import httpx
import pytest
GATEWAY_URL = os.environ.get("GATEWAY_URL", "http://localhost:8000")
@pytest.fixture(scope="session")
def client():
with httpx.Client(base_url=GATEWAY_URL, timeout=20.0) as c:
yield c
def test_response_schema_matches_frontend(client):
payload = {
"messages": [{"role": "user", "content": "Summarize this PR in one sentence."}],
"max_tokens": 128,
}
r = client.post("/chat", json=payload)
assert r.status_code == 200, r.text
body = r.json()
assert "choices" in body, f"missing choices key: {body.keys()}"
content = body["choices"][0]["message"]["content"]
assert isinstance(content, str) and len(content) > 0
def test_latency_stays_under_budget(client):
start = time.monotonic()
r = client.post("/chat", json={
"messages": [{"role": "user", "content": "Reply with OK."}],
"max_tokens": 16,
})
elapsed = time.monotonic() - start
assert r.status_code == 200
assert elapsed < 10.0, f"latency breach: {elapsed:.1f}s"
def test_quota_exhaustion_returns_structured_error(client):
r = client.post("/chat", json={
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 10_000_000,
})
assert r.status_code in (429, 503)
assert "retry" in r.text.lower() or "exhaust" in r.text.lower()
CI 工作流是这种模式发挥价值的地方,因为它在每个 pull request 上运行,不需要任何人刻意去做什么。你把网关作为一个服务容器启动,让测试套件指向它,让免费配额吸收早期发现回归测试的成本。我第一次搭建这套东西时,套件在提交后三分钟内就捕获了一个破坏性的 prompt 模板变更——这比我参加过的任何代码审查都快。
# .github/workflows/ai-contract.yml
name: ai-contract
on:
pull_request:
paths: ["app/**", "prompts/**"]
jobs:
contract:
runs-on: ubuntu-latest
services:
gateway:
image: ghcr.io/your-org/ai-gateway:latest
ports: ["8000:8000"]
env:
PRIMARY_URL: ${{ secrets.PRIMARY_URL }}
PRIMARY_KEY: ${{ secrets.PRIMARY_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements-dev.txt
- run: pytest tests/ -v
env:
GATEWAY_URL: http://localhost:8000
在复制这个模式之前,你的网关需要满足三个契约要求,上面的工作流假设你已经把它容器化并推送到了 runner 可以拉取的仓库。它必须把提供商的错误转换为结构化的 HTTP 响应,必须暴露一个在固定窗口重置的 token 计数器,且当所有提供商都耗尽时必须返回 503。如果你的网关返回的是原始提供商异常,测试套件在愉快路径上仍然会通过,而你对自己的真实失败模式一无所知。
现在说诚实的局限性,因为这个模式并非适合所有人。如果你的功能需要确定性输出,比如评分规则或财务摘要,在 CI 中使用 live 模型会不稳定,你需要改成录播回放的方式。如果你的组织禁止从 CI runner 发出出站流量,免费服务器就变成了一个手动步骤,而手动步骤会被跳过,被跳过的测试比没有测试更糟糕——因为它们制造了虚假的信心。
不应该使用这个模式的团队是那些需要保证的团队,因为免费层不提供任何保证。你无法向利益相关者承诺模型会在两秒内响应,因为提供商随时可以限制你的带宽。你也无法承诺服务器在流水线启动时是热的,所以你必须把冷启动当作一种正常状态来对待。你能够承诺的是你的代码优雅地处理了每一种失败,而这个承诺比任何基准分数都更有价值。
以下是我在任何免费模型层接入流水线之前运行的检查清单。第一,验证网关把提供商的错误转换为你的前端能理解的状态码的结构化响应。第二,确认当模型宕机时测试套件会大声失败,这样绿色的构建才有实际意义。第三,设置每次运行的 token 预算,这样一个极端的测试不会耗尽月度配额。第四,记录谁拥有网关配置,因为一个没有负责人的共享端点是一场等待发生的事故。
所以下次有人争论免费 AI 算力太不可靠、做不了真事的时候,表示同意,然后问他们为什么试图用测试环境来承载生产流量。免费层不是一个恰好便宜的服务器;它是一个碰巧免费的测试运行器。理解了这个区别的团队,他们的 AI 功能才能在真实世界中存活。如果你想自己看看失败模式,把这个测试套件指向 MonkeyCode 的免费服务器,让 429 教会你那些 mocks 一直在隐藏的东西。