Flow 2: Per-Call Analysis
Lambda: ai-analysis-processor(callytics-infrastructure) · 触发: SQS(transcribe-processor 完成后) · 错误处理: Neon batch 阻塞,SQS send 非阻塞
性能预期(SLA)
End-to-end p95 ≤ 5 分钟(recording_available → AI 分析完成、Neon 落库)。Runtime 24h 实测(2026-05-06 audit):95.7% < 5min,p95 = 4.48 min,max 19.33 min(异常 outlier ~1%)。Lambda compute 本身 p95 = 61s,主要 latency 来自 OneRouter AI 调用 + S3/SSM round-trip。
⚠️ 已知 gap — DDB / Neon 双写 reconciliation 缺失
DDB write(saveAnalysis / updateAnalysisFlags)在 Neon db.batch() 之前且独立。若 DDB 成功 + Neon 失败:SQS retry 时 checkExistingAnalysis 命中 DDB 已有 record → skip 整段 → Neon 永远拿不到这通通话。详见 infra#782。Migration 期间双写 reconciliation 决策见 #454 epic owner。
::: details 实施溯源(2026-05-04)
写入模式:全部 db.batch()(2026-03-28 Neon-primary 决策)。DDB 写入(call-analysis 表)是过渡期冗余,独立代码块,不参与 batch。
关键事实校对:
franchiseId从 SQS 上游 metadata 传入,不硬编码。P0 #632 修复防止把_client_id整串当 franchiseId 写入resolvePhoneIdentity经 PhoneStoreAssignments DDB 返回{ storePhone, contactPhone, storeId }- 下游 Contact Analysis trigger:non-blocking SQS send,需
contactPhone + storeId都解析成功才触发,失败由 daily cron 兜底 store_id已写入 contacts / calls / contact_timelinesite_id→account_idrename 已完成(common migration 0016)
timeline 双 writer 共存(Phase 1):2026-04-29 起 transcribe-processor 也写 timeline(call.status_changed),与本 Flow 的 call.created 互补。Phase 2(infra#683,≥4 周后)删 transcribe 的 call.created writer。详见 call-lifecycle-tracking.md § 6.3。
:::
数据流概览
db.batch() 一次 HTTP 原子写 calls UPDATE + contacts UPSERT + timeline INSERT。DDB 写入在 batch 之外,SQS 发送在 batch 之后。
读取数据
触发输入
AI 分析前从 Neon 读取
从 Neon 读以下 3 张表(2026-05 起 customer-history reader 已切到 Neon),包含 customerSummary(DDB 没有)。
读取失败不阻塞 AI 分析(graceful degradation,跟当前 DDB 模式一致)。
AI Prompt 注入方式
历史上下文放 user message,不放 system prompt,保持 system prompt 可缓存:
Per-Call Analysis AI 输出
Per-Call Analysis 分析单通电话(含上下文),输出 33 个字段写入 calls 表(不是 contacts 表):staffName / callState / customerType / confidence / topics / summary / follow-up / coaching feedback 等。
calls 表另有 2 个 AI 相关字段(original_staff_name + amendment_history)由 studio-api 在用户手动 amend 时写入,不属于 ai-analysis-processor 路径。详见 Step ①.5。
Step ①:UPDATE calls(33 AI 字段)
写入 33 个 AI 分析字段到 calls 表。
Step ①.5:UPDATE calls(2 个 staff amendment 字段)
由 studio-api 在用户手动修正 staff 归属时写入,不属于 ai-analysis-processor 路径。callytics-common PR #85 加列,studio-website-monorepo PR #289 / #290 加双写逻辑。
端点 PUT /v3/calls/:id/staff 双写 DDB + Neon。PUT /v2/calls/:id 已恢复纯 DDB,不再写 Neon(v2/v3 解耦)。
Step ②:UPSERT contacts(基础字段)
只写基础字段,不写任何 AI 判断字段(AI 判断由 Contact Analysis 负责,见本节末尾)。
INSERT 时写入
firstAttemptedAt/firstConnectedAt字段已从 schema 移除(callytics-common#32 完成),由contact_timeline.call.created事件替代。code 不再写。
ON CONFLICT (phone, store_id) DO UPDATE
PK 由 (phone, franchise_id, site_id) 改为 (phone, store_id),common migration 0014。
Per-Call Analysis 不写的字段(Contact Analysis 所有权)
以下字段全部由 Contact Analysis 负责,Per-Call Analysis 不碰:
customerSummary— Contact Analysis 综合分析leadStatus,leadStatusReason— Contact Analysis AI 判断— Contact Analysis 规则计算,待移除(#32)leadTemperature,leadTemperatureReasonlifecycleStage,lifecycleState— Contact Analysis AI 判断purchaseIntent,goals,leadObjections,leadRejectionReasons— Contact Analysis AI 判断actionNeeded,actionNeededReason,suggestedActions— Contact Analysis AI 判断lastContactAnalysisAt— Contact Analysis 追踪字段
Step ③:不写 tasks
Per-Call Analysis 只看单通电话 + 24h 上下文,没有全量 context,不应创建 task。Task 创建由 Contact Analysis(综合分析后)或 lead-tracking(新 Lead SLA)负责。
Step ④:INSERT contact_timeline
Phase 2 (infra#683) 完成后:
transcribe-processor删除其call.createdwriter,留下ai-analysis这一行作为唯一call.created事件,occurredAt = call.startTime不变(用真实通话开始时间,不是 AI 完成时间)。
错误处理:在 db.batch() 内原子写入。失败 → 整个 batch 回滚 → SQS batchItemFailures 重试。
Step ⑤:SQS → Contact Analysis
每通电话无条件触发 Contact Analysis re-analysis。dailyBatchQueue 是 FIFO,SendMessageCommand 必须带 MessageGroupId 和 MessageDeduplicationId。
消息体字段:
MessageGroupId 用 ${phone}#${storeId},同一 contact 串行处理。MessageDeduplicationId 用 per_call_analysis-${phone}-${callId},不同通话不被合并。
Body schema 约定:消息体只携带 phone + storeId,不再传 franchiseId / accountId。Consumer(contacts-analyzer)从 contact 行通过 PK lookup 自行读取 franchiseId / accountId。
触发条件:contactPhone + storeId 都解析成功才触发。storeId 未解析时跳过本次触发,daily cron 兜底。SQS send 失败 log warn,不阻塞主流程。
V1 不做 Cooldown。uq_tasks_pending_contact_category DB index 保护数据完整性,duplicate task 在 DB 层拦截。