给Agent上线前加一道自动化稳定性闸门
大模型应用最危险的时刻,不是开发时,而是上线后。模型会漂移,接口会超时,返回格式会悄悄变了样,而你直到用户投诉才知道。本文给 Agent 项目加一道"稳定性闸门":用 pytest 做回归测试、用 mock 模拟真实接口、统计失败率与超时,再配一份灰度发布清单。整套东西能接进 CI,每次发版前自动跑,拦住八成的线上事故。所有代码可直接抄进你的项目。
一、为什么要给 Agent 加稳定性闸门
传统软件测试测的是"逻辑对不对",Agent 测试多了一层"模型行为稳不稳"。同一个 prompt,今天返回 JSON,明天可能返回一段自然语言;上游工具接口从 200ms 变成 8 秒,Agent 会卡死在等待;第三方模型临时降级,输出质量断崖。这些问题靠人工点测根本发现不了,必须靠自动化回归。
我见过最典型的事故:据行业公开复现案例报道,一个客服 Agent 上线两周一切正常,某天模型供应商把 temperature 默认值改了,回答从简洁变啰嗦,触发下游字符长度校验失败,整条工单链路瘫痪六小时。如果上线前有断言"输出必须能在 3 秒内返回且是合法 JSON",这道事故在灰度阶段就会被拦下。
稳定性闸门的三个目标很明确:第一,格式不变形------断言输出结构;第二,延迟不失控------统计每个 case 的耗时;第三,行为不漂移------用固定用例集持续比对。
二、用 mock 模拟真实接口
Agent 依赖的大模型、工具接口、数据库都不该在单元测试里真调,否则测试慢、贵、且不稳定。正确做法是用 mock 把外部依赖替掉,专注测 Agent 自身的编排逻辑。
以 Python 为例,用 unittest.mock 替换模型调用:
python
# agent.py
def run_agent(query, llm_client):
resp = llm_client.complete(prompt=build_prompt(query))
return parse_tool_call(resp)
# test_agent.py
from unittest.mock import MagicMock
from agent import run_agent
def test_agent_returns_valid_tool_call():
fake_llm = MagicMock()
fake_llm.complete.return_value = '{"action": "search", "query": "北京天气"}'
result = run_agent("查天气", fake_llm)
assert result["action"] == "search"
assert "query" in result
比如你的 Agent 会调用搜索工具,fake_llm 返回固定的工具调用 JSON,测试就能稳定验证"解析逻辑"是否正确,而不受模型随机性影响。再比如要测"模型返回自然语言而非 JSON"的降级分支,只要让 return_value 改成一段散文,断言系统能正确报错而不是崩溃。
三、统计失败率与超时
光断言对错不够,还要量化"稳不稳"。用 pytest 的钩子收集每个用例的耗时,超阈值就标红:
python
# conftest.py
import pytest, time
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
start = time.time()
outcome = yield
elapsed = time.time() - start
item.user_properties.append(("latency_s", round(elapsed, 3)))
if elapsed > 3.0:
print(f"[SLOW] {item.nodeid} took {elapsed:.2f}s")
配合一个汇总测试,统计整体失败率:
python
def test_failure_rate_within_threshold(results):
failed = sum(1 for r in results if r.failed)
rate = failed / len(results)
assert rate < 0.05, f"失败率 {rate:.1%} 超过 5% 阈值"
把这组用例固定成一个"黄金集"(golden set),每天跑、每次发版跑。一旦失败率从 0 跳到 3%,说明模型或依赖有漂移,先别全量发。
四、灰度发布清单
闸门过了,不等于能一把梭全量。Agent 的失败往往不是"全错",而是"对大多数、错一小撮",全量发布会把那一小撮放大成事故。所以发布本身也要有节奏,用流量逐步验证,而不是赌一把。给 Agent 的发布加一道灰度:
- 先在内部环境跑完整回归,失败率、超时全部达标。
- 小流量放行:接 5% 真实用户流量,监控错误率、平均延迟、人工抽检输出质量。
- 观察窗口:至少 30 分钟无异常,再把流量提到 25%、50%、100%。
- 熔断兜底:错误率超 2% 或 P99 延迟翻倍,自动回滚到上一版。
灰度阶段最该盯的是"软指标"------比如用户重试率、会话中断率,这些比成功率更早暴露体验退化。
五、端到端冒烟与监控看板
单元测试和 mock 再全,也替代不了"真刀真枪跑一次"。所以我建议在灰度前再加一道端到端冒烟:用一份受限的真实 key,挑三五个覆盖核心路径的真实 query,连真实模型和真实工具跑一遍,断言"能返回、能解析、能完成工具调用"。这一道不是为了测逻辑,是为了测"链路通不通"------mock 永远模拟不出供应商临时把接口从 v1 换成 v2 这种破事。
冒烟之外,还要有监控看板。闸门拦住的是"发版前",但线上漂移是"发版后"的事。我习惯盯四个指标:第一,成功率,即完整跑完不报错的比例,低于 98% 就要警觉;第二,平均延迟和 P99 延迟,重点看尾部的长请求有没有变慢;第三,输出格式合规率,比如要求 JSON 的接口里,实际返回合法 JSON 的比例;第四,用户侧软指标,比如重试率、会话中断率、人工接管率,这些比成功率更早暴露体验退化。把这四个指标做成看板,每天扫一眼,比等故障工单主动得多。
值得强调的是,看板的价值不在"漂亮",在"有基线、有对比"。没有历史基线的指标没有意义------今天 P99 是 1.2 秒,你看不出好坏;只有当你知道上周同一时间它是 0.8 秒,才会意识到"慢了 50%"是个问题。所以每版发完,记得把当天的指标快照存下来,作为下一版的对照锚点。
六、三类典型事故复盘
复盘过几十次 Agent 线上事故后,我发现八成集中在三类,正好对应闸门能拦的方向。
第一类是格式退化。模型升级后,原本稳定输出 JSON 的接口开始夹杂解释性文字,下游解析直接报错。这类事故,只要闸门里有"断言输出是合法 JSON"的用例,灰度阶段立刻会变红。教训是:不要把解析器写得"太宽容",能容错不等于该容错,格式契约必须显式断言。
第二类是延迟雪崩。某个工具接口从几百毫秒退化到十几秒,Agent 在等待里堆积,连接池耗尽,连锁拖垮整条链路。这类事故的早期信号是 P99 延迟缓慢爬升,看板比对能提前发现;闸门里对每个 case 统计耗时、超 3 秒告警,就是为此设计的。
第三类是行为漂移。模型没报错、格式也对,但回答质量肉眼可见地变差------比如客服 Agent 突然开始建议用户"重启路由器"而不是查订单。这种最阴险,因为硬指标全绿。应对办法是保留一小批"人工精标黄金用例",定期让人抽看输出,或者用另一个强模型做"裁判"比对语义相似度。闸门拦得住前两类,第三类要靠人和裁判模型补位。
七、工程取舍与踩坑
任何测试策略都是"用今天的成本换明天的安稳",闸门也不例外。落地时最容易踩的坑,不是技术不会写,而是尺度没拿准------太松拦不住事故,太严拖慢发布还养出虚假安全感。下面这几点是我带团队踩过一遍后沉淀下来的。
-
mock 不能太"理想"。如果 mock 永远返回完美 JSON,测试永远绿,但线上一碰脏数据就崩。比如一定要加几个"畸形返回"用例:空字符串、超长文本、半截 JSON、带思考链的包裹格式,验证解析器的健壮性。曾经有个项目 mock 只返回标准 JSON,上线后模型某次返回了带 Markdown 代码围栏的 JSON,解析器直接抛异常,而测试全程没发现,因为没人造过这种样例。
-
黄金集要有人维护。模型升级后,旧用例可能集体失效,但这未必是 Agent 的错,而是预期该更新。建议每个季度复核一次黄金集,删掉过时断言、补新边界。
-
CI 超时别设太松。Agent 测试本身慢,CI 单步超时设成 10 分钟看似安全,实则掩盖了"某用例悄悄变慢 3 倍"的问题。更稳的做法是记录每次耗时基线,偏离 20% 就告警。
-
别用真实 API key 跑测试。一旦把密钥写进测试,等于给每次 CI 打卡烧钱,还可能触发供应商限流。统一走 mock,只有"端到端冒烟"才用受限的真实调用,且单独开关。
-
测试报告要能追溯到具体用例。很多团队的 CI 只输出一个总分,红了却不知道红在哪。建议每次跑完把每个用例的耗时、断言结果、失败原因都落盘成可读报告,发版前扫一眼就知道"是哪一类问题在变多"。当某类畸形输入的失败率连续三次上升,那就是模型供应商在偷偷改行为的信号,比任何监控都早。
八、辩证:闸门也会误伤
必须说清代价。其一,维护测试集有成本,小团队可能觉得"写测试比写功能还累",但 Agent 一旦上线出错,修复成本远高于前期投入。我见过一个三人团队为了赶版本,把回归用例从八十个砍到十个,结果上线当天就因一个工具返回格式变化崩了四小时,事后补测试花的时间比当初写测试还多。所以测试集不是负担,是 insurance。
其二,mock 越强,离真实越远。mock 永远模拟不出模型"偶尔抽风"的长尾------比如某个罕见输入触发了模型输出一段带表情符号的回复,mock 里你根本不会想到要造这种样例。这就是为什么端到端冒烟测试不能省:它用真实接口跑,专门抓 mock 抓不到的脏数据。两者是互补关系,不是替代关系。
其三,过度严格的断言会让发布变慢,错失时效。如果你的断言连"回答里多了一个空格"都判失败,那每次模型供应商做个无害的格式化更新,你的发布就被卡住。正确做法是核心路径严、边缘路径松,分级设阈值:涉及金额、权限、安全的断言零容忍,纯表达层面的差异容忍。
其四,闸门会产生"虚假安全感"。绿色不代表没问题,只代表"没触发你设的那几道断言"。 Agents 最危险的失败往往不是报错,而是"答非所问但格式完美"。所以闸门之外,永远保留人工抽检和裁判模型比对,把它们当成第二道防线。
结论很朴素:稳定性闸门不是为了不出错,是为了把错挡在灰度里、挡在用户看到之前。它降低的是"低级事故"的概率,而不是"所有事故"的概率;把它当成的不是万能锁,而是一道该层层设防的第一道门。
九、互动提问
稳定性闸门不是银弹,但它把"靠运气上线"变成了"靠流程上线"。下面几个问题,欢迎结合实际项目聊聊:
- 你的 Agent 项目现在有线上的自动化回归吗,还是靠人工点测?
- 如果只能加一个测试,你会先断言"格式正确"还是"延迟达标",为什么?
- 欢迎在评论区聊聊:你踩过最贵的那次 Agent 线上事故,最后是怎么定位的?
数据与事件来源:
以下为本方案涉及的工具与经验依据,便于读者在自己的项目里复现与进一步查证;阈值类数值均为通用经验起点,请按业务实际容忍度调整:
- pytest 官方文档与钩子机制(docs.pytest.org)
- Python unittest.mock 标准库说明(docs.python.org/3/library/unittest.mock)
- 本文 CI 配置与测试代码为作者工程实践总结,灰度阈值(5%/2%/30分钟)为通用经验值,请按业务实际调整
- "模型默认值变更导致链路瘫痪"为行业中已多次公开复现的典型事故模式,非特指单一厂商