首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >LangSmith全链路观测:Agent调试与生产监控

LangSmith全链路观测:Agent调试与生产监控

作者头像
陆业聪
发布2026-07-20 20:54:10
发布2026-07-20 20:54:10
280
举报

📰 科技要闻

• watchOS 27公测版放出,动态应用网格、Liquid Glass微调成为AI功能之外最受关注的更新点。

• 英特尔与Google Cloud宣布深化战略合作,阿里1688同步公布AI时代B2B交易互联互通开放标准。

• 森博科技董事长于林义谈AI应用落地:拼的不只是技术,更是能跑通、能验证的业务闭环。

上一篇写完评估体系没几天,线上就来了个"送分题"——夜里黄金测试集跑批,Faithfulness分数从0.83掉到0.61,直接触发了我们的P0拦截。按照排查顺序,我先看Context Recall,正常;再看Prompt有没有改,也没改。三个人对着日志文件大眼瞪小眼,从检索模块的print输出翻到生成模块的日志,中间还夹着一次Query改写和一次工具调用,硬是花了小半天才发现,是Reranker服务那天偷偷升级了模型版本,返回的片段顺序变了,进而影响了Prompt里片段的排列,模型"理解"错了优先级。

这件事让我彻底服了——靠print和肉眼翻日志去调试一个多步骤Agent,本质上是在用石器时代的工具修复宇宙飞船。评估体系告诉你"分数掉了",但不会告诉你"哪一步掉的"。这篇要解决的,就是把这个黑盒变成一棵可以点开、可以回放的调用树——这就是LangSmith的活儿。

一、为什么日志这套老办法在Agent系统里彻底失效了

我做了十几年Android开发,日志这件事我太熟了——Logcat配合过滤tag,配合火焰图,配合Systrace,单进程内的问题基本都能揪出来。但LLM应用的调试难度不是同一个量级的,原因有三个:

一是非确定性:同一个输入,两次调用可能得到不同输出,日志里的"复现步骤"经常复现不了。二是调用链又长又跨进程:一次Agent请求可能串了Query改写、检索、Rerank、工具调用、多轮生成,日志散落在五六个服务里,靠grep拼不回一条完整的时间线。三是成本和延迟藏在细节里:你不知道是哪一步的Token消耗炸了,也不知道是哪一次LLM调用拖慢了整体响应,日志文本本身回答不了这些结构化问题。

说白了,我们需要的不是"打印更多日志",而是把每一次调用组织成一棵结构化的Trace树——根节点是这次用户请求,子节点是检索、重排、生成、工具调用……每个节点自带耗时、Token数、输入输出,点开就能看,不用再靠肉眼在几十屏日志里做侦探。

二、LangSmith Tracing:把一次调用拆成一棵树

如果你的Pipeline已经是LangChain/LangGraph搭的,开Tracing几乎是零成本——设几个环境变量就行:

代码语言:javascript
复制
# .env
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=ls__xxx
LANGSMITH_PROJECT=kb-agent-prod
LANGSMITH_ENDPOINT=
https://api.smith.langchain.com

但我们项目里,检索、Rerank、评分这些环节很多是自研的,没有全走LangChain封装。这种情况下真正有用的是@traceable装饰器——它不要求你用LangChain的任何抽象,任意Python函数套上就能进树:

代码语言:javascript
复制
from langsmith import traceable@traceable(
run_type="retriever",
name="hybrid_search",
)
def retrieve(query: str, top_k: int = 10):
dense = vector_search(query, top_k)
sparse = bm25_search(query, top_k)
return merge_and_dedup(dense, sparse)@traceable(
run_type="tool", name="bge_rerank",
)
def rerank(query, contexts):
scores = bge_reranker.score(
query, contexts,
)
return sort_by_score(contexts, scores)@traceable(
run_type="chain", name="kb_agent",
)
def answer(query: str):
ctx = retrieve(query)
ctx = rerank(query, ctx)
return generate(query, ctx)

关键在于run_type参数——它决定了这个节点在Trace树里怎么展示、怎么统计。LangSmith内置了几种类型:chain(编排逻辑)、llm(模型调用,自动统计Token)、retriever(检索)、tool(工具调用)。只要类型标对,一次请求跑完,UI上就是一棵完整的树,每层缩进清清楚楚,哪一步耗时最长、哪一步返回了什么,点开就能看,跟Android Studio的Profiler点开方法调用栈的体验其实很像。

回到开头那次排障——如果当时Rerank是@traceable的,我们只需要在Trace列表里过滤"Faithfulness低于阈值"的请求,展开树,一眼就能看到Rerank节点这次返回的片段顺序跟平时不一样——五分钟的事,而不是半天。

用metadata和tags做精准过滤

生产环境每天几十万条Trace,光靠时间排序去翻是找不到问题的。我们的做法是在每次调用上打好业务元信息,后续排查直接筛:

代码语言:javascript
复制
from langsmith.run_helpers import (
trace,
)with trace(
name="kb_agent",
tags=["tenant:acme", "v2.3"],
metadata={
"user_id": user_id,
"session_id": session_id,
"chunk_size": 256,
},
) as run:
result = answer(query)
run.end(outputs={
"answer": result,
})

线上出问题第一反应就是按tenant和版本号过滤,快速判断是不是某个租户或某次发布特有的问题,这比"整个系统都在报错"式的排查效率高得多。

三、Prompt Hub:把Prompt当"代码"一样管理

这是我们踩的另一个坑:早期Prompt都是写在代码里的字符串常量,改一次Prompt要走一次完整的发布流程——review、CI、部署,一套流程走完,半天就没了。后来才想明白,Prompt本质上是配置,不是代码逻辑,非要绑在代码发布节奏上,纯属自缚手脚。

LangSmith的Prompt Hub把Prompt拆出来单独版本化管理,代码里只保留一个引用:

代码语言:javascript
复制
from langsmith import Clientclient = Client()# 拉取生产环境标记的版本
prompt = client.pull_prompt(
"kb-agent-system:production",
)# 灰度实验:拉取另一个候选版本
prompt_v2 = client.pull_prompt(
"kb-agent-system:candidate-v2",
)def generate(query, ctx):
active = (
prompt_v2 if in_ab_bucket(query)
else prompt
)
messages = active.invoke({
"query": query,
"context": ctx,
})
return llm.invoke(messages)

好处很直接:产品或运营同事可以直接在Hub的Playground里改Prompt文案、跑几个测试用例,确认效果后打上production标签,代码零改动、零发布就上线了。每个版本都有diff记录,出问题一键回滚,跟Git的体验很接近——只是版本对象换成了Prompt文本。

提醒一句:Prompt从代码里"解耦"出去之后,务必把它纳入你的评估流水线——每次切换production标签前,先跑一遍上一篇提到的黄金测试集。不然Prompt Hub会变成一个"绕过CI直接改生产逻辑"的后门,谁都能改,谁都不用负责。

四、生产监控看板:延迟、Token、错误率、满意度

Tracing解决"这一次调用发生了什么",监控看板解决"整体上系统跑得怎么样"。LangSmith的Monitor页面默认就有延迟分布、Token消耗、错误率这几个维度,但真正有用的是把你自己的业务指标也接进去——比如上一篇的Faithfulness分数,作为一个"在线评估"结果实时写回Trace:

代码语言:javascript
复制
# 异步跑一个轻量裁判,把分数打回Trace
def score_and_log(run_id, question, answer, ctx):
score = judge_faithfulness(
question, answer, ctx,
)
client.create_feedback(
run_id,
key="faithfulness",
score=score,
comment=f"auto-judge: {score}",
)

这样一来,看板上的"忠实度均值"曲线就不是离线跑批才有的东西,而是每一次真实用户请求都在实时打分——你能看到分数在哪个时间点开始掉、掉了多少,配合上面的tags过滤,基本能秒定位是哪个版本或哪个租户出了问题。

另外一个我们很依赖的维度是Token成本归因——按tenant标签拆分Token消耗,月底账单一出来,能直接说清楚"这个客户的Agent调用占了多少成本",而不是一锅粥地看总账单发愁。对做企业级产品来说,这个能力比看着挺酷的Trace树更有商业价值。

五、LangSmith vs LangFuse vs Phoenix,怎么选

说实话,我不觉得LangSmith是唯一选择,尤其是数据合规要求严的场景,自托管开源方案吸引力不小。这是我们实际选型时对比出来的表:

维度

LangSmith

LangFuse

Phoenix

开源/自托管

闭源SaaS为主

开源,自托管友好

开源,Arize出品

与LangChain集成

原生,最深

良好,需接入

基于OpenTelemetry

Prompt管理

Hub,成熟

有,功能相近

较弱,偏观测

框架绑定程度

弱绑定,SDK通用

弱绑定

几乎无绑定

数据合规/私有化

企业版支持私有部署

天然friendly

天然friendly

我们最终的选择是:开发阶段用LangSmith,生产环境自托管LangFuse——开发调试期,LangSmith的Trace树和Playground体验确实是最顺手的,团队协作也方便;但一旦涉及真实用户数据进入生产链路,数据不出内网这条线是硬要求,LangFuse基于OpenTelemetry协议、部署简单,迁移成本可以接受。如果你的团队完全没有用LangChain/LangGraph,纯自研Pipeline,Phoenix的OpenTelemetry原生方案反而更轻量,不会有"为了观测反过来被框架绑架"的顾虑。

六、告警与自动降级:分数掉线之后系统自己该干什么

监控看板解决"人能不能发现问题",但真正把损失降到最低的,是系统在人还没醒之前就自己先兜底。我们设计了一套简单的三级响应:

在线评分持续低于阈值?

轻微异常(连续5分钟低于阈值10%)→ 企微机器人告警,人工介入排查

中度异常(连续15分钟低于阈值25%)→ 自动回滚Prompt Hub到上一个production版本

严重异常(分数腰斩或大量报错)→ 自动切换到无检索的兜底Prompt,只用基座模型能力应答,保证服务不中断

中度异常那一档最能体现Prompt Hub解耦的价值——回滚Prompt Hub的版本标签,是一次纯配置切换,不需要重新发布服务,几秒钟就能完成,这在过去Prompt写死在代码里的年代是不可想象的响应速度。

一个容易被忽略的细节:在线评分本身也会消耗Token和引入延迟,别用最贵的模型当"实时裁判"给每一条线上请求打分,成本会失控。我们的做法是抽样(比如10%流量)实时打分,剩下的走异步批处理,兼顾及时性和成本。

这套自动降级机制跑了一个多月,真正触发"中度异常自动回滚"的次数不到五次,但每一次都省下了至少半小时的人工响应时间——对一个7×24小时对外服务的Agent系统来说,这半小时可能就是几十个用户体验的差异。

从这一篇往后,可观测性算是补齐了。但说实话,我现在越来越觉得,观测和评估这两件事其实应该是整个Agent系统设计阶段就要考虑的一等公民,而不是像我们这样吃了亏之后才补——如果重新设计一次,我会把@traceable写进每个函数的骨架模板里,而不是等出了故障才想起来埋点。下一篇我们要解决另一个越来越棘手的问题:知识库越做越大,对话越拉越长,Context窗口撑不住了,怎么在不丢关键信息的前提下把Token压下来。到时候见。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陆业聪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档