基于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 或 ClickHouse2.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。我们团队没有专职 DBADoris 的部署和运维相对更轻量这在实际落地中是很重要的加分项。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_metaDoris 表不需要改动就能消费。等确认这个字段具备长期分析价值再通过表结构变更把它提升为独立列。这里要注意 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 的很多问题其实并没有那么玄学只是以前你根本没机会看到它到底做了些什么。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →