Multi-Agent系统Tracing实战:从线上事故到可观测性体系搭建
1. 从一次线上事故说起为什么Multi-Agent系统必须做Tracing去年年底我参与的一个多智能体协作项目在灰度阶段翻了一次车。现象很诡异用户提交一个复合任务系统返回的结果里有一半字段是空的但日志里没有任何报错每个Agent单独跑测试用例都是绿的。我们花了整整两天最后靠着一个临时加的调用链埋点才发现——规划Agent把子任务分发给了检索Agent和计算Agent检索Agent返回的上下文被计算Agent覆盖写入了同一个共享内存槽位导致最终汇总Agent读到的是一份被污染的数据。这个坑让我彻底明白一件事Multi-Agent系统的复杂度不是线性叠加而是指数级爆炸的。单Agent你还能靠print大法定位问题一旦Agent数量超过三个、开始互相调用、共享状态、异步执行没有一套完整的Tracing记录执行链你就是在黑箱里抓瞎。这篇内容我想聊的就是这件事怎么给Multi-Agent系统搭一套能用的Tracing体系把每个Agent的输入、输出、决策依据、调用关系、耗时、Token消耗全部串成一条可回溯的执行链。适合正在做Agent编排、任务分发、多角色协作系统的同学参考不管你是用现成框架还是手搓调度器这套思路都能落地。核心关键词我先摆出来Multi-Agent、Tracing、执行链、Span、TraceID、上下文传播、可观测性。这几个词后面会反复出现你先把它们的关系在脑子里建立起来——Trace是一条完整的任务执行链Span是链上的一个节点通常对应一次Agent调用或一次工具调用TraceID是贯穿整条链的唯一标识上下文传播是让TraceID在Agent之间传递的机制。2. Multi-Agent Tracing的核心设计思路拆解2.1 单Agent Tracing和Multi-Agent Tracing的本质区别很多人做Agent可观测性的时候习惯性地把单Agent那套日志方案直接搬过来结果发现根本不够用。我先把两者的差异讲清楚你就知道为什么必须重新设计。单Agent场景下一次请求就是一条直线用户输入 → LLM推理 → 工具调用 → LLM再推理 → 输出。你只要记录每一步的输入输出按时间排序就能还原全过程。这种场景下一个简单的结构化日志就够了。Multi-Agent场景完全不是这么回事。它有三个显著特征让传统日志方案失效第一是调用关系的树状/图状结构。规划Agent调用检索Agent检索Agent可能又调用了一个摘要Agent同时规划Agent还并行调用了计算Agent。这不是一条线是一棵树甚至一张有向图。你用平铺的日志行去记录根本看不出谁调用了谁。第二是上下文的多路复用与隔离。多个Agent可能共享一部分上下文比如原始任务描述但又有各自私有的中间状态。Tracing必须能区分哪些是共享的、哪些是私有的否则排查问题时你会被误导。第三是异步与并发。Agent之间经常是异步调用的A发起对B的调用后不等B返回就继续干别的。这时候时间戳排序会骗你你必须靠父子Span关系来还原真实的执行顺序。我踩过的坑早期我用时间戳排序来还原执行链结果异步场景下完全错乱。后来改成基于parent_span_id构建调用树问题才解决。记住执行顺序要靠Span的父子关系推导不能靠时间戳。2.2 为什么选Span模型而不是简单的日志关联设计Tracing方案时我对比过三种主流做法最后选了Span模型。这里把选型逻辑摊开讲方便你判断自己的场景适不适用。方案实现方式优点缺点适用场景日志关联ID每条日志带一个request_id实现简单改造成本低无法表达层级关系无法记录耗时区间单Agent、简单链路事件流记录每个事件及因果关系表达力强适合复杂因果查询和可视化复杂存储成本高研究型、需要因果分析的场景Span模型每个操作一个Span含parent_id、start、end层级清晰天然支持耗时统计生态成熟需要上下文传播机制Multi-Agent、分布式系统Span模型的核心优势在于它天然就是一棵树。每个Span记录四个关键信息TraceID整条链的唯一标识、SpanID当前节点标识、ParentSpanID父节点标识、时间区间start_time到end_time。有了这四个信息你就能把任意复杂的Agent调用关系还原成一棵可展开的树。我实测下来Span模型对Multi-Agent场景的适配度是最高的。因为Agent调用本质上就是一个操作触发了另一个操作这和Span的父子语义完全吻合。而且OpenTelemetry这套生态已经非常成熟你不需要从零造轮子。2.3 TraceID的生成与传播策略TraceID是整个Tracing体系的基石它的生成和传播策略直接决定了你能不能把跨Agent的调用串起来。这块我踩过不少坑详细说说。生成策略上我建议用128位的随机十六进制字符串也就是32个字符。为什么不用UUID因为UUID有版本位和变体位虽然也能用但OpenTelemetry标准用的是128位随机数为了生态兼容我建议对齐这个标准。生成时机是在任务入口处也就是用户请求进入系统的第一刻由调度器或网关生成。传播策略是重点。TraceID必须在Agent之间传递否则下游Agent生成的Span就挂不到同一棵树上。传播方式取决于你的Agent通信机制如果是函数直接调用最简单把TraceID作为参数透传即可或者用contextvarsPython这类上下文变量自动传播。如果是消息队列通信必须把TraceID写进消息头message header消费方从消息头读取并注入到自己的上下文。如果是HTTP/RPC调用遵循W3C Trace Context标准放在traceparent头里格式是00-{trace_id}-{span_id}-{flags}。如果是共享内存/黑板模式需要在共享状态里显式维护一个trace上下文区域每个Agent读写时都要带上。关键提醒上下文传播最容易在异步边界丢失。比如Agent A用asyncio.create_task起了个协程去调用Agent B如果没显式传递contextB就拿不到TraceID。我的做法是封装一个traced_task装饰器强制在任务创建时注入上下文。2.4 采样策略全量记录还是按需采样生产环境里全量记录每个Span的成本是很高的。一个复杂任务可能产生几十上百个Span每个Span还带着完整的输入输出存储和传输压力都不小。所以采样策略必须提前设计。我的经验是分三层采样第一层是入口采样。在任务入口决定这个Trace是否被记录。简单场景用固定比例采样比如10%复杂场景用尾部采样tail-based sampling——先缓存整条链的Span等任务结束后根据结果决定是否保留。比如任务失败了一定要保留任务成功且耗时正常的可以丢弃。第二层是Span级采样。即使Trace被采样了也不是所有Span都需要完整记录。LLM调用的Span记录完整prompt和response工具调用的Span只记录参数摘要内部状态流转的Span只记录关键字段。第三层是字段级脱敏。用户输入里可能包含敏感信息记录前必须做脱敏。我的做法是在Span创建时过一个脱敏过滤器把手机号、邮箱、身份证号这类模式匹配的字段替换掉。采样率这块给个参考值开发环境100%全采预发环境50%生产环境核心链路10%加失败全采。这个比例可以根据你的存储成本调整。3. 核心细节解析与实操要点3.1 Span的数据结构设计Span的数据结构设计得好不好直接决定了你后面查询和可视化的体验。我把生产环境验证过的字段结构列出来你可以直接参考。class Span: trace_id: str # 32位十六进制整条链唯一 span_id: str # 16位十六进制当前节点唯一 parent_span_id: str # 父节点ID根Span为None name: str # Span名称如agent.retrieval agent_id: str # 产生该Span的Agent标识 start_time: int # 纳秒时间戳 end_time: int # 纳秒时间戳 status: str # OK / ERROR / UNSET attributes: dict # 自定义属性如token数、模型名 events: list # 时间点事件如开始调用工具 input_ref: str # 输入内容的引用不直接存大文本 output_ref: str # 输出内容的引用这里有两个设计决策值得展开说。第一个是input_ref和output_ref用引用而不是直接存内容。原因是LLM的输入输出动辄几千token直接塞进Span会让存储爆炸。我的做法是把大文本存到对象存储或专门的日志系统Span里只存一个引用ID。查询时按需拉取。这样Span本身很轻量可以长期保留大文本可以按生命周期策略清理。第二个是events字段。有些操作不是有明确起止时间的比如Agent决定切换到备用工具这种决策点。用events记录这些时间点事件比硬塞一个Span更自然。events里每条记录包含时间戳和事件描述。3.2 Agent调用链的埋点位置埋点位置选错了记录出来的执行链就是残缺的。我把Multi-Agent系统里必须埋点的位置梳理成一张清单你对照自己的系统检查。任务入口创建根Span生成TraceID记录原始任务描述。Agent调度决策点每次调度器决定把任务分给哪个Agent时创建一个Span记录决策依据比如路由规则、Agent负载。Agent执行开始/结束每个Agent的run方法入口和出口创建Span记录输入输出。LLM调用每次调用大模型创建Span记录模型名、prompt摘要、response摘要、token消耗、耗时。工具调用每次调用外部工具或函数创建Span记录工具名、参数、返回结果摘要。Agent间消息传递每次Agent A向Agent B发消息创建一个Span记录消息类型和大小。共享状态读写对共享内存的每次读写操作创建轻量Span记录操作类型和key。异常捕获点任何异常被捕获的地方在对应Span上记录error事件和堆栈。实操心得埋点不要贪多。我一开始恨不得每个函数都埋结果Span数量爆炸查询时噪音太大。后来精简到上面这八类覆盖了95%的排查需求。埋点的原则是记录跨越边界的操作而不是记录每个内部函数调用。3.3 上下文传播的实现细节上下文传播是Multi-Agent Tracing里最容易出bug的地方我单独拎出来讲。核心问题是怎么让TraceID和当前SpanID在Agent之间自动传递而不需要每个调用点手动传参。Python环境下我推荐用contextvars。它比threading.local更适合异步场景因为contextvars在asyncio的协程切换时能正确隔离和传递。import contextvars _current_span contextvars.ContextVar(current_span, defaultNone) def get_current_span(): return _current_span.get() def set_current_span(span): return _current_span.set(span) class traced: 装饰器自动为被装饰函数创建Span并管理上下文 def __init__(self, name): self.name name def __call__(self, func): functools.wraps(func) def wrapper(*args, **kwargs): parent get_current_span() span Span( trace_idparent.trace_id if parent else generate_trace_id(), span_idgenerate_span_id(), parent_span_idparent.span_id if parent else None, nameself.name, start_timetime.time_ns(), ) token set_current_span(span) try: result func(*args, **kwargs) span.status OK return result except Exception as e: span.status ERROR span.events.append({name: exception, detail: str(e)}) raise finally: span.end_time time.time_ns() export_span(span) _current_span.reset(token) return wrapper这段代码的关键点在于token set_current_span(span)和_current_span.reset(token)的配对使用。reset必须放在finally里否则异常情况下上下文会泄漏导致后续的Span挂错父节点。对于跨进程的场景比如Agent跑在不同的服务里contextvars就不管用了。这时候必须在通信协议层显式传递。我的做法是定义一个统一的TraceContext结构序列化后放在消息头里class TraceContext: trace_id: str parent_span_id: str sampled: bool def to_header(self) - dict: return { x-trace-id: self.trace_id, x-parent-span-id: self.parent_span_id, x-sampled: str(self.sampled), } classmethod def from_header(cls, header: dict): return cls( trace_idheader.get(x-trace-id), parent_span_idheader.get(x-parent-span-id), sampledheader.get(x-sampled) True, )发送方在发消息前调用to_header()塞进消息头接收方在消费消息时调用from_header()还原上下文然后用它作为父Span创建新的Span。这样跨进程的调用链就串起来了。3.4 存储选型从开发到生产的演进路径Tracing数据的存储选型我的建议是分阶段演进不要一上来就上重型方案。开发阶段直接输出到本地文件或控制台就够了。格式用JSON Lines每行一个Span方便grep和肉眼查看。这个阶段追求的是快速验证埋点是否正确。小规模生产用ClickHouse或者Elasticsearch。ClickHouse的优势是写入吞吐高、聚合查询快适合Span这种写多读少、需要按TraceID聚合的数据。Elasticsearch的优势是全文检索强适合你需要按内容搜索Span的场景。我个人的偏好是ClickHouse因为Tracing查询大多是按TraceID拉取整条链这种结构化查询ClickHouse更合适。大规模生产考虑专业的Tracing后端比如Jaeger、Tempo、SkyWalking这类。它们对Span模型有原生支持自带调用链可视化和依赖分析。代价是运维复杂度上升需要评估团队是否有精力维护。阶段存储方案数据量级核心诉求开发本地文件/控制台每天万级Span快速验证埋点小规模生产ClickHouse/ES每天百万级Span结构化查询聚合大规模生产Jaeger/Tempo每天亿级Span可视化依赖分析长期存储避坑提醒不管用哪种存储TraceID一定要建索引。我见过有团队忘了给TraceID建索引查询一条链要全表扫描慢得没法用。另外Span的写入建议走异步批量不要同步写否则会拖慢Agent本身的执行。4. 实操过程与核心环节实现4.1 从零搭建一个最小可用的Tracing模块光讲原理不够我带你从零搭一个能跑的最小版本。这个版本不依赖任何第三方Tracing库纯手写目的是让你彻底理解每个环节。理解了之后你再用OpenTelemetry这类成熟方案就知道每个配置项在干什么。第一步定义核心数据结构。前面已经给过Span的字段这里补充SpanContext和Tracer两个类import time import uuid import functools import contextvars from dataclasses import dataclass, field from typing import Optional, List, Dict dataclass class Span: trace_id: str span_id: str parent_span_id: Optional[str] name: str agent_id: str start_time: int 0 end_time: int 0 status: str UNSET attributes: Dict field(default_factorydict) events: List[Dict] field(default_factorylist) def gen_id(n_bytes: int) - str: return uuid.uuid4().hex[:n_bytes * 2] class Tracer: def __init__(self, exporter): self.exporter exporter self._current contextvars.ContextVar(span, defaultNone) def start_span(self, name: str, agent_id: str ) - Span: parent self._current.get() span Span( trace_idparent.trace_id if parent else gen_id(16), span_idgen_id(8), parent_span_idparent.span_id if parent else None, namename, agent_idagent_id, start_timetime.time_ns(), ) return span def span_context(self, name: str, agent_id: str ): return _SpanContext(self, name, agent_id) class _SpanContext: def __init__(self, tracer, name, agent_id): self.tracer tracer self.name name self.agent_id agent_id def __enter__(self): span self.tracer.start_span(self.name, self.agent_id) self.token self.tracer._current.set(span) self.span span return span def __exit__(self, exc_type, exc_val, exc_tb): self.span.end_time time.time_ns() if exc_type: self.span.status ERROR self.span.events.append({ name: exception, type: exc_type.__name__, message: str(exc_val), }) else: self.span.status OK self.tracer.exporter(self.span) self.tracer._current.reset(self.token) return False第二步实现一个简单的导出器。开发阶段直接打到控制台import json def console_exporter(span: Span): record { trace_id: span.trace_id, span_id: span.span_id, parent_span_id: span.parent_span_id, name: span.name, agent_id: span.agent_id, duration_ms: (span.end_time - span.start_time) / 1e6, status: span.status, attributes: span.attributes, events: span.events, } print(json.dumps(record, ensure_asciiFalse))第三步在Agent里埋点。假设你有一个检索Agent和一个计算Agenttracer Tracer(exporterconsole_exporter) class RetrievalAgent: def run(self, query: str): with tracer.span_context(agent.retrieval, agent_idretrieval-1) as span: span.attributes[query] query # 模拟检索 docs self._search(query) span.attributes[doc_count] len(docs) return docs def _search(self, query): with tracer.span_context(tool.vector_search) as span: span.attributes[top_k] 5 # 实际检索逻辑 return [doc1, doc2] class ComputeAgent: def run(self, docs: list): with tracer.span_context(agent.compute, agent_idcompute-1) as span: span.attributes[input_docs] len(docs) with tracer.span_context(llm.call) as llm_span: llm_span.attributes[model] gpt-4 llm_span.attributes[prompt_tokens] 500 # 模拟LLM调用 result computed result llm_span.attributes[completion_tokens] 100 return result跑一遍你就会看到控制台输出了一串JSON每个JSON是一个Span通过trace_id和parent_span_id能还原出完整的调用树。这就是最小可用版本。4.2 把执行链还原成可视化调用树有了Span数据下一步是把它还原成人类能看懂的调用树。核心算法很简单按trace_id分组然后根据parent_span_id构建树。def build_tree(spans: List[Span]) - dict: by_id {s.span_id: s for s in spans} children {} root None for s in spans: if s.parent_span_id is None: root s else: children.setdefault(s.parent_span_id, []).append(s) return root, children def render_tree(span, children, indent0): duration (span.end_time - span.start_time) / 1e6 prefix * indent line f{prefix}[{span.name}] {duration:.1f}ms status{span.status} if span.agent_id: line f agent{span.agent_id} print(line) for child in sorted(children.get(span.span_id, []), keylambda x: x.start_time): render_tree(child, children, indent 1)输出大概长这样[task.root] 1520.3ms statusOK [agent.planning] 320.5ms statusOK agentplanner-1 [agent.retrieval] 180.2ms statusOK agentretrieval-1 [tool.vector_search] 45.1ms statusOK [agent.compute] 890.4ms statusOK agentcompute-1 [llm.call] 850.2ms statusOK [agent.summary] 280.1ms statusOK agentsummarizer-1一眼就能看出哪个环节慢、哪个环节出错、调用关系是什么。这就是Tracing的价值。4.3 关键参数计算耗时归因与瓶颈定位光有调用树还不够你还需要能快速定位瓶颈。我常用的两个计算是自身耗时和耗时占比。自身耗时的计算方式是self_time span.duration - sum(child.duration)。这个值反映的是这个Span自己干活花了多久不包括等待子Span的时间。很多时候你会发现某个Agent的duration很长但self_time很短说明它大部分时间在等下游瓶颈不在它身上。def compute_self_time(span, children): child_total sum( (c.end_time - c.start_time) for c in children.get(span.span_id, []) ) return (span.end_time - span.start_time) - child_total耗时占比则是span.duration / root.duration用来判断哪个环节是主要耗时来源。我一般会按耗时占比排序取前20%的Span重点看。实操技巧并行调用的耗时归因要特别小心。如果规划Agent并行调用了检索和计算两个Agent这两个子Span的时间区间是重叠的。这时候父Span的self_time计算不能简单减去子Span耗时之和否则会算出负数。正确做法是计算子Span时间区间的并集再用父Span耗时减去并集长度。4.4 与OpenTelemetry生态对接手搓版本理解原理后生产环境我建议直接上OpenTelemetry。它提供了标准的API和SDK生态成熟能直接对接Jaeger、Tempo这些后端。对接的核心是把前面手写的Span替换成OTel的Span上下文传播用OTel的propagator。Python环境下大致是这样from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter provider TracerProvider() processor BatchSpanProcessor(OTLPSpanExporter(endpointhttp://localhost:4317)) provider.add_span_processor(processor) trace.set_tracer_provider(provider) tracer trace.get_tracer(multi-agent) def agent_run(agent_id, task): with tracer.start_as_current_span(fagent.{agent_id}) as span: span.set_attribute(agent.id, agent_id) span.set_attribute(task.type, task.type) result do_work(task) span.set_attribute(result.size, len(result)) return resultOTel的好处是跨语言、跨进程的上下文传播都帮你处理好了你只需要在跨进程边界调用inject和extract。而且它的BatchSpanProcessor会自动做批量异步导出不会阻塞主流程。5. 常见问题与排查技巧实录5.1 执行链断裂TraceID丢失的五大场景执行链断裂是Multi-Agent Tracing最常见的问题表现就是本该连成一棵树的Span变成了好几棵独立的树。我把踩过的坑整理成一张速查表。断裂场景根本原因排查方法解决方案异步任务创建contextvars未传递到新任务检查create_task处是否有context拷贝用copy_context()显式拷贝线程池调用线程间上下文不共享检查是否用了ThreadPoolExecutor手动传递TraceContext消息队列消费消息头未携带TraceID检查消息生产端是否注入header生产端注入消费端提取跨服务HTTP调用未使用标准传播头抓包看请求头接入OTel propagator异常中断异常导致Span未正常结束检查是否有未闭合的Span用with语句确保finally执行我重点说下异步任务这个坑。Python的asyncio里asyncio.create_task默认会拷贝当前context但如果你在创建任务之前已经切换过context拷贝的可能不是你想要的那个。稳妥的做法是显式拷贝import asyncio import contextvars async def spawn_agent(coro): ctx contextvars.copy_context() return await asyncio.create_task(coro, contextctx)5.2 Span数量爆炸如何控制噪音Span数量爆炸是另一个高频问题。我见过一个任务产生上万条Span的极端案例查询时根本没法看。控制噪音的核心思路是分级记录。把Span分成三个级别关键SpanAgent调用、LLM调用、工具调用全量记录辅助Span状态读写、内部函数只在采样命中时记录调试Span细粒度内部步骤只在开发环境记录。class SpanLevel: CRITICAL 1 AUXILIARY 2 DEBUG 3 def should_record(level, sampled): if level SpanLevel.CRITICAL: return True if level SpanLevel.AUXILIARY: return sampled return ENV development另外对于循环调用场景比如Agent反复调用同一个工具可以设置一个阈值超过N次后只记录第一次和最后一次中间的合并成一条汇总Span。5.3 性能开销Tracing本身不能成为瓶颈Tracing本身是有开销的如果实现不当可能拖慢整个系统。我实测过几个关键开销点Span创建单次约1-2微秒可以忽略。属性写入单次约0.5微秒但如果你写大对象会慢很多。导出这是大头。同步导出单次可能几毫秒到几十毫秒必须异步批量。我的优化经验是Span创建和属性写入同步做导出异步批量做。用队列缓冲后台线程或协程定期批量导出。这样对主流程的影响能控制在1%以内。import queue import threading class AsyncExporter: def __init__(self, backend, batch_size100, flush_interval5): self.queue queue.Queue(maxsize10000) self.backend backend self.batch_size batch_size self.flush_interval flush_interval self._start_worker() def _start_worker(self): def worker(): batch [] while True: try: span self.queue.get(timeoutself.flush_interval) batch.append(span) if len(batch) self.batch_size: self.backend.write_batch(batch) batch [] except queue.Empty: if batch: self.backend.write_batch(batch) batch [] threading.Thread(targetworker, daemonTrue).start() def export(self, span): try: self.queue.put_nowait(span) except queue.Full: # 队列满了就丢弃保护主流程 pass注意队列满了要丢弃而不是阻塞这是保护主流程的关键。Tracing数据丢一点没关系主流程卡住才是大事故。5.4 排查实录一次上下文污染的定位过程最后分享一个真实的排查案例。现象是汇总Agent偶尔会读到不属于当前任务的中间结果。因为是多任务并发怀疑是上下文串了。排查步骤是这样的先按TraceID把出问题的任务的所有Span拉出来发现汇总Agent读到的那个中间结果其来源Span的trace_id是另一个任务的。这就定位到了——某个共享状态被跨任务污染了。进一步看那个来源Span的parent_span_id发现它的父Span属于另一个任务。说明在写入共享状态时上下文被错误地切换了。最后查到根因是一个全局单例的缓存对象多个任务共用写入时没做任务隔离。解决方案是给缓存key加上trace_id前缀实现任务级隔离。改完之后问题消失。这个案例的教训是Multi-Agent系统里任何全局共享的可变状态都是潜在的污染源。Tracing不仅能帮你定位问题还能帮你发现架构上的隐患。当你发现Span的父子关系出现跨Trace的异常连接时基本就是共享状态出问题了。5.5 常见问题速查表把上面这些整理成一张速查表方便你遇到问题时快速对照。问题现象可能原因快速验证解决方向调用链断裂成多棵树TraceID未传播检查各Span的trace_id是否一致补全上下文传播Span父子关系错乱并发下上下文串了检查是否用了全局变量存当前Span改用contextvars查询慢TraceID未建索引explain查询计划建索引冷热分离存储增长过快全量记录大文本统计Span平均大小大文本用引用加采样主流程变慢同步导出阻塞压测对比开关Tracing的耗时改异步批量导出部分Span丢失队列满被丢弃监控队列丢弃计数扩容队列或降采样6. 我在Multi-Agent Tracing上的一些个人体会做了一段时间的Multi-Agent Tracing有几个体会是文档里不会写、但实际很重要的。第一Tracing要趁早做不要等出问题才补。我见过太多团队是先上线、出问题、再补Tracing结果补的时候发现代码里到处是隐式的上下文传递改造成本极高。正确的做法是在设计Agent通信协议的时候就把TraceContext作为协议的一部分定下来。第二可视化比数据本身更重要。你存了一堆Span如果只能靠写SQL查询那价值大打折扣。一个能展开收起的调用树视图能让排查效率提升十倍。我建议哪怕后端用最简单的方案前端也要做一个调用树展示。第三Tracing数据是优化Multi-Agent系统的金矿。通过分析Span数据你能发现哪些Agent调用是冗余的、哪些环节耗时最长、哪些工具调用失败率最高。这些洞察直接指导你的架构优化。我现在会定期跑一个分析脚本统计各Agent的平均耗时、失败率、调用频次作为优化的输入。第四不要追求完美先跑起来。我一开始想设计一个完美的Tracing体系结果纠结了两周还没落地。后来简化到最小可用版本先跑起来再根据实际需求迭代。事实证明这个策略是对的很多需求是跑起来之后才发现的。最后分享一个小技巧给每个Span加一个agent_id属性然后在可视化时按agent_id着色。这样你一眼就能看出任务在哪些Agent之间流转哪个Agent是瓶颈。这个小小的着色让我们的排查效率又提升了一截。

相关新闻

从搜索语法到自动化监控:用Google和GitHub Hacking打造资产哨兵

从搜索语法到自动化监控:用Google和GitHub Hacking打造资产哨兵

经常有甲方客户只丢一个主域名过来,要求在最短时间内摸清资产暴露面。很多人的第一反应是上扫描器、跑子域探测,结果往往跑了一晚上,出来一堆“疑似资产”还得人工核对。我自己的习惯是,先别急着上工具,把搜索引擎和代…

2026/10/18 10:24:42 阅读更多 →
STM32无板仿真入门:纯软件跑通GPIO、串口、定时器与中断

STM32无板仿真入门:纯软件跑通GPIO、串口、定时器与中断

很多人在 STM32 入门这件事上,被一只无形的门槛拦住了:板子。倒不是说开发板有多贵,而是从“想学”到“真正跑起来第一个程序”之间,隔了选型、下单、等快递、装驱动、接线、怕烧板子这一长串事情。尤其是学生党或者业余爱好者&am…

2026/10/18 10:24:42 阅读更多 →
JSP+SQL Server社区居民信息管理系统部署指南:JDK1.8到Tomcat全流程

JSP+SQL Server社区居民信息管理系统部署指南:JDK1.8到Tomcat全流程

简介:这是一套面向毕业设计的社区居民信息管理系统完整源码与文档工具包,适用于需要完成Java Web课程设计或毕业设计的本专科学生。系统采用JSP页面配合SQL Server数据库,基于JDK1.8开发,可在eclipse、myeclipse、sts、idea等常见…

2026/10/18 10:24:42 阅读更多 →

最新新闻

DDIA核心思想:数据系统是一套精密妥协机制

DDIA核心思想:数据系统是一套精密妥协机制

简介:本资源是《设计数据密集型应用程序》(DDIA)中文译本的完整 GitBook 风格学习包,面向分布式系统工程师、数据库开发者、后端架构师及进阶全栈工程师,聚焦数据系统设计核心能力提升——从底层存储、事务一致性到流处…

2026/10/18 11:11:00 阅读更多 →
基于SpringBoot的农用车4S店管理系统实战解析

基于SpringBoot的农用车4S店管理系统实战解析

每年到了毕业季,计算机专业的同学就开始为选题头疼。电商、商城、博客、会议室预约,这些老面孔估计不少人都看厌了。我今天想认真聊聊一个比较冷门但很实用的切入点:基于springboot的农用车4S店管理系统。这套题目如果你正在犹豫选什么&#…

2026/10/18 11:11:00 阅读更多 →
DDIA实战:用四层抽象重构订单履约系统

DDIA实战:用四层抽象重构订单履约系统

简介:本资源是《设计数据密集型应用程序》(DDIA)中文译本的完整 GitBook 风格学习包,面向分布式系统工程师、数据库开发者、后端架构师及进阶全栈工程师,聚焦数据系统设计核心能力提升——从底层存储、事务一致性到流处…

2026/10/18 11:11:00 阅读更多 →
Delphi ODAC 安装配置全攻略:从环境准备到连接测试

Delphi ODAC 安装配置全攻略:从环境准备到连接测试

简介:本资源为Delphi与CBuilder开发者准备的ODAC(Oracle Data Access Components)Oracle连接控件组件库,面向需要在Windows桌面应用中高效访问Oracle数据库的中高级开发者。ODAC由Devart开发,核心组件TOracleConnectio…

2026/10/18 11:11:00 阅读更多 →
MySQL 8.0 免安装版部署指南:从解压到远程连接全流程

MySQL 8.0 免安装版部署指南:从解压到远程连接全流程

简介:这是一份面向后端开发、运维及数据库学习者的 MySQL 8.0 免安装绿色版资源,主打解压即用,无需繁琐安装流程,适合快速搭建本地测试环境或临时部署数据库服务。压缩包共 420 个文件,以 sdi、dll、pdb、exe、xml、sy…

2026/10/18 11:11:00 阅读更多 →
rea技术路线全解析:React、REA会计模型与实时分析实践指南

rea技术路线全解析:React、REA会计模型与实时分析实践指南

1. 从一个字母组合说起:为什么"rea"值得单独拿出来聊第一次看到"rea"这个标题,我脑子里蹦出来的第一反应是——这大概率是个缩写,而且是个被严重低估的缩写。在技术圈里,三个字母的组合往往承载着远超字面长度…

2026/10/18 11:09:58 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →