从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践

"回答看起来对"并不等于"这次执行是对的"。

做 Agent workflow 时,最容易被忽略的就是这件事。

例如在一个"取消订单"的工作流中,Agent 最后回复了用户"订单已取消"。这句话可能是真的,也可能是模型在工具调用失败后补出来的;它可能绕过了鉴权节点,也可能该进入人工审核时走错了分支。等到改了一段 prompt、换了模型,或者调整了节点配置,问题会变得更拆不清楚:我们究竟是在修复问题,还是在引入另一种退化?

这也正是开源项目 Workrun(一款强调 Local-first 的桌面端 Agent Workflow 自动化工具)在设计 Telemetry(遥测)与 Evaluation(评测)时最想解决的问题。

我没有把它们设计成两套独立的"附加功能":一套用来打日志,另一套用来跑测试。相反,Workrun 先保留一份可回放、经过脱敏的运行事实;本地诊断、成本指标、评测结果和版本比较,都是从这份事实派生出来的。

这篇文章会以一个订单取消 workflow 为例,介绍这套设计是怎样落地的,也会如实说明它目前的工程边界。

问题不只在最终输出

先看一个很典型的工作流:用户请求取消订单,Agent 需要先查询订单、检查权限和风控状态,再决定取消还是转人工审核。

flowchart TD A["用户:请取消订单 42"] --> B[authorization] B --> C[lookup_order] C --> D[risk_check] D -->|safe| E[cancel_order] E --> F[respond] D -->|risky| G[manual_review]

如果只验证最终文本,我们大概会写一个测试:输出中必须包含"订单 42 已取消"。但这远远不够:

  • lookup_order 可能没有成功,模型却凭空猜了一个答案;
  • cancel_order 可能根本没被调用,模型只是"伪造"了成功回复;
  • 风控命中后本应转人工,工作流却依然继续执行了取消;
  • 工具结果或最终输出可能带出了敏感字段;
  • 新版本看起来还能回答问题,但 token、延迟和错误率已经明显变差。

因此,我们希望一次运行结束后至少能回答两类问题:

  1. 这次运行中发生了什么? 哪个节点、模型调用或工具调用出了问题?(Telemetry 的职责)
  2. 这次运行的行为是否符合预期? 下一次修改后,是否发生了可识别的回归?(Evaluation 的职责)

前者是 Telemetry 的职责,后者是 Evaluation 的职责。它们的共同基础,是同一份运行证据

一份运行事实,多个派生视图

Workrun 中,每一次 workflow 或 app 执行都会有一个 Run。运行过程中产生的事件会按顺序持久化到本地 Run History;输出面板、span、聚合指标和评测观察值都不是唯一事实来源,而是围绕事件日志建立的投影。

flowchart TD A[Workflow execution] --> B[脱敏事件日志] B --> C["本地 Run History / span projection"] C --> C1["节点、模型、工具耗时"] C --> C2["token、成本、错误"] C --> C3["成功率、p50/p95、版本指标"] B --> D[Evaluation observation] D --> D1[最终输出] D --> D2[节点与路由轨迹] D --> D3[工具调用和结果] B --> E[可选 OTLP export] E --> E1["workflow trace context + GenAI spans"]

这个设计看起来朴素,但它带来了两个很重要的工程结果:

  1. 诊断信息与业务执行解耦:Span 是事件日志的派生投影;即使某次 telemetry 写入失败,Run 本身仍然完好保留,工作流绝不会因为"观测系统挂了"而崩溃。
  2. 评测不必再造一套平行的执行器:评测系统可以直接读取同一份经过处理的运行证据,评估真实的执行路径,而不是只对一个脱离 runtime 的模拟结果打分。

本地 telemetry:把运行过程变成可查询的数据

Workrun 的本地 telemetry 重点不是堆很多日志,而是把运行中的关键动作投影为可查询的 span

对于 workflow,当前会记录三类核心 span:

  • workflow node:节点在哪一步执行、执行了多久、最后是否完成;
  • model call:模型名、输入/输出 token、cache token、reasoning token、audio token、估算成本以及是否 BYOK;
  • tool call:调用了什么工具、耗时多久、成功还是失败。

这些 span 都通过 run_id 关联到一条 Run。例如,下面是一条写入 run_events 的脱敏后模型调用事件:

json 复制代码
{
  "type": "custom",
  "node": "risk_check",
  "event_type": "agent.model_call",
  "data": {
    "modelCallId": "4a563af4-5f36-4b5b-9ac5-2c6372b3164f",
    "model": "gpt-5",
    "startedAt": "2026-09-21T10:30:12Z",
    "endedAt": "2026-09-21T10:30:13Z",
    "durationMs": 842,
    "inputTokens": 320,
    "outputTokens": 45,
    "totalTokens": 365,
    "totalTokensEstimated": false,
    "cacheReadTokens": 128,
    "estimatedCostMicrousd": 730,
    "isByok": true
  }
}

这个事件会被投影为 model_call span;模型调用的内容不需要进入 span 表,查询时仍可在保留期内回到经过脱敏的事件证据。

在聚合层,Workrun 会按 workflow、版本和时间范围计算成功率、平均耗时、p50/p95、token 与估算成本。离线 evaluation 流量会和普通生产运行显式标记区分开,避免批量测试把日常运行指标冲高。

一个容易漏掉的细节:终态 span 收口

正常情况下,节点会收到 node_start → node_end,工具会收到 tool_call → tool_resulttool_error,span 会自然结束。

但真实系统里还有另一种情况:运行在节点或工具执行中被取消,或者因为异常提前失败。此时最后的事件未必能送达;如果不处理,本地历史里会留下永远处于 running 的 span。

Workrun 现在会在 Run 进入终态时,在同一个数据库事务 中收口仍处于 running 的 span:

  • Run 正常完成时,遗留 span 标记为 completed
  • Run 失败时,遗留 span 标记为 failed
  • Run 取消或中断时,遗留 span 标记为 cancelled
  • 已经有明确结束状态的 span 不会被覆盖。

这不是一个很"炫"的功能,却直接决定了历史数据是否可信。对诊断系统而言,终态一致性比多一个图表更重要

OTLP:把本地诊断接入标准 tracing 工具

本地 Run History 适合在 Workrun 中快速复盘单次运行;当需要跨运行、跨服务,或者希望接入团队已有的 APM 系统时,Workrun 也支持把 tracing 数据导出到远端 OTLP collector。

项目设置中提供了 OTLP 端点:填入兼容 OTLP/gRPC 的 collector 地址并保存,重启后生效。未配置时,工作流完全在本地高能运行;配置后,Workrun 会将 workflow 和 ADK runtime 的 tracing context 实时导出到远端。

下图来自一条真实的"退款申请" workflow:它先提取结构化信息,随后调用模拟 CRM 查询,再根据工具结果生成 最终 JSON。该次运行在 Jaeger 中持续约 6.5 秒 ,共导出了 25 个 span

text 复制代码
workrun.workflow.run [Run ID, Workflow ID, Version, Thread ID]
├── run (Agent Loop 1: 语义解析)
│   └── call_llm
│       └── model.generate_content
│           ├── execute_stream
│           └── gen_ai.generate [Model: gpt-5, Tokens: 320/45]
├── run (Agent Loop 2: 工具调用)
│   ├── call_llm ──► gen_ai.generate
│   └── execute_tool: crm_lookup_user [Duration: 420ms]
└── run (Agent Loop 3: 最终响应生成)
    └── call_llm ──► gen_ai.generate

根 span workrun.workflow.run 附带 Run ID、workflow ID、版本和 thread ID;子 span 则清晰拆解出每轮 Agent 执行、模型请求(含 token 与 provider)和工具调用的耗时。这能一眼看出时间究竟是花在了"模型等待"还是"工具执行"上。

本地 Run History 用于面向作者的执行复盘,OTLP trace 则把同一次运行接入 Jaeger 或 Grafana Tempo。两者共享上下文标识,排查远端异常时,凭 run_id 就能精准锚定本地历史现场。

需要提醒的是,远端 collector 属于本地设备之外的数据系统。虽然 Workrun 会对事件进行脱敏,但 trace 仍包含部分运行元数据,应按照生产级基础设施规范来配置访问控制与保留期。

Evaluation:评估 workflow 行为,而不只评估答案

有了运行证据,下一步才是 Evaluation。

回到取消订单的例子。下面是一个"高风险订单不允许直接取消"的 Case 定义:

json 复制代码
{
  "id": "cancel-order-with-risk",
  "name": "高风险订单转人工审核",
  "input": { "message": "请取消订单 42" },
  "expectation": {
    "assertions": [
      {
        "kind": "node_trajectory",
        "id": "expected-path",
        "mustExecute": ["authorization", "lookup_order", "risk_check", "manual_review"],
        "mustNotExecute": ["cancel_order"],
        "orderedNodes": ["authorization", "lookup_order", "risk_check", "manual_review"],
        "requireCompleted": true
      },
      {
        "kind": "route",
        "id": "risk-route",
        "nodeId": "risk_check",
        "expectedRoute": "risky"
      },
      {
        "kind": "tool_trajectory",
        "id": "lookup-only",
        "tools": [{ "name": "lookup_order", "args": { "orderId": "42" } }],
        "config": { "strictOrder": true, "strictArgs": true }
      }
    ]
  },
  "fixture": {
    "toolFixtures": [
      {
        "tool": "lookup_order",
        "args": { "orderId": "42" },
        "result": { "status": "high_risk", "owner": "user_123" }
      }
    ]
  }
}

配置了两个评测用例 高风险订单转人工审核安全订单直接取消

Workrun 当前的评测能力以确定性断言为主,支持:

  • 最终文本的精确、包含或相似度匹配;
  • 最终 JSON 的 JSONPath 断言;
  • 工具轨迹、工具参数和返回结果匹配;
  • 节点是否执行、是否完整执行、是否遵守预期顺序;
  • 控制节点是否走到了指定 route;
  • 指定节点的输出、文本和工具轨迹;
  • 面向最终输出、工具参数或工具结果的安全断言。

编辑评测用例:

评测结果:

查看运行输出:

这使得"最终回答对了,但行为错了"不再会被轻易放过。比如模型回复了"订单已取消",却没有调用 cancel_order,工具轨迹断言会失败;如果风控命中却没有进入 manual_review,route assertion 会失败。最终文本只是证据的一部分,不再是唯一判据。

为什么离线评测不会真的取消订单

对涉及外部系统的 workflow 来说,测试安全性是第一位的。Workrun 的 evaluation 不会放开真实工具调用,而是通过 exact-match fixture 提供工具响应。

json 复制代码
{
  "tool": "cancel_order",
  "args": { "orderId": "42" },
  "result": { "status": "cancelled" }
}

评测时,工具调用必须准确命中 fixture;没有 fixture 的调用会直接失败。这样做有几个好处:

  • 🔒 安全性:评测不会真的写入订单系统;
  • 🐛 暴露隐患:未预期的工具调用会暴露出来,而不是悄悄穿透到真实环境;
  • 🧪 可复现:工具返回结果是稳定的,因此断言失败更容易解释和复现。

快照:为什么一次历史评测不会被后来的编辑改写

评测系统很容易犯一个错误:今天打开三周前的一次失败记录,却发现它正在用今天的 case 定义重新解释过去的运行。

Workrun 在创建 evaluation run 时,会冻结并生成快照(workflow snapshot、workflow fingerprint、suite/case、输入、预期、fixture 和 execution profile)。后续修改 prompt、节点、case 或 fixture,不会改写已经存在的评测证据。

这让版本比较有了实际意义:

text 复制代码
v1.2.0:10 / 10 passed
v1.3.0: 9 / 10 passed

Regression:cancel-order-with-risk
原因:risk_check 后没有进入 manual_review

即使两个版本都能产出"看起来合理"的最终文本,版本差异仍然能指出:哪一个 case 从通过变成失败、哪一条 criterion 发生了退化,以及对应的执行证据是什么。

脱敏优先:证据有用,但不该成为新的泄露面

Run History 和 evaluation 都会接触到模型输出、工具参数和工具结果,这些地方最容易出现敏感数据。

Workrun 的做法是先建立脱敏的可见证据投影,再将其用于历史查看和评测。它会处理常见凭据字段、文本中的秘密模式,以及部分 PII;workflow 也可以配置明确的敏感字段。安全断言会保留"哪个路径命中"或"禁止文本出现了几次"这类结论,而不会把命中的敏感值再次写进评测结果。

发布前检查:可审计的旁路,而不是硬门禁

Workrun 可以为 workflow 配置发布前质量检查:例如最低通过率、最高成本、最长耗时,以及必须通过的 suite。检查的是当前 candidate workflow snapshot 对应的评测结果,而不是某个历史版本的旧数据。

版本比较则给这次检查补上了"相对变化"的上下文。对同一个 suite,先为基线 workflow 运行一次评测;修改节点、prompt 或配置并保存后,再运行一次。每次运行都会冻结各自的 workflow snapshot,评测页会将不同快照(团队模式下也可对应已发布版本)列为可选的基线和候选版本。

选择两个版本后,Workrun 会并排展示通过率、估算成本和总耗时,并列出发生变化的 Case:新增失败、修复、持续失败或只存在于一侧的 Case。点开某个变化项,还可以逐条比较冻结的 criterion 判定。于是"高风险订单转人工审核 从通过变为失败"不只是一个红色状态,而能继续定位到是节点路径少了 manual_reviewrisk_route 命中了错误分支,还是工具轨迹出现了不应有的 cancel_order 调用。

对比总览:

对比详情:

在个人模式中,没有语义化发布版本时,比较单位仍是不可变的 draft snapshot fingerprint;在团队模式中,发布后的版本号会成为更易辨认的比较对象。无论哪种模式,比较读取的是各版本最近一次非重试的完整评测运行,而不是将某次失败重试混入基线。

当前的产品语义是:如果检查未通过,发布界面会要求操作者显式确认旁路并填写原因,同时保存当时的质量策略和评测快照,供之后回看。

这是一种可审计的发布前质量检查,不是服务端不可绕过的强制发布门禁。把边界说清楚并不会削弱它的价值:在本地优先的工作流工具里,显式旁路和可追溯记录往往比静默忽略一次失败更有意义。

目前已经解决了什么,还有什么没有解决

到目前为止,Workrun 已经把几件通常容易断开的事情串了起来:

  • 脱敏优先的本地运行历史;
  • 节点、模型和工具维度的耗时、token、成本与错误诊断;
  • ✅ 运行终态与 span 终态的 DB 事务级一致性
  • ✅ 基于真实 workflow runtime 的确定性评测
  • Fixture 隔离 和不可变评测快照;
  • Case / Criterion 维度的版本回归比较;
  • 可选的 OTLP 导出(已验证 Jaeger 链路)。

同时,它仍然有清晰的边界:

  • 🚧 当前评测以确定性断言为主,还不是 LLM-as-a-judge 或 rubric 平台;
  • 🚧 没有用 fixture 替代真实外部系统的端到端验收;
  • 🚧 OTLP 仍然是可选诊断出口;
  • 🚧 本地 span 也没有被设计成一套完整的 distributed tracing tree。

LLM-as-a-judge 是后续计划补上的能力。它更适合处理"回复是否专业""是否完整解释了风险""语气是否符合预期"这类难以写成固定规则的问题。但它会作为确定性断言的补充,而不是替代:工具是否被调用、路由是否正确、结构化字段是否存在,仍然应该优先由可复现、可解释的规则来判断。

结语

对 Agent workflow 来说,最先需要解决的往往不是"采集更多数据",而是让一次运行留下足够可信的证据:

能定位问题,能解释结果,能复现失败,也能在下一次修改后识别回归。

当运行事实、telemetry 和 evaluation 共享同一条证据链时,workflow 才开始从"能跑的自动化"变成真正可维护的工程系统。

Workrun 是一个开源项目,欢迎在 GitHub 查看源码与交流:1111mp/workrun-app

相关推荐
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
回眸&啤酒鸭1 天前
【回眸】Minicart 电商购物车核心功能落地指南
人工智能
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
一隅论数智1 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
AI的探索之旅1 天前
97 个 OpenCV 实例(三十):双目立体,从标定到点云
人工智能·opencv·计算机视觉
AlbertZein1 天前
Step-5-Preview 上手实测:3D 游戏、金融分析、网页设计一次跑完
人工智能·aigc
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
LaughingZhu1 天前
Product Hunt 每日热榜 | 2026-09-19
人工智能·深度学习·神经网络·搜索引擎·百度
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)