
1. 从一堆看不懂的 Trace 说起为什么需要智能聚类做过 Agent 开发的人大概都有过这种体验线上跑着几百上千个会话每个会话里 Agent 要调用工具、检索知识、生成回复、可能还要多轮反思一天下来 Trace 数据能堆到几个 GB。打开日志平台满屏都是span_id、parent_span_id、latency、token_count一条条翻下去眼睛都花了可真正想找的哪类任务最容易失败哪种用户问法最烧 token哪个工具调用链最拖后腿这些问题靠人肉翻日志基本等于大海捞针。这就是智能聚类从海量 Trace 中理解 Agent 的行为和表现这个项目要解决的核心痛点。简单说它做的事情是把 Agent 运行时产生的海量 Trace 数据通过 Embedding 把每条执行链路或者每个 Session转成向量再用聚类算法自动分组让相似的执行模式聚到一起最后从每个簇里提炼出可读的行为标签和性能指标。你不再需要一条条看 Trace而是看这个 Agent 一共有 7 类典型行为其中第 3 类占了 40% 但失败率高达 18%。这套东西适合谁我梳理了一下大概三类人最用得上。第一类是Agent 应用开发者尤其是做多工具编排、多轮对话的需要持续优化 prompt 和工具链第二类是AI 平台/基础设施工程师要监控大量 Agent 实例的运行健康度第三类是做 Agent 评测和研究的同学需要从真实流量里归纳行为模式而不是靠人工构造测试集。哪怕你只是刚入门 Agent 开发理解这套思路也能帮你建立用数据驱动优化的意识而不是凭感觉改 prompt。关键词里的Trace、Agent、Session、Embedding、智能聚类这五个词基本就是整条技术链路的骨架Trace 是原料Session 是聚合单元Embedding 是表示手段聚类是核心算法最终服务于理解 Agent 的行为和表现。下面我按实际落地的顺序把每个环节拆开讲包括我踩过的坑和验证过的参数。2. 整体设计思路为什么是Embedding 聚类这条路2.1 先想清楚我们要聚的到底是什么这是整个项目最容易走偏的地方。很多人一上来就说我要聚类 Trace但 Trace 的粒度差别巨大。一条 Trace 可能是一个 HTTP 请求级别的 span也可能是一个完整的 Agent 会话Session包含几十个 span。聚错了粒度后面全白搭。我的经验是分两个层次来看Span 级别单个工具调用、单次 LLM 请求。适合分析哪类工具调用慢哪类 prompt 消耗 token 多。Session 级别一次完整的用户交互包含多轮对话和多次工具调用。适合分析哪类任务整体表现差用户意图分布。实际项目里Session 级别聚类价值最高因为 Agent 的行为和表现本质上是完成一个任务的全过程而不是孤立的单步。所以这个项目的核心聚合单元是 SessionSpan 级别的信息作为 Session 向量的组成部分。提示如果你的 Trace 系统里 Session 概念不清晰比如没有统一的 session_id 贯穿第一步要先把 Session 边界定义好否则聚类结果会非常混乱。这是我在两个项目里都遇到过的前置问题。2.2 为什么选 Embedding 而不是手工特征传统做法是手工提取特征调用工具次数、总 token、耗时、错误码分布……然后喂给 KMeans。这套方法能用但有两个硬伤。第一语义信息丢失。两个 Session 可能工具调用次数、耗时都差不多但一个是查天气一个是写代码行为模式完全不同。手工特征很难捕捉这种语义差异。第二扩展性差。Agent 每加一个新工具、新能力特征工程就要重做一遍。而 Embedding 是把整个执行链路包括用户输入、工具名、工具参数摘要、Agent 回复摘要拼成一段文本用 Embedding 模型转成向量语义相近的自然就靠近。所以这个项目的技术选型是用 Embedding 做语义表示用聚类做无监督分组再用 LLM 给每个簇生成人类可读的标签。这个组合的好处是端到端、可扩展新增工具不需要改特征工程。2.3 整体架构长什么样我把整个链路拆成五步后面每一节都会展开Trace 采集与 Session 聚合从日志/追踪系统拉数据按 session_id 聚合成会话。Session 文本化把结构化 Trace 转成一段可读文本这是 Embedding 的输入。Embedding 生成调用 Embedding 模型得到向量。聚类降维 聚类确定簇数量。簇解读统计每个簇的指标用 LLM 生成行为标签。这个架构的关键设计取舍在于文本化那一步决定了上限。你喂给 Embedding 模型的文本质量直接决定聚类效果。后面我会专门讲怎么拼这段文本。3. 核心细节解析Trace 到向量的关键处理3.1 Session 聚合把散落的 Span 串成一条线Trace 数据通常是扁平的 span 列表每个 span 有trace_id、span_id、parent_span_id、name、start_time、duration、attributes。要聚合成 Session核心是找到贯穿一次完整交互的标识。常见的有三种情况有明确的session_id字段最理想直接 group by。只有trace_id如果一次用户交互对应一个 trace那 trace_id 就是 session 边界。都没有只能靠时间窗口 用户标识来切分比如同一用户 30 分钟内的连续请求算一个 Session。我实测下来时间窗口切分是最容易出错的。窗口太短会把一次任务切成好几段太长会把不相关的任务混在一起。经验值是对话类 Agent 用 15-30 分钟任务型 Agent 用 5-10 分钟具体要看你的业务节奏。聚合后每个 Session 应该包含用户输入序列、Agent 的每一步动作工具调用、LLM 调用、每步的耗时和 token、最终输出、是否有错误。3.2 Session 文本化这一步决定聚类质量这是整个项目里最需要动脑子的地方。Embedding 模型吃的是文本所以我们要把结构化的 Session 转成一段能表达这个 Session 干了什么的文本。我试过几种模板最后稳定下来的格式大概是这样任务类型: 用户咨询 用户输入: 帮我查一下明天北京的天气然后推荐穿什么 执行步骤: 1. 调用工具 weather_query, 参数 {city: 北京, date: 明天}, 耗时 320ms, 成功 2. 调用工具 clothing_recommend, 参数 {weather: 晴, temp: 5-15度}, 耗时 210ms, 成功 3. 生成回复: 明天北京晴气温5到15度建议穿薄羽绒服... 结果: 成功 总耗时: 1.2s, 总token: 850几个关键点工具名要保留因为工具名本身就是强语义信号。参数做摘要不要把完整 JSON 塞进去太长会稀释语义。比如{city: 北京, date: 明天}就够了。结果状态要带上成功/失败是行为模式的重要维度。用户输入要完整保留这是意图的核心。注意不要把所有 span 的原始文本都拼进去那样向量会被噪声淹没。我见过有人把完整日志拼进去结果聚类出来的簇全是按日志长度分的毫无意义。3.3 Embedding 模型选型别盲目追排行榜热词里有embedding模型排行但我要泼盆冷水排行榜第一不一定适合你的场景。选型要看三个维度。维度考虑点我的建议语言中文/英文/混合中文场景优先选中文优化过的模型维度向量维度大小768-1024 维通常够用太高反而增加聚类成本成本调用价格/自部署数据量大时自部署更划算长度最大输入长度Session 文本可能较长注意截断策略我实际用下来对于中文 Agent 场景768 维左右的中文优化模型性价比最高。如果你的 Session 文本经常超过模型最大长度要么做摘要要么分段 Embedding 再平均但分段平均会损失顺序信息慎用。还有一个坑同一个项目里 Embedding 模型必须固定。中途换模型会导致新旧向量不在同一空间聚类结果直接失效。如果非要换得全量重新生成。3.4 降维与聚类参数怎么定Embedding 出来是 768 维直接聚类不是不行但高维空间里距离度量会退化维度灾难。常规做法是先降维再聚类。降维我用UMAP比 PCA 保留的局部结构更好。关键参数n_neighbors控制局部与全局的平衡一般 15-50数据量大就调大。min_dist控制点的聚集程度聚类场景建议 0.0-0.1让同簇点更紧。n_components降到多少维聚类用 5-10 维比较合适可视化用 2 维。聚类算法选HDBSCAN而不是 KMeans原因是你事先不知道有多少类。KMeans 要指定 K而 Agent 的行为类别是未知的。HDBSCAN 能自动确定簇数量还能把噪声点单独标出来这些往往是异常行为很有价值。HDBSCAN 的核心参数是min_cluster_size即一个簇最少多少个样本。这个值直接影响结果太小会碎成很多小簇太大会把不同行为合并。我的经验是设为总样本数的 1%-5%比如 1000 个 Session 就设 10-50。4. 实操过程从原始 Trace 到行为报告4.1 环境与依赖准备先把依赖列清楚避免版本踩坑pip install pandas numpy scikit-learn umap-learn hdbscan pip install openai # 或其他 Embedding 服务 SDK如果你用本地 Embedding 模型还需要sentence-transformers和torch。我建议先用 API 跑通流程验证效果后再考虑自部署降本。4.2 第一步加载并聚合 Session假设你的 Trace 存在 JSONL 或数据库里先读进来import pandas as pd # 假设每条记录是一个 span spans pd.read_json(traces.jsonl, linesTrue) # 按 session_id 聚合 sessions spans.groupby(session_id).apply( lambda g: g.sort_values(start_time).to_dict(records) ).reset_index(namespans) print(f共聚合出 {len(sessions)} 个 Session)这一步要检查几个数据质量点有没有 session_id 为空的、有没有单个 Session span 数异常多的可能是聚合错误、时间戳有没有乱序。我一般会先跑一遍统计把异常 Session 剔掉再往下走。4.3 第二步Session 文本化按前面说的模板写一个转换函数def session_to_text(spans): lines [] user_input steps [] total_duration 0 total_tokens 0 status 成功 for i, span in enumerate(spans, 1): name span.get(name, ) attrs span.get(attributes, {}) duration span.get(duration, 0) total_duration duration if name user_input: user_input attrs.get(text, ) elif name.startswith(tool_): tool_name name.replace(tool_, ) params summarize_params(attrs.get(params, {})) ok 成功 if not span.get(error) else 失败 steps.append(f{i}. 调用工具 {tool_name}, 参数 {params}, 耗时 {duration}ms, {ok}) elif name llm_call: total_tokens attrs.get(total_tokens, 0) steps.append(f{i}. LLM生成, 耗时 {duration}ms) text f用户输入: {user_input}\n执行步骤:\n \n.join(steps) text f\n结果: {status}\n总耗时: {total_duration}ms, 总token: {total_tokens} return text def summarize_params(params): # 只保留关键字段避免文本过长 if isinstance(params, dict): items list(params.items())[:3] return { , .join(f{k}: {v} for k, v in items) } return str(params)[:100]summarize_params这个函数很关键它决定了参数信息保留多少。我的经验是最多保留 3 个字段每个值截断到 50 字符再多就是噪声。4.4 第三步生成 Embeddingfrom openai import OpenAI import numpy as np client OpenAI() def get_embedding(text, modeltext-embedding-3-small): resp client.embeddings.create(inputtext, modelmodel) return resp.data[0].embedding texts [session_to_text(s) for s in sessions[spans]] embeddings np.array([get_embedding(t) for t in texts]) np.save(session_embeddings.npy, embeddings)数据量大时一定要批量调用 加缓存。我一般会把text - embedding存成字典缓存到本地避免重复调用浪费钱。另外注意 API 的速率限制加个time.sleep或并发控制。4.5 第四步降维 聚类import umap import hdbscan # 降维 reducer umap.UMAP( n_neighbors30, min_dist0.0, n_components10, metriccosine, random_state42 ) reduced reducer.fit_transform(embeddings) # 聚类 clusterer hdbscan.HDBSCAN( min_cluster_size20, min_samples5, metriceuclidean ) labels clusterer.fit_predict(reduced) n_clusters len(set(labels)) - (1 if -1 in labels else 0) noise_ratio (labels -1).mean() print(f聚出 {n_clusters} 个簇, 噪声占比 {noise_ratio:.1%})这里metriccosine很重要因为 Embedding 向量的语义相似度用余弦距离度量最合适。降维后再用欧氏距离聚类是常规组合。噪声占比是个重要指标。如果超过 30%说明min_cluster_size设太大了或者数据本身太分散。我一般会调到噪声占比在 10%-20% 之间。4.6 第五步簇解读与指标统计聚类只是分组真正有价值的是每个簇代表什么行为、表现如何。这一步要统计每个簇的指标再用 LLM 生成标签。def analyze_cluster(sessions, labels, cluster_id): members sessions[labels cluster_id] # 统计指标 avg_duration members[total_duration].mean() avg_tokens members[total_tokens].mean() error_rate members[has_error].mean() # 取几个代表性样本 samples members[text].head(5).tolist() return { size: len(members), avg_duration: avg_duration, avg_tokens: avg_tokens, error_rate: error_rate, samples: samples }然后用 LLM 给每个簇打标签def label_cluster(cluster_info): prompt f以下是同一类 Agent 会话的样本请用一句话概括这类会话的行为特征 样本1: {cluster_info[samples][0]} 样本2: {cluster_info[samples][1]} 样本3: {cluster_info[samples][2]} 要求不超过20字突出任务类型和主要动作。 # 调用 LLM 返回标签 return call_llm(prompt)最终产出的报告大概长这样簇ID行为标签样本数占比平均耗时平均token失败率0天气查询与穿衣推荐32032%1.2s8502%1多轮代码调试18018%8.5s420015%2简单闲聊25025%0.6s3200%3复杂数据分析909%12s680022%看到这张表优化方向就一目了然了簇 3 失败率 22% 且 token 消耗最高优先排查簇 1 耗时最长看看是不是工具链有瓶颈。5. 常见问题与排查技巧实录5.1 聚类结果全是噪声怎么办这是最常见的问题。表现是labels里 -1 占了一大半。原因通常有三个min_cluster_size太大数据量小的时候尤其明显1000 个样本设 50 就可能全成噪声。先降到 10 试试。Embedding 质量差文本化模板没设计好向量区分度低。检查一下是不是把太多无关信息拼进去了。数据本身太分散如果 Agent 处理的都是高度个性化的任务确实可能没有明显聚类结构。这时候要考虑换更细的粒度或者接受没有强聚类这个事实。我的排查顺序是先调小min_cluster_size再看几个噪声样本的文本判断是文本问题还是数据问题。5.2 簇的数量太多或太少簇太多比如 50 个簇每个就几十个样本说明min_cluster_size太小或者降维参数让局部结构过度保留。调大min_cluster_size或者把 UMAP 的n_neighbors调大。簇太少比如就 2-3 个簇说明区分度不够。可能是 Embedding 模型不适合你的领域也可能是文本化时把关键差异抹掉了。我遇到过一次因为工具参数全被截断成一样导致所有工具调用看起来都差不多后来把工具名单独强化才解决。5.3 标签生成不准确LLM 生成的簇标签有时候很泛比如用户咨询信息查询这种没信息量的词。解决办法是给 LLM 更多上下文除了样本把该簇的统计指标耗时、失败率、工具分布也喂进去让它结合数据生成标签。另外样本要选靠近簇中心的而不是随机选代表性更强。5.4 常见问题速查表问题现象可能原因排查方向噪声占比 40%min_cluster_size 过大逐步调小观察噪声比例簇数量 30参数过细调大 min_cluster_size 和 n_neighbors簇内样本不相似Embedding 质量差检查文本化模板强化关键字段标签太泛LLM 上下文不足补充统计指标和中心样本新旧结果不一致Embedding 模型变了固定模型全量重算运行太慢逐条调用 Embedding批量调用 本地缓存5.5 几个我踩过的坑坑一Session 边界没对齐。有次线上 session_id 是前端生成的但后端重试时会生成新 id导致一次任务被拆成多个 Session。聚类出来的结果全是碎片。后来统一用 trace_id 做边界才解决。坑二Embedding 缓存没做版本管理。换了文本化模板后忘了清缓存新旧向量混在一起聚类结果诡异了好几天才发现。坑三忽略时间维度。Agent 的行为会随版本迭代变化如果拿三个月的数据一起聚类会把不同版本的行为混在一起。建议按周或按版本分批聚类再对比不同批次的结果这样还能看出行为漂移。提示聚类不是一劳永逸的。Agent 每次大改版后行为分布都会变建议把聚类做成定期任务比如每天跑一次持续监控行为变化。6. 让这套方案真正跑起来的几个经验6.1 从采样开始别一上来就全量我第一次做的时候直接把一周的全量 Trace 拉下来跑几百万条Embedding 调用花了不少钱聚类跑了半小时结果发现文本化模板有问题全部重来。后来学乖了先随机采样 1000-2000 个 Session 跑通全流程验证效果后再上全量。采样阶段成本几乎可以忽略但能帮你快速迭代模板和参数。6.2 把聚类结果接回监控聚类本身不是目的驱动优化才是。我现在的做法是每天跑一次聚类把每个簇的失败率、耗时、token 消耗写进监控看板。一旦某个簇的失败率突增就自动告警。这样比盯着总体指标灵敏得多因为总体指标会被大簇稀释而细分到簇就能快速定位问题。6.3 和人工标注结合纯无监督聚类有个问题簇的语义是涌现出来的不一定符合业务视角。我的做法是定期人工 review 几个簇把业务上关心的分类比如售前咨询售后投诉和聚类结果做映射。如果发现某个业务类别被拆散在多个簇里说明文本化模板需要调整要把该业务的关键特征强化进去。6.4 关于成本和性能Embedding 调用是主要成本。以 1000 个 Session、每个 500 token 计算一次全量大概几十万 token用便宜的模型成本很低。真正贵的是频繁重跑。所以缓存一定要做而且缓存 key 要包含文本内容和模型名避免混用。聚类本身是 CPU 计算1000 个样本秒级完成10000 个样本也就几十秒。瓶颈通常在 Embedding 调用和 LLM 标签生成上这两个都可以并发优化。6.5 后续可以怎么扩展这套框架跑通后有几个自然的扩展方向。一是做行为漂移检测对比不同时间段的聚类结果看哪些簇在变大变小提前发现 Agent 行为异常。二是做失败归因把失败率高的簇单独拿出来深入分析是工具问题还是 prompt 问题。三是和 A/B 测试结合对比两个版本的 Agent 在同一批任务上的聚类分布量化版本改进效果。我个人在实际操作中的体会是这套方案最大的价值不在于算法多高级而在于它把理解 Agent 行为这件事从靠直觉变成了靠数据。以前优化 Agent 全靠拍脑袋现在打开报告就能看到哪类任务在拖后腿优化方向清晰太多了。如果你也在做 Agent 开发强烈建议把这套流程搭起来哪怕先用最简陋的版本也比没有强。