AI Analysis Processor Runbook
last-verified: 2026-07-14(对照 infra PR #1459;发现和代码不符以代码为准)
服务: ResultProcessorTS(Lambda call-analytics-{env}-ai-analysis-processor-ts-{region}),读转录结果、跑 3 段 AI pipeline、写 DynamoDB + Neon。
这本 runbook 管深度调查:某通电话为什么分析失败、AI 输出质量为什么差、DLQ 里的 message 怎么补。告警响应(DLQ 有 message、queue 积压该怎么办)不在这里 —— 那是 Pipeline Monitoring Runbook 的事,本文只在需要时链过去。
Table of Contents
- 现状速览
- 处理流程
- 3 段 AI Pipeline
- 验证与重试机制
- 告警来自哪里
- Common Issues 深度排障
- CloudWatch Logs Insights 查询
- 操作手册
- Code Reference
现状速览
一句话:单模型 deepseek/deepseek-v4-flash 经 OpenRouter.ai 调用,没有 model fallback,分 3 段跑,每段独立 Zod schema 校验。以前那套 Bedrock vs Cloudflare 双 provider 已完全废弃 —— 看到旧文档写 "根据 ai_provider 选 Bedrock/Cloudflare" 直接忽略。
Model 迁移史(背景,无需操作): 从 Grok 4.1 via OneRouter 换到 DeepSeek V4 Flash via OpenRouter.ai(PR #939/#941/#953),原因是成本(~5x 便宜)+ 高 cache 命中率。OneRouter 代码已全删。DDB config 表里存量 item 还带 ai_model: 'moonshot.kimi-k2-thinking' / ai_provider: 'bedrock' 是 dead data,没人读,别信。
Provider routing: OpenRouter 上层 provider 顺序 ['deepseek', 'alibaba', 'atlas-cloud'](见 lambda/shared/utils/ai/provider-routing.ts),allow_fallbacks: true,p99 latency gate 60s。这些是 OpenRouter 侧的 upstream 选择,不是我们的 model fallback。
处理流程
入口: lambda/ai-analysis-processor/src/handler.ts。SQS 每条 message = 一通电话的一次分析,失败走 batchItemFailures → SQS redrive。
- 解析事件: 从 EventBridge-wrapped SQS message 提取 S3 key,还原
transcriptionJobName。 - 查 Neon: 用 session/recording 直接查 Neon call 记录(migration 期间保留 DDB
transcriptionJobName-indexGSI 做 fallback)。 - 去重: 检查 Neon 行的
s3AnalysisPath;已有 = 跳过,避免重复 AI 成本。另有一层 claim-based defense-in-depth 防并发重复([SKIP] Another Lambda owns this session→ 返回成功让 SQS 删掉重复消息)。 - 下载转录: 从 S3 拉转录,最短
MIN_TRANSCRIPT_LENGTH = 10字符。太短 → 存空分析记录(不调 AI,零成本)直接返回成功。 - 跑 pipeline:
runPipeline()顺序跑 triage → classify → coaching(详见下一节)。 - 合并 + 校验:
mergePipelineResult()把 3 段结果拼成 legacyCallAnalysis形状,过一遍 legacy schema 校验。 - 持久化: 写 S3 分析摘要 + DynamoDB + Neon(calls + contacts + timeline 原子 batch),更新
s3AnalysisPath。 - 触发下游: enqueue Contacts Analyzer(per-call SQS)。
任何步骤 throw → batchItemFailures → SQS 按 maxReceiveCount=5 redrive,5 次后进 DLQ。
3 段 AI Pipeline
入口: lambda/ai-analysis-processor/src/core/stages/pipeline.ts。核心变化:不再是"一次 AI call 返回全部字段",而是最多 3-4 段顺序 AI call,每段有自己的 prompt、schema、超时和重试预算。这是排障时最需要先理解的结构 —— 一通电话失败,先问是哪一段失败。
为什么 classify 的超时最长且重试最少(lambda/ai-analysis-processor/src/infrastructure/ai/invoke-model.ts 的 STAGE_CONFIG): 2026-05-25 PR #997 实测 classify 是结构性慢的一段(长转录输入 + 复杂 discriminated-union schema),p99 latency 在多个 provider 上 55-57s,超过默认 45s 会频繁 abort。所以 classify 拉到 90s × 2 次;真卡过 90s 的第 3 次也救不回,交给 SQS redrive 换个新 Lambda 实例(可能路由到不同 upstream)。triage / coaching 成功率 99%+,不是瓶颈,保持 [45s, 60s] × 3。
Stage 2.5 Verify 是 observe-only(2026-05-30 team 决策): verify 会对易混 subcategory 独立重推理,但结果不写入持久化记录、不影响下游。它只 emit 一条 verify_decision 日志,用来统计 adopt/disagree/confirm 率,决定将来要不要让它真改数据。排障时看到 stage_2_5_verify_review 日志,那是观测数据不是错误。
Worst-case 单次调用耗时(对照 Lambda 600s timeout):
- triage: 45+60+60 + 2×2s backoff = 169s
- classify: 90+90 + 1×2s = 182s
- coaching: 45+60+60 + 2×2s = 169s
- 合计 ≈ 520s,留 80s 给 DB 写入 + SQS publish + Sentry flush。
验证与重试机制
每段 AI 输出用 Zod schema 校验,校验失败会把具体错误反馈给下一次 attempt 让模型自我修正(不是盲重试)。这是保留下来仍准确的核心机制,只是实现从旧的 CallAnalysisSchema.parse() + 手搓 retry loop 换成了 Vercel AI SDK + withRetry。
为什么用 mode: 'json' 而不是严格 json_schema(lambda/shared/utils/ai/invoke.ts): Vercel AI SDK 的 mode: 'json' 发 response_format: { type: 'json_object' },schema 作为描述注入 prompt,由 Zod 在 client 侧校验。严格 json_schema 模式表达不了我们用的 oneOf / discriminatedUnion 形状(triage/classify/coaching 输出都靠它)。改成严格模式会直接 break 这些 schema。
Validation-feedback 重试(issue #1081,invoke.ts 的 extractValidationFeedback + buildRetryUserMessage): 校验失败时,把失败的字段和原因(最多 10 条 issue / 1500 字符)追加到下一次 attempt 的 user message 末尾(system prompt 保持 byte-identical 以维持 cache),让 attempt 2 精准自我修正。日志里 validationFeedbackInjected: true 标记这类重试。
为什么需要: schema 从 @retaintive/common 组合后收窄了几个 AI 可发的 enum(如 create 的 typeCategory 限于 AI_ASSIGNABLE_TASK_TYPE_CATEGORIES)。一个越界 enum 值就让整个响应在 Zod parse 时失败。低温度下盲重试大概率重复同样的错;把具体失败字段告诉模型才能自我纠正,而不是烧完重试预算掉进 SQS redrive/DLQ。
哪些错误会重试(isRetryableError,invoke.ts):
重试预算: 默认 retries: 1(= 2 次 attempt)。per-stage 覆盖见 STAGE_CONFIG。SQS maxReceiveCount=5 负责 provider 级恢复(换新 Lambda 实例)。
告警来自哪里
先记住一条,能省一半误判: logger.error 不会 page 人。它只写 CloudWatch + Sentry。日志 severity 和打扰人的紧急度是两回事(见 lambda/shared/utils/logger.ts)。
旧文档里"logger.error 会发 Discord/Lark/page 人"的说法全错 —— Discord 路径已死,忽略所有 Discord 提法。
告警卡从哪来(所有环境同构,按环境分群 — PROD 群 = 立即,TEST 群 = 工作时间;infra PR #1472): 不是靠 logger.error,而是靠 CloudWatch 告警 → SNS → alarm-dispatcher Lambda → Lark + Sentry(lib/stacks/monitoring-stack.ts)。跟本 Lambda 相关的 pageable 告警:
AiAnalysisProcessorErrorAlarm 是诊断性的 —— PR #1459 明确把 ai-analysis 的 error/provider-failure 告警"de-action"了,理由是最终队列健康才是客户影响的 page 信号。所以 DLQ 和 queue-age 才是真正 pageable 的症状,error count 只是 dashboard/ticket 信号。
Common Issues 深度排障
告警响应的通用步骤(先看 queue、修永久原因、redrive 后确认回 0)在 Pipeline Runbook,这里讲具体某类失败为什么发生、怎么定位根因。
Issue #1: DLQ 里堆积 message
先做: 按 Pipeline Runbook #dlq 确认失败类型和影响范围,再回来对号入座根因。
看最近一小时的 error 分布:
看 error 类型 × 哪段失败:
关注:同一 error 反复出现(10+ 次)、特定 store 持续失败、集中在某一 stage(尤其 classify)。
根因 A: AI 调用超时(classify 最常见)
日志特征: ai_attempt_aborted 事件,stage: "classify",latencyMs ≈ timeoutMs。
为什么: classify 段长转录 + 复杂 schema 结构性慢。ai_attempt_aborted 日志会 dump inputPreview(前 8KB)供 on-call 复现具体卡住的 payload。abort 是设计内的(我们自己的 timer 触发),单次 abort 不是 incident;大量 abort 才是 provider TTFB 降级信号。
怎么办:
- 单通失败 → SQS 已自动 redrive,大概率换个 upstream 就过。不用手动干预。
- 大面积 classify abort → 看 §Issue #4 判断是不是 provider 降级。
- 若
promptChars异常大(超长转录)→ 见 §Issue #3 转录长度分布。
根因 B: Schema 校验失败(重试用尽)
错误特征: NoObjectGeneratedError / TypeValidationError / schema_validation failureType,重试后仍失败。
为什么: 模型反复吐同一个越界 enum / 错类型,即使 validation-feedback 注入也没纠正过来。常见触发:
- 越界 enum: AI 发了个 taxonomy 里没有的 subcategory / typeCategory。
@retaintive/common收窄 AI-emittable enum 后(PR #1077),一个越界值就整条 fail。 - 类型错: 该给 number 给了 string。
- outcome 条件规则违反:
membership_cancel只能cancelled/retained/pending_follow_up;其他 subcategory 只能success/attempted/na。
注意 classify 的 subcategory 有降级机制(pipeline.ts deriveClassifyTopicFields): classify 的 subcategory 是 plain string,遇到未知值降级为 other 而不是烧一次 schema-repair 重试。所以 subcategory 未知不一定进 DLQ —— 看日志里 Classify emitted unknown subcategory, falling back to "other" 的 warn。
怎么办:
- 若是最近改了 schema/taxonomy → 检查
@retaintive/common的 enum 是否和 prompt 里描述的一致。 - 若某 store 转录有特殊 pattern 持续触发 → 收集样本转录 + 错误日志开 issue。
- 校验规则本身要改 → 走
@retaintive/common,消费方同 PR 适配(高风险,过 review)。
根因 C: S3 转录不存在
错误特征: NoSuchKey / 找不到转录 key。
根因:
- TranscribeProcessor 成功但 S3 上传失败 → 查上游 TranscribeProcessor Runbook 同
telephonySessionId。 - Neon 行里的转录 S3 key 与实际路径不符。
- S3 对象被删(retention / 手动)→ 查 S3 versioning + CloudTrail
DeleteObject。
怎么办: 若录音还在,重跑转录(见 TranscribeProcessor runbook);若有备份转录,重新上传到对应 S3 路径会触发 EventBridge 重新分析。
根因 D: Neon / DDB 写入失败
错误特征: 写 s3AnalysisPath / calls / contacts / timeline batch 时 throw。
为什么: Neon 连接失败 / DDB 限流 / batch 里某字段约束违反。AI 分析已完成但持久化挂了 → 整条重试(浪费已完成的 AI call,所以这类要尽快修)。
怎么办:
- 查是不是 Neon 侧问题(连接数 / migration drift)。
- 短暂故障 → SQS redrive 会自愈。
- 系统性写入失败 → 见 Pipeline Runbook 场景 B。
Issue #2: 校验重试率飙升(输出质量差)
症状: 大量 validationFeedbackInjected: true 的重试,即使能成功也拖慢处理(每次重试加一整段 AI 调用)。这是输出质量问题,不一定进 DLQ,但吃成本和延迟。
定位失败字段 pattern:
看哪一段质量差:
- 集中在 classify → subcategory/outcome/enum 类问题最常见。
- 集中在 coaching → 输出结构(
detailed_feedback_item嵌套)问题。
常见根因:
- Schema 改动后模型没跟上: 最近改了
@retaintive/commontaxonomy 或某段 prompt,模型还在按旧约束吐。查 prompt version(ai_attempt_start日志的promptVersion)是否和 schema 一致。 - Prompt 里 enum 描述和 schema 不一致:
mode: 'json'靠 prompt 描述引导模型,描述过时就会持续越界。 - 模型 drift: OpenRouter 上游 provider 换了模型 quant 变体导致行为漂移 —— 看
ai_usage.provider是不是变了。
怎么办:
- 先确认 validation-feedback 机制在工作: 应看到 attempt 2 有
validationFeedbackInjected: true,且大部分随后ai_usage attemptOutcome: success。 - 若 feedback 注入了但 attempt 2 仍失败 → prompt/schema 有硬矛盾,不是模型能自我修正的,查 prompt ↔ schema 一致性。
- 持续 >baseline → 收集样本开 issue,考虑改 prompt(prompt 改动至少更新 snapshot,涉及 task/lifecycle 补 golden eval)。
Issue #3: 处理慢(Duration 飙升)
症状: Lambda duration 逼近 600s timeout;ai-analysis-queue 积压增长。
先做: queue 积压响应看 Pipeline Runbook #queue-age。这里定位为什么单通慢。
看每段 latency:
正常基线(2026-05 实测):
- triage / coaching: p99 2-14s
- classify: p99 40-57s(结构性慢,已是最慢一段)
异常判读:
- classify p99 逼近 90s 且 abort 增多 → provider TTFB 降级,见 §Issue #4。
- 多段都慢 → OpenRouter 整体降级或某 upstream 出问题。
- 单通极慢 + 转录超长 → 见下面转录长度。
看转录长度分布:
超长转录(>30 分钟通话)会拉高 classify TTFB。这是已知 trade-off,单通会 redrive 自愈;若某 store 系统性超长,收集样本讨论是否要截断策略。
Issue #4: Provider 降级或 OpenRouter 故障
症状: AiProviderFailureAlarm 触发(AIRequestFailure ≥ 10 / 15min),或某类 failureType 突增。这不是我们的 model fallback 问题(没有 fallback)—— 是 OpenRouter 或模型本身降级。
看 failure 类型分布:
AIRequestFailure 的维度是 FailureType(timeout / http_429 / http_5xx / http_4xx / no_object / json_parse / schema_validation / unknown)× Analyzer(pipeline stage)。判读:
http_429突增 → OpenRouter 限流。等待自愈或联系 OpenRouter。http_5xx/timeout突增 → 上游 provider 宕。OpenRouter 的allow_fallbacks: true会自动切别的 upstream,通常几分钟自愈。schema_validation突增 → 见 §Issue #2(是输出质量,不是 provider)。
看当前路由到哪个 upstream:
ai_usage.provider 显示实际路由的上游(DeepSeek / Alibaba / AtlasCloud 等)。若某平时常用的 upstream 突然 0 calls,说明它被 p99 latency gate 挤下去了,routing 已自动降级到下一个 —— 这是设计内的自愈,不用干预。
怎么办:
- 大部分 provider 降级靠 OpenRouter fallback + SQS redrive 自愈,先观察不手动干预。
- 若 OpenRouter 整体故障(所有 upstream 都失败)→ pipeline 会积压,DLQ 会堆,走 Pipeline Runbook;根因确认(OpenRouter 恢复)后 redrive DLQ。
- 没有手动切 provider 的操作 —— 旧文档里
aws dynamodb update-item ... ai_provider那套已完全废弃,ai_provider是 dead field。
CloudWatch Logs Insights 查询
Log Group: /aws/lambda/call-analytics-{env}-ai-analysis-processor-ts-{region}
日志字段来自 invoke.ts 的结构化 log,关键事件:ai_attempt_start / ai_attempt_aborted / ai_usage / verify_decision。租户字段是 franchiseId / accountId / storeId(旧 client_id 已 deprecated)。
Query 1: 某通电话完整 trace
跨 stage 看这通电话走到哪一段、哪段失败。
Query 2: 各段 latency 对比(24h)
Query 3: 各段 abort 率(定位 timeout 段)
Query 4: Provider 分布 + cache 命中(成本相关)
cache 命中率高 = 成本低(cache read 比 fresh input 便宜 ~5x)。costUsd 是 OpenRouter 已算好的账单值,不是本地估算。
Query 5: Validation-feedback 重试有效性(#1081)
看有多少 attempt 靠 feedback 重试;配合 Query 1 看这些重试是否随后成功。
Query 6: 未知 subcategory 降级(质量信号)
高频未知 subcategory = taxonomy 或 prompt 该补;这些不进 DLQ(降级为 other),但是分类质量流失。
Query 7: Verify 观测(stage 2.5,observe-only)
看 verify 的 adopt/disagree/confirm 率。decisionType = corrected_classify 表示 verify 若上线会改这次分类 —— 现阶段只观测不改。
Query 8: S3 转录缺失
Query 9: 短转录处理(voicemail / 挂断量)
短转录(<10 字符)存空记录不调 AI;高短通话率反映 voicemail/挂断多。
操作手册
重新分析一通电话(Reprocess)
触发条件: 分析结果错了要重跑、schema 改动后要回填、单通调试。
- 从 Neon
call-analysis删掉这通电话的s3AnalysisPath(去重靠它,不删会被 skip)。 - 重新上传转录到原 S3 路径 → 触发 EventBridge → 重新分析。
- 详细步骤见 reprocess-ai-analysis runbook(infra repo)或
/reprocessskill。
Redrive DLQ(根因修复后)
先确认根因已修(跑 Query 1 看最近无同类 error),再 redrive,否则 message 会立刻回 DLQ。
安全提示: redrive 会对每条 message 重新调 Lambda(有成本 + AI 调用);message 走正常流程(最多 5 次 redrive);根因没修则回 DLQ。redrive 后盯 DLQ 掉回 0,让 AiAnalysisDLQAlarm 恢复 OK —— DLQ 长期留在 ALARM 会掩盖后续新增 message。
Region 按环境: test=us-west-2, pre=us-east-2, prod=us-east-1。AWS profile 用 aws configure list-profiles 找有账号 237206024479 访问权的(通常 AdminAccess-237206024479)。
本地验证 schema 改动
改 @retaintive/common taxonomy 或某段 schema 后,部署前先跑 Lambda 单测:
schema/taxonomy 改动是高风险: 走 @retaintive/common,消费方同 PR 适配,过 review。prompt 改动至少更新 prompt snapshot;涉及 task/lifecycle/worldview 补 golden eval case。
Code Reference
入口:
lambda/ai-analysis-processor/src/handler.ts— SQS batch 处理 +processSingleRecord()编排
Pipeline:
lambda/ai-analysis-processor/src/core/stages/pipeline.ts— 3-4 段编排 + verify 决策lambda/ai-analysis-processor/src/core/stages/{triage,classify,verify,coaching}.ts— 各段 prompt + schemalambda/ai-analysis-processor/src/infrastructure/ai/invoke-model.ts— per-stage timeout/retry 配置(STAGE_CONFIG)
共享 AI 层:
lambda/shared/utils/ai/invoke.ts— Vercel AI SDK + OpenRouter 核心调用、重试分类、validation-feedback、usage 日志lambda/shared/utils/ai/provider-routing.ts— OpenRouter upstream provider 顺序lambda/shared/utils/ai/model-config.ts— 默认 model ID(deepseek/deepseek-v4-flash)
日志 / 告警:
lambda/shared/utils/logger.ts—error(CloudWatch+Sentry) vsalert(+Lark)lambda/ai-analysis-processor/src/utils/metrics.ts—AIRequestFailure/ProcessingDuration/NeonWriteDurationEMF metric
基础设施:
lib/stacks/lambda-stack.ts— Lambda 配置(600s / 512MB / reserved 8)lib/stacks/monitoring-stack.ts— DLQ / queue-age / provider-failure 告警 + alarm-dispatcherlib/stacks/sqs-stack.ts—ai-analysis-queue/ai-analysis-dlq(maxReceiveCount 5)
规则:
.claude/rules/ai-analysis.md(infra repo)— AI 层的 Why + 隐式约束
Related Documentation
- Pipeline Monitoring Runbook — 告警响应 + 跨 Lambda trace(告警来这里先看)
- TranscribeProcessor Runbook — 上游转录 Lambda
- Reprocess AI Analysis — 重跑单通分析
- OpenRouter Activity — AI 用量 + 成本(不在 Lambda 内 track)
- Zod — schema 校验库