news 2026/10/1 18:31:06

基于Apache Doris构建AI Agent可观测性平台:链路追踪与决策还原实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Apache Doris构建AI Agent可观测性平台:链路追踪与决策还原实践

AI Agent 上线后最难的还不只是让它干活,而是它干完活之后你完全说不清它刚才经历了什么。团队在第一版 Agent 接入真实用户流量以后,几乎每周都会遇到一次"结果不对但不知道为什么"的工单。我们上过 LangChain 自带 debug 模式,也翻过模型提供方后台,最后还是觉得缺一个属于自己的可观测性底座。Apache Doris 就是在这个阶段进入选型视野的。它最后承担了 AI Agent 链路数据的存储、分析与定位,让我们从只能看模型侧日志,变成了能按 trace_id 一步步还原 Agent 的完整决策过程。这篇文章主要聊聊我在这个过程中的架构设计和踩坑。

1. Agent 的黑盒:不只是少日志,而是链路本身会分叉

1.1 不确定性从模型层传导到系统层

传统 Web 服务的可观测性有一个隐含前提:请求路径是相对确定性的。就算有网关、缓存、数据库、第三方 API,一条链路整体上还是能用"服务间调用关系"来抽象。但 AI Agent 不一样。Agent 的每一步都可能由模型决定:要不要调用工具、调用哪个工具、工具返回后如何总结、下一轮要不要继续循环、最终答案什么时候该结束。同一个 prompt,模型今天给出的工具选择可能和昨天完全不同,这个不确定性会直接从模型层传导到下游的数据库查询、API 调用、文件操作,甚至影响用户看到的最终结果。

这意味着我们不能只观测"调用延迟"和"状态码"。Agent 的每一次决策本身也是需要观测的对象。如果某次 Agent 答非所问,原因可能是 prompt 写得含糊、模型温度参数太高、工具返回格式变化、上下文被前面几轮内容污染,甚至只是模型供应商的某个版本出现行为漂移。要定位到具体环节,就必须把模型输入输出、工具请求响应、决策分支、token 消耗这些数据全部记录成可分析的结构化数据。传统 APM 工具对这类分叉链路的支持普遍很弱,它们设计的 Trace 模型天然围绕"固定路径的 RPC 调用",放到 Agent 场景就会显得僵硬。

所以我在设计初期就定了一个原则:可观测性数据不能只看单个请求的延时和成功率,还要能回答"这个 Agent 为什么选择走这条路",以及"这条路走的成本有多高"。这也是后来选择 Apache Doris 作为承载底座的重要原因,因为它能同时处理高并发写入和高灵活度的分析查询,而不是只解决"日志能存下来"的问题。

1.2 分支和循环让"一次请求"不再等于"一条调用链"

传统 Trace 里面,一个 trace 通常对应一次用户请求,span 之间是串行或简单的上下游关系。Agent 执行过程中经常出现循环:工具返回异常,模型决定换一个工具再试;第一次结果不够好,Agent 自己追加一轮检索;多轮对话场景下,上下文里已经积累了多次工具调用的结果。这种循环结构如果硬套 OpenTelemetry 的标准 span 模型,你会看到一堆 parent_span_id 来回跳,树状结构变得非常混乱。

我们在早期接入 LangGraph 时发现,它的 State 对象天然包含节点状态、检查点、历史消息,但这些状态分散在内存里,进程一重启就没了。我们需要把这些状态周期性快照到外部存储,做“决策路径重放”。所以后来在埋点设计上,我给每个执行节点生成了独立的 span_id,同时保留 agent_session_id 和 parent_span_id。这样既能在需要时还原成一棵完整的决策树,又能按 session 维度把整条多轮链路拼起来。

这个"一张表既能按树查、又能按流查"的需求,直接决定了 Doris 表结构不能设计成纯关系型范式。字段冗余、重复存储、用宽表保存必要上下文,都是刻意接受的。可观测性本身就是重数据的场景,宁可在存储上多花一点成本,也要保证查询时不用做复杂的 join。

1.3 成本、质量、体验必须在一张表里对齐

Agent 的可观测性除了传统稳定性和性能,还多了两个很现实的问题:成本和回答质量。一次用户请求可能内部调用了五次 LLM,其中四次都是为了修正第一步的理解偏差;从用户视角看,他等了三秒,结果还行,但从成本视角看,这次请求已经花掉了正常情况的三倍 token。如果不把成本和质量放进同一张分析表,运营侧和研发侧很难对齐结论。

我们早期分开存指标和日志,成本数据在模型网关侧,质量反馈在业务库里,链路日志在 ES 里。结果每次复盘都要写脚本把三份数据关联起来,非常痛苦。后来把 token 数、成本金额、agent 版本、用户反馈标签全部作为字段写进 Doris 的明细表里,再需要复盘时,一条 SQL 就能同时按模型、版本、反馈类型分组。这个改动让整个团队的使用习惯都变了,大家开始真的在 Doris 上做分析,而不是导数据到 Excel。

1.4 可观测性本身也要算 ROI

可观测性平台做多做少,本质上是个预算问题。1000 个用户并发时,Agent 每秒可能产生几百条埋点,每条埋点里有 prompt 原文、工具返回、token 统计,一天下来单是文本字段就有几十 GB。如果无脑全量存,存储成本很快会比模型调用成本还高。所以架构上必须考虑压缩、采样、冷热分层和聚合,不能照搬传统日志平台的全量链路方案。

这也是我坚持引入 Doris 的原因之一。Doris 的列式存储和前缀索引对这类“高基数、宽字段、文本多”的数据压缩效果明显,配合分区和 TTL 能很好控制成本。后面我会专门写我们遇到的膨胀和采样问题,这里先记住一件事:可观测性平台不是把所有数据堆在一起,而是在"是否可查"和"存储成本"之间做设计取舍。

2. 为什么底座选了 Apache Doris 而不是 ES 或 ClickHouse

2.1 可观测性数据的三个特征

Agent 埋点数据有明显特征:第一是写入峰值高,Agent 执行过程中同一时刻可能有大量节点回写,如果用户请求并发上来,每秒写入事件数会非常不稳定;第二是维度基数极高,agent_version、model_name、tool_name、prompt_hash、用户 ID、session ID 组合起来有千万甚至亿级基数;第三是字段不固定,不同节点类型需要记录的字段差别很大,工具调用要记入参和出参,LLM 节点要记模型名和 token,条件分支要记判定结果,这些属性很难提前用固定列全部定义好。

这三个特征叠加在一起,给时序数据库和传统日志搜索引擎都出了难题。时序库擅长指标,但对文本检索和任意维度 group by 支持弱;日志搜索引擎擅长全文检索,但聚合性能在高基数场景下容易崩;而 Doris 的定位是实时分析型数据库,列式存储、向量化执行、倒排索引、JSON 类型支持,恰好把这几项需求同时覆盖了。

2.2 Doris 的关键能力清单

以我们生产环境实际用下来的体验,Doris 在可观测性场景里最有价值的能力是这几个:

  • 实时写入能力强:Doris 的 Stream Load 支持攒批导入,配合 Kafka Routine Load 可以做准实时入库,写入能力能扛住 Agent 高并发埋点。
  • 高压缩比列式存储:重复的模型名、工具名、状态字符串在列存下压缩效果很好,实际使用中原始 JSON 文本压到五分之一左右很常见。
  • 灵活的半结构化支持:Doris 的 JSON 类型可以在建表后继续扩展字段,不需要每次改埋点都做一次迁移。
  • 倒排索引:可以直接对 error_message、tool_input 这类文本字段建立倒排索引,实现类似 ES 的检索能力。
  • 明细+聚合一体化:既可以存最细的 trace 明细,也可以用物化视图或聚合表做秒级预聚合,避免维护两套平台。

这几点放在一起,意味着我们不需要在链路存储上用 ES、在指标上用 Prometheus、在成本报表上再搞一套 ClickHouse。一套 Doris 就能把明细检索和聚合分析打通。

2.3 为什么不是 ES、Prometheus 或 ClickHouse

选型时我们确实对比过其他方案。ES 的全文检索能力很强,但高基数聚合不稳定,集群规模一大,磁盘和内存消耗都很夸张;而且 ES 的数据模型偏向日志文本,做"按 agent 版本统计成功率、P99 延迟、token 数"这类查询,性能和 SQL 表达力都不理想。

Prometheus 的问题是数据模型太窄。它适合 counter、gauge、histogram,但 Agent 可观测性强依赖 trace 明细和上下文文本,指标只是其中一小部分。如果以 Prometheus 为核心,还要在外面再接 Loki 和 Tempo,链路变长,维护成本直接上升。ClickHouse 分析性能确实好,但运维复杂度高,同时对高并发点查和小批量实时写入的友好度不如 Doris。我们团队没有专职 DBA,Doris 的部署和运维相对更轻量,这在实际落地中是很重要的加分项。

2.4 架构定位:少一套系统,多一个底座

最终我们把 Doris 定位成整个 AI Agent 可观测性平台的统一数据底座。所有埋点经过采集端统一处理,进入 Kafka,再由 Routine Load 写入 Doris。Doris 上面直接承载 Grafana 看板、告警查询、内部复盘工具和运营报表。这个定位让我们只维护一套集群,同时服务研发、算法、产品和运营四类角色。

这里我特别想强调一点:选 Doris 不是因为它是万能的,而是因为这个场景的核心矛盾是"数据既要写得快、又要查得快、还要字段灵活"。Doris 在这三者之间给了我们比较均衡的解法。实际跑了几个月之后,集群规模不大,但稳定性和查询并发上都够用,这让我更加确信选型方向是对的。

3. 埋点、采集与写入:把 Agent 每一步拍平到一行行记录

3.1 先想清楚要观测什么

在做埋点之前,我问过团队一个问题:如果一次线上事故只能留三个维度的数据,你希望是哪三个?最后大家一致认为,第一是决策路径,也就是 Agent 从收到请求到最终返回经历了哪些节点;第二是每一跳的模型和工具明细,包括 prompt 片段、输出文本、token;第三是状态和成本,判断这次执行到底成功没有,花了多少钱。这三个维度构成了我们埋点的核心。

围绕这个目标,我们确定了埋点粒度:每个 Agent 节点执行结束都产生一条事件,事件里包含统一 header 和节点专属 body。header 包括 event_id、trace_id、agent_session_id、agent_version、node_type、node_name、开始时间、结束时间、状态;body 则按节点类型灵活填充。LLM 节点记 model、prompt_tokens、completion_tokens、input_text、output_text;工具节点记 tool_name、tool_input、tool_output、error_message;分支节点记 condition_name、condition_result。这样一条条事件组合起来,就能完整还原一次 Agent 执行。

3.2 采集端:从回调 Hook 到统一 Collector

采集端我们实现了两层。第一层是框架接入层,针对不同的 Agent 框架分别写适配器。LangChain 和 LangGraph 都有回调机制,LangChain 的 BaseCallbackHandler 可以在 on_llm_start、on_llm_end、on_tool_start、on_tool_end 等钩子里拿到上下文;LangGraph 可以通过节点装饰器或在状态里注入 recorder 来记录节点状态。Java 侧如果接到 Spring AI,也有对应的 Observation API,可以统一输出 OpenTelemetry 格式数据。第二层是统一 Collector,收到框架回调后再做字段归一化、链路补全、协议转换,最终把数据异步发送到 Kafka。

采集端最容易被忽略的是异常兜底。回调埋点本身不能影响主链路,如果采集代码抛异常,必须吞掉并降级为本地日志记录。我们一开始因为埋点代码里拉了远程配置,导致 Agent 响应直接超时,后来改成所有埋点写入本地环形缓冲,由独立线程批量上报,彻底把影响降到最低。

3.3 数据通道:为什么中间要加 Kafka

最初我们图省事,直接在应用进程里调用 Doris Stream Load,把每条埋点单条导入。结果并发一上来,Doris 的导入事务数量暴涨,集群写入压力骤增,还出现过峰值丢数据的情况。后来我们把链路调整成 Agent 应用 -> Kafka -> Doris Routine Load,由一个独立的数据管道统一负责写入。

Kafka 在这里的作用不光是削峰填谷。埋点数据在 Kafka 里可以做一次预处理:比如把 message 里的 JSON 字段做规范化、过滤掉不需要保存的大文本、按 trace_id 做分区保证同一链路的数据有序。Routine Load 从 Kafka 拉取数据后自动写入 Doris,整个过程是准实时的,延迟基本在秒级以内,足够支撑看板和告警。

如果你的 Agent 并发量还没有那么高,也可以直接用 Stream Load 做攒批写入,但建议在代码里禁止单条导入。我们后来把应用侧埋点统一走 Kafka,反而让写入链路更简单,因为 Doris 侧只需要维护 Routine Load 任务,不需要关心生产端波动。

3.4 Doris 写入链路:Stream Load / Routine Load 与事务一致性

Routine Load 的消费位点管理和 Doris 表的导入状态是绑定的。如果表结构变更或任务重启,Routine Load 会从上次提交的 offset 继续消费,基本不丢数据。但如果埋点数据本身存在重复,比如 Agent 重试导致同一 trace_id 重复上报,Routine Load 不会自动去重。我们有两种处理思路:一种是在 Kafka 生产端保证消息幂等,另一种是在 Doris 表上设置合适的 duplicate key 和 sequence 列,导入时根据事件序号更新。这里给个建议:能往上排查的问题不要压到数据库层解决,Kafka 生产端的去重通常更简单。

写入模型上,我们把所有节点事件统一写进一张大的明细表,而不是拆成 LLM 表、工具表、流程表。原因是查询时通常要跨节点类型关联分析,拆表会让 SQL 变得很复杂。宽表虽然会有冗余,但 Doris 列存储对冗余的容忍度很高,实测下来写入和查询都没有成为瓶颈。

4. Schema 设计:不要为了灵活性掉进全部塞 JSON 的坑

4.1 用明细事实表承载原始行为

在做表设计时,团队内部有过争论:是不是所有字段都放 JSON,这样 Agent 版本升级后不需要改表?我明确反对。因为 Doris 的 JSON 类型虽然支持,但 JSON 字段在过滤、排序、聚合上的性能远不如普通列,而且可观测性查询里有大量对 model、status、agent_version 这类字段的过滤和 group by,必须把它们抽成独立列。

我的设计原则是:高频过滤字段、高频聚合字段、需要排序和分区的字段必须显式建模;低频扩展字段、不确定结构字段才放进 JSON。这样既能保证查询性能,又不会每次埋点加一个属性就改表。下面的建表语句就是我们生产环境跑过的核心表,稍微简化了一些。

CREATE TABLE IF NOT EXISTS agent_execution_detail ( event_id VARCHAR(128) COMMENT '事件唯一ID', trace_id VARCHAR(128) COMMENT '全局链路ID', span_id VARCHAR(128) COMMENT '当前节点Span ID', parent_span_id VARCHAR(128) COMMENT '父节点Span ID', agent_session_id VARCHAR(128) COMMENT 'Agent会话ID', agent_version VARCHAR(64) COMMENT 'Agent版本', node_type VARCHAR(32) COMMENT '节点类型: llm/tool/condition/loop', node_name VARCHAR(128) COMMENT '节点名称', tool_name VARCHAR(128) COMMENT '工具名称', llm_provider VARCHAR(64) COMMENT '模型供应商', llm_model VARCHAR(64) COMMENT '模型名称', prompt_tokens BIGINT COMMENT '输入token数', completion_tokens BIGINT COMMENT '输出token数', total_tokens BIGINT COMMENT '总token数', total_cost_usd DECIMAL(20,8) COMMENT '本次节点成本', status VARCHAR(16) COMMENT 'success/failed/timeout', error_message STRING COMMENT '错误信息', input_text STRING COMMENT '输入文本摘要或原文', output_text STRING COMMENT '输出文本摘要或原文', extra_meta JSON COMMENT '扩展字段', start_time DATETIME COMMENT '节点开始时间', end_time DATETIME COMMENT '节点结束时间', latency_ms BIGINT COMMENT '节点耗时' ) DUPLICATE KEY(event_id) PARTITION BY RANGE(start_time) () DISTRIBUTED BY HASH(trace_id) BUCKETS 32 PROPERTIES ( "replication_num" = "3", "dynamic_partition.enable" = "true", "dynamic_partition.time_unit" = "DAY", "dynamic_partition.start" = "-7", "dynamic_partition.end" = "1" );

这张表用 event_id 作为 duplicate key,用 trace_id 做分桶键,按天动态分区。这样同一链路的事件大概率落在同一个分桶内,查询 trace 时能有效减少扫描。partition 的保留周期我建议根据自己的数据量和查询需求来定,我们默认保留 7 到 30 天,再久的数据转存冷集群或归档。

4.2 索引和排序键的取舍

Doris 的 Duplicate Key 模型里,建表时指定的 key 列会作为排序键参与前缀索引。我们这里把 event_id 作为唯一键,但这样对 trace_id 查询并不友好,因为 trace_id 不在排序键首位。为了让 trace_id 点查更快,可以再给 trace_id 建立一个倒排索引。Doris 的倒排索引支持用USING INVERTED对普通列建索引,文本列尤其适合。

CREATE INDEX idx_trace_id ON agent_execution_detail (trace_id) USING INVERTED; CREATE INDEX idx_error_msg ON agent_execution_detail (error_message) USING INVERTED;

我这里踩过一个坑:一开始只依赖排序键,以为 trace_id 查询能走前缀索引,结果因为排序键首位是 event_id,实际查询变成扫描大量分桶。后来建了 trace_id 倒排索引,点查速度提升了一个量级。如果你也遇到"明明有索引还是慢"的问题,先检查数据分布和排序键顺序,再考虑加倒排索引。

4.3 会话画像表与成本汇总表

明细表解决的是"查具体某一次链路",但运营侧经常需要按会话聚合。比如某个用户会话一共调了多少次 LLM、花多少钱、最后成功没有。每次从明细表实时聚合当然可以,但会话数量一旦上来,查询耗时不可控。所以我们额外同步了一张 agent_session_agg 聚合表,按 agent_session_id 做聚合,字段包括会话总耗时、总 token、总成本、成功/失败次数、最终状态、用户反馈标签等。

这张聚合表不需要自己费力维护。生产上我们选择由 Doris 的物化视图来自动维护,这里给一个简化版的建表思路。物化视图在 Doris 里可以做到明细表写入后自动更新聚合结果,适合会话级和分钟级维度。

CREATE MATERIALIZED VIEW mv_agent_session_stats AS SELECT agent_session_id, agent_version, DATE_FORMAT(start_time, 'yyyy-MM-dd HH:00:00') AS window_time, COUNT(*) AS node_count, SUM(total_tokens) AS session_tokens, SUM(total_cost_usd) AS session_cost, SUM(IF(status = 'success', 1, 0)) AS success_nodes, AVG(latency_ms) AS avg_latency_ms FROM agent_execution_detail GROUP BY agent_session_id, agent_version, DATE_FORMAT(start_time, 'yyyy-MM-dd HH:00:00');

注意 Doris 物化视图对表达式的支持有限,聚合字段尽量保持简单。我们第一批物化视图就写得太复杂,带了多个 case when 和字符串截断,结果构建一直失败,最后拆成多个简单视图才通过。

4.4 分区、分桶、排序键、索引怎么定

如果你手里没有成熟的 Doris 使用经验,可以直接参考我们这一套默认策略:时间列做 RANGE 分区,高基数字段做 HASH 分桶,中低基数字段进入排序键或倒排索引。排序键不要放太长的字符串,event_id、trace_id 都是 128 字符的字符串,作为排序键会降低前缀索引效率。所以我们把 event_id 放 sort key,但实际高频查询 trace_id 用倒排索引,靠索引把 rowid 拉出来再回表。

分桶数量按数据量来,不要盲目设大。我们的核心明细表每天大概新增 3 亿行,分桶设 32,单桶数据量在千万行级别,对大多数查询来说已经足够。分桶太大反而导致导入时小文件变多,后台 compaction 压力很大。这个参数要随着业务量增长慢慢调,不是一次定死。

5. 透明化之后,我们每天都在跑的三类查询

5.1 会话级复盘:用 trace_id 重建决策路径

排查线上问题最常用的场景,是输入一个 trace_id,把所有相关节点按时间顺序查出来。我们内部叫"时间旅行"。

SELECT span_id, parent_span_id, node_type, node_name, tool_name, llm_model, status, latency_ms, input_text, output_text, error_message FROM agent_execution_detail WHERE trace_id = 'xxx' ORDER BY start_time ASC;

拿到结果后,我会按 parent_span_id 画一棵树,就能清楚看出模型在某一步是怎么决策的。比如有一次用户反馈 Agent 把天气查成了股票行情,回溯时发现模型在工具选择阶段先选了 stock_search,而不是 weather_search,原因是 prompt 里把"查询"的权重描述得太泛化。这个结论直接指导了 prompt 里工具说明的重写。没有 trace 数据的话,这类问题根本无从下手。

5.2 群体洞察:按版本、模型、节点聚合

复盘单条 trace 能解决事故定位,但无法回答"新版本是不是真的更好了"。我们每周会跑一次群体统计,按 agent_version 和 llm_model 分组,看成功率、P50/P95 延迟、token 消耗。

SELECT agent_version, llm_model, COUNT(*) AS total_nodes, SUM(IF(status = 'success', 1, 0)) AS success_nodes, ROUND(SUM(IF(status = 'success', 1, 0)) / COUNT(*), 4) AS success_rate, ROUND(PERCENTILE(latency_ms, 0.95), 0) AS p95_latency_ms, SUM(total_tokens) AS total_tokens, SUM(total_cost_usd) AS total_cost_usd FROM agent_execution_detail WHERE start_time >= NOW() - INTERVAL 7 DAY GROUP BY agent_version, llm_model ORDER BY agent_version DESC;

这张报表帮助我们上线过很多次 Agent 版本灰度。我特别推荐把成本字段放到这个聚合里,因为模型版本之间的成本差异往往非常大,只比较成功率会漏掉关键决策。

5.3 异常下钻:从指标找到具体那一条事故链路

告警一般来自指标,比如 p95 延迟突然升高或者失败率超过阈值。得到指标信号后,需要快速定位到具体的 trace。这时候 Doris 的倒排索引就能派上用场:

SELECT trace_id, COUNT(*) AS bad_node_count FROM agent_execution_detail WHERE start_time >= NOW() - INTERVAL 10 MINUTE AND status != 'success' AND error_message MATCH 'timeout|rate limit' GROUP BY trace_id ORDER BY bad_node_count DESC LIMIT 20;

拿到 trace_id 列表后再回明细表逐条看链路。这个"指标 -> 异常 trace -> 根因节点"的下钻路径,是我认为可观测性平台最重要的能力。很多团队把精力花在建复杂看板上,却忽略了下钻路径是否顺畅,这是本末倒置。

5.4 评估反馈关联:把用户反馈灌回 Agent 样本

Agent 质量评估不能只靠工程师自己打分。我们把用户反馈和人工标注结果异步写回 Doris 的一张 feedback 表,按 agent_session_id 关联明细数据,形成带质量标签的样本集。

SELECT f.feedback_type, a.node_type, a.llm_model, COUNT(*) AS node_count, SUM(a.total_tokens) AS total_tokens FROM agent_execution_detail a INNER JOIN agent_feedback f ON a.agent_session_id = f.agent_session_id WHERE f.feedback_time >= NOW() - INTERVAL 7 DAY GROUP BY f.feedback_type, a.node_type, a.llm_model;

有了这个关联之后,我们可以用数据回答"用户点踩的会话里,是不是工具调用失败更多""差评会话和好评会话在 token 消耗上差多少"。这些结论直接变成迭代 prompt 和工具链路的依据。可观测性平台从"追查事故"升级到"驱动质量改进",靠的就是这种跨域关联能力。

6. 生产环境踩过的坑:并发写入、乱序与字段膨胀

6.1 高并发写入:单条 Stream Load 是灾难,攒批才是常态

写这套系统最开始的版本里,Agent 每个节点跑完就立刻调一次 Stream Load,把一条 JSON 导入 Doris。并发不高时没问题,但上了灰度流量后,单条导入的事务数和对应的磁盘 IO 一路上涨,Doris 集群的 BE 节点 CPU 经常被打到 80% 以上,查询性能也跟着被拖垮。

后来的解法是引入 Kafka + Routine Load,以及配合应用侧的数据攒批。应用侧每 2 秒或者攒够 500 条才向 Kafka 发送一次,Routine Load 侧又会按最大消费条数批量写入 Doris。两个环节的批量效果叠加起来,Doris 收到的写入事务数下降了 90% 以上。想追高并发,核心是尽量减少写入小文件,而不是增加导入频率。

6.2 乱序与重复:幂等需求和 sequence 的处理

Agent 框架为了容错,经常会对工具调用做重试。重试会导致同一条事件被上报两次。Doris 的 Duplicate Key 模型本身就允许重复,所以表里会出现两个完全一样的 event_id。我们需要在查询时去重,但这样做会让 SQL 更重。一个更干净的方案是在生产端确保事件 ID 唯一:回调里如果拿不到事件 ID,就自己生成一个 uuid,以 trace_id + node_type + node_name + start_time 的哈希作为 event_id。这样即使框架内部重试,回调产生的还是同一个 ID。

乱序问题主要来自 Kafka 多分区消费。同一个 trace_id 的不同节点事件如果进到不同分区,消费时写入顺序会乱,对按 start_time 排序的查询影响不大,但如果你依赖后写入的事件更新前一个事件的父节点状态,可能出现数据不一致。解决办法是 Kafka 生产端按 trace_id 做分区,保证同一个链路的事件进入同一个分区,维护顺序。

6.3 字段演化:Agent 版本一变,埋点就多几个字段

Agent 迭代非常快,今天加一个 reranker,明天换一个工具,埋点字段跟着变。我们的 JSON 列 extra_meta 专门用来兜底这类变化。比如新版本增加了一个 orchestrator_mode 字段,在 Agent 插件层把字段写入 extra_meta,Doris 表不需要改动就能消费。等确认这个字段具备长期分析价值,再通过表结构变更把它提升为独立列。

这里要注意 JSON 字段在导入时如果格式不合法,Routine Load 会把这批数据标记为错误。我们在 Kafka 做了一层格式校验,非法 JSON 直接丢弃并打点,避免把 Doris 导入任务搞挂。

6.4 数据膨胀:该采样的采样,该压缩的压缩

Prompt 和工具返回的原始文本是数据膨胀的大头。全量保留原始文本,一天几个 TB 都很正常。我们的策略是分字段处理:prompt_tokens、cost、状态这类量化字段全量保留;input_text、output_text 根据内容长度做截断或摘要,超过 2000 字的部分只保留前 2000 字;对超大工具响应,直接存哈希值和关键字段,不存全文。另外,对非核心 Agent 版本,可以按 10% 比例采样,只保留 trace 头尾数据。

Doris 的列式存储本身对字符串有很高的压缩比。我们实测,原始 JSON 数据经 Stream Load 导入 Doris 后,存储膨胀比大约在 1.2 到 1.5 倍之间,相比存日志系统动不动两三倍膨胀,已经好了很多。

6.5 查询侧常见慢查:小心高基数 group by

有一段时间,运营团队经常跑"按用户维度统计每天消耗 token"的报表,用户数一多,group by 维度到达千万级,查询经常超时。Doris 对高基数 group by 的支撑比一般引擎强,但也不能无脑用。

我的建议是,把高频但基数没那么高的维度放进分桶键或排序键,而真正达到亿级基数的 user_id、session_id 这类字段,尽量只做点查或范围查,不做全表级 group by。如果业务确实需要按用户维度出聚合报表,就建独立的用户聚合表,由任务定期写入,而不是直接查明细大表。

7. 可观测性平台的演进路径:从追查事故到驱动优化

7.1 阶段一:先把黑盒还原成白盒

我们最开始的目标很简单:把 Agent 的执行过程能存下来、能查出来。这个阶段的产出是一张 agent_execution_detail 明细表、一个按 trace_id 搜索的页面、一份按小时的成功率和延迟看板。虽然功能朴素,但它已经解决了"事故回溯靠猜"的问题。任何可观测性建设都要从这个基础版开始,不要一上来就想着上大而全的反馈闭环。

7.2 阶段二:从白盒到度量,让成本和质量可见

第二阶段是把成本、质量标准纳入数据模型。我们增加了 token、成本、用户反馈、人工标注字段,并通过聚合表让运营和产品自助查询。这个阶段的收益非常直接:模型的选型从"凭感觉"变成"看数据"。我们曾经在对比两个模型时发现,成本低的模型虽然成功率略低,但结合自动重试后整体成本反而更低,这个结论完全是通过 Doris 上的聚合数据得到的。

7.3 阶段三:从度量到闭环,让可观测性驱动 Agent 迭代

现在我们已经开始尝试第四阶段:把可观测性数据接入自动化流程。比如当某个 agent_version 的失败率超过阈值时,自动触发告警并生成失败样本集;评估服务从 Doris 拉取最近一周的差评样本,自动跑回归;甚至尝试根据成本数据调整模型路由策略。这个闭环运转起来之后,Agent 迭代才能真正做到有数据支撑,而不是每次发新版都像开盲盒。

7.4 我的个人体会和给后来者的建议

回看整个实践过程,最想分享的经验有三条:第一,埋点设计比查询设计更重要,字段统一和链路 ID 规范必须在 Agent 框架接入阶段就定好,否则后面所有分析都会受制于脏数据;第二,不要在可观测性系统里过度设计,先解决"能不能查到 trace",再扩展"能不能自动优化";第三,Apache Doris 这类分析型数据库选作底座时,要充分利用它的批量导入和索引特性,而不是把它当成普通 MySQL 用,高并发写入一定要走批量和队列。

如果你正在搭自己的 AI Agent 可观测性平台,我建议从一张足够宽的明细表开始,先把 trace、状态、token、成本完整记录下来,再逐步加聚合和反馈。你会发现,当黑盒变成透明之后,Agent 的很多问题其实并没有那么玄学,只是以前你根本没机会看到它到底做了些什么。

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

基于启发式算法的换热器PI控制器参数整定与Matlab实现

换热器温度控制,参数整定这件事,看着不难,实际调起来特别头大。系统是大惯性加纯滞后,Kp稍微给大一点出口温度就来回振荡,给小了又半天爬不到设定值。Ziegler-Nichols整出来的参数往往太激进,拿到现场根本不…

作者头像 李华
网站建设 2026/10/1 18:29:53

大模型工具调用(Function Calling)深度解析:让 AI 突破限制

本文深入剖析了大模型工具调用(Function Calling)的底层运作机制,从理论到实战,详细介绍了如何让 AI 突破赛博空间的限制,具备操作真实世界业务系统的能力。文章首先阐述了大模型在处理真实业务时存在的无法访问远程和…

作者头像 李华
网站建设 2026/10/1 18:29:14

Windows桌面Agent实战:OpenClaw与Claude Code本地化部署指南

1. 项目概述:当AI从对话框走向任务栏——桌面Agent的真实工作现场“AI 不再只陪你聊天,它开始替你上班了!”这句话最近在技术圈刷屏,不是营销话术,而是Windows用户真实截图里正在运行的进程:一个叫OpenClaw…

作者头像 李华
网站建设 2026/10/1 18:28:28

AI课堂常态化落地的全链路服务:从选型到运营迭代的关键方法

很多学校在引入AI课堂项目时,都走着一条相似的路:先采购一批智能设备,再部署一套教学平台,开一场全员培训,然后就等着老师们"自发地用起来"。结果往往是一学期过去,设备开机率不到两成&#xff0…

作者头像 李华
网站建设 2026/10/1 18:28:06

银行面试通关指南:高频问题、答题思路与避坑经验

先说个结论:银行面试问题没有网上传的那么玄乎,但也没有“学历够了就能进”那么简单。这几年我陆续参加过几家银行的校招和社招面试,也曾以内部员工身份旁听过面试官的复盘会,最大的感受是:大部分人的面试准备方向搞反…

作者头像 李华
网站建设 2026/10/1 18:27:49

基于Spring Boot的大学生房屋租赁系统全解析:从JavaWeb到Docker部署

Spring Boot 的大学生房屋租赁系统,前前后后我做过好几个版本,有给毕设做的,也有给实际业务改造的。这个“基于 JavaWeb 的大学生房屋租赁系统的设计与实现”,属于非常典型的 Java 全栈入门偏进阶项目。放到今天来看,它…

作者头像 李华