主题
Workers 下的 Tracing 实现
1. 从一个线上排查场景开始
你有没有遇到过这种情况:线上 Agent 任务输出的产物出了问题,你打开日志只看到模型调用耗时、token 用量和错误码,却不知道问题到底出在目标解析、资料召回、审稿规则还是交付检查。
你只好把用户输入、Prompt、模型输出、中间产物全部翻一遍,像手动回放一段录像。如果任务还经过了好几个 Agent 节点,排查时间就会翻倍。
这种问题的根因不是日志不够多,而是日志没有围绕「一次任务」组织。一次 Agent 任务包含多个判断节点,单个模型调用只是其中的一个 span。Tracing 要做的,就是把这些节点串成一条可检索、可回看的链,让维护者先定位到具体环节,再决定要不要深入现场。
2. 先确定 tracing 要回答的问题
Agent 项目的线上问题,通常会跨过多个节点:
- 资料召回命中了哪些来源,是否缺少官方或一手资料。
- 结构是否承接了用户的目标和对象设定。
- 产物是否引用了未验证材料,是否遗漏了前文设定的边界。
- 审稿智能体发现了哪些问题,哪些问题被改写阶段处理掉。
- 交付检查为何失败,是事实风险、版权风险、风格不一致,还是必填元信息缺失。
如果 trace 只记录模型、耗时和错误码,排查时仍然需要回放整条任务。更合适的目标,是用可控成本留下足够的信息,让维护者能判断问题出现在「输入资料」「任务分解」「模型输出」「审稿规则」还是「交付前检查」。
3. Workers 环境里的边界
Cloudflare Workers 适合承接边缘 API、鉴权、流式代理和轻量编排。它的运行模型和传统长驻进程不同,设计 tracing 时需要先接受几条边界:
- 请求结束后不能依赖本地内存继续存在。
- 写入 D1、KV、R2 或外部观测平台都会增加延迟。
- 流式输出场景下,响应主路径对同步写入更敏感。
- 高频任务不能全量保存产物、资料片段和完整 Prompt。
- trace 可能包含用户未发布产物、内部资料和模型中间产物,需要脱敏和保留周期。
这些边界会影响实现方式:Workers 中的 trace 更适合做「轻量记录、后台写入、失败优先、摘要留存」。完整现场只在高价值样本里保留。
4. TraceContext 应该跟着任务走
一次任务可以创建一个 TraceContext。它的粒度对应 taskId 或 productId,单个 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";
};
};这里最重要的是 inputSummary 和 outputSummary。它们不保存完整产物,而是保存排障和评估需要的结构化摘要。
| 节点 | 建议记录的输入摘要 | 建议记录的输出摘要 |
|---|---|---|
| 目标解析 | goal、audience、目标长度、语言 | planCount、coverageWarnings |
| 资料召回 | queryCount、requiredSourceTypes | sourceCount、officialSourceCount、failedTools |
| 产物生成 | planVersion、sourceCount、promptVersion | sectionCount、citationCount、tokenEstimate |
| 审稿 | reviewRuleset、riskLevel | findingCount、blockingCount、styleIssueCount |
| 改写 | requestedChanges、blockingFindingCount | resolvedFindingCount、remainingFindingCount |
| 交付检查 | metadataRequired、seoRequired、citationRequired | passed、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。更稳妥的策略是:
- 请求过程中只在内存里累计
TraceContext。 - 返回响应或结束流之后,用后台任务写入。
- 正常完成的任务按比例采样。
error、degraded、needs_review、交付检查失败的任务优先保存。- 写入失败只记录观测错误,不反向影响已经完成的响应。
在 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 默认包含敏感信息,因此保存策略要从第一版实现就进入设计。
建议至少做四件事:
- 保存结构化摘要,不保存完整产物。
- 对用户输入、资料片段和模型输出做长度截断。
- 对内部 URL、邮箱、密钥形态文本和未发布标题做脱敏。
- 设置不同保留周期,例如正常任务 7-14 天,失败和人工确认任务保留更久。
采样也应该按任务状态区分。正常完成任务可以低比例保存;触发降级、审稿阻断、交付检查失败或人工确认的任务,应优先保存摘要和必要现场。这样能把存储预算留给更需要排查的样本。
9. 小结
Workers 下的 tracing,要服务于 Agent 项目的排障和评估:一次任务如何经过目标解析、资料召回、结构、产物、审稿、改写和交付检查,每个节点留下了哪些可判断的摘要。
实现上可以把 TraceContext 绑定到任务,用 span 包装每个 agent 节点,通过 ctx.waitUntil() 把写入放到后台,再用 D1 保存摘要、R2 保存必要现场。这样线上出现质量问题时,维护者可以先沿 trace 查到具体节点和状态,再决定是否需要打开更详细的任务现场。
一句话总结
Agent 项目的 trace 不是记录每次模型调用,而是围绕一次任务留下可检索、可回放的节点摘要,让排查从「翻找所有日志」变成「定位到具体环节」。