Skip to content

Workers 下的 Tracing 实现

1. 从一个线上排查场景开始

你有没有遇到过这种情况:线上 Agent 任务输出的产物出了问题,你打开日志只看到模型调用耗时、token 用量和错误码,却不知道问题到底出在目标解析、资料召回、审稿规则还是交付检查。

你只好把用户输入、Prompt、模型输出、中间产物全部翻一遍,像手动回放一段录像。如果任务还经过了好几个 Agent 节点,排查时间就会翻倍。

这种问题的根因不是日志不够多,而是日志没有围绕「一次任务」组织。一次 Agent 任务包含多个判断节点,单个模型调用只是其中的一个 span。Tracing 要做的,就是把这些节点串成一条可检索、可回看的链,让维护者先定位到具体环节,再决定要不要深入现场。

2. 先确定 tracing 要回答的问题

Agent 项目的线上问题,通常会跨过多个节点:

  1. 资料召回命中了哪些来源,是否缺少官方或一手资料。
  2. 结构是否承接了用户的目标和对象设定。
  3. 产物是否引用了未验证材料,是否遗漏了前文设定的边界。
  4. 审稿智能体发现了哪些问题,哪些问题被改写阶段处理掉。
  5. 交付检查为何失败,是事实风险、版权风险、风格不一致,还是必填元信息缺失。

如果 trace 只记录模型、耗时和错误码,排查时仍然需要回放整条任务。更合适的目标,是用可控成本留下足够的信息,让维护者能判断问题出现在「输入资料」「任务分解」「模型输出」「审稿规则」还是「交付前检查」。

3. Workers 环境里的边界

Cloudflare Workers 适合承接边缘 API、鉴权、流式代理和轻量编排。它的运行模型和传统长驻进程不同,设计 tracing 时需要先接受几条边界:

  • 请求结束后不能依赖本地内存继续存在。
  • 写入 D1、KV、R2 或外部观测平台都会增加延迟。
  • 流式输出场景下,响应主路径对同步写入更敏感。
  • 高频任务不能全量保存产物、资料片段和完整 Prompt。
  • trace 可能包含用户未发布产物、内部资料和模型中间产物,需要脱敏和保留周期。

这些边界会影响实现方式:Workers 中的 trace 更适合做「轻量记录、后台写入、失败优先、摘要留存」。完整现场只在高价值样本里保留。

4. TraceContext 应该跟着任务走

一次任务可以创建一个 TraceContext。它的粒度对应 taskIdproductId,单个 agent 的本地运行状态记录在 span 里。

typescript
type SpanStatus = "ok" | "error" | "degraded" | "needs_review";

type TraceSpan = {
  spanId: string;
  parentSpanId?: string;
  name: string;
  agentRole?:
    | "planner"
    | "researcher"
    | "executor"
    | "reviewer"
    | "editor"
    | "publisher";
  status: SpanStatus;
  startedAt: number;
  durationMs?: number;
  inputSummary?: Record<string, unknown>;
  outputSummary?: Record<string, unknown>;
  errorCode?: string;
  degradedReason?: string;
};

type TraceContext = {
  traceId: string;
  taskId: string;
  productId?: string;
  locale?: string;
  spans: TraceSpan[];
  tags: {
    workflowVersion?: string;
    promptVersion?: string;
    model?: string;
    channel?: "web" | "admin" | "api";
  };
};

这里最重要的是 inputSummaryoutputSummary。它们不保存完整产物,而是保存排障和评估需要的结构化摘要。

节点建议记录的输入摘要建议记录的输出摘要
目标解析goal、audience、目标长度、语言planCount、coverageWarnings
资料召回queryCount、requiredSourceTypessourceCount、officialSourceCount、failedTools
产物生成planVersion、sourceCount、promptVersionsectionCount、citationCount、tokenEstimate
审稿reviewRuleset、riskLevelfindingCount、blockingCount、styleIssueCount
改写requestedChanges、blockingFindingCountresolvedFindingCount、remainingFindingCount
交付检查metadataRequired、seoRequired、citationRequiredpassed、failedChecks、needsHumanReview

这张表的核心作用,是把多智能体协作里的中间判断变成可检索的事件。后续排查质量问题时,可以先看 span 摘要,再决定是否需要打开更详细的现场。

5. 用 span 包装每个 agent 节点

每个任务节点都可以通过同一层包装记录耗时、状态、摘要和错误码。节点内部仍然保持自己的职责边界,例如资料召回只处理来源,审稿只输出问题列表,改写只处理可执行修改。

typescript
async function withSpan<T>(
  trace: TraceContext,
  span: Omit<TraceSpan, "spanId" | "startedAt" | "status">,
  run: () => Promise<T>,
  summarizeOutput: (value: T) => Record<string, unknown>,
): Promise<T> {
  const currentSpan: TraceSpan = {
    ...span,
    spanId: crypto.randomUUID(),
    status: "ok",
    startedAt: Date.now(),
  };

  try {
    const result = await run();
    currentSpan.durationMs = Date.now() - currentSpan.startedAt;
    currentSpan.outputSummary = summarizeOutput(result);
    return result;
  } catch (error) {
    currentSpan.durationMs = Date.now() - currentSpan.startedAt;
    currentSpan.status = "error";
    currentSpan.errorCode =
      error instanceof Error ? error.name : "UNKNOWN_ERROR";
    throw error;
  } finally {
    trace.spans.push(currentSpan);
  }
}

例如资料召回节点可以这样记录:

typescript
const sources = await withSpan(
  trace,
  {
    name: "research.retrieve_sources",
    agentRole: "researcher",
    inputSummary: {
      queryCount: queries.length,
      requiredSourceTypes: ["official", "paper", "docs"],
    },
  },
  () => retrieveSources(queries),
  (value) => ({
    sourceCount: value.length,
    officialSourceCount: value.filter((source) => source.type === "official")
      .length,
    failedTools: value.flatMap((source) => source.failedTools ?? []),
  }),
);

这类包装能让各个 agent 的观测字段保持一致,也能保留节点之间的父子关系。需要分析一次任务为何进入人工确认时,可以沿着 parentSpanId 从交付检查回看审稿、资料召回和产物生成。

6. 写入策略放到后台路径

不要在响应主路径里同步写完整 trace。更稳妥的策略是:

  1. 请求过程中只在内存里累计 TraceContext
  2. 返回响应或结束流之后,用后台任务写入。
  3. 正常完成的任务按比例采样。
  4. errordegradedneeds_review、交付检查失败的任务优先保存。
  5. 写入失败只记录观测错误,不反向影响已经完成的响应。

在 Workers 里,可以用 ctx.waitUntil() 处理后台写入:

typescript
ctx.waitUntil(persistTrace(trace));

流式输出场景下,trace 写入尤其需要和用户响应分开。否则资料召回、审稿和交付检查已经完成,用户仍然可能因为观测写入变慢而等待。

7. 存储分层

trace 可以拆成两层保存:

层级内容存储建议
Trace 摘要taskId、productId、状态、耗时、span 列表、失败检查、降级原因D1
详细现场资料来源摘要、审稿问题、Prompt 版本、工具结果摘要、人工确认原因R2 或压缩 JSON

D1 更适合查询和聚合,例如按 promptVersion 统计审稿阻断率,或按 workflowVersion 比较交付检查失败类型。R2 更适合保存较大的结构化现场,例如资料来源摘要和审稿问题列表。

不建议把大段产物正文放进 D1。正文既会增加成本,也会扩大隐私和版权风险。需要长期分析质量趋势时,优先把 trace 转成指标,例如:

  • 资料召回失败率。
  • 官方来源覆盖率。
  • 审稿阻断率。
  • 改写后问题残留数。
  • 交付检查失败类型分布。

8. 脱敏、采样和保留周期

任务 trace 默认包含敏感信息,因此保存策略要从第一版实现就进入设计。

建议至少做四件事:

  1. 保存结构化摘要,不保存完整产物。
  2. 对用户输入、资料片段和模型输出做长度截断。
  3. 对内部 URL、邮箱、密钥形态文本和未发布标题做脱敏。
  4. 设置不同保留周期,例如正常任务 7-14 天,失败和人工确认任务保留更久。

采样也应该按任务状态区分。正常完成任务可以低比例保存;触发降级、审稿阻断、交付检查失败或人工确认的任务,应优先保存摘要和必要现场。这样能把存储预算留给更需要排查的样本。

9. 小结

Workers 下的 tracing,要服务于 Agent 项目的排障和评估:一次任务如何经过目标解析、资料召回、结构、产物、审稿、改写和交付检查,每个节点留下了哪些可判断的摘要。

实现上可以把 TraceContext 绑定到任务,用 span 包装每个 agent 节点,通过 ctx.waitUntil() 把写入放到后台,再用 D1 保存摘要、R2 保存必要现场。这样线上出现质量问题时,维护者可以先沿 trace 查到具体节点和状态,再决定是否需要打开更详细的任务现场。

一句话总结

Agent 项目的 trace 不是记录每次模型调用,而是围绕一次任务留下可检索、可回放的节点摘要,让排查从「翻找所有日志」变成「定位到具体环节」。

参考资料

基于 MIT 协议开源