一次会话一个trace_id,串起Agent全链路日志

Agent 出问题最折磨人的不是没日志,是日志全在、但凑不到一块儿 。模型调用一条日志、工具调用一条、RAG 检索一条,各躺各的表,用户报"刚才那次答错了",你连"刚才那次"具体走了哪几步都拼不出来。解法很朴素:进来就发一个 trace_id,全链路每条日志都带上它,排错从翻半天缩到一次查询。

核心就一件事:trace_id 全程透传

会话(或单次请求)入口生成一个 trace_id,之后模型调用、每次工具调用、RAG 检索、降级兜底------全部带着它打日志。查问题时拿 trace_id 一捞,整条链路的每一步、每个耗时、每个出参,按时间顺序齐了。

复制代码
import uuid, contextvars, time, logging

trace_ctx = contextvars.ContextVar("trace_id", default=None)

def new_trace():
    tid = uuid.uuid4().hex[:16]
    trace_ctx.set(tid)
    return tid

def log_step(step, **kw):
    logging.info(json.dumps({
        "trace_id": trace_ctx.get(),
        "step": step,
        "ts": round(time.time(), 3),
        **kw
    }, ensure_ascii=False))

# 入口
def handle(user_msg, user_id):
    tid = new_trace()
    log_step("request_in", user_id=user_id, msg=user_msg)
    docs = rag_search(user_msg);     log_step("rag", hit=len(docs))
    resp = llm_call(user_msg, docs); log_step("llm", latency_ms=resp.ms, tokens=resp.tokens)
    if resp.tool_calls:
        for tc in resp.tool_calls:
            r = run_tool(tc);        log_step("tool", name=tc.name, ok=r.ok, latency_ms=r.ms)
    log_step("request_out", trace_id=tid)
    return resp

contextvars 而不是到处手传 trace_id,是因为异步、多层调用里手传太容易漏,contextvars 能在协程上下文里自动带着走。

我趟出来的几个点

  1. trace_id 要回给前端/用户。 响应头或结果里带上它。用户报错时让他把那串 id 给你,比"大概下午三点那次"精确一万倍。我现在客服后台直接显示 trace_id,截图一发我秒定位。

  2. 每步都记耗时。 光记发生了啥不够,记 latency_ms。线上慢,一查 trace 就知道是 RAG 慢还是模型慢还是某个工具卡住,不用瞎猜。

  3. 多 Agent 接力,trace_id 必须跨 Agent 传。 主 Agent 调子 Agent,把 trace_id 透传过去,子 Agent 的日志才能挂到同一条链路上。跨进程就放进调用的 header 或消息体里。

  4. 结构化输出,别打成大段文本。 JSON 一行一条,方便日志系统按 trace_id 聚合、按字段过滤。纯文本日志想聚合得自己写正则,难受。

一个真实收益

上了 trace 之后,有次用户投诉"回答里报价是错的"。以前得翻一堆日志连蒙带猜,这次拿 trace_id 一查:RAG 命中的是一份过期文档,模型老老实实照着答了。问题根本不在模型,在知识库该下架的没下架。要没有全链路串起来,我大概率会先去冤枉模型半天。

说点不好的

trace 日志是有成本的------每步都打、还结构化,量大了存储和写入压力都在。我的做法是分级:正常请求只打关键节点(入口、模型、工具、出口),出错或命中采样的请求才打详细 trace(带完整出入参)。全量详细打,存储账单能吓你一跳。另外 contextvars 在某些线程池场景下上下文不会自动继承,得手动 copy,这个边界要注意,不然 trace_id 会丢。

模型推理我用讯飞星辰 MaaS 现成 API,每次调用的耗时、token 用量它接口里都给,我直接抠出来塞进 trace 日志,全链路的模型这环不用自己埋点估算。

你们 Agent 的链路追踪是怎么搭的?有没有把 trace_id 暴露给用户?评论区交流。