一条 AI 内容管线可能依次经过抓取、清洗、去重、分类、总结、审核和发布。页面最后只显示一篇文章,但后台真正需要回答的是:这次总结实际用了哪个模型?消耗了多少 Token?慢在抓取还是生成?一次请求失败后切换过模型吗?发布异常能否追回最初的输入批次?
如果记录里只有一行 summary failed,就算任务最终重试成功,也无法判断成本、质量和稳定性。本篇聚焦 AI 内容管线可观测性:用日志、指标、追踪和告警还原每次真实执行。任务如何入队和重试可参考AI 内容流水线与 BullMQ 任务队列对比;429、超时和 5xx 的处置则见大模型 API 429、超时和 5xx 排查。
一、先定义要回答的问题,而不是先装监控组件
可观测性不是“日志越多越好”。设计前先列出生产排查必须回答的问题:
- 哪个业务任务、哪一篇文章、哪个阶段出了问题?
- 配置选中的模型与请求实际命中的模型是否一致?
- 是否发生过重试、轮询、降级或故障切换?
- 输入、输出与总 Token 是多少,供应商是否返回可信用量?
- 模型调用耗时、完整任务耗时和排队等待分别是多少?
- 失败属于配置、限流、网络、HTTP、解析、质量门禁还是发布?
- 同类异常影响一个任务、一个模型,还是整个管线?
- 记录能否排查问题,同时不泄露 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% 又可能意味着大量文章受影响。阈值应从真实历史与业务目标推导。
可按以下步骤建立基线:
- 先运行只记录不告警的观察期,覆盖工作日、周末、定时任务和内容高峰;
- 按管线、阶段、供应商、模型和请求类型分组,检查样本量;
- 计算成功率、错误类型占比、P50/P95/P99 耗时、Token 分布、队列等待和每日调用量;
- 标记发布、模型切换、提示模板变化和上游故障,避免把变更造成的偏移误当正常;
- 根据服务目标与可接受影响定义告警,而不是机械复制历史最高值;
- 在测试环境或受控窗口验证告警能触发、通知能送达、恢复后能关闭;
- 定期复核基线,模型、流量和内容长度变化后重新校准。
低流量任务适合直接告警“连续 N 次计划运行失败”或“到约定时间仍无成功产物”;请求量较大时,可使用带最小样本量的时间窗口错误率。N、窗口长度和百分比必须由本站历史数据决定。
推荐把告警分成三类:
- 立即处理:整条管线停止、连续计划任务未产出、鉴权在全部请求中失败;
- 快速调查:某模型错误率相对基线显著上升、队列年龄持续增长、P95 延迟持续恶化;
- 趋势观察:单篇 Token 异常、降级比例增加、人工审核率缓慢变化。
单次 429 或一次模型切换通常只需记录,不应直接叫醒维护者。告警还应包含面板与排查手册链接,并按 pipeline + provider + errorType 合并,避免同一事故产生大量重复通知。
九、面板要同时展示可靠性、成本代理和内容结果
一个可操作的管理面板可以分四层:
- 管线概览:计划次数、成功次数、最近成功时间、当前运行阶段;
- 模型调用:实际模型分布、成功率、错误分类、P50/P95 耗时和切换比例;
- 用量:供应商返回 Token、缺失用量占比、按模型和阶段的趋势;
- 内容结果:输入条目、去重后数量、总结产物、质量门禁和人工审核数量。
后台的“当前使用模型”应同时展示本次运行快照:配置选择 → 实际尝试 → 最终成功模型。如果只有请求模型而没有响应确认,应明确标注“请求模型”,不要写成“供应商确认模型”。总结质量本身不能由延迟和 Token 推断,需另建评估数据集和门禁,参考AI 总结质量评估与回归测试。
十、上线前检查清单
- 每次管线运行有稳定的
jobId和traceId; - 每个阶段和每次模型尝试都有独立事件或 Span;
- 保存配置模型、请求模型、响应模型和切换原因,不读取当前配置冒充历史;
- Token 标明来自供应商、估算或缺失;
- 分开记录排队时间、阶段耗时、模型耗时与端到端耗时;
- 错误有稳定分类,原始错误消息不作为指标标签;
- 指标标签不包含 trace ID、文章 ID、正文或无限增长的值;
- 日志默认不保存 Key、鉴权头、提示词与完整响应;
- 告警建立在真实观察期、样本量和服务目标上;
- 告警经过触发与恢复演练,并附带负责人和排查入口;
- 模型、提示词或管线版本变更能在图表和 Trace 中识别;
- 数据保留期限、访问权限和删除流程已经明确。
真正有用的可观测性不是事后堆出更多日志,而是让一次内容生产从输入批次、模型选择、每次尝试、Token 与耗时,一直追到审核和发布都有可验证证据。先把事实记录准确,再用本站真实基线设置告警,才能避免“系统一直报警却没人相信”和“内容已经停更却无人发现”这两种极端。
从 Token 指标进入成本核算
可观测性记录 usage 后,还需绑定价格版本并与真实账单对账。具体方法见 大模型 Token 成本核算与预算控制。