埋点接入全链路
埋点接入全链路
日期: 2026-07-08 阶段: Phase 1 - 产品定义×评测×可观测底座 标签: #instrumentation #trace-tree #trace-eval-correlation #static-export
核心问题
Day 22 定了属性语义、Day 23 定了属性映射层,今天回答「埋点接到哪些环节、span 怎么组织成树、trace 和 eval 怎么互查」。AML Copilot 一次案件调查不是一次 LLM 调用——它是 orchestrator 拆解任务 → 检索证据 → 调类型学工具 → 生成 SAR 草稿的一条链。要回答「这条 eval 失败的案件,到底卡在检索还是生成?花了多少钱?」,必须把全链路每个环节都 span 化,并让 trace 与 eval 用同一执行 ID 关联。
但本项目有个诚实的现实约束:output:export 静态导出、无后端 collector。所以今天还要回答:在没有真实 OTLP 后端的前提下,telemetry 落地的现实方案是什么,哪些是计划态,不能谎称已接生产 Datadog。
关键内容
A. 全链路 span 树:workflow > agent > (chat + tool + retrieval)
OTel GenAI agent-spans 规范(2026-03,Development 状态)定义了 agent 场景的 span 层级,操作名是标准化的:
| 操作名(gen_ai.operation.name) | span 名约定 | span kind | 对应本项目环节 |
|---|---|---|---|
invoke_workflow | invoke_workflow {workflow.name} | INTERNAL | 一次案件调查的顶层编排 |
invoke_agent | invoke_agent {agent.name} | INTERNAL(同进程)/ CLIENT(远程) | orchestrator / 子 agent |
chat | chat {request.model} | CLIENT | 每次 LLM 调用 |
execute_tool | execute_tool {tool.name} | INTERNAL | 类型学评分、检索工具 |
规范有个微妙规定:只有当仪表化能可靠区分 workflow 与 agent 时才报 invoke_workflow,否则统一报 invoke_agent。这是为了避免框架在「单 agent 多步」和「多 agent 编排」之间误标层级——层级标错会让成本归因算到错误的父节点。
本项目一次案件调查的 span 树:
invoke_workflow "aml-case-investigation" (INTERNAL, trace_id=T1)
│ attrs: case.id, eval.execution_id=E1
├── invoke_agent "orchestrator" (INTERNAL, span=S1, parent=root)
│ ├── chat "gpt-4o" (CLIENT, span=S2, parent=S1) ← 任务拆解
│ │ attrs: input_tokens=420, output_tokens=88, finish_reasons=["stop"]
│ ├── execute_tool "retrieve_evidence" (INTERNAL, span=S3, parent=S1)
│ │ attrs: tool.call.id=tc1, retrieved_k=8, recall_hit=true
│ └── execute_tool "assess_typology" (INTERNAL, span=S4, parent=S1)
│ attrs: tool.call.id=tc2, top_typology="structuring"
└── invoke_agent "sar-drafter" (INTERNAL, span=S5, parent=root)
└── chat "gpt-4o" (CLIENT, span=S6, parent=S5) ← 生成草稿
attrs: input_tokens=1320, output_tokens=540, finish_reasons=["length"] ⚠ 被截断
span 层级表(父子关系靠 trace_id + parent_span_id 串联,OTel context propagation):
| span | parent | kind | 关键观测点 |
|---|---|---|---|
| S1 orchestrator | root | INTERNAL | 编排总耗时 |
| S2 chat 拆解 | S1 | CLIENT | 拆解 token 成本 |
| S3 retrieve | S1 | INTERNAL | 检索是否命中金标(接 eval) |
| S4 assess | S1 | INTERNAL | 类型学判定正确性 |
| S5 sar-drafter | root | INTERNAL | 生成阶段耗时 |
| S6 chat 草稿 | S5 | CLIENT | finish_reasons=["length"] → 草稿被截断告警 |
这棵树一眼能回答「卡在哪」:S6 的 finish_reasons=["length"] 说明 SAR 草稿被 max_tokens 截断,证据链不完整——这正是 Day 3 PRD「不丢证据」指标的可观测对应,定位到具体 span 而非笼统的「质量差」。
B. trace ↔ eval 以执行 ID 关联:可观测与评测互查
eval 和 trace 是两个分离的系统:eval 跑在 CI(src/aml/evalBaseline.ts 对 66 案金标算 recall),trace 记录运行时执行。要让它们互查,关键是用同一个执行 ID(execution_id)作为关联键,双向贯穿。
关联机制(伪代码):
每次 eval 运行一个案件:
execution_id = uuid()
把 execution_id 注入到本次调查的根 span 属性:
rootSpan.setAttribute('eval.execution_id', execution_id)
eval 结果记录:{ execution_id, case_id, predicted, label, recall_hit }
互查方向 1(eval → trace):
eval 报告里某案 recall=0(检索没命中金标)
→ 按 execution_id 查 trace
→ 看 S3 retrieve span 的 retrieved_k 与命中明细,定位是检索召回不足还是 rerank 排错
互查方向 2(trace → eval):
trace 里某 span finish_reasons=["length"](草稿截断)
→ 按 execution_id 反查 eval
→ 看该案 SAR judge 分是否同时下降,验证「截断 → 质量降」假设
这正是 Day 23 把 eval.execution_id 作为自有稳定字段、经 attributeMap 写入 span 的原因——关联键属于「核心模型词汇」,绝不能因 semconv 改名而断链。
关联键放在哪一层 span 也有讲究:放在**根 span(invoke_workflow)**而非每个子 span 上。OTel 的 context propagation 让子 span 自动继承根 span 的 trace_id,但属性默认不向下继承;要让任意子 span(如 S3 retrieve)都能被 execution_id 反查,靠的是 trace_id ↔ execution_id 在根 span 上的一次绑定,再用 trace_id 把整棵树拉出来。换句话说,execution_id 标在根、trace_id 串全树,二者在根 span 上建立一次映射即可覆盖全链路——不需要在每个子 span 重复写 execution_id,那既冗余又增加基数。这也意味着 eval 报告只需记 {execution_id, trace_id} 一对,就能从指标直跳到完整执行现场。
反直觉洞察(trace 与 eval 各自为政是最常见的可观测反模式):很多团队 eval 有 eval 的报表、tracing 有 tracing 的仪表盘,两套数据用不同 ID、永远对不上。结果是 eval 告诉你「recall 掉了」,但你无法跳到具体那次执行看「为什么掉」——只能重跑复现,慢且未必复现。一个共享的 execution_id 把「指标退化」和「现场证据」焊死在一起,是 eval 可调试性的命门。本项目从第一天就让 evalBaseline 产出携带 execution_id。
C. 静态导出(无后端)下 telemetry 的现实约束与诚实方案
本项目是 Next.js output: export 静态站点,部署在 Cloudflare Pages,没有服务端、没有 OTLP collector。这意味着标准 OTel「SDK → OTLP exporter → collector → 后端存储」的链路后半段在生产环境根本不存在。诚实地区分什么能做、什么是计划态:
| 能力 | 静态导出现实 | 方案 |
|---|---|---|
| 在浏览器内生成 span 树 | ✅ 可做 | 前端 trace store(src/agent/trace/useTraceStore.ts)记录 span,产出 A 节树 |
| 前端可视化 trace 树 | ✅ 可做 | 复用 dsdb-lab 的交互式 lab 模式,浏览器内渲染 span 时间线 |
| 确定性回放 | ✅ 可做 | 用 dsdb-lab 的确定性 PRNG 思路,让 demo trace 可复现 |
| 导出到真实 OTLP collector | ❌ 计划态 | 需自托管 collector(Day 25 Langfuse),非静态站能力 |
| 接生产 Datadog/Langfuse | ❌ 计划态 | 需后端,作品集里标注「设计就绪、未接生产」 |
| 真实 token 成本聚合 | ⚠ 部分 | 前端可估算;真实计费需 W7 gateway 实测 |
诚实方案:本项目的 telemetry 是「前端可视化 + 确定性回放」的教学/演示形态,对标 src/dsdb-lab/(浏览器内交互式教学 lab,确定性 PRNG),而非生产监控管道。attributeMap.ts(Day 23)让这套前端 telemetry 已经按 semconv 标准属性组织,所以「接真实 collector」是一次纯增量适配(换 exporter),核心数据结构不变——这就是把它写成「设计就绪」而非「已上生产」的底气,且不夸大。
反直觉洞察(埋点的成本不在采集,在存储与查询):直觉以为埋点贵在「到处加 span 代码」,其实加 span 几乎免费。真正的账单在后端:一条 RAG 链路(检索+LLM+后处理)产生的 telemetry 是等价传统 API 调用的 10-50 倍(OneUptime 2026-04),把 AI 监控加进现有 Datadog 会让账单涨 40-200%。罪魁是高基数(high-cardinality)属性——把
case_id、request_id、prompt 正文、tool 参数 blob 当 span 属性,每个唯一值都炸开存储。所以省钱的杠杆不是少埋点,是控基数 + 尾采样:错误/慢/截断的 trace 全留,成功快的 trace 只留 1-5%(tail-based sampling)。对本项目,静态导出阶段没有这个账单,但作品集叙事必须答出「上生产后怎么控成本」——这恰是金融场景 PM 的成本意识体现。
设计要点/决策表
| 要点 | 说明 | 与朴素做法差异 |
|---|---|---|
| 标准操作名分层 | workflow/agent/chat/execute_tool | 朴素埋点用自定义名,后端无法聚合 |
| 父子靠 trace+parent_span_id | OTel context propagation | 手工串 ID 易断链 |
| execution_id 贯穿 eval+trace | 共享关联键双向互查 | 两套系统各自 ID,永远对不上 |
| finish_reasons 进 span | length→截断告警定位到 span | 笼统「质量差」无法定位 |
| 静态导出诚实标注 | 前端可视化是真,生产管道是计划 | 谎称已接 Datadog=作品集硬伤 |
| 控基数 + 尾采样 | prompt/blob 不当属性,错误全留成功抽样 | 全采高基数→账单炸 40-200% |
对本项目的落地
- src/agent/trace/useTraceStore.ts 扩展为 span 树存储:记录 A 节的 workflow>agent>(chat+tool) 层级,每 span 携带经 attributeMap.ts(Day 23)翻译的 semconv 属性 + 自有
eval.execution_id。 - evalBaseline 产出携带 execution_id:src/aml/evalBaseline.ts 的
BaselineEval结果在每案级别附execution_id,使 B 节「eval→trace」互查可行;recall=0 的案件可一键跳到对应 retrieve span。 - 检索环节 span 化接现有 eval 框架:
execute_tool "retrieve_evidence"span 的recall_hit属性直接复用 src/agent/eval/retrievalGolden.ts + runRetrievalEval.ts 的 recall@k 判定结果——已知其金标是结构匹配(slug)非语义,故 span 上诚实标注match_type="structural",不夸大为语义召回。 - 前端可视化复用 dsdb-lab 模式:trace 树渲染对标 src/dsdb-lab/ 的交互式 lab(确定性 PRNG,浏览器内),定位为教学/演示形态,README 明确「telemetry 为前端可视化,OTLP collector 为计划态」。
- 成本控制设计先行:即便静态阶段无账单,作品集文档写明上生产后的基数管控(prompt/tool-arg 不入属性、case_id 用低基数维度替代)+ 尾采样策略(错误/截断 100% 留、成功 5%),呼应 C 节金融成本意识。
- 长文#1《从 recall@k 到生产级 evals》:把本节 B(trace↔eval 互查)作为「为什么 eval 不能脱离 trace」的核心论据。
参考资料
- OpenTelemetry — Semantic Conventions for GenAI agent and framework spans(invoke_workflow/invoke_agent/execute_tool 操作名、span kind、嵌套、Development 状态)官方规范 (2026-03)
- Greptime — How OpenTelemetry Traces LLM Calls, Agent Reasoning, and MCP Tools(trace 树 invoke_agent>chat+execute_tool 实例、context propagation)(2026-05-09)
- OneUptime — Your AI Workloads Are About to Blow Up Your Observability Bill(RAG telemetry 10-50x、账单涨 40-200%、cardinality traps、tail-based sampling)(2026-04-01)
- Chanl Blog — What to Trace When Your AI Agent Hits Production(四类 span:LLM/tool/memory/orchestration)(2026)
- Uptrace — OpenTelemetry for AI Systems: LLM and Agent Observability(六层约定、多 agent 一请求数百 span)(2026)
- maketocreate — OpenTelemetry GenAI: Tracing AI Agents Without Leaking PII(agent trace 不泄 PII 的埋点法)(2026)
SOTA 检查 (2026-06-11)
- agent-spans 约定仍是 SOTA 且仍 experimental:invoke_workflow/invoke_agent/execute_tool 是 2026 标准操作名,LangChain/CrewAI/Bedrock Agents 均向其对齐;但与 Day 22/23 一致,仍 Development 状态,操作名/属性名有改名风险,故本项目埋点经 attributeMap.ts 隔离。
- 成本数据为 2026 最新口径:10-50x telemetry 放大与 40-200% 账单涨幅引自 2026-04 一手工程博客,反映 AI 可观测成本危机是当年热点,非陈旧估计。
- 是否仍是 SOTA:✅ 「全链路 span 树 + execution_id 关联 eval + 尾采样控成本」是 2026 生产 agent 可观测的事实最佳实践;LangChain、Arize Phoenix、Langfuse 的 trace 模型均同构。
- 静态导出约束如实保留:本项目无后端是真实约束,telemetry 定位为前端可视化/确定性回放(对标 dsdb-lab),生产 collector 为计划态——此诚实标注本身是作品集可信度的一部分,不因「显得更完整」而夸大。
- 过时认知警示:2024 年「给每个 LLM 调用单独埋一个扁平 span」的做法已被「分层 span 树(workflow>agent>step)」取代——扁平 span 无法做成本归因与卡点定位;引用旧博客的扁平埋点示例时须升级为分层模型。
- 待跟踪:W7 LLM gateway 上线后,用真实 token 计费回填 C 节成本表的「真实 token 成本聚合」行;agent-spans 若转 stable,核对操作名是否变更。
- WebSearch 验证关键词: "OpenTelemetry agent spans invoke_workflow 2026", "LLM observability cost cardinality tail sampling 2026"