1. 从一堆 Trace 里挖出 Agent 的真实行为:为什么这件事值得认真做
做 Agent 开发的朋友大概率都经历过这个场景:线上跑着几十上百个会话,每个会话里 Agent 要调用工具、检索知识、做多轮推理,一天下来 Trace 数据能堆到几个 G。打开日志一看,密密麻麻的 span 记录,每条都带着时间戳、输入输出、token 消耗、工具调用链路。你想知道"用户到底在问什么类型的问题""Agent 在哪些环节容易卡住""哪类会话的 token 消耗异常高",靠肉眼翻日志基本等于大海捞针。
这就是"智能聚类"要解决的问题。简单说,它做的事情是:把海量 Trace 数据里的每个 Session 或每个 Agent 执行片段,通过 Embedding 转成向量,再用聚类算法自动分组,让行为模式相似的那些会话自己聚到一起。你不用提前定义"什么算客服类问题""什么算代码生成类问题",聚类会告诉你数据里天然存在哪几类模式。做完聚类之后,你再去看每一类的典型 Trace,就能快速理解 Agent 在真实场景下的行为分布和表现差异。
这套方法适合谁?如果你是大模型开发工程师、Agent 平台的建设者、或者正在做 AI 应用可观测性相关的工作,这套思路可以直接落地。如果你只是刚接触 Agent 开发,想了解怎么评估自己的 Agent 表现,这篇文章也能给你一个可操作的框架。核心关键词就几个:Trace、Agent、Session、Embedding、智能聚类,我会围绕这几个点把整条链路拆开讲。
我自己的经验是,很多团队在 Agent 上线初期只盯着"成功率"和"平均延迟"这两个指标,结果出了问题根本定位不到根因。Trace 数据里其实藏着大量信息,只是缺少一个自动化的手段把它们组织起来。聚类就是那个把"原始矿石"变成"可读地图"的环节。
2. 整体设计思路:Trace、Session、Embedding、聚类怎么串起来
2.1 先搞清楚 Trace 和 Session 的关系
在动手之前,必须把数据模型理清楚。Trace 通常指的是一次完整请求的调用链,里面包含多个 span,每个 span 代表一个操作单元,比如一次 LLM 调用、一次工具执行、一次检索。Session 则是更高层的概念,它把同一个用户在一段时间内的多次交互串在一起。一个 Session 里可能有多个 Trace,一个 Trace 里可能有多个 span。
做聚类的时候,你得先决定聚类的粒度。我试过三种粒度,各有适用场景:
| 聚类粒度 | 数据单元 | 适合回答的问题 | 计算成本 |
|---|---|---|---|
| Session 级 | 整个会话的所有交互拼接 | 用户意图分布、会话整体表现 | 低 |
| Trace 级 | 单次请求的完整链路 | 单次任务类型分布、失败模式 | 中 |
| Span 级 | 单个操作单元 | 工具调用模式、LLM 调用特征 | 高 |
大多数情况下,从 Session 级或 Trace 级入手性价比最高。Span 级聚类虽然细,但噪声大,而且数量级可能是 Session 的几十倍,Embedding 和聚类的成本会迅速上升。
2.2 为什么用 Embedding 而不是关键词匹配
有人可能会问:我直接用关键词分类不行吗?比如包含"退款"的归一类,包含"代码"的归一类。这种做法在早期确实能用,但问题很明显。第一,用户表达千变万化,"我要退钱""这个能退吗""申请售后"其实是一类意图,关键词很难穷举。第二,Agent 的 Trace 里不只有用户输入,还有工具调用序列、中间推理步骤,这些结构化信息用关键词根本表达不了。第三,你事先不知道数据里有哪些类别,关键词分类的前提是你已经知道要分几类,这跟"探索性分析"的目标是矛盾的。
Embedding 的好处是把文本映射到高维语义空间,语义相近的内容在向量空间里距离就近。这样聚类算法就能自动发现数据里的自然分组。对于 Trace 数据,你可以把用户输入、Agent 的推理摘要、工具调用名称序列拼接成一段文本再做 Embedding,也可以对不同类型的字段分别 Embedding 再加权融合。
2.3 聚类算法的选型逻辑
聚类算法不是随便选一个就行。我实际用下来,这几种最常见:
- K-Means:速度快,适合数据量大、类别数大致已知的场景。缺点是要提前指定 K 值,而且对非球形簇效果一般。
- HDBSCAN:不需要指定类别数,能识别噪声点,适合探索性分析。缺点是参数敏感,数据量大时比较慢。
- 层次聚类:能生成聚类树,方便你从粗到细观察。缺点是复杂度高,不适合超大规模数据。
- BERTopic 这类基于主题模型的方案:结合了 Embedding 和主题发现,输出可解释性好,适合做报告。
我的建议是:第一轮探索用 HDBSCAN 或 BERTopic,先看看数据里大概有多少类、每类长什么样;等对数据分布有感觉了,如果要做线上实时分类,再换成 K-Means 或者训练一个分类器。这样既保证了探索阶段的灵活性,又兼顾了后续工程化的效率。
注意:聚类结果没有"绝对正确"这一说。同一批数据,换个 Embedding 模型、换个距离度量、换个参数,结果可能都不一样。关键是要结合业务场景去解读,而不是追求某个"标准答案"。
3. 核心细节拆解:从原始 Trace 到可解读的聚类结果
3.1 Trace 数据的预处理与特征构造
原始 Trace 数据通常长这样:一堆 JSON 记录,字段包括 session_id、trace_id、span_name、start_time、end_time、input、output、token_count、status 等。直接把这些丢给 Embedding 模型效果不会好,因为里面有很多噪声字段,而且结构化信息没有被利用。
我的做法是分三步构造文本表示:
第一步,抽取关键字段。对每个 Session,按时间顺序把用户输入、Agent 的最终回复、调用的工具名称列表、是否发生错误这些信息抽出来。工具名称列表特别重要,它能反映 Agent 的行为模式,比如"检索+总结"和"代码执行+测试"明显是两类任务。
第二步,拼接成自然语言描述。不要简单用逗号拼接,而是写成一段有结构的文本。比如:
用户请求:帮我查一下上个月的销售数据并生成图表 Agent 调用的工具:database_query, data_analysis, chart_generation 执行结果:成功 交互轮数:3这种格式对 Embedding 模型更友好,因为模型在预训练时见过大量类似的结构化文本。
第三步,对超长文本做截断或摘要。Embedding 模型都有最大输入长度限制,一般 512 到 8192 token 不等。如果 Session 很长,可以只保留前几轮和最后几轮,或者用一个小模型先做摘要再 Embedding。我实测下来,保留"首轮用户意图+工具调用序列+最终状态"这三样,信息损失最小。
3.2 Embedding 模型的选择与对比
Embedding 模型的选择直接决定聚类质量。我对比过几个主流方案:
| 模型类型 | 代表模型 | 维度 | 优势 | 劣势 |
|---|---|---|---|---|
| 通用文本 Embedding | text-embedding-3-small/large | 1536/3072 | 语义理解强,多语言 | 成本较高,维度大 |
| 开源 Embedding | bge-large, gte-large | 1024 | 可本地部署,成本低 | 需要自己调优 |
| 领域微调 Embedding | 基于业务数据微调 | 自定义 | 贴合业务语义 | 需要标注数据 |
如果数据量不大、预算充足,直接用商用 API 最省事。如果数据敏感或者量特别大,本地部署开源模型更划算。我自己的经验是,对于 Trace 这种半结构化文本,通用 Embedding 模型已经够用,不一定非要微调。但如果你的业务领域特别垂直,比如医疗、法律,微调能带来明显提升。
还有一个细节:Embedding 的维度会影响聚类速度和效果。高维向量表达能力强,但距离计算慢,而且容易受"维度灾难"影响。实践中 768 到 1536 维是比较平衡的选择。如果维度太高,可以先做 PCA 或 UMAP 降维再聚类,UMAP 降维后聚类效果往往比直接在高维空间聚类更好。
3.3 聚类参数调优的实操方法
以 HDBSCAN 为例,核心参数有两个:min_cluster_size 和 min_samples。前者控制一个簇最少要有多少个点,后者控制核心点的邻域密度要求。
调参的思路是这样的:先看数据总量,假设你有 10000 个 Session,那 min_cluster_size 可以从 50 开始试,也就是至少 0.5% 的数据才算一个簇。如果发现簇太多太碎,就调大;如果发现很多点被归为噪声,就调小 min_samples。
我一般会跑一组参数网格,然后用轮廓系数和 Calinski-Harabasz 指数来辅助判断。但这两个指标只能参考,最终还是要人工看每个簇的典型样本。因为聚类是为了理解数据,不是为了刷指标。
实操心得:聚类前一定要做归一化。Embedding 向量通常已经归一化了,但如果你自己拼接了额外特征(比如 token 数量、交互轮数),这些数值特征的量纲和 Embedding 完全不同,必须标准化后再融合,否则数值大的特征会主导距离计算。
4. 完整实操流程:从数据到洞察的每一步
4.1 数据导出与清洗
假设你的 Trace 数据存在 ClickHouse 或者 Elasticsearch 里,第一步是导出。我一般会写一个查询,按时间范围拉取最近 7 天或 30 天的数据,字段包括 session_id、trace_id、span 列表、用户输入、最终输出、状态、token 消耗。
导出后要做清洗:
- 去掉测试账号和内部调试产生的 Session
- 去掉明显异常的记录,比如 token 数为 0 或者状态为超时的
- 对超长 Session 做截断,保留关键轮次
- 统一时间格式和字段命名
这一步看起来简单,但实际做的时候坑很多。比如有些 Session 的结束时间缺失,有些工具调用记录不完整。我的建议是宁可少要一些数据,也要保证质量,因为脏数据会严重干扰聚类结果。
4.2 构造 Embedding 并降维
清洗完之后,对每个 Session 构造文本描述,然后批量调用 Embedding 接口。这里要注意并发控制和重试机制,因为批量调用容易触发限流。我一般用 10 到 20 的并发,每批 100 条,失败自动重试三次。
拿到 Embedding 后,先做 UMAP 降维到 5 到 10 维。为什么是 5 到 10 维而不是 2 维?因为 2 维适合可视化,但信息损失太大,聚类效果会下降。5 到 10 维能在保留主要结构的同时降低计算复杂度。降维后再做聚类,最后用 2 维的 UMAP 结果做可视化展示。
import umap import hdbscan import numpy as np # embeddings 是 N x D 的矩阵 reducer = umap.UMAP(n_components=10, n_neighbors=15, min_dist=0.1, metric='cosine') reduced = reducer.fit_transform(embeddings) clusterer = hdbscan.HDBSCAN(min_cluster_size=50, min_samples=10, metric='euclidean') labels = clusterer.fit_predict(reduced)这段代码里,n_neighbors 控制 UMAP 关注局部还是全局结构,值小更关注局部,值大更关注全局。min_dist 控制降维后点的聚集程度。这两个参数需要根据数据规模调整,数据量大就适当调大 n_neighbors。
4.3 聚类结果的可视化与解读
聚类跑完之后,你会得到每个 Session 的簇标签,-1 表示噪声点。接下来最重要的一步是解读每个簇的含义。
我的做法是:对每个簇,随机抽 10 到 20 个典型样本,看它们的用户输入、工具调用序列、执行结果。然后给每个簇起一个描述性的名字,比如"数据查询与报表生成""代码调试与修复""多轮澄清型对话"。
可视化方面,用 2 维 UMAP 散点图,每个点代表一个 Session,颜色代表簇标签。这样能直观看到簇的分布和重叠情况。如果两个簇在图上严重重叠,说明它们的语义确实接近,可以考虑合并。
| 簇编号 | 样本数 | 典型特征 | 建议命名 |
|---|---|---|---|
| 0 | 1200 | 数据库查询+图表生成 | 数据查询类 |
| 1 | 800 | 代码执行+错误修复 | 代码调试类 |
| 2 | 600 | 多轮追问+澄清 | 澄清对话类 |
| 3 | 400 | 检索+总结 | 知识问答类 |
| -1 | 300 | 无明显模式 | 噪声/长尾 |
4.4 把聚类结果接入日常监控
聚类不是跑一次就完事。我建议把它做成一个定期任务,比如每天凌晨跑一次,对比前一天和后一天的簇分布变化。如果某个簇的样本量突然暴涨,或者出现了新的簇,那可能意味着用户行为发生了变化,或者 Agent 的某个功能出了问题。
具体做法是:把每个 Session 的簇标签写回数据库,然后在监控面板上展示各簇的占比趋势、各簇的平均 token 消耗、各簇的失败率。这样你就能回答"哪类任务的成本最高""哪类任务的失败率在上升"这类问题。
注意:簇的编号在不同批次之间是不稳定的,因为聚类算法每次跑出来的编号可能不一样。所以不要直接用编号做跨批次对比,而是要用簇的语义描述或者典型样本来对齐。
5. 常见问题与排查技巧实录
5.1 聚类结果全是噪声怎么办
这是最常见的问题。HDBSCAN 把大量点标为 -1,说明参数太严格或者数据本身太分散。排查顺序是:先看 min_cluster_size 是不是设太大了,调小试试;再看 UMAP 降维后的维度是不是太低,信息损失太多;最后检查 Embedding 质量,是不是文本构造得太粗糙。
我遇到过一次,聚类结果 80% 都是噪声,后来发现是 Embedding 时把整个 Session 的原始 JSON 直接丢进去了,里面全是无意义的字段名和括号。改成结构化文本描述后,噪声比例降到了 15% 以下。
5.2 簇的数量太多或太少
簇太多说明 min_cluster_size 太小,或者数据本身类别就多。簇太少说明参数太宽松,把不同类的东西合并了。我的经验是,对于 10000 条左右的 Session 数据,10 到 30 个簇是比较合理的范围。如果超过 50 个簇,基本就没法人工解读了。
另一个技巧是分层聚类。先用大粒度聚出 5 到 10 个大类,再对每个大类内部做细粒度聚类。这样既能把握整体分布,又能看到细节差异。
5.3 Embedding 成本太高怎么优化
如果数据量很大,Embedding 成本确实是个问题。几个优化方向:一是只对关键字段做 Embedding,不要全文都做;二是用更小的模型,比如从 large 换成 small;三是做增量更新,只对新产生的 Session 做 Embedding,老数据复用之前的向量;四是对相似 Session 做去重,很多 Session 其实高度重复,没必要每个都单独 Embedding。
我实测下来,通过字段筛选和去重,能把 Embedding 成本降低 60% 以上,而聚类效果几乎不受影响。
5.4 聚类结果不稳定怎么处理
同样的数据跑两次,结果不一样,这很正常。因为 UMAP 和 HDBSCAN 都有随机性。解决办法是固定随机种子,并且用多次运行取共识的方法。具体来说,跑 10 次聚类,然后统计每对 Session 被分到同一簇的频率,用这个频率矩阵再做一次层次聚类,得到最终稳定的结果。
这个方法叫共识聚类,虽然计算量大了点,但结果稳定性和可解释性都明显更好。对于需要长期跟踪的场景,我强烈建议用这种方式。
| 问题现象 | 可能原因 | 排查方向 | 解决建议 |
|---|---|---|---|
| 大量噪声点 | 参数过严/文本质量差 | 检查 min_cluster_size 和 Embedding 文本 | 调小参数,优化文本构造 |
| 簇数量过多 | 参数过松/数据类别多 | 检查 min_cluster_size | 调大参数,或做分层聚类 |
| 结果不稳定 | 算法随机性 | 检查随机种子 | 固定种子,用共识聚类 |
| 成本过高 | 数据量大/模型大 | 检查 Embedding 字段和模型 | 筛选字段,换小模型,增量更新 |
6. 几个我踩过的坑和实际体会
第一个坑是过早追求自动化。我一开始想做一个全自动的聚类流水线,每天自动跑、自动生成报告。结果发现聚类结果的解读必须有人参与,因为算法不知道你的业务含义。后来改成半自动:算法负责分组和可视化,人负责命名和判断。这样效率反而更高。
第二个坑是忽略时间维度。同样的用户请求,在 Agent 刚上线时和迭代几个月后,行为模式可能完全不同。如果聚类时不考虑时间,就会把不同阶段的数据混在一起。我的做法是按周或按月分别聚类,然后对比不同时期的簇分布变化,这样能看出 Agent 行为的演化趋势。
第三个坑是只看聚类结果,不看原始 Trace。聚类给的是宏观视图,但真正的洞察往往藏在具体的 Trace 里。我现在的习惯是,每发现一个有意思的簇,就抽几条原始 Trace 仔细看,经常能发现一些指标上看不出来的问题,比如 Agent 在某类任务上反复调用同一个工具、或者中间推理步骤明显冗余。
这套方法我用了大半年,最大的感受是:Trace 数据的价值远不止于排查故障。当你把海量 Trace 通过 Embedding 和聚类组织起来之后,它其实变成了一张 Agent 行为的"地图"。你能看到用户真正在用什么、Agent 在哪些地方表现好、哪些地方还有提升空间。这种全局视角,是单看几个指标或者翻几条日志永远给不了的。
如果让我给一个最小可落地的建议,那就是:先从一周的 Session 数据开始,构造简单的文本描述,用现成的 Embedding API 和 HDBSCAN 跑一遍,人工看 20 个簇的典型样本。整个过程一个人一天就能搞定,但收获的洞察可能比你看一个月监控面板还多。