AI Agent可观测性实战:给大模型代理装上行车记录仪
1. 项目概述:当AI代理开始“装样子”,你还能信它吗?
我带过六支不同行业的AI工程团队,从金融风控到智能客服,从工业质检到教育陪练。最常被问到的问题不是“怎么让代理更聪明”,而是“它刚才到底干了啥?”。去年帮一家做法律文书辅助的客户上线新代理系统,上线第三天,客户法务总监直接打电话来:“你们那个‘合同风险识别代理’,昨天把一份明显含霸王条款的采购协议标成了‘低风险’,连基础条款比对都没触发——它到底是没看,还是看了装没看?”这个问题像一记闷棍。我们花了整整两天回溯日志,最后发现:代理确实在执行流程里调用了条款比对工具,但工具返回了空结果,代理却用一个默认的“低风险”标签草草收场,整个过程在传统日志里只留下一行“tool_call: clause_checker → success”,连返回值都没记录。这就是典型的“黑箱幻觉”——系统日志显示一切正常,业务结果却彻底失真。这篇文章要解决的,就是这个痛点: 如何穿透AI代理表面的“运行中”状态,真正确认它是否在按设计逻辑思考、决策、行动 。核心不在于它有没有报错,而在于它是否在关键节点做出了符合预期的判断。关键词“Towards AI - Medium”指向的是一类高度务实、面向一线开发者的实践指南,所以这里不会讲抽象理论,只讲我在真实项目里验证过的、能立刻上手的观测方法。适合三类人:正在调试自己第一个Agent的工程师、需要向业务方证明Agent可靠性的技术负责人、以及被“代理明明在跑,效果却飘忽不定”折磨已久的AI产品经理。它不是教你怎么写Prompt,而是教你怎么给Agent装上“行车记录仪”和“心电图仪”。
2. 为什么传统日志在AI代理面前彻底失效?
2.1 日志的“成功陷阱”:一行代码背后的千种可能
传统软件工程的日志哲学是“记录关键状态与错误”。一个HTTP请求日志可能包含:时间戳、URL、状态码(200/404/500)、耗时。这足够判断服务是否“活着”。但AI代理的执行链路完全不同。以一个标准的ReAct模式代理为例,它的单次推理循环通常包含:接收用户输入 → 调用LLM生成Thought → 根据Thought决定调用哪个Tool → 执行Tool获取结果 → LLM整合Tool结果生成Final Answer。在这个链条里,“成功”这个词变得极其危险。
-
状态码的欺骗性 :当Tool调用返回
{"status": "success", "data": null}时,传统日志只会记下“Tool call succeeded”。但data为空意味着什么?是上游API真的没返回数据?是网络超时后代理自动填充了空值?还是代理在Thought阶段就误判了需要调用的Tool类型,导致调用了一个完全无关的接口?日志不告诉你。 -
Thought的不可见性 :LLM生成的Thought是代理决策的“大脑活动”,但它通常被当作中间变量丢弃。日志里只有最终Answer,没有Thought。这就如同只记录医生开的处方,却不记录他诊断时的思路、排除了哪些疾病、依据了哪几条症状。当Answer出错,你无从判断是LLM本身幻觉了,还是Tool返回了错误数据导致LLM被误导,抑或是Prompt设计缺陷让LLM根本没理解任务目标。
-
上下文的断层 :现代Agent框架(如LangChain、LlamaIndex)普遍支持长上下文和记忆机制。但传统日志是离散的、按时间戳排列的碎片。你看到第100行日志写着“Retrieved document X from vector DB”,第105行写着“Generated answer based on context”,但这两行之间缺失了最关键的环节:LLM是如何从文档X中提取出那三条关键信息的?它是否忽略了文档Y里更相关的反例?日志无法重建这个“信息蒸馏”的过程。
我曾在一个电商推荐Agent项目里踩过这个坑。日志显示所有步骤都“success”,但用户反馈推荐商品越来越偏离兴趣。回溯发现,Agent的“记忆压缩”模块在处理长对话历史时,会将用户多次表达的“不要推荐奢侈品”压缩成一个模糊的“偏好:性价比”,导致后续推荐完全丢失了这个强约束。这个压缩逻辑本身没有报错,日志里只有一行“Compressed memory: success”,真相被完美掩盖。
2.2 观测(Observability)的本质:从“是否运行”到“如何思考”
观测(Observability)这个概念,最早源于控制理论,指仅通过系统外部输出,就能推断其内部状态的能力。在AI Agent领域,它被重新定义为: 通过结构化、可关联、可追溯的数据,还原Agent每一次决策的完整因果链 。这要求数据必须具备三个硬性特征:
-
结构化(Structured) :数据不能是纯文本日志。它必须是带有明确Schema的JSON或数据库记录,字段如
trace_id(追踪ID)、span_id(操作ID)、parent_span_id(父操作ID)、input、output、metadata(元数据)。这就像给每个操作发一张带唯一编号的“身份证”,确保你能把它从海量数据里精准揪出来。 -
可关联(Correlated) :单个操作的数据毫无意义。关键是要把一次用户请求(User Query)所触发的所有动作——LLM调用、Tool调用、记忆读写、最终Answer——用同一个
trace_id串起来。这形成一条完整的“执行轨迹”(Trace)。没有关联,你看到的只是散落一地的零件;有关联,你才能看到整台发动机如何运转。 -
可追溯(Traversable) :光有轨迹还不够。你必须能沿着轨迹“钻进去”,查看每个环节的细节。比如点击一个LLM调用的Span,应该能看到原始Prompt、实际传入的上下文、模型返回的完整Response(包括token数、耗时)、甚至模型的logprobs(如果支持)。点击一个Tool调用,应该能看到传入参数、原始API响应、Agent对响应的解析逻辑。这种“层层下钻”的能力,是定位问题的根本。
这三点,恰恰是传统日志(如Python的 logging 模块)完全不具备的。它只能告诉你“发生了什么”,而观测系统要告诉你“为什么发生,以及它是如何一步步发生的”。在我经手的项目里,引入观测系统后,平均故障定位时间(MTTR)从4-6小时缩短到15-30分钟。这不是因为问题变少了,而是因为问题第一次出现时,你就已经拿到了全部线索。
2.3 为什么Langfuse成为当前最务实的选择?
市面上的观测工具不少:OpenTelemetry是行业标准,但配置复杂,需要自建后端;Helicone偏重LLM API网关监控;PromptLayer功能全面但闭源。而Langfuse之所以成为我团队的首选,核心在于它精准击中了“一线开发者最痛的三个点”:
-
零侵入式集成(Zero-Code Instrumentation) :Langfuse提供官方SDK,只需在Agent框架的关键Hook点(如
on_llm_start,on_tool_start)插入几行初始化和回调代码。以LangChain为例,你不需要改任何业务逻辑,只需在创建LLMChain或AgentExecutor时,传入一个Langfuse回调处理器。它会自动捕获所有LLM调用、Tool调用、链式调用的完整数据流。我试过一个500行的LangChain Agent脚本,加观测只花了7分钟,新增代码不到20行。 -
开箱即用的分析视图(Out-of-the-Box Analytics) :安装完Langfuse Server(Docker一键部署),打开Web UI,你立刻就能看到:按
trace_id组织的完整执行轨迹树、各LLM调用的耗时/Token分布热力图、Tool调用成功率排行榜、甚至基于规则的自动异常检测(如“连续3次Tool返回空数据”)。这些不是需要你写SQL去查的原始数据,而是直接可视化的洞察。上周我帮一个客户排查一个“回答总是延迟”的问题,打开Langfuse的“Slowest Traces”面板,一眼就看到90%的延迟来自一个向外部知识库发起的vector_searchTool,平均耗时8.2秒。点进去看具体Span,发现是查询向量维度不匹配导致数据库全表扫描——这个结论,传统日志里埋几百个print都得不出。 -
评估即代码(Evaluation-as-Code)的落地 :观测的终极目的不是看热闹,而是驱动改进。Langfuse内置了评估框架,允许你用Python代码定义评估规则。例如,你可以写一个函数,检查每次LLM生成的Thought是否包含了对Tool返回数据的明确引用(如“根据Tool返回的订单状态‘已发货’…”),如果没包含,就标记为“Thought-Data Mismatch”。这个评估会自动跑在每一条历史Trace上,生成量化报告。这比人工抽检100条样本高效且客观。我们一个金融Agent项目,就靠这个规则发现了37%的Thought存在“数据引用缺失”,直接推动了Prompt模板的重构。
选择Langfuse,不是因为它最炫酷,而是因为它把“观测”这件事,从一个需要组建专门SRE团队的高门槛工程,降维成一个普通工程师下午茶时间就能搞定的实操技能。这才是“让AI Agent真正工作”的基石。
3. 实战:手把手搭建你的AI Agent观测系统
3.1 环境准备与Langfuse服务部署
部署Langfuse服务是整个观测体系的地基。它有两个核心组件:后端(Backend)负责接收、存储、索引数据;前端(Frontend)提供可视化界面。官方推荐使用Docker Compose一键部署,这是最稳定、最省心的方式。以下是我经过20+个项目验证的标准化流程,全程在Linux/macOS终端执行。
第一步:安装Docker与Docker Compose 确保你的服务器或本地开发机已安装Docker(>=20.10)和Docker Compose(>=2.20)。验证命令:
docker --version && docker-compose --version
如果未安装,请访问 Docker官网 按指引安装。这是唯一需要你手动完成的环境依赖。
第二步:获取并配置docker-compose.yml Langfuse官方提供了预配置的 docker-compose.yml 文件。我们不直接使用,而是基于它进行安全加固。创建一个新目录,例如 langfuse-deploy ,进入该目录,执行:
curl -fsSL https://raw.githubusercontent.com/langfuse/langfuse/main/docker/docker-compose.yml -o docker-compose.yml
然后,用你喜欢的编辑器(如 nano 或 vim )打开 docker-compose.yml ,找到 environment 部分下的 LANGFUSE_SECRET_KEY 。 这是最关键的安全配置,绝对不能留空或使用默认值! 它相当于你观测系统的“主密码”,用于签名所有上报的数据。生成一个强密钥:
openssl rand -base64 32
将生成的随机字符串,替换掉 docker-compose.yml 中 LANGFUSE_SECRET_KEY 的值。同时,检查 POSTGRES_PASSWORD ,也建议用同样方式生成一个新密码并替换。这一步做完,你的服务就具备了基础的安全防护。
第三步:启动服务 在 langfuse-deploy 目录下,执行:
docker-compose up -d
Docker会自动拉取镜像、创建网络、启动PostgreSQL数据库和Langfuse后端服务。等待约30秒,执行:
docker-compose ps
你应该看到 langfuse 和 postgres 两行状态均为 Up 。至此,后端服务已就绪。
第四步:访问Web UI并创建项目 打开浏览器,访问 http://localhost:3000 (如果你在远程服务器部署,请将 localhost 替换为服务器IP,并确保3000端口已开放)。首次访问会引导你创建管理员账户。完成注册后,点击左上角 + Create Project ,为你的AI Agent项目起一个名字(如 e-commerce-recommender ),并选择 Production 环境。创建成功后,页面会显示两个至关重要的密钥:
Public Key:用于前端(如网页应用)上报数据,可公开。Secret Key:用于后端(如你的Agent服务)上报数据, 必须严格保密,绝不能提交到Git或暴露在前端代码中 。
提示:
Secret Key是你Agent服务连接Langfuse的“钥匙”。请立即将它保存在一个安全的地方(如密码管理器),并在后续Agent代码中使用。泄露此密钥可能导致他人向你的Langfuse实例注入伪造数据,污染观测结果。
3.2 在LangChain Agent中植入观测钩子
假设你正在使用LangChain构建一个基于ReAct模式的客服Agent。以下是将Langfuse观测能力注入到Agent中的完整、可复用的代码模板。我以一个真实的电商客服场景为例:Agent需要能查询订单状态、解释退换货政策、并根据用户情绪调整回复语气。
第一步:安装依赖
pip install langchain langfuse python-dotenv
python-dotenv 用于安全地管理环境变量,避免密钥硬编码。
第二步:配置环境变量 在项目根目录创建 .env 文件:
LANGFUSE_SECRET_KEY=your_actual_secret_key_here # 替换为你在UI中复制的Secret Key
LANGFUSE_PUBLIC_KEY=your_actual_public_key_here # 替换为你在UI中复制的Public Key
LANGFUSE_HOST=http://localhost:3000 # 如果Langfuse部署在远程服务器,请修改为对应IP
第三步:编写核心观测代码(agent_with_observability.py)
import os
from dotenv import load_dotenv
from langchain.agents import AgentExecutor, create_react_agent
from langchain import hub
from langchain_openai import ChatOpenAI
from langchain.tools import Tool
from langfuse import Langfuse
from langfuse.callback import CallbackHandler
# 加载环境变量
load_dotenv()
# 初始化Langfuse客户端(注意:这是全局单例)
langfuse = Langfuse(
secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
host=os.getenv("LANGFUSE_HOST")
)
# 创建Langfuse回调处理器(这是注入观测能力的核心)
langfuse_handler = CallbackHandler(
secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
host=os.getenv("LANGFUSE_HOST")
)
# 模拟一个查询订单状态的Tool
def query_order_status(order_id: str) -> str:
"""模拟调用订单API。实际项目中这里应是requests.post调用。"""
# 这里加入一个“故障点”用于演示观测效果:当order_id以'ERR'开头时,返回空数据
if order_id.startswith("ERR"):
return "" # 故意返回空字符串,模拟API故障
return f"Order {order_id} status: Shipped. Estimated delivery: 2 days."
order_tool = Tool(
name="OrderStatusChecker",
func=query_order_status,
description="Useful for checking the current status of a user's order by order ID."
)
# 模拟一个查询退换货政策的Tool
def get_return_policy() -> str:
return "Our return policy allows returns within 30 days of purchase. Items must be in original condition with tags attached."
policy_tool = Tool(
name="ReturnPolicyFetcher",
func=get_return_policy,
description="Useful for retrieving the company's return and exchange policy."
)
# 创建LLM(这里用OpenAI,实际可替换为任何兼容LangChain的LLM)
llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.3)
# 获取ReAct提示模板(LangChain Hub提供标准模板)
prompt = hub.pull("hwchase17/react-chat")
# 创建Agent
tools = [order_tool, policy_tool]
agent = create_react_agent(llm, tools, prompt)
# 创建AgentExecutor,并注入Langfuse回调处理器
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # 开启verbose便于本地调试,生产环境可关闭
handle_parsing_errors=True, # 自动处理LLM输出格式错误
callbacks=[langfuse_handler] # 关键!将观测处理器注入
)
# 测试函数
def run_agent(query: str):
"""运行Agent并返回结果。每次调用都会自动生成一个trace。"""
try:
result = agent_executor.invoke({"input": query})
return result["output"]
except Exception as e:
# 即使Agent执行失败,Langfuse也会捕获到错误事件
print(f"Agent execution failed: {e}")
raise e
# 示例调用
if __name__ == "__main__":
# 正常查询
print(run_agent("What's the status of my order #12345?"))
# 故障查询(触发我们预设的空返回)
print(run_agent("What's the status of my order #ERR999?"))
关键原理与注意事项:
-
CallbackHandler的工作机制 :LangChain的callbacks参数是一个钩子列表。CallbackHandler会在Agent执行的每一个关键生命周期事件(如on_llm_start,on_tool_start,on_chain_end)自动触发,并将事件数据(包括输入、输出、耗时、错误堆栈)打包,通过HTTP POST发送给Langfuse后端。你完全不需要手动调用langfuse.trace()或langfuse.span(),框架已为你封装好。 -
verbose=True的双重作用 :开启它不仅能在控制台看到详细执行流(方便本地调试),更重要的是,LangChain会将这些详细的日志信息(如Thought内容、Tool调用参数)作为metadata的一部分,一并上报给Langfuse。这是观测Thought的必要条件。 -
错误处理的鲁棒性 :即使
agent_executor.invoke()抛出异常(如网络超时、LLM返回格式错误),CallbackHandler依然会捕获到on_chain_error事件,并在Langfuse UI中清晰地标记为红色的失败Span。这让你能第一时间发现“静默失败”的环节。
运行这段代码后,回到Langfuse Web UI的 Traces 页面,你将立即看到两条新的Trace记录。点击其中一条,就能看到一棵完整的执行树:根节点是用户Query,子节点是LLM调用(显示Prompt和Response),再下一层是Tool调用(显示传入的 order_id 和返回的 Shipped 字符串)。这就是你Agent的“数字孪生”。
3.3 设计并运行首个自动化评估:捕捉“Thought-Data脱节”
观测的价值,在于驱动改进。仅仅看到Trace是不够的,你需要一套自动化的“质检员”来扫描所有历史数据,找出模式化的问题。Langfuse的Evaluations功能,就是为此而生。下面,我将带你创建一个针对“Thought-Data脱节”问题的评估脚本。这个问题在实践中极为普遍:LLM生成的Thought声称要基于某个Tool的结果做判断,但Thought文本里却完全没提那个结果,说明它要么没看到,要么没理解。
第一步:理解评估的核心概念 Langfuse评估基于两个核心对象:
- Dataset :一个测试用例集合,每个用例包含
input(用户Query)和expected_output(你期望的正确Answer)。它相当于你的“考卷”。 - Evaluation :一个Python函数,接收一个
trace(一次完整的执行记录)作为输入,返回一个score(0.0-1.0)和一个comment(文字说明)。它相当于“阅卷老师”。
第二步:创建Dataset(test_dataset.py)
from langfuse import Langfuse
langfuse = Langfuse(
secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
host=os.getenv("LANGFUSE_HOST")
)
# 创建一个新的Dataset,命名为"thought-data-alignment"
dataset = langfuse.create_dataset(
name="thought-data-alignment",
description="Test cases to evaluate if LLM Thought references Tool output data."
)
# 添加测试用例
# 用例1:正常情况,Thought应提及Tool返回的'Shipped'
dataset_item_1 = langfuse.create_dataset_item(
dataset_name="thought-data-alignment",
input={"input": "What's the status of my order #12345?"},
expected_output="Your order #12345 has been shipped and will arrive in 2 days."
)
# 用例2:故障情况,Tool返回空,Thought应体现'无法获取状态',而非假装知道
dataset_item_2 = langfuse.create_dataset_item(
dataset_name="thought-data-alignment",
input={"input": "What's the status of my order #ERR999?"},
expected_output="I couldn't retrieve the status for order #ERR999. Please check your order ID or contact support."
)
print(f"Dataset 'thought-data-alignment' created with {len([dataset_item_1, dataset_item_2])} items.")
运行此脚本,你的Langfuse UI中就会出现一个名为 thought-data-alignment 的新Dataset。
第三步:编写评估函数(eval_thought_data.py)
import re
from langfuse import Langfuse
from langfuse.decorators import langfuse_context
langfuse = Langfuse(
secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
host=os.getenv("LANGFUSE_HOST")
)
def evaluate_thought_data_alignment(trace):
"""
评估LLM Thought是否合理引用了Tool的输出数据。
规则:1. Thought中必须包含对Tool返回的关键信息(如'Shipped', '30 days')的明确提及。
2. 如果Tool返回空,Thought中必须包含'not found', 'empty', 'error'等表示失败的词汇。
"""
# 从trace中提取所有LLM调用的Thought(通常在第一个LLM Span的output中)
llm_spans = [span for span in trace.observations if span.type == "GENERATION" and "Thought:" in span.output]
if not llm_spans:
return {"score": 0.0, "comment": "No LLM generation with 'Thought:' found in trace."}
thought_text = llm_spans[0].output
# 提取所有Tool调用及其返回值
tool_spans = [span for span in trace.observations if span.type == "SPAN" and span.name.startswith("Tool:")]
if not tool_spans:
return {"score": 0.0, "comment": "No Tool calls found in trace."}
# 检查每个Tool返回值是否在Thought中被提及
all_mentioned = True
comments = []
for tool_span in tool_spans:
tool_output = tool_span.output or ""
if not tool_output.strip():
# Tool返回空,检查Thought是否承认失败
if not re.search(r"(not found|empty|error|failed|unable|couldn't)", thought_text.lower()):
all_mentioned = False
comments.append(f"Tool '{tool_span.name}' returned empty, but Thought doesn't acknowledge failure.")
else:
# Tool有返回,检查Thought是否提及关键信息
# 这里简化:检查Thought是否包含Tool输出中的任意一个单词(实际项目中应使用更精确的NLP匹配)
key_words = tool_output.split()[:3] # 取前3个词作为关键词
if not any(word.lower() in thought_text.lower() for word in key_words):
all_mentioned = False
comments.append(f"Thought doesn't mention key words from Tool '{tool_span.name}' output: {key_words}")
score = 1.0 if all_mentioned else 0.0
comment = " | ".join(comments) if comments else "Thought and Tool data are well aligned."
return {"score": score, "comment": comment}
# 将评估函数注册到Langfuse
langfuse_context.instrument(evaluate_thought_data_alignment)
第四步:在UI中运行评估
- 回到Langfuse Web UI,进入
Datasets页面,找到thought-data-alignment。 - 点击右侧的
Run Evaluation按钮。 - 在弹出窗口中,选择
Evaluate with function,然后从下拉菜单中选择你刚刚注册的evaluate_thought_data_alignment函数。 - 点击
Run。Langfuse会自动遍历Dataset中的每个测试用例,对它们的历史Trace(或触发新的执行)运行评估函数。 - 几秒钟后,结果会显示在
Evaluations页面。你会看到一个清晰的表格:每一行是一个测试用例,列有Score(0或1)、Comment(具体原因)、Trace ID(链接到原始Trace)。
这个评估脚本,就是你Agent的“思想体检报告”。它不再依赖人工抽查,而是每天自动扫描所有流量,一旦发现Thought与数据脱节,立刻告警。在我负责的一个项目中,这个评估上线后第一周,就发现了23%的Thought存在严重脱节,直接推动了Prompt中“强制引用”指令的强化。
4. 常见问题与实战排错技巧实录
4.1 “Trace没出现!”——数据上报失败的五大元凶与速查表
这是新手遇到的最高频问题。你兴冲冲地跑完Agent代码,满怀期待地刷新Langfuse UI,结果 Traces 页面一片空白。别慌,按照这个清单逐项排查,90%的问题都能在5分钟内解决。
| 排查项 | 检查方法 | 典型症状与解决方案 | 我的实操心得 |
|---|---|---|---|
| 1. Secret Key错误或缺失 | 检查 .env 文件中的 LANGFUSE_SECRET_KEY 是否与UI中复制的完全一致(注意大小写、空格、特殊字符)。在代码中 print(os.getenv("LANGFUSE_SECRET_KEY")) 确认是否读取成功。 |
症状:控制台无任何错误,但Langfuse无数据。 方案: Secret Key 必须100%匹配,复制时务必选中全部字符,避免漏掉末尾的 = 号。 |
我曾因复制时多了一个看不见的换行符,折腾了1小时。现在我的标准操作是:在UI中双击 Secret Key ,右键“复制”,然后在终端用 echo "xxx" | wc -c 检查长度是否与UI显示的字符数一致。 |
| 2. Langfuse服务未运行或网络不通 | 在终端执行 curl -v http://localhost:3000/healthz (本地)或 telnet your-server-ip 3000 (远程)。 |
症状: curl 返回 Connection refused 或 timeout 。 方案: docker-compose ps 确认服务状态;检查防火墙( sudo ufw status );远程部署时确认云服务器安全组是否放行3000端口。 |
在AWS EC2上部署时,我忘了配置安全组, curl 本地通,但从Agent服务器 curl 不通。记住:Agent服务器和Langfuse服务器必须能互相 ping 通且端口可达。 |
| 3. CallbackHandler未正确注入 | 检查 AgentExecutor 或 LLM 初始化代码,确认 callbacks=[langfuse_handler] 已传入,且 langfuse_handler 变量已正确定义。 |
症状:Agent能正常运行并返回结果,但Langfuse无数据。 方案:在 AgentExecutor 创建后, print(agent_executor.callbacks) ,应输出 [<langfuse.callback.CallbackHandler object at 0x...>] 。 |
LangChain版本升级有时会改变回调接口。如果 callbacks 参数不生效,查阅你所用LangChain版本的官方文档,确认回调注册方式(如新版可能需用 llm.bind(callbacks=[...]) )。 |
| 4. LLM/Tool调用未被LangChain框架捕获 | 如果你绕过了LangChain的 LLMChain 或 AgentExecutor ,直接用 llm.invoke() ,则 CallbackHandler 不会触发。 |
症状:只有你手动添加的 langfuse.trace() 有数据,LLM调用无记录。 方案:将所有LLM调用统一纳入LangChain的 Runnable 链中,或手动为每个 llm.invoke() 包裹 langfuse.span() 。 |
我们有个老项目,大量使用裸 openai.ChatCompletion.create() 。为了快速接入观测,我写了一个装饰器: @track_llm_call ,在调用前后自动创建Span,效果一样好。 |
| 5. 数据被过滤或采样 | 检查Langfuse SDK初始化时是否设置了 sample_rate=0.1 (默认1.0,即100%上报)。 |
症状:只有部分Trace出现,且出现频率不稳定。 方案:在 CallbackHandler 初始化时,显式指定 sample_rate=1.0 。 |
生产环境流量大时,可以设置 sample_rate=0.01 (1%采样)来平衡性能与可观测性。但调试期务必设为1.0。 |
注意:Langfuse的
CallbackHandler默认会捕获所有事件,但如果你在代码中使用了langfuse.flush(),它会强制清空缓冲区并上报。在调试时,可以在agent_executor.invoke()之后加一行langfuse.flush(),确保数据立即可见,避免因网络延迟导致的“假阴性”。
4.2 “Trace里看不到Thought!”——解锁LLM中间产物的隐藏开关
很多用户反馈:“Trace里只有最终Answer,Thought在哪?”。这是因为LangChain默认只将LLM的最终 message.content 作为 output 上报,而Thought是嵌在 message.content 里的一个文本片段(如 Thought: I need to check the order status... ),Langfuse不会自动解析它。
根本原因与解决方案: LangChain的 ChatModel (如 ChatOpenAI )返回的是一个 AIMessage 对象,其 content 属性是字符串。 CallbackHandler 上报的 output ,就是这个 content 的原始值。Thought是这个字符串的一部分,不是独立字段。
实操解法(三步走):
- 启用Verbose模式 :在
AgentExecutor初始化时,确保verbose=True。这会让LangChain在内部日志中打印出完整的ReAct步骤,而CallbackHandler会将这些日志作为metadata的一部分上报。 - 在Langfuse UI中查看Metadata :在Trace详情页,找到对应的
GENERATION类型的Span(通常是第一个),点击展开。向下滚动,找到Metadata区域。在这里,你会看到一个名为messages的数组,其中就包含了完整的、带Thought的原始消息序列。 - (进阶)自定义Callback,提取Thought :如果需要在评估或告警中直接使用Thought,可以继承
CallbackHandler,重写on_llm_end方法:
然后用这个自定义Handler替换原来的from langfuse.callback import CallbackHandler class ThoughtExtractingHandler(CallbackHandler): def on_llm_end(self, response, **kwargs): # response.generations 是一个列表,取第一个 if response.generations and response.generations[0].message.content: content = response.generations[0].message.content # 使用正则提取Thought thought_match = re.search(r"Thought:\s*(.*?)(?:\n|$)", content, re.DOTALL) if thought_match: thought = thought_match.group(1).strip() # 将Thought作为额外字段上报 self._log_to_langfuse( type="GENERATION", name="LLM with Thought", input=kwargs.get("input", ""), output=content, metadata={"extracted_thought": thought} ) super().on_llm_end(response, **kwargs)CallbackHandler。这样,Thought就会作为一个独立的metadata字段出现在Langfuse中,方便后续所有分析。
4.3 “评估分数全是0!”——评估函数失效的典型陷阱
当你运行评估,发现所有测试用例的 Score 都是0.0,而你知道它们应该是1.0时,问题大概率出在评估函数本身。以下是三个最隐蔽的坑。
陷阱一:Trace数据未加载完全 Langfuse的 trace.observations 是一个懒加载的列表。如果你在评估函数中直接 for span in trace.observations: ,它可能只返回了部分Span(如只返回了LLM调用,没返回Tool调用),因为数据是分页加载的。
破解方法 :在评估函数开头,强制加载所有观测数据:
def my_evaluation(trace):
# 关键!强制加载所有observations
trace.observations # 这行代码会触发一次完整的API调用,加载所有数据
# 然后再进行你的逻辑
for span in trace.observations:
...
陷阱二:正则表达式匹配过于脆弱 在 evaluate_thought_data_alignment 函数中,我用了 re.search(r"(not found|empty|error|failed|unable|couldn't)", thought_text.lower()) 。这看起来很合理,但如果LLM返回的Thought是“I’m unable to find the order status.”,这里的 unable 后面跟的是空格,而正则里是 | 分隔,没问题。但如果LLM返回的是“I'm unableto find...”( unable 和 to 连在一起),正则就失效了。
破解方法 :使用更健壮的匹配,或结合语义相似度。简单版:
# 检查Thought是否包含任何表示“失败”的词汇
failure_keywords = ["not found", "empty", "error", "failed", "unable", "couldn't", "unavailable", "no result"]
if not any(keyword in thought_text.lower() for keyword in failure_keywords):
...
陷阱三:Dataset Item与Trace未正确关联 Langfuse的评估,是将Dataset中的 input 与历史Trace的 input 进行字符串匹配。如果你的Agent在处理Query时,会对输入做预处理(如去除标点、转小写、添加前缀),那么Dataset中存的原始 input 就无法匹配上Trace中处理后的 input 。
破解方法 :在创建Dataset Item时, input 字段应填写Agent实际接收到的、未经处理的原始输入。或者,在评估函数中,对 trace.input 做与Agent相同的预处理,再与 dataset_item.input 比较。
提示:Langfuse UI的
Evaluations页面,每个评估结果旁都有一个Debug按钮。点击它,会打开一个弹窗,显示评估函数的完整执行日志,包括trace.input、trace.observations的快照、以及你的函数返回的score和comment。这是排查评估逻辑问题的终极武器。我90%的评估调试,
更多推荐


所有评论(0)