1. 从一堆看不懂的 Trace 说起:为什么 Agent 行为分析这么难
做过 Agent 开发的人都有一个共同的痛点:上线跑了一段时间,日志里堆了几十万条 Trace,每条 Trace 里嵌套着十几层 Span,每个 Span 又带着一堆属性字段。你想搞清楚"用户到底在问什么""Agent 在哪一步开始跑偏""哪个工具调用最耗时""为什么同一个问题有时候答得好有时候答得烂",结果打开日志面板一看,密密麻麻的 JSON 直接劝退。
这不是个别现象。Agent 系统和传统后端服务最大的区别在于:它的执行路径是非确定性的。同一个用户输入,因为模型采样的随机性、工具返回结果的差异、上下文窗口里历史消息的不同,Agent 可能走完全不同的推理链路。传统 APM 那套"按接口名聚合、按状态码分类"的思路,放到 Agent 场景里基本失效——你没法用固定的维度去归类一个"每次都不一样"的东西。
我自己的项目就踩过这个坑。早期做客服 Agent 的时候,我们按tool_name和status做了简单的聚合看板,结果发现 80% 的 Trace 都落在"其他"这个桶里,根本看不出问题。后来才意识到,Agent 的行为特征不在单个字段上,而藏在整条 Trace 的语义结构里——它调用了哪些工具、按什么顺序调、每步的输入输出语义是什么、整个 Session 里多轮对话是怎么演化的。
所以这篇文章要聊的核心思路就是:把 Trace 当作文本来理解,用 Embedding 把每条 Trace 映射成向量,再用聚类算法自动发现行为模式。这套方法不需要你预先定义分类规则,而是让数据自己"说话",把海量 Trace 自动分成若干有意义的簇,每个簇代表一种典型的 Agent 行为模式。配合 Session 维度的聚合,你还能看到同一类行为在多轮对话中是怎么演变的。
这套东西适合谁?如果你是大模型开发工程师、Agent 平台的建设者、或者正在做 Agent 可观测性相关的工作,那这篇内容基本可以直接抄作业。如果你只是刚入门 Agent 开发,也能从中理解"为什么 Agent 的行为分析不能照搬传统监控"这个根本问题。
2. 整体设计思路:为什么是 Embedding + 聚类,而不是规则匹配
2.1 传统规则匹配为什么在 Agent 场景下会崩
先说清楚为什么不能用老办法。传统做法是给 Trace 打标签,比如"调用了搜索工具"、"调用了代码执行"、"报错了"、"超时了"。这套逻辑在确定性系统里很好用,因为同样的输入必然产生同样的路径。但 Agent 不是这样。
我举个实际例子。用户问"帮我查一下北京明天天气,如果下雨就提醒我带伞"。Agent 可能的行为路径有:
- 路径 A:调用天气 API → 拿到"小雨" → 调用提醒工具 → 结束
- 路径 B:调用天气 API → 拿到"小雨" → 直接生成文本回复"记得带伞" → 结束
- 路径 C:先调用搜索工具确认"北京"指哪个城市 → 再调天气 API → 再调提醒工具 → 结束
- 路径 D:调用天气 API 失败 → 重试 → 成功 → 调提醒工具 → 结束
这四条路径,用规则匹配怎么归类?按工具名?A 和 B 工具序列不同但语义相近。按状态?D 有重试但最终成功。按耗时?C 明显更长但不代表有问题。你会发现任何单一维度的规则都无法捕捉"行为模式"这个整体概念。
更麻烦的是,Agent 的行为空间是开放的。你永远不知道用户会问出什么问题,也就无法预先枚举所有可能的工具组合。规则匹配的维护成本会随着 Agent 能力扩展而指数级上升。
2.2 Embedding 把"行为"变成可计算的向量
Embedding 的核心价值在于:它能把一段变长的、结构化的、语义复杂的Trace 文本,压缩成一个固定维度的稠密向量,而且这个向量保留了语义相似性——行为相似的 Trace,向量距离就近;行为差异大的 Trace,向量距离就远。
关键在于怎么把 Trace "文本化"。我的做法是构造一个行为描述模板,把 Trace 里的关键信息按固定格式拼成一段自然语言。比如上面路径 A 的 Trace,可以转成:
用户意图: 天气查询与提醒 工具调用序列: weather_api -> reminder_tool 工具调用次数: 2 是否重试: 否 最终状态: 成功 关键参数: city=北京, date=明天, condition=小雨 响应长度: 短这段文本丢给 Embedding 模型,得到的向量就编码了这条 Trace 的"行为指纹"。路径 B 的文本会是"工具调用序列: weather_api -> text_response",向量距离就会和 A 拉开一点,但比和路径 D(有重试)的距离要近。这就是我们想要的语义聚类效果。
注意:Trace 文本化的模板设计是整套方案里最需要打磨的部分。字段选多了会引入噪声,选少了会丢失区分度。我的经验是优先保留"工具调用序列""最终状态""关键参数摘要"这三类,其他字段作为可选补充。
2.3 聚类算法选型:为什么我最终选了 HDBSCAN
Embedding 之后就是聚类。常见的 K-Means、DBSCAN、HDBSCAN、层次聚类我都试过,最后稳定用的是HDBSCAN。原因有三个:
第一,不需要预先指定簇数量。Agent 的行为模式数量是未知的,K-Means 要求你拍脑袋定 K 值,这在探索性分析里是致命的。HDBSCAN 自动决定簇的数量,还能把不属于任何簇的样本标记为噪声。
第二,能处理密度不均的簇。Agent 行为分布天然不均匀——常见行为(比如简单问答)会形成一个大而密的簇,罕见行为(比如复杂多工具编排)会形成小而疏的簇。DBSCAN 用固定半径,很容易把稀疏簇当成噪声丢掉。HDBSCAN 通过层次化的密度估计,能同时捕捉这两种簇。
第三,对噪声鲁棒。真实 Trace 里总有一些异常样本(比如调试时手动触发的、测试环境的脏数据),HDBSCAN 会把它们归为 -1 类噪声,不污染正常簇的质心。
参数上主要调两个:min_cluster_size和min_samples。前者控制一个簇最少要有多少条 Trace 才算数,我一般设成总样本量的 0.5%~2%;后者控制核心点的邻域密度,设小一点(3~5)能让簇更宽松。这两个参数需要根据你的数据量做几轮实验。
2.4 Session 维度聚合:从单条 Trace 到多轮行为演化
单条 Trace 的聚类只能告诉你"有哪些行为模式",但 Agent 的真实价值往往体现在多轮 Session里。用户和 Agent 来回对话十几轮,行为模式会演化——可能从"简单问答"逐渐变成"复杂任务编排",也可能因为某轮失败而陷入"反复重试"的循环。
所以我在 Trace 聚类之上又加了一层 Session 聚合:把同一个 Session 下的所有 Trace 按时间排序,得到这个 Session 的"行为模式序列"。比如[简单问答, 工具调用, 工具调用, 报错重试, 成功回复]。这个序列本身就是一种更高层的特征,可以用来分析:
- 哪些行为模式序列最容易导致用户满意度下降
- 哪些序列代表"任务成功完成"的典型路径
- 哪些序列是"卡住"的信号,需要人工介入
这层聚合让分析从"静态分类"升级到"动态演化",价值提升非常明显。
3. 核心细节拆解:Trace 文本化、Embedding 选型与聚类参数
3.1 Trace 文本化模板的字段设计
前面提到文本化模板是关键,这里展开讲具体怎么设计。一条完整的 Trace 通常包含这些信息:
| 字段类别 | 具体字段 | 是否必选 | 说明 |
|---|---|---|---|
| 基础信息 | trace_id, session_id, timestamp | 必选 | 用于关联和排序,不进入文本 |
| 用户意图 | 首轮用户输入摘要 | 必选 | 用 LLM 压缩成一句话 |
| 工具调用 | 工具名序列、调用次数 | 必选 | 行为模式的核心特征 |
| 执行状态 | 最终状态、是否重试、错误类型 | 必选 | 区分成功/失败/异常路径 |
| 参数摘要 | 关键参数键值对 | 可选 | 增强语义区分度 |
| 性能指标 | 总耗时、token 消耗 | 可选 | 用于后续分层分析 |
| 输出特征 | 响应长度、是否含结构化数据 | 可选 | 辅助判断输出类型 |
文本化的格式我建议用结构化自然语言,而不是纯 JSON。原因是 Embedding 模型对自然语言的语义捕捉能力远强于对 JSON 结构的理解。比如:
这是一条 Agent 执行记录。 用户意图是查询天气并设置提醒。 Agent 依次调用了 weather_api 和 reminder_tool 两个工具,共调用 2 次。 执行过程中没有发生重试,最终状态为成功。 关键参数包括城市北京、日期明天、天气状况小雨。这段文本丢给 Embedding 模型,得到的向量会很好地编码"天气查询+提醒+成功"这个语义。如果换成 JSON,模型需要额外学习 JSON 结构,效果会打折扣。
实操心得:用户意图摘要这一步,我强烈建议用一个小模型(比如 7B 级别的)离线批量生成,而不是实时调用。因为 Trace 分析是离线任务,没必要为了实时性牺牲成本。批量跑一次几百万条 Trace,用 vLLM 部署的小模型几个小时就能搞定。
3.2 Embedding 模型选型:中文场景下的实测对比
Embedding 模型的选择直接决定聚类质量。我在中文 Agent 场景下实测过几个主流模型,结论如下:
| 模型 | 维度 | 中文语义 | 长文本 | 推理速度 | 我的评价 |
|---|---|---|---|---|---|
| BGE-large-zh | 1024 | 优秀 | 一般(512) | 中等 | 中文场景首选,性价比高 |
| BGE-m3 | 1024 | 优秀 | 优秀(8192) | 较慢 | 长 Trace 场景推荐 |
| text-embedding-3-large | 3072 | 良好 | 优秀 | 快(API) | 英文为主,中文略弱 |
| GTE-large-zh | 1024 | 优秀 | 良好 | 中等 | BGE 的有力竞品 |
| M3E-base | 768 | 良好 | 一般 | 快 | 轻量场景可用 |
我的最终选择是BGE-m3。原因是 Trace 文本化之后长度可能到 500~1000 token,BGE-m3 的 8192 上下文窗口能完整覆盖,不需要截断。而且它支持多语言,如果你的 Agent 有中英混合场景,一个模型就够了。
维度方面,1024 维是个甜点。太低(如 384)会丢失语义细节,太高(如 3072)会让聚类计算变慢且容易过拟合噪声。1024 维在效果和效率之间平衡得最好。
注意:Embedding 模型一定要和你的业务语料对齐。如果你的 Agent 大量使用特定领域的术语(比如医疗、法律),通用 Embedding 模型可能表现不佳。这时候可以考虑用领域语料做微调,或者用 LLM 生成一批"行为相似/不相似"的样本对做对比学习。
3.3 聚类参数调优:min_cluster_size 怎么定
HDBSCAN 的参数调优是门手艺。我总结了一套流程:
第一步,先跑一版粗聚类。min_cluster_size设成总样本量的 1%,min_samples设成 5。看聚类结果的簇数量和噪声比例。如果噪声超过 30%,说明参数太严;如果簇数量超过 50 个,说明太松。
第二步,根据业务目标调整。如果你想要"粗粒度行为分类",就把min_cluster_size调大(比如 3%),让簇更少更稳定。如果你想要"细粒度异常发现",就调小(比如 0.3%),让罕见行为也能成簇。
第三步,用轮廓系数和业务可解释性双重验证。轮廓系数是纯数学指标,但 Agent 场景下更重要的是"每个簇能不能用一句话解释清楚"。如果某个簇你看了半天说不出它代表什么行为,那这个簇大概率是噪声或者参数不当的产物。
我一般会做 5~8 轮参数扫描,把每轮的结果导出成表格,人工看几个代表性样本,最后选定一组参数。这个过程听起来繁琐,但一次调好之后可以复用很久。
3.4 Session 序列构建:时间窗口与状态机
Session 聚合的难点在于如何定义 Session 边界。简单按session_id分组是最直接的,但实际场景里会有问题:有些 Session 可能跨越很长时间(用户隔天回来继续),有些 Session 可能因为超时被切断。
我的做法是加一个时间窗口约束:同一个session_id下,相邻两条 Trace 的时间间隔超过 30 分钟就切分成新的逻辑 Session。这个阈值可以根据业务调整,客服场景可能 10 分钟就够,复杂任务编排场景可能需要 1 小时。
切分完之后,每个逻辑 Session 就是一个 Trace 序列。我把这个序列映射成行为模式序列(用聚类标签替换),然后统计序列的转移矩阵。这个转移矩阵能直观地告诉你:从"简单问答"出发,下一步最可能变成什么模式;哪些模式是"死胡同"(进去就出不来)。
4. 完整实操流程:从原始 Trace 到行为洞察
4.1 数据准备与清洗
第一步是把原始 Trace 从存储里拉出来。不管你是用 Jaeger、Tempo 还是自建的 ClickHouse 表,核心是把每条 Trace 的所有 Span 聚合成一条记录。这里有个坑:Span 的父子关系要正确重建,否则工具调用序列会乱序。
我用的是 ClickHouse 存 Trace,查询语句大概长这样:
SELECT trace_id, session_id, min(timestamp) AS start_time, max(timestamp) AS end_time, groupArray((span_id, parent_span_id, name, attributes)) AS spans FROM traces WHERE start_time >= now() - INTERVAL 7 DAY GROUP BY trace_id, session_id拿到数据后要做几件事:
- 过滤测试数据:把
user_id是测试账号的、environment是 dev 的剔除 - 过滤不完整 Trace:有些 Trace 因为采样或超时只有部分 Span,这些会干扰聚类
- 标准化工具名:同一个工具可能有多个别名(比如
weather_api和get_weather),要统一 - 脱敏:用户输入里可能含手机号、身份证等,文本化之前必须脱敏
实操心得:脱敏这一步千万别偷懒。我见过有团队直接把原始 Trace 丢给 Embedding 模型,结果向量里编码了用户隐私信息,聚类结果里能反推出具体用户。合规风险极大。建议用正则+NER 双重脱敏,把手机号、邮箱、身份证、银行卡号都替换成占位符。
4.2 Trace 文本化与批量 Embedding
清洗完之后,用前面设计的模板把每条 Trace 转成文本。这一步我建议用 Python 脚本批处理,核心逻辑:
def trace_to_text(trace): intent = summarize_intent(trace.user_input) # 小模型生成 tools = " -> ".join(trace.tool_sequence) status = "成功" if trace.success else f"失败({trace.error_type})" retry = "有重试" if trace.retry_count > 0 else "无重试" params = ", ".join([f"{k}={v}" for k, v in trace.key_params.items()]) return f"""这是一条 Agent 执行记录。 用户意图是{intent}。 Agent 依次调用了{tools},共调用 {len(trace.tool_sequence)} 次。 执行过程中{retry},最终状态为{status}。 关键参数包括{params}。"""文本生成后,用 BGE-m3 批量编码。这里有个性能优化点:用 GPU 批推理,batch_size 设成 64~128,几百万条 Trace 几个小时能跑完。如果只有 CPU,建议用 ONNX 加速,速度能提升 3~5 倍。
编码完得到的是一个(N, 1024)的矩阵,N 是 Trace 数量。这个矩阵就是后续聚类的输入。
4.3 降维与聚类执行
1024 维直接聚类计算量不小,而且高维空间里距离度量会退化(维度灾难)。我一般先用UMAP降到 50 维左右,再做 HDBSCAN。UMAP 的好处是保留局部和全局结构,降维后聚类效果比 PCA 好很多。
import umap import hdbscan # 降维 reducer = umap.UMAP(n_components=50, n_neighbors=15, min_dist=0.1, metric='cosine') embeddings_50d = reducer.fit_transform(embeddings) # 聚类 clusterer = hdbscan.HDBSCAN( min_cluster_size=int(len(embeddings) * 0.01), min_samples=5, metric='euclidean', cluster_selection_method='eom' ) labels = clusterer.fit_predict(embeddings_50d)跑完之后,labels里每个值就是对应 Trace 的簇编号,-1 表示噪声。统计一下簇数量和噪声比例,如果噪声超过 30%,就调大min_cluster_size重跑。
4.4 簇的解读与命名
聚类本身不产生意义,意义需要人来赋予。对每个簇,我会做这几件事:
- 抽样看典型样本:从簇里随机抽 10 条 Trace,看它们的文本描述
- 提取高频特征:统计簇内工具序列、状态、参数的分布
- 用 LLM 生成簇描述:把抽样样本丢给 LLM,让它总结"这个簇代表什么行为模式"
- 人工校验命名:LLM 生成的描述需要人工确认,确保准确
比如某个簇的样本都是"用户问简单事实性问题,Agent 直接回答,无工具调用,成功",那这个簇就可以命名为"直接问答型"。另一个簇都是"多工具调用+重试+最终成功",可以命名为"复杂任务重试成功型"。
命名之后,你就有了一个行为模式字典。后续所有新来的 Trace,都可以通过 Embedding + 最近邻查找,快速归类到已知模式,或者标记为"新模式"触发人工审查。
4.5 Session 序列分析与可视化
有了 Trace 级别的簇标签,Session 级别的分析就水到渠成。把每个 Session 的 Trace 按时间排序,用簇标签替换,得到行为序列。然后统计:
- 序列长度分布:大部分 Session 是几轮?有没有异常长的?
- 模式转移矩阵:从模式 A 到模式 B 的概率是多少?
- 终止模式分布:Session 最后停在哪个模式?是"成功回复"还是"报错退出"?
这些统计结果用热力图或桑基图展示,一眼就能看出问题。比如我发现过某个 Agent 的 Session 有 40% 终止在"反复重试"模式,追查下去发现是某个工具的超时配置不合理,导致大量请求卡在重试循环里。
5. 常见问题与排查技巧实录
5.1 聚类结果全是噪声怎么办
这是最常见的问题,通常有三个原因:
原因一:Embedding 质量差。检查你的文本化模板是不是把太多无关信息编码进去了。比如把trace_id、timestamp这种随机性强的字段也放进文本,会让向量变得分散。解决方法是只保留语义相关的字段。
原因二:min_cluster_size 太大。如果你的数据量只有几千条,min_cluster_size设成 1% 就是几十条,可能超过了很多真实簇的大小。这时候要调小,甚至设成 5~10 这种绝对值。
原因三:数据本身太分散。如果 Agent 的行为确实高度多样,没有明显聚集,那聚类效果差是正常的。这时候可以考虑先按业务维度(比如用户类型、任务类型)分层,再在每层内聚类。
5.2 簇的数量太多或太少怎么调
簇太多(比如超过 50 个)说明min_cluster_size太小,或者 Embedding 维度太高导致过拟合。调大min_cluster_size,或者先做一次粗粒度降维(比如降到 20 维)。
簇太少(比如只有 3~5 个)说明min_cluster_size太大,把很多小簇合并了。调小参数,或者换cluster_selection_method从eom改成leaf,后者会保留更多细粒度簇。
我的经验是:对于百万级 Trace,10~30 个簇是比较合理的范围。太少看不出细节,太多没法人工解读。
5.3 新 Trace 如何归类到已有簇
生产环境里,你不可能每次都重新跑全量聚类。我的做法是:
- 保存每个簇的质心向量(簇内所有 Trace 向量的均值)
- 新 Trace 编码后,计算它到各质心的余弦距离
- 距离小于阈值的归入最近簇,大于阈值的标记为"未知模式"
- 定期(比如每周)把累积的"未知模式"样本重新聚类,发现新行为模式
这个流程能保证分析系统的持续演进,同时控制计算成本。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方法 |
|---|---|---|---|
| 噪声比例 > 50% | 文本化引入噪声 | 检查模板字段 | 移除随机性字段 |
| 簇数量 > 50 | 参数过松 | 看簇大小分布 | 调大 min_cluster_size |
| 簇数量 < 5 | 参数过严 | 看噪声比例 | 调小 min_cluster_size |
| 簇内样本不相似 | Embedding 质量差 | 抽样看文本 | 换模型或微调 |
| 聚类结果不稳定 | 数据量太小 | 看总样本数 | 累积更多数据再跑 |
| 新 Trace 归类不准 | 质心漂移 | 看质心更新频率 | 定期重算质心 |
避坑技巧:聚类结果一定要做时间稳定性检查。同一个簇,在连续两周的数据上应该保持相似的语义。如果某个簇这周代表"天气查询",下周变成"订单查询",说明 Embedding 或聚类参数有问题,需要重新调优。
6. 从行为洞察到产品优化:这套方法能带来什么
6.1 发现隐藏的失败模式
传统监控只能告诉你"成功率 95%",但聚类能告诉你"那 5% 的失败里,有 3% 是同一个模式——工具调用超时后没有正确降级"。这种细粒度的洞察,是优化 Agent 稳定性的关键输入。
我在一个项目里通过聚类发现,Agent 在处理"多轮追问"场景时,有 15% 的 Session 会陷入"用户重复问同一个问题,Agent 重复给相似答案"的死循环。这个模式在传统指标里完全看不出来(因为每轮都"成功"了),但用户体验极差。定位到问题后,我们在 Prompt 里加了"检测到重复意图时主动澄清"的逻辑,死循环比例降到了 2% 以下。
6.2 指导 Prompt 和工具优化
聚类结果能直接指导优化方向。比如某个簇代表"Agent 调用了不必要的工具",那就可以优化 Prompt,让模型更谨慎地决定是否调用工具。某个簇代表"工具参数经常传错",那就可以在工具定义里加更严格的参数校验和示例。
6.3 建立行为基线,做异常检测
有了稳定的行为模式字典,就可以建立行为基线。正常情况下,各模式的分布应该是稳定的。如果某天某个模式的占比突然飙升,那就是异常信号,可能意味着:
- 上游数据源变了(比如某个 API 返回格式改了)
- 模型版本更新引入了行为变化
- 用户群体发生了变化(比如来了大量新用户)
这种基于行为分布的异常检测,比基于单条 Trace 的阈值告警要灵敏得多,而且误报率低。
6.4 支撑 Agent 的持续迭代
Agent 系统是持续迭代的。每次改 Prompt、换模型、加工具,都会影响行为模式。用这套聚类方法,你可以量化每次迭代的影响:新模式出现了吗?旧模式消失了吗?各模式的比例变化了多少?
这比"上线后看几天数据感觉还行"要科学得多。我现在的习惯是,每次 Agent 版本更新后,跑一次聚类对比,生成一份"行为变化报告",作为迭代决策的依据。
7. 一些实操中的个人体会
这套方法我从最早的规则匹配一路踩坑过来,中间试过不少弯路。最开始我想用 LLM 直接给每条 Trace 打标签,结果成本高得离谱,而且 LLM 的标签一致性很差——同一条 Trace 今天标"查询类",明天标"信息获取类"。后来才转向 Embedding + 聚类的路线,成本降了两个数量级,一致性也好了很多。
另一个体会是:不要追求一次做到完美。聚类分析是个迭代过程,第一版结果肯定不理想,但只要能看到一些有意义的簇,就说明方向对了。然后通过调整文本化模板、换 Embedding 模型、调聚类参数,逐步优化。我现在的项目跑了半年多,行为模式字典从最初的 8 个扩展到了 25 个,覆盖了绝大部分业务场景。
最后分享一个小技巧:把聚类结果和业务指标关联起来。比如把每个簇的 Trace 和用户满意度评分、任务完成率做交叉分析,你会发现某些行为模式和低满意度强相关。这些就是优先优化的目标。单纯看聚类结果容易陷入"为了聚类而聚类",只有和业务价值挂钩,这套方法才真正有意义。
后续如果数据量继续增长,可以考虑用 Faiss 做向量索引加速最近邻查找,或者用增量聚类算法(比如 BIRCH)支持流式更新。这些是规模化之后的优化方向,初期用 UMAP + HDBSCAN 的批处理方案完全够用。