原创

AI 内容管线可观测性:如何记录模型、Token、耗时、Trace 与告警

为 AI 内容管线建立可审计的日志、指标、追踪与告警:记录实际使用模型、Token、阶段耗时和失败类型,用 Trace ID 串联抓取到发布,并依据真实历史基线设定阈值。

AI 内容工程专题 · 第 9/11 篇查看专题目录 →

一条 AI 内容管线可能依次经过抓取、清洗、去重、分类、总结、审核和发布。页面最后只显示一篇文章,但后台真正需要回答的是:这次总结实际用了哪个模型?消耗了多少 Token?慢在抓取还是生成?一次请求失败后切换过模型吗?发布异常能否追回最初的输入批次?

如果记录里只有一行 summary failed,就算任务最终重试成功,也无法判断成本、质量和稳定性。本篇聚焦 AI 内容管线可观测性:用日志、指标、追踪和告警还原每次真实执行。任务如何入队和重试可参考AI 内容流水线与 BullMQ 任务队列对比;429、超时和 5xx 的处置则见大模型 API 429、超时和 5xx 排查。

一、先定义要回答的问题,而不是先装监控组件

可观测性不是“日志越多越好”。设计前先列出生产排查必须回答的问题:

  1. 哪个业务任务、哪一篇文章、哪个阶段出了问题?
  2. 配置选中的模型与请求实际命中的模型是否一致?
  3. 是否发生过重试、轮询、降级或故障切换?
  4. 输入、输出与总 Token 是多少,供应商是否返回可信用量?
  5. 模型调用耗时、完整任务耗时和排队等待分别是多少?
  6. 失败属于配置、限流、网络、HTTP、解析、质量门禁还是发布?
  7. 同类异常影响一个任务、一个模型,还是整个管线?
  8. 记录能否排查问题,同时不泄露 API Key、提示词与用户内容?

这组问题决定字段和信号。日志适合保留离散事件与上下文,指标适合看趋势和触发告警,Trace 适合还原跨阶段路径。三者应通过同一个 traceId 或业务任务 ID 关联,而不是各自生成互不相干的编号。

二、记录“实际使用模型”,不能只读当前配置

后台配置会变化。任务开始时选择了模型 A,执行中可能重试到模型 B;管理员随后又把默认模型改成 C。如果任务详情只读取当前配置,它会错误地显示“本次使用 C”。

每次执行应保存不可变快照,至少区分四个概念:

字段 含义 示例用途
configuredModelId 任务创建时选中的内部配置 ID 追查当时的配置选择
attemptedModelId 本次尝试准备调用的配置 ID 分析轮询与重试路径
requestedModel 发给接口的 model 值 发现模型名配置错误
responseModel 响应中供应商返回的模型值 确认实际服务版本;无值则记 null

内部配置 ID 比展示名称稳定。供应商、base URL 主机、模型名、配置版本可以保存;API Key 和完整鉴权头不能保存。base URL 如果包含租户、部署或查询参数,也应先规范化,只保留排查所需部分。

一次调用的结构化记录可以长这样:

{
  "event": "ai_model_attempt_finished",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "jobId": "daily-2026-08-13",
  "stage": "summarize",
  "attempt": 2,
  "providerConfigId": "model-config-02",
  "requestedModel": "example-model",
  "responseModel": null,
  "outcome": "timeout",
  "durationMs": 30124
}

这里的 responseModel: null 表示供应商没有返回或响应未完成,不能用请求模型回填并伪装成响应事实。多模型配置和轮询策略可继续阅读OpenAI 兼容 API 多模型统一配置。

三、日志:每个阶段记录开始、结果和决策

建议为抓取、去重、分类、总结、审核、发布定义统一事件,而不是让每个模块自由拼字符串:

  • pipeline_started:任务来源、配置版本和输入批次;
  • stage_started、stage_finished:阶段名、耗时、输入与输出数量;
  • ai_model_attempt_started、ai_model_attempt_finished:模型快照、尝试序号、用量和结果;
  • model_route_decided:选择、轮询、降级或切换的原因代码;
  • quality_gate_finished:通过、拒绝或进入人工审核,以及规则版本;
  • article_published:文章 ID、slug 和内容版本;
  • pipeline_finished:最终状态、总耗时和失败阶段。

错误不要只保留 message,应使用有限枚举的 errorType:

const ERROR_TYPES = new Set([
  'auth', 'invalid_request', 'rate_limit', 'timeout',
  'network', 'provider_5xx', 'response_parse',
  'quality_rejected', 'storage', 'publish', 'unknown'
]);

同时保存供应商状态码和经过清洗的错误代码。这样既能按稳定维度聚合,又不会丢失定位线索。不要把原始异常文本直接作为指标标签:它可能含敏感内容,而且高基数会让时序数据库产生大量序列。

四、Token:以响应事实为准,缺失就是缺失

支持用量字段的接口通常会提供输入、输出和总 Token,但字段名与口径可能不同。接入层应先保留供应商原始值,再映射到内部字段:

function normalizeUsage(rawUsage) {
  if (!rawUsage) {
    return { inputTokens: null, outputTokens: null, totalTokens: null, source: 'missing' };
  }

  return {
    inputTokens: rawUsage.prompt_tokens ?? rawUsage.input_tokens ?? null,
    outputTokens: rawUsage.completion_tokens ?? rawUsage.output_tokens ?? null,
    totalTokens: rawUsage.total_tokens ?? null,
    source: 'provider_response'
  };
}

需要注意:

  • 流式接口的用量可能只出现在结束事件,连接中断时可能拿不到;
  • 本地 tokenizer 的估算可用于请求前限长,但必须标为 estimated,不能混进供应商返回的实测值;
  • 不同供应商对缓存 Token、推理 Token、多模态输入的计量口径不同;
  • 成本应根据当时的价格版本单独计算,不能用今天的价格回算并声称是历史账单;
  • 若请求失败但供应商可能已经处理,Token 记录应为未知,不能直接写零。

建议同时记录输入条目数、输出字符数等非计费指标,它们能帮助发现“输入数量没变但 Token 突增”的模板或内容异常,但不能替代官方计费用量。

五、指标:控制标签基数,分别看阶段与模型

一组实用的最小指标可以包括:

ai_pipeline_runs_total{result,failed_stage}
ai_pipeline_stage_duration_seconds{stage,result}
ai_model_requests_total{provider,model,result,error_type}
ai_model_request_duration_seconds{provider,model,result}
ai_model_tokens_total{provider,model,direction,usage_source}
ai_pipeline_items_total{stage,direction}
ai_pipeline_inflight{stage}
ai_pipeline_queue_age_seconds

articleId、jobId、traceId、完整 URL 和错误消息不应作为指标标签,它们的取值几乎无限,适合放日志或 Trace。模型标签也要规范化:若响应会返回带日期或临时版本的模型名,应决定保留完整版本还是映射成稳定系列,并在日志里保留原值。

耗时建议使用直方图而不是只记录平均值。平均值会掩盖长尾;分位数和桶分布更适合观察慢请求。桶边界不能照搬示例,应根据自己的真实延迟范围设置,并在模型或请求类型变化后重新检查。

六、Trace:从抓取一路串到发布

按照 W3C Trace Context,可通过 traceparent 在服务之间传播追踪上下文。即使当前系统只有一个 Node.js 服务,也可以先建立 Span 层级:

pipeline.run
├── source.fetch
├── content.deduplicate
├── content.classify
├── ai.summarize
│   ├── ai.request attempt=1 model=A
│   └── ai.request attempt=2 model=B
├── quality.review
└── article.publish

顶层 Span 表示一次管线运行;每个阶段是子 Span;每次真实模型请求再单独建 Span。模型切换不能覆盖上一条 Span,否则只能看到最终成功,看不到第一次失败造成的额外耗时和可能成本。

日志写入同一个 traceId 和 spanId,后台任务详情即可从摘要跳转到完整 Trace。跨消息队列时,应把追踪上下文放入任务元数据,并在消费端继续或链接原 Trace;不要把整份用户正文塞进元数据。OpenTelemetry 的生成式 AI 语义约定可用于统一属性,但其具体版本和稳定性状态会变化,实现时要以所用版本的官方文档为准。

七、脱敏:默认不记录正文,再按需要受控采样

AI 调用最容易在日志里泄露的是提示词、原始文章、模型输出与 Key。建议默认采用允许列表,只写明确批准的字段:

function safeModelLog(attempt) {
  return {
    traceId: attempt.traceId,
    jobId: attempt.jobId,
    stage: attempt.stage,
    providerConfigId: attempt.providerConfigId,
    model: attempt.requestedModel,
    status: attempt.status ?? null,
    errorType: attempt.errorType ?? null,
    durationMs: attempt.durationMs,
    inputTokens: attempt.usage?.inputTokens ?? null,
    outputTokens: attempt.usage?.outputTokens ?? null
  };
}

至少禁止记录:Authorization、API Key、Cookie、完整请求头、原始提示词、未经审核的正文、完整模型响应和可能带凭证的 URL 查询参数。仅靠正则替换 Bearer 不够,因为敏感值可能出现在 JSON、异常对象或代理日志中。

确实需要样本排查质量时,应使用独立的受控存储:先做内容分级和脱敏,限制访问者与保存期限,记录查看审计,并允许按文章或用户删除。普通应用日志不应承担提示词档案库的角色。

八、告警阈值必须来自真实基线

“错误率超过 1% 就告警”听起来明确,却可能不适合低频日报:一天只有一次任务时,一个失败就是 100%;高频系统中,1% 又可能意味着大量文章受影响。阈值应从真实历史与业务目标推导。

可按以下步骤建立基线:

  1. 先运行只记录不告警的观察期,覆盖工作日、周末、定时任务和内容高峰;
  2. 按管线、阶段、供应商、模型和请求类型分组,检查样本量;
  3. 计算成功率、错误类型占比、P50/P95/P99 耗时、Token 分布、队列等待和每日调用量;
  4. 标记发布、模型切换、提示模板变化和上游故障,避免把变更造成的偏移误当正常;
  5. 根据服务目标与可接受影响定义告警,而不是机械复制历史最高值;
  6. 在测试环境或受控窗口验证告警能触发、通知能送达、恢复后能关闭;
  7. 定期复核基线,模型、流量和内容长度变化后重新校准。

低流量任务适合直接告警“连续 N 次计划运行失败”或“到约定时间仍无成功产物”;请求量较大时,可使用带最小样本量的时间窗口错误率。N、窗口长度和百分比必须由本站历史数据决定。

推荐把告警分成三类:

  • 立即处理:整条管线停止、连续计划任务未产出、鉴权在全部请求中失败;
  • 快速调查:某模型错误率相对基线显著上升、队列年龄持续增长、P95 延迟持续恶化;
  • 趋势观察:单篇 Token 异常、降级比例增加、人工审核率缓慢变化。

单次 429 或一次模型切换通常只需记录,不应直接叫醒维护者。告警还应包含面板与排查手册链接,并按 pipeline + provider + errorType 合并,避免同一事故产生大量重复通知。

九、面板要同时展示可靠性、成本代理和内容结果

一个可操作的管理面板可以分四层:

  1. 管线概览:计划次数、成功次数、最近成功时间、当前运行阶段;
  2. 模型调用:实际模型分布、成功率、错误分类、P50/P95 耗时和切换比例;
  3. 用量:供应商返回 Token、缺失用量占比、按模型和阶段的趋势;
  4. 内容结果:输入条目、去重后数量、总结产物、质量门禁和人工审核数量。

后台的“当前使用模型”应同时展示本次运行快照:配置选择 → 实际尝试 → 最终成功模型。如果只有请求模型而没有响应确认,应明确标注“请求模型”,不要写成“供应商确认模型”。总结质量本身不能由延迟和 Token 推断,需另建评估数据集和门禁,参考AI 总结质量评估与回归测试。

十、上线前检查清单

  • 每次管线运行有稳定的 jobId 和 traceId;
  • 每个阶段和每次模型尝试都有独立事件或 Span;
  • 保存配置模型、请求模型、响应模型和切换原因,不读取当前配置冒充历史;
  • Token 标明来自供应商、估算或缺失;
  • 分开记录排队时间、阶段耗时、模型耗时与端到端耗时;
  • 错误有稳定分类,原始错误消息不作为指标标签;
  • 指标标签不包含 trace ID、文章 ID、正文或无限增长的值;
  • 日志默认不保存 Key、鉴权头、提示词与完整响应;
  • 告警建立在真实观察期、样本量和服务目标上;
  • 告警经过触发与恢复演练,并附带负责人和排查入口;
  • 模型、提示词或管线版本变更能在图表和 Trace 中识别;
  • 数据保留期限、访问权限和删除流程已经明确。

真正有用的可观测性不是事后堆出更多日志,而是让一次内容生产从输入批次、模型选择、每次尝试、Token 与耗时,一直追到审核和发布都有可验证证据。先把事实记录准确,再用本站真实基线设置告警,才能避免“系统一直报警却没人相信”和“内容已经停更却无人发现”这两种极端。

从 Token 指标进入成本核算

可观测性记录 usage 后,还需绑定价格版本并与真实账单对账。具体方法见 大模型 Token 成本核算与预算控制。

查看原文链接 →

实测与内容说明

实测记录

  • 本文提供通用的数据结构、埋点和告警设计示例,未声称本站已经测得特定延迟、成功率、Token 成本或告警降噪效果。
  • 文中的阈值均用于解释方法,不是生产推荐值;实际阈值必须由目标系统自己的历史数据、服务目标和业务容忍度确定。
  • 示例代码按 Node.js ESM 语法进行静态审阅,字段需结合所用模型供应商的响应结构、计费规则和 OpenTelemetry SDK 版本验证。
  • 来源采用 W3C、OpenTelemetry、Prometheus 与 OWASP 等官方规范或项目文档;对于供应商不返回的 Token 或请求 ID,正文明确要求标记缺失而不是估算成实测值。

参考资料

内容版本 1.1 · 审核:推荐智能手记 · 计划复审:2026-11-13