ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用 Flue 的 Evals 测试 Agent 真实行为:从进程内 Vitest 到 HTTP、Judge 与 CI

2026/9/16 20:39:04 拓冰建站 浏览量
用 Flue 的 Evals 测试 Agent 真实行为:从进程内 Vitest 到 HTTP、Judge 与 CI 用 Flue 的 Evals 测试 Agent 真实行为从进程内 Vitest 到 HTTP、Judge 与 CI【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue本文以 Fluesandbox agent framework官方 Evals 指南为主体讲解如何为 Agent 编写端到端行为测试它回答了「普通单测覆盖不到什么」、如何用init()在进程内驱动 Agent 并断言回复与工具调用、如何通过flue/sdk走完整 HTTP 边界评估部署形态以及如何用vitest-evals蓝图引入 JudgeLLM 判分器并把评估套件接入 CI。读完你可以直接在自己的 Flue 项目里搭建一套独立的 eval 套件用行为契约而非字符串快照来守护 Agent 的质量。什么是 eval为什么普通单测覆盖不到 AgentAgent 的行为由三部分共同涌现你的指令instructions、模型model和工具tools。其中你亲手写的部分仍然是普通代码可以用普通单测覆盖——例如一个工具的run函数本质上是纯函数不涉及模型完全可以直接单测。单测覆盖不到的是模型的贡献Agent 是否调用了正确的工具、是否遵守指令、是否给出正确的答案。Eval 就是弥补这个空白的自动化测试让一个 Agent 跑在真实模型上跑完「指令 — 模型 — 工具」的完整闭环然后对其可观察行为产生的回复、调用的工具、输出的数据做断言。两个特性决定了 eval 的写法Eval 是非确定性的。同样的输入可能产生不同的措辞、不同的工具调用顺序偶尔甚至不同的结果。因此断言要落在行为契约上——必须调用的工具、回复中的关键事实、结构化数据的形状——而不是精确的输出字符串。Eval 消耗真实的 token 和真实的时间。每个用例至少要跑一次真实模型的完整回合。所以 eval 必须有独立的套件、独立的配置、独立的凭据、独立的超时和运行节奏与普通单测分开。Flue 没有专门的 eval 框架。一个 eval 就是一个 Vitest 句柄或 HTTP 会话接口——然后对结果做断言。vitest-evals 生态页描述的是在此基础上叠加 eval harness、judge 和 CI 报告的集成层见下文「vitest-evals 集成」一节。搭建独立的 eval 套件实时模型测试与单测在文件发现规则、超时和凭据上需求都不同因此把 eval 放在独立的 Vitest 配置里让两套测试可以各自独立运行import { defineConfig } from vitest/config; export default defineConfig({ test: { include: [src/evals/**/*.eval.ts], testTimeout: 60_000, }, });60_00060 秒替代了 Vitest 默认的 5 秒超时——单次真实模型回合就可能超过 5 秒。在package.json里加一个脚本让整套 eval 用一条命令跑起来{ scripts: { evals: vitest run --config vitest.evals.config.ts } }eval 文件放在src/evals/下按能力或场景命名如service-health.eval.ts、refund-policy.eval.ts而不是每个 Agent 一个文件。仓库里的完整可运行示例 examples/vitest-evals 使用的就是这套配置它的 vitest.evals.config.ts 额外注册了vitest-evals/reporter终端报告器并在 package.json 中补充了evals:info详细工具与用量输出和evals:json输出 CI 用的 JSON 报告脚本。进程内评估start()init()在测试里跑 Agent 最直接的方式是用flue/runtime/node的start()——它把 Flue 运行时直接启动在测试进程内部不需要服务器、不需要构建。随后用init()句柄发送消息并等待回复落定import { init } from flue/runtime; import { start } from flue/runtime/node; import { afterAll, expect, it } from vitest; import { ServiceStatus } from ../agents/service-status.ts; const flue await start({ agents: [ServiceStatus] }); afterAll(() flue.stop()); it(checks live service status before answering, async () { const toolsCalled: string[] []; // No id: init() mints a fresh conversation for this case. const agent init(ServiceStatus); const receipt await agent.dispatch(Is the checkout service currently operational?); const reply await agent.read(receipt, { onEvent: (chunk) { if (chunk.type tool-input) toolsCalled.push(chunk.toolName); }, }); expect(reply.text).toContain(operational); expect(toolsCalled).toContain(get_service_status); });这里的每一处都是普通的程序化接口结合 packages/runtime/src/agent-client.ts 的实现可以看得更透每个用例一个全新会话。init(agent)不传id时内部调用generateInstanceId()铸造一个新的唯一实例地址会话彼此独立已保存的历史不会串到其他用例源码见 agent-client.ts。要评估「会话记忆」这类能力则复用同一个句柄通过它连续发多组dispatch(...)/read(...)。回复就是断言目标。read()返回的AgentReply中text是最终助手文本data承载命名过的useDataWriter数据块——结构化结果就在这里断言此外还有metadata、uid和submissionId见 AgentReply 定义。一次失败或中止的运行会让read()以AgentRunError拒绝outcome为failed | aborted从而让测试失败——这正是你想要的行为见 AgentRunError。工具调用以事件到达。read()的onEvent回调会收到每一条被持久化记录下来的会话 chunktool-inputchunk 携带模型发起的每次工具调用的toolName和input。完整的运行时行为。hooks、持久化、sandbox 都和在服务器里完全一致——start()是同一套装配只是没有 HTTP 表面。start()的实现位于 packages/runtime/src/node/start.ts。使用进程内方式有三个约束一个进程只能持有一个 Flue 运行时所以每个测试文件调用一次start()、文件结束时stop()。Vitest 默认的文件级隔离每个测试文件独立 worker正好保证文件之间互不干扰。Provider 凭据来自测试进程的环境变量见 Models — Provider credentials。eval 直接 import 了 Agent 模块所以该模块必须能在普通 Vitest 下加载。依赖构建期解析的 import例如SKILL.mdimport的 Agent需要走 Flue 构建此时应改用 HTTP 方式评估。通过 HTTP 评估flue/sdk挂载在app.ts中的 Agent见 Routing — Mounting an agent可以通过其 HTTP 表面、用 Flue Agent SDK 来评估——这正是部署后的应用对外服务的同一边界包含你的路由中间件。一个全新会话就是往挂载 URL 后面追加一个新的 idimport { createFlueClient } from flue/sdk; import { expect, it } from vitest; // The agents mount URL from app.ts; point FLUE_AGENT_URL at a deployment. const mountUrl process.env.FLUE_AGENT_URL ?? http://127.0.0.1:5173/agents/service-status; it(checks live service status before answering, async () { const conversation createFlueClient({ url: ${mountUrl}/eval-${crypto.randomUUID()}, }); const admission await conversation.send({ message: { kind: user, body: Is the checkout service currently operational? }, }); await conversation.wait(admission); const { messages } await conversation.history(); const reply messages.findLast((message) message.role assistant); const text reply?.parts .filter((part) part.type text) .map((part) part.text) .join() ?? ; expect(text).toContain(operational); });HTTP 上的提示词是即发即忘fire-and-forget语义send()先把消息送入admissionwait()等待它完成history()返回整段完成的会话——包括助手回复及其工具调用 parts。eval 进程不负责启动应用在另一个终端里跑vite dev或构建后的服务器或者把 URL 指到已部署的环境。当路由有保护时向createFlueClient(...)传token或headers见 Routing — Protecting your agents。send()、wait()、history()的完整契约见 Agent SDK — flue-client。两种评估表面怎么选取决于 eval 要覆盖什么表面覆盖范围前提进程内start()Agent 本身指令、模型、hooks、工具测试环境需具备 provider 凭据HTTPflue/sdkAgent 加app.ts路由与中间件需要运行中的 dev server 或部署环境两个表面都是公共 API因此它们也天然是接入其他 eval 库或托管平台例如 Braintrust 集成的集成点——用同样的方式驱动 Agent把结果交给自己的评分管道即可。send()的message: { kind: user, body }结构以及history()返回的messages/parts形状与 examples/vitest-evals/src/evals/harness.ts 中 harness 的用法完全一致。vitest-evals 集成蓝图、harness 与 judgevitest-evals为 Vitest 扩展了 eval harness、LLM judge、归一化报告和 CI 报告。用 Flue 的蓝图即可接入flue add tooling vitest-evals蓝图会替你创建上文「搭建独立的 eval 套件」中的 eval 配置与脚本并生成src/evals/harness.ts——一个每个用例驱动一个会话、通过flue/sdk把回复、工具调用和用量转换成归一化vitest-evals结果的 harness。完整可运行项目见 examples/vitest-evals。生成的 harness见 examples/vitest-evals/src/evals/harness.ts做了这些事通过createFlueClient({ url })提示一个已挂载的 Agent 会话每个用例一个全新会话 id${agentUrl}/eval-${crypto.randomUUID()}用服务端提供的 offset 与 submission id 捕获提示词的事件序列把响应文本、模型用量、成本和工具调用记录进归一化的 eval 结果通过FLUE_BASE_URL示例里是FLUE_AGENT_URL同时支持本地服务器与已部署应用支持token和headers选项用于受保护的目标。需要注意蓝图不会自动挂载 Agent。在走 HTTP 评估前先确认app.ts用createAgentRouter(...)挂载了 Agent并且其认证中间件配置得当示例中的挂载方式见 examples/vitest-evals/src/app.ts。用例用describeEval编写它把 harness 绑定到一个套件并给每个测试一个run(...)函数import { expect } from vitest; import { describeEval, toolCalls } from vitest-evals; import { createFlueAgentHarness } from ./harness.ts; const harness createFlueAgentHarness({ agentUrl: process.env.FLUE_AGENT_URL ?? http://127.0.0.1:5173/agents/service-status, }); describeEval(service status agent, { harness }, (it) { it(checks live service status before answering, async ({ run }) { const result await run(Is the checkout service currently operational?); expect(result.output).toContain(operational); expect(toolCalls(result).map((call) call.name)).toContain(get_service_status); }); });示例项目在此基础上还断言了用量examples/vitest-evals/src/evals/service-health.eval.ts 里追加了expect(result.usage.totalTokens).toBeGreaterThan(0)。这里的 usage/model 元数据来自 Agent 自己的useResponseFinish生产者——示例 Agent 在 examples/vitest-evals/src/agents/service-status.ts 中用useResponseFinish(({ response }) ({ usage: response.usage, model: MODEL }))把用量挂到回复的 metadata 上harness 再据此解析。如果 Agent 什么都不挂则报告无用量。Judge语义行为的判分器确定性断言deterministic assertions适合精确契约必需的工具、禁止的工具、结构化输出、稳定的内容。对于语义行为——事实一致性、语气、策略合规——vitest-evals提供了judge一种对结果打分、低于阈值即判失败的评分器。LLM 判分的 judge 跑在独立的judge harness上judge 自己的模型连接与被评估的 Agent 分开配置import { expect } from vitest; import { describeEval, FactualityJudge } from vitest-evals; describeEval(service status agent, { harness, judgeHarness }, (it) { it(reports status consistent with the reference answer, async ({ run }) { const result await run(Is the checkout service currently operational?); await expect(result).toSatisfyJudge(FactualityJudge(), { expected: The checkout service is currently operational., threshold: 0.6, }); }); });createJudge(...)可以定义自定义 judge支持确定性或 LLM 判分两种实现内置的FactualityJudge、ToolCallJudge、StructuredOutputJudge覆盖了最常见的评分维度。原则是先用确定性断言只有无法精确检查的行为才引入 judge——毕竟每次 LLM 判分都是额外的 token 开销和不确定性来源。本地运行与 CI 接入本地运行时一旦 provider 凭据就位进程内套件用一条命令即可pnpm run evalsHTTP 套件额外需要一个可达的目标先在另一个终端启动应用或把套件的 URL 变量指向已部署环境FLUE_AGENT_URLhttps://preview.example.com/agents/service-status pnpm run evalsvitest-evals 生态页使用FLUE_BASE_URL作为基础地址变量示例项目使用FLUE_AGENT_URL指向 Agent 挂载 URL两者含义一致服务器进程需要应用正常的模型 provider 凭据且绝不把 provider 或应用凭据提交进仓库。在 CI 中eval 套件就是一次普通的 Vitest 运行——用例失败即非零退出从而像任何其他测试任务一样为流水线设卡。但它应该与单测分开成独立的 job实时模型运行更慢、消耗 token而且可能在没有任何代码变更的情况下失败所以它们值得拥有自己的节奏——合并时跑、定时跑或按需跑。Provider 凭据来自 CI secrets对于 HTTP 套件要么在 job 内构建并启动应用要么把目标指向一个 preview 部署。关于报告vitest-evals 蓝图会添加一个evals:json脚本写出vitest-results.json产物。本地可以用vitest-evals serve vitest-results.json打开查看示例项目的对应脚本见 examples/vitest-evals/package.json或通过getsentry/vitest-evalsGitHub Action 从 CI 发布。注意报告可能包含提示词、输出、工具参数与结果、错误和应用元数据上传前要审视其留存策略与访问要求。下一步Vitest Evals 生态页——蓝图、生成的 harness 与报告命令的完整说明。Agents 指南——start()与 standalone 脚本即 eval 所基于的同一表面。Agent API 参考——完整的init()句柄契约、AgentReply与AgentRunError。Agent SDK——单个会话 URL 上的send()、wait()、history()。可观测性指南——用 Braintrust 等 provider 追踪 Agent 运行submissionId与会话 id 这类标识符可以把一个失败的用例关联到产生它的那次执行。examples/vitest-evals——可直接运行的完整示例项目含 Agent、harness、eval 用例与全部脚本。【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考