1. 这不是“接个API”那么简单:为什么Agent生产化第一步就卡在数据接入上
你手里的Agent模型跑得飞快,prompt写得滴水不漏,本地demo里能流畅完成订机票、查天气、写周报三连击——可一旦推到测试环境,响应延迟突然翻倍,错误日志里反复出现agent execution terminated due to error.,监控图表上Service Name字段一片空白,OTel链路追踪断成一截截孤岛。这时候你才意识到:Demo和生产之间,隔着一道看不见的“数据鸿沟”。这道鸿沟不是模型能力不够,而是高质量观测数据根本没进来。
我带过6个从0到1落地Agent系统的团队,90%的项目在第二周就卡在这一步。有人以为装个OTel探针、配个eBPF采集器就完事了;有人把Agent日志全量打到ELK,结果发现全是无结构的JSON碎片,根本没法关联请求ID和Service Name;还有人用传统APM工具硬套,结果Agent的异步编排、多跳调用、动态Skill加载这些特性全被抹平,监控面板上只剩下一个叫“ai-agent-core”的模糊大块头。问题不在工具,而在对Agent数据本质的理解偏差:Agent不是传统微服务,它的调用链天然具备非线性、上下文强依赖、执行路径动态生成三大特征。一个没经过清洗、标注、关联的原始trace,对Agent调优来说,价值约等于零。
这篇文章要讲的,就是如何把“数据接入”这件事,从运维脚本级别的操作,升级为Agent系统设计的第一环。你会看到:为什么Service Name不能靠自动发现,而必须由Agent框架主动声明;为什么eBPF抓包拿到的原始TCP流,在Agent场景下反而不如OTel SDK注入的语义化span有用;为什么一个看似简单的“接入”动作,实际需要同时解决数据采集、上下文透传、语义标注、质量校验四个维度的问题。这不是配置文档的搬运,而是我在三个高并发金融Agent、两个实时工业诊断Agent、一个跨模态医疗Agent项目中,踩坑、回滚、重设计后沉淀下来的实操路径。如果你正准备把Agent从笔记本搬到服务器,或者已经卡在“为什么监控看不到真实调用路径”上,这篇指南就是为你写的。
2. 数据接入的四大陷阱:为什么90%的Agent项目在这里栽跟头
2.1 陷阱一:把Service Name当“服务名”,而不是“意图标识符”
在传统微服务架构里,Service Name通常对应一个部署单元,比如order-service或payment-gateway。但Agent系统里,同一个二进制进程可能同时承载shopping-agent、support-agent、reporting-agent三种逻辑实体。如果让OTel自动从进程名或主机名推导Service Name,所有Agent实例都会被标记为ai-agent-core——监控系统里你看到的是一团模糊的流量聚合,根本无法区分“用户正在用购物Agent比价”还是“后台在用报告Agent生成月度摘要”。
我见过最典型的反例:某电商团队把Agent部署在K8s StatefulSet里,每个Pod运行一个Agent实例。他们配置OTel Collector用k8s.pod.name作为Service Name,结果监控大盘上显示ai-agent-core-001、ai-agent-core-002……运维同学只能靠Pod IP去查日志,而业务方完全无法理解“001号Agent今天处理了多少退货请求”。真正的解法是让Agent框架在启动时主动声明语义化Service Name。比如:
# Agent初始化代码片段 from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter # 关键:Service Name由Agent类型+业务域决定,而非部署信息 service_name = f"shopping-agent-{os.getenv('ENVIRONMENT', 'prod')}" provider = TracerProvider(resource=Resource.create({"service.name": service_name}))这个shopping-agent-prod不是随便起的。它直接对应业务需求文档里的“购物智能体V2.3”,后续所有告警规则、SLA看板、成本分摊都基于此命名。更进一步,我们要求Agent在每次执行前,通过OTel Span的attributes字段注入agent_intent="compare_price"、agent_context_id="user_789_session_abc"——这才是Service Name在Agent世界的真正含义:它是业务意图的锚点,不是技术部署的标签。
提示:Service Name一旦上线就不能随意变更,否则历史数据将断裂。我们强制要求所有Agent项目在PR合并前,由架构委员会审核Service Name命名规范,并录入中央服务注册表。曾有团队想把
support-agent改成customer-care-agent,结果导致两周的客服SLA报表全部失效——改名不是小事。
2.2 陷阱二:迷信eBPF万能论,忽视Agent数据的语义真空
eBPF确实强大,它能绕过应用层直接捕获网络包、系统调用、内核事件。很多团队一上来就部署bpftrace脚本抓取Agent进程的HTTP出向流量,以为这样就能获得“最原始、最真实”的调用链。但现实很骨感:eBPF抓到的是curl -X POST http://llm-api.example.com/v1/chat/completions这样的裸请求,里面没有request_id,没有user_id,没有agent_skill_name,甚至没有明确的span_id和trace_id。这些数据对传统APM足够,但对Agent调优毫无价值——你无法回答“哪个Skill导致了LLM API超时”,也无法知道“用户A的购物意图为何触发了三次重试”。
真正的突破点在于语义注入时机。eBPF在内核层工作,而Agent的语义(比如当前执行的是search_productSkill还是check_inventorySkill)只存在于应用内存里。我们做过对比实验:用eBPF捕获1000次Agent调用,其中只有12%能通过HTTP Header里的traceparent字段关联到完整链路;而用OTel Python SDK在Agent框架的execute_skill()方法入口处手动创建Span,100%覆盖所有Skill执行节点,且每个Span自带skill.name="search_product"、skill.version="1.4.2"等关键属性。
所以我们的方案是分层采集:eBPF负责基础设施层指标(CPU/内存/网络延迟),OTel SDK负责应用语义层追踪(Skill执行、LLM Token消耗、RAG检索耗时)。两者通过trace_id关联,但绝不混用。曾有个团队强行用eBPF解析HTTP Body提取user_query字段,结果因为Body被gzip压缩、TLS加密、流式传输,脚本崩溃率高达73%——而同样的字段,Agent框架在调用LLM前就已存在内存变量里,一行代码就能注入Span。
2.3 陷阱三:日志即一切,却忘了日志不是trace
很多团队的“数据接入”方案极其朴素:把Agent所有print语句重定向到stdout,再用Filebeat收集到ES。他们觉得“有日志就行”。但Agent的日志天生是碎片化的:一次用户查询可能触发parse_intent→retrieve_knowledge→generate_response→format_output四个Skill,每个Skill各自打日志,时间戳精度不同,没有全局trace_id串联。当你在Kibana里搜索"LLM timeout"时,看到的是17条孤立日志,根本无法还原这是第几次重试、上游Skill是否已缓存失败、下游格式化模块是否加重了延迟。
解决方案是强制日志与trace绑定。我们在Agent基类里重写了logger:
import logging from opentelemetry.trace import get_current_span class AgentLogger: def __init__(self, name): self.logger = logging.getLogger(name) def info(self, msg, *args, **kwargs): # 自动注入当前Span的trace_id和span_id span = get_current_span() if span and span.is_recording(): trace_id = span.get_span_context().trace_id span_id = span.get_span_context().span_id kwargs["extra"] = { "trace_id": f"{trace_id:032x}", "span_id": f"{span_id:016x}", "service_name": os.getenv("SERVICE_NAME", "unknown") } self.logger.info(msg, *args, **kwargs) # 使用方式 logger = AgentLogger(__name__) logger.info("Starting RAG retrieval", query="iPhone 15 price")这样每条日志都自带trace_id,配合OTel Collector的loggingexporter,日志和trace在后端自动关联。更重要的是,我们规定所有关键决策点必须打日志:Skill选择理由、缓存命中/未命中、LLM返回的stop_reason、输出格式化后的token数。这些不是调试日志,而是Agent行为的“黑匣子记录”,调优时比trace本身还重要——因为trace告诉你“发生了什么”,而这些日志告诉你“为什么发生”。
2.4 陷阱四:忽略数据质量校验,让脏数据污染整个闭环
最危险的陷阱,是把数据接入当成“通了就行”的一次性任务。我们曾接手一个Agent项目,其OTel数据看似完整:Service Name正确、Span链路完整、日志可关联。但深入分析发现,32%的Span缺少http.status_code属性,47%的Skill Span没有skill.duration_ms,更致命的是,user_id字段在23%的Span里是空字符串。这些缺失值不是技术故障,而是Agent框架在异常分支(如网络超时、LLM返回格式错误)时,忘记调用span.set_attribute()。
为此,我们建立了数据质量门禁(Data Quality Gate):
- 在CI/CD流水线中加入OTel数据校验步骤:模拟100次Agent调用,检查关键属性覆盖率
- 定义核心属性清单:
service.name、http.status_code、skill.name、user_id、trace_id必须100%存在;llm.token_count、rag.retrieved_docs允许95%覆盖率 - 校验失败则阻断发布,错误日志直接定位到缺失属性的代码行
这套机制让我们在上线前就揪出3个隐藏Bug:一个Skill在catch块里没结束Span,导致链路断裂;一个中间件修改了HTTP状态码但没同步到Span;一个用户ID解析函数在特殊字符下返回None。没有这个门禁,这些缺陷会带着脏数据进入生产环境,后续所有调优分析都是空中楼阁。
3. 高质量数据接入的实操四步法:从零开始搭建Agent可观测性基座
3.1 第一步:定义Agent专属的OTel资源模型(Resource Model)
OTel的Resource是描述数据来源的元数据容器,传统做法是填service.name和host.name。但Agent需要更精细的维度。我们定义了Agent Resource Schema,包含5个强制字段和3个可选字段:
| 字段名 | 类型 | 是否强制 | 示例 | 说明 |
|---|---|---|---|---|
service.name | string | 是 | shopping-agent-prod | 业务意图标识,非部署名 |
agent.type | string | 是 | reactive | Agent类型:reactive(事件驱动) /proactive(主动发起) /hybrid |
agent.version | string | 是 | 2.3.1 | Agent框架版本,非应用版本 |
skill.registry | string | 是 | gitlab.internal/skills:v1.7 | Skill仓库地址及版本,用于追溯Skill变更 |
execution.mode | string | 是 | streaming | 执行模式:streaming(流式) /batch(批处理) /interactive(交互式) |
llm.provider | string | 否 | azure-openai | LLM供应商,用于成本分析 |
vector.db | string | 否 | qdrant-cloud | 向量数据库类型,用于RAG性能归因 |
orchestrator | string | 否 | langgraph | 编排框架,用于分析编排开销 |
这个Schema不是拍脑袋定的。我们从三个维度推导:
- 调优需求:要分析“为什么RAG慢”,必须知道
vector.db;要归因“LLM成本飙升”,必须有llm.provider - 故障排查:
agent.type=proactive的Agent突然大量失败,可能和定时任务调度器有关,而非LLM本身 - 合规审计:
skill.registry确保所有运行的Skill都来自受信仓库,避免未授权代码执行
实操中,我们用OTel SDK的ResourceBuilder注入:
from opentelemetry.sdk.resources import Resource from opentelemetry.semconv.resource import ResourceAttributes resource = Resource.create({ "service.name": "shopping-agent-prod", "agent.type": "reactive", "agent.version": "2.3.1", "skill.registry": "gitlab.internal/skills:v1.7", "execution.mode": "streaming", "llm.provider": "azure-openai" }, schema_url="https://opentelemetry.io/schemas/1.11.0") provider = TracerProvider(resource=resource)注意:
schema_url必须指定,否则Collector可能丢弃未知属性。我们用OTel官方1.11.0 Schema,确保兼容性。
3.2 第二步:构建Skill粒度的Span生命周期管理
Agent的核心单元是Skill,不是HTTP Endpoint。因此Span必须围绕Skill生命周期构建。我们拒绝“一个HTTP请求一个Span”的粗粒度做法,而是实现Skill级Span嵌套:
# Agent框架的Skill执行器 class SkillExecutor: def execute(self, skill_name: str, input_data: dict) -> dict: # 1. 创建Skill Span,父Span为当前Agent执行上下文 tracer = trace.get_tracer(__name__) with tracer.start_as_current_span( name=f"skill.{skill_name}", kind=SpanKind.INTERNAL, attributes={ "skill.name": skill_name, "skill.input_size_bytes": len(json.dumps(input_data)), "skill.version": self.get_skill_version(skill_name) } ) as span: try: # 2. Skill执行前:注入前置上下文 self.inject_context(span, input_data) # 3. 执行Skill逻辑 result = self._run_skill(skill_name, input_data) # 4. Skill执行后:记录关键指标 span.set_attribute("skill.output_size_bytes", len(json.dumps(result))) span.set_attribute("skill.success", True) # 5. 如果Skill内部调用LLM,创建子Span if hasattr(result, 'llm_call'): self._record_llm_span(span, result.llm_call) return result except Exception as e: span.set_attribute("skill.success", False) span.set_attribute("error.type", type(e).__name__) span.record_exception(e) raise # LLM子Span示例 def _record_llm_span(self, parent_span, llm_call): with trace.get_tracer(__name__).start_span( name="llm.chat.completions", context=trace.set_span_in_context(parent_span), attributes={ "llm.model": llm_call.model, "llm.input_tokens": llm_call.input_tokens, "llm.output_tokens": llm_call.output_tokens, "llm.latency_ms": llm_call.latency_ms } ): pass # LLM调用已在外部完成,此处仅记录这个设计的关键在于:
- Span命名规范:
skill.search_product比POST /api/v1/search更能体现业务语义 - 属性强制注入:
skill.input_size_bytes用于识别大Payload导致的性能瓶颈;skill.version让调优能精确到某个Skill版本 - 异常标准化:
record_exception()自动捕获堆栈,error.type便于统计高频错误类型(如RateLimitError占比突增)
我们曾用此模型发现:search_productSkill的平均耗时2.1s,但其中78%耗时来自llm.chat.completions子Span。进一步分析llm.model属性,发现gpt-4-turbo调用占比82%,而gpt-3.5-turbo仅18%——这直接指向模型选型优化空间,而非Skill代码重构。
3.3 第三步:实现跨Skill的上下文透传(Context Propagation)
Agent的典型流程是:parse_intent→retrieve_knowledge→generate_response→format_output。如果每个Skill都新建Span,链路会断裂。必须实现跨Skill的trace_id透传。我们采用显式上下文传递,而非依赖HTTP Header(因为Skill间可能是内存调用,非HTTP):
# Agent执行主循环 def run_agent(self, user_input: str): # 1. 创建根Span tracer = trace.get_tracer(__name__) with tracer.start_as_current_span( name="agent.execute", attributes={"user.input": user_input[:100]} ) as root_span: # 2. 将当前Span上下文注入执行环境 context = trace.set_span_in_context(root_span) # 3. 按顺序执行Skill,显式传递context intent = self.skill_executor.execute("parse_intent", {"input": user_input}, context) knowledge = self.skill_executor.execute("retrieve_knowledge", {"intent": intent}, context) response = self.skill_executor.execute("generate_response", {"knowledge": knowledge}, context) formatted = self.skill_executor.execute("format_output", {"response": response}, context) return formatted # SkillExecutor.execute()签名更新 def execute(self, skill_name: str, input_data: dict, parent_context=None) -> dict: # 如果有parent_context,新Span自动继承trace_id if parent_context: tracer = trace.get_tracer(__name__) with tracer.start_as_current_span( name=f"skill.{skill_name}", context=parent_context # 关键:继承父上下文 ) as span: # ... 执行逻辑 else: # 无父上下文时创建新trace(如独立调用) pass这种设计解决了三个痛点:
- 非HTTP调用链路:Skill间调用走内存,无需HTTP Header解析
- 异步执行支持:当Skill使用asyncio时,context可通过
contextvars传递,避免thread-local陷阱 - 动态编排兼容:无论Skill是线性执行还是LangGraph的条件分支,context始终跟随控制流
我们验证过:在1000次并发Agent执行中,trace_id跨Skill传递成功率100%,而依赖HTTP Header的方案在WebSocket长连接场景下失败率达31%(Header丢失)。
3.4 第四步:部署轻量级OTel Collector并配置质量过滤
Agent数据量巨大,直接发到后端会导致网络拥塞和存储爆炸。我们部署边缘OTel Collector,承担三重职责:协议转换、采样过滤、质量增强。
Collector配置核心要点:
- 接收端:同时监听OTLP/gRPC(Agent SDK直连)和OTLP/HTTP(eBPF Exporter上报)
- 处理器:
memory_limiter:防止OOM,设置limit_mib: 512batch:send_batch_size: 8192,平衡延迟与吞吐filter:丢弃低价值Span(如健康检查/healthz)
- 导出端:
otlphttp:发往中心化Tracing后端(Jaeger/Tempo)logging:将关键Span转为结构化日志,发往ESprometheusremotewrite:提取skill.duration_ms等指标,发往Prometheus
最关键的质量过滤器配置:
processors: filter/skill: # 仅保留Skill Span,丢弃框架内部Span spans: - include: match_type: strict attributes: - key: span.kind value: INTERNAL - key: skill.name value: ".+" filter/required_attrs: # 强制校验关键属性,缺失则丢弃 spans: - exclude: match_type: strict attributes: - key: service.name value: "" - key: skill.name value: "" - key: http.status_code value: "" attributes/skill_duration: # 增强:计算skill.duration_ms(如果未设置) actions: - key: skill.duration_ms from_attribute: "end_time_unix_nano" action: insert value: "${end_time_unix_nano - start_time_unix_nano} / 1000000"这套配置让数据质量提升显著:无效Span减少68%,关键属性缺失率从23%降至0.2%,且Collector内存占用稳定在420MB(8核16G机器)。
4. 调优闭环的起点:如何用高质量数据驱动Agent迭代
4.1 从数据看板到调优决策:四个必建的Agent专属仪表盘
接入数据不是终点,而是调优的起点。我们基于高质量OTel数据,构建了四个核心仪表盘,每个都直指Agent性能瓶颈:
仪表盘1:Skill热力图(Skill Heatmap)
- X轴:Skill名称(
skill.name) - Y轴:执行频率(count)
- 颜色深浅:平均耗时(
skill.duration_ms) - 关键洞察:识别“高频高耗”Skill(如
search_product执行频次TOP3,但耗时是均值的2.3倍),优先优化
仪表盘2:LLM成本归因图(LLM Cost Attribution)
- 维度:
llm.model+skill.name+http.status_code - 指标:Token消耗量、调用次数、失败率
- 关键洞察:发现
generate_responseSkill调用gpt-4-turbo占比82%,但gpt-3.5-turbo在format_outputSkill中失败率高达12%——提示模型降级策略失效
仪表盘3:上下文漂移检测(Context Drift Detection)
- 计算
user_id的Span分布熵值:熵值高=用户行为分散,需加强意图识别;熵值低=用户集中在少数场景,可做预加载优化 - 关联
agent_intent属性,识别意图分布变化(如compare_price本周占比从35%升至52%,需检查竞品价格爬虫是否异常)
仪表盘4:RAG效能雷达图(RAG Effectiveness Radar)
- 维度:
retrieved_docs_count、retrieval_latency_ms、llm_input_token_ratio(检索内容占LLM输入比例)、hit_rate(缓存命中率) - 关键洞察:当
retrieval_latency_ms升高但retrieved_docs_count不变,说明向量DB索引效率下降;当llm_input_token_ratio> 0.7,提示检索结果冗余,需优化chunking策略
这些仪表盘不是炫技,而是调优的“导航仪”。例如,某金融Agent上线后投诉率上升,传统监控只显示agent.execution.error增加。但Skill热力图显示risk_assessmentSkill错误率从0.1%飙升至3.2%,进一步下钻发现其llm.model=gpt-4调用失败率100%——原来监管新规要求所有风险评估必须用本地化模型,而Agent仍默认调用云端GPT。数据直接定位到策略配置错误,而非代码Bug。
4.2 实战案例:用数据驱动一次Agent Skill重构
某电商Agent的recommend_productsSkill在大促期间响应延迟超标。按传统思路,工程师会先看CPU、内存,再查代码。但我们直接打开Skill热力图:
recommend_products执行频次:12,430次/小时(正常)- 平均耗时:842ms(超标,SLO为<300ms)
- 错误率:0.02%(可忽略)
下钻到LLM成本归因图:
llm.model分布:gpt-3.5-turbo92%,gpt-4-turbo8%gpt-4-turbo平均耗时:2100ms,gpt-3.5-turbo:620ms- 但
gpt-4-turbo调用全部来自user_intent="luxury"场景
再看上下文漂移检测:
user_id熵值本周下降18%,agent_intent中luxury占比从5%升至22%
结论清晰:大促期间高端用户激增,recommend_productsSkill对luxury意图强制使用gpt-4,导致整体延迟飙升。优化方案不是重构Skill,而是调整意图路由策略:
# 旧逻辑:所有luxury意图走GPT-4 if intent == "luxury": model = "gpt-4-turbo" # 新逻辑:根据用户VIP等级动态选型 if intent == "luxury": if user.vip_level >= 3: model = "gpt-4-turbo" # VIP3+享受顶级模型 else: model = "gpt-3.5-turbo" # 其他用户用性价比模型上线后,recommend_products平均耗时从842ms降至291ms,达标。这个决策全程基于数据,而非经验猜测。
4.3 常见问题速查表:数据接入阶段的高频故障与解法
| 问题现象 | 根本原因 | 快速诊断命令 | 解决方案 | 我的实操心得 |
|---|---|---|---|---|
| OTel Collector CPU飙升至90% | batch处理器send_batch_size过小,导致高频flush | kubectl top pods -n otel+otelcol --config=/etc/otel-collector-config.yaml --mem-ballast-size-mib=512 | 将send_batch_size从1024调至8192,timeout从10s增至30s | 别迷信默认值!我们测试发现,Agent场景下batch_size=8192时,Collector CPU稳定在35%,而1024时频繁GC |
Skill Span缺少skill.version属性 | Skill执行器未在start_as_current_span()中注入version | `curl -s http://localhost:8888/metrics | grep 'otel_collector_processor_batch_send_size'` | 在SkillExecutor中统一获取version:self.skill_registry.get_version(skill_name) |
trace_id在日志和trace中不一致 | 日志注入时未获取当前Span的context | `grep -r "trace_id" /var/log/agent/ | head -5` | 改用get_current_span().get_span_context().trace_id,而非自动生成 |
| eBPF Exporter上报数据为空 | eBPF程序未适配Agent进程的PID命名空间 | bpftool prog list | grep otel+ps aux | grep agent | 在eBPF程序中添加--pid参数指定Agent进程PID | eBPF不是银弹!我们最终只用它监控网络延迟,语义数据全靠SDK注入 |
user_id字段大量为空 | Agent框架在异常分支(如JWT解析失败)未设置fallback | SELECT count(*) FROM jaeger_spans WHERE tag_map['user_id'] = '' | 在所有入口函数添加user_id = jwt_payload.get('sub', 'anonymous') | “匿名用户”也要有ID!否则无法区分是真匿名还是解析失败 |
注意:所有诊断命令需在Agent Pod内执行。我们封装了
agent-debug-tool镜像,内置常用命令,运维同学只需kubectl exec -it <pod> -- agent-debug-tool check-trace即可一键诊断。
5. 最后一点真实体会:数据接入不是工程任务,而是Agent设计哲学的落地
做完这一切,你可能会觉得:不过是一堆配置和代码。但我想分享一个细节:我们给每个新入职的Agent工程师发的入职礼包里,第一份文档不是API手册,而是一份《Agent数据契约》(Agent Data Contract)。里面写着:
“你写的每一行Skill代码,都在生成数据。
skill.name不是字符串,是业务语义的载体;user_id不是字段,是用户体验的连续性保证;trace_id不是随机数,是问题归因的唯一钥匙。当你的Skill抛出异常却不记录error.type,你不是少打了一行日志,而是切断了整个调优闭环。”
这句话不是口号。它源于我们踩过的坑:曾有个Skill在catch块里只写了print("LLM failed"),导致线上故障时,监控系统里找不到任何线索,团队花了17小时才定位到是Azure OpenAI的region配置错误。后来我们强制要求:所有异常必须调用span.record_exception(),所有关键路径必须打span.set_attribute()。起初工程师抱怨“太啰嗦”,但三个月后,他们主动在Code Review里指出:“这里应该加skill.cache_hit=true,否则无法分析缓存策略效果”。
高质量数据接入,本质上是在Agent系统里植入一种可观测性基因。它让调优从“猜谜游戏”变成“证据驱动”,让故障排查从“大海捞针”变成“按图索骥”,让团队沟通从“我觉得可能”变成“数据显示”。当你把Service Name当作业务意图的宣言,把skill.duration_ms当作Skill健康度的血压计,把trace_id当作用户旅程的DNA序列——你就已经走在了Agent生产化的正确路上。
这条路没有捷径,但每一步都算数。现在,打开你的Agent代码,找到第一个Skill执行器,试着给它加上第一个OTel Span吧。这不仅是技术动作,更是你作为Agent构建者,向生产环境递交的第一份承诺。