ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Embedding与聚类的AI Agent Trace行为分析实战

基于Embedding与聚类的AI Agent Trace行为分析实战 1. 从一堆看不懂的 Trace 说起做过 Agent 开发的人大概都有过这种体验线上跑着几百上千个会话每个会话里 Agent 要调用工具、检索知识、做多轮推理一轮下来产生的 Trace 数据动辄几十万条。打开日志平台一看满屏都是span_id、parent_span_id、latency、token_count眼睛都花了但真正想知道的问题——我的 Agent 到底在干什么哪类任务表现好哪类任务在翻车——却很难从这些原始数据里直接读出来。这就是智能聚类从海量 Trace 中理解 Agent 的行为和表现这个项目要解决的核心问题。简单说它做的事情是把 Agent 运行时产生的大量 Trace 数据通过 Embedding 把每条执行路径转成向量再用聚类算法把行为相似的会话Session自动归到一类最后从每一类里提炼出这类任务 Agent 是怎么处理的、耗时多少、成功率如何、卡在哪一步。它面向的是所有在做 AI Agent 开发、需要做效果分析和性能优化的工程师不管你是刚上手 Agent 框架的新手还是已经在调优多智能体编排的老手这套思路都能直接用。我最初接触这个方向是因为手上一个 Agent 项目上线后用户反馈有时候很快有时候慢得离谱但翻日志根本看不出规律。后来把 Trace 按执行路径聚类之后问题一下就清楚了慢的那一类全都是触发了多轮工具重试的会话而重试的根因是某个检索接口在特定 query 下返回空结果。这种洞察靠人肉翻日志基本不可能发现。下面我把这套方案从设计思路到落地细节完整拆一遍包括我踩过的坑和实测有效的参数配置。2. 整体设计思路为什么是Embedding 聚类这条路2.1 传统 Trace 分析的三个死穴在讲方案之前先说说为什么常规做法不够用。目前大部分团队分析 Agent Trace无非三种方式第一种是关键词过滤。比如搜error、搜某个工具名看有没有报错。这种方式只能发现已知的坏情况对于Agent 用了奇怪的方式完成了任务或者某类任务普遍偏慢这种问题完全无感。第二种是单条 Trace 逐条看。打开一个 Session 的完整调用链从头看到尾。问题是 Agent 的调用链动辄几十层嵌套一个 Session 看下来十几分钟看十个 Session 一天就没了而且人脑很难在几十条 Trace 之间做横向对比。第三种是纯指标聚合。统计 P50、P99 延迟、平均 token 消耗、工具调用次数分布。这些指标能告诉你整体怎么样但没法告诉你哪一类任务怎么样。平均值是最会骗人的东西一个 Agent 可能 80% 的简单任务秒回20% 的复杂任务拖到超时平均下来看着还行实际体验是灾难。这三个死穴的共同点是缺少对行为模式的自动归纳。而聚类恰好就是干这个的。2.2 为什么选 Embedding 而不是规则匹配有人会问我能不能用规则来分类比如按调用了几个工具有没有走 RAG轮次多少来打标签。可以但很快会撞墙。原因是 Agent 的行为空间太大了。同样是调用搜索工具query 的语义可能千差万别同样是三轮推理中间的逻辑路径可能完全不同。规则匹配只能覆盖你事先想到的维度而 Agent 的失败模式往往出现在你没想到的地方。Embedding 的好处是把行为映射到语义空间让相似的执行路径自然靠近。这里的关键洞察是Agent 的一次执行本质上是一段有结构的文本——系统提示、用户输入、每一步的思考、工具调用的参数和返回、最终输出。把这段文本喂给 Embedding 模型得到的向量就编码了这次执行的语义指纹。两条 Trace 的向量余弦相似度高说明它们在做类似的事哪怕表面上的工具名不一样。2.3 聚类的粒度选择Session 还是 Span这是设计时第一个要拍板的决策。Trace 数据是树状结构一个 Session 包含多个 Trace一个 Trace 包含多个 Span。你到底聚哪一层我的经验是以 Session 为主、Span 为辅。原因很实际如果聚 Span粒度太细一个 Session 里几百个 Span 聚出来全是调用工具生成文本这种无意义的类看不出任务级别的模式。如果聚 Session粒度刚好对应一次用户请求的完整处理过程这正是我们关心的单元——用户感知到的快慢、成败都是 Session 级别的。但 Span 也不是完全不用。在 Session 聚类之后我会对每一类 Session 内部的 Span 序列再做一次轻量分析看这类任务的典型执行骨架是什么。这属于二次下钻后面会细讲。2.4 整体流水线把上面的思路串起来整条流水线是这样的Trace 采集 → Session 重组 → 行为文本构造 → Embedding 向量化 → 降维 → 聚类 → 类簇画像 → 可视化与下钻每一步都有坑下面逐个拆。这里先给个整体认知这不是一个跑一次就完事的离线任务而是一个应该定期跑的流水线。Agent 的行为会随着 prompt 调整、工具更新、用户群体变化而漂移聚类结果也要跟着更新否则你分析的是上个月的行为模式。3. 核心细节解析从原始 Trace 到可聚类向量3.1 Session 重组别小看这一步Trace 数据落到存储里通常是扁平的每条记录带trace_id、span_id、parent_span_id、session_id、时间戳。要按 Session 聚合第一步就是根据session_id把散落的 Span 捞回来再按parent_span_id重建调用树。这里有个容易忽略的点Session 的边界判定。很多框架的session_id是复用的一个用户连续对话十轮可能共用一个session_id。如果你直接按session_id聚合会把十轮对话揉成一个巨大的 SessionEmbedding 出来一团糨糊。我的做法是引入时间窗口切分同一个session_id下如果两条 Trace 的时间间隔超过阈值我一般设 5 分钟就切成两个逻辑 Session。这个阈值不是拍脑袋而是根据业务场景定的——客服类 Agent 用户思考时间长可以放宽到 10 分钟工具调用类 Agent 基本是连续执行2 分钟就够。def split_sessions(traces, gap_seconds300): traces.sort(keylambda t: t.start_time) sessions, current [], [] for t in traces: if current and (t.start_time - current[-1].end_time).seconds gap_seconds: sessions.append(current) current [] current.append(t) if current: sessions.append(current) return sessions注意切分阈值一定要可配置不同业务线差别很大。我见过有团队硬编码 30 秒结果把正常的用户打字停顿全切成了独立 Session聚类结果碎得没法看。3.2 行为文本构造决定聚类质量的关键Embedding 的输入是文本所以核心问题是怎么把一次 Session 的执行过程写成一段能表达行为的文本直接拼接所有 Span 的原始内容是最偷懒的做法但效果很差因为里面充斥着大量噪声时间戳、ID、重复的系统提示。我的构造模板是这样的[任务类型] {从首轮用户输入提取的意图} [执行路径] {按顺序列出关键 Span 的类型和工具名} [关键决策] {每轮推理的核心结论截断到 N 字} [工具调用] {工具名 参数摘要 返回状态} [结果] {成功/失败/部分成功}耗时 {X}s共 {N} 轮举个具体例子一个查天气并推荐穿衣的 Agent 执行构造出来的文本大概长这样[任务类型] 天气查询与生活建议 [执行路径] LLM推理 → 调用weather_api → LLM推理 → 调用clothing_db → LLM生成 [关键决策] 用户问北京天气获取到温度5度需要推荐保暖衣物 [工具调用] weather_api(北京,今日)成功; clothing_db(5度,户外)成功 [结果] 成功耗时 3.2s共 2 轮这样一段文本既保留了行为结构又去掉了噪声Embedding 出来的向量质量高很多。这里有个关键取舍关键决策要不要保留原文我的经验是保留但截断。完全去掉决策内容向量只能反映调了哪些工具丢失了语义完全保留长文本会稀释掉行为特征。我一般截断到每轮 50 字只留结论性的句子。3.3 Embedding 模型选型别盲目追排行榜热词里embedding模型排行是个高频搜索说明大家都在纠结选哪个。我的观点很直接Agent Trace 聚类这个场景不需要最强的通用 Embedding 模型需要的是对结构化短文本敏感的模型。原因是你的输入不是自然语言长文而是带标记的结构化文本。有些在通用榜单上排名很高的模型反而对[工具调用]这种标记不敏感因为它们训练时没见过这种格式。我实测下来选型时重点看三个维度维度说明我的建议维度大小向量维度768 或 1024 够用1536 收益递减且拖慢聚类中文支持中文语义区分度必须实测别只看榜单推理成本单条耗时海量 Trace 场景下成本比精度更重要具体到模型我一般会准备 2-3 个候选用一批人工标注过的 Session比如 200 条人工分成 10 类做小规模验证看哪个模型的聚类结果和人工标注的 ARI调整兰德指数最高。这个验证成本很低半天就能跑完但能避免选错模型后返工。实操心得Embedding 之前一定要做文本归一化——统一大小写、去掉多余空白、把数字 ID 替换成占位符。我踩过一次坑工具返回里带了随机 request_id导致本该聚在一起的 Session 因为 ID 不同被拆散了。3.4 降维不是可选项是必选项Embedding 出来是 768 维甚至更高直接聚类有两个问题一是维度灾难高维空间里距离度量会失效所有点看起来都差不多远二是计算量大几十万条向量做聚类很慢。所以降维是必须的。常用的是 UMAP 或 PCA。我的选择是UMAP 优先PCA 兜底UMAP 能更好地保留局部结构聚类效果通常更好但它是非线性的参数敏感n_neighbors和min_dist要调。PCA 是线性的快且稳定但保留的全局结构多、局部结构少聚类容易糊在一起。我的默认配置是 UMAP 降到 15-30 维不是降到 2 维2 维只用于可视化不用于聚类。降到 2 维再聚类是新手常犯的错误信息损失太大。import umap reducer umap.UMAP( n_neighbors15, # 小数据集用 5-10大数据集用 15-50 min_dist0.1, # 越小簇越紧凑 n_components20, # 聚类用 20 维可视化另降到 2 维 metriccosine ) vectors_reduced reducer.fit_transform(vectors)3.5 聚类算法HDBSCAN 为什么比 K-Means 更合适选聚类算法时K-Means 是最容易想到的但它有两个硬伤一是你得预先指定簇数量 K而 Agent 的行为类别你事先根本不知道有几个二是它假设簇是球形的而行为向量分布往往是不规则的。所以我强烈推荐HDBSCAN。它的优势不需要指定簇数量自动发现能识别噪声点不属于任何簇的 Session这些往往就是异常行为特别有价值对不规则形状的簇友好。代价是参数需要调主要是min_cluster_size最小簇大小和min_samples。我的经验值min_cluster_size设为总样本数的 1%-2%min_samples设为 5-10。import hdbscan clusterer hdbscan.HDBSCAN( min_cluster_sizemax(10, int(len(vectors) * 0.01)), min_samples5, metriceuclidean, cluster_selection_methodeom ) labels clusterer.fit_predict(vectors_reduced)跑完之后labels -1的就是噪声点。别急着丢掉它们先看看这些噪声点有什么共性——我遇到过好几次噪声点里藏着一批Agent 陷入死循环的异常 Session正是最该修的问题。4. 实操过程从零跑通一条聚类流水线4.1 数据准备与清洗假设你已经从日志平台导出了 Trace 数据格式是 JSON Lines每行一个 Span。第一步是加载和清洗。import json import pandas as pd def load_traces(path): records [] with open(path) as f: for line in f: records.append(json.loads(line)) df pd.DataFrame(records) # 基础清洗 df df.dropna(subset[session_id, span_id]) df[start_time] pd.to_datetime(df[start_time]) df[end_time] pd.to_datetime(df[end_time]) return df清洗阶段要处理几个常见脏数据缺失session_id的孤立 Span直接丢、时间戳格式不一致的统一转换、超长文本字段截断避免后续 Embedding 爆 token。4.2 Session 重组与特征提取按前面讲的逻辑切分 Session然后对每个 Session 提取行为特征。def build_session_text(session_spans): session_spans.sort(keylambda s: s[start_time]) path [] decisions [] tools [] for span in session_spans: stype span.get(span_type, unknown) if stype llm: path.append(LLM推理) if span.get(output_summary): decisions.append(span[output_summary][:50]) elif stype tool: tool_name span.get(tool_name, unknown) path.append(f调用{tool_name}) status 成功 if span.get(status) ok else 失败 tools.append(f{tool_name}{status}) total_time (session_spans[-1][end_time] - session_spans[0][start_time]).total_seconds() return f[执行路径] { → .join(path)} [关键决策] {; .join(decisions)} [工具调用] {; .join(tools)} [结果] 耗时 {total_time:.1f}s共 {len(path)} 步这段代码是核心实际项目中我会根据业务再补充任务类型字段从首轮用户输入用一个小模型分类得到和结果状态字段。4.3 向量化与批量处理海量 Trace 的 Embedding 一定要批量处理单条调用 API 会慢到怀疑人生。同时要做好失败重试和缓存。from tqdm import tqdm def embed_sessions(texts, batch_size64, cacheNone): vectors [] for i in tqdm(range(0, len(texts), batch_size)): batch texts[i:ibatch_size] # 先查缓存 if cache: cached [cache.get(t) for t in batch] if all(c is not None for c in cached): vectors.extend(cached) continue # 调用 Embedding 接口此处为示意 batch_vectors embedding_client.embed(batch) vectors.extend(batch_vectors) if cache: for t, v in zip(batch, batch_vectors): cache[t] v return vectors注意缓存 key 用文本的哈希不要用 Session ID。因为文本相同但 Session 不同的情况很常见比如两个用户问了同样的问题用文本哈希能提高缓存命中率。我实测缓存命中率能到 30%-40%省下不少成本。4.4 聚类与类簇画像聚类跑完后最有价值的一步是给每个类簇生成画像。光有一堆编号没意义得让工程师一眼看懂这一类是什么。我的画像生成逻辑是对每个簇取离簇中心最近的 5 条 Session 作为代表提取它们的共同特征高频工具、平均耗时、成功率、典型执行路径再用一个小模型生成一句自然语言描述。def profile_cluster(cluster_id, sessions, labels): members [s for s, l in zip(sessions, labels) if l cluster_id] avg_time sum(s[total_time] for s in members) / len(members) success_rate sum(1 for s in members if s[success]) / len(members) tool_counter Counter() for s in members: tool_counter.update(s[tools]) top_tools tool_counter.most_common(3) return { cluster_id: cluster_id, size: len(members), avg_time: round(avg_time, 2), success_rate: round(success_rate, 3), top_tools: top_tools, description: generate_description(members[:5]) }跑完画像你会得到一张表长这样簇 ID规模平均耗时成功率主要工具行为描述012402.1s0.98search, calc简单查询类单轮工具调用138012.4s0.72search, db, search多轮检索存在重复搜索29528.7s0.31code_exec代码执行类失败率高-12108.3s0.55混合噪声行为异常这张表一出来优化方向就非常明确了簇 2 成功率只有 31%必须重点排查簇 1 平均耗时 12 秒还带重复搜索说明检索策略有问题。4.5 可视化让非技术同学也能看懂聚类结果最终要给人看可视化很重要。我一般做两张图第一张是UMAP 降到 2 维的散点图每个点是一个 Session颜色代表簇点的大小代表耗时。这张图能直观看出簇的分布和重叠情况。第二张是簇的指标对比图横轴是簇纵轴是成功率或耗时用柱状图展示。这张图适合放进周报。可视化工具我用 Plotly交互性好能 hover 看每个点的详情。注意 2 维 UMAP 只用于展示不要用它做聚类前面强调过了。5. 常见问题与排查技巧实录5.1 聚类结果糊成一团怎么办这是最常见的问题表现为所有点被分到一两个大簇或者全是噪声。排查顺序先看 Embedding 质量。随机抽 10 条 Session人工判断哪些应该相似然后看它们的向量余弦相似度。如果人工认为相似的向量相似度却很低说明 Embedding 模型或文本构造有问题。检查降维维度。如果降到 2 维再聚类几乎必然糊。改回 15-30 维。调 HDBSCAN 参数。min_cluster_size太大小簇会被吞掉太小会碎成很多小簇。从总样本数的 1% 开始试。5.2 噪声点太多怎么处理噪声点占比超过 30% 就要警惕了。可能原因文本构造里噪声太多ID、时间戳没清干净Embedding 模型不适合你的文本格式数据本身确实高度异质那噪声多也正常。我的处理策略是先看噪声点的共性如果它们确实是一类特殊行为比如异常 Session那就单独拎出来分析不要强行塞进簇里。5.3 聚类结果不稳定每次跑都不一样UMAP 和 HDBSCAN 都有随机性。解决办法固定随机种子random_state42如果数据量大用approximate_predict对新数据做增量分配而不是每次全量重跑对稳定性要求高的场景跑多次取共识consensus clustering。5.4 常见问题速查表问题现象可能原因排查方向全是一个簇降维过度/参数不当检查维度调 min_cluster_size噪声占比过高文本噪声多/模型不匹配清洗文本换 Embedding 模型簇内行为不一致文本构造丢失关键信息补充决策内容调整截断长度每次结果不同随机性未固定设随机种子用增量预测聚类很慢维度太高/样本太多降维采样用近似算法5.5 几个我踩过的坑坑一把系统提示拼进了文本。系统提示对所有 Session 都一样拼进去只会稀释差异让所有向量都往一个方向靠。一定要剔除。坑二忽略了时间维度。早期我把所有历史 Session 一起聚类结果发现最近一周的行为和一个月前的行为混在一起看不出趋势。后来改成按周滚动聚类才能发现行为漂移。坑三聚类完就完事没有闭环。聚类只是手段目的是优化。我现在的做法是每次聚类后把画像表同步给团队标注出需要重点关注的簇然后跟踪这些簇的指标变化。没有闭环的聚类就是自娱自乐。6. 让聚类真正产生价值的几个延伸玩法6.1 行为漂移监控把每周的聚类结果做对比看簇的分布有没有明显变化。如果某个原本很小的簇突然变大说明 Agent 的行为模式变了可能是 prompt 改动引入的副作用也可能是用户群体变化。这是最早期的预警信号。具体做法是计算相邻两周簇分布的 JS 散度超过阈值就告警。我实测这个指标比单纯的延迟告警更早发现问题。6.2 用聚类结果反哺 Prompt 优化每个簇的典型执行路径其实就是 Agent 的行为模板。如果发现某个簇的路径特别绕比如反复调用同一个工具说明 prompt 里对工具使用的引导不够清晰。把这类簇的代表 Session 拿出来人工分析后针对性改 prompt效果立竿见影。6.3 异常检测的天然素材噪声点label -1和成功率低的簇就是异常检测的天然训练集。我一般会把它们导出人工标注后作为负样本训练一个轻量的异常分类器上线后实时监控新 Session。6.4 成本归因把每个簇的 token 消耗、工具调用次数统计出来就能做成本归因。很多时候你会发现80% 的成本集中在 20% 的簇上优化这几个簇就能大幅降本。这比笼统地看总成本有用得多。7. 一些参数和选型的经验值最后把我常用的参数配置整理一下方便直接抄作业。这些值不是金科玉律但作为起点能省不少调参时间。环节参数推荐值说明Session 切分时间间隔300s客服类可放宽到 600s文本构造决策截断50 字/轮太长稀释特征Embedding维度768/10241536 收益递减降维UMAP n_components20聚类用非可视化降维UMAP n_neighbors15大数据集可到 50聚类min_cluster_size样本数 1%最小 10聚类min_samples5噪声多时调大这套流水线我从最初的手写脚本到现在封装成可配置的 pipeline前后迭代了大概七八个版本。最大的体会是聚类本身不难难的是把 Trace 数据整理成值得聚类的形态。文本构造那一步花的时间往往比调聚类参数多得多但它决定了整个方案的上限。如果你刚开始做建议先别急着上复杂算法把 Session 重组和文本构造做扎实用最简单的 K-Means 跑一版看看效果往往就能发现不少问题。等这套流程跑顺了再逐步引入 UMAP、HDBSCAN 这些更精细的工具收益会更明显。
返回列表