Skybridge Evals 实战:如何在 CI 中用真实模型断言你 MCP App 的工具调用
【免费下载链接】skybridgeSkybridge is a full-stack TypeScript framework for MCP Apps and ChatGPT Apps. Type-safe. React-powered. Platform-agnostic.项目地址: https://gitcode.com/gh_mirrors/skybr/skybridge
🧪Skybridge Evals是 Skybridge(一款面向 MCP Apps 与 ChatGPT Apps 的全栈 TypeScript 框架)内置的测试能力:它让一个真实模型在你的 App 上发起对话,把模型实际做出的工具调用交给你断言——不需要端口、不需要 fixture,跑在 Vitest 里,天然适合放进 CI。
为什么需要 Evals:工具能跑 ≠ 模型会调用
对 MCP App 来说,最隐蔽的 bug 不是"工具本身报错",而是"工具明明可用,模型却不调用它"——比如改了工具名、描述或参数 schema 之后,模型开始绕过它,或传错参数。
Skybridge 的测试工具箱按"保真度 vs 成本"分了层(见 docs/test/index.mdx):
| 想验证什么 | 用什么 | 真实模型 | 适合 CI |
|---|---|---|---|
| 手动调工具、看视图 | DevTools | ❌ | ❌ |
| 用真实模型先手动对话 | Playground | ✅ | ❌ |
| 在 CI 中断言模型的工具调用 | Evals | ✅ | ✅ |
| 提交前整体合规检查 | Audit | ✅ | 部分 |
推荐节奏:先用 Playground 手动确认"模型大致会用我的工具",再用 Evals 把这个行为固化成可重复运行的测试,之后每次改动由 CI 替你盯住。
一键接入:3 步开启 Evals
第 1 步:安装测试运行时
pnpm add -D @skybridge/test vitest@^4 ai @ai-sdk/anthropic第 2 步:在 Vite 插件里打开evals开关
// vite.config.ts export default defineConfig({ plugins: [ skybridge({ evals: {} }), // 注册 expect.chat 匹配器,收集 evals/**/*.eval.ts react(), ], });evals: {}会同时做四件事:注册断言匹配器、自动收集evals/目录下的场景、把单场景超时提到 2 分钟、加载.env让模型 API Key 可用。
第 3 步:加一个运行脚本
{ "evals": "vitest run evals" }写第一个场景:自然语言提问 + 工具调用断言
场景的结构非常简单:start()在进程内把你的 App 服务起来(App 只需从src/server.ts导出,导入不会启动任何服务),发一条消息,然后断言:
// evals/search.eval.ts const chat = await start({ app, model: anthropic("claude-sonnet-4-5") }); await chat.send("Find me running shoes under 100 dollars"); expect.chat(chat).toHaveCalledToolWith("search-products", { query: "running shoes", });关键点:expect.chat(chat)的类型直接来自你的 App——工具名有自动补全,参数按各工具的 input schema 做类型检查。改错工具名,编译器就会先于模型发现问题。
完整匹配器清单:断言模型的每一步行为
| 匹配器 | 通过条件 |
|---|---|
toHaveCalledToolOnce(name, args?) | 恰好一次成功的调用,可匹配参数 |
toHaveCalledToolWith(name, args) | 某次成功调用匹配了参数 |
toNeverHaveCalledTool(name) | 该工具从未被尝试 |
toHaveFailedToolCall(name) | 某次调用被拒绝或抛错 |
toHaveSaid(text) | 助手回合包含文本(支持正则) |
toHaveCalledToolsInOrder(...names) | 按相对顺序调用了这些工具 |
toHaveCalledNoTools() | 完全没调用任何工具 |
toPassJudgment(criteria) | 裁判模型按标准评分通过 |
所有匹配器都支持.not、覆盖整段对话(不只是最后一次send),失败信息会列出模型实际做过的每一次调用及参数。需要自定义断言时,chat.toolCalls和chat.assistantTurns也全部可用。
裁判模型:断言"语气得体"这类软性标准
有些行为没法用确定性匹配器表达,比如"回答是否礼貌、是否基于工具结果编造价格"。toPassJudgment会把整段对话交给一个裁判模型打分,失败的输出里直接带上裁判理由:
judge: FAIL The plan totals 640 euros. The assistant quoted hotel prices that do not appear in the search-hotels result.裁判默认使用对话自身的模型,可传model换成更便宜的模型,也可以传judge换成任意打分服务(官方示例里用了一个返回概率的评估模型,缺 Key 时自动跳过自己,其余场景照常跑)。
💡 提示:裁判断言是实时模型调用——费钱且不完全可复现,只在其他匹配器都表达不了时再上,并把标准写得足够窄。
CI 稳定性的关键:用 stubs 钉住会漂移的数据
依赖"今天日期"或线上商品目录的场景,今天绿、下月红。stubs让你在场景里直接回答某个工具,而不经过你的 handler:
const chat = await start({ app, model, stubs: { "search-flights": ({ to }) => (to === "LIS" ? lisbonFixture : undefined), }, });返回undefined就回落到真实工具——这样既能钉住一组参数、又让其余调用保持"真实"。被 stub 的调用同样计入chat.toolCalls,所有匹配器照常工作。
🔐测试带鉴权的工具:传authInfo即可为会话声明身份。注意它只跳过 token 验证,每个工具自己的鉴权方案和 scope 检查都会真实执行;不传则走匿名路径(含鉴权挑战)。
CI 实践:何时跑、怎么省
Evals 是真实模型调用,会消耗 token,建议这样安排:
- 触发时机——在改动工具的名字、描述或 input schema时运行(这正是最容易让模型"不会调用"的改动),或按夜间任务跑
- 成本控制——保持
temperature: 0(默认值),断言落在工具调用而非措辞上;toHaveSaid尽量用宽松正则;裁判断言少而精 - 密钥注入——CI 中通过环境变量注入
ANTHROPIC_API_KEY(.env会被插件自动加载) - 数据稳定——会漂移的数据一律走
stubs,别依赖线上接口 - 调参——
skybridge({ evals: { systemPrompt, maxSteps, timeout } })设置全局默认,单个场景可在start中覆盖
相关代码与文档路径
- 官方文档:docs/test/evals.mdx(完整参数与默认值)
- 测试运行时源码:packages/test/,入口 packages/test/src/index.ts、会话与
start()实现 packages/test/src/session-registry.ts - 匹配器实现:packages/test/src/matchers/index.ts
- Vite 插件中 Evals 的接线逻辑:packages/vite-plugin/src/plugin.ts
- 完整参考示例(一个专为测试而生的滑雪商店 App,4 个工具、每个匹配器都有对应场景):examples/evals/,场景如 examples/evals/evals/search.eval.ts
小结
✅ Skybridge Evals 让你用三行代码回答一个关键问题:真实模型到底会不会正确地调用我的 MCP App 工具?进程内运行、类型安全断言、stub 钉住易变数据,再配上 CI 触发策略,你的工具改动从此有一道自动守门员。
【免费下载链接】skybridgeSkybridge is a full-stack TypeScript framework for MCP Apps and ChatGPT Apps. Type-safe. React-powered. Platform-agnostic.项目地址: https://gitcode.com/gh_mirrors/skybr/skybridge
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考