
链路追踪:让LangChain的"黑盒"变"白盒",告别"代码跑完却不知道发生了啥"的绝望时刻
全文总结:本文将手把手教你用LangChain的Callback机制实现完整的链路追踪,从基础概念到实战代码,让你彻底掌握如何记录和可视化LLM应用的完整执行路径,排查问题不再抓瞎。
目录:
- 链路追踪核心原理
- CallbackHandler体系
- 同步与异步追踪
- 结构化日志记录
- 可视化追踪方案
- 生产环境最佳实践
嗨,大家好呀,我是你的老朋友精通代码大仙。接下来我们一起学习 《LangChain核心技术与LLM项目实践》,震撼你的学习轨迹!学习+:caogenzhishi 推荐朋友UP:个人快速成长/精进学习
“代码能跑就行,别问为什么”——这句话是不是你的日常写照?
我见过太多同学,LangChain代码跑起来了,输出也对,但中间到底经历了什么?Token用了多少?哪个环节最耗时?LLM到底被调用了几次?一问三不知。等到线上出问题,只能对着屏幕干瞪眼,重启大法都不好使。这种"黑盒"状态,在LLM应用里简直是灾难。今天咱们就把这个盒子拆开,装个透亮的玻璃罩。
一、链路追踪核心原理
点题
链路追踪的本质,是在LangChain的执行流程中埋点,记录每个关键节点的输入、输出、耗时和元数据。LangChain的执行是个链式结构,从用户输入到最终输出,中间可能经过多个组件: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_start | Chain开始执行 | 输入数据、配置参数 |
| on_llm_start | LLM调用前 | 完整Prompt、模型参数 |
| on_llm_end | LLM返回后 | 完整响应、Token用量 |
| on_chain_end | Chain执行完毕 | 最终输出、总耗时 |
# 正确姿势:注入Callback
from langchain.callbacks import StdOutCallbackHandler
handler = StdOutCallbackHandler()
chain = LLMChain(
llm=llm,
prompt=prompt,
callbacks=[handler] # 关键!
)
这样每个步骤都会打印详细日志,执行路径一目了然。
小结
链路追踪不是可有可无的"锦上添花",而是LLM应用的"基础设施"。没有它,你就是闭着眼睛开车。
二、CallbackHandler体系
点题
LangChain的CallbackHandler是一套插件化的追踪机制。你可以自定义Handler,继承BaseCallbackHandler,实现你关心的钩子方法。
痛点分析
很多新手看到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用于异步场景,避免阻塞事件循环。
痛点分析
新手最容易踩的坑:混用同步和异步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也要"对号入座"。别让追踪本身成为性能瓶颈。
四、结构化日志记录
点题
原始日志是给人看的,结构化日志是给机器分析的。链路追踪的数据最终要能被查询、聚合、可视化。
痛点分析
最常见的反模式:打印一堆字符串,没有统一格式。
# 错误示范:无法解析的日志
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等标准链路追踪平台,享受成熟的可视化能力。
小结
可视化不是炫技,是刚需。选对工具,小团队也能拥有大厂的观测能力。
六、生产环境最佳实践
点题
开发环境的追踪是"越多越好",生产环境要平衡信息量与性能、成本、隐私。
痛点分析
直接把开发配置上线?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 智能体项目实战开发课》
《课程:大模型训练营配套补充资料》
399

被折叠的 条评论
为什么被折叠?



