PostHog Signals 异常检测 Scout 的持久化记忆设计:Watchlist 台账、Explore-vs-Exploit 与 Scratchpad 内存约定
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
导读
signals-scout-anomaly-detection是 PostHog Signals 体系中的一只异常检测 Scout,它持续监控团队最常查看的 Dashboard 与 Insight,并针对最新完整数据桶相对其季节性基线的偏离发出告警。它无法在一次运行中扫描整个项目,其核心杠杆在于一份持久化的 watchlist(观察清单):每次运行按预算拆分为exploit(利用:复查到期项目)与explore(探索:补充新项目),让覆盖率随运行轮次不断复利累积,而不是每次冷启动。本文档即这份账本(ledger)与其背后内存约定的完整设计——读完你将掌握 scratchpad 键位词汇表、条目模式(schema)、轮询调度、退役与四态分类器等全部约定,并能直接复刻到自己的 Scout 技能中。
关联文档位于 watchlist-and-memory.md,是这只 Scout 的"脊梁",建议与技能主文件 SKILL.md、评分方法 anomaly-methods.md、报告契约 report-contract.md 一起阅读。
为什么需要一份持久化账本
一个繁忙项目拥有的 Dashboard 与 Insight 数量,远超单次短运行能够完成评分的范围。如果不做任何记忆,每次运行都会从零开始:既不知道上一次查到了什么,也无法区分"新出现的异常"与"早已上报过、仍在持续的异常"。
Watchlist-and-memory 设计给出的答案是一条核心原则:"scratchpad(便签板)是唯一的持久化层",Scout 把一整套账本维护在 scratchpad 上,并将每次运行拆分为两半:
- Exploit(占运行主体):复查 watchlist 中到期的项目——每日项目约 24 小时复查一次,小时项目约 1–3 小时复查一次;按"超期最久优先"排序,逐个对照基线打分,直到时间预算耗尽。
- Explore(占运行的一个切片):通过趋势榜单与最近访问的 Dashboard 发现少量新的高价值项目,让覆盖率跟上团队当前真正关心的内容。
二者缺一不可:只 exploit 会让清单逐渐过时,只 explore 则永远不做跟进。这正是 scout-patterns.md 中"Watchlist explore/exploit"模式的规范实现,其 canonical 例子就是本 Scout。
Scratchpad:唯一的持久化层
Scratchpad 是按团队隔离、以字符串为键的持久化散文存储(per-team prose keyed by string)。没有标签(tags)、没有 TTL(默认为永久有效):
- 类别(category)就是键前缀(key prefix):后续运行只需一次
text=搜索即可找到某类条目。 - 复用键名会原地重写该条目(幂等刷新):可以用它更新基线或
last_checked时间戳,而不会产生重复条目。
从仓库后端实现可以印证这套语义。scratchpad.py 中的remember()是基于(team, key)的幂等 upsert:已存在则只更新content/expires_at/updated_at,并保留最初的created_by_run溯源;forget()按精确键删除(返回是否真的删除了东西);search_scratchpad()对key与content做 ILIKE 子串匹配。存储模型 SignalScratchpad 的字段约束与此一一对应:key为 300 字符、按团队唯一;content为无界 TextField(写端钳制在 50,000 字符);expires_at为可选的 TTL(不传即为永久)。搜索默认上限 20、最大 1000,这正是 SKILL.md 中"默认 limit 是 20,必须传高 limit,否则老旧到期项会被挤出视野"这一告诫的根源。
补充说明:
expires_at在 PR2 简化评审时曾被移除,后来因"写了临时记忆的 Scout 几乎不会回来 forget"而恢复——过期条目先对search_scratchpad隐藏,超过宽限期后由每日 janitor 硬删除,而expires_at为空的条目永远不会被清扫。这只异常检测 Scout 的所有账本键位默认都是持久条目,恰好落在"durable by default"的语义上。
键位词汇表(Key vocabulary)
这是整个账本的核心契约。类别编码在键前缀中,一次text=搜索即可命中。完整词汇表如下(必须完整保留,勿缩水):
| 键前缀 | 存放内容 |
|---|---|
watchlist:anomaly_detection:insight:<short_id> | 一条精选的待观察 Insight(账本行,schema 见下文)。 |
watchlist:anomaly_detection:dashboard:<id> | 一个精选的整页 Dashboard(当其所有磁贴被集体视为关键时进行整页清扫)。 |
watchlist:anomaly_detection:importance-refresh | 备忘:watchlist 的重要性排序上次在何时被重新校准,以及改变了什么。 |
baseline:anomaly_detection:insight:<short_id> | 已学习的"正常值":每个季节性桶的中位数 + MAD,使评分保持廉价。 |
report:anomaly_detection:insight:<short_id> | 指向你为此异常撰写的收件箱报告的指针:report_id+ 重新升级(re-escalation)条件。当一条 Insight 同时携带真正不同的并发异常(多序列 / breakdown,或反向移动)时,追加:<series-or-direction>后缀,避免它们坍缩到同一份报告。 |
retired:anomaly_detection:<suffix of the retired row> | 对某个已停止评分的 watchlist 项目的墓碑(tombstone):为什么停止,以及什么条件下会把它加回来。一行即可。 |
reviewer:anomaly_detection:<area> | 缓存的负责人:某个 Dashboard / 指标区域的裸小写 GitHub 登录名。 |
noise:anomaly_detection:<topic> | 一个需要忽略的模式(长期剧烈波动的 Insight、季节性怪癖)。 |
addressed:anomaly_detection:<topic> | 团队已确认的预期事件(发布/回填)或已修复——跳过。 |
allowlist:anomaly_detection:insight:<short_id> | 一个永远不向外呈现的 Insight(已弃用、沙箱、测试)。 |
not-in-use:anomaly_detection:team{team_id} | 收尾备忘:团队当前并未积极使用已保存的分析。 |
这套键位与 SKILL.md 的"Save memory as you go"一节完全一致:类别编码在前缀中,未来运行用一次text=搜索即可找到。在"快速收尾"场景中,not-in-use:anomaly_detection:team{team_id}支持以相同键幂等刷新时间戳,避免团队长期无人查看时反复做无用功。
Watchlist 条目 schema
每条watchlist:条目应保持为紧凑、可解析的一行,使下一次运行可以廉价地读取与更新:
key: watchlist:anomaly_detection:insight:ym0K91uz content: "Revenue over time | dashboards: go/revenue(198672) | metric: daily revenue sum | cadence: daily | priority: high | last_checked: 2026-06-07T12:00Z | next_due: 2026-06-08T12:00Z | last_status: normal (z=0.8) | added: 2026-06-05"字段语义:
- 人类可读名称(human name):如 "Revenue over time";
- 所在 Dashboard:如
go/revenue(198672),便于跳转与关联; - 实际打分的指标(metric):如
daily revenue sum; - 节奏(cadence):
hourly/daily,由 Insight 定义与数据量推断而来(详见 anomaly-methods 的节奏选择表); - 优先级(priority):
high/med/low,依据视图数 + 业务重要性得出; last_checked:上次检查时间;next_due:下次到期时间;last_status:normal/watch/reported(并附最近一次 z 值);对还无法建立基线的项目,low-data也是合法取值;added:加入日期。
last_checked与next_due这对时间戳正是 round-robin 调度的载体:SKILL.md 中明确"只评分最新完整数据桶"、"复查到期项、最超期者优先",而账本让不同轮次覆盖不同项目,避免每小时的运行都重复检查同一批头部 Insight。
Baseline 条目 schema
基线条目让下一次运行无需重算整个基线即可对最新数据桶评分:
key: baseline:anomaly_detection:insight:ym0K91uz content: "daily revenue sum, same-weekday baseline over 8 weeks (computed 2026-06-07): Mon median ~$X MAD ~$Y; Tue median ~$X MAD ~$Y; ... weekend lower. Refresh weekly."存储的信息量要足够下一次运行直接打分,但约每周从新鲜数据重新推导一次,使基线跟随真实漂移而非日渐陈旧。这对应 anomaly-methods 中"基线窗口"表(每日 6–8 周、同日星期匹配;每小时 2–4 周、同时段匹配)的落地形态:基线不是一次性的快照,而是周期性再推导的活数据。
Explore vs Exploit:每次运行的预算分配
Exploit:复查到期项(占运行主体)
- 复查 watchlist 中到期的项目:每日项目
next_due已过(约 24 小时节奏),小时项目已过约 1–3 小时节奏。 - 按最超期优先排序,自上而下工作,直到时间预算接近耗尽。
- 边查边更新每个项目的
last_checked/next_due/last_status。 - 异常正是在这里被真正捕获的。
Explore:补充新项目(运行的一个切片)
新增少量高价值项目,使覆盖率跟随团队当前关注点:
insights-trending-retrieve——days=7用于稳定常青项目,days=1用于当下热门。高view_count是"团队关心"的首要信号。recent_dashboards(来自 profile)+dashboard-get磁贴 —— 最近被访问的 Dashboard 上的 Insight 具有关联高价值。- 与已有 watchlist 交叉比对,只添加真正的新项目,每次运行至多约 2–3 个,每个都附带首条基线与节奏。
每几天刷新一次重要性——清单变大不等于"完成"
发现不只是新增项目;清单的成员与优先级会随团队焦点转移而过时。约每 3 天,把重要性排序本身当作要复查的对象:
- 重新拉取
insights-trending-retrieve(days=7)与recent_dashboards,与已有 watchlist 对账——不只是"添加",更要重新排序与修剪:把视图数上升的项目priority调高,把 Dashboard 不再被访问或视图数崩塌的项目退役。上周创建、如今打开次数最多的 Dashboard 应当上榜;一个月无人打开的不该继续消耗预算。 - "冷"需要正面证据,而非"缺席"。这两次读取都是 top-N——trending 默认为 10,
recent_dashboards只装最近访问的——所以一个项目缺席可能只是它排在第 11 名。退役前要调高 limit 并检查项目本身。在有界列表中的缺席意味着"未知",不是"冷"。 - 无论看起来多冷,以下条目永不退役:带有活跃
report:指针的项目(你仍欠它下文四态所需的复发检查,且它的基线正是衡量已上报移动是否持续的尺子);以及importance-refresh备忘本身(节奏要运作,它必须存活)。 - 通过处理真正变冷的项目来收尾刷新。在备忘里写下"活跃可评分集合"不是修剪——每条冷行都存活,账本继续增长而备忘却说覆盖率良好。要真正退役你判定为冷的行(见下文"退役条目"),使账本与备忘一致。在稳定清单上"本轮回合未退役任何条目"是完全正当的结局,在备忘里写明即可。绝不为了显得刷新有成果而退役活跃条目。
- 清单很大不是跳过的理由。"清单已经成熟"是陷阱:它把覆盖率冻结在 bootstrap 时重要的内容上。刷新很廉价——两次读取加一次 diff——正是它让"重要"保持"当下"的含义。
- 让它真正发生:维护一条
watchlist:anomaly_detection:importance-refresh备忘,带last_refreshed时间戳与一行"改了什么"的说明。若它缺失或超过约 3 天,本轮在 exploit 之前先做刷新,然后复用同一键名原地更新。与每周基线再推导一样,这防止清单过时——但运行得更频繁,因为团队的注意力变化快于指标自身的分布。 - 一条备忘,一个键名——绝不加日期后缀(如
…:importance-refresh:2026-06-18)或用第二种写法。带日期的键无法原地重写,每次刷新都会留下一行残留,你想找的备忘越来越难找。这与remember()的幂等 upsert 语义直接呼应:只有固定键名才能"覆盖即更新"。
Round-robin:不要每次全量重扫
watchlist +next_due时间戳正是让连续多轮覆盖不同项目的机制,而不是每小时的运行都重复同一批头部 Insight。信任账本:如果某项目 20 分钟前被前一轮检查过,它现在就不到期。
给自己留指针
预算耗尽于扫描中途时,快速写一条笔记(复用运行摘要,或将下一条目的next_due设为"now"),让下一轮知道从哪继续。运行摘要(scout-runs-list)是天然的落点:写下"已检查 A–F;G–K 下一轮仍到期"。这与 SKILL.md 的"Close out"一节一致:摘要由 harness 保存,未来运行通过scout-runs-list读取。
退役条目(终结态)
不再评分的项目必须离开账本。priority: low不是终结态——它让该行继续出现在每次watchlist:搜索、round-robin 与真正被评分项目的路径中。退役按以下顺序执行:
- 先写紧凑的
retired:墓碑(形状见下)。三步调用彼此独立,因此中途崩溃的运行留下的是一条记录,而不是覆盖率上无声的洞。 scout-scratchpad-forget掉watchlist:anomaly_detection:…行。scout-scratchpad-forget掉该行拥有的baseline:anomaly_detection:…条目。基线比使用它的行活得更久,因此基线是第一个撑爆账本的东西——只退役 watchlist 行只是把臃肿挪了个位置。只删除"仅被退役行使用"的基线;Dashboard 行的磁贴可能与仍在评分的 Insight 行共享基线。
key: retired:anomaly_detection:insight:ym0K91uz content: "Revenue over time — retired 2026-06-14: the go/revenue dashboard hasn't been opened in ~6 weeks and its view count fell 412 → 3. Re-add if it returns to the trending ranking."墓碑的键后缀与被替代行相同。它存在的意义是让下一次 explore 不把刚退役的项目再加回来——一行即可,不需要图表,不需要结论式散文。它与allowlist:不同:allowlist:表示"永远不要呈现这个"(已弃用 / 沙箱 / 测试);而retired:项目是正当的,只是今天不值得消耗预算。
在 explore 提出项目时搜索retired:,而不是在 orientation 阶段——否则墓碑会挤占你上限受限的读取,这正是整套约定要防止的失败。当退役项目确实赢得回归资格时,写入新的watchlist:行与基线的同时scout-scratchpad-forget掉墓碑,让账本绝不同时声称两种状态。
这条规则适用于每一条 watchlist 行,不只 Insight 键位的:一次完成的调查、一周某天的清扫、或已完成使命的 bootstrap 队列,都以同样方式退役——运行日志不是账本行。复盘文案应放在运行摘要(scout-runs-list)里,而不是watchlist:键下(round-robin 会不断把它捞起来)。importance-refresh备忘是唯一永不退役的watchlist:行。
保持工作集可扫视。orientation 搜索有上限,无界账本会把到期项目静默地藏在 limit 之后——这正是本约定要防止的确切失败。若某次watchlist:搜索返回的行数超过你几轮都评不完的量,多出来的部分是积压而非覆盖率:把最冷的退役,直到放得下。丢掉你自己的、超过约 90 天的retired:墓碑,以及窗口已过的带日期一次性条目。
只忘记自己拥有的键。scratchpad 是与所有其他 Scout 共享的团队级键空间,而scout-scratchpad-forget按精确键删除、不检查写入者是谁。把所有删除限制在本技能写入的anomaly_detection键内。另一个 Scout 的 cursor 或dedupe:行是它进行中的工作,不是你的积压——移除它会让那只 Scout 重复上报或丢失位置。这一点在 ARCHITECTURE.md 的SignalScratchpad一节有更广的表述:"一个键空间,许多写入者:团队内任何 agent 都能覆盖任何键,这正是为什么每个写提示都要求先搜索键并压缩、而不是盲写,也为什么每个读取者都把条目视为不可信输入。"
四个状态:上报前先分类
每次候选异常在进入上报通道前,必须对照先例、收件箱与 scratchpad 分类为以下四态之一(完整分类器见 watchlist-and-memory.md 与 SKILL.md 的 "Decide" 一节):
- Net new(全新)—— 没有先前的报告或 scratchpad 条目覆盖这次指标移动。若它越过门槛(稳健 z ≥ ~3.5、guard 通过、排除了季节性),则撰写报告(
emit_report),并存放带新report_id的report:指针。 - Material update(实质性更新)—— 你已经上报过这条 Insight 的异常,但出现了新证据(仍在触发、升级了、扩散到相关 Insight、或与新的部署相关)。则编辑既有报告(
edit_report):用append_evidence追加新观测(为新的时间窗口链接一份新 notebook)。不要为同一移动撰写第二份报告。 - Already covered(已被覆盖)—— 报告存在且移动未变,仍在窗口内。→ 跳过;可选地原地刷新
report:指针的备注。 - Addressed or noise(已解决或噪音)—— 某条
noise:/addressed:/allowlist:条目点名了它(长期剧烈波动的 Insight、已知的发布/回填、已弃用的 Insight)。→ 跳过;在摘要中记录。
这四态与 report-contract.md 的上报通道纪律咬合:上报前先搜索收件箱(inbox-reports-list,配合report:指针),通道不是幂等的,绝不撰写重复报告;复发时优先编辑既有报告而非新建;report:指针按稳定的short_id键控(无日期),再次确认会原地更新同一指针。
工作记忆示例(Worked memory examples)
好的条目对"未来运行"可执行——下一次运行读到它就会改变行为。以下三个是好条目,一个坏条目是反例:
key: report:anomaly_detection:insight:SRVNODib content: "report_id 0192f3a1-... — authored 2026-06-07 for the spike on 'LLM Costs By AI Product': daily sum 3.4x the 8-Saturday baseline (z=5.1), started 06-06. If still elevated next run, use `edit_report(append_evidence=...)` to escalate as sustained; if back within baseline, leave the report and stop."这条report:指针同时携带report_id与重新升级条件——下一次运行据此决定是edit_report升级为持续异常,还是回到基线后"留报告、停止"。注意它落在 SKILL.md 强调的规则上:report:按稳定short_id键控(无日期),并仅在一条 Insight 承载真正不同的并发异常时才加:<series-or-direction>后缀。
key: noise:anomaly_detection:insight:tQnsSMoI content: "'Generation calls' is chronically spiky — big legit swings on model launches and backfills. Require z>=4.5 AND a same-day deploy/launch correlation before reporting; otherwise refresh baseline only."noise:条目不只是一句"忽略它",而是编码了新的触发条件(z ≥ 4.5 且需当日部署/发布相关),把误报源转化为可执行规则。
key: addressed:anomaly_detection:revenue-backfill-2026-06 content: "Revenue insights show a one-off step on 2026-06-03 from a Stripe backfill, not a real change. Team aware. Don't report revenue-series steps dated 2026-06-03."addressed:条目把"团队已知的预期一次性事件"(Stripe 回填导致的台阶)固化为跳过规则,防止未来运行把它当真实变化上报。
坏条目反例:键note-1,内容 "revenue looked weird today"——没有实体、没有条件、没有类别前缀,既不可查找也不可执行。这正是键位词汇表与紧凑 schema 要防止的东西。
冷启动一个没有历史的团队(头几次运行)
第一次运行没有 watchlist。按以下方式引导:
insights-trending-retrieve(days=7,limit=15)→ 团队观看次数最多的 Insight。- 从 profile 读
recent_dashboards→ 人类实际打开的 Dashboard。 - 从中挑出约 5–10 个最高价值的,为每个设置基线与节奏,写入它们的
watchlist:条目。 - 本轮把时间允许范围内的都打上分;其余的下一轮到期。
到第 3–5 轮左右,你将拥有一份稳定清单,且每轮的大部分时间都花在对存储基线的快速廉价复查上——这正是整套设计的全部意义。探索阶段的工具组合(insights-trending-retrieve、recent_dashboards+dashboard-get、必要时dashboards-get-all/insights-list/execute-sql查system.dashboards/system.insights)在 SKILL.md 的 "Explore" 一节有完整清单。
从源码看这套约定的实现支撑
这套文档约定并非空中楼阁,而是与仓库中的实际实现一一对应:
- Scratchpad 语义:scratchpad.py 的
remember()/forget()/search_scratchpad()分别实现了幂等 upsert、精确键删除、以及content+key的 ILIKE 检索;DEFAULT_SCRATCHPAD_SEARCH_LIMIT = 20与MAX_SCRATCHPAD_SEARCH_LIMIT = 1000正是 SKILL.md 告诫"默认 limit 只有 20"的源码出处。 - 存储模型:SignalScratchpad 以
(team, key)唯一约束、content无界(读入未来运行提示)、expires_at可选 TTL(默认永久)——与"类别是键前缀、durable by default、无 tags 无 TTL"的约定完全吻合。 - 模式地位:scout-patterns.md 将 "Watchlist explore/exploit" 单列为一档模式,明确指出"这是唯一自带参考文档集的特化 Scout,完整处理见
signals-scout-anomaly-detection/",并概括其要点:watchlist:<domain>:<id>条目携带 last-checked 时间戳与逐项基线;"只 exploit 会过时,只 explore 从不跟进"。 - 评分与上报接口:exploit 阶段的主力评分器
alert-simulate、SQL 回退评分、以及emit_report/edit_report通道的字段契约,分别在 anomaly-methods.md 与 report-contract.md 中有完整细节;四态分类器中的 "Material update → edit_report" 与报告契约中的"复发即编辑、绝不重复上报"互为表里。
小结
Watchlist 与内存约定是这只异常检测 Scout 得以"越跑越聪明"的根:scratchpad 以键前缀承载类别、以固定键名支持幂等原地更新;exploit/explore 的预算拆分让覆盖率随时间复利;retired:墓碑与"只忘记自己拥有的键"防止账本臃肿与跨 Scout 误删;四态分类器让每一次候选异常都先对先例与记忆归类再决定上报、编辑还是跳过。对任何想要构建"watchlist 式"监控 Agent 的读者,这套键位词汇表、条目 schema 与调度纪律都是可直接照搬的成熟范本。
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考