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_events、log_events、score_events与feedback_events。其中:
- tracing 使用
ReplacingMergeTree(借助dedupeKey = traceId || ':' || spanId实现重试幂等); metric_events、log_events、score_events、feedback_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)
traceId、spanId、experimentId。文档强调:反馈可以附加到 trace 与 span,但 v-next 必须支持"无 trace 的反馈行"——这决定了traceId的可空性。
3. 实体层级与上下文(Entity hierarchy and context)
- 实体三级层级:
entityType/entityId/entityName,以及parentEntity*、rootEntity*三套; - 部署与执行上下文:
userId、organizationId、resourceId、runId、sessionId、threadId、requestId、environment、executionSource、serviceName、sourceId。
4. 反馈专用标量(Feedback-specific scalars)
feedbackSource:反馈来源(如 human / llm / system 等类别),注意它与sourceId语义不同;feedbackType:反馈类型;- 逻辑上的
value:反馈的取值。
5. 信息型负载(Information-only payloads)
metadata、comment:仅作信息承载,v0 不参与过滤、搜索、发现或分组。
设计文档对两个容易混淆的字段给出了明确澄清(这也是实现feedback.ts中feedbackSource与sourceId分列存储的直接依据):
sourceId是反馈所链接的源记录标识符,而不是反馈类别本身;- 反馈类别单独存放在
feedbackSource字段中。
三、物理模型(Physical Shape):设计意图与真实 DDL 的对照
3.1 设计文档定义的物理形状
| 维度 | 设计取值 |
|---|---|
| 表引擎 | MergeTree |
| 分区键 | PARTITION BY toDate(timestamp) |
| 排序键 | ORDER BY (traceId, timestamp) |
traceId | Nullable(String),支撑无 trace 反馈 |
feedbackSource/feedbackType | 强LowCardinality候选 |
valueString/valueNumber | 两个可空列,不做LowCardinality |
设计文档同时对物理形状给出了三条关键注解:
- 排序键的选择是有意的:v0 中反馈预计主要被"以 trace 为作用域"的读路径消费,因此
ORDER BY (traceId, timestamp)优先于全局按时间倒序(recency-first)的列表;全局倒序列表仍可作为次要的兼容/管理面存在,但不是本表物理设计的首要驱动。 PARTITION BY toDate(timestamp)支撑按天粒度的 TTL 管理,与 共享设计文档 中"TTL 按信号以天为增量可配置、按天分区为默认物理策略"的跨表约定一致。- 可空排序键要求建表时开启 ClickHouse 的可空键设置;值存储使用
valueString、valueNumber两个可空列,一条合法的 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并新增writeVersion:writeVersion UInt64 DEFAULT 0与排序键中的feedbackId共同构成"按反馈 ID 保留最新历史版本"的能力。feedback.ts中的feedbackRowsWithWriteVersions会在写入前查询该feedbackId已有的最大版本并递增,从而支持反馈记录的更新(如审查状态变更),同时保留完整历史。这是设计文档"纯 MergeTree 追加"之外的实现演进,也解释了为何实现仍保持"读取用FINAL"以保证拿到每个feedbackId的最新版本。 - 排序键追加
feedbackId:ORDER BY (traceId, timestamp, feedbackId)在设计文档的(traceId, timestamp)基础上补充了行内唯一性锚点,同时保持"trace 优先"的物理取向。 SETTINGS allow_nullable_key = 1:正是设计文档强调的"可空排序键需要对应 ClickHouse 设置",此处针对排序键中的可空列(traceId可能为NULL)显式开启。- 额外字段:
feedbackUserId(反馈行为者,与上下文userId分离)、reviewStatus(审查工作流,默认needs-review)、scope、tags(继承共享的可查询柔性字段约定)。
valueString/valueNumber均非LowCardinality,与设计文档及 共享设计文档 的 LowCardinality 决策完全一致。
3.3 时间与类型约定
DDL 头部注释总结了 v-next 全表的物理约定(与feedback_events直接相关者):
- 所有时间戳统一
DateTime64(3, 'UTC'); - 必填 ID 用
String,可选 ID 用Nullable(String); - 低基数维度用
LowCardinality; tags为Array(LowCardinality(String)) DEFAULT [],metadata等 JSON 负载为Nullable(String)(JSON 编码字符串);- 不设物理
createdAt/updatedAt列。
四、查询契约(Query Contract):过滤面、读重构与写映射
4.1 v0 过滤面
设计文档规定:v0 中feedbackSource与feedbackType必须可过滤,同时反馈行自身要支持当前公开反馈过滤面的其余部分:timestamp、traceId、spanId、userId、organizationId、experimentId、executionSource。
真实实现 过滤构建器 filters.ts 中的buildFeedbackFilterConditions与之一一对应,并做了更细的落地:
- 共享上下文过滤(
addCommonFilterFields)覆盖上述全部字段,timestamp支持start/end与startExclusive/endExclusive开闭区间; - 反馈专用过滤:
feedbackUserId(优先于userId,落在专用列feedbackUserId)、feedbackSource、reviewStatus、feedbackType(支持单值与IN数组); assertNoDeprecatedSourceFilter会显式拒绝已废弃的source过滤参数,提示改用feedbackSource或executionSource,从源码层面锁死了新契约。
需要强调的是设计文档的两个"排除":
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,数值反馈写入valueNumber,string / 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;executionSource、serviceName等上下文直接来自记录顶层字段,而不是从metadata提升——这正是设计文档"类型化上下文字段应从显式顶层字段写入,而非从 metadata 提升"的要求;metadata/scope以 JSON 编码字符串存储(jsonEncode),读取时parseJson还原。
4.4 信息型字段的边界
设计文档明确三条"不参与"规则:
value不参与过滤、搜索、发现与分组;comment同样不参与过滤、搜索、发现与分组;metadata在 v0 中保持纯信息负载。
背后的动机(设计文档原话要点)是:类型化拆分存储是有意的——这样未来新增数值排序或数值后过滤排序时,无需重新设计物理值表示。也就是说,"不检索 value"是 v0 的刻意取舍,而不是实现缺陷。
五、有意的 v0 限制(Intentional v0 Limitations)
设计文档以专门一节列出 v0 明确不做的事,读代码时应将其视为契约而非缺口:
- 无 feedback metadata 搜索;
value不可搜索;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):
getFeedbackAggregate:sum/avg/min/max/count/last(argMax(valueNumber, timestamp)),并支持previous_period/previous_day/previous_week对比周期与环比百分比;getFeedbackBreakdown:按白名单列(FEEDBACK_TYPED_COLUMNS)分组,metadata/scope/tags明确排除在groupBy之外;getFeedbackTimeSeries:toStartOfInterval分桶,桶宽支持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从主表增量派生,支撑listFeedback的delta模式游标拉取(serial或基于 fingerprint 的回退游标策略)。listFeedback的普通模式则使用FINAL+ 参数化 WHERE +LIMIT/OFFSET分页并返回总数,可见该表同时承载"按过滤面分页浏览"与"增量同步"两种读取形态。
七、设计权衡小结
回顾整个feedback_events设计,可以提炼出四条贯穿始终的权衡主线:
- trace 优先的物理布局 vs 无 trace 行:排序键围绕
traceId组织以服务 trace 作用域读取,同时用Nullable(String)+allow_nullable_key保证脱离 trace 的反馈仍可落库——在查询主路径与数据完整性之间取平衡。 - 类型化值列 vs JSON 混合标量:
valueString/valueNumber双列取代"一个字符串列存 JSON 混合标量",换取数值分析(聚合、分位数、未来排序)的直接可计算性;代价是 v0 明确定义 value 不可搜索。 - 过滤面收敛 vs 通用检索:v0 只承诺
feedbackSource、feedbackType及共享上下文字段的过滤,value、comment、metadata全部留在热路径之外,使物理设计可预测、可演进。 - 设计文档 vs 实现演进:设计文档规划的
MergeTree在实现中升级为带writeVersion的ReplacingMergeTree(支撑审查更新与历史保留),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),仅供参考