Grok 契约测试与 CI mock
2026-11-04 17:32 更新
CI 里没有真实额度,这件事常被当成「所以接口不用测」。结果是一次字段改名、一次解析层重构,本地跑得好好的,上线才发现取答案的位置变了。问题不在于有没有额度,而在于有没有把「契约」固定下来。
这一节给一套不花钱的契约测试:七类 fixture,覆盖正常与六种异常。
一、问题不是「测不了真实接口」,而是「没定义契约」
契约测试不验证「模型答得好不好」,只验证「你的代码对响应的假设是否还成立」。这些假设包括:
- 请求体字段名(
input而不是messages)。 - 取答案的位置(
output_text还是output[].content)。 - 工具调用的错误分类(未知工具、畸形参数)。
- 错误响应的形态(401 / 429 怎么表现)。
只要这些假设被写进 fixture,CI 就能在没有额度的情况下守住它们。真正的模型质量,另由人工评审与小样本实测负责。
二、最小可运行示例:七类 fixture 与断言
下面的脚本不联网、不需要真实密钥。它用一份 mock 处理器模拟七种输入,逐条比对期望结果。
"""契约测试骨架(mock):七类 fixture 覆盖正常与六种异常。"""
FIXTURES = [
{"name": "正常回答", "input": {"kind": "text"}, "expect": "text"},
{"name": "单工具", "input": {"kind": "tool", "n": 1}, "expect": "tool1"},
{"name": "多工具", "input": {"kind": "tool", "n": 2}, "expect": "tool2"},
{"name": "畸形参数", "input": {"kind": "tool_bad"}, "expect": "json_error"},
{"name": "未知工具", "input": {"kind": "tool_unknown"}, "expect": "unknown_tool"},
{"name": "429 限流", "input": {"kind": "rate"}, "expect": "retry"},
{"name": "无效 Key", "input": {"kind": "auth"}, "expect": "fail"},
]
def mock_handler(payload: dict) -> str:
kind = payload["kind"]
if kind == "text":
return "text"
if kind == "tool":
return f"tool{payload['n']}"
if kind == "tool_bad":
return "json_error"
if kind == "tool_unknown":
return "unknown_tool"
if kind == "rate":
return "retry"
return "fail"
def run(fixtures: list[dict]) -> list[tuple[str, bool]]:
results = []
for f in fixtures:
try:
got = mock_handler(f["input"])
results.append((f["name"], got == f["expect"]))
except Exception:
results.append((f["name"], False))
return results
def offline_check() -> None:
results = run(FIXTURES)
for name, ok in results:
print(f"{name} {'ok' if ok else 'FAIL'}")
passed = sum(1 for _, ok in results if ok)
print(f"{passed}/{len(results)} 通过")
if __name__ == "__main__":
offline_check()
真正落地时,把 mock_handler 换成「解析你代码的输出」的函数:给定一份固定响应,验证解析层取到的是不是期望值。fixture 存成 JSON 文件,跟着代码一起评审。
三、七类 fixture 覆盖什么
| fixture | 模拟的输入 | 守住什么 |
|---|---|---|
| 正常回答 | 纯文本响应 | 取答案的位置 |
| 单工具 | 一个 tool_call |
工具名与参数解析 |
| 多工具 | 并行多个调用 | 是否全部处理 |
| 畸形参数 | 非法 JSON | 解析失败分类 |
| 未知工具 | 未注册的工具名 | 拒绝而非崩溃 |
| 429 | 限流响应 | 重试策略 |
| 无效 Key | 401 | 不重试、快速失败 |
三件要强调的事:
- fixture 是契约,不是快照。不要存整段真实响应(会含敏感信息、也会过期),只存「你的代码依赖的那几个字段」。
- 七类是下限,不是上限。你的业务若有多轮、流式、引用,各加一条;但先保证这七类绿,再谈扩展。
- 静态校验 + mock 是本书的验证上限。官方能力、真实延迟、限流阈值都无法在 CI 里复现;这些必须靠小流量实测与线上的观测补足。
四、预期输出与异常分支
运行测试骨架,预期输出:
正常回答 ok
单工具 ok
多工具 ok
畸形参数 ok
未知工具 ok
429 限流 ok
无效 Key ok
7/7 通过
换成真实代码时,若出现异常,按下面顺序定位:
| 现象 | 优先怀疑 | 第一步动作 |
|---|---|---|
| 解析用例红 | 取答案字段变了 | 对照官方响应结构 |
| 工具用例红 | 参数结构变了 | 核对 arguments 与 schema |
| 只有 CI 红本地绿 | fixture 过期 | 用当前响应重录一份 |
| 异常用例红 | 错误分类改了 | 对齐 7.2 的三类错误 |
最后一行尤其重要:fixture 过期是这类测试最常见的失效方式。它不是「测试没用」,而是提醒你该更新契约了。
五、判据卡
| 判断项 | 该这么做 | 不该这么做 |
|---|---|---|
| 测什么 | 契约(字段与位置) | 模型回答质量 |
| fixture 内容 | 只存依赖的字段 | 存整段真实响应 |
| 覆盖范围 | 正常 + 六类异常 | 只测 happy path |
| 失败含义 | 契约变了,去核对 | 直接改期望值让它绿 |
| 验证上限 | mock + 小流量实测 | 指望 CI 覆盖一切 |
这张卡的重点是契约与质量分开。五条里有三条在说:CI 守结构,人守质量。
六、来源与核验
- 官方 API 参考:
https://docs.x.ai/developers/rest-api-reference(核验日期 2026-11-04)。响应结构与错误形态以该页为准。 - 官方 Function Calling:
https://docs.x.ai/developers/tools/function-calling(核验日期 2026-11-04)。tool_call与function_call_output的结构来自该页,工具解析的三类错误见 7.2。 - 七类 fixture 的划分与测试骨架为本书原创;具体断言内容需按你的实现补充,本节不提供可直接运行的线上测试。
七、动手试
- 把七类 fixture 落成 JSON 文件,写一个读取并逐条断言的脚本,接入你现有的 CI。
- 给「多工具」用例补一条断言:当模型一次返回三个调用时,你的代码是否全部执行、是否有遗漏。
- 设计一条 fixture 更新流程:谁负责在接口变更后重录、如何评审、多久检查一次过期。写成一个三行的小节,贴进项目 README。
以上内容是否对您有帮助:

TRAE-AI编程


更多建议: