Files
geo/docs/ai-brand-monitoring-tech-design-v5.md
T

87 KiB
Raw Blame History

AI 品牌曝光监测系统技术方案 V5

1. 文档信息

项目 内容
文档名称 AI 品牌曝光监测系统技术方案 V5(插件采样首期交付 / API 精确采集预留)
文档版本 V5.9
文档状态 待评审
创建日期 2026-04-09
基线文档 docs/ai-brand-monitoring-tech-design-v4.md
适用范围 数据追踪模块全部页面
关联文档 docs/geo-platform-prd-v1.mdPRD 8.4 数据追踪)
关联文档 docs/question-driven-monitoring-design-v1.md(问题驱动模型)
核心约束 单租户仅 1 个监测插件实例,插件所在设备不保证全天在线

2. V5 核心结论

V5 在架构上定义了两种采集模式,但首期只实现插件采集模式

  1. 插件采集模式(首期实现):免费,依赖本地插件与设备在线率,结果用于趋势参考。
  2. API 采集模式([V6] 预留):付费,走服务端统一 provider 调用,结果作为精确口径。
  3. 数据模型和协议层预留双模式扩展点,首期所有租户固定 collection_mode = plugin
  4. 页面百分比按实际采样成功样本计算,展示覆盖率和置信度。
  5. 引入 RabbitMQ 和独立 Monitoring PostgreSQL 隔离写链路,插件结果通过统一 ingest 队列处理。
  6. 数据详情页按“每日单快照”展示:用户可手动催发,同步存在自动调度,但同一自然日只保留一份展示快照。

2.1 相比 V4 的根本变化

维度 V4 V5(首期)
产品定位 全量品牌曝光监测 插件采样趋势参考(数据模型预留 API 精确模式)
采集目标 尽量覆盖全部问题 × 全平台 插件模式尽量覆盖,受预算和在线时长限制
插件节点假设 大量用户端节点可互补 单租户单主插件实例
数据时效 准实时 + T+1 T+1 为主,允许延迟
数据精度承诺 接近真实全量 趋势参考,精度受覆盖率影响
任务处理 PG + Redis 锁 + RabbitMQ Monitoring PG 任务表 + 插件租约 + RabbitMQ 结果异步化
存储边界 单业务库承载配置与监测结果 主业务 PG 存配置,Monitoring PG 存监测数据
页面表达 指标卡直接展示百分比 指标卡 + 采样覆盖信息 + 置信度提示

2.2 首期实施范围

V5.9 首期只实现插件采集模式。API 采集模式在数据模型和协议层预留扩展点,但不进入首期实施:

  1. 不实现 API provider adapter 和 API worker
  2. 不实现 API 模式三级限流
  3. 不实现 collection_mode 切换状态机和切换 UI
  4. 不对外暴露模式选择入口,所有租户固定 collection_mode = plugin
  5. 数据模型中的 collector_typecollection_mode 字段首期保留并写入 plugin,为后续扩展做好铺垫

文档中标注 [V6] 的段落为预留设计,首期跳过实现。

2.3 非目标

V5 明确不做以下承诺:

  1. 不承诺所有问题每天都有结果。
  2. 不承诺所有 AI 平台每天都有样本。
  3. 不承诺分钟级实时更新。
  4. 不承诺百分比可用于财务、法务、投放结算或精确 KPI 审计。

3. 产品口径与页面表达

3.1 页面定位

数据追踪页展示的是:

  • 在当前采集模式下的 AI 曝光结果
  • 在当前采集模式下的品牌表现
  • 在当前采集模式下的文章引用趋势

不是:

  • 品牌在全网 AI 场景中的绝对真实份额
  • 所有问题、所有平台、所有时间点的完整监控

3.2 页面必须新增的展示项

为避免误导,数据追踪页和 Dashboard composite 接口必须返回并展示:

  1. collection_modeplugin | api
  2. desired_sample_count:按全部启用问题推导出的目标采样数
  3. planned_sample_count:受预算截断后实际下发的计划采样数
  4. actual_sample_count:实际采样成功数
  5. coverage_rate:覆盖率 = actual / planned
  6. last_sampled_at:最近成功采样时间
  7. confidence_levelhigh / medium / low
  8. last_trigger_source:最近一次成功更新来自 manual | automatic

推荐前端呈现方式:

  • 顶部右侧信息条:首期展示 插件采样API 精确采集[V6] 预留
  • 插件模式副文案:目标 60,计划 30,已采样 18,覆盖率 60%,更新于 04-08 09:20 · 自动
  • [V6] API 模式副文案:已按 API 精确模式执行,更新于 04-08 09:20
  • 仅插件模式下展示 样本不足,仅供参考
  • 品牌时序区首期使用离散采样时序视图,不使用连续折线图
  • 数据详情页提供 立即采集 入口,但文案必须明确:同一自然日只保留最新一份有效快照

3.3 低覆盖场景的展示规则

覆盖率 置信度 页面表现
>= 70% high 正常展示百分比和趋势
40% - 69% medium 展示百分比,并提示“样本偏少”
< 40% low 保留原始计数,百分比弱化展示或标记“仅供参考”

collection_mode = pluginactual_sample_count < 5 时:

  1. 不展示“精确到整数”的百分比变化解释。
  2. 不使用连续趋势线,改为“快照脉冲条 + 当期聚合指标”。
  3. 引用排行允许为空,不用补零。
  4. 当日尚无有效快照时,页面显示“等待下一次有效快照”,而不是大面积重复展示“未采样”字样。

3.4 数据详情页首屏设计

首期数据详情页采用**「结果驱动 + 按需探索」**信息层次,主视觉聚焦品牌最新表现,而非采样过程:

  1. 第一层:顶部筛选 + 操作区
    • 筛选器:品牌、关键词
    • 信息条:插件采样 · 最近更新: 04-08 09:20 · 自动
    • 操作区:立即采集 按钮
    • 模型筛选器仅影响指标卡数值,不影响平台对比矩阵
  2. 第二层左:最新快照概览(约 60% 宽度)
    • 4 个指标卡(2x2 网格):提及率、首位提及率、首选推荐率、正面提及率
    • 每个卡片内容:指标名 + 大号百分比数字 + 趋势箭头(对比上一份有效快照)+ mini sparkline 离散点(最近 N 个有效快照点位,不连线)
    • 当窗口内有效快照 < 2 时,不展示趋势箭头,只展示当前值
    • 当窗口内无有效快照时,指标卡显示 --mini sparkline 区域留白
  3. 第二层右:平台对比矩阵(约 40% 宽度)
    • 紧凑表格,列头:平台 | 提及率 | 首位提及 | 状态
    • 始终展示全部已接入平台,不受模型筛选器影响
    • 状态值:已采样 / 等待采样 / 未登录 / 不可用
    • 已采样平台展示指标数值;未采样或未登录平台对应指标显示 --
    • 点击某平台行可展开该平台的详细采样记录(tooltip 或抽屉)
  4. 第三层左:采样覆盖条(约 60% 宽度)
    • 水平进度条:展示覆盖率百分比
    • 文案:目标 30 · 已采样 21 · 覆盖率 70%
    • 日指示点行:最近 N 天每天一个圆点,实心点 = 有效快照日,空心点 = 无数据日(类 GitHub 贡献图极简版)
    • 日指示点悬浮时 tooltip 展示该日 sample_status / trigger_source / actual_sample_count
  5. 第三层右:高频问题(约 40% 宽度,可搜索)
    • 顶部搜索框,搜索范围为当前所选关键词下的全部问题
    • 问题列表:按采样成功次数排序,每行展示问题文案 + 采样次数 + 提及率
    • 每个问题行右侧展示展开图标,点击后弹出问题详情面板(见 3.5 节)
    • 列表底部提供链接跳转到品牌词库问题管理,方便用户提报新问题
  6. 第四层:底部明细区
    • 引用排行榜(保留原设计):饼图 + 模型平台引用表格(模型平台 | 引用文章数 | 引用率)
    • 被引用文章列表

设计原则:

  1. 主视觉传达品牌在各 AI 平台的最新表现,而非采样过程是否完整。
  2. 采样覆盖信息降维为第三层的辅助进度条,不喧宾夺主。
  3. 未采样日不以 0 值或错误提示呈现,而是日指示点留空 + tooltip 按需解释。
  4. sample_status 只作为日指示点 tooltip 和 API 字段,不作为逐日主文案。
  5. 平台对比矩阵是品牌监测最有价值的洞察维度,独立成区常驻展示。
  6. 高频问题支持搜索和问题提报闭环,实现问题驱动的监控体验。

3.5 高频问题展开详情面板设计

每个高频问题均可点击展开,展示该问题在各 AI 平台的完整回答及引用信息。详情面板分为以下四个区域:

3.5.1 答案内容区

  • 位置:面板左上主区域(约 60% 宽度)
  • 内容:展示当前所选 AI 平台对该问题的完整回答原文(支持 Markdown 渲染)
  • 平台切换器:面板右上角展示平台切换按钮组(如 DeepSeek / 通义千问 / 豆包),支持左右箭头翻页或 Tab 点击
    • 切换时答案内容区实时更新为对应平台的回答原文
    • 若某平台当日未采样或登录态失效,对应 Tab 显示为灰色,回答区展示 暂无数据
    • 默认展示最近采样成功的平台回答
  • 回答元信息:回答区底部展示:平台名称 · 采样时间 · 模型版本(如有)
  • 品牌高亮:回答中涉及当前监测品牌的词汇自动高亮显示

3.5.2 引用来源区

  • 位置:面板右侧列表区(约 40% 宽度,与答案内容区并排)
  • 内容:展示当前所选平台在该问题回答中引用的外部链接列表
  • 每条引用展示:
    • 来源站点图标(favicon)+ 来源站点名称(registrable_domain 映射后的 site_name
    • 引用 URL 标题(截断展示,完整 title 可 hover 查看)
    • 引用 URL 链接(可点击跳转)
    • 是否为本品牌内部文章(标记 内容引用 徽标)
  • 空状态:若当前平台回答无可解析引用,展示 该平台无引用来源
  • 引用来源区随平台切换联动更新

3.5.3 引用分析区

  • 位置:面板下半部分左侧(答案内容区下方)
  • 内容:以表格形式汇总该问题在各平台的引用来源分布
  • 表格列来源平台 | 引用次数 | 引用站点数 | 是否含品牌内容
  • 表格行:每行对应一个已有引用记录的 AI 平台(按引用次数降序)
  • 说明:该分析基于当前时间窗口内所有采样成功记录中该问题的全部引用事实聚合,而不仅限于当日单次回答
  • 若当日仅有一次采样,引用分析等同于单次引用列表;若多次采样(手动 + 自动),则聚合所有采样的引用事实

3.5.4 内容引用区

  • 位置:面板下半部分右侧(引用来源区下方)
  • 内容:展示在该问题回答中被 AI 引用的品牌内部文章列表
  • 每条内容引用展示:
    • 文章标题(可点击进入文章管理页面)
    • 发布平台名称
    • 被引用次数(当前时间窗口内)
    • 引用率(该文章引用次数 / 该问题采样成功次数
  • 空状态:若无内部文章被引用,展示 暂无内容引用
  • 内容引用区随平台切换联动更新(仅展示当前所选平台的内容引用记录)

3.5.5 面板交互规则

  1. 点击问题行展开详情面板,再次点击或点击面板外区域收起
  2. 同一时刻只有一个问题详情面板处于展开状态
  3. 面板展开时,问题列表区域内容保持可见和可滚动
  4. 平台切换器 Tab 状态在面板展开期间持久化,切换问题时重置为默认平台
  5. 详情面板内数据加载失败时展示错误提示,不影响外层问题列表展示

4. 采样配额与容量模型

4.1 套餐定义

V5.9 架构支持双采集模式,但首期只开放插件模式

模式 计费 数据特征 适用场景 首期状态
plugin 免费 依赖插件在线率,结果可能不稳定 趋势参考 首期实现
api 付费 服务端统一执行,结果更稳定更准确 精确监测 [V6] 预留

首期规则:

  1. 所有租户固定 collection_mode = plugin,不提供切换入口
  2. 数据模型保留 collection_mode / collector_type 字段,查询始终带模式过滤条件
  3. 后续引入 API 模式时,只需实现 adapter + worker,不需要迁移历史数据
配额项 Free Pro Enterprise
最大品牌数 1 3 自定义
题库总量 不限于监测能力,只做配置资产 不限于监测能力,只做配置资产 自定义
问题覆盖策略 尽量覆盖全部启用问题 尽量覆盖全部启用问题 自定义
采集频率 每天 每天 每天 / 自定义
平台范围 当前已接入且满足访问条件的平台 当前已接入且满足访问条件的平台 同左
采集模式 plugin(首期唯一) plugin(首期唯一) pluginapi [V6]
数据承诺 趋势参考 趋势参考 趋势参考;API 模式精确口径 [V6]

4.2 问题执行策略

系统不再限制“每日活跃问题数”,而是采用 尽量覆盖全部启用问题 的策略。

实现原则:

  1. brand_questions 保留全部问题题库
  2. 只要问题为 monitor_enabled = true,就进入当日候选集合
  3. 调度器会尽量让每个品牌的全部启用问题去各个模型提问
  4. 若当日预算、在线时长或平台可访问性不足,则按优先级做 best-effort 截断

这意味着:

  1. 约束从“问题数限制”转成“执行预算限制”
  2. 对用户的承诺是“尽量多覆盖问题”,不是“每天只跑固定几题”
  3. 页面通过 desired_sample_count / planned_sample_count / actual_sample_count 解释覆盖差异

4.3 单实例前提下的目标任务量

按“每个品牌尽量覆盖全部启用问题 × 全部可访问平台”的原则,理论任务量为:

desired_sample_count =
  启用品牌数
  × 每品牌启用问题数
  × 当日可访问平台数

但实际下发任务量受 task_daily_hard_cap 和设备在线约束影响:

planned_sample_count = min(desired_sample_count, task_daily_hard_cap, runtime_accessible_capacity)

说明:

  1. desired_sample_count 代表理想覆盖量。
  2. planned_sample_count 才是当天真正发出的任务量。
  3. 实际完成量取决于:
    • 主监测插件是否在线
    • 插件机器当日在线时长
    • 平台是否要求登录,以及对应登录态是否可用
    • 平台风控和采集成功率

4.4 核心调度原则

V5 的调度器不追求“补齐所有漏采任务”,而追求“稳定输出可比较样本”。

因此:

  1. 每天只生成当天计划任务,不补历史大积压。
  2. 在线就采集,不在线就放弃;当日未执行任务不顺延、不补跑。
  3. 超过当天统计窗口的未执行任务直接过期,不回补到新一天的分母中。
  4. 读侧按 planned_sample_countactual_sample_count 共同解释结果。

4.5 主插件在线约束

单租户只指定 1 个 primary_monitoring_installation 参与自动监测。

调度前先判断该插件是否活跃:

  • last_seen_at <= 24h:生成当天任务
  • last_seen_at > 24h:不生成新任务,页面沿用最近一次成功数据并提示“近期无新采样”

其中:

  • last_seen_at 由插件心跳接口持续刷新
  • 插件心跳除刷新安装实例活跃时间外,还同步上报监测平台 detect() 结果,供 monitoring_platform_access_snapshots 更新

这条规则的目的:

  1. 防止设备长期离线时任务无限堆积
  2. 防止“未执行任务”污染覆盖率统计
  3. 让系统行为和单实例现实约束一致

这意味着:

  1. 系统不是“补数型监控”,而是“机会型采样”
  2. 用户离线期间缺失的数据天然放弃
  3. 趋势连续性依赖设备在线率,而不是依赖补采机制

5. 数据口径定义

5.1 顶部指标口径

所有百分比的分母统一为 实际采样成功回答数,不是计划任务数,也不是题库总数。

前提:

  1. 所有口径都必须在同一 collection_mode 下计算
  2. 插件模式和 API 模式的数据禁止在同一指标中混算
指标 定义
提及率 mentioned_count / actual_sample_count
首位提及率 top1_mentioned_count / actual_sample_count
首选推荐率 first_recommended_count / actual_sample_count
正面提及率 positive_mentioned_count / actual_sample_count
品牌可见率 与提及率同源,可作为 UI 别名展示
文章引用率 cited_answer_count / actual_sample_count

5.2 品牌时序视图口径

首期 plugin 模式下,品牌数据不适合使用连续折线图,因为插件不保证 24h 在线,日样本天然可能中断。

V5.9 将时序展示从独立主图区降维为两个辅助元素:

  1. 指标卡内 mini sparkline:在最新快照概览的每个指标卡右侧展示离散点,每个点代表一个有效快照日的 metric_value,点之间不连线
  2. 采样覆盖条内日指示点行:最近 N 天每天一个圆点,实心 = 有效快照日,空心 = 无数据日

数据层仍沿用完整时间桶结构,每个时间桶必须同时具备:

  • date
  • sample_statussampled | partial | unsampled
  • metric_value
  • planned_sample_count
  • actual_sample_count
  • coverage_rate
  • trigger_sourcemanual | automatic | null
  • snapshot_updated_at

展示规则:

  1. sample_status = unsampled 时,metric_value = nullmini sparkline 跳过该日(不画点),日指示行渲染为空心点
  2. sample_status = partial 时,mini sparkline 画半透明点,日指示行渲染为半实心点
  3. sample_status 只作为日指示点 tooltip 的解释依据,不作为任何逐日直出标签
  4. 只有 API 精确模式 [V6] 才可考虑恢复独立连续折线图
  5. 同一 business_date 只展示一个日快照;手动催发成功后刷新当日点位,不追加第二个点

首期推荐:

  1. 默认展示最近 7/14/30 天的时间桶
  2. mini sparkline 最多展示最近 7 个有效快照点,超出部分截断
  3. 当窗口内 sampled + partial < 2 时,mini sparkline 不展示,指标卡只展示当前值不展示趋势箭头

5.2A 平台对比矩阵口径

平台对比矩阵为 V5.9 新增的展示区域,用于直观对比各 AI 平台的品牌表现差异。

数据来源:

  1. (tenant_id, brand_id, business_date, ai_platform_id) 维度从 monitoring_*_daily 汇总
  2. 始终展示全部已接入平台,不受页面模型筛选器影响
  3. 各平台独立计算 mention_rate / top1_mention_rate / actual_sample_count

状态判定规则:

条件 platform_sample_status 说明
actual_sample_count >= 1 sampled 已有有效样本
平台在计划内但尚未采样 pending 等待采样
平台 auth_requirement = login_required 且未检测到登录态 not_logged_in 未登录
平台 adapter 不可用 unavailable 不可用

展示规则:

  1. sampled 状态展示具体指标数值
  2. 其他状态指标列显示 --
  3. 平台行默认按 actual_sample_count 降序排列
  4. 悬浮或点击平台行可查看该平台最近采样详情

5.3 高频问题口径

高频问题区不再解释为"全站高频触发问题",而是:

在当前时间窗口内,被实际采样成功次数最多的问题

问题列表展示字段:

  • 问题文案
  • 成功采样次数
  • 提及率
  • 最近采样时间

问题详情面板数据口径:

问题详情面板通过独立接口(见 12.1A 节)按需加载,数据口径如下:

  1. 答案内容:按 ai_platform_id 分组,取最近一次 status = succeededraw_answer_text;随平台切换按需请求,不在首屏预加载全部平台回答
  2. 引用来源:从 monitoring_citation_facts(run_id, ai_platform_id) 聚合,每条引用对应 cited_url / site_name / article_id;随平台切换联动更新
  3. 引用分析:按 ai_platform_id 聚合,跨平台汇总展示各平台 citation_count / distinct_site_count / has_brand_article_citation;不受平台切换过滤影响,始终展示全平台汇总
  4. 内容引用:关联 monitoring_article_url_aliases,过滤 article_id IS NOT NULL,聚合 article_citation_countarticle_citation_rate = article_citation_count / question_sample_count;随平台切换联动

口径边界:

  • 引用分析和内容引用的聚合时间窗口与外层问题列表保持一致(默认当日快照)
  • 若当日有多次采样(手动 + 自动),则聚合全部成功运行的引用事实
  • 平台切换仅影响「答案内容」和「引用来源」;「引用分析」默认展示所有平台的聚合不随切换变化
  • 「内容引用」随平台切换联动,仅展示当前所选平台中归因成功的内部文章记录

5.4 引用排行口径

引用排行按模型平台展示:

  • 有过可归因引用的回答数
  • 引用文章数
  • 引用率

若平台当日无样本,或平台要求登录但当前未登录:

  • 在 tooltip 或侧边说明中展示为 暂无快照
  • 不强行按 0 并入分母

6. 数据模型修订

6.1 双 PostgreSQL 存储边界

V5.9 将数据按“业务配置”和“监测结果”拆到两个 PostgreSQL:

主业务 PostgreSQL

  • tenants
  • brands
  • brand_keywords
  • brand_questions
  • articles
  • publish_records
  • plugin_installations
  • tenant_monitoring_quotas
  • tenant_collection_mode_switches [V6]
  • monitoring_config_outbox

Monitoring PostgreSQL

  • monitoring_collect_tasks
  • monitoring_question_config_snapshots
  • monitoring_platform_access_snapshots
  • question_monitor_runs
  • question_monitor_parse_results
  • monitoring_citation_facts
  • monitoring_article_url_aliases
  • site_domain_mappings
  • monitoring_*_daily

设计原则:

  1. 不做跨库物理外键,只保留逻辑 ID 引用。
  2. 监测链路的高频写入、重试、归档不影响主业务 PG。
  3. 读侧监测查询优先从 Monitoring PG 获取。
  4. 当前代码库只有单数据库配置与单套 sqlc 产物,V5.9 需新增 monitoring_database 配置、第二个 pgxpool.Pool、第二套 sqlc query/codegen。

6.2 question_version_id 移除后的替代方案

V5 接受原系统不再维护 question_version_id,但仍然保留“输入快照不可变”原则。

替代方案:

  1. 任务生成时在任务表写入 question_hash
  2. 运行结果入库时在运行表写入 question_text_snapshot
  3. 历史汇总按 question_hash 区分口径,必要时可回查 question_text_snapshot

这样即使 question_id 不变、文案发生修改,历史数据也不会被新文案污染。

实现建议:

  1. 文档中继续使用 question_hash 表达语义
  2. 实际落库使用定长二进制指纹或等价短哈希,而不是 VARCHAR(64)
  3. 任务表负责调度,尽量不承载大文本;question_text_snapshot 只保留在运行表
  4. 任务执行时需要的题目文案,由服务端按 (tenant_id, question_id, question_hash)monitoring_question_config_snapshots 读取并组装到执行请求
  5. Go 层建议封装 QuestionHash 值对象,统一存储层 []byte 与 API 层字符串之间的编解码,避免 sqlc 写入点散落转换逻辑
  6. 目的是保留版本语义,同时控制复合索引体积和任务表膨胀
  7. question_hash 统一建议使用 SHA-25616 字节截断(128bit)的定长二进制指纹;在 question_id 已参与主键约束的前提下,碰撞概率可忽略
  8. 存储层落为 Monitoring PG 的 BYTEA(16) 等价二进制指纹;API / JSON / MQ envelope 的 wire format 统一为 v1:<lowercase-hex>,其中 v1 表示当前哈希算法与编码版本

6.3 tenant_monitoring_quotas

CREATE TABLE tenant_monitoring_quotas (
    tenant_id                    BIGINT PRIMARY KEY REFERENCES tenants(id),
    max_brands                   INT NOT NULL DEFAULT 1,
    collection_mode             VARCHAR(20) NOT NULL DEFAULT 'plugin',
    question_selection_mode      VARCHAR(20) NOT NULL DEFAULT 'best_effort_all',
    collect_frequency            VARCHAR(20) NOT NULL DEFAULT 'daily',
    enabled_platforms            JSONB NOT NULL DEFAULT '["deepseek","qwen","doubao"]',
    primary_installation_id      BIGINT REFERENCES plugin_installations(id),
    task_daily_hard_cap          INT NOT NULL DEFAULT 36,
    plan_tier                    VARCHAR(20) NOT NULL DEFAULT 'free',
    created_at                   TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at                   TIMESTAMPTZ NOT NULL DEFAULT now()
);

建议默认值:

  • Freecollection_mode = pluginquestion_selection_mode = best_effort_alltask_daily_hard_cap = 36
  • Procollection_mode = pluginquestion_selection_mode = best_effort_alltask_daily_hard_cap = 72

说明:

  1. tenant_monitoring_quotas 放在主业务 PostgreSQL。
  2. primary_installation_id 继续引用主业务库中的 plugin_installations
  3. collection_mode 首期固定写入 plugin,字段保留是为了后续 API 模式 [V6] 扩展时无需迁移。
  4. question_selection_mode 当前默认 best_effort_all,预留模式位。

6.4 monitoring_collect_tasks

V5.9 使用 Monitoring PG 单表状态机进行任务租约,并用 RabbitMQ 解耦结果写入。

CREATE TABLE monitoring_collect_tasks (
    id                      BIGSERIAL PRIMARY KEY,
    tenant_id               BIGINT NOT NULL,
    brand_id                BIGINT NOT NULL,
    question_id             BIGINT NOT NULL,
    question_hash           BYTEA NOT NULL,
    ai_platform_id          VARCHAR(30) NOT NULL,
    collector_type          VARCHAR(20) NOT NULL DEFAULT 'plugin',
    trigger_source          VARCHAR(20) NOT NULL DEFAULT 'automatic',
    run_mode                VARCHAR(30) NOT NULL DEFAULT 'plugin_standard',
    business_date           DATE NOT NULL,
    planned_at              TIMESTAMPTZ NOT NULL DEFAULT now(),
    status                  VARCHAR(20) NOT NULL DEFAULT 'pending',
    lease_token_hash        VARCHAR(128),
    leased_to_executor      VARCHAR(100),
    leased_at               TIMESTAMPTZ,
    lease_expires_at        TIMESTAMPTZ,
    callback_received_at    TIMESTAMPTZ,
    completed_at            TIMESTAMPTZ,
    skip_reason             VARCHAR(50),
    request_payload_json    JSONB,
    error_message           TEXT,
    created_at              TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at              TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uk_collect_task_idempotent
        UNIQUE (tenant_id, brand_id, question_id, ai_platform_id, collector_type, run_mode, business_date)
);

CREATE INDEX idx_collect_tasks_lease
    ON monitoring_collect_tasks(tenant_id, status, business_date, lease_expires_at);

状态流转:

  • pending
  • leased
  • received
  • completed
  • skipped
  • expired
  • failed

补充说明:

  1. trigger_source 表示该任务最终由“自动调度”还是“用户手动催发”进入执行链路。
  2. 首期不为手动采集创建第二套同日任务;手动采集的本质是把当日尚未完成的任务提权/刷新为 manual 来源。
  3. 因为同日同题同平台仍只有一条任务记录,所以页面天然只会生成一个日快照点位。
  4. 同一 business_date 内若问题文案发生修改,已生成任务不重新分裂出第二条同日任务;任务继续使用生成时锁定的 question_hash

说明:

  1. 任务租约状态在 Monitoring PG 中维护。
  2. received 表示执行通道已把结果成功提交到 ingest 队列,但 worker 尚未完成最终入库。
  3. collector_type 首期固定为 plugin,字段保留为 [V6] API 模式扩展预留。
  4. leased_to_executor 首期填入插件 installation ID;后续 API 模式 [V6] 可填入 API worker 标识。
  5. question_hash 仍然落库,用于锁定该任务对应的题目版本和后续运行快照,但不再参与同日任务幂等键
  6. 问题文案修改从下一个 business_date 开始生效;不追溯重写当日已创建任务

6.5 monitoring_question_config_snapshots

该表属于 Monitoring PG 中的配置快照表,职责是为任务生成、租约接口和 API 执行器提供“只读且不可变”的问题版本视图。

CREATE TABLE monitoring_question_config_snapshots (
    id                     BIGSERIAL PRIMARY KEY,
    tenant_id              BIGINT NOT NULL,
    brand_id               BIGINT NOT NULL,
    question_id            BIGINT NOT NULL,
    keyword_id             BIGINT,
    question_hash          BYTEA NOT NULL,
    question_text_snapshot TEXT NOT NULL,
    monitor_enabled        BOOLEAN NOT NULL DEFAULT true,
    superseded_at          TIMESTAMPTZ,
    deleted_at             TIMESTAMPTZ,
    projected_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
    created_at             TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at             TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uk_monitoring_question_snapshot
        UNIQUE (tenant_id, question_id, question_hash)
);

CREATE INDEX idx_monitoring_question_snapshot_active
ON monitoring_question_config_snapshots(tenant_id, brand_id, monitor_enabled, deleted_at, superseded_at);

约束:

  1. 调度器生成任务只读取 monitor_enabled = truedeleted_at IS NULLsuperseded_at IS NULL当前版本快照记录。
  2. 租约接口和 API 执行器装配题目文案时,只读取这张快照表,不回源主业务 PG。
  3. 问题文案变更时,projector 先 INSERTquestion_hash 快照,再将旧当前版本标记 superseded_at = now();不覆盖历史版本。
  4. brand_question.updated 只负责“新版本追加 + 旧当前版本 supersede”,不会把历史快照物理删除。
  5. brand_question.disabled / brand.deleted 负责把当前快照标记 deleted_at,而不是复用 superseded_at 语义。

6.5A monitoring_platform_access_snapshots

该表用于沉淀主监测插件最近一次探测到的“平台可访问性”快照,作为调度器与读侧共享的状态源,避免页面根据“有没有成功运行记录”反推 未登录 / 不可用

CREATE TABLE monitoring_platform_access_snapshots (
    id                  BIGSERIAL PRIMARY KEY,
    tenant_id           BIGINT NOT NULL,
    installation_id     BIGINT NOT NULL,
    ai_platform_id      VARCHAR(30) NOT NULL,
    business_date       DATE NOT NULL,
    access_status       VARCHAR(20) NOT NULL,
    connected           BOOLEAN NOT NULL DEFAULT false,
    accessible          BOOLEAN NOT NULL DEFAULT false,
    detected_at         TIMESTAMPTZ NOT NULL,
    reason_code         VARCHAR(50),
    reason_message      TEXT,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uk_platform_access_snapshot
        UNIQUE (tenant_id, installation_id, ai_platform_id, business_date)
);

CREATE INDEX idx_platform_access_snapshot_lookup
ON monitoring_platform_access_snapshots(tenant_id, business_date, ai_platform_id, access_status);

约束:

  1. access_status 推荐值:accessible | not_logged_in | unavailable
  2. 数据语义与插件当前 detect() 返回的 connected 状态保持一致,并扩展出调度可用的 accessible
  3. 调度器只为 access_status = accessible 的平台生成当天任务
  4. 读侧平台矩阵和问题详情面板使用这张表 + monitoring_collect_tasks + question_monitor_runs 共同渲染 sampled / pending / not_logged_in / unavailable
  5. 主监测插件每次心跳(默认 15 分钟)都应同时上报各 AI 平台 detect() 结果,服务端按 (tenant_id, installation_id, ai_platform_id, business_date) 执行 UPSERT
  6. business_date 的第一条心跳会创建当日快照;在此之前,00:10 调度器允许回退读取最近 24h 内的上一份可访问快照做机会型计划
  7. 00:10 后同日首次心跳把平台状态更新为 not_logged_in / unavailable,服务端需立即对该平台当日仍为 pending 的任务执行 reconcile,转为 skipped 并写入 skip_reason = platform_not_accessible
  8. 对已进入 leased 的任务不做强制回收;允许其继续执行、自然完成或按过期策略回收

6.6 question_monitor_runs

首期直接定义为独立结果表,不采用“先建空表再 ALTER”的增量方式。

CREATE TABLE question_monitor_runs (
    id                   BIGSERIAL PRIMARY KEY,
    tenant_id            BIGINT NOT NULL,
    brand_id             BIGINT NOT NULL,
    question_id          BIGINT NOT NULL,
    question_text_snapshot TEXT NOT NULL,
    question_hash        BYTEA NOT NULL,
    ai_platform_id       VARCHAR(30) NOT NULL,
    collector_type       VARCHAR(20) NOT NULL,
    trigger_source       VARCHAR(20) NOT NULL DEFAULT 'automatic',
    run_mode             VARCHAR(30) NOT NULL DEFAULT 'plugin_standard',
    business_date        DATE NOT NULL,
    provider_model       VARCHAR(100),
    provider_request_id  VARCHAR(100),
    request_id           VARCHAR(100),
    raw_answer_text      TEXT,
    raw_answer_storage_key TEXT,
    raw_response_json    JSONB,
    status               VARCHAR(20) NOT NULL DEFAULT 'succeeded',
    completed_at         TIMESTAMPTZ,
    created_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at           TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uk_monitor_run_idempotent
        UNIQUE (tenant_id, brand_id, question_id, ai_platform_id, collector_type, run_mode, business_date)
);

CREATE INDEX idx_monitor_runs_query
ON question_monitor_runs(tenant_id, business_date, ai_platform_id, brand_id);

建议:

  1. question_monitor_runs 落在 Monitoring PostgreSQL。
  2. 若原始回答文本不超过阈值,可直接存库;超大原文再转对象存储。
  3. trigger_source 用于读侧展示“最近一次有效快照来自手动还是自动”,并支持 tooltip 解释层。
  4. collector_typeprovider_request_id 为插件/API 二选一模式打通统一入库协议。
  5. 所有读侧查询必须显式按 collector_type 或等价 collection_mode 过滤,禁止混算。
  6. 运行表沿用任务创建时锁定的 question_hash,用于保证回答口径与当日任务快照一致
  7. 运行表唯一键故意与任务表幂等键保持一致,保证同日同题同平台只有一条运行记录,不因当日文案修改而分裂出第二条结果

6.6A question_monitor_parse_results

该表为 question_monitor_runs 的一对一解析结果表,用于承接 3.4 / 3.5 页面所需的回答层语义字段,并作为日聚合的事实来源之一。

CREATE TABLE question_monitor_parse_results (
    id                     BIGSERIAL PRIMARY KEY,
    tenant_id              BIGINT NOT NULL,
    run_id                 BIGINT NOT NULL,
    brand_id               BIGINT NOT NULL,
    question_id            BIGINT NOT NULL,
    question_hash          BYTEA NOT NULL,
    ai_platform_id         VARCHAR(30) NOT NULL,
    business_date          DATE NOT NULL,
    brand_mentioned        BOOLEAN,
    brand_mention_position VARCHAR(20),
    first_recommended      BOOLEAN,
    sentiment_label        VARCHAR(20),
    matched_brand_terms    JSONB,
    parse_version          VARCHAR(40) NOT NULL DEFAULT 'v1',
    parse_status           VARCHAR(20) NOT NULL DEFAULT 'succeeded',
    created_at             TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at             TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uk_question_parse_run
        UNIQUE (tenant_id, run_id)
);

CREATE INDEX idx_question_parse_lookup
ON question_monitor_parse_results(tenant_id, business_date, brand_id, question_id, ai_platform_id);

字段约定:

  1. brand_mention_positiontop1 | mentioned | not_mentioned | null
  2. sentiment_labelpositive | neutral | negative | unknown
  3. matched_brand_terms 用于回答区品牌高亮;若为空,前端可退化为关键词字符串高亮
  4. 日级 mention_rate / top1_mention_rate / first_recommend_rate / positive_mention_rate 均由本表聚合得出

6.7 文章 URL 归因表

为支撑“总引用数 / 文章引用率 / 引用文章数”,新增 URL 归因别名表:

CREATE TABLE monitoring_article_url_aliases (
    id                  BIGSERIAL PRIMARY KEY,
    tenant_id           BIGINT NOT NULL,
    article_id          BIGINT NOT NULL,
    publish_record_id   BIGINT,
    platform_id         VARCHAR(64),
    publish_platform_name_snapshot VARCHAR(100),
    external_article_id VARCHAR(128),
    article_title_snapshot TEXT,
    original_url        TEXT NOT NULL,
    normalized_url      TEXT NOT NULL,
    last_path_segment   VARCHAR(255),
    confidence          VARCHAR(20) NOT NULL DEFAULT 'high',
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX uk_article_url_alias
ON monitoring_article_url_aliases(tenant_id, normalized_url);

补充说明:

  1. article_title_snapshot / publish_platform_name_snapshot 允许冗余存储,优先服务读侧问题详情接口,避免逐条回源主业务 PG
  2. 若历史数据尚未补齐上述快照字段,读侧允许退化为批量 IN 查询主业务 PG 并配合本地短 TTL 缓存

6.8 跨库一致性策略

主业务 PG 是配置真源,Monitoring PG 是结果投影库。

为避免跨库数据漂移,V5.9 采用:

  1. 软删除优先:品牌、问题、文章删除时先写主业务库 deleted_at,不立即物理删除 Monitoring PG 历史数据
  2. Outbox 事件传播:主业务库通过 outbox 记录配置变更事件,由 projector 异步投影到 Monitoring PG
  3. 最终一致:监测任务生成只读取“已投影完成”的配置快照

首期实现方式明确为:

  1. 使用应用层 outbox,不在首期引入 Debezium/CDC
  2. 主业务事务提交配置变更时,同事务写入 monitoring_config_outbox
  3. projector 进程每 5 秒轮询一次 outbox,按批次使用 FOR UPDATE SKIP LOCKED 投影到 Monitoring PG
  4. 正常配置投影延迟目标为 <= 30 秒,超过 2 分钟 进入告警
  5. 未完成投影的配置变更不得参与新任务生成
  6. 首期维持 PostgreSQL + 应用层 outbox + polling projector,不切到 CDC/WAL 订阅;原因是当前同步对象为低频配置变更、下游单一,CDC 运维复杂度高于收益

主业务 PG 中的 outbox 表建议为:

CREATE TABLE monitoring_config_outbox (
    id              BIGSERIAL PRIMARY KEY,
    tenant_id       BIGINT NOT NULL REFERENCES tenants(id),
    event_type      VARCHAR(64) NOT NULL,
    aggregate_type  VARCHAR(50) NOT NULL,
    aggregate_id    BIGINT NOT NULL,
    payload         JSONB NOT NULL,
    status          VARCHAR(20) NOT NULL DEFAULT 'pending',
    retry_count     INT NOT NULL DEFAULT 0,
    available_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    projected_at    TIMESTAMPTZ,
    last_error      TEXT
);

CREATE INDEX idx_monitoring_config_outbox_pending
ON monitoring_config_outbox(status, available_at, id);

首期推荐事件类型:

  • brand.deleted
  • brand_question.disabled
  • brand_question.updated(文案变更触发新快照投影)
  • tenant_monitoring_quota.updated
  • article.deleted

[V6] 预留事件类型(首期不实现):

  • tenant.collection_mode.switch_requested

处理原则:

  1. 主业务配置被软删除后,不再生成新任务
  2. Monitoring PG 保留历史结果用于趋势回看
  3. 读侧默认过滤已删除配置的“新任务入口”,但允许查询历史统计

Projector 运行要求:

  1. projector 作为独立 deployable/worker role 运行,不内嵌在 Monitor Worker 的 goroutine 中
  2. 生产环境推荐至少 2 个无状态副本,通过 FOR UPDATE SKIP LOCKED 竞争领取 outbox 批次,任一实例退出不影响整体投影
  3. 调度器生成任务前,只读取 monitoring_question_config_snapshots 与本地缓存配置,不回源主业务 PG
  4. 若租户存在超过阈值仍未投影完成的 outbox 事件,则当天任务生成跳过该租户并输出告警,避免读到过期快照

Replay / 补偿要求:

  1. 支持按 outbox_id 范围、tenant_idaggregate_type + aggregate_id 进行手动 replay
  2. replay 必须保持幂等,重复投影只允许 UPSERT 当前快照/缓存表,不重复生成业务结果
  3. 每日 06:00 增加一次轻量 reconcile,校验主业务有效配置与 Monitoring PG 快照数量是否一致,发现漂移时输出告警并允许重放
  4. 06:00 reconcile 的目标是在 06:30 日聚合前先完成配置快照校验,降低聚合读取过期配置的风险

6.9 [V6] collection_mode 切换状态机

首期不实现。 所有租户固定 collection_mode = plugin,无切换入口。以下为 API 模式上线时的预留设计。

插件模式和 API 模式是租户级二选一,因此模式切换必须显式建模。

该表属于主业务 PostgreSQL,因为它本质上是配置域状态,而不是监测结果。

推荐新增表([V6] 时建表):

CREATE TABLE tenant_collection_mode_switches (
    id               BIGSERIAL PRIMARY KEY,
    tenant_id        BIGINT NOT NULL REFERENCES tenants(id),
    from_mode        VARCHAR(20) NOT NULL,
    to_mode          VARCHAR(20) NOT NULL,
    status           VARCHAR(20) NOT NULL,
    requested_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    effective_at     TIMESTAMPTZ NOT NULL,
    grace_until      TIMESTAMPTZ NOT NULL,
    completed_at     TIMESTAMPTZ,
    cancelled_reason TEXT,
    created_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at       TIMESTAMPTZ NOT NULL DEFAULT now()
);

状态建议:

  • pending
  • freezing_old_mode
  • cutover_active
  • completed
  • cancelled

切换规则:

  1. effective_at 之前,不改变当前查询口径
  2. 到达 effective_at 后,旧模式停止生成新任务
  3. 旧模式已 leased / 已下发的任务允许执行到 grace_until
  4. 超过 grace_until 未完成的旧模式任务直接过期
  5. 新模式任务从 effective_at 起生成
  6. 同一租户 completed 状态的模式切换,最短间隔为 30 天
  7. 当存在 pending / freezing_old_mode / cutover_active 记录时,不允许再次发起新切换

前端展示规则:

  1. 趋势图按 collection_mode 分段展示
  2. 切换点展示模式切换标记
  3. 禁止把切换前后的数据拼成单一连续口径

6.10 分区与数据生命周期

Monitoring PG 中以下表建议按 business_date 做月分区:

  1. monitoring_collect_tasks
  2. question_monitor_runs
  3. question_monitor_parse_results
  4. monitoring_citation_facts
  5. monitoring_*_daily

推荐默认保留策略:

  1. 原始任务与原始回答:60 天热数据
  2. 引用事实:60 天热数据,必要时归档到对象存储或冷库
  3. 日聚合结果:30 天
  4. 超期数据按分区归档或删除,不做逐行清理

7. 解析标准化与 SoV 统计模型

7.1 解析分层

V5.9 的解析分为 3 层:

  1. 回答层:品牌提及、首位提及、首选推荐、情感倾向
  2. 引用层:从回答、citation JSON、search_results 中提取 URL 事实
  3. 来源层:基于域名清洗和站点聚合生成 SoV 统计结果

7.2 基于 tldextract 的顶级域名清洗

为避免 www、二级域名、国家后缀等问题导致同一站点重复计数,引用归因与来源排行统一采用 Public Suffix List + tldextract 口径。

Go 栈实现建议:

  1. 首期优先使用 golang.org/x/net/publicsuffix + 自封装完成 registrable_domain 提取。
  2. 若后续需要更接近 Python tldextract 的行为,再评估引入等价 Go 实现。

处理目标:

  1. 从原始 URL 中提取 subdomain / domain / suffix
  2. 生成 registrable_domain
  3. 生成统一站点主键 site_key
  4. 为页面级和站点级聚合提供稳定维度

示例:

原始 URL registrable_domain 说明
https://www.zhihu.com/question/123 zhihu.com www 折叠
https://zhuanlan.zhihu.com/p/123 zhihu.com 子域归并到站点
https://mp.weixin.qq.com/s/abc weixin.qq.com 基于 PSL 提取可注册域
https://finance.sina.com.cn/roll/123.html sina.com.cn 正确处理 com.cn
https://sub.example.co.uk/a example.co.uk 正确处理 co.uk

推荐清洗流程:

  1. URL 小写化主机名
  2. 去掉默认端口
  3. 去掉 fragment
  4. path 做基础规范化
  5. host 通过 tldextract 计算 registrable_domain
  6. 保存:
    • normalized_url
    • host
    • registrable_domain
    • subdomain
    • suffix

7.3 monitoring_citation_facts 推荐字段

为支撑域名清洗和来源排行,建议引用事实表至少包含:

CREATE TABLE monitoring_citation_facts (
    id                   BIGSERIAL PRIMARY KEY,
    tenant_id            BIGINT NOT NULL,
    run_id               BIGINT NOT NULL,
    brand_id             BIGINT NOT NULL,
    ai_platform_id       VARCHAR(30) NOT NULL,
    business_date        DATE NOT NULL,
    cited_url            TEXT NOT NULL,
    cited_title          TEXT,
    normalized_url       TEXT NOT NULL,
    host                 VARCHAR(255) NOT NULL,
    registrable_domain   VARCHAR(255) NOT NULL,
    subdomain            VARCHAR(255),
    suffix               VARCHAR(50),
    site_key             VARCHAR(255) NOT NULL,
    article_id           BIGINT,
    publish_record_id    BIGINT,
    resolution_status    VARCHAR(20) NOT NULL DEFAULT 'resolved',
    resolution_confidence VARCHAR(20) NOT NULL DEFAULT 'high',
    created_at           TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_citation_facts_domain
ON monitoring_citation_facts(tenant_id, business_date, registrable_domain);

CREATE INDEX idx_citation_facts_run
ON monitoring_citation_facts(run_id);

约定:

  1. normalized_url 用于页面级去重和文章归因
  2. registrable_domain 用于站点级来源排行
  3. site_key 默认等于 registrable_domain
  4. 如业务需要区分站群,可对特定域名做覆写映射
  5. cited_title 优先使用平台返回的 citation title;缺失时读侧允许退化为 URL 文本

7.4 site_key 映射规则

默认规则:

  • site_key = registrable_domain

可选增强规则:

  1. mp.weixin.qq.comweixin.qq.com 统一到 weixin.qq.com
  2. 将业务上需要区分的站群写入 site_domain_mapping
  3. 对同集团但不同产品是否合并,必须通过配置显式决定,不靠临时规则猜测

推荐新增配置表:

CREATE TABLE site_domain_mappings (
    id                  BIGSERIAL PRIMARY KEY,
    tenant_id           BIGINT,
    registrable_domain  VARCHAR(255) NOT NULL,
    site_key            VARCHAR(255) NOT NULL,
    site_name           VARCHAR(255) NOT NULL,
    is_active           BOOLEAN NOT NULL DEFAULT true,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

映射优先级:

  1. 租户级映射(tenant_id = 当前租户
  2. 全局默认映射(tenant_id IS NULL

落库建议:

  1. site_domain_mappings 的配置真源放在主业务域
  2. Monitoring 读侧使用由 projector 投影到 Monitoring PG 的本地只读缓存,避免问题详情接口在高频查询时跨库查站点映射
  3. 读侧若未命中映射,则 site_name 回退为 registrable_domain

7.5 SoV 统计模型

V5.9 中的 SoV 统一解释为:

在当前采样窗口内,某品牌、某站点或某文章在全部有效样本中的相对占比

必须明确分母,禁止不同口径混用。

7.6 品牌 SoV

品牌 SoV 适用于品牌曝光类页面。

定义:

brand_sov = brand_mentioned_answers / actual_sample_count

其中:

  • brand_mentioned_answers:成功采样回答中,提及目标品牌的回答数
  • actual_sample_count:成功采样回答总数

这和页面上的“提及率”同源,可视为品牌在样本回答中的 Share of Voice

7.7 引用来源 SoV

引用来源 SoV 适用于“引用来源排行”“来源平台占比”。

定义:

source_sov = source_citation_count / total_resolved_citation_count

其中:

  • source_citation_count:某 site_key 在窗口内被引用的次数
  • total_resolved_citation_count:全部已成功解析并完成站点归类的引用次数

补充口径:

  • unresolved 引用不进入分子
  • unresolved 是否进入分母,要固定为“不进入”
  • unresolved 单独展示占比,作为质量指标

7.8 文章 SoV

文章 SoV 用于内部文章在可归因引用中的占比。

定义:

article_sov = article_citation_count / total_article_resolved_citation_count

其中:

  • article_citation_count:某篇内部文章被 AI 引用的次数
  • total_article_resolved_citation_count:全部成功归因到内部文章的引用次数

注意:

  1. 文章 SoV 不是品牌 SoV
  2. 文章 SoV 的分母不能拿全部回答数
  3. 若用户看的是“文章引用率”,仍然应使用 cited_answer_count / actual_sample_count

7.9 覆盖率与 SoV 的关系

覆盖率和 SoV 必须分开解释:

  1. coverage_rate 反映“今天采样够不够”
  2. SoV 反映“在已采样样本里占比多少”

因此页面解释顺序应为:

  1. 先看覆盖率
  2. 再看 SoV / 提及率 / 引用率

coverage_rate < 40% 时:

  • 允许展示 SoV
  • 但必须附带低置信度提示

7.10 聚合输出建议

建议在 monitoring_citation_source_dailymonitoring_citation_page_daily 中增加:

  1. citation_count
  2. citation_sov
  3. resolved_count
  4. unresolved_count
  5. confidence_level

这样前端可以同时展示:

  • 引用次数
  • SoV
  • 数据质量

8. 任务协议与插件侧流程

8.1 统一为租约式协议

V5 删除 GET tasks -> POST claim 两步式协议,统一改为:

  1. 插件请求租约
  2. 服务端原子分配任务并返回 lease_token
  3. 插件执行采集
  4. 插件带 lease_token 回传结果
  5. 服务端校验归属、先写任务状态为 received 并投递 RabbitMQ
  6. Monitor Worker 异步写入运行结果、解析结果并完成任务

8.2 插件 API

POST /api/plugin/monitoring/heartbeat
POST /api/plugin/monitoring/tasks/lease
POST /api/plugin/monitoring/tasks/{id}/result
POST /api/plugin/monitoring/tasks/{id}/skip

请求头:

  • X-Geo-Installation-Token
  • X-Geo-Installation-Id

说明:

  1. POST /api/plugin/monitoring/heartbeat 用于刷新 plugin_installations.last_seen_at 并批量上报平台 detect() 结果
  2. 心跳接口建议在同一事务内完成:安装实例鉴权、last_seen_at 更新、monitoring_platform_access_snapshots 批量 UPSERT
  3. 这组接口只服务 collector_type = plugin
  4. collection_mode = api 的服务端调度入口为 [V6] 预留,首期不实现

8.3 租约接口行为

租约接口建议按 PG 原子领取:

SELECT id
FROM monitoring_collect_tasks
WHERE tenant_id = $1
  AND status = 'pending'
  AND business_date = $2
ORDER BY planned_at ASC, id ASC
FOR UPDATE SKIP LOCKED
LIMIT $3;

服务端完成以下动作:

  1. 选出任务
  2. 生成短期 lease_token
  3. 更新 status = leased
  4. 写入 lease_expires_at = now() + interval '15 minutes'
  5. (tenant_id, question_id, question_hash)monitoring_question_config_snapshots 装配题目文案并返回给插件

8.3A 租户侧“立即采集”入口

数据详情页提供租户侧手动触发入口:

POST /api/tenant/monitoring/brands/{brand_id}/collect-now

行为定义:

  1. 仅当 primary_monitoring_installation 在线时可触发;离线则返回 installation_offline
  2. 手动触发不创建第二套同日任务,而是把当日该品牌下尚未完成的 pending / expired / failed 任务刷新为可再次租约的状态,更新 planned_at = now(),并将 trigger_source 更新为 manual
  3. 若当日已存在成功快照,则接口直接返回“今日已有有效快照”,不新增第二个日点位
  4. 若存在已被插件领取的 leased 任务,接口不等待当前租约批次完成;这些任务继续执行,本次手动触发只刷新未被领取的任务
  5. 接口返回值建议包含 refreshed_task_count / leased_task_count / completed_task_count,前端文案明确为“已刷新未领取任务,已领取任务继续执行”
  6. 手动触发成功后,页面先进入“手动采集中”状态;待新结果成功入库并完成局部聚合后,再刷新“今日快照”和对应日桶的 snapshot_updated_at / trigger_source
  7. 首期仍遵守“在线就采集,不在线就放弃”的原则,不做离线补数
  8. 若用户当天修改问题文案后立即手动触发,仍执行当日已生成任务锁定的题目版本;新文案从下一 business_date 起生效

8.4 插件执行策略

V5.9 复用的是现有扩展框架,而不是现有发布 adapter 体系:

代码库对齐说明:

  1. 可复用的是 WXT 框架、background service worker、alarm/message passing、平台状态存储等基础设施
  2. 现有 src/adapters/ 是媒体发布 adapter,接口为 detect() + publish(),继续服务文章发布链路
  3. 监测侧需要新建独立 AI 监测 adapter 模块,例如 src/monitoring-adapters/,接口建议为 detect() + query() + parseResponse()
  4. 发布 adapter 与监测 adapter 可以共享平台状态探测能力,但不共享同一接口定义、平台枚举和执行流水线

但采样策略改为“小批量、固定预算”:

项目 建议值
心跳频率 15 分钟
单次租约任务数 2-3
单任务超时 60 秒
租约时长 15 分钟
每日硬上限 按套餐 36 / 72

8.4A 平台状态上报闭环

为让 monitoring_platform_access_snapshots 真正成为调度器与读侧共享状态源,首期明确采用以下闭环:

  1. 主监测插件每次心跳都执行监测平台 detect(),并把 connected / accessible / reason_code / reason_message 随心跳一并回传服务端
  2. 服务端以心跳接收时间所属的 business_date 为准,UPSERT monitoring_platform_access_snapshots
  3. 00:10 调度器生成当天任务时,优先读取当日快照;若当日快照尚未出现,则回退读取最近 24h 内最新快照做机会型计划
  4. 若调度后首次心跳把平台状态修正为 not_logged_in / unavailable,服务端立即对该平台当日仍为 pending 的任务执行 reconcile,改为 skipped
  5. leased 中的任务不因状态回传被强制中断;仍按正常回调或过期回收处理
  6. 读侧平台矩阵与问题详情面板统一读取该表,不直接根据前端本地状态猜测 未登录 / 不可用

8.5 任务过期与回收

定时任务每 30 分钟处理一次:

  1. lease_expires_at < now() 且状态仍为 leased 的任务改为 expired
  2. received 状态超过阈值未被 worker 完成时,进入异常告警
  3. 同一 business_dateexpired 任务最多重试 1 次
  4. 超过 business_date + 2 days 的未完成任务不再重试

8.6 [V6] API 采集模式接口预留

首期不实现。 本节仅保留数据模型和协议契约,供后续 API 模式接入时复用。

V5.9 中插件采集和 API 采集是二选一模式,因此必须提前统一“结果入库协议”。

建议内部抽象:

CollectTask
  -> collector_type = plugin | api

CollectorResultEnvelope
  -> collector_type
  -> task_id
  -> tenant_id
  -> brand_id
  -> question_id
  -> question_hash
  -> ai_platform_id
  -> run_mode
  -> provider_model
  -> provider_request_id
  -> answer
  -> citations
  -> search_results
  -> raw_response_json
  -> status

统一原则:

  1. 插件采集和 API 采集最终都投递到同一个 monitor.result.ingest
  2. parse worker 只认标准化后的 CollectorResultEnvelope
  3. 采集通道差异在 adapter 层消化,不传播到聚合层和查询层
  4. 同一租户、同一统计窗口只能启用一种 collector_type
  5. 传输层中的 question_hash 统一使用 v1:<lowercase-hex> 字符串表示,ingest 后统一解码落为 Monitoring PG 的二进制指纹

8.7 [V6] API 模式执行器并发模型

首期不实现。 本节为 API 模式上线时的执行器并发和限流预留设计。

API 模式不复用插件租约接口,但仍复用同一任务表和同一领取模型。

建议:

  1. API worker 从 monitoring_collect_tasks 中仅领取 collector_type = api 的任务
  2. API worker 同样使用 FOR UPDATE SKIP LOCKED 控制并发
  3. 并发粒度按 ai_platform_id 和租户配额限流
  4. 同一问题同一平台同一天仍由幂等键保证不重复落数

推荐并发层级:

  1. 租户级并发上限
  2. 平台级并发上限
  3. 全局 provider QPS / TPM 限流

首期实现建议:

  1. 全局 provider QPS / TPM 限流先在 API worker 进程内以 token bucket 实现
  2. worker 集群总吞吐通过部署副本数和进程内并发参数共同控制
  3. 当后续进入多实例横向扩容或 provider 配额需要跨实例共享时,再升级为 Redis 分布式限流
  4. 限流接口在代码层保持抽象,避免首期实现和后续分布式版本耦合

9. 采集、解析、聚合架构

9.1 首期简化版链路

插件每 15 分钟心跳并上报 detect() 结果
      ↓
服务端 upsert monitoring_platform_access_snapshots
      ↓
每日 00:10 调度器生成当天 plugin 任务
      ↓
用户可在数据详情页手动催发当日未完成任务
      ↓
插件心跳租约并执行
      ↓
插件回传结果 -> 写入 RabbitMQ ingest
      ↓
Monitor Worker:消费结果 -> 写 Monitoring PG -> 解析结果 -> 完成任务
      ↓
每日 06:30 聚合 Job 汇总到 monitoring_*_daily
      ↓
读侧 API + Redis 缓存

[V6] 预留分支:

  1. API 模式上线后,再在调度器后增加服务端 provider adapter 执行链路
  2. 该扩展不改变下游 ingest / parse / aggregate 主链路

首期调度器实现选择:

  1. 不复用当前 schedule_tasks
  2. 原因是当前代码库中 schedule_tasks 只有 CRUD 和 schema,尚无 executor / dispatcher
  3. 首期监测调度器直接作为 Monitor Worker 进程内独立 cron goroutine 实现,同时负责每日任务生成和过期回收
  4. 待通用调度执行器成熟后,再评估是否统一收敛到平台级调度框架

9.2 RabbitMQ 的职责边界

V5.9 中 RabbitMQ 只承担“结果异步化”,不承担“任务分发”。

使用 RabbitMQ 的原因:

  1. 插件回调接口可以快速返回,降低超时和重试风险
  2. 解析、引用归因、写库失败不会直接影响插件侧体验
  3. 方便把高频写入隔离到独立 worker
  4. 后续若企业版放大量级,不需要重做写链路

但 RabbitMQ 不参与以下事项:

  1. 不做任务租约
  2. 不做读侧查询
  3. 不强制引入准实时前端刷新

推荐队列:

  • monitor.result.ingest
  • monitor.result.dlq
  • monitor.aggregate.trigger

说明:

  1. 首期 monitor.result.ingest 只接收 plugin 采集结果
  2. [V6] 引入 API 模式后,继续复用同一 ingest 队列和同一标准化 envelope
  3. 队列消费者不关心采集来源,只关心统一 envelope

MQ 部署建议:

  1. 开发 / docker-compose 环境先落单节点 RabbitMQ,验证 publisher、consumer、DLQ 全链路
  2. 生产环境使用 3 节点 RabbitMQ 集群
  3. 核心队列在生产环境使用 durable + quorum queue
  4. 发布端启用 publisher confirm
  5. 消息使用 persistent delivery mode

消费与幂等建议:

  1. worker 在 Monitoring PG 事务提交成功后再 ack
  2. task_id + collector_type + business_date 或运行表唯一键做最终幂等
  3. 同一消息重复投递时,允许消费端幂等落空后直接 ack

DLQ 处理流程:

  1. 首次失败按重试策略回投主队列,重试 3 次,退避间隔为 5s / 30s / 120s
  2. 重试次数通过消息 header 传递,超过阈值进入 monitor.result.dlq
  3. 运维或研发基于任务 ID、错误类型、原始 payload 做人工排查
  4. 排查完成后支持“重放到 ingest”或“标记任务 failed”
  5. 所有重放动作必须写审计日志,记录操作人、时间、原因、task_id 和消息摘要

9.3 Monitoring PostgreSQL 的职责

独立 Monitoring PG 的目的不是“多一套库”,而是隔离监测链路的写放大。

承载内容:

  1. 任务表
  2. 原始回答
  3. 解析结果
  4. 引用事实
  5. 日聚合结果

收益:

  1. 主业务 PG 不承受高频插入和大文本更新
  2. Monitoring PG 可独立做分区、归档、VACUUM 策略
  3. 监测表膨胀不会影响文章、品牌、发布主链路

9.4 原始回答存储

V5.9 中抓取结果优先进入 Monitoring PG

  1. 原始回答文本
  2. 平台响应元数据
  3. 引用与搜索结果原始 JSON
  4. 解析后的结构化字段

可选归档策略:

  1. 小于 64 KB 的原文直接存 Monitoring PG
  2. 超大原文或完整调试负载转存对象存储
  3. Monitoring PG 仅保留摘要、关键字段和存储路径

10. 文章引用归因策略

10.1 为什么不能只看 URL 最后一段

仅按最后一段匹配存在以下问题:

  1. 不同平台可能出现相同 path segment
  2. 同一平台 PC / H5 / 分享页 URL 可能不同
  3. AI 引用结果可能带参数、重定向或 canonical URL
  4. 同一篇文章可能存在多个可访问别名

因此 V5 的归因策略是 分层匹配 + 置信度标记

10.2 归因顺序

  1. normalized_url 精确匹配
  2. (platform_id, external_article_id) 精确匹配
  3. (domain, last_path_segment) 唯一匹配
  4. 无法唯一命中时,记为 unresolved

10.3 低置信度处理

当仅通过 last_path_segment 命中时:

  1. 给归因结果打 low_confidence
  2. 可以进入趋势参考视图
  3. 不建议进入需要精确计费或精确归属的报表

这与 V5 的“趋势参考”定位是一致的。


11. 平台接入范围

11.1 V5 交付范围

数据模型支持 6 平台扩展,但首期交付只承诺首批 3 平台:

  1. DeepSeek
  2. 通义千问
  3. 豆包

11.2 页面与套餐表达

平台访问条件不能一刀切,V5.9 统一按“平台访问模式”处理。

推荐在平台配置中增加:

  • auth_requirement = login_required | anonymous_allowed

当前已知口径:

平台 访问模式 说明
DeepSeek login_required 必须登录后才能提问
Kimi login_required 必须登录后才能提问
其他当前接入平台 anonymous_allowed 可在未登录状态下直接提问

任务生成规则:

  1. login_required 平台:只有检测到有效登录态才进入当天采样计划
  2. anonymous_allowed 平台:默认可进入当天采样计划
  3. 若免登录平台后续改版为必须登录,应通过平台配置热更新切换,不改调度代码

实现建议:

  1. 平台访问模式放入 ai_platforms 或等价配置表
  2. 插件 detect() 返回 accessibleconnected 两个概念
  3. 调度器判断的是 accessible,不是简单判断 connected

页面和套餐都必须按“当前已接入且满足访问条件的平台”解释:

  • 已接入且满足访问条件:进入采样计划
  • 已接入但不满足访问条件:展示 未登录不可访问
  • 未接入:展示 未接入

不得再用“全 6 平台稳定可用”作为首期对外承诺。


12. 读侧与汇总表

V5 读侧继续沿用 V3 的聚合表和缓存思路,但口径增加采样字段:

  1. desired_sample_count
  2. planned_sample_count
  3. actual_sample_count
  4. coverage_rate
  5. confidence_level

推荐所有 monitoring_*_daily 汇总表统一增加以上字段。

12.0 monitoring_brand_daily 推荐 DDL

为避免实现期对“品牌日聚合表”字段理解不一致,至少明确一张完整日聚合表定义:

CREATE TABLE monitoring_brand_daily (
    id                       BIGSERIAL PRIMARY KEY,
    tenant_id                BIGINT NOT NULL,
    brand_id                 BIGINT NOT NULL,
    collector_type           VARCHAR(20) NOT NULL DEFAULT 'plugin',
    business_date            DATE NOT NULL,
    sample_status            VARCHAR(20) NOT NULL,
    desired_sample_count     INT NOT NULL DEFAULT 0,
    planned_sample_count     INT NOT NULL DEFAULT 0,
    actual_sample_count      INT NOT NULL DEFAULT 0,
    coverage_rate            NUMERIC(8,4),
    confidence_level         VARCHAR(20) NOT NULL,
    trigger_source           VARCHAR(20),
    snapshot_updated_at      TIMESTAMPTZ,
    mentioned_count          INT NOT NULL DEFAULT 0,
    top1_mentioned_count     INT NOT NULL DEFAULT 0,
    first_recommended_count  INT NOT NULL DEFAULT 0,
    positive_mentioned_count INT NOT NULL DEFAULT 0,
    cited_answer_count       INT NOT NULL DEFAULT 0,
    mention_rate             NUMERIC(8,4),
    top1_mention_rate        NUMERIC(8,4),
    first_recommend_rate     NUMERIC(8,4),
    positive_mention_rate    NUMERIC(8,4),
    citation_rate            NUMERIC(8,4),
    created_at               TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at               TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uk_monitoring_brand_daily
        UNIQUE (tenant_id, brand_id, collector_type, business_date)
);

CREATE INDEX idx_monitoring_brand_daily_lookup
ON monitoring_brand_daily(tenant_id, brand_id, business_date DESC, collector_type);

说明:

  1. overviewbrand_time_buckets 可直接读取本表
  2. desired_sample_count 用于支撑 3.2 节“目标 60,计划 30”的副文案;不建议每次读接口动态回算
  3. 平台矩阵建议使用同风格的 monitoring_brand_platform_daily,仅额外加入 ai_platform_id 并纳入唯一键
  4. sample_status / trigger_source / snapshot_updated_at 为页面 tooltip 和手动刷新反馈保留,不建议只在缓存层临时拼装

推荐最小结构:

CREATE TABLE monitoring_brand_platform_daily (
    id                    BIGSERIAL PRIMARY KEY,
    tenant_id             BIGINT NOT NULL,
    brand_id              BIGINT NOT NULL,
    ai_platform_id        VARCHAR(30) NOT NULL,
    collector_type        VARCHAR(20) NOT NULL DEFAULT 'plugin',
    business_date         DATE NOT NULL,
    platform_sample_status VARCHAR(20) NOT NULL,
    planned_sample_count  INT NOT NULL DEFAULT 0,
    actual_sample_count   INT NOT NULL DEFAULT 0,
    mention_rate          NUMERIC(8,4),
    top1_mention_rate     NUMERIC(8,4),
    last_sampled_at       TIMESTAMPTZ,
    created_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at            TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uk_monitoring_brand_platform_daily
        UNIQUE (tenant_id, brand_id, ai_platform_id, collector_type, business_date)
);

12.1 Dashboard composite

/api/tenant/monitoring/dashboard/composite 继续保留,但返回结构需扩展以支撑新的四层页面布局:

{
  "overview": {
    "collection_mode": "plugin",
    "mention_rate": 0.42,
    "top1_mention_rate": 0.18,
    "first_recommend_rate": 0.25,
    "positive_mention_rate": 0.67,
    "citation_rate": 0.18,
    "desired_sample_count": 60,
    "planned_sample_count": 30,
    "actual_sample_count": 21,
    "coverage_rate": 0.7,
    "confidence_level": "high",
    "last_sampled_at": "2026-04-08T09:20:00Z",
    "last_trigger_source": "automatic",
    "prev_snapshot_mention_rate": 0.37,
    "prev_snapshot_top1_mention_rate": 0.2,
    "prev_snapshot_first_recommend_rate": 0.22,
    "prev_snapshot_positive_mention_rate": 0.67
  },
  "platform_breakdown": [
    {
      "ai_platform_id": "doubao",
      "platform_name": "豆包",
      "mention_rate": 0.45,
      "top1_mention_rate": 0.2,
      "platform_sample_status": "sampled",
      "actual_sample_count": 8,
      "last_sampled_at": "2026-04-08T09:20:00Z"
    },
    {
      "ai_platform_id": "deepseek",
      "platform_name": "DeepSeek",
      "mention_rate": 0.38,
      "top1_mention_rate": 0.15,
      "platform_sample_status": "sampled",
      "actual_sample_count": 6,
      "last_sampled_at": "2026-04-08T08:45:00Z"
    },
    {
      "ai_platform_id": "qwen",
      "platform_name": "通义千问",
      "mention_rate": null,
      "top1_mention_rate": null,
      "platform_sample_status": "not_logged_in",
      "actual_sample_count": 0,
      "last_sampled_at": null
    }
  ],
  "brand_time_buckets": [
    {
      "date": "2026-04-03",
      "sample_status": "sampled",
      "metric_value": 0.42,
      "planned_sample_count": 12,
      "actual_sample_count": 8,
      "coverage_rate": 0.67,
      "trigger_source": "automatic",
      "snapshot_updated_at": "2026-04-03T09:12:00Z"
    },
    {
      "date": "2026-04-04",
      "sample_status": "unsampled",
      "metric_value": null,
      "planned_sample_count": 0,
      "actual_sample_count": 0,
      "coverage_rate": null,
      "trigger_source": null,
      "snapshot_updated_at": null
    }
  ],
  "hot_questions": [
    {
      "question_id": 42,
      "question_hash": "v1:0f3be2f94a4e7b51d9d1f9a6c2208d15",
      "question_text": "北京口腔医院推荐?",
      "sample_count": 3,
      "mention_rate": 0.67,
      "last_sampled_at": "2026-04-08T09:15:00Z"
    }
  ],
  "citation_ranking": [
    {
      "ai_platform_id": "deepseek",
      "platform_name": "DeepSeek",
      "sample_status": "sampled",
      "cited_answer_count": 2,
      "cited_article_count": 1,
      "citation_rate": 0.33
    }
  ],
  "cited_articles": [
    {
      "article_id": 88,
      "article_title": "北京口腔医院全面测评",
      "publish_platform": "微信公众号",
      "citation_count": 3,
      "citation_rate": 0.14
    }
  ]
}

补充规则:

  1. 查询接口必须返回当前 collection_mode
  2. 首期所有查询固定按 collector_type = plugin 过滤
  3. [V6] 若后续引入模式切换,历史趋势需按模式分段或按模式筛选,不做跨模式拼接
  4. overview 新增 prev_snapshot_* 字段,供前端计算指标卡趋势箭头
  5. platform_breakdown 始终返回全部已接入平台,不受前端模型筛选器影响
  6. brand_time_buckets 继续保留,供 mini sparkline 和日指示点渲染
  7. sample_status 是读侧解释字段,前端只在日指示点 tooltip 中使用,不作为逐日主文案
  8. 同一自然日若发生手动催发,接口返回的仍然是一个日桶;只更新 trigger_source / snapshot_updated_at
  9. hot_questions(question_id, question_hash) 聚合,避免同一 question_id 在窗口内因文案变更而混算
  10. 前端打开问题详情面板时,应从 hot_questions 透传 question_id + question_hash
  11. hot_questionssample_count / mention_rate / last_sampled_atquestion_monitor_runs + question_monitor_parse_results 聚合得出
  12. citation_rankingcited_articles 缺数据时返回空数组,不以 0 强补
  13. platform_sample_statusquestion_monitor_runs + monitoring_collect_tasks + monitoring_platform_access_snapshots 共同判定,而不是只看成功运行记录

12.1A 高频问题详情接口

问题详情面板通过独立接口按需加载,不合并到 composite 接口,避免拖慢首屏渲染。

GET /api/tenant/monitoring/brands/{brand_id}/questions/{question_id}/detail
    ?date_from=2026-04-08
    &date_to=2026-04-08
    &question_hash=v1:0f3be2f94a4e7b51d9d1f9a6c2208d15
    &ai_platform_id=deepseek   // 可选,默认返回全部平台数据

返回结构示例:

{
  "question_id": 42,
  "question_hash": "v1:0f3be2f94a4e7b51d9d1f9a6c2208d15",
  "question_text": "北京口腔医院推荐?",
  "time_window": {
    "date_from": "2026-04-08",
    "date_to": "2026-04-08"
  },
  "sample_count": 3,
  "platforms": [
    {
      "ai_platform_id": "deepseek",
      "platform_name": "DeepSeek",
      "sample_status": "sampled",
      "run_id": 1001,
      "sampled_at": "2026-04-08T09:15:00Z",
      "provider_model": "deepseek-chat",
      "answer_text": "根据用户口碑和专业评价,推荐以下几家...",
      "brand_mentioned": true,
      "brand_mention_position": "top1",
      "citations": [
        {
          "cited_url": "https://www.zhihu.com/question/123",
          "cited_title": "北京口腔医院哪家比较好?",
          "site_name": "知乎",
          "site_key": "zhihu.com",
          "favicon_url": "https://zhihu.com/favicon.ico",
          "article_id": null,
          "resolution_status": "resolved",
          "resolution_confidence": "high"
        },
        {
          "cited_url": "https://mp.weixin.qq.com/s/abc123",
          "cited_title": "北京口腔医院全面测评",
          "site_name": "微信公众号",
          "site_key": "weixin.qq.com",
          "favicon_url": "https://weixin.qq.com/favicon.ico",
          "article_id": 88,
          "article_title": "北京口腔医院全面测评",
          "resolution_status": "resolved",
          "resolution_confidence": "high"
        }
      ]
    },
    {
      "ai_platform_id": "qwen",
      "platform_name": "通义千问",
      "sample_status": "not_logged_in",
      "run_id": null,
      "sampled_at": null,
      "provider_model": null,
      "answer_text": null,
      "brand_mentioned": null,
      "citations": []
    }
  ],
  "citation_analysis": [
    {
      "ai_platform_id": "deepseek",
      "platform_name": "DeepSeek",
      "citation_count": 4,
      "distinct_site_count": 3,
      "brand_article_citation_count": 1,
      "has_brand_article_citation": true
    }
  ],
  "content_citations": [
    {
      "article_id": 88,
      "article_title": "北京口腔医院全面测评",
      "publish_platform": "微信公众号",
      "ai_platform_id": "deepseek",
      "platform_name": "DeepSeek",
      "citation_count": 1,
      "citation_rate": 0.33
    }
  ]
}

接口说明:

  1. platforms 数组始终返回全部已接入平台,未采样平台的 answer_text / citations 为空
  2. 若请求时携带 ai_platform_id 参数,platforms 数组只返回指定平台的数据
  3. citation_analysis 为跨平台引用汇总,不受 ai_platform_id 参数过滤,始终展示全平台聚合
  4. content_citationsai_platform_id 参数过滤;未传时返回全部平台的内容引用记录
  5. 该接口主读 question_monitor_runsquestion_monitor_parse_resultsmonitoring_citation_factsmonitoring_collect_tasksmonitoring_platform_access_snapshotssite_domain_mappingsmonitoring_article_url_aliases
  6. brand_mention_positiontop1 | mentioned | not_mentioned | null
  7. question_hash 由外层 hot_questions 透传,用于锁定同一问题文案版本;若省略,后端只允许解析为窗口内最新版本,不保证历史版本一致性
  8. favicon_urlsite_keyregistrable_domain 在读侧运行时派生,不要求持久化到 Monitoring PG
  9. content_citations.citation_rate 的分母固定为同一 question_id + question_hash 在当前时间窗口内的成功采样次数
  10. article_title / publish_platform 优先读取 monitoring_article_url_aliases 中的快照字段;若缺失,只允许按 article_id / publish_record_id 执行单次批量 IN 查询并配合 TTL = 5 分钟 本地缓存,禁止按引用逐条回源主业务 PG

12.2 缓存策略

读侧缓存策略可继续沿用 V3

  • composite 聚合缓存
  • singleflight
  • TTL 抖动
  • stale-while-revalidate

但缓存失效触发点改为:

  1. 每日聚合完成
  2. 用户切换品牌 / 时间窗口
  3. 手动采集触发的局部增量聚合完成

不需要为单条采样结果做准实时逐条失效。

12.3 聚合补算策略

为避免 06:30 后仍有 received / completed 结果陆续入库导致统计偏差,V5.9 采用“双阶段聚合”:

  1. 06:30 产出首版日聚合
  2. 08:00 对当日和前一日做一次增量修正

08:00 后仍有迟到结果:

  1. 更新原始表与事实表
  2. 在下一次修正窗口内补算
  3. 不要求逐条触发读侧实时失效

手动触发例外:

  1. collect-now 触发的任务产生新的成功结果后,worker 额外投递一条 monitor.aggregate.trigger
  2. 聚合消费者按 (tenant_id, brand_id, business_date) 做一次局部增量重算,覆盖 monitoring_brand_daily、高频问题列表所需汇总以及问题详情相关引用汇总
  3. 局部聚合成功后,主动失效该品牌当日的 dashboard/compositequestions/{question_id}/detail 缓存
  4. 该路径仅服务“手动采集后的用户可见刷新”,目标 SLA 为 <= 1 分钟,不把全链路改造成逐条准实时聚合

13. 降级、告警与风险

13.1 降级策略

场景 V5 行为
插件模式下插件 24h 未活跃 暂停生成新任务,页面展示上次数据
登录必需平台未登录 该平台不进入当天计划样本
当日覆盖率低 页面提示“样本不足,仅供参考”
某平台 adapter 失效 该平台展示“暂不可用”,其余平台继续
URL 无法归因 计入 unresolved,不强行挂到文章

13.2 关键告警

指标 阈值
主插件离线时长 > 24h
日覆盖率 < 40%
单平台连续失败 > 20 次 / 天
unresolved 引用占比 > 30%
每日聚合延迟 > 08:00 未完成
projector 无心跳 > 1 分钟
outbox 投影延迟 > 2 分钟
outbox 堆积量 > 100 持续 10 分钟

13.3 Token 生命周期与回调安全

X-Geo-Installation-Token 需要完整生命周期管理。

建议:

  1. 安装注册时签发长期 token,仅存 hash
  2. 支持手动吊销与重新签发
  3. 插件结果回调除安装 token 外,仍要求携带短期 lease_token
  4. task_id + lease_token + collector_type 共同校验,防止 replay attack
  5. 任务进入 received / completed 后,重复结果直接按幂等处理,不重复计数

13.4 可观测性建设

V5.9 需要最小可运维基线,而不只是阈值表。

建议输出:

  1. Prometheus metrics
    • task lease latency
    • queue depth
    • worker consume lag
    • projector heartbeat
    • outbox pending count
    • outbox projection lag
    • outbox replay count
    • projector reconcile drift
    • parse success rate
    • unresolved citation ratio
    • per-mode coverage rate
  2. 结构化日志:
    • task_id
    • tenant_id
    • collector_type
    • ai_platform_id
    • provider_request_id
  3. 链路追踪:
    • lease -> execute -> ingest -> parse -> aggregate

13.5 风险接受说明

V5.9 明确接受以下风险:

  1. 单日样本偏少导致波动大
  2. 插件离线导致当天无新数据
  3. 部分 URL 只能低置信度归因
  4. 调度窗口前 24h 内曾在线但次日实际未再上线,导致当日任务自然过期;首期接受该浪费,不做离线补数

但不接受以下风险:

  1. 任务重复执行导致统计翻倍
  2. 输入口径变更污染历史数据
  3. 未授权回调写入虚假监测结果

14. 实施计划

14.1 Phase 分期

Phase 内容 估时
A 双库基建:Monitoring PG 实例、双连接池、monitoring_database 配置、双 sqlc codegen、主业务/监测库 schema 4-5 天
B 后台基建:RabbitMQ、publisher/consumer 骨架、DLQ、outbox polling projector 4 天
C 插件:在现有扩展框架内新建 AI 监测 adapter 模块、3 平台、租约轮询、结果回传 10-12 天
D 后端:租约 API、监测调度 cron、结果回调、MQ 消费、解析入库、日聚合 5-6 天
E 前端:数据追踪页补覆盖率、更新时间、置信度提示、每日单快照脉冲视图 3 天
F 可观测性:metrics、日志、告警、DLQ 运维脚本 3 天
G 联调、坏样本验证、口径验收 4 天
基线合计 33-37 天

建议:

  1. Phase A 需拆成 A1 Monitoring PG + docker-composeA2 双连接池 + 双 sqlcA3 schema / outbox / snapshot 表
  2. Outbox DDL 归 Phase Aoutbox polling projector 归 Phase Bcollection_mode 切换状态机延后到 [V6]
  3. Phase C 建议拆成 C1 框架 + 1 平台 + 结果回传C2 +2 平台 + 登录态/风控收尾 两个子阶段,以降低 DOM 适配风险
  4. 33-37 天 是按“首期只做 plugin 模式”校准后的基线工期
  5. 若按 20% buffer 对外承诺,建议按 40-44 天 评估

14.2 验收标准

  1. 页面可展示 planned_sample_count / actual_sample_count / coverage_rate
  2. 页面可正常展示离散采样时序视图、高频问题、引用排行
  3. 当覆盖率不足时,页面有明确提示
  4. 同一任务不会被重复计入统计
  5. 问题文案修改后,历史趋势不被污染
  6. 引用文章数可通过 URL alias 规则稳定归因,无法归因时进入 unresolved
  7. 首期所有读侧查询固定按 collector_type = plugin 过滤,为 [V6] 扩展保留模式边界
  8. MQ、DLQ、worker lag、覆盖率等关键指标可观测
  9. 未采样日期不会被渲染为 0 值折线点
  10. 数据详情页支持“立即采集”,但同一自然日不会新增第二个展示点位
  11. 前端不会把 sampled / partial / unsampled 直接逐日渲染为主文案

15. V4 → V5 变更摘要

编号 V4 V5
C1 全量监测叙事 采样趋势叙事
C2 Free 1 品牌 / 40 问题 Free 1 品牌,尽量覆盖全部启用问题,受执行预算限制
C3 Pro 3 品牌 / 40 问题 Pro 3 品牌,尽量覆盖全部启用问题,受执行预算限制
C4 GET + claim 两步任务协议 一步租约式任务协议
C5 question_version_id question_hash 负责任务快照锁定,question_text_snapshot 负责运行快照
C6 RabbitMQ + 增量聚合 RabbitMQ 仅做结果异步化,不参与任务分发或前端准实时
C7 准实时可见 T+1 / 延迟可接受
C8 仅展示百分比 百分比 + 采样覆盖信息
C9 首期 6 平台叙事 首期只承诺 3 平台
C10 单一业务库承载全部数据 主业务 PG + Monitoring PG 双库隔离
C11 模式切换未定义 collection_mode 状态机 + grace period
C12 MQ 仅概念化引入 durable + quorum + publisher confirm + DLQ 运维流程
C13 未定义分区与保留 Monitoring PG 按 business_date 分区 + 生命周期策略
C14 告警阈值表 告警 + metrics + tracing + 结构化日志

16. 最终建议

V5 适合当前阶段的原因不是“它最强”,而是“它和当前约束一致”:

  1. 和单实例插件现实相符
  2. 和 PRD 的 V1 边界相符
  3. 和用户对“趋势参考”的接受度相符

如果未来要升级到 V6,再考虑:

  1. 专用在线采集节点
  2. 企业版更高采样量
  3. 更细粒度的增量聚合和近实时刷新
  4. 更强的 URL 归因与多次重复采样校准