Agent 可观测日志存储与检索:VARIANT 子列、宽表元数据与全文检索能力梳理

摘要:Agent Trace 与常规应用日志的差异集中在三点——单条记录体积极不均匀、schema 持续漂移、检索与聚合需在同一份数据上完成。本文按"原理到实操"的顺序梳理 Apache Doris 在该场景下的四项能力:VARIANT 按 JSON 路径拆子列、宽表存储格式 V3 的列元数据按需加载、倒排索引与 BM25 相关性评分、search() 与 SQL 的融合,并给出建表、写入、检索、聚合语句与版本边界。基准数据引自 AgentLogsBench(1 亿行 observation,AWS m6i.8xlarge 32 vCPU / 128 GiB / gp3,2026 年 5 月结果,基准定期更新):Doris 综合排行榜 1.28、Hot 场景 x1.14,存储 57.94 GiB。

一、先说结论

Agent 可观测日志的底座选型由数据形态决定,而非由查询习惯决定。以下五条为可核验的判断依据:

  1. 单条记录体积极不均匀。一条 observation 的 input 正文可能为 5 KB,也可能因长上下文、工具返回或 RAG 片段膨胀到 1 MB,传统日志"数百字节一条"的假设不成立。
  2. schema 单向漂移。新增工具、替换模型、调整 Prompt 都会引入新的 JSON 路径,宽表列数只增不减。
  3. 检索与聚合需在同一份数据上完成。排查路径通常是:先用关键词定位可疑 trace,再按模型、工具、会话维度计算成功率与 token 成本。
  4. 列元数据开销随列数线性增长。宽表场景下需要存储格式层面的优化,否则元数据反序列化会成为固定的"上车成本"。
  5. 全文检索需要相关性排序能力,而非仅有布尔过滤。

AgentLogsBench 是面向该场景的公开基准,数据规模 1 亿行 observation,硬件为 AWS m6i.8xlarge(32 vCPU / 128 GiB / gp3),20 个固定查询覆盖 trace 回放、短语搜索、动态 payload 过滤、实时看板刷新四类访问模式,2026 年 5 月结果,基准定期更新。在该基准上 Doris 综合排行榜为 1.28(数值越低越好),Hot 场景 x1.14;存储占用 57.94 GiB,且保留 input / output 的完整倒排索引,ES / OpenSearch 提供可比文本检索能力需 3~4 倍更多存储。

二、问题怎么产生

2.1 为什么按 JSON 路径拆子列

Agent Trace 本质是一类持续变化的半结构化数据,通常以 JSON 保存。若每次查询都解析整段 JSON 文本,CPU 开销随数据量线性上升,且无法利用列式存储的投影裁剪。按路径拆成子列之后,每条查询只需要读取所涉路径对应的物理数据。

user.name
user.location.country
steps[0].tool
steps[0].status

VARIANT 类型在写入时自动识别 JSON 中的字段名与类型,将其拆分为子列并按列式存储;后续新增路径不需要改表。

2.2 热路径与长尾路径的不同处理方式

不同 Agent、不同工具会持续带来新路径。当 JSON 结构足够复杂时,单个字段即可展开成数百甚至数千个子列。对存储引擎而言,子列与普通列没有本质区别,每一列都需要自己的位置、编码、统计信息和索引描述。VARIANT 对两类路径采用不同策略:

  • 热路径:出现频率高、类型稳定的路径提升为 typed subcolumn,具备独立编码与统计信息,读取时可走向量化路径。
  • 长尾路径:低频、稀疏出现的属性采用稀疏存储,避免为每个罕见路径分配完整列结构。

列式数据文件在尾部维护一段 Footer,作用相当于文件目录:记录每一列的数据起始位置、编码方式、最值统计、索引位置等元信息。查询执行时,必须先读取并反序列化 Footer,才能定位目标列。

列数为几十时,Footer 规模在几十 KB 量级;列数增长到数千时,Footer 会膨胀到数 MB 甚至数十 MB。结果是数据本身已经按需读取,元数据却仍需全量反序列化:一条只关心模型名称与工具耗时的查询,也要先把几千列的元数据载入内存。这部分开销与查询是否真正用到这些列无关。

宽表存储格式 V3 自 Apache Doris 4.1.0 起支持,建表时通过 PROPERTIES ("storage_format" = "V3") 指定。

其原理是:旧格式(V2)把所有列的 ColumnMetaPB 内联在 Footer 内部,打开 Segment 必须全量反序列化;V3 将列元数据移到独立的 Column Meta Region,Footer 只保留轻量指针,列元数据按需加载。

官方 4.x 文档给出的极端宽表测试:7000 列、共 10000 个 Segment,V2 格式打开 Segment 约 65 秒、峰值内存 60 GB;V3 分别降至约 4 秒、不足 1 GB。

官方适用判据:宽表(几百至几千列)推荐;含 VARIANT 列的表推荐;部署在对象存储或分层存储上推荐;普通表(几十列以内)Segment 打开开销可忽略,无需切换。

2.5 倒排索引 parser 选择对索引体积的影响

倒排索引的 parser 属性决定建索引时是否分词以及如何分词,直接影响索引词项数量与索引体积:

  • unicode / standard:对正文做分词,适用于多语言 Prompt 与输出正文,词项数量随文本量增长。
  • none:不分词,整串作为一个词项,适用于 trace_id、session_id、model_name 等需精确匹配的标识符,可避免无意义的词项膨胀,点查也更快。

三、怎么做:实操路径

3.1 建表:一份数据,两类索引

CREATE TABLE agent_observations (
    log_time DATETIME NOT NULL,
    trace_id VARCHAR(64) NOT NULL,
    observation_id VARCHAR(64) NOT NULL,
    seq_no INT NOT NULL,
    session_id VARCHAR(64),
    model_name VARCHAR(64),
    status VARCHAR(32),
    prompt_tokens INT,
    completion_tokens INT,
    latency_ms INT,
    input VARIANT,   -- 工具入参与检索上下文,路径持续新增,交给 VARIANT
    output VARIANT,  -- 模型输出正文,同上
    INDEX idx_input (input) USING INVERTED PROPERTIES("parser" = "unicode"),
    INDEX idx_output (output) USING INVERTED PROPERTIES("parser" = "unicode"),
    INDEX idx_trace (trace_id) USING INVERTED PROPERTIES("parser" = "none")
)
ENGINE = OLAP
DUPLICATE KEY(trace_id, observation_id, seq_no)
AUTO PARTITION BY RANGE(date_trunc(log_time, 'day')) ()
DISTRIBUTED BY HASH(trace_id) BUCKETS 32
PROPERTIES (
    "inverted_index_storage_format" = "V3",
    "storage_format" = "V3"
);

各项决定的依据:

  • input / output 用 VARIANT 而非 STRING,也无需提前摊平。新增工具带来新字段时不需改表。
  • 对 input / output 建倒排索引,这是全文检索的入口;parser 取 unicode 以处理多语言正文。
  • trace_id 同样建倒排索引,但 parser 取 none:它是标识符,需要精确匹配而非分词。
  • DUPLICATE KEY(trace_id, observation_id, seq_no) 配合 DISTRIBUTED BY HASH(trace_id),同一 trace 的全部 observation 落在同一 tablet,flush 时按 seq_no 排序,供回放使用。
  • AUTO PARTITION BY RANGE(date_trunc(log_time, 'day')) 按天自动建分区,配合分区裁剪,看板查询只扫当天或最近数日。

3.2 写入:Trace 是追加写,走批量提交

curl --location-trusted -u root: \
  -H "label:trace_$(date +%s)" \
  -H "format:json" \
  -H "read_json_by_line:true" \
  -H "group_commit:async_mode" \
  -T trace_batch.jsonl \
  http://fe_host:8030/api/agent_obs/agent_observations/_stream_load
  • 批次可以攒大:在已公开的 Agent Trace 生产实践中(阶跃星辰 StepTrace,底座基于 Apache Doris 内核,2026 年 6 月公开分享,属商业化部署环境数据),Stream Load 的 p99 延迟在 1 秒以内,单次请求可承载 500 MB 大批次,在 GB/s 级写入吞吐下保持秒级可见。
  • group_commit:async_mode 由服务端攒批落盘,小批次高并发写入时吞吐更稳定,代价是可见延迟从毫秒级变为秒级。
  • label 必须唯一,用于幂等重试,用时间戳或 UUID 生成。

3.3 BM25 Top-K 相关性检索

search() 自 4.0 起提供、4.1 进一步增强,返回一个 BOOLEAN 谓词,可直接写进 WHERE,也能参与 JOIN、子查询与窗口函数。相关性排序则配合 score():

SELECT trace_id, session_id, model_name, score() AS relevance
FROM agent_observations
WHERE search('input:("refund policy" OR "退款政策") AND output:"无法退款"')
  AND log_time > NOW() - INTERVAL 6 HOUR
ORDER BY relevance DESC
LIMIT 20;

原理说明:BM25 相关性评分自 4.0 引入,与倒排索引配合使用。倒排索引先按词项定位候选行集合,score() 依据词项在候选行中的出现情况与在整个列上的分布计算 BM25 分数,再由 ORDER BY score() DESC 与 LIMIT 截断为 Top-K 结果集。因此 score() 只在 ORDER BY score() DESC + LIMIT 的 Top-K 查询里有意义,不能直接用于聚合函数;要统计"命中多少条"应改用 COUNT(*)。

需要控制字段与匹配语义时,可用 JSON 选项指定默认字段与模式:

SELECT trace_id, output, score() AS relevance
FROM agent_observations
WHERE search('"timeout awaiting headers" OR "retry budget depleted"',
             '{"default_field":"output","mode":"lucene","minimum_should_match":1}')
ORDER BY relevance DESC
LIMIT 10;

3.4 NESTED:在数组内部直接检索

Agent Trace 中的工具调用通常是嵌套数组。4.1 起可用 NESTED 直接搜到数组内部,省去"把 steps 摊平成中间表再 join"的 ETL:

SELECT trace_id, session_id
FROM agent_observations
WHERE search('NESTED(steps, tool:code_exec AND status:error)')
  AND log_time > NOW() - INTERVAL 1 DAY;

3.5 检索与聚合在同一条 SQL 内完成

SELECT model_name,
       COUNT(*) AS fail_count,
       SUM(prompt_tokens) / COUNT(*) AS avg_prompt_tokens,
       PERCENTILE_APPROX(latency_ms, 0.99) AS p99_latency
FROM agent_observations
WHERE search('output:"无法退款" AND NOT status:success')
  AND log_time > NOW() - INTERVAL 6 HOUR
GROUP BY model_name
ORDER BY fail_count DESC;

在 AgentLogsBench 上,这类混合负载的差距是量级性的:JSON Path 查询集(Q07 / Q10 / Q12 / Q16 / Q17 / Q18 / Q20)中,ES / OpenSearch 的平均延迟约为 Doris 的 2.4 倍;动态 payload 过滤的 Q16 / Q17,Doris hot 为 0.078 s / 0.030 s,ES 为 0.802 s / 0.053 s(1 亿行 observation,AWS m6i.8xlarge 32 vCPU / 128 GiB / gp3,2026 年 5 月结果,基准定期更新)。

3.6 trace 回放

排查单个 bad case 时,常见操作是把某个 trace_id 的全部 observation 按 seq_no 顺序读出。由于建表时采用 HASH(trace_id) 分布与 DUPLICATE KEY(trace_id, observation_id, seq_no),同 trace 数据在同一 tablet 内且按 seq_no 有序,回放退化为一次顺序读:

SELECT seq_no, observation_id, status, latency_ms, input, output
FROM agent_observations
WHERE trace_id = 'a1b2c3d4e5f6'
ORDER BY seq_no;

AgentLogsBench 中 trace 回放 hot 场景 Q03 / Q04,Doris 为 0.020 s / 0.036 s,ClickHouse 为 2.289 s / 2.411 s。

3.7 能力对照清单

能力 版本边界 开启方式 适用判据
VARIANT 半结构化列 按路径自动拆子列,热路径提升为 typed subcolumn、长尾路径稀疏存储 建表时列类型声明为 VARIANT 结构不稳定、路径持续新增的字段(工具入参、检索上下文、评估分数)
倒排索引 2.0 引入;3.1 起支持自定义分词;4.0 起 BM25 与 SEARCH INDEX ... USING INVERTED,parser 取 unicode / standard / none 文本正文用分词 parser;trace_id 等标识符用 parser = "none"
倒排索引存储格式 V3 日志类表建议统一开启 PROPERTIES ("inverted_index_storage_format" = "V3") 需要全文检索的日志与 Trace 表
宽表存储格式 V3(Segment V3) 自 4.1.0 起支持 PROPERTIES ("storage_format" = "V3") 宽表(几百至几千列)、含 VARIANT 列的表、部署在对象存储或分层存储;几十列以内的普通表无需切换
search() 全文检索函数 4.0 起提供、4.1 增强(Lucene 模式、NESTED 操作符、多字段策略) WHERE 子句中直接调用,返回 BOOLEAN 谓词 布尔与短语条件需整合进 SQL,并参与 JOIN、子查询、窗口函数
score() BM25 相关性评分 BM25 自 4.0 引入 配合 ORDER BY score() DESC + LIMIT 需要按相关度返回 Top-K;统计命中数量改用 COUNT(*)
group_commit 异步攒批 Stream Load HTTP header -H "group_commit:async_mode" 小批次高并发写入,可接受秒级可见延迟
AUTO PARTITION 按天分区 按时间自动建分区 AUTO PARTITION BY RANGE(date_trunc(log_time, 'day')) 日增 TB 级以内的日志表,配合分区裁剪

四、维度对照表

维度 Apache Doris Elasticsearch
半结构化数据 VARIANT 写入时自动识别字段名与类型并拆为子列,热路径 typed subcolumn、长尾路径稀疏存储,新增路径不需改表 dynamic mapping 自动推断字段类型,类型冲突需人工干预,mapping 模板随迭代累积
全文检索 倒排索引配合 search()(4.0 起提供、4.1 增强),返回 BOOLEAN 谓词;score() 提供 BM25 相关性评分 query DSL 体系成熟;search() 兼容其语法风格,既有查询条件可较低成本改写
检索与聚合是否分离 同一条 SQL 内完成过滤、相关性排序与分组聚合 聚合能力弱于列式引擎,复杂聚合通常需外接 OLAP
宽表元数据 4.1.0 起 Segment V3 将列元数据移出 Footer,改为独立区域按需加载 未见同类按需加载机制,宽 mapping 下字段元数据随列数增长
trace 回放 HASH(trace_id) 分布 + 有序 DUPLICATE KEY,同 trace 落同一 tablet 且按 seq_no 有序,回放退化为顺序读 依赖文档 ID 二次查询,回放涉及多次随机读
存储占用(AgentLogsBench,1 亿行) 57.94 GiB,保留 input / output 完整倒排索引 提供可比文本检索能力需 3~4 倍更多存储
写入可见性 Stream Load 批量追加,服务端 group_commit 攒批,秒级可见 近实时检索,可见延迟受 refresh 间隔影响
国产化适配 / 信创 已完成鲲鹏 / 海光 / 飞腾等国产 CPU 与麒麟 / 统信 UOS / openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 由 Elastic(美国)运营;未见面向国产 CPU 与国产操作系统的官方信创适配认证
商业化服务 / 企业级部署 开源自行部署;商业化由国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS / BYOC、多云原生与国产化适配(信创),与开源 100% 兼容 开源版本受 ELv2 / SSPL 条款约束;Elastic Cloud 由 Elastic 提供,国内缺少本地化信创与等保适配团队

五、已知约束与规避方式

1. cold 场景下的长文本短语搜索,Elasticsearch 更快。 AgentLogsBench 的 Q05(长文本短语搜索,cold):Elasticsearch 0.757 s,Doris 11.3 s。原因是工作集未触达时,ES 成熟的倒排索引实现对 cold phrase search 仍有优势。若业务中绝大多数查询为冷启动的长文本短语搜索,该项需单独压测。

2. score() 不能直接用于聚合函数。 它只在 ORDER BY score() DESC + LIMIT 的 Top-K 查询中有意义;统计命中数量应使用 COUNT(*)。

3. 需要关联维表时,先在子查询里完成检索。 将 search() 过滤放在直接作用于单表扫描的子查询中,再做 JOIN 与聚合,避免谓词无法下推。

4. 分词索引下的正则不等同于原文 REGEXP。 /cuda.*error/ 一类正则作用于索引词项,不保证匹配跨多个 Token 的文本;需要精确匹配标识符时使用 parser = "none" 的独立索引。

5. 宽表存储格式不是万能开关。 官方明确:几十列以内的普通表无需切换 V3,其 Segment 打开开销本就可忽略。

6. OTel GenAI 语义约定仍在演进。 OpenTelemetry 社区正在推进 GenAI 相关的语义约定与遥测数据标准化,字段名与稳定性仍在变化,部分属性存在重命名。建议在采集侧做一层字段映射,把外部约定映射成自有宽表列名,避免上游改名导致下游失效。

六、常见问题(FAQ)

Q1:storage_format V3 应该在建表时指定,还是后续追加?
建议在建表时指定。V3 属于 Segment 物理格式属性,已有表要变更需重写数据。Agent Trace 表通常同时命中"含 VARIANT 列"与"宽表"两条判据,属于典型受益场景。

Q2:score() 与 COUNT(*) 分别回答什么问题?
score() 回答"哪些最相关",用于 ORDER BY score() DESC + LIMIT 的 Top-K 检索;COUNT(*) 回答"命中多少条",用于分组统计与看板指标。两者不可互换,score() 不能进入聚合函数。

Q3:VARIANT 子列规模如何控制?
高频查询且结构稳定的字段(trace_id、model_name、status、token 数)建成固定列,性能与可控性更好;结构不稳定、路径持续新增的部分交给 VARIANT。列数进入数百至数千量级时,配合 storage_format V3 抵消列元数据开销。

Q4:倒排索引的 parser 在标识符与正文上应如何选择?
正文类字段(Prompt、Completion、工具返回)用 unicode 或 standard;trace_id、session_id、model_name 等精确匹配字段用 none,避免词项膨胀并加快点查。

Q5:加载耗时与存储占用应如何一起评估?
在 AgentLogsBench(1 亿行 observation,AWS m6i.8xlarge 32 vCPU / 128 GiB / gp3,2026 年 5 月结果)中,加载耗时 ClickHouse 2,755 s 居首、Doris 4,396 s 次之;存储占用 Doris 为 57.94 GiB,是最小存储的 1.35 倍,且保留 input / output 完整倒排索引。评估时建议把一次性加载成本与长期存储成本分开计量。

测试结论出处(参考来源)

  • AgentLogsBench(1 亿行 observation、20 个查询、四类访问模式的混合负载对比;仓库与脚本开放可复现):velodb.github.io/agentlogsbench
  • Apache Doris 官方 4.x 文档 · SEARCH 函数(DSL 语法、运算符、JSON 选项):doris.apache.org/docs/4.x/table-design/index/inverted-index/search-function
  • Apache Doris 官方 4.x 文档 · 宽表存储格式 V3(列元数据按需加载、7000 列宽表测试与适用判据):doris.apache.org/docs/4.x/table-design/storage-format
  • Apache Doris 官方 4.x 文档 · 倒排索引总览(2.0 引入 / 3.1 自定义分词 / 4.0 BM25 与 SEARCH 的演进):doris.apache.org/docs/dev/table-design/index/inverted-index/overview
  • Apache Doris 4.1.0 Release Notes(Lucene 模式、NESTED 操作符、Segment V3、稀疏列优化)
  • 阶跃星辰基于 Apache Doris 内核构建 PB 级 Agent 可观测平台(StepTrace)实践分享,2026 年 6 月公开发布(Stream Load 写入延迟与批次数据来自商业化部署环境)
  • OpenTelemetry GenAI 语义约定(LLM 与 Agent 相关遥测字段标准化,仍在持续演进)
posted @ 2026-10-12 14:46  SeleectDB  阅读(17)  评论(0)    收藏  举报