追踪思维的指纹:基于 LangSmith 的嵌套追踪实战
What(本文讲什么)
对 agent 来说,tracing 不是“更漂亮的日志”,而是“可复盘的因果链”。它把一次任务拆成可嵌套的执行树:每次模型调用、每次工具调用、每次重试与超时、每一次幂等提交点,都在 trace 里有位置。
这篇文章用 LangSmith 作为具体落地例子,同时用 OpenTelemetry 的概念把抽象钉死:
- trace/span/attributes/events 的语义分别是什么。
- agent 的 span 树应该怎么分层,才能定位“哪个步骤把系统带偏了”。
- 必须记录哪些字段,才能把事故从“猜测”变成“证据链”(审计、观测)。
Problem(要解决的工程问题)
agent 的失败模式往往跨越多层:
- 模型误读: 把工具输出看错,拿错路径,导致后续写错文件。
- 工具异常: 超时、权限拒绝、返回截断,触发重试风暴(超时、重试)。
- 副作用重复: 没有幂等 key,重试导致重复提交(幂等)。
- 上下文污染: 某次读文件返回太大,导致后续步骤注意力退化(降级)。
如果你只有“控制台长日志”,你很难回答三个问题:
- 出错发生在第几步?
- 当时模型看到了什么输入?
- 当时系统是否触发了超时/重试/幂等/权限门禁?
tracing 的目标就是让这三个问题可回答。
Principle(tracing 的语义:span 是因果片段,不是日志行)
OpenTelemetry 把 observability 的基本对象讲得很清楚:
- trace: 一次请求/任务的端到端因果链。
- span: trace 内的一个操作片段(有开始/结束时间),可嵌套。
- attributes: 结构化键值字段,用于聚合与过滤。
- events: span 内的离散事件(例如“重试一次”“权限拒绝一次”)。
这套语义的价值是:你不再靠“读文本”定位问题,而是靠“聚合字段 + 时间线 + 父子关系”定位问题。
Usage(怎么用:给 agent 画出正确的 span 树)
1) 一棵可用的 span 树应该长什么样
建议至少分四层:
task(root span): 一次任务的生命周期。plan(child span): 一次推理/规划步骤。tool(child span): 一次工具调用(包括超时/重试信息)。commit(child span): 一次副作用提交(WAL 记录点,幂等 key 生成点)。
关键点是把“副作用”与“推理”分开:commit 是证据链的锚点。
2) 最小字段规范(建议强制)
不管你用 LangSmith、Langfuse 还是 OTel,字段应该尽量一致,便于跨系统聚合:
- 关联字段:
task_idtrace_idspan_id
- 工具字段:
tool_nametool_timeout_ms(超时)tool_attempt/retry_reason(重试)
- 副作用字段:
idempotency_key(幂等)resource_targets(写入资源集合)commit_id/wal_id(审计)
- 预算字段:
input_tokens/output_tokenscache_read_tokens(如可用)
- 安全字段:
permission_scoperedaction_applied(是否脱敏)
这套字段的目的不是“漂亮”,而是让你能回答:
- 哪里最容易超时?
- 哪里最容易重试?
- 是否出现幂等冲突?
- 是否有越权访问尝试?
3) LangSmith 的最小接入(示例)
下面只是示意:你要做的是“把关键函数包上 trace”,并且在工具层和提交层补齐 attributes。
import os
from langsmith import traceable
def _env(name: str) -> str:
v = os.environ.get(name)
if not v:
raise RuntimeError(f"missing env: {name}")
return v
class AgentEngine:
"""
Agent 引擎:
演示如何把 planning/tool/commit 这三层变成可追踪的 span。
"""
@traceable(run_type="chain", name="task")
async def run_task(self, task: dict):
return await self._plan_and_execute(task)
@traceable(run_type="chain", name="plan")
async def _plan_and_execute(self, task: dict):
plan = await self._make_plan(task)
for step in plan:
await self._invoke_tool(step)
@traceable(run_type="tool", name="tool")
async def _invoke_tool(self, tool_call: dict):
# 在这里补齐:tool_name / timeout / attempt / retry_reason
# 并把工具输出做截断与脱敏,避免把敏感信息直接进入 trace
pass
@traceable(run_type="tool", name="commit")
async def _commit(self, change: dict):
# 在这里生成 idempotency_key,并写 WAL,再做真实提交
pass
4) 隐私与泄露风险:tracing 本身是数据面
最容易被忽略的一点是:trace 里常常会存 prompt、工具输出、文件片段。
因此你必须把 tracing 当成“敏感数据系统”来设计:
- 脱敏: API key、token、邮箱、连接串必须在写入前 redaction(审计)。
- 权限: 谁能看 trace?按项目/环境隔离(权限、隔离)。
- 保留: 生产 trace 的保留期与删除策略(合规风险),并配合日志轮转(资源释放)。
Design(设计取舍:为什么 tracing 不等于 logging)
- log: 适合记录细节文本,适合人读。
- trace: 适合表达因果树,适合系统聚合与回放。
你需要两者,但不要混用:
- 关键路径用 trace(span 树 + attributes)。
- 细节补充用 log(结构化日志 + 关联 trace_id)。
Pitfall(常见坑与防错)
- 只追踪模型,不追踪工具: 最终你仍然不知道副作用为什么发生。
- 不记录超时/重试: 线上最常见的事故来源之一就是重试风暴(超时、重试)。
- 不记录幂等 key: 你无法区分“重试”与“重复提交”(幂等)。
- trace 里写敏感信息: 观测系统变成泄露源(权限、审计)。
- tracing 开销不可控: 采样与字段裁剪要可配置(降级)。
Debug(用 tracing 定位一次真实事故)
排查顺序建议:
- 从 root span 找到失败的 tool span。
- 看 tool span 的 timeout/attempt/retry_reason。
- 若涉及写入,定位 commit span,检查 idempotency_key 与 wal_id。
- 对比同类任务的 span 分布,找出退化点(观测)。
指标与告警(让 tracing 变成运行时防线)
把 tracing 接入后,不要只当“调试 UI”。你应该立刻产出可告警指标:
- 超时率(按 tool_name 聚合)(超时)。
- 重试次数分布(按 retry_reason 聚合)(重试)。
- 幂等冲突次数(按 idempotency_key 聚合)(幂等)。
- 权限拒绝次数(按 permission_scope 聚合)(权限)。
- P95 延迟(按 span 类型聚合:plan/tool/commit)。
这些指标的意义是提前发现“系统正在走向失控”,而不是等到事故后再打开 trace。
采样与降级(tracing 也会拖垮系统)
tracing 本身有成本。你需要两层开关:
- 采样:只采集一部分任务的全量 trace,其他任务只采集关键 span(例如 commit)。
- 字段裁剪:对 prompt/工具输出做截断与脱敏,避免把大文本打进观测系统。
当系统压力升高时,必须允许降级:
- 只保留审计关键字段(commit span + wal_id)。
- 暂停记录大 payload(例如文件全文)。
这也是“观测系统必须可观测”的一部分(降级、资源释放)。
一个最小“commit span”清单(建议硬编码)
commit span 是证据链锚点,你可以把它当作强制字段集合:
commit_id/wal_ididempotency_keyresource_targetsresult/error_codeapproved_by(如果有人工批准链)
即使你采样降级,也必须保留这些字段,否则事后无法追责与回滚(审计、回滚)。
Source(资料来源)
- LangSmith tracing: https://docs.smith.langchain.com/observability/how_to_guides/tracing
- OpenTelemetry concepts: https://opentelemetry.io/docs/concepts/observability-primer/
- OpenTelemetry trace spec: https://opentelemetry.io/docs/specs/otel/trace/
- Langfuse docs: https://langfuse.com/docs