ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PostHog Notebook 组装实战:把 MCP 工具使用动机分类学发布为可独立交付的分析产物

PostHog Notebook 组装实战:把 MCP 工具使用动机分类学发布为可独立交付的分析产物 PostHog Notebook 组装实战把 MCP 工具使用动机分类学发布为可独立交付的分析产物【免费下载链接】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本文是 PostHog 内部分析技能exploring-mcp-tool-original-user-motive“探究 MCP 工具的原始用户动机”的 Notebook 组装指南。该技能回答“人们为什么使用这个 MCP 工具”——通过从会话开头的工具调用序列重建用户动机、聚类成主题并把结果以 PostHog Notebook 的形式发布。本篇聚焦其中决定成败的一环把已标注的分类学数据组装成一个可分享、可审计、无需凭据的 Notebook。读完你将掌握 Notebook 的细胞编排顺序、计算资源配置、隐私分层的 SQL 写法、帧物化frame materialization上限的底层原理以及一整套经真实运行验证的坑与对策。Notebook 是交付物三条铁律Notebook 是分析流程的最终交付物因此它必须能独立站住脚一个月后打开它的人应该能直接看到语料corpus、看到每个意图intention覆盖了哪些会话、并且可以在不同意某个主题theme划分时不必追问“这是怎么做的”就能自行核查。组装时记住三条铁律Notebook 本身不需要任何凭据。每一个细胞要么是 SQL要么是一小段 pandas 重塑reshape唯一需要模型的那一步——facet 抽取——早在 Notebook 存在之前就已经在你的上下文中完成了见技能步骤 4。细胞必须按依赖顺序排列一个细胞读取另一个细胞的 dataframe就必须等那个细胞先跑完。可分享意味着可被任何人读取任何放进 Notebook 的数据都默认公开给持有链接的人隐私边界由此划定。细胞顺序总览Notebook 的组装顺序如下来自 SKILL.md 步骤 5 的 12 步形状本指南的 references 文档是其细胞级细化notebooks-create-markdown— 标题 一段简短的方法说明notebooks-configure-compute— 4 核 / 8 GB必须在第一个 Python 细胞之前notebooks-add-cell(sql) — 语料每个会话一行携带 caller 与 orgnotebooks-add-cell(sql) — 工具级 caller 占比基于$mcp_tool_call让被过滤掉的自动化流量仍然可见notebooks-add-cell(python) — 每会话一行的 facets以sid为键(sid, starting_intention, theme, data_touched, 第三维度)notebooks-add-cell(sql) — 起始意图表对该 frame 做GROUP BYnotebooks-add-cell(sql) — 主题表同一 frame 再上一级列出每个主题下包含的意图notebooks-add-cell(python) — caller 与 org 按会话展开第二个以同一sid为键的字面量notebooks-add-cell(sql) — 每组织意图分布join 上述两个 frame携带主题notebooks-add-cell(sql) — 浓度检查技能步骤 6notebooks-add-cell(markdown可选) — 示例会话解析为 trace URL见 SKILL.md “Linking an intention to real sessions”notebooks-add-cell(markdown) — 发现与结论以及步骤 3 的偏斜修正要点细胞 6 和 7 不携带 caller 和 org。分类学回答的是“人们来做什么”把人口统计列混进去等于在一张表里回答两个问题两个都答不好。细胞 8 才是两个维度交汇的地方也应该是唯一交汇的地方。第 1 步创建 Notebook —— 先把数字校准再写一个字引言Notebook 的开篇段落会写明语料规模而这是唯一一个之后无法修改的细胞markdown 细胞没有可寻址的node_id数字一旦写错就意味着销毁重建整个 Notebook。因此先让语料查询和 caller-share 查询互相校验只引用你实际分析的窗口内得出的数字SKILL.md 步骤 2 明确指出绝不要从更早的 sizing 查询取数——曾经有一次头部写着 520 会话 / 507 organic而实际窗口只有 237 / 233因为 sizing 查询用的是 30 天、语料用的是 14 天两个数字都是真的只是描述的不是同一件事。创建调用示例notebooks-create-markdown { title: Why people use tool, markdown: Starting-point taxonomy for tool, 90 days to date.\n\nEach sessions goal is reconstructed from its opening tool calls, then clustered on goal text. Clusters below 5 sessions are suppressed. No customer names or raw intents appear below. }返回notebook_id——这是 URL 里的短 id后续所有调用都依赖它。第 2 步在第一个 Python 细胞之前提升计算资源默认配置是 1 核 / 2 GB只能启动内核、做不了多少事对几百个短字符串做凝聚聚类agglomerative clustering会一直卡在那里。所以要在第一个 Python 细胞之前执行notebooks-configure-compute { short_id: id, cpu_cores: 4, memory_gb: 8 }务必“先”做。如果内核已经在运行响应会置restart_required而重启会丢弃所有已物化的 dataframe。这也是为什么技能要求它在步骤 2创建之后、步骤 3第一个 SQL 细胞之前就执行——越早越好。第 3 步语料细胞SQL——可分享版本的查询绝对不要把 SKILL.md 步骤 2 的查询原样粘进这个细胞。那条查询把原始的$mcp_intent文本拼接起来供你自己阅读而 Notebook 是可分享的任何拿到链接的人都能读到细胞返回的任何内容包括其中的客户名、项目 id 和粘贴进来的凭据。步骤 2 的查询只服务于你自己的阅读上下文到此为止。发布版是一个更窄的查询——同一批会话但意图文本被替换为工具名本身-- inside the steps CTE, instead of concat(tool, : , substring(intent, 1, 130)) coalesce(nullIf(toString(properties.$mcp_tool_name), ), toString(properties.tool_name)) AS step其余部分完全相同。发布时用dataframe_name: corpus标题类似 Sessions that reached the tool。结果是sid、caller、org和一个opening_tools序列如execute-sql read-data-schema workflows-list足以审计分类学覆盖了哪些会话且不携带任何客户文本。这是 Clio 式分层隐私的第 4 层也是最容易因为“把上面的查询粘进细胞”而丢失的一层。第 4 步facets 细胞Python——每会话一行以 sid 为键每个会话内联一行以sid为键让 corpus frame 提供 caller 和 org。先澄清一个反直觉的权衡按不同 facet 组合预先聚合会更小——一个 500 会话的语料会塌缩到约 120 行、约 8 KB 源码而每会话一行是 30 KB。但这是错误的取舍一旦分析需要 org组合键就会膨胀回大约会话数因为大多数 org 只持有一个会话。以sid为键字面量可以和任何东西 join转写时也不需要数数。**起始意图是主表。**主题是手工赋值的列不是计算出来的列见下文“为什么分组要手工”。# One row per session: sid|starting intention|theme|data_touched|destination. # # The starting intention is the goal the person held before any tool was chosen. # It is not a recorded property. $mcp_intent records the action an agent took, # so each label here was written by reading that sessions opening calls. # Proper nouns were stripped at that point, not later. DATA a1b2c3d4|build a recurring metrics digest|recurring reporting|1|slack e5f6a7b8|fix a misfiring workflow|maintenance|0|unclear import pandas as pd MIN_SESSIONS 5 COLUMNS [sid, starting_intention, theme, data_touched, destination] facets pd.DataFrame([line.split(|) for line in DATA.strip().split(\n)], columnsCOLUMNS) facets[data_touched] facets[data_touched].astype(int) assert len(facets) 428, fexpected 428 sessions, got {len(facets)} assert facets[sid].is_unique, a sid appears twice kept facets.groupby(starting_intention).filter(lambda g: len(g) MIN_SESSIONS) print(f{len(facets)} sessions, {facets[starting_intention].nunique()} distinct intentions) print(f{kept[starting_intention].nunique()} at or above {MIN_SESSIONS} sessions f({len(kept)} of {len(facets)} sessions kept)) facets这里没有 TF-IDF 步骤。早期版本确实用聚类来构建主题现在它是手工赋值的列因为按 org 的表格读取主题列任何词法层面的错配都会传播进从它派生的每一个数字。下面保留的实测错配是作为“原因”而非“免责声明”呈现的。为什么分组要手工技能只在scripts/audit_intentions.py中嵌入一次意图向量用于捕捉漂移刻意不用嵌入来构建主题。三个原因按权重排序**输入已经是规范化的。**抽取把几百个会话塌缩到一个小的受控词汇表上嵌入本应恢复的语义方差在它们看到数据之前就已被移除。在workflows-create语料上实测105 个意图两两之间的最高相似度只有 0.819——它们本就分得很开。分组是有目的的而嵌入看不到目的。verify a feature flag configuration核对配置属于分析工作因为它在检查产品状态它不属于manage feature flag rollout管理发布因为后者在改变状态。嵌入每次都会把这两个含 flag 的短语放一起。语义上正确分析上错误。**它恢复了那条警示。**嵌入分组比词法分组好得多但依然不可审计一旦 per-org 表格按主题计算任何坏合并都会传播进每个派生数字。两个条件会翻转这个决定意图数量达到几千个时手工分组不再可行如果分类学要成为跨窗口对比的常设报告确定性分组就优于更好但不可复现的分组——否则运行间的漂移会与用户行为的漂移无法区分。**只要有任何东西挂在分组上就手工赋主题不要聚类。**TF-IDF 按共享词合并而非共享语义合并在短规范字符串上会明显出错——它把investigate a production error并进analyze revenue attribution因为都含 investigate把migrate feature flags并进measure feature usage volume因为都含 feature。当聚类只是装饰时可以记录这些错误一旦 caller breakdown 或其他任何切分按聚类计算错误就会传播进每个派生数字就再也不能记录了。抽取已经应用了语义判断来产出意图。用同样的判断把 70 多个短短语分成十几个主题成本很低而且消除了一整类警示THEMES { recurring reporting: [build a recurring metrics digest, report on a launch, ...], automated monitoring: [run a scheduled anomaly scout, ...], } theme_of {intent: theme for theme, intents in THEMES.items() for intent in intents} unmapped sorted(set(facets[starting_intention]) - set(theme_of)) assert not unmapped, fintentions with no theme: {unmapped} assert len(theme_of) sum(len(v) for v in THEMES.values()), an intention appears in two themes在 Notebook 中明确说明分组是手工的。不同意的读者可以退回意图表——无论哪种分组方式意图都是原始单元。MIN_SESSIONS同样适用于意图表。一个只有一次会话的意图就是一个“单例聚类”这正是聚合阈值要防止的东西——即使字符串本身不含专有名词。**在细胞里断言语料规模。**你在把一百多行数好的行转写进一个工具调用漏掉或打错一行会在不出错的情况下改变表中的每一个占比assert len(facets) 428, fexpected 428 sessions, got {len(facets)}这个断言真的抓住过一次失误——漏了一行、四个计数打错表现为 418 而不是 428。每一个百分比都是错的而没有任何东西报错。SKILL.md 还建议在两个手写字面量之间加assert set(population[sid]) set(facets[sid])这是唯一能抓住跨字面量转写失误的检查成本极低。4b把人口数据作为第二个字面量发布而不是 join 查询Python 细胞发布facets以sid为键。caller 和 org 作为第二个 Python 字面量到达同样以sid为键每张人口统计表都是一个 join 二者的 SQL 细胞SELECT p.org, f.theme, f.starting_intention, count(*) AS sessions FROM facets AS f JOIN population AS p ON f.sid p.sid GROUP BY p.org, f.theme, f.starting_intention ORDER BY sessions DESC**两个字面量都是可执行源码所以没有任何不受信任的东西能原样进入它们。**caller 和 org 来自客户端可控的属性一个包含的值会闭合字面量并在细胞运行时执行其后的内容。技能的语料查询把二者约束到安全字符集对其他任何值输出unsafe-caller-value转写那条查询返回的内容绝不要手工放宽它。目标标签走另一条安全路径——是你自己写的。**join 语料细胞是行不通的原因值得在设计前了解。**一个被其他细胞 join 的 SQL 细胞必须物化进 Notebook 内核而物化在自己的上限下运行见 frame_materialize.py50 GB 扫描预算、2 GB 结果上限、50 万行、16 线程。一个把 90 天$mcp_tool_call按 session id 分组的语料查询会撞爆它们细胞以如下错误失败This query exceeds the frame materialization limits (scan or memory budget). Narrow it and re-run.关于这条消息有三件事值得注意**它不说你撞的是哪个预算。**同一个字符串由 ClickHouse 错误码 158TOO_MANY_ROWS、241MEMORY_LIMIT_EXCEEDED和 307TOO_MANY_BYTES映射而来。时间限制和 2 GB 结果上限各有独立消息所以收到这条消息排除了那两个仅此而已。在 frame_materialize.py 中可以看到这组消息与错误码的完整映射_RESOURCE_BUDGET_MESSAGE对应 158/241/307时间预算对应 159/160结果尺寸对应 396。**收窄查询未必有用。**50 GB 很慷慨而对项目内每个会话做高基数的GROUP BY再 joinperson.properties是内存形状而非扫描形状。把语料查询改写成单趟、不带 session 子查询仍然同样失败——这正是内存受限失败的样子。**不要为了腾出余量而收窄时间窗口。**那会改变语料包含哪些会话而标注过的 frame 是固定快照两者从此不再描述同一人群。这个交易永远不值得。语料细胞仍然有价值它是同一两列的、可审计的副本。它只是不能做 join 源。join 细胞里保持可移植 SQLsum(case when ... then ... else 0 end)用min(col)而非any(col)让同一查询无论跑在哪个引擎上都成立。4c把 ClickHouse 细胞钉在绝对时间戳上两个 ClickHouse 细胞——语料和 caller share——都应该用显式的timestamp toDateTime(...) AND timestamp toDateTime(...)范围而不是now() - INTERVAL 90 DAY。标注过的 frame 是读取语料时拍的快照滚动窗口会一直在它下面移动。在workflows-create运行中实时计数在 Notebook 构建的短短几个小时里从 761 漂移到 769而标签保持在 719。头部细胞写明了语料规模而 markdown 细胞之后无法编辑所以漂移一旦出现就无法修复。选取上界时让钉住的查询能复现你标注的计数并在写引言前验证那次运行中精确返回 761 / 734 / 719 / 340 的截断点比 sizing 查询自己的时钟早了两个小时。4d人口统计表有三张表值得发布且它们都不属于意图表或主题表**工具级 caller 占比。**每个 caller 一行表示为全部会话中的占比和 organic 会话中的占比。用基于$mcp_tool_call的 ClickHouse 细胞而不是基于 facets frame这样你过滤掉的自动化流量仍然可见。**每组织意图分布。**来自 4b 的 join携带主题。用它判断一个主题是“模式”还是“一个客户在重复”。**浓度。**技能步骤 6 的检查在 caller 和 org 两个维度上跑。caller 的集中程度远超总计所暗示的这正是重点。在notebooks-create运行中automated monitoring 94% 是 PostHog Desktopproduct analysis 和 feedback review 都超过 60% 是 Claude Codeweb performance 43% 是 plugincustomer and account analysis 68% 是 Cowork 和 Claude.ai 合计、只有 6% 是 Claude Code——销售工作流与工程工作流完全在不同的表面上。要找的结论是存在一个人群还是多个。如果主题按 caller 干净地切分就没有“唯一用户”可设计这一点值得直说。**把unattributed当作缺口而不是 caller。**一个看似集中在那里的主题缺的是埋点而不是人群。把这个说明写在数字旁边否则它会被读成一个发现。org 是两个维度中更可靠的那个。$mcp_organization_id在 90 天workflows-create语料的每个会话上都设置了而 caller 属性只有约三分之二的会话有。当两者对流量集中程度的判断不一致时信 org。**分析表跑在 org id 上而不是名字上。**它们需要知道两个会话属于同一客户不需要知道是哪个客户。如果分析的目的就是决定该找谁谈把名字解析放在一个单独标记为“可识别客户”的细胞里让这些表保持在 8 字符前缀上——见技能中的 “Default to the org id”。第 5 步示例 Trace可选如果分类学需要证据支撑每个意图解析一两段会话到 AI observability trace用技能 Linking an intention to real sessions 里的 join——注意 join 必须从 trace 侧发起$mcp_tool_call不带$ai_trace_id而$ai_generation带$mcp_session_id。每一行都带上会话计数和占比与意图表相同的两列。没有它们表格读起来像每个意图一样常见更重要的是它掩盖了一行是否跨过了聚合下限。**只列出达到MIN_SESSIONS的意图。**在稀有意图下列出带名字的会话正是阈值要防止的事组越小链接越容易识别出某人。这一点很容易做错因为 trace 最有趣的意图往往是最稀有的。这张表的第一版带了 2、3、4 个会话的三个意图只有加上计数列才让问题显形。保留$mcp_session_id和 trace URL把原始$mcp_intent排除在外。id 是不透明的意图则逐字携带客户、项目和产品名。在表里说明链接打开的是什么PostHog 在该会话期间的服务端查询工作而不是用户的对话。第 6 步结论细胞Markdown把占比写成散文从最大的起始意图开始。包含偏斜修正——如果某个关键词过滤器漏了在这里陈述修正后的占比而不是悄悄使用过滤后的数字。如果 rollup 合并了无关意图说明这一点而不是把合并后的聚类当作一个数字引用。为什么配方删掉了聚类步骤早期版本确实对目标字符串跑 TF-IDF 和凝聚聚类来构建主题。下面的实测就是为什么那一步被删除而不是被调优。**摘要步骤已经完成了几乎全部工作。**抽取按设计把每个会话映射到一个小型共享词汇表——一次 499 会话的运行只产生 73 个不同的目标字符串。等到聚类器看到它们时分组已经发生了。它只是第二次、更松散的合并把相关标签并到一起而不是发现结构的步骤。TF-IDF 按共享词而非语义合并在短规范字符串上会明显出错。notebooks-create运行中的真实合并Merged intoWrongly absorbedShared tokeninvestigate attribution trackinginvestigate a production error (4)investigatemeasure feature usage volumemigrate feature flags (1), manage feature flag rollout (1)featurerun a product health reviewevaluate a product for adoption (3)product提高聚类数可以拆开其中一部分但永远修不干净——失败是词法层面的而不是分辨率问题。手工赋值主题列在 70 多个短短语上花几分钟就消除了整类错误。audit_intentions.py源码是整个流程里唯一的嵌入步骤它用text-embedding-3-small嵌入每个不同意图并打印最接近的对默认阈值 0.80是校准过的——105 个意图的最高对只有 0.819阈值再高就什么都标不出来了。它故意不合并任何东西因为语义接近不是合并指令。这印证了“分组靠手、去重靠审计”的分工。Gotchas从真实运行中沉淀的坑**dataframe_name发布的是细胞最后一个表达式而不是它命名的那个变量。**以clusters.sort_values(sessions).head(20)结尾发布的就是那 20 行下游细胞读到的 frame 只有这些。以裸 dataframe 结尾。下游空 frame 会以InvalidInputException: Need a DataFrame with at least one column失败完全指不回这里。参数名在工具之间会变。notebooks-add-cell收notebook_idnotebooks-run-cell-result和notebooks-configure-compute收short_id。同一个值不同的键。第一个 Python 细胞很慢因为沙箱内核正在启动。notebooks-add-cell大约要等 45 秒可能返回status: running用run_id和short_id一起轮询notebooks-run-cell-result间隔几秒。**轮询可能比真相滞后几分钟。**有一次运行 5 秒就完成了notebooks-run-cell-result却继续报running大约三分钟。notebooks-list-frames立刻显示了完成后的 dataframe带真实的列清单和行数。轮询看起来卡住时先查 frames 再考虑notebooks-run-cell-interrupt——打断一个已经成功的细胞会白白损失内核状态。**一个表达式里求两次any()不会返回同一行。**写成multiIf(any(consumer) ! , concat(consumer:, any(consumer)), ...)的 caller 列产生了带空值的前缀consumer:——条件看到了非空行分支却看到了空行。在内层查询里计算聚合把multiIf放到外层SELECT。**notebooks-add-cell对 markdown 细胞收markdown不收code。**传code会以A markdown cell requires non-empty markdown失败。SQL 和 Python 细胞收code。**Python frame 活在内核里它消失后 join 它的 SQL 细胞就会失败。**错误是Input registration failed: local frame facets is not in the kernel — run the node that creates it first即使 Python 细胞已报done且带行数也会出现因为 frame 没活到 join 那一刻。notebooks-list-frames对此是诚实的frame 真正在的时候才列出它。重跑 Python 细胞再跑 SQL 细胞并读更新响应里的stale_dependents看还有什么需要重跑。如果细胞挂起notebooks-run-cell-interrupt可以清掉它。内核已进入stopped但运行仍显示running时必须先用 interrupt否则任何东西都不会继续执行。用notebooks-update-cell迭代而不是每次改查询就新增一个细胞——否则 Notebook 会积累死尝试读者得一路翻过去。注意notebooks-update-cell只收code所以发布后再重命名 frame 意味着删除细胞重新添加。前置工作回顾这些数据从哪来本指南假设你已完成技能的前置步骤用 SKILL.md 步骤 2 的语料查询读取会话开头调用4~5 个调用是工作默认值opening列携带客户文本、只读不发布、在步骤 3 检查偏斜用 caller 拆分而非意图关键词、在步骤 4 为每个会话抽取 facet。抽取可用 extract_facets.pygpt-4.1-mini、8 并发、固定响应 schema注意其 docstring 要求只对已同意 AI 数据处理的组织运行因为opening列是客户撰写的文本会发送给第三方模型与后端 intent_generation.py 的is_ai_data_processing_approved门槛对齐或自己逐会话阅读——技能基于实测推荐后者脚本式抽取在 500 会话上产生 487 个不同标签且把“修复一个误触发的 workflow”37 会话、7.4%压成“更新 workflow 内容”4 会话、0.8%。第三个 facet 的选择参考 facet-schemas.mdnotebook_role / destination / edit_scope / depth 等按工具形态选。组装完成后Notebook 就是最终答案语料可审计、意图是原始单元、主题是手工赋值、每个占比都有断言兜底、任何访客无需凭据即可复现阅读——这正是“为什么有人使用这个工具”这个问题的、可交付的、诚实的答案。【免费下载链接】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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进