【智能体开发】《LangChain核心技术与LLM项目实践》_87.[第9章 回调机制] 链路追踪实现:记录完整执行路径

在这里插入图片描述

链路追踪:让LangChain的"黑盒"变"白盒",告别"代码跑完却不知道发生了啥"的绝望时刻

全文总结:本文将手把手教你用LangChain的Callback机制实现完整的链路追踪,从基础概念到实战代码,让你彻底掌握如何记录和可视化LLM应用的完整执行路径,排查问题不再抓瞎。

链路追踪核心原理

CallbackHandler体系

同步与异步追踪

结构化日志记录

可视化追踪方案

生产环境最佳实践

目录:

  1. 链路追踪核心原理
  2. CallbackHandler体系
  3. 同步与异步追踪
  4. 结构化日志记录
  5. 可视化追踪方案
  6. 生产环境最佳实践

嗨,大家好呀,我是你的老朋友精通代码大仙。接下来我们一起学习 《LangChain核心技术与LLM项目实践》,震撼你的学习轨迹!学习+:caogenzhishi 推荐朋友UP:个人快速成长/精进学习


“代码能跑就行,别问为什么”——这句话是不是你的日常写照?

我见过太多同学,LangChain代码跑起来了,输出也对,但中间到底经历了什么?Token用了多少?哪个环节最耗时?LLM到底被调用了几次?一问三不知。等到线上出问题,只能对着屏幕干瞪眼,重启大法都不好使。这种"黑盒"状态,在LLM应用里简直是灾难。今天咱们就把这个盒子拆开,装个透亮的玻璃罩。


一、链路追踪核心原理

点题

链路追踪的本质,是在LangChain的执行流程中埋点,记录每个关键节点的输入、输出、耗时和元数据。LangChain的执行是个链式结构,从用户输入到最终输出,中间可能经过多个组件:Prompt模板、LLM调用、输出解析、工具调用…

用户输入

Chain开始

Prompt构建

LLM调用

输出解析

工具调用?

最终输出

痛点分析

新手最常犯的错误?完全不追踪,或者只打印最终结果

# 错误示范:裸奔代码
from langchain import OpenAI, LLMChain, PromptTemplate

llm = OpenAI(temperature=0)
prompt = PromptTemplate(
    input_variables=["product"],
    template="给{product}写个广告语"
)
chain = LLMChain(llm=llm, prompt=prompt)

result = chain.run("智能手表")  # 就这一行,里面发生了什么?鬼知道
print(result)

跑完你看到一段广告语,但Prompt最终长啥样?Token用了多少?有没有重试?耗时多久?统统不知道。线上一旦超时或报错,你连问题在哪都定位不了。

解决方案

理解Callback的生命周期钩子。LangChain在关键节点会触发回调事件:

事件阶段触发时机能拿到什么
on_chain_startChain开始执行输入数据、配置参数
on_llm_startLLM调用前完整Prompt、模型参数
on_llm_endLLM返回后完整响应、Token用量
on_chain_endChain执行完毕最终输出、总耗时
# 正确姿势:注入Callback
from langchain.callbacks import StdOutCallbackHandler

handler = StdOutCallbackHandler()
chain = LLMChain(
    llm=llm, 
    prompt=prompt,
    callbacks=[handler]  # 关键!
)

这样每个步骤都会打印详细日志,执行路径一目了然。

小结

链路追踪不是可有可无的"锦上添花",而是LLM应用的"基础设施"。没有它,你就是闭着眼睛开车。


二、CallbackHandler体系

点题

LangChain的CallbackHandler是一套插件化的追踪机制。你可以自定义Handler,继承BaseCallbackHandler,实现你关心的钩子方法。

BaseCallbackHandler

+on_chain_start()

+on_chain_end()

+on_llm_start()

+on_llm_end()

+on_tool_start()

+on_tool_end()

+on_text()

+on_retry()

自定义Handler

+记录到数据库()

+发送到监控平台()

+本地文件日志()

痛点分析

很多新手看到BaseCallbackHandler十几个方法就头大,要么全实现(冗余),要么随便挑几个(漏关键信息),要么在Handler里写业务逻辑(耦合)。

# 错误示范:Handler里塞业务逻辑
class BadHandler(BaseCallbackHandler):
    def on_llm_end(self, response, **kwargs):
        # 坑:在这里调用另一个LLM做分析?!
        analysis = another_llm.predict(f"分析这个响应:{response}")
        self.save_to_db(analysis)  # 又慢又容易死循环

Handler应该只做一件事:记录。别让它变成另一个业务逻辑层。

解决方案

按需实现,保持单一职责。一个完整的自定义Handler示例:

from langchain.callbacks.base import BaseCallbackHandler
from datetime import datetime
import json

class ExecutionTracer(BaseCallbackHandler):
    """专注记录执行轨迹的追踪器"""
    
    def __init__(self):
        self.traces = []
        self.current_chain = []
    
    def on_chain_start(self, serialized, inputs, **kwargs):
        trace = {
            "event": "chain_start",
            "timestamp": datetime.now().isoformat(),
            "chain_type": serialized.get("name", "unknown"),
            "inputs": inputs,
            "depth": len(self.current_chain)
        }
        self.traces.append(trace)
        self.current_chain.append(serialized.get("id", "unknown"))
    
    def on_llm_start(self, serialized, prompts, **kwargs):
        trace = {
            "event": "llm_start",
            "timestamp": datetime.now().isoformat(),
            "model": serialized.get("name", "unknown"),
            "prompt_count": len(prompts),
            "prompt_lengths": [len(p) for p in prompts],
            "depth": len(self.current_chain)
        }
        self.traces.append(trace)
    
    def on_llm_end(self, response, **kwargs):
        trace = {
            "event": "llm_end",
            "timestamp": datetime.now().isoformat(),
            "token_usage": response.llm_output.get("token_usage", {}),
            "response_length": len(response.generations[0][0].text),
            "depth": len(self.current_chain)
        }
        self.traces.append(trace)
    
    def on_chain_end(self, outputs, **kwargs):
        self.current_chain.pop()
        trace = {
            "event": "chain_end",
            "timestamp": datetime.now().isoformat(),
            "outputs": outputs,
            "depth": len(self.current_chain)
        }
        self.traces.append(trace)
    
    def get_trace_report(self):
        return {
            "total_events": len(self.traces),
            "execution_path": self.traces
        }

使用方式:

tracer = ExecutionTracer()

chain = LLMChain(
    llm=llm,
    prompt=prompt,
    callbacks=[tracer]
)

result = chain.run("智能手表")
print(json.dumps(tracer.get_trace_report(), indent=2, ensure_ascii=False))

输出结构化的追踪报告,每个事件的深度、时间戳、关键信息一应俱全。

小结

好的Handler像好的监控探头:只记录不干预,视角清晰不遗漏,存储高效不冗余。


三、同步与异步追踪

点题

LangChain支持同步和异步两种执行模式,Callback也要对应处理。AsyncCallbackHandler用于异步场景,避免阻塞事件循环。

异步模式

不阻塞

用户请求

Callback处理

异步IO

回调恢复

处理其他请求

同步模式

用户请求

Callback处理

等待IO

继续执行

痛点分析

新手最容易踩的坑:混用同步和异步Handler,或者在异步代码里用同步的数据库操作。

# 错误示范:异步代码里阻塞
import time

class SlowHandler(BaseCallbackHandler):
    def on_llm_end(self, response, **kwargs):
        # 坑:同步写入数据库,阻塞整个事件循环!
        time.sleep(0.5)  # 模拟数据库写入
        db.insert(response)

在异步Chain里用这个Handler,并发量一上来直接卡死。

解决方案

异步场景用AsyncCallbackHandler,IO操作全部异步化:

from langchain.callbacks.base import AsyncCallbackHandler
import asyncio
import aiohttp

class AsyncTracer(AsyncCallbackHandler):
    """异步追踪器,适合高并发场景"""
    
    def __init__(self):
        self.traces = []
    
    async def on_llm_start(self, serialized, prompts, **kwargs):
        trace = {
            "event": "llm_start",
            "timestamp": datetime.now().isoformat(),
            "prompts": prompts
        }
        self.traces.append(trace)
        # 异步发送到远程日志服务,不阻塞
        await self._send_async(trace)
    
    async def on_llm_end(self, response, **kwargs):
        trace = {
            "event": "llm_end",
            "token_usage": response.llm_output.get("token_usage", {})
        }
        self.traces.append(trace)
        await self._send_async(trace)
    
    async def _send_async(self, trace):
        # 真正的异步IO,不阻塞事件循环
        async with aiohttp.ClientSession() as session:
            await session.post(
                "http://your-log-service/trace",
                json=trace
            )

使用异步Chain:

from langchain.chains import LLMChain

tracer = AsyncTracer()

async def run_async():
    chain = LLMChain(
        llm=llm,
        prompt=prompt,
        callbacks=[tracer]
    )
    return await chain.arun("智能手表")  # 注意是arun

# 并发执行多个追踪
results = await asyncio.gather(
    run_async(),
    run_async(),
    run_async()
)

小结

同步异步要分清,Handler也要"对号入座"。别让追踪本身成为性能瓶颈。


四、结构化日志记录

点题

原始日志是给人看的,结构化日志是给机器分析的。链路追踪的数据最终要能被查询、聚合、可视化。

40% 25% 20% 15% 日志使用场景分布 问题排查 性能分析 成本核算 安全审计

痛点分析

最常见的反模式:打印一堆字符串,没有统一格式

# 错误示范:无法解析的日志
print(f"[INFO] 调用LLM,prompt长度:{len(prompt)}")
print(f"模型返回了:{response[:100]}...")
print(f"用了{tokens}个token,花了{cost}美元")

这种日志想统计平均Token消耗?想筛选特定模型的调用?想按时间聚合?几乎不可能。

解决方案

采用JSON结构化格式,统一Schema:

import json
from dataclasses import dataclass, asdict
from typing import Optional, Dict, Any

@dataclass
class LLMCallRecord:
    trace_id: str           # 全局追踪ID
    span_id: str            # 当前调用ID
    parent_span_id: Optional[str]  # 父调用ID
    timestamp: str
    model: str
    prompt: str
    response: str
    input_tokens: int
    output_tokens: int
    total_tokens: int
    latency_ms: float
    metadata: Dict[str, Any]

class StructuredLogger(BaseCallbackHandler):
    """结构化日志记录器"""
    
    def __init__(self, trace_id: str):
        self.trace_id = trace_id
        self.span_stack = []
        self.records = []
    
    def _new_span_id(self):
        import uuid
        return str(uuid.uuid4())[:8]
    
    def on_llm_start(self, serialized, prompts, **kwargs):
        span_id = self._new_span_id()
        parent_id = self.span_stack[-1] if self.span_stack else None
        self.span_stack.append(span_id)
        
        self._current_start = datetime.now()
        self._current_prompt = prompts[0] if prompts else ""
        self._current_span = {
            "span_id": span_id,
            "parent_span_id": parent_id
        }
    
    def on_llm_end(self, response, **kwargs):
        span_id = self.span_stack.pop()
        latency = (datetime.now() - self._current_start).total_seconds() * 1000
        
        usage = response.llm_output.get("token_usage", {})
        
        record = LLMCallRecord(
            trace_id=self.trace_id,
            span_id=span_id,
            parent_span_id=self._current_span["parent_span_id"],
            timestamp=self._current_start.isoformat(),
            model="gpt-3.5-turbo",  # 从serialized获取
            prompt=self._current_prompt[:500],  # 截断避免过大
            response=response.generations[0][0].text[:500],
            input_tokens=usage.get("prompt_tokens", 0),
            output_tokens=usage.get("completion_tokens", 0),
            total_tokens=usage.get("total_tokens", 0),
            latency_ms=latency,
            metadata={"finish_reason": response.generations[0][0].generation_info.get("finish_reason")}
        )
        
        # 输出结构化JSON,可直接被ELK/Loki采集
        print(json.dumps(asdict(record), ensure_ascii=False))
        self.records.append(record)

输出示例:

{
  "trace_id": "req-20240503-001",
  "span_id": "a3f7b2d9",
  "parent_span_id": "e8c5a1f2",
  "timestamp": "2024-05-03T10:30:15.123456",
  "model": "gpt-3.5-turbo",
  "prompt": "给智能手表写个广告语...",
  "response": "掌控时间,智享生活...",
  "input_tokens": 45,
  "output_tokens": 32,
  "total_tokens": 77,
  "latency_ms": 1250.5,
  "metadata": {"finish_reason": "stop"}
}

这种格式可以直接导入Elasticsearch,用Kibana做分析:按模型聚合Token消耗、按时间看延迟趋势、按trace_id还原完整调用链。

小结

日志结构化,后期分析才能自动化。现在多花10分钟设计Schema,将来省10小时写正则解析。


五、可视化追踪方案

点题

文本日志适合查询,可视化适合理解。把追踪数据变成火焰图、调用树、时序图,问题定位效率翻倍。

原始追踪数据

数据处理

存储

火焰图

调用树

时序图

监控大盘

痛点分析

很多团队记录了追踪数据,但躺在日志文件里吃灰。出了问题还是grep+vim,效率极低。或者自己造轮子做可视化,投入巨大。

解决方案

方案一:轻量级自建(适合中小项目)

用Python生成火焰图数据:

def generate_flamegraph_data(records):
    """生成火焰图所需的折叠格式"""
    stacks = []
    
    for record in sorted(records, key=lambda x: x['timestamp']):
        # 构建调用栈路径
        stack_path = f"langchain;{record.get('chain_type', 'unknown')};{record['model']}"
        duration = record['latency_ms']
        stacks.append(f"{stack_path} {int(duration)}")
    
    return "\n".join(stacks)

# 输出到文件,用FlameGraph工具生成SVG
with open("langchain.folded", "w") as f:
    f.write(generate_flamegraph_data(tracer.records))

方案二:集成开源方案(推荐)

LangChain官方支持对接LangSmith,但也可以接入更通用的OpenTelemetry生态:

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# 配置OTel
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="your-collector:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

class OpenTelemetryCallback(BaseCallbackHandler):
    """将LangChain追踪对接OpenTelemetry"""
    
    def __init__(self):
        self.tracer = trace.get_tracer("langchain")
        self.active_spans = {}
    
    def on_chain_start(self, serialized, inputs, run_id, **kwargs):
        span = self.tracer.start_span(
            name=f"chain:{serialized.get('name', 'unknown')}",
            attributes={"chain.type": serialized.get("_type", "unknown")}
        )
        self.active_spans[run_id] = span
    
    def on_chain_end(self, outputs, run_id, **kwargs):
        span = self.active_spans.pop(run_id, None)
        if span:
            span.set_attribute("output.length", len(str(outputs)))
            span.end()
    
    def on_llm_start(self, serialized, prompts, run_id, **kwargs):
        span = self.tracer.start_span(
            name=f"llm:{serialized.get('name', 'unknown')}",
            attributes={
                "llm.model": serialized.get("name"),
                "prompt.count": len(prompts)
            }
        )
        self.active_spans[run_id] = span
    
    def on_llm_end(self, response, run_id, **kwargs):
        span = self.active_spans.pop(run_id, None)
        if span:
            usage = response.llm_output.get("token_usage", {})
            span.set_attributes({
                "tokens.input": usage.get("prompt_tokens", 0),
                "tokens.output": usage.get("completion_tokens", 0),
                "tokens.total": usage.get("total_tokens", 0)
            })
            span.end()

接入后,数据可流入Jaeger、Zipkin、Grafana Tempo等标准链路追踪平台,享受成熟的可视化能力。

小结

可视化不是炫技,是刚需。选对工具,小团队也能拥有大厂的观测能力。


六、生产环境最佳实践

点题

开发环境的追踪是"越多越好",生产环境要平衡信息量与性能、成本、隐私。

生产环境

采样记录

敏感信息脱敏

异步批量发送

错误全量记录

开发环境

全量记录

详细Prompt/Response

实时打印

痛点分析

直接把开发配置上线?Token日志把用户隐私全暴露了,采样率100%把服务压垮了,同步发送追踪数据把延迟拉爆了

解决方案

1. 智能采样

import random

class ProductionTracer(BaseCallbackHandler):
    def __init__(self, sample_rate=0.1, error_sample_rate=1.0):
        self.sample_rate = sample_rate      # 正常请求10%采样
        self.error_sample_rate = error_sample_rate  # 错误100%采样
        self.should_record = False
    
    def on_chain_start(self, serialized, inputs, **kwargs):
        # 后续根据是否报错调整采样决策
        self.should_record = random.random() < self.sample_rate
    
    def on_chain_end(self, outputs, **kwargs):
        if not self.should_record:
            return
        # 记录...
    
    def on_llm_error(self, error, **kwargs):
        # 错误强制记录
        self.should_record = True
        # 详细记录错误信息...

2. 敏感信息脱敏

import re

def desensitize(text: str) -> str:
    """脱敏处理"""
    # 手机号
    text = re.sub(r'1[3-9]\d{9}', '1**********', text)
    # 身份证号
    text = re.sub(r'\d{17}[\dXx]', '*****************', text)
    # 邮箱
    text = re.sub(r'[\w\.-]+@[\w\.-]+', '***@***.com', text)
    return text

class PrivacyAwareTracer(BaseCallbackHandler):
    def on_llm_start(self, serialized, prompts, **kwargs):
        safe_prompts = [desensitize(p) for p in prompts]
        # 只记录脱敏后的内容...

3. 异步批量发送

import asyncio
from collections import deque

class BatchAsyncTracer(AsyncCallbackHandler):
    def __init__(self, batch_size=100, flush_interval=5):
        self.batch = deque(maxlen=batch_size)
        self.flush_interval = flush_interval
        self._start_flush_task()
    
    async def on_llm_end(self, response, **kwargs):
        self.batch.append(self._extract_trace(response))
        if len(self.batch) >= self.batch.maxlen:
            await self._flush()
    
    async def _flush(self):
        if not self.batch:
            return
        batch_data = list(self.batch)
        self.batch.clear()
        # 批量异步发送
        async with aiohttp.ClientSession() as session:
            await session.post(
                "http://log-service/batch",
                json={"traces": batch_data}
            )
    
    def _start_flush_task(self):
        async def periodic_flush():
            while True:
                await asyncio.sleep(self.flush_interval)
                await self._flush()
        asyncio.create_task(periodic_flush())

小结

生产环境的追踪是门平衡艺术:够用的信息、可接受的开销、合规的隐私保护,三者缺一不可。


写在最后

链路追踪这件事,说大不大,说小不小。但它是你从"能跑就行"迈向"专业可靠"的关键一步。我见过太多项目,前期图省事不埋点,上线后出了问题只能重启、回滚、祈祷三连。也见过团队把追踪做好,线上异常5分钟定位,用户还没察觉就已修复。

编程之路不易,但每一步成长都算数。今天花点时间把Callback机制吃透,明天你就能从容面对生产环境的各种"妖魔鬼怪"。保持好奇,持续学习,你也能成为那个别人眼中的"代码大仙"。


关注私信备注:“资料代找获取”,全网计算机学习资料代找:例如:
《课程:2026 年多模态大模型实战训练营》
《课程:AI 大模型工程师系统课程 (22 章完整版 持续更新)》
《课程:AI 大模型系统实战课第四期 (2026 年开课 持续更新)》
《课程:2026 年 AGI 大模型系统课 23 期》
《课程:2026 年 AGI 大模型系统课 21 期》
《课程:AI 大模型实战课 8 期 (2026 年 2 月最新完结版)》
《课程:AI 大模型系统实战课三期》
《课程:AI 大模型系统课程 (2026 年 2 月开课 持续更新)》
《课程:AI 大模型全阶课程 (2025 年 12 月开课 2026 年 6 月结课)》
《课程:AI 大模型工程师全阶课程 (2025 年 10 月开课 2026 年 4 月结课)》
《课程:2026 年最新大模型 Agent 开发系统课 (持续更新)》
《课程:LLM 多模态视觉大模型系统课》
《课程:大模型 AI 应用开发企业级项目实战课 (2026 年 1 月开课)》
《课程:大模型智能体线上速成班 V2.0》
《课程:Java+AI 大模型智能应用开发全阶课》
《课程:Python+AI 大模型实战视频教程》
《书籍:软件工程 3.0: 大模型驱动的研发新范式.pdf》
《课程:人工智能大模型系统课 (2026 年 1 月底完结版)》
《课程:AI 大模型零基础到商业实战全栈课第五期》
《课程:Vue3.5+Electron + 大模型跨平台 AI 桌面聊天应用实战 (2025)》
《课程:AI 大模型实战训练营 从入门到实战轻松上手》
《课程:2026 年 AI 大模型 RAG 与 Agent 智能体项目实战开发课》
《课程:大模型训练营配套补充资料》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

精通代码大仙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值