
PostHog 离线评测实战基于 MCP 的 Markdown 笔记本 Agent 评测体系搭建指南【免费下载链接】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导读本文基于仓库内products/notebooks/plan/notebooks_mcp_evals_plan.md这份评测规划文档系统讲解 PostHog 如何为「基于 MCP 工具创建与编辑 Markdown 笔记本」的 AI Agent 建立离线评测offline evals体系。你将理解评测基建eval harness如何与 MCP server、Temporal、kernel 沙箱协同工作如何设计创建型与编辑型评测用例eval cases如何用「工具轨迹 → 数据库终态 → LLM 裁判」三层评分栈衡量 Agent 行为以及如何编写 seeder 与 synthesizer 让评测数据可复现。文中的目录与代码均已按当前仓库实际实现核对可对照阅读。1. 评测体系的底座独立 eval harness 与两种套件类型PostHog 的 Agent 离线评测并不运行在 pytest 之上而是由一个独立的评测框架承载——位于 products/posthog_ai/eval_harness/ 的 standalone harness。其运作方式是hogli evals一次性启动共享基础设施测试数据库、Django live server、LLM gateway、MCP server、Temporal、personhog然后并发运行所有被选中的套件以 Braintrust 作为评测引擎。套件分两种类型由模块级常量SUITE_KIND声明见 harness/requirements.py套件类型声明方式每个用例的执行方式启动的基础设施sandboxed默认不声明或SUITE_KIND SuiteKind.SANDBOXED真实编码 Agent 在真实沙箱中驱动 MCP server全部基础设施one-shotSUITE_KIND SuiteKind.ONE_SHOT一次进程内模型调用测试数据库、personhog、demo 数据Notebook MCP 评测属于 sandboxed 类型被测对象是「驱动真实工具、操作真实状态的 Agent」一次性的单次调用根本无法覆盖这种行为。这也是整个评测设计的出发点。围绕这套体系规划文档强调了几条塑造设计的关键事实均已在实际代码中得到印证按约定发现套件无注册表产品自有套件放在products/product/evals/注意是复数evals/单数products/signals/eval/是无关的 pytest 树套件 ID 格式为product/module::fn导入路径为products.product.evals.module。本评测的目录即为products/notebooks/evals/。一个套件 一个 Braintrust experimentexperiment_name是历史键history key改名等于重置跨轮次对比。现有套件统一保留-cli后缀以对齐历史上 MCP 的cli表面。每个用例拥有独立组织/团队/用户每个 case 都会从一个 master Hedgebox 团队克隆出自己的 org/team/user用例之间永远看不到彼此的状态预置用户名为 Karen Smith。Hedgebox 数据集在固定种子下可字节级复现因此评测断言应检查形状和相对日期范围绝不要断言绝对计数。case 字段name、prompt、expected按 scorer 的_name()键控、metadata、setup即 seeder。seeder 的返回值会进入output[seed]这是 scorer 获取预置 ID 的唯一通道。harness 会给每个 experiment 自动添加ExitCodeZeroscorer见 scorers/deterministic.py套件自身不得重复声明否则会被拒绝。沙箱评测不进 CI.github/workflows/ci-ai.yml只运行较旧的ee/hogai/eval/cipytest 树且需要evals-readylabel本评测按需在本地或 Modal 上运行。想了解 harness 内部运行细节可阅读 eval_harness/README.md 与 harness/README.md。2. 被测表面revamped-py-notebooks旗标下的工具集Notebook 评测的目标表面是 MCP 中的 notebook 工具集合。在revamped-py-notebooks功能旗标开启后注册的是以下工具hand-written 为手写工具generated 为从 OpenAPI schema 脚手架生成工具类型说明notebooks-create-markdownhand-written标题 可选正文明确拒绝携带 cellsnotebooks-add-cellhand-writtensql/python/markdown/saved_insight/componentsql 与 python 会派发运行并把结果写回文档notebooks-update-cellhand-written编辑代码并重跑或省略 code 原样重跑返回stale_dependentsnotebooks-delete-cellhand-written—notebook-edithand-writtenMarkdown 字符串替换old_markdown/new_markdown/replace_allnotebooks-getgenerated标题、markdown、cells、depends_on/dependents、派生的 stale 状态notebooks-list-frames、notebooks-configure-compute、notebooks-run-cell-result、notebooks-run-cell-interruptgeneratedkernel 侧工具的定义与描述可以在 products/notebooks/mcp/tools.yaml 中直接查看——例如notebooks-get被标记为feature_flag: revamped-py-notebooks而notebooks-create采用feature_flag_behavior: disable且superseded_by: notebooks-create-markdown说明新旧两套工具是互斥关系。旗标关闭时注册的是旧版工具集notebooks-create、notebooks-retrieve、notebooks-partial-update而所有 markdown 工具直接消失。两套工具集互斥这一点直接影响下文 harness 的改造方式。3. 已验证的环境阻塞点让评测跑通的关键改造规划文档列出了让 Notebook 评测真正可运行的五个阻塞点及其解决方案其中大部分已经在 harness 中落地a. Markdown 工具通过旗标覆写到达评测 MCP server。工具过滤tool filtering以revamped-py-notebooks门控整个 markdown 工具集测试见 services/mcp/tests/unit/tool-filtering.test.ts而评测 MCP server 在FEATURE_FLAG_OVERRIDES中设置该旗标。实测代码位于 harness/services.py其覆写字典为{revamped-py-notebooks: True, ...}。副作用是它同时把notebooks-create/notebooks-retrieve从评测工具列表中移除因此若想评测旧版 notebook 工具需要另一种杠杆见第 9 节「开放决策」。b. Django 侧的门控已经满足。harness/lifecycle.py 会 patchposthoganalytics.feature_enabled使其在整个运行期间返回True因此受旗标门控的 SQLV2 端点可以正常应答。c. 两条 cell 运行通道目前仅 docker 可用。纯 HogQL 运行走直连通道direct laneenqueue_direct_run位于 backend/presentation/views/notebook.py挂载在异步查询管理器上。harness 以TEST1运行此时 Celery 是 eager 模式查询内联执行不涉及沙箱。Python 与 DuckDB 运行调用start_sql_v2_run_workflow需要 notebook 的 Temporal 工作流已注册、且存在 kernel 沙箱。harness 现在两者都做到了详见 eval_harness/README.md#notebook-kernel-sandboxes在这项改造之前运行会一直停留在running状态直到轮询窗口过期。kernel 通道仅 dockernotebook kernel 直接从SANDBOX_PROVIDER读取后端而 modal provider 将其设为MODAL_EVALS这不是KernelRuntime.Backend的合法值。因此python/duckdb 用例必须用--provider docker运行纯 SQL 用例两种 provider 均可。d. Scope 没有问题。沙箱上下文默认为fullMCP scopes见 custom_prompt_internals.py其中已包含notebook:write。e. 终态评分可行但必须异步。Braintrust 基类的_run_eval_async会在事件循环上直接调用_run_eval_sync因此任何在 scorer 里做同步 ORM 操作的行为都会触发 Django 的异步安全守卫。读取数据库的 scorer 应继承AsyncOnlyScorerMixin, Scorer见 scorers/contract.py并把数据库工作放进asyncio.to_thread。仓库中已有先例DuplicateUniqueFlagKey位于 products/posthog_ai/evals/experiments/scorers.py。前提是 seeder 必须返回team_id否则终态评分无从谈起。4. 用例选型用 MCP 分析数据锚定「会回归的行为」用例选择的原则是从工具实际行为出发而非追求覆盖面上的面面俱到。规划文档建议在选型前先用 MCP 分析工具查看 notebook 工具的真实使用数据query-mcp-tool-stats与query-mcp-tool-failures或 MCP analytics UI。规划撰写时的观测结果很有参考价值notebook-edit在所有编辑工具中错误率最高且多数失败是晦涩的internal错误——遥测中抛出的Error的表现形态通常意味着old_markdown未找到或不唯一。这是最高价值的目标也是唯一具有真实失败签名可复现的用例。notebooks-add-cell使用量尚小评测在这里是「在大规模使用前锁定行为」而非救火。旧版notebooks-partial-update主要失败在 HTTP 409 版本冲突。值得长期坚持的选型原则每个可合理回归的行为一个用例而非每个工具一个用例。优先选择「错误答案易检测、且在生产中代价高昂」的用例例如覆盖clobber文档、丢失 cell 结果、重建已存在的 insight。至少包含一个「正确行为是不调用任何工具」的用例以及一个「工具拒绝首次尝试、Agent 必须恢复」的用例——这两类能捕捉纯成功用例漏掉的 prompt 回归。已落地的 Suite 0eval_notebook_cells通道检查规划文档中标记为「已发布」的套件实际实现见 products/notebooks/evals/eval_notebook_cells.py。它包含三个用例python_cell_arithmetic最便宜的 kernel 通道冒烟测试无查询、无 dataframe、无 cell 间依赖。若它通过而其他用例失败说明沙箱没问题、是 Agent 的分析坏了。sql_cell_report证明直连通道——要求创建名为 Signup momentum 的 notebook、用 SQL cell 返回最近 8 周signed_up事件的周计数并加一段 markdown 摘要。expected中的cell_runs_completed.node_types为[hogql]。python_cell_from_dataframe端到端证明 kernel 通道——先加 SQL cell 取周计数再加 Python cell 读取该 cell 的 dataframe 计算周环比变化。node_types为[hogql, python]两条通道都覆盖。值得注意的评分设计CellRunsCompleted评的是NotebookNodeRun行而非对话记录因为「已派发的运行」和「已完成的运行」在日志里看起来几乎一样——工具调用返回的要么是status: running要么是已完成。只有 run 行才是 notebook 真正渲染内容的诚实来源。完整 scorer 实现见 products/notebooks/evals/scorers.py。提案套件 Aeval_notebook_authoring从分析请求创建用例Prompt 形态评分要点create_signup_reportCreate a notebook called X showing weeklysigned_upcounts over the last 8 weeks, with a short summary使用notebooks-create-markdown添加一个完成的 SQL cell命名标题添加 prosemulti_cell_dependency两个相关 SQL 步骤后者读取前者的 dataframedataframe 命名、refs、依赖顺序embed_saved_insightPut our Weekly signups insight in a notebook通过saved_insightcell 复用预置 insight而不是重写 SQLcomponent_hogql_rejected诱导 Agent 使用 HogQL source 的componentcell工具拒绝该尝试Agent 恢复为cell_type: sql提案套件 Beval_notebook_editing修改既有 notebook 且不破坏它用例Prompt 形态评分要点local_prose_editReword the intro section of notebook X只编辑目标片段其余每个 section 与两个 cell 的runId/result原样保留insert_cell_afterAdd a breakdown cell right after the signups cell使用after_node_id而非在末尾追加delete_one_cellDrop the file-size cell恰好删除一个 cellrefresh_stale_cells预置了一个上游 cell 已变更的 notebook读取notebooks-get用notebooks-update-cell且不带code按依赖顺序重跑 stale cells八个 sandboxed 用例是合理的第一批规模每个用例都是一次完整的 Agent 运行成本线性增长20 个用例的套件会变得难以迭代。5. 数据生成synthesizer 与 seeder 的分工Hedgebox 提供事件、insights、dashboards 和 flags但不提供 notebooks。因此编辑类和定向类用例需要 seeder 来预置数据。规划文档建议沿用data_warehouse的 synthesizer/seeder 分工仓库当前目录products/notebooks/evals/已按此结构落地products/notebooks/evals/ __init__.py constants.py # 共享字面量标题、锚点、node id、dataframe 名 synthesizer.py # 纯确定性 markdown 文档构建器 seeders.py # 把数据安装进每个用例的团队ORM[已发布] scorers.py # 轨迹 终态 裁判评分器 [已发布] eval_notebook_cells.py # 两条运行通道 [已发布] eval_notebook_authoring.py eval_notebook_editing.pysynthesizer.py纯 Python、不依赖 Django、确定性。把 markdown 文档构建为 frozen dataclasssection 锚点、固定nodeId的 cell 标签、dataframe 名以及用例必须找到的预置「needle」。单元测试应放在products/notebooks/backend/test/绝不放进evals/树内。以其中的build_churn_needle为例默认种植 6 个账户count6oldest_active_day120、newest_active_day70、session_stride_days4——即账户在 120 天前注册、持续活跃到 70 天前、此后完全沉默形成教科书式的流失形状。所有标识符distinct_id、account_key、email、name都携带CHURN_TOKEN hollowbrook标记。seeders.py接收CustomPromptSandboxContext在用例自己的团队里创建Notebook行内容等价于buildMarkdownNotebookContent(markdown)对 staleness 用例还会创建NotebookNodeRun行让notebooks-get报出真实的运行状态。constants.py被 prompt、expected载荷和 scorer 共享的所有字面量标题、锚点字符串、node ID、dataframe 名集中存放这是防止 prompt 与 scorer 漂移的关键机制。Seeder 契约中容易踩坑的细节务必遵守同步函数def seed_x(context) - dict[str, Any]。harness 通过asyncio.to_thread调用它见 base.py永远不要把它写成 async。没有每用例参数用例特有的状态意味着要写专用的 seeder 函数而不是给共享 seeder 加开关。返回 scorer 需要的一切包括team_id和 notebookshort_id编辑类用例还要返回markdown_before供 scorer 做 diff。seeder 抛异常 基础设施错误该用例会被排除在平均值之外破损的 seeder 永远不会被误读为 Agent 回归。对 authoring 套件而言seeder 是可选的Hedgebox 已经自带了embed_saved_insight需要的 Weekly signups insight。但一个只返回{team_id: ...}的最小 seeder 仍然值得写——它能解锁终态评分seeders.py 中的seed_case_team正是如此。6. 三层评分栈轨迹 → 终态 → 裁判评分采用三层结构确定性层作为主干仅在问题真正属于定性判断时才动用 LLM 裁判。第一层工具轨迹来自 Agent 日志确定性Scorer子类读取LogParser.cached(output[raw_log], ...)解析器见 log_parser.py。优先复用通用评分器RequiredToolCall、NoToolCall、LastToolCallNot、CalledTargetTool来自 cli_mcp/scorers.py 等通用集。notebook 特有新增UsedMarkdownNotebookFlow—— 通过notebooks-create-markdown创建而非手拼文档。CellsTitled—— 每个成功的 sql/pythonadd-cell都携带非空title工具描述强制要求PR #75471 正是因 Agent 跳过它而存在。ReusedSavedInsight—— 使用带预置 short_id 的saved_insightcell且没有重复相同查询的 SQL cell。RanCellsInDependencyOrder—— 对更新链每次上游重跑都先于其依赖者的重跑。RecoveredFromToolRejection—— 先尝试被拒绝的形态、随后正确调用成功是对RecoveredToCorrectTool的泛化。第二层终态来自数据库—— 本仓库的新增层这一层才能真正捕获生产故障也是本仓库评测体系的新增部分。异步 scorerAsyncOnlyScorerMixin, Scorerasyncio.to_thread使用seed[team_id]和seed[notebook_short_id]从 Postgres 读回 notebookNotebookIsMarkdown—— content 仍是单个ph-markdown-notebook节点没有被覆盖成旧版富文本。PreservedUnrelatedContent——seed[markdown_before]中除编辑目标外的每个锚点仍然存在每个既有 cell 标签仍携带其nodeId、runId、resultprops。这是覆盖故障的直接反制。CellRunSucceeded—— 新增 cell 的NotebookNodeRun行处于DONE且 envelope 非空。用于捕获「写了一个看起来对、实际报错的 cell」。system.notebooks也通过 HogQL 暴露markdown列但 scorer 直接读 ORM 更简单还能省去一次查询往返。第三层LLM 裁判质量层JudgedScorer子类基类见 scorers/judged.py只需实现_prepareNotebookAnswersQuestion—— 给定最终 markdownnotebook 是否真正回答了 prompt正确的指标、正确的时间窗口、有图表、有可快速浏览的 prose。CellTitlesAreDescriptive—— 标题说明 cell 展示的内容而不是「这是 SQL」。裁判从同一次数据库读取中拿到最终 markdown因此天然是异步的。仓库中已落地的定性裁判NotebookApproachQuality同文件 scorers.py展示了完整形态从工具调用记录中读取 Agent 撰写的 cells而非数据库用NOTEBOOK_APPROACH_PROMPTNOTEBOOK_APPROACH_RUBRIC六档量表perfect → useless让裁判按预期 approach 打分。评分纪律scoreNone表示「此检查不适用于此用例」会从聚合中剔除0.0表示 Agent 做错了且同样覆盖「裁判损坏」与「输入缺失」这类不能无声消失的路径。只统计成功的工具调用is_errorFalse——Agent 被允许尝试并失败。每个 scorer 只实现一个分支_run_eval_sync与_run_eval_async二选一绝不双实现。expected载荷按 scorer 名称键控这样一份 scorer 列表就能覆盖整个套件。7. 目录布局与套件骨架评测树内不允许出现conftest.py或pytest导入synthesizer 的单元测试放在products/notebooks/backend/test/。这一约定与 pytest 默认只收集test_*.py的规则天然契合——eval_*.py不会被误收集。套件骨架取自规划文档与实际实现一致from products.posthog_ai.eval_harness.base import SandboxedPublicEval from products.posthog_ai.eval_harness.config import SandboxedEvalCase from products.posthog_ai.eval_harness.harness.context import EvalContext async def eval_notebook_editing(ctx: EvalContext) - None: await SandboxedPublicEval( experiment_namesandboxed-notebooks-editing-cli, cases[...], scorers[...], ctxctx, )默认使用SandboxedPublicEval除非某个 prompt 或 seed 不应离开本机public 形态会获得 Braintrust URL 和跨运行历史。SandboxedPublicEval/SandboxedPrivateEval的实现是SandboxedEval的两个偏函数见 base.py前者is_publicTrue, no_send_logsFalse会同时向 Braintrust 与 PostHog 上报后者反之只写本地日志。保留-cli后缀让命名与其余 MCP 套件对齐。8. 上线顺序与运行命令规划文档给出了明确的推进次序前两步已完成落地在评测 MCP server 中启用revamped-py-notebooks注册 notebook Temporal 工作流回收 kernel 沙箱。已完成eval_notebook_cells就是证明这条链路通畅的用例。添加eval_notebook_authoring——先用轨迹评分器覆盖判别性用例复用 saved insight、从 componentHogQL 拒绝中恢复。添加文档 seeder 与PreservedUnrelatedContent然后是eval_notebook_editing。裁判最后加——它们是最嘈杂的一层也最容易过拟合。稳定后用--trials 3运行观察方差后再把任何分数当作基线。常用命令需在 flox shell 中运行hogli evals --list | grep notebooks # 确认发现import 检查模块 hogli evals eval_notebook_cells --eval python_cell_from_dataframe --provider docker hogli evals notebooks --provider docker # 整个域docker 是因为 kernel 通道更完整的 harness 参数可从 eval_harness/README.md 查阅常用的包括--eval substr只跑名称含子串的用例、--provider {docker,modal}默认 docker、--max-sandboxes N并发沙箱上限docker 默认 4、--trials N每用例跑 N 次、--fail-under fraction均值低于阈值则非零退出等。运行结束后的排查要点读取最后一行打印的 transcript 路径然后查看 experiment agent-log 目录下的每用例case.jsonl与case.summary.txt——光看分数无法告诉你用例为什么失败原始 Agent 日志才是最快的定位途径。9. 开放决策与已知边界Kernel 通道的 modal 支持目前仅 docker这使 python cell 用例并发被限制在四个沙箱左右。要解除限制需要让 notebook kernel 认识MODAL_EVALS为 modal 后端——这是为评测而改生产代码只有当 python 套件规模超出 docker 承载能力时才值得。旧版 notebook 工具启用旗标会隐藏它们因此任何针对notebooks-create/notebooks-partial-update409 冲突路径有真实错误量的评测需要「每套件」的旗标杠杆而不是进程级环境变量。裁判模型成本裁判按「每个用例 × 每个 scorer」计费。八个用例配两个裁判没问题十个裁判就太多了。附从规划到已落地的代码对照规划文档写作时标注为「已发布」shipped的部分在仓库中均可找到实现套件入口products/notebooks/evals/eval_notebook_cells.py三条用例experiment_namesandboxed-notebook-cells-cli另一已落地的开放分析套件 products/notebooks/evals/eval_notebook_analysis.py 用seed_churn_signal预置流失账户群并用ChurnCohortSurfaced按「笔记本 prose 中点名了预置账户的占比」打分。评分器products/notebooks/evals/scorers.pyNotebookCreated、CellRunsCompleted、ChurnCohortSurfaced、NotebookApproachQuality。Seederproducts/notebooks/evals/seeders.pyseed_case_team、seed_churn_signal含 ClickHouse 事件种植与 persons 数据库镜像。合成器products/notebooks/evals/synthesizer.py确定性 churn needle 构建。工具定义products/notebooks/mcp/tools.yamlmarkdown 工具集与旧工具集的旗标互斥关系。评测基建harness 的 MCP 服务装配与 kernel 沙箱支持见 eval_harness/harness/services.py 与 eval_harness/harness/kernel_sandboxes.py。这套评测体系的最终目的是用可复现、可并发、可跨运行对比的方式把「Agent 会不会用 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),仅供参考