
1. 项目概述这不是在画“行为地图”而是在给 Agent 做一次高精度的“行为 CT 扫描”你有没有遇到过这样的情况一个看似聪明的 Agent在测试环境里跑得飞快、回答精准一上线就频繁超时、逻辑错乱、甚至反复兜圈子日志里只有一堆 timestamp 和 method_nametrace_id 像天书一样串着几十个 span但没人能说清——到底是哪个环节开始“失智”的是工具调用失败后没做 fallback还是记忆模块在长对话中悄悄丢帧又或者根本不是代码 bug而是用户提问方式的细微变化触发了模型推理路径的系统性偏移“智能聚类从海量 Trace 中理解 Agent 的行为和表现”这个标题说的正是解决这类问题的核心方法论。它不依赖人工埋点、不靠猜、不靠“看运气”而是把每一次 Agent 的完整执行过程——从用户输入、规划决策、工具调用、记忆检索、到最终输出——全部转化为结构化 trace 数据再用聚类算法自动发现其中隐藏的行为模式。这里的“智能”不是指聚类算法本身多高级K-means 或 DBSCAN 完全够用而是指整个 pipeline 的设计逻辑它知道该提取哪些特征、该忽略哪些噪声、该在哪个粒度上分组才能让聚类结果真正对应到可解释、可干预的业务行为上。比如把所有“规划阶段耗时 3s 且工具调用失败率 80%”的 trace 归为一类这一类就极大概率指向“工具描述模糊导致 LLM 无法正确生成 action 参数”的共性缺陷再比如把“记忆检索命中率 20% 且上下文 token 超限”的 trace 单独聚出来那基本就是提示词工程或记忆压缩策略需要重构的明确信号。我过去三年带团队落地过 7 个不同形态的 Agent 项目从客服导购到金融投顾最深的体会是Agent 的稳定性问题90% 不出在单点代码而出在行为路径的组合脆弱性上。一个 span 的 error_code 是明确的但十个 span 组成的“失败链路”却有上百种变体。传统监控只告诉你“失败了”而智能聚类告诉你“哪一类失败正在批量发生”。它适合三类人一是正在被线上 Agent 行为不可控问题困扰的算法工程师和 SRE二是想系统性评估 Agent 能力边界的 QA 团队三是准备把 Agent 接入核心业务、需要提前识别风险模式的产品负责人。它不是替代 APM 工具而是给 APM 加上一层“行为语义理解”能力——让 trace 数据从“技术流水账”变成“业务行为诊断报告”。2. 核心思路拆解为什么必须是“聚类”而不是“规则匹配”或“异常检测”2.1 传统方案的三大死穴决定了聚类是唯一可行路径很多团队第一反应是写规则“如果 span.duration 5000ms 且 statusERROR则告警”。这在单服务时代有效但在 Agent 场景下会迅速失效。原因有三第一行为缺陷具有强组合性。一个典型的“幻觉式工具调用”可能表现为规划 span 耗时正常1s但生成的 tool_name 拼写错误如 “serach_web”工具调用 span 状态是 SUCCESSAPI 返回了 200但 response.body 是空字符串后续的解析 span 因为空响应抛出异常。三个 span 各自都“合规”但组合起来就是致命错误。规则引擎很难覆盖这种跨 span 的隐式逻辑断裂。第二阈值设定毫无依据。设 duration 5000ms 为慢那当 Agent 在处理一份 50 页 PDF 时10s 也合理。设 error_rate 5% 为异常可如果某类用户高频触发一个尚未优化的冷门工具error_rate 天然就是 40%。规则依赖静态阈值而 Agent 的行为边界本身就是动态的、场景化的。第三无法发现未知模式。规则只能捕获你预设的“已知坏行为”但 Agent 最危险的问题往往来自“从未见过的新失败模式”。比如某次模型版本升级后Agent 开始在特定日期格式如 “2024-01-01” vs “Jan 1, 2024”下产生系统性记忆混淆这种模式不会触发任何现有 error_code但会导致后续所有决策偏移。规则对此完全失明。提示我亲眼见过一个团队花两个月写了一套包含 137 条规则的监控脚本上线后漏报了 68% 的真实线上故障因为故障模式全是他们没预料到的“新组合”。2.2 聚类为何能破局它把“定义问题”交给数据自己聚类的本质是让数据根据内在相似性自动分组。应用到 Agent trace 上它的优势在于无监督不预设故障定义我们不告诉算法“什么是坏”而是提供一组能刻画行为本质的特征如各 span 的耗时分布、状态码序列、token 使用量、工具调用深度、记忆检索命中率等算法会找出哪些 trace 在这些维度上“长得像”。那些占比小但内部高度一致的簇往往就是潜伏的、未被识别的行为缺陷。天然支持模式发现与归因聚类完成后我们不是只看“哪个簇报错了”而是深入分析每个簇的特征中心点centroid。比如簇 A 的中心点显示规划耗时均值 1200ms远高于全局均值 450ms、工具调用次数均值 1.2 次低于全局均值 3.8 次、memory_hit_rate 均值 5%。这个特征组合强烈暗示该簇 Agent 正在陷入“规划-失败-重试”的无效循环根源很可能是 prompt 中的约束条件过于严苛导致 LLM 首次规划即失败又因重试机制缺失而卡死。这比单纯看到“规划 span 报错”要深刻得多。可扩展性强适配演进当 Agent 新增一个记忆模块或接入新工具时我们只需在特征工程中加入对应的指标如 new_memory_module_latency聚类 pipeline 无需修改即可识别出新模块引入的行为模式变化。规则引擎则需要人工补全所有新组合。2.3 关键取舍为什么选 DBSCAN 而非 K-means在具体算法选型上我们放弃了更常见的 K-means坚定选择了 DBSCANDensity-Based Spatial Clustering of Applications with Noise。理由非常实际K-means 强制所有点归属某簇且要求簇呈球形。Agent trace 的特征空间极度不规则有的 trace 极短仅 2 个 span有的极长超 50 个 span有的耗时集中在规划阶段有的分散在工具调用它们在多维空间里绝不是一堆“球状云团”而是稀疏、拉长、带噪声的流形。K-means 会强行把明显不属于任何群体的离群 trace如网络抖动导致的单点超时硬塞进某个簇污染整个簇的特征解释。DBSCAN 能自然识别噪声点Noise Points。它基于密度高密度区域形成簇低密度区域的点被标记为噪声。在我们的实践中被 DBSCAN 标记为噪声的 trace85% 以上都是真正的、孤立的、偶发的技术异常如某次 DNS 解析失败、某台机器临时 GC。这些点恰恰是我们最不想混入行为分析的“干扰项”。保留它们作为独立类别反而帮助我们快速区分“系统性行为缺陷”和“偶发技术故障”。无需预设簇数量 K。K-means 要求你先猜一个 K 值而 Agent 的行为模式数量是未知的、动态的。用肘部法则或轮廓系数去试既耗时又主观。DBSCAN 只需两个参数eps邻域半径和min_samples核心点最小邻居数这两个参数有明确的物理意义可通过 trace 特征的标准差进行经验估算实操中调整成本极低。3. 核心细节解析从原始 trace 到可聚类特征向量的完整炼金术3.1 Trace 数据的“清洗-对齐-标准化”三步法原始 trace 数据通常来自 OpenTelemetry 或 Jaeger是一堆嵌套 JSON直接喂给聚类算法是灾难性的。我们必须先完成三步“预处理”第一步清洗Cleaning—— 剔除无效与污染数据过滤掉 trace_state ! OK 的 traceOpenTelemetry 规范中trace_state字段用于标记 trace 的采样状态。00表示未采样01表示已采样。我们只保留01的 trace避免因采样率波动导致的统计偏差。曾有项目因混入大量00trace导致聚类结果严重偏向“超短路径”误判为 Agent 过于简单。剔除 span_count 3 的 trace一个健康的 Agent trace 至少包含user_input、planning、tool_call或final_answer三个核心 span。少于 3 个 span 的 trace要么是采样截断不完整要么是测试探针非真实用户行为必须丢弃。我们用jq .spans | length快速统计并过滤。统一时间戳精度原始 trace 中start_time_unix_nano可能是纳秒级而某些 SDK 输出的是毫秒级。不统一会导致duration计算错误。我们统一转换为微秒μs精度并存储为整数避免浮点误差影响距离计算。第二步对齐Alignment—— 让不同长度的 trace “站在同一起跑线”这是最关键的一步。K-means 或 DBSCAN 要求所有样本维度相同。但一个 trace 可能有 5 个 span另一个有 25 个如何把它们变成固定长度的向量我们的方案是不按 span 序列建模而按 span 类型span_kind聚合建模。我们预先定义 Agent 的核心 span 类型集合[llm_planning, tool_call, memory_retrieval, llm_generation, final_output]。对于每一个 trace我们遍历其所有 span按类型分组然后对每组计算一组统计特征。例如对所有llm_planning类型的 span计算mean_duration,std_duration,count,error_rate。对所有tool_call类型的 span计算mean_duration,max_duration,success_rate,unique_tool_count调用了几种不同的工具。对所有memory_retrieval类型的 span计算hit_rate,mean_retrieved_tokens,mean_latency。这样无论 trace 多长它最终都会被压缩成一个固定长度的向量。以 5 种类型为例每种类型计算 4 个特征总维度就是 20。这个向量不再记录“第几个 span 干了什么”而是记录“Agent 在规划、调用、记忆等各个能力模块上的整体表现”。注意这个设计牺牲了 span 的时序信息但换来了可聚类性和可解释性。时序模式如“先失败后重试”我们通过分析簇内 trace 的 span 序列来事后挖掘而非塞进特征向量。第三步标准化Standardization—— 让不同量纲的特征“公平竞争”特征向量里mean_duration可能是 5000毫秒hit_rate是 0.85unique_tool_count是 3。如果不标准化duration的数值差异会完全主导欧氏距离计算其他特征形同虚设。我们采用Z-score 标准化x (x - μ) / σ其中 μ 和 σ 是该特征在全体 trace 样本上的均值和标准差。为什么不用 Min-Max因为 Min-Max 对异常值outlier极其敏感。一个 trace 的max_duration是 30s因网络问题会把整个特征范围拉到 0-30000导致其他 99% 的 trace 在标准化后都挤在 0.01 附近失去区分度。Z-score 用标准差作为分母天然对离群值鲁棒。3.2 特征工程哪些指标真正“懂” Agent 的行为特征决定了聚类的上限。我们花了两个月时间从 47 个候选指标中筛选出 12 个真正能反映 Agent 行为健康度的核心特征。筛选标准只有一条该特征的变化是否能稳定地、可复现地对应到一个可解释、可干预的 Agent 内部状态或策略缺陷以下是最终入选的特征及其物理意义特征名称计算方式为什么重要典型问题映射planning_duration_mean所有llm_planningspan 的平均耗时规划是 Agent 的“大脑”耗时异常直接反映 prompt 复杂度或模型负载Prompt 过长、约束过多导致 LLM 思考时间激增planning_span_countllm_planningspan 的出现次数健康 Agent 通常只规划一次。多次规划意味着首次失败后无有效 fallback规划失败后未触发重试或降级逻辑tool_success_ratetool_callspan 中status_code 200的比例工具调用是 Agent 的“手脚”成功率低说明工具集成或参数生成有问题LLM 生成的 tool_params 格式错误、工具 API 变更未同步tool_unique_counttool_callspan 中调用的不同工具名的数量反映 Agent 的“技能广度”和任务分解能力过度依赖单一工具或无法根据需求灵活切换工具memory_hit_ratememory_retrievalspan 中hit true的比例记忆是 Agent 的“经验库”命中率低说明记忆管理或检索策略失效记忆向量化质量差、检索 query 构造不合理、记忆过期策略不当memory_latency_meanmemory_retrievalspan 的平均耗时记忆检索不应成为瓶颈向量数据库索引未优化、检索 top-k 设置过大generation_token_ratiollm_generationspan 的output_tokens / input_tokens反映 LLM 的“表达效率”。比值过高如 5可能意味着冗余输出Prompt 中的“请详细回答”等指令导致模型过度发挥final_output_lengthfinal_outputspan 的output_text字符长度直接关联用户体验。过短可能未回答过长可能信息过载输出格式化逻辑缺失、未做内容摘要trace_span_counttrace 中 span 的总数量整体复杂度指标。异常高可能意味着无限循环或重试风暴工具调用失败后无退出条件进入死循环trace_duration_totaltrace 的总耗时end_time - start_time用户端感知的“响应时间”多个慢 span 叠加、异步调用未正确 awaiterror_span_countstatus ! OK 的 span 数量最粗粒度的“失败计数”作为辅助指标验证聚类结果是否与错误相关is_user_query_longuser_input 字符长度 200 ? 1 : 0二值化特征捕捉长 query 的特殊影响长 query 更易触发 LLM 上下文截断、规划失误这个特征集的设计哲学是80% 的特征描述“能力模块的健康度”20% 的特征描述“整体行为的宏观表现”。它避开了所有“技术性”但“行为无关”的指标如http.status_code对内部 span 无意义、process.pid纯运维信息、span.kind类型已用于分组无需再作为特征。3.3 DBSCAN 参数调优eps和min_samples的实战经验值DBSCAN 的效果90% 取决于eps邻域半径和min_samples核心点最小邻居数这两个参数。网上教程常教你怎么用 k-distance 图但在海量 trace 场景下那太慢。我们总结了一套 5 分钟快速调优法min_samples的确定它代表你希望一个“行为模式”至少有多少个实例才值得被关注。太少如 1会把噪声当模式太多如 1000会淹没早期、小规模的问题。我们的经验值是min_samples max(5, total_trace_count * 0.001)。例如你有 100 万 tracemin_samples 1000只有 1 万 tracemin_samples 10。这个公式保证了簇的规模既有统计显著性又不会因总量小而失敏。eps的确定这是难点。eps决定了“多相似才算一类”。我们不直接调eps而是先计算所有 trace 特征向量的平均成对欧氏距离Mean Pairwise Distance, MPD。用 Python 一行搞定from sklearn.metrics.pairwise import pairwise_distances mpd np.mean(pairwise_distances(X, metriceuclidean))然后eps的初始值设为mpd * 0.3。为什么是 0.3因为 MPD 是所有点对的平均距离而我们关心的是“紧密聚集”的点。0.3 意味着我们只把距离小于平均距离 30% 的点视为“邻居”这足够严格能有效分离不同行为模式。实测下来这个初始值在 80% 的项目中经过最多 2 轮微调±0.05就能得到理想结果。实操心得调参时永远先看n_clusters_簇数量和n_noise_噪声点数量。理想的组合是n_clusters_在 3-12 之间太少则粒度太粗太多则过拟合n_noise_占总 trace 数的 5%-15%太高说明eps太小把正常变异当噪声太低说明eps太大把不同模式强行合并。我们曾在一个电商 Agent 项目中将eps从 0.25 调到 0.28n_clusters_从 18 降到 7n_noise_从 2% 升到 8%聚类结果立刻从“一团乱麻”变得清晰可读。4. 实操流程从数据导入到行为洞察的端到端实现4.1 环境准备与依赖安装轻量级零 GPU 依赖整个 pipeline 的核心是 Python对硬件要求极低。一台 8 核 16GB 内存的普通服务器轻松处理日均千万级 trace。我们刻意避开了 Spark、Flink 等重量级框架因为Trace 数据虽大但特征向量极小100 万 trace每个 20 维浮点数内存占用仅约 1.6GB。聚类是批处理非实时我们每天凌晨跑一次分析昨日数据对延迟不敏感。降低团队使用门槛算法、SRE、甚至资深 QA 都能快速上手调试无需学习新生态。所需依赖清单requirements.txtpandas2.0.3 numpy1.24.3 scikit-learn1.3.0 opentelemetry-proto1.21.0 protobuf4.23.4 joblib1.3.2关键点scikit-learn必须 1.2.0因为旧版本的 DBSCAN 在处理大型稀疏矩阵时有内存泄漏。opentelemetry-proto用于解析.proto格式的 trace 数据这是 OTel 的标准序列化格式比解析 JSON 快 5 倍且内存占用低 70%。4.2 数据加载与预处理从 OTLP 导出文件到特征 DataFrame假设你已通过 OTel Collector 将 trace 数据导出为traces_20240501.json文件每行一个 trace 的 JSON。加载脚本load_traces.py的核心逻辑如下import json import pandas as pd import numpy as np from opentelemetry.proto.trace.v1.trace_pb2 import TracesData def parse_trace_json(line): 解析单行 JSON trace返回结构化字典 data json.loads(line) # 提取关键字段trace_id, spans, resource_attributes trace_id data[resourceSpans][0][scopeSpans][0][spans][0][traceId] spans [] for rs in data[resourceSpans]: for ss in rs[scopeSpans]: for span in ss[spans]: # 标准化 span 字段确保 key 一致 spans.append({ span_id: span.get(spanId, ), name: span.get(name, ), kind: span.get(kind, SPAN_KIND_UNSPECIFIED), start_time_unix_nano: int(span.get(startTimeUnixNano, 0)), end_time_unix_nano: int(span.get(endTimeUnixNano, 0)), status_code: span.get(status, {}).get(code, 0), attributes: span.get(attributes, {}), parent_span_id: span.get(parentSpanId, ) }) return {trace_id: trace_id, spans: spans} def load_traces(file_path): 主加载函数返回包含所有 trace 的列表 traces [] with open(file_path, r) as f: for line_num, line in enumerate(f, 1): try: trace_data parse_trace_json(line.strip()) if len(trace_data[spans]) 3: # 过滤掉太短的 trace traces.append(trace_data) except Exception as e: print(fLine {line_num} parse error: {e}) continue return traces # 使用示例 traces load_traces(traces_20240501.json) print(fLoaded {len(traces)} valid traces.)这段代码的关键在于parse_trace_json函数。它不依赖任何第三方 OTel SDK而是直接解析 JSON 结构因为 OTel 的 JSON 格式是公开、稳定的。我们手动提取traceId、spans等核心字段并对spans数组进行标准化确保后续特征计算逻辑能统一处理。4.3 特征提取与向量化20 行代码构建行为指纹特征提取是整个 pipeline 的心脏。我们将其封装为extract_features.py核心函数extract_trace_features(trace)如下def extract_trace_features(trace): 从单个 trace 字典中提取 12 维特征向量 spans trace[spans] # 初始化各类型 span 的空列表 planning_spans [] tool_spans [] memory_spans [] generation_spans [] final_spans [] # 按类型分类 span for span in spans: name span[name].lower() if plan in name or llm in name and plan in name: planning_spans.append(span) elif tool in name or call in name: tool_spans.append(span) elif memory in name or retriev in name: memory_spans.append(span) elif generate in name or llm in name and gen in name: generation_spans.append(span) elif final in name or output in name: final_spans.append(span) # 计算特征 features {} # Planning features if planning_spans: durations [s[end_time_unix_nano] - s[start_time_unix_nano] for s in planning_spans] features[planning_duration_mean] np.mean(durations) / 1000 # 转为毫秒 features[planning_span_count] len(planning_spans) else: features[planning_duration_mean] 0 features[planning_span_count] 0 # Tool features if tool_spans: success_count sum(1 for s in tool_spans if s[status_code] 200) features[tool_success_rate] success_count / len(tool_spans) features[tool_unique_count] len(set(s.get(attributes, {}).get(tool.name, ) for s in tool_spans)) else: features[tool_success_rate] 1.0 features[tool_unique_count] 0 # Memory features if memory_spans: hit_count sum(1 for s in memory_spans if s.get(attributes, {}).get(memory.hit, False)) features[memory_hit_rate] hit_count / len(memory_spans) latencies [s[end_time_unix_nano] - s[start_time_unix_nano] for s in memory_spans] features[memory_latency_mean] np.mean(latencies) / 1000 else: features[memory_hit_rate] 0 features[memory_latency_mean] 0 # Generation features if generation_spans: input_tokens [s.get(attributes, {}).get(llm.token.input, 0) for s in generation_spans] output_tokens [s.get(attributes, {}).get(llm.token.output, 0) for s in generation_spans] # 避免除零 avg_input np.mean(input_tokens) if input_tokens else 1 avg_output np.mean(output_tokens) if output_tokens else 0 features[generation_token_ratio] avg_output / avg_input if avg_input 0 else 0 else: features[generation_token_ratio] 0 # Final output feature if final_spans: text_len len(final_spans[0].get(attributes, {}).get(output.text, )) features[final_output_length] text_len else: features[final_output_length] 0 # Trace-level features total_duration spans[-1][end_time_unix_nano] - spans[0][start_time_unix_nano] if spans else 0 features[trace_span_count] len(spans) features[trace_duration_total] total_duration / 1000 features[error_span_count] sum(1 for s in spans if s[status_code] ! 200) # Binary feature user_input next((s for s in spans if s[name] user_input), None) if user_input: input_text user_input.get(attributes, {}).get(user.input.text, ) features[is_user_query_long] 1 if len(input_text) 200 else 0 else: features[is_user_query_long] 0 # 返回 12 维 numpy 数组顺序与特征表一致 return np.array([ features[planning_duration_mean], features[planning_span_count], features[tool_success_rate], features[tool_unique_count], features[memory_hit_rate], features[memory_latency_mean], features[generation_token_ratio], features[final_output_length], features[trace_span_count], features[trace_duration_total], features[error_span_count], features[is_user_query_long] ]) # 批量提取 X np.array([extract_trace_features(t) for t in traces]) print(fFeature matrix shape: {X.shape}) # 应为 (N, 12)这段代码的精妙之处在于它用最朴素的if/else和for循环完成了复杂的 span 分类和特征计算。没有魔法只有对 Agent 行为逻辑的深刻理解。例如planning_span_count的计算直接揭示了 Agent 是否具备“一次成功”的规划能力is_user_query_long这个二值特征虽然简单却在多个项目中成功聚出了“长 query 下记忆检索失效”的关键簇。4.4 DBSCAN 聚类与结果分析不只是打标签更是找病因有了特征矩阵X聚类就是一行命令from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler # 标准化 scaler StandardScaler() X_scaled scaler.fit_transform(X) # DBSCAN 聚类 clustering DBSCAN(eps0.28, min_samples100).fit(X_scaled) labels clustering.labels_ # 统计 n_clusters len(set(labels)) - (1 if -1 in labels else 0) n_noise list(labels).count(-1) print(fFound {n_clusters} clusters, {n_noise} noise points.)但聚类结束才是分析的开始。我们绝不满足于“簇 0 有 5000 个 trace簇 1 有 300 个 trace”。我们的分析脚本analyze_clusters.py会为每个簇生成一份《行为诊断简报》def analyze_cluster(cluster_id, X, labels, traces): 分析单个簇返回可读性极强的诊断报告 # 获取该簇的所有 trace 索引 cluster_mask (labels cluster_id) cluster_traces [traces[i] for i in range(len(traces)) if cluster_mask[i]] # 计算该簇的特征中心centroid cluster_X X[cluster_mask] centroid np.mean(cluster_X, axis0) # 与全局均值对比找出显著偏离的特征|z-score| 2 global_mean np.mean(X, axis0) global_std np.std(X, axis0) 1e-8 # 防止除零 z_scores np.abs((centroid - global_mean) / global_std) # 找出 top 3 最偏离的特征 top_deviant_indices np.argsort(z_scores)[-3:][::-1] feature_names [ planning_duration_mean, planning_span_count, tool_success_rate, tool_unique_count, memory_hit_rate, memory_latency_mean, generation_token_ratio, final_output_length, trace_span_count, trace_duration_total, error_span_count, is_user_query_long ] report f Cluster {cluster_id} Diagnostic Report \n report fSize: {len(cluster_traces)} traces ({len(cluster_traces)/len(traces)*100:.1f}% of total)\n report Top 3 Deviant Features (vs Global Mean):\n for idx in top_deviant_indices: feat_name feature_names[idx] feat_val centroid[idx] global_val global_mean[idx] report f • {feat_name}: {feat_val:.3f} (Global: {global_val:.3f}) - Z-score: {z_scores[idx]:.2f}\n # 抽取 3 个典型 trace展示其 span 序列用于人工验证 sample_traces cluster_traces[:3] report \nSample Trace Span Sequences (first 5 spans):\n for i, t in enumerate(sample_traces): span_names [s[name] for s in t[spans][:5]] report f Trace {i1}: { - .join(span_names)}\n return report # 为每个簇生成报告 for cluster_id in set(labels): if cluster_id -1: continue # 跳过噪声点 print(analyze_cluster(cluster_id, X, labels, traces))这份报告的价值在于它把冰冷的数字翻译成了工程师能立刻行动的语言。例如一份报告指出• planning_duration_mean: 2450.321 (Global: 450.123) - Z-score: 8.72 • planning_span_count: 3.876 (Global: 1.023) - Z-score: 7.51 • tool_success_rate: 0.124 (Global: 0.921) - Z-score: 9.33这几乎可以断定该簇 Agent 正在经历“规划-失败-重试-再失败-再重试”的恶性循环根源极大概率是规划 prompt 中的约束条件如“必须使用工具A”与当前用户 query 不兼容导致 LLM 无法生成有效 action。工程师拿到这份报告5 分钟内就能定位到 prompt 的具体行号并开始修改。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 问题聚类结果“全是一类”n_clusters_ 1n_noise_ 0这是新手最常遇到的“假阳性”问题。看起来算法运行成功但结果毫无价值。根本原因只有一个你的特征向量缺乏足够的区分度所有 trace 在特征空间里挤成一团。排查步骤检查特征标准化打印X_scaled的前几行确认每个特征的均值是否接近 0标准差是否接近 1。如果某个特征如trace_span_count的值域是 0-100而另一个如is_user_query_long是 0 或 1标准化后前者仍会主导距离计算。解决方案对trace_span_count这类整数计数特征改用RobustScaler用中位数和四分位距缩放它对离群值更鲁棒