跳转至

OpenRouter Dashboard 反查 Runbook

Service: shared AI invoke (lambda/shared/utils/ai/invoke.ts) + OpenRouter dashboard Last Updated: 2026-05-20 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 (retaintive dashboard)                        │
│ ─────────────────────────────────────────────────────────── │
│ 客户报告"这通分析不对" → 拿到 phone number + 大概时间          │
│ 例: +19142654371, 2026-05-20 13:15                          │
└────────────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Step 2: Neon `calls` 表查 telephony_session_id                │
│ ─────────────────────────────────────────────────────────── │
│ 用 from_phone_number / to_phone_number / contact_phone 查    │
│ → 拿到 telephony_session_id + created_at                     │
└────────────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Step 3: CloudWatch Logs Insights 拿 openrouterId             │
│ ─────────────────────────────────────────────────────────── │
│ filter @message like /ai_usage/                              │
│ filter telephonySessionId = "abc123"                         │
│ → 拿到 openrouterId = "gen-1779299823-8nd5Ci0U..."           │
│ (3-stage pipeline = 3 行 = 3 个 openrouterId)               │
└────────────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Step 4: OpenRouter Activity Page                             │
│ ─────────────────────────────────────────────────────────── │
│ openrouter.ai/activity/{openrouterId}                        │
│ → 完整 payload + provider routing + cache status + 计费     │
└──────────────────────────────────────────────────────────────┘

Step 1: Studio dashboard 拿电话号码

客户报告:"5 月 20 号下午 1 点多有通 +1 (914) 265-4371 的电话,AI 分析说是 voicemail 但其实是真人对话。"

你需要从客户报告里提取:

信息 用在哪 必须吗
Phone number Neon calls 表 phone 列查询 必须
大概时间 时间窗口缩小到 ±2h,避免历史撞号 强烈推荐
Store / Franchise 进一步过滤 可选
通话方向(接 / 打) 决定查 from_phone_number 还是 to_phone_number 可选

Phone 格式规范化:Neon 里所有电话都是 E.164 格式(+19142654371,前面带 +1)。客户给的 (914) 265-4371 / 914-265-4371 / 19142654371 都要先转 E.164 再查。

# 简单 normalize(去掉所有非数字 + 前置 +1)
echo "(914) 265-4371" | tr -cd '0-9' | sed 's/^/+1/'
# → +19142654371

Step 2: Neon calls 表查 telephonysessionid

Neon 表结构(相关字段)

字段 类型 含义
telephony_session_id text 反查需要的 ID(一通电话唯一)
from_phone_number text (E.164) 主叫号码
to_phone_number text (E.164) 被叫号码
contact_phone text (E.164) 客户号码(接 vs 打无关)
store_id uuid 店铺隔离键
franchise_id text 品牌
created_at timestamp 通话创建时间
s3_analysis_path text AI 分析结果 S3 路径(已分析非 null)

来源:@retaintive/common/src/db/schema/calls.ts

Query:按客户电话 + 时间窗

SELECT
  telephony_session_id,
  store_id,
  franchise_id,
  from_phone_number,
  to_phone_number,
  contact_phone,
  created_at,
  s3_analysis_path
FROM calls
WHERE contact_phone = '+19142654371'
  AND created_at BETWEEN '2026-05-20 12:00:00' AND '2026-05-20 15:00:00'
ORDER BY created_at DESC;
  • contact_phone 字段最稳——不管接还是打,客户号码都在这一列
  • 时间窗 ±2h 避免命中历史同号通话
  • 如果同时段多通 → 让客户进一步描述("是早上还是下午?接的还是打的?")

如果不确定客户电话归在哪一列

也可以用 from / to 任一边匹配兜底:

SELECT
  telephony_session_id,
  from_phone_number,
  to_phone_number,
  contact_phone,
  created_at
FROM calls
WHERE (from_phone_number = '+19142654371' OR to_phone_number = '+19142654371')
  AND created_at BETWEEN '2026-05-20 12:00:00' AND '2026-05-20 15:00:00'
ORDER BY created_at DESC;

Neon 怎么连

方式 适合谁
Neon Console SQL Editor (https://console.neon.tech) 偶尔查、不想搭客户端
psql (CLI) 重度查询、需要 export
项目 IDE 集成 (Drizzle Studio / DBeaver) 长期

连接串在 SSM Parameter Store。Test 环境路径 / 凭据找 Platform team 拿,不要把 connection string commit 进 git


Step 3: CloudWatch Insights 拿 OpenRouter Generation ID

选对 Log Group

Lambda Log Group 模式
ai-analysis-processor /aws/lambda/call-analytics-{env}-ai-analysis-processor-{region}
contacts-analyzer /aws/lambda/call-analytics-{env}-contacts-analyzer-{region}

{env} = test / pre / prod{region} = us-west-2 / us-east-2 / us-east-1。实际名字以 AWS Console "Log groups" 列表为准。

Query:按 telephonySessionId 反查

fields @timestamp, openrouterId, provider, model, inputTokens, outputTokens,
       cacheReadTokens, cacheReadRatio, costUsd, finishReason
| filter @message like /ai_usage/
| filter telephonySessionId = "abc123"
| sort @timestamp asc

正常情况返回 3 行(ai-analysis 3-stage pipeline: triage → classify → coaching)。每行有自己的 openrouterId

ai_usage 字段对照表

字段 类型 含义 SDK 路径
openrouterId string (可能 undefined) OpenRouter Generation ID,格式 gen-{timestamp}-{rand} result.response.id
provider string (可能 undefined) 上游 provider,如 "AtlasCloud" / "Alibaba Cloud Int" result.providerMetadata.openrouter.provider
model string 模型 ID,如 "deepseek/deepseek-v4-flash" 我们传的
inputTokens number 总 input token 数 result.usage.inputTokens
outputTokens number 总 output token 数 result.usage.outputTokens
cacheReadTokens number (可能 undefined) 命中 cache 的 input token result.usage.inputTokenDetails.cacheReadTokens
cacheReadRatio number (0-1) cacheReadTokens / inputTokens (3-decimal) invoke.ts 计算
costUsd number (可能 undefined) OpenRouter 已计费的金额 providerMetadata.openrouter.usage.cost
finishReason string "stop" / "length" / "content-filter" result.finishReason

⚠️ openrouterId 偶尔可能 undefined(SDK shape 变 / provider 异常 / log 抽取失败)。如果反查不到 → 用 fallback 流程(章节 当 openrouterId 缺失)。


Step 4: OpenRouter Activity Page

直接打开 URL

https://openrouter.ai/activity/{openrouterId}

例:https://openrouter.ai/activity/gen-1779299823-8nd5Ci0U0mwlGRruPcXq

页面叫 Generation details,会显示:

Section 内容
Header 模型 + provider + Cached badge(如果命中)
顶部 metrics Provider latency / Throughput tok/s / Cost USD / Tokens 拼接 / Fallbacks
Overview Model ID / Canonical ID / Data policy
Request Request ID(开发用)/ Generation ID(URL 里那个)/ Finish reason / Streaming
Provider routing 实际走的 provider 链 + fallback 历史
Tokens input/output details, cache breakdown
Generation params temperature / maxtokens / responseformat 等实际发送参数

没权限?

OpenRouter Activity 页要求 API key 所有者账号登录。没账号 → 联系 platform team 拿 read access,或让有权限的人代查。

备用 API(无 UI 也能查)

# 从 SSM 临时取 API key
OPENROUTER_API_KEY=$(aws ssm get-parameter \
  --name /callytics/openrouter-api-key \
  --with-decryption \
  --query 'Parameter.Value' \
  --output text)

# 查 generation
curl -s -H "Authorization: Bearer ${OPENROUTER_API_KEY}" \
  "https://openrouter.ai/api/v1/generation?id=gen-1779299823-8nd5Ci0U0mwlGRruPcXq" | jq .

# 用完清掉
unset OPENROUTER_API_KEY

⚠️ API key 别 commit 进 git / 别贴进 chat。用完立刻清,shell history 也清。


完整示例:从客户报告到 OpenRouter

客户原话

"2026-05-20 下午 1:15 左右有通电话 (914) 265-4371,你们的 AI 分析说是 voicemail 但其实是真人在说话。"

Step 1 — normalize 电话

(914) 265-4371 → +19142654371

Step 2 — Neon 查

SELECT telephony_session_id, store_id, created_at, s3_analysis_path
FROM calls
WHERE contact_phone = '+19142654371'
  AND created_at BETWEEN '2026-05-20 12:00:00' AND '2026-05-20 15:00:00'
ORDER BY created_at DESC;

返回:

telephony_session_id              | store_id                              | created_at          | s3_analysis_path
s-abc123def456                    | a1b2c3d4-...                          | 2026-05-20 13:14:52 | s3://...

telephony_session_id = 's-abc123def456'

Step 3 — CloudWatch Insights

Log group: /aws/lambda/call-analytics-prod-ai-analysis-processor-us-east-1

fields @timestamp, openrouterId, provider, model, cacheReadRatio, costUsd, finishReason
| filter @message like /ai_usage/
| filter telephonySessionId = "s-abc123def456"
| sort @timestamp asc

返回 3 行(triage / classify / coaching):

@timestamp           | openrouterId                          | provider    | cacheReadRatio | costUsd  | finishReason
2026-05-20 13:14:55  | gen-1779299695-aBcDeFg                | AtlasCloud  | 0.92          | 0.000122 | stop
2026-05-20 13:14:57  | gen-1779299697-hIjKlMn                | AtlasCloud  | 0.88          | 0.000774 | stop
2026-05-20 13:15:00  | gen-1779299700-oPqRsTu                | AtlasCloud  | 0.85          | 0.000639 | stop

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 行里 openrouterIdundefined(SDK shape 变 / provider 异常 / log 抽取失败),走 degraded path:

  1. 拿同一行的 4 元组 (timestamp, inputTokens, outputTokens, costUsd)
  2. 打开 openrouter.ai/activity
  3. App filter 选对应 Lambda(callytics-ai-analysis / callytics-contacts-analyzer
  4. 时间窗口卡到目标秒附近 ±10s
  5. 用 token 数 + cost 在 dashboard 行里手动对

⚠️ 同 store 同时段重复 transcript 可能撞数字。这是 degraded path,频繁触发说明 telemetry 出问题,给 platform team 报。


Dashboard 整体观察(不针对单 call)

定期巡检 / 看大盘时用,不是 per-call 反查。

打开

https://openrouter.ai/activity — 默认显示账号下所有 generation,时间倒序。

关键 filter

Filter 用法 用在哪
App callytics-ai-analysiscallytics-contacts-analyzer 分开看两个 Lambda 流量
Model deepseek/deepseek-v4-flash 跟其他模型流量混了的话隔离
Provider AtlasCloud / Alibaba Cloud Int 看 sticky routing 是否在工作
Time range 6h / 24h / 7d / 自定义 圈定调查窗口

看什么

  • 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 异常高

fields @timestamp, openrouterId, costUsd, cacheReadRatio, inputTokens
| filter @message like /ai_usage/
| filter @timestamp > now() - 2h
| sort costUsd desc
| limit 20

拿 top costUsd 行的 openrouterId → Activity 页看:

  • inputTokens 暴涨 → transcript 太长(去对 transcribe-processor 看是不是漏切)
  • cacheReadRatio = 0 → cache miss(prompt 飘了 / cold start / sticky routing 没生效)

场景 3:cache 命中率长期是 0

fields @timestamp, model, cacheReadRatio
| filter @message like /ai_usage/
| stats avg(cacheReadRatio) as avg_hit by bin(1h)

连续多小时 avg_hit ≈ 0

  1. 检查 lambda/ai-analysis-processor/src/core/stages/prompts.ts 有没人加 ${...} 插值
  2. 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 还在跑旧版本:

# 看 Lambda 当前部署的 commit
aws lambda get-function \
  --function-name call-analytics-test-ai-analysis-processor-us-west-2 \
  --query 'Configuration.CodeSha256'

# 强制重新部署
cd ~/workspace/callytics-infrastructure
npx cdk deploy CallAnalytics-Test-Lambda --require-approval never

场景 5:finishReason 是 length

输出被截断。Activity 页看 maxTokens 实际值,对比 Lambda 配置:

Lambda maxTokens 当前值 配置位置
ai-analysis-processor 4096 lambda/ai-analysis-processor/src/infrastructure/ai/invoke-model.ts
contacts-analyzer 4000 lambda/contacts-analyzer/src/infrastructure/ai-client.ts

如果 transcript 极长且需要长输出 → 提 PR 改 maxTokens;如果偶发 → 看 input 是不是异常长的 transcript。

场景 6:provider 频繁切换 / fallback 多

Activity 页 Provider routing section 显示 fallback 链。某 primary provider 频繁 fallback:

  1. OpenRouter dashboard 看 Provider 列分布
  2. sticky routing 失效 → 联系 OpenRouter support(平台层)
  3. 短期 mitigation:在 lambda/shared/utils/ai/invoke.tscreateOpenRouter()provider: { order: ['AtlasCloud'], allow_fallbacks: false }会牺牲 availability,先讨论再做)

Identity 字段速查(哪个 ID 反查什么)

ID 字段 哪儿能看到 反查什么
Phone number (E.164) Studio dashboard / Neon calls.from_phone_number, to_phone_number, contact_phone 客户视角入口
telephonySessionId Neon calls.telephony_session_id / DDB call-analysis PK / CloudWatch loggerContext 一通电话的完整 pipeline 痕迹
openrouterId (gen-xxx) CloudWatch ai_usage.openrouterId / OpenRouter dashboard "Generation ID" 列 OpenRouter 一次 AI call 的完整 payload + routing
requestId (req-xxx) OpenRouter dashboard "Request ID"(Activity 页) OpenRouter 内部 trace(开发用,operator 通常不用)
awsRequestId CloudWatch loggerContext.requestId / context.awsRequestId Lambda 一次 invocation 的所有 log

关键关系

  • 1 个 phone number 多对多 telephonysessionid(同号码可以打多次电话)
  • 1 个 telephonysessionid 一对多 openrouterId(一通电话触发 3 次 AI call)
  • 1 个 openrouterId 永远对应唯一 1 通

哪儿改 log 字段

只需要知道源在哪 / 想改 log 字段的人去看:

关注点 文件路径(callytics-infrastructure
Generation ID 提取 lambda/shared/utils/ai/invoke.tslogUsage() 函数
App 名传给 OpenRouter lambda/shared/utils/ai/invoke.tscreateOpenRouter({ appName, appUrl })
ai-analysis App 名 lambda/ai-analysis-processor/src/infrastructure/ai/invoke-model.ts const APP_NAME
contacts App 名 lambda/contacts-analyzer/src/infrastructure/ai-client.ts const APP_NAME
Cache invariant test lambda/ai-analysis-processor/tests/unit/prompts-cache-invariant.test.ts

相关链接

  • 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.tslogUsage() 加字段。

为什么 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。