news 2026/9/13 11:58:29

Mastra ClickHouse vNext 反馈事件(feedback_events)存储设计深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Mastra ClickHouse vNext 反馈事件(feedback_events)存储设计深度解析

Mastra ClickHouse vNext 反馈事件(feedback_events)存储设计深度解析

【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra

导读

本文基于 Mastra 仓库中observability/clickhouse-design/feedback-events.md这一设计文档,系统讲解v-next可观测性体系中反馈事件(feedback_events)的逻辑模型、物理模型与查询契约,并结合stores/clickhouse/src/storage/domains/observability/v-next/下的真实源码(DDL、读写实现、过滤构建器)进行对照验证。读完本文,你将掌握 Mastra 如何用 ClickHouse 存储"可脱离 Trace 存在"的反馈数据、为何把反馈值拆分为valueString/valueNumber两个类型化列、以及 v0 阶段对过滤、搜索与保留的刻意取舍,从而在自建可观测性存储或贡献代码时快速对齐其设计意图。

一、设计定位:feedback_events 在 ClickHouse vNext 体系中的角色

在 ClickHouse vNext 可观测性设计入口 中,v-next覆盖五大可观测信号:tracing(span_events+trace_roots)、metric_eventslog_eventsscore_eventsfeedback_events。其中:

  • tracing 使用ReplacingMergeTree(借助dedupeKey = traceId || ':' || spanId实现重试幂等);
  • metric_eventslog_eventsscore_eventsfeedback_events在设计文档中规划为追加式MergeTree,v0 不引入去重键(见 共享设计文档)。

feedback 与 score 有一个共同的特殊点:它们既可以挂载到 trace/span 上,也允许在没有 trace 的场景下独立记录,因此其traceId物理上必须是可空的。而 feedback 又与 score 不同:score 是程序化打分(数值),feedback 则是"用户/外部来源的反馈",其取值既可能是数值(如评分 1–5)也可能是字符串(如一句评语),这正是后文类型化值列设计的出发点。

需要注意:本设计文档明确属于"实现前指导"(design documentation for the initial v-next implementation),一旦实现完成,代码与测试才是一致性行为的权威来源。本文后续会对照ddl.ts展示实际落地的物理表结构,两者存在少量演进差异,均会如实标注。

二、逻辑模型(Logical Shape):一张行可表达"任意来源、可选挂 trace"的反馈

设计文档对feedback_events的逻辑形状给出如下分层:

1. 事件元数据(Event metadata)

  • timestamp:事件时间戳,是唯一的元数据标量,也是分区与排序的根基。

2. Trace 关联(Trace correlation)

  • traceIdspanIdexperimentId。文档强调:反馈可以附加到 trace 与 span,但 v-next 必须支持"无 trace 的反馈行"——这决定了traceId的可空性。

3. 实体层级与上下文(Entity hierarchy and context)

  • 实体三级层级:entityType/entityId/entityName,以及parentEntity*rootEntity*三套;
  • 部署与执行上下文:userIdorganizationIdresourceIdrunIdsessionIdthreadIdrequestIdenvironmentexecutionSourceserviceNamesourceId

4. 反馈专用标量(Feedback-specific scalars)

  • feedbackSource:反馈来源(如 human / llm / system 等类别),注意它与sourceId语义不同
  • feedbackType:反馈类型;
  • 逻辑上的value:反馈的取值。

5. 信息型负载(Information-only payloads)

  • metadatacomment:仅作信息承载,v0 不参与过滤、搜索、发现或分组。

设计文档对两个容易混淆的字段给出了明确澄清(这也是实现feedback.tsfeedbackSourcesourceId分列存储的直接依据):

  • sourceId是反馈所链接的源记录标识符,而不是反馈类别本身;
  • 反馈类别单独存放在feedbackSource字段中。

三、物理模型(Physical Shape):设计意图与真实 DDL 的对照

3.1 设计文档定义的物理形状

维度设计取值
表引擎MergeTree
分区键PARTITION BY toDate(timestamp)
排序键ORDER BY (traceId, timestamp)
traceIdNullable(String),支撑无 trace 反馈
feedbackSource/feedbackTypeLowCardinality候选
valueString/valueNumber两个可空列,不做LowCardinality

设计文档同时对物理形状给出了三条关键注解:

  1. 排序键的选择是有意的:v0 中反馈预计主要被"以 trace 为作用域"的读路径消费,因此ORDER BY (traceId, timestamp)优先于全局按时间倒序(recency-first)的列表;全局倒序列表仍可作为次要的兼容/管理面存在,但不是本表物理设计的首要驱动。
  2. PARTITION BY toDate(timestamp)支撑按天粒度的 TTL 管理,与 共享设计文档 中"TTL 按信号以天为增量可配置、按天分区为默认物理策略"的跨表约定一致。
  3. 可空排序键要求建表时开启 ClickHouse 的可空键设置;值存储使用valueStringvalueNumber两个可空列,一条合法的 v0 反馈行必须恰好其中一个非空

3.2 真实实现 DDL:实现比设计多走了一步

在 v-next DDL 定义 中,FEEDBACK_EVENTS_DDL是实际落地的表结构,与设计文档相比有几处值得注意的演进(实现是当前行为的权威来源):

CREATE TABLE IF NOT EXISTS mastra_feedback_events ( -- Timestamp timestamp DateTime64(3, 'UTC'), -- IDs feedbackId String, writeVersion UInt64 DEFAULT 0, traceId Nullable(String), spanId Nullable(String), experimentId Nullable(String), -- Entity hierarchy entityType LowCardinality(Nullable(String)), entityId Nullable(String), entityName Nullable(String), entityVersionId Nullable(String), parentEntityVersionId Nullable(String), parentEntityType LowCardinality(Nullable(String)), parentEntityId Nullable(String), parentEntityName Nullable(String), rootEntityVersionId Nullable(String), rootEntityType LowCardinality(Nullable(String)), rootEntityId Nullable(String), rootEntityName Nullable(String), -- Context userId Nullable(String), organizationId Nullable(String), resourceId Nullable(String), runId Nullable(String), sessionId Nullable(String), threadId Nullable(String), requestId Nullable(String), environment LowCardinality(Nullable(String)), executionSource LowCardinality(Nullable(String)), serviceName LowCardinality(Nullable(String)), -- Feedback actor / linkage feedbackUserId Nullable(String), sourceId Nullable(String), -- Review workflow reviewStatus LowCardinality(String) DEFAULT 'needs-review', -- Feedback identity feedbackSource LowCardinality(String), feedbackType LowCardinality(String), -- Feedback value (exactly one non-null per valid row) valueString Nullable(String), valueNumber Nullable(Float64), -- Information-only comment Nullable(String), -- Query-relevant flexible fields tags Array(LowCardinality(String)) DEFAULT [], -- Information-only JSON payloads metadata Nullable(String), scope Nullable(String) ) ENGINE = ReplacingMergeTree PARTITION BY toDate(timestamp) ORDER BY (traceId, timestamp, feedbackId) SETTINGS allow_nullable_key = 1

与设计文档的差异及实现动机:

  • 引擎升级为ReplacingMergeTree并新增writeVersionwriteVersion UInt64 DEFAULT 0与排序键中的feedbackId共同构成"按反馈 ID 保留最新历史版本"的能力。feedback.ts中的feedbackRowsWithWriteVersions会在写入前查询该feedbackId已有的最大版本并递增,从而支持反馈记录的更新(如审查状态变更),同时保留完整历史。这是设计文档"纯 MergeTree 追加"之外的实现演进,也解释了为何实现仍保持"读取用FINAL"以保证拿到每个feedbackId的最新版本。
  • 排序键追加feedbackIdORDER BY (traceId, timestamp, feedbackId)在设计文档的(traceId, timestamp)基础上补充了行内唯一性锚点,同时保持"trace 优先"的物理取向。
  • SETTINGS allow_nullable_key = 1:正是设计文档强调的"可空排序键需要对应 ClickHouse 设置",此处针对排序键中的可空列(traceId可能为NULL)显式开启。
  • 额外字段feedbackUserId(反馈行为者,与上下文userId分离)、reviewStatus(审查工作流,默认needs-review)、scopetags(继承共享的可查询柔性字段约定)。

valueString/valueNumber均非LowCardinality,与设计文档及 共享设计文档 的 LowCardinality 决策完全一致。

3.3 时间与类型约定

DDL 头部注释总结了 v-next 全表的物理约定(与feedback_events直接相关者):

  • 所有时间戳统一DateTime64(3, 'UTC')
  • 必填 ID 用String,可选 ID 用Nullable(String)
  • 低基数维度用LowCardinality
  • tagsArray(LowCardinality(String)) DEFAULT []metadata等 JSON 负载为Nullable(String)(JSON 编码字符串);
  • 不设物理createdAt/updatedAt列。

四、查询契约(Query Contract):过滤面、读重构与写映射

4.1 v0 过滤面

设计文档规定:v0 中feedbackSourcefeedbackType必须可过滤,同时反馈行自身要支持当前公开反馈过滤面的其余部分:timestamptraceIdspanIduserIdorganizationIdexperimentIdexecutionSource

真实实现 过滤构建器 filters.ts 中的buildFeedbackFilterConditions与之一一对应,并做了更细的落地:

  • 共享上下文过滤(addCommonFilterFields)覆盖上述全部字段,timestamp支持start/endstartExclusive/endExclusive开闭区间;
  • 反馈专用过滤:feedbackUserId(优先于userId,落在专用列feedbackUserId)、feedbackSourcereviewStatusfeedbackType(支持单值与IN数组);
  • assertNoDeprecatedSourceFilter显式拒绝已废弃的source过滤参数,提示改用feedbackSourceexecutionSource,从源码层面锁死了新契约。

需要强调的是设计文档的两个"排除":

  • sourceId用于源记录关联,但不在 v0 的公开过滤面内
  • metadata存在于记录上,但不属于 v0 公开过滤 schema

4.2 读路径的 value 重构

设计文档规定读路径重构逻辑value时:valueNumber非空则取数值,否则取valueString。实现 helpers.ts 的rowToFeedbackRecord完全照此执行:

const hasNumber = row.valueNumber != null; // ... value: hasNumber ? Number(row.valueNumber) : (nullableString(row.valueString) ?? ''),

4.3 写路径的类型化映射

设计文档规定:字符串反馈写入valueString,数值反馈写入valueNumberstring / number 之外的值类型在 v0 反馈存储中不予支持。实现feedbackRecordToRow以类型判断完成映射:

valueString: typeof feedback.value === 'string' ? feedback.value : null, valueNumber: typeof feedback.value === 'number' ? feedback.value : null,

同时写路径还完成了若干归一化:

  • feedbackSource = feedback.feedbackSource ?? feedback.source ?? '':兼容旧source字段的迁移;
  • feedbackUserId = feedback.feedbackUserId ?? feedback.userId ?? null
  • executionSourceserviceName等上下文直接来自记录顶层字段,而不是从metadata提升——这正是设计文档"类型化上下文字段应从显式顶层字段写入,而非从 metadata 提升"的要求;
  • metadata/scope以 JSON 编码字符串存储(jsonEncode),读取时parseJson还原。

4.4 信息型字段的边界

设计文档明确三条"不参与"规则:

  • value不参与过滤、搜索、发现与分组;
  • comment同样不参与过滤、搜索、发现与分组;
  • metadata在 v0 中保持纯信息负载。

背后的动机(设计文档原话要点)是:类型化拆分存储是有意的——这样未来新增数值排序或数值后过滤排序时,无需重新设计物理值表示。也就是说,"不检索 value"是 v0 的刻意取舍,而不是实现缺陷。

五、有意的 v0 限制(Intentional v0 Limitations)

设计文档以专门一节列出 v0 明确不做的事,读代码时应将其视为契约而非缺口:

  1. 无 feedback metadata 搜索
  2. value不可搜索
  3. comment不可搜索

这三条与"类型化列拆分""信息型负载离热路径"共同构成 v0 的稳定性边界:反馈数据的检索面收敛、物理布局可预测,为后续(例如数值分位数、数值排序)的扩展保留空间。

六、从设计到实现:写路径、删除、审查与 OLAP

设计文档定义了表结构与查询契约,而 feedback.ts 给出了完整的领域实现。以下四点最能体现设计如何落到代码:

6.1 写路径:批量创建与版本递增

batchCreateFeedback通过feedbackRowsWithWriteVersions先按feedbackId查询既有最大writeVersion,再逐条+1,随后以JSONEachRow批量插入mastra_feedback_events。这使得同一feedbackId的多条历史(例如多次更新)在表内有序累积,读取时靠FINAL收敛到最新版本。

6.2 删除:轻量删除 + 持久化删除请求

deleteFeedback走 ClickHouse 轻量删除(DELETE ... WHERE feedbackId IN (...)),可按organizationId/resourceId收窄作用域,并通过recordDeletionRequest先落一条持久化删除请求(写入mastra_deletion_requests,见 ddl.ts 中的DELETION_REQUESTS_DDL)。物理清除依赖表的保留 TTL,读取立即可见、物理清除最终一致——与共享设计中的"删除事件最终一致"约定一致。

6.3 审查工作流:reviewStatus

updateFeedbackReviewStatus读取最新行(FINAL+ORDER BY writeVersion DESC, timestamp DESC LIMIT 1),校验不存在删除请求后,把整行(含新reviewStatus)重新批量写入形成新版本。合法取值为needs-review/reviewed,未知/遗留值在读取时被coerceFeedbackReviewStatus归一化为needs-review(见 review-status.ts)。rowToFeedbackRecord/feedbackRecordToRow的注释明确要求"双向映射无丢失",因为整行重插是审查更新的核心机制。

6.4 OLAP 查询面:聚合、分组、时序与分位数

反馈领域实现了四类分析查询,全部基于"仅数值行参与"的身份过滤(buildFeedbackIdentityFilter强制feedbackType匹配且valueNumber IS NOT NULL):

  • getFeedbackAggregatesum/avg/min/max/count/lastargMax(valueNumber, timestamp)),并支持previous_period/previous_day/previous_week对比周期与环比百分比;
  • getFeedbackBreakdown:按白名单列(FEEDBACK_TYPED_COLUMNS)分组,metadata/scope/tags明确排除在groupBy之外;
  • getFeedbackTimeSeriestoStartOfInterval分桶,桶宽支持1m/5m/15m/1h/1d,支持按维度拆分多序列;
  • getFeedbackPercentiles:按桶计算quantile(p)(valueNumber),p 必须在 0–1 之间。

这些查询全部落在valueNumber上——正是设计文档"类型化拆分存储以便未来支持数值排序/后过滤"的前瞻性体现:同一套物理列,既服务于普通读路径,也直接支撑 OLAP 数值分析

6.5 增量拉取(delta polling)

作为附带的实现层能力,mastra_feedback_events_delta表(ddl.ts 中buildFeedbackEventsDeltaDDL)以cursorId为排序键、TTL ingestedAt + toIntervalDay(2)两日过期,由物化视图MV_FEEDBACK_EVENTS_DELTA从主表增量派生,支撑listFeedbackdelta模式游标拉取(serial或基于 fingerprint 的回退游标策略)。listFeedback的普通模式则使用FINAL+ 参数化 WHERE +LIMIT/OFFSET分页并返回总数,可见该表同时承载"按过滤面分页浏览"与"增量同步"两种读取形态。

七、设计权衡小结

回顾整个feedback_events设计,可以提炼出四条贯穿始终的权衡主线:

  1. trace 优先的物理布局 vs 无 trace 行:排序键围绕traceId组织以服务 trace 作用域读取,同时用Nullable(String)+allow_nullable_key保证脱离 trace 的反馈仍可落库——在查询主路径与数据完整性之间取平衡。
  2. 类型化值列 vs JSON 混合标量valueString/valueNumber双列取代"一个字符串列存 JSON 混合标量",换取数值分析(聚合、分位数、未来排序)的直接可计算性;代价是 v0 明确定义 value 不可搜索。
  3. 过滤面收敛 vs 通用检索:v0 只承诺feedbackSourcefeedbackType及共享上下文字段的过滤,valuecommentmetadata全部留在热路径之外,使物理设计可预测、可演进。
  4. 设计文档 vs 实现演进:设计文档规划的MergeTree在实现中升级为带writeVersionReplacingMergeTree(支撑审查更新与历史保留),ORDER BY追加feedbackId提供行内锚点;以 v-next DDL 和 feedback 领域实现 为准的当前行为才是落地后的权威契约。

延伸阅读

  • ClickHouse vNext 可观测性设计入口:五信号整体蓝图与落地顺序
  • 共享设计文档:跨信号决策(追加式存储、LowCardinality 指引、归一化规则、删除与保留)
  • 物理类型约定:全表类型约定的细节
  • feedback_events 设计文档:本文主题的原始设计
  • v-next DDL 实现:FEEDBACK_EVENTS_DDL与 delta 表真实结构
  • feedback 领域实现:读写、删除、审查与 OLAP 全量代码
  • 行映射与归一化:rowToFeedbackRecord/feedbackRecordToRow的 value 重构与写映射
  • 过滤构建器:buildFeedbackFilterConditions的 v0 过滤面落地

【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 11:58:25

OpenAI Agents SDK 沙箱如何挂载 S3 等远程存储

OpenAI Agents SDK 沙箱如何挂载 S3 等远程存储 【免费下载链接】openai-agents-python A lightweight, powerful framework for multi-agent workflows 项目地址: https://gitcode.com/GitHub_Trending/op/openai-agents-python 如果你的 Sandbox Agent 需要读写存放在…

作者头像 李华
网站建设 2026/9/13 11:57:37

三分钟装好 Microsoft Office:一键下载、安装、激活工具

三分钟装好 Microsoft Office:一键下载、安装、激活工具 【免费下载链接】LKY_OfficeTools 一键自动化 下载、安装、激活 Office 的利器。 项目地址: https://gitcode.com/GitHub_Trending/lk/LKY_OfficeTools LKY_OfficeTools 是一个开源的命令行部署工具&a…

作者头像 李华
网站建设 2026/9/13 11:54:44

Wagtail API v2 使用指南:从数据拉取到字段定制的完整实战手册

Wagtail API v2 使用指南:从数据拉取到字段定制的完整实战手册 【免费下载链接】wagtail A Django content management system focused on flexibility and user experience 项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail 本指南基于 Wagtail 官…

作者头像 李华
网站建设 2026/9/13 11:50:07

智能终端四大核心芯片协同升级指南

1. 这不是芯片清单,而是一张智能终端升级路线图你刷到“12月新品推荐:车用CPU、5G小基站基带芯片、安全控制器、GaN RF”这个标题时,第一反应可能是——又一张厂商通稿式的参数罗列?但作为连续跟踪芯片产业十年、亲手调试过37款车…

作者头像 李华