OpenRouter 仪表盘反查 Runbook
last-verified: 2026-07-14(对照 infra PR #1459)
Service: shared AI invoke (lambda/shared/utils/ai/invoke.ts) + OpenRouter dashboard
Last Updated: 2026-07-14
Owner: Platform Team
Related Docs: AI Analysis Runbook | Pipeline Monitoring
前置 PR: callytics-infrastructure #966(response-healing + usage telemetry)+ #967(generation ID + provider in log)
这个 runbook 解决什么问题
主场景:客户在 Studio (retaintive dashboard) 上看到一通有问题的 call — task 抓错、staff name 错、call_state 离谱、cost 异常。客户给你的信息通常只有电话号码 + 时间,没有 telephonySessionId、更没有 OpenRouter Generation ID。
需要从电话号码反向走 4 步追到 OpenRouter 那一次具体调用的完整 payload。
反例:如果只是想看整体成本 / 模型使用 → 直接打开 openrouter.ai/activity 看聚合就行,不用走这套反查。
4-Step 反查路径(~2 分钟)
Step 1: Studio dashboard 拿电话号码
客户报告:"5 月 20 号下午 1 点多有通 +1 (914) 265-4371 的电话,AI 分析说是 voicemail 但其实是真人对话。"
你需要从客户报告里提取:
Phone 格式规范化:Neon 里所有电话都是 E.164 格式(+19142654371,前面带 +1)。客户给的 (914) 265-4371 / 914-265-4371 / 19142654371 都要先转 E.164 再查。
Step 2: Neon calls 表查 telephony_session_id
Neon 表结构(相关字段)
来源:@retaintive/common 的 packages/common/src/db/schema/calls.ts。
⚠️ 没有
created_at列——本表用 RC 通话时间start_time做时间窗过滤。系统级时间戳是synced_at(首次同步)/updated_at(最后更新),不是通话时间,别拿来卡时间窗。
Query:按客户电话 + 时间窗
- 用
contact_phone字段最稳——不管接还是打,客户号码都在这一列 - 时间窗 ±2h 避免命中历史同号通话
- 如果同时段多通 → 让客户进一步描述("是早上还是下午?接的还是打的?")
如果不确定客户电话归在哪一列
也可以用 from / to 任一边匹配兜底:
Neon 怎么连
连接串在 SSM Parameter Store。Test 环境路径 / 凭据找 Platform team 拿,不要把 connection string commit 进 git。
Step 3: CloudWatch Insights 拿 OpenRouter Generation ID
选对 Log Group
{env} = test / pre / prod,{region} = us-west-2 / us-east-2 / us-east-1。实际名字以 AWS Console "Log groups" 列表为准。
Query:按 telephonySessionId 反查
正常情况返回 3 行(ai-analysis 3-stage pipeline: triage → classify → coaching)。每行有自己的 openrouterId。
ai_usage 字段对照表
⚠️ openrouterId 偶尔可能 undefined(SDK shape 变 / provider 异常 / log 抽取失败)。如果反查不到 → 用 fallback 流程(章节 当 openrouterId 缺失)。
Step 4: OpenRouter Activity Page
直接打开 URL
例:https://openrouter.ai/activity/gen-1779299823-8nd5Ci0U0mwlGRruPcXq
页面叫 Generation details,会显示:
没权限?
OpenRouter Activity 页要求 API key 所有者账号登录。没账号 → 联系 platform team 拿 read access,或让有权限的人代查。
备用 API(无 UI 也能查)
⚠️ API key 别 commit 进 git / 别贴进 chat。用完立刻清,shell history 也清。
完整示例:从客户报告到 OpenRouter
客户原话
"2026-05-20 下午 1:15 左右有通电话 (914) 265-4371,你们的 AI 分析说是 voicemail 但其实是真人在说话。"
Step 1 — normalize 电话
Step 2 — Neon 查
返回:
→ telephony_session_id = 's-abc123def456'
Step 3 — CloudWatch Insights
Log group: /aws/lambda/call-analytics-prod-ai-analysis-processor-us-east-1
返回 3 行(triage / classify / coaching):
Step 4 — OpenRouter
打开第一行(triage):https://openrouter.ai/activity/gen-1779299695-aBcDeFg
Generation details 页面看:
- 输入 transcript 是不是被截断了
- 模型实际返回的 raw JSON 是不是
{"call_state": "voicemail", ...} - finish_reason 是不是
stop
如果 raw JSON 就是 voicemail → 模型判断错了,prompt 问题或transcript 转录质量问题(去对 transcribe-processor 的输出)。
如果 raw JSON 是 human_conversation 但 Neon 存的是 voicemail → 下游 mapping bug(去查 ai-analysis-processor 的 post-process 逻辑)。
当 openrouterId 缺失
如果 CloudWatch ai_usage 行里 openrouterId 是 undefined(SDK shape 变 / provider 异常 / log 抽取失败),走 degraded path:
- 拿同一行的 4 元组
(timestamp, inputTokens, outputTokens, costUsd) - 打开 openrouter.ai/activity
- App filter 选对应 Lambda(
callytics-ai-analysis/callytics-contacts-analyzer) - 时间窗口卡到目标秒附近 ±10s
- 用 token 数 + cost 在 dashboard 行里手动对
⚠️ 同 store 同时段重复 transcript 可能撞数字。这是 degraded path,频繁触发说明 telemetry 出问题,给 platform team 报。
Dashboard 整体观察(不针对单 call)
定期巡检 / 看大盘时用,不是 per-call 反查。
打开
https://openrouter.ai/activity — 默认显示账号下所有 generation,时间倒序。
关键 filter
看什么
- Cached badge 出现率:DeepSeek implicit cache hit rate 应该 90%+(社区数据)。整列长期没 Cached badge → prompt prefix 飘了,去 grep
prompts.ts找${...}插值 - Provider 列分布:sticky routing 应该把同 prompt 路由到同 provider 最大化 cache。频繁切换 → 报告给 platform team
- Finish reason 异常:
length多 → maxTokens 设太小;content-filter多 → prompt 触发 moderation,检查 transcript 内容
6 个常见 debug 场景
场景 1:客户报告"某通分析全错"
照 4-step 流程走完。重点看 Generation details 的 raw response:
- raw response 跟 Neon 存的一致 → 模型判断错(prompt 或 transcript 质量问题)
- raw response 跟 Neon 存的不一致 → 下游 mapping bug
场景 2:某段时间 cost 异常高
拿 top costUsd 行的 openrouterId → Activity 页看:
inputTokens暴涨 → transcript 太长(去对 transcribe-processor 看是不是漏切)cacheReadRatio = 0→ cache miss(prompt 飘了 / cold start / sticky routing 没生效)
场景 3:cache 命中率长期是 0
连续多小时 avg_hit ≈ 0:
- 检查
lambda/ai-analysis-processor/src/core/stages/prompts.ts有没人加${...}插值 - 跑
cd lambda/ai-analysis-processor && npm test tests/unit/prompts-cache-invariant.test.ts,测试应该挂;如果没挂说明 prompt 改动绕过了 invariant test
场景 4:dashboard 显示 "Unknown" App
PR #967 已合并 + deploy 但 dashboard 还看到 Unknown → Lambda 还在跑旧版本:
场景 5:finishReason 是 length
输出被截断。Activity 页看 maxTokens 实际值,对比 Lambda 配置:
如果 transcript 极长且需要长输出 → 提 PR 改 maxTokens;如果偶发 → 看 input 是不是异常长的 transcript。
场景 6:provider 频繁切换 / fallback 多
Activity 页 Provider routing section 显示 fallback 链。某 primary provider 频繁 fallback:
- OpenRouter dashboard 看 Provider 列分布
- sticky routing 失效 → 联系 OpenRouter support(平台层)
- 短期 mitigation:在
lambda/shared/utils/ai/invoke.ts的createOpenRouter()加provider: { order: ['AtlasCloud'], allow_fallbacks: false }(会牺牲 availability,先讨论再做)
Identity 字段速查(哪个 ID 反查什么)
关键关系:
- 1 个 phone number 多对多 telephony_session_id(同号码可以打多次电话)
- 1 个 telephony_session_id 一对多 openrouterId(一通电话触发 3 次 AI call)
- 1 个 openrouterId 永远对应唯一 1 通
哪儿改 log 字段
只需要知道源在哪 / 想改 log 字段的人去看:
相关链接
- OpenRouter dashboard: https://openrouter.ai/activity
- OpenRouter API Reference: https://openrouter.ai/docs/api/reference/overview
- Prompt Caching 解释: https://openrouter.ai/docs/features/prompt-caching
- Usage Accounting 解释: https://openrouter.ai/docs/use-cases/usage-accounting
- AI Analysis Runbook: ai-analysis.md
- Pipeline Monitoring: pipeline-monitoring.md
- Reprocess AI Analysis SOP: ../reprocess-ai-analysis.md
- Neon
calls表 schema:@retaintive/common/src/db/schema/calls.ts
历史 / 设计决策
为什么不存 requestId? OpenRouter 里 Request ID(req-xxx)是开发用的内部 trace,operator 反查永远用 Generation ID(gen-xxx),所以我们只 log 后者。如果有一天发现需要 requestId → 改 lambda/shared/utils/ai/invoke.ts 的 logUsage() 加字段。
为什么 App 名是 callytics-ai-analysis 不是 callytics-ai-analysis-triage / -classify / -coaching? stage 信息走 CloudWatch(哪个 stage 在调用 logger 看 service 字段就知道),dashboard App 维度只到 Lambda 粒度,避免把 dashboard column 拆得过细。
为什么入口是电话号码不是 telephonySessionId? 客户视角永远从 Studio dashboard 开始,他们看不到 telephonySessionId(那是后端 ID)。Engineer 视角已知 telephonySessionId 时可以从 Step 2 开始跳过 Step 1。