首页 / 资讯中心 / 文章详情

Agent排障实战:用LangSmith链路追踪看清每一次执行

Agent排障实战:用LangSmith链路追踪看清每一次执行 ★ FEATURED ARTICLE
搞Agent开发有一段时间了,最让我头疼的从来不是模型效果,而是排障。一个Agent在跑复杂任务时,可能要调用三四个工具、经历七八轮模型推理,一旦某个环节出了问题,传统print大法根本看不出是工具返回解析错了、上下文被截断了,还是模型自己走岔了路。直到我把链路追踪正式接入LangSmith,这个老大难问题才算真正被解决。这篇文章就是想把LangSmith这套链路追踪工具在Agent场景下的用法,从核心概念到实际排障,再到数据评估,完整地梳理一遍,给正在被Agent可观测性折磨的开发者一个可以直接落地的参考。1. 为什么说Agent开发最大的坑是执行链路不可见先聊一个反直觉的结论:Agent代码里的bug,大多数时候不是代码问题,而是看不见的问题。传统软件开发里,代码路径是固定的,if-else怎么写、函数怎么调,跑一遍就能大概猜到问题在哪儿。Agent不一样,它的执行过程是模型本身驱动的——模型根据当前上下文决定下一步调哪个工具、传什么参数,这意味着同一个输入,每次执行的路径可能完全不同。我见过太多调试现场:开发者对着终端里几百行日志,翻来覆去找到底哪一步出了问题,最后发现是第四轮模型调用时,把工具返回的长文本错误地塞进了系统提示词里,把原有指令挤掉了。这种问题,靠肉眼盯日志,效率极低。1.1 传统日志方案为什么在Agent场景下失效普通日志只记录发生了什么事,比如调用了search_weather工具返回了25度。但Agent出问题恰恰是为什么调用了search_weather而不是search_hotel为什么传入了错误的城市名为什么工具调用在第三轮失败后模型没有重试反而开始胡说。这些为什么,需要把整个决策上下文还原出来才能判断,而传统日志不会帮你记录。更麻烦的是复现问题。Agent的行为有随机性,同样的输入跑三五次,可能只有一次出错。你打了一堆debug日志抓现场,侥幸抓到了一次报错,却发现日志里只截断了最后那段,前几次关键的工具调用记录根本没打全。链路追踪工具之所以必要,是因为它天然就把一次完整执行的所有环节结构化保存下来,不用你事后补日志,追溯的是全过程。1.2 Agent链路追踪到底要追踪什么要理解LangSmith的价值,得先搞清楚Agent执行链路里有哪几层信息值得追踪。我把日常Agent开发中需要关注的链路数据归纳成四类:模型推理轨迹:每轮调用了哪个模型、输入了哪些消息、输出了什么内容、消耗了多少token、推理耗时多长。这是判断Agent思路是否跑偏的第一手资料。工具调用链路:Agent请求了哪个工具、传入了什么参数、工具返回了什么结果、返回结果有没有被正确处理。工具调用是Agent场景下最脆弱的环节,参数格式错、返回格式不符预期、超时,全是高频坑。内部状态流转:上下文消息列表在每一轮的增量变化、Agent状态(比如LangGraph里的State)在节点间的传递。很多隐蔽bug,比如上下文被意外覆盖、状态字段没更新,都藏在这一层。成本与性能指标:一次任务总消耗token数、各环节耗时、调用失败率。在生产环境,这直接关系到毛利用户还是亏损运营。LangSmith做的事情,就是把上面四层信息统一结构化,存成一个trace树,让你打开浏览器就能一层层点开看。2. LangSmith的核心建模:把agent执行拆成一颗trace树LangSmith的整套设计,核心就是一个叫Run的数据结构。理解Run,基本就理解了LangSmith一半。2.1 Run是什么:一次执行里最小的可观测单元在LangSmith的模型里,一次Agent执行(一个trace)是由若干嵌套的run组成的。每个run代表一个具体的执行步骤,类型包括chain、llm、tool、retriever、embedding等。举个例子,我的一个LangGraph Agent执行一次帮用户查天气并推荐穿搭任务,整个过程会产生这样的run层级:最外层通常是一个chain类型的root run,代表整个任务的执行子run是llm类型,代表模型第一次推理,输出需要查询天气再下层是tool类型的run,代表实际调用了search_weather工具,输入参数是城市名,输出是多云,18度工具结果返回后,又产生一个llm run,代表模型基于工具结果生成最终回答这个结构在LangSmith UI里展示,就是一棵层层嵌套的可展开树。每个run的关键字段包括:name:这个步骤的名称,比如chat_openai或search_weatherrun_type:步骤类型,chain/llm/tool等inputs / outputs:该步骤的输入输出快照start_time / end_time:耗时信息error:如果有异常,错误信息会挂在这里metadata:自定义附加信息,后面会专门讲2.2 Trace树与父子关系:决策路径的结构化表达理解run之间的父子关系是读懂trace树的关键。LangSmith里,子run通过parent_run_id关联到父run,而不是用嵌套对象内联保存。这个设计的价值在于,它天然支持并发场景——你的Agent可能同时发起多个工具调用,LangGraph的并行节点、或者你自己用asyncio.gather并发执行多个任务,每个子run都可以独立上报,只要带上正确的parent_run_id,UI上就会正确渲染成同一个父节点的多个分支。我在实际开发里特别受益于这一点。我的Agent在规划阶段会并行调用两个工具查询信息,以前用日志排查,并发输出会交错在一起,根本分不清哪个结果是哪个请求的。用LangSmith之后,每次并行调用的输入输出在UI里各自归属到自己的子run下,一眼就能对照起来。2.3 被大多数人忽略的metadata设计聊完基本的run模型,我想特别提一下metadata。这是LangSmith里特别实用但容易被忽略的设计。metadata是附加在run或trace上的一组自定义键值对,可以在创建run时通过metadata参数传入。我的习惯是无论跑什么任务,都会把这些信息挂在metadata上:业务维度:user_id、session_id、订单号、页面来源实验维度:prompt版本号、模型温度、使用的Agent版本环境维度:开发/测试/生产环境标识有了这些metadata,你在LangSmith的项目列表页就能按业务维度筛选、搜索trace。比如线上用户投诉某个订单的Agent处理错了,我可以直接按order_id搜索到那一条trace,精确回放那次执行。没有metadata,你只能在几百条trace里一条条翻,效率天差地别。3. 五分钟接入:把现有Agent变成可追踪体概念讲得再多,不如直接上手跑通。LangSmith接入的方式非常灵活,我按侵入程度从低到高介绍三条路线,你可以根据自己的技术栈选。3.1 准备工作:API Key、环境变量、SDK安装先到LangSmith官网注册账号,创建一个API Key。然后安装Python SDK:pip install -U langsmith如果你用的是LangChain或LangGraph,还要装对应版本:pip install -U langchain langgraph环境变量配置很简单,LangChain的回调机制会自动读取这些环境变量:export LANGCHAIN_TRACING_V2true export LANGCHAIN_API_KEYls_xxx export LANGCHAIN_PROJECTmy_agent_project这里有两个容易踩的细节。第一,LANGCHAIN_TRACING_V2必须显式设为true,只设API Key不设这个,数据不会上报。第二,LANGCHAIN_PROJECT可以理解为项目分组,不同环境、不同Agent建议分开命名,否则所有trace会堆在同一个地方,后续筛选成本很高。3.2 基于LangChain/LangGraph的零侵入接入如果你已经用LangChain或LangGraph搭Agent,那接入成本几乎为零。设好环境变量后,所有通过LangChain调用的LLM、工具、链,都会被自动采集并上报。我用LangGraph举一个最小示例。假设我已经定义了一个带工具调用的Agent:from langgraph.prebuilt import create_react_agent from langchain_openai import ChatOpenAI import os # 环境变量务必在构造模型前完成配置 os.environ[LANGCHAIN_TRACING_V2] true os.environ[LANGCHAIN_API_KEY] ls_xxx os.environ[LANGCHAIN_PROJECT] weather_agent_demo model ChatOpenAI(modelgpt-4o-mini, temperature0) tools [search_weather, recommend_clothing] app create_react_agent(model, tools) # 接下来的每次invoke都会自动生成完整trace result app.invoke({messages: [{role: user, content: 北京明天天气怎么样?适合穿什么?}]})运行之后,打开LangSmith项目页面,就能看到这条记录的完整trace树。模型推理、工具调用、参数传递、每一步的耗时和token消耗,全部可视化呈现。我实测下来,从安装依赖到跑通第一条trace,五分钟左右足够。3.3 不依赖LangChain时用原生Tracing手动打点如果你用的是CrewAI、AutoGen、自研Agent框架,或者干脆就是裸调OpenAI API,也没关系,LangSmith SDK提供手动打点能力。核心就是用langsmith.run_helpers里的traceable装饰器,或者直接创建Client手工上报run。用traceable是最简单的方式,直接在关键函数上加装饰器:from langsmith.run_helpers import traceable traceable(run_typechain, nameweather_agent) def run_agent(user_query: str): # 你的原始Agent逻辑 plan make_plan(user_query) weather search_weather(plan[city]) reply generate_reply(weather) return reply traceable(run_typetool, namesearch_weather) def search_weather(city: str): # 真实的天气查询逻辑 return f{city}明天多云,18~25度 result run_agent(北京明天适合穿什么?)traceable会自动把函数名、参数、返回值、调用链层级关系上报。装饰器会隐式传递context,只要子函数也被装饰,它就会自动挂在父函数的run下面,不需要手动维护run_id。这套方案对已有代码项目的侵入极小,我甚至把一些老项目里的关键函数加个装饰器,就能在不改业务逻辑的情况下把链路可视化出来。如果函数式装饰器不够灵活,还可以用底层的Client手动创建run:from langsmith import Client client Client() with client.trace( namemanual_agent, run_typechain, inputs{query: 测试}, ) as root_run: with root_run.create_child( namellm_call, run_typellm, inputs{messages: [...]}, ) as llm_run: result call_openai(...) llm_run.end(outputs{response: result}) root_run.end(outputs{final: result})手动打点虽然代码量略多,但胜在完全可控,适合那种需要自定义粒度、或者框架本身做了高度封装的场景。4. 实战排查:一次真实Agent幽灵Bug的完整复盘理论讲完,上一段实践。这个案例是我自己踩过的坑,现象诡异得一度让我怀疑是模型抽风,最后靠LangSmith的trace树定位到了真正原因。整个过程特别能体现链路追踪在排障中的价值,我完整复盘出来。4.1 症状描述与初步假设我的Agent业务是帮用户推荐附近餐厅并发起预订。某天有用户反馈:说推荐结果里出现了一家已经停业的餐厅。我的第一反应是数据源问题,检查了餐厅数据库,发现状态字段是正常的,这家店确实是营业状态。我准备在LangSmith里查一下这个用户的trace,按用户ID搜metadata:from langsmith import Client client Client() traces client.list_runs( project_namerestaurant_recommender, filter{user_id: user_12345}, )UI里找到对应记录后,我点开那棵trace树,立即看到了异常。整个执行分这么五层:第一层:llm run,模型接收用户请求,输出规划先搜索附近餐厅第二层:tool run,调用search_nearby_restaurants,传入参数{cuisine: 川菜, location: 望京}第三层:llm run,模型拿到搜索结果,输出推荐理由第四层:tool run,调用get_restaurant_detail,传入参数{restaurant_id: r_8823}第五层:llm run,产出最终推荐文案4.2 在Trace树里定位第一处异常一步步看下来,第一处可疑点出现在第二层的工具调用。search_nearby_restaurants返回的结果里,排在第一个的餐厅r_8823状态确实是营业中,但餐饮类型是火锅店,不是用户要求的川菜。继续往下看第四层,get_restaurant_detail接收r_8823后,返回了这家火锅店的详情和正在营业标记。到这一步,问题已经基本清楚:工具返回的营业状态是对的,但推荐本身就不符合用户意图——模型为什么放着搜索结果里更靠前、更匹配的川菜馆不选,偏偏挑了这家火锅店?我回到第三层那个llm run,点开它的输出,发现模型在选择餐厅时的推理文本里写着:根据搜索结果,r_8823是川菜馆,评分4.8,很合适。这里暴露了一个隐藏逻辑:模型不知道r_8823是火锅店,它是在误判的基础上继续行动的。4.3 揪出上下文截断的真凶那问题就变成:为什么模型不知道r_8823是火锅店?工具返回里明明写得很清楚。我再看第二层search_nearby_restaurants的输出,发现一个惊人的细节:这个工具返回了10家餐厅的完整信息,但LangSmith记录里显示,传给第三层模型的token里,餐厅列表的最后几个字段被截断了。具体来说,每家餐厅的cuisine_type字段在第五家餐厅之后就没了,后面几家全是{name: xxx, rating: 4.2, status: open}这样的残缺结构。原因找到了。我在代码里给search_nearby_restaurants的结果做了个简单的字符数截断,本意是防止超长结果塞爆上下文,但实现方式是粗暴截断,把后半段的cuisine_type字段切掉了。模型拿到的数据里,r_8823确实没有火锅店这个信息,它只能根据残缺数据脑补。这个bug如果没有trace树,我大概率会怀疑是模型问题或者工具返回问题,反复测试好几轮才能碰巧定位到是结果预处理环节的截断逻辑。但有了可视化链路,每一步的输入输出都在那里,顺着链路看下去,原因一目了然。4.4 修复与预防修复方案很简单,把粗暴截断改成结构化截断——先保证每家餐厅的关键字段完整,再按数量限制返回条数。这个案例给我最大的教训是:在Agent链路里,任何你自作主张的精简优化,都有可能悄悄破坏数据的语义完整性,而这种破坏很难靠表面测试发现。现在我的做法是:凡是对工具返回结果做了清理、截断、重排的地方,都要作为独立函数隔离出来,并用LangSmith打点,方便后续审查每次数据处理的实际效果。这个习惯帮我避开了很多类似的坑。5. 只追踪还不够:用数据集和评估器把经验沉淀下来链路追踪解决的是出了事能不能看清现场的问题。但光有现场还不够,一个更高级的问题是:你怎么保证修完的Agent不会在下一次改动里把老问题带回来。LangSmith在追踪之外提供的Dataset和Evaluator能力,就是为这个目的设计的。5.1 把真实用户的正确样本沉淀成回归数据集我的做法是:凡是线上运行中确认结果正确的trace,就拉到Dataset里作为回归样本。LangSmith支持直接从trace创建数据集,选中那条trace点Add to dataset就行。你可以在代码里维护一批典型的用户问题——覆盖各种边界场景,比如:常规问题:正常的、典型的用户请求边界输入:非常短的query、非常长的query、带特殊符号的query需要工具修正的:第一轮工具调用返回空结果、工具返回超时等异常情况多轮对话:需要结合历史上下文才能回答的请求官方SDK也支持用代码创建数据集和添加样本:from langsmith import Client client Client() dataset client.create_dataset( dataset_namerestaurant_agent_regression, description餐厅推荐Agent的回归测试集, ) client.create_examples( dataset_iddataset.id, inputs[ {messages: [{role: user, content: 推荐一下望京附近的川菜馆}]}, {messages: [{role: user, content: 帮我预订一家适合带孩子的餐厅}]}, ], outputs[ {restaurant_type: 川菜}, {features: [儿童友好]}, ], )5.2 用LLM评估器自动化批量打分有了数据集,评估阶段可以用Evaluator来打分。LangSmith里可以自定义评估函数,也可以直接用内置的LLM评估器。最简单的入门方式是写一个检查输出中是否包含关键字段的评估器:from langsmith.evaluation import evaluate from langsmith.schemas import Example, Run def check_cuisine_match(run: Run, example: Example) - dict: output_text run.outputs.get(output, ) expected example.outputs.get(restaurant_type, ) return { key: cuisine_match, score: 1 if expected in output_text else 0, comment: f期望包含 {expected}, } evaluate( lambda inputs: my_agent.invoke(inputs[messages]), datadataset_name, evaluators[check_cuisine_match], experiment_prefixregression_v2, )加入Evaluator评估的价值在于,每次改动Agent代码或换prompt时,先在数据集上批量跑一遍评分,数字一对比就知道改动是变好了还是变差了,不用靠感觉。这相当于给Agent开发加了自动化测试,我从第一次用就觉得这功能特别适合Agent这种很难写精确断言的项目。5.3 成本与延迟监控的实用配置生产环节还有一类重要但容易被忽视的指标:成本和延迟。LangSmith的项目概览页会展示该项目的平均耗时、成功率、Token消耗趋势。我现在的习惯是:每次发布新Agent版本前,先在数据集上跑回归,对比Token消耗是否有明显异常增长日常巡检时,按metadata里的environment字段筛选生产环境的trace,看成功率是否有波动对线上反馈异常的trace,顺手加个标注(annotation)备注原因,方便团队一起复盘一个偷懒技巧:我会定期用SDK拉取项目的run列表,按耗时排序看最慢的几条trace,逐个点开检查是哪个环节耗时最长。曾经发现某个工具接口响应特别慢,在trace里确认是上游服务性能问题,而不是Agent推理无限循环后,直接找对应团队优化即可,定位过程前后不到十分钟。6. 使用LangSmith的几点个人体会与实用建议最后分享几个我踩过坑之后的实用体会。第一,环境变量要在进程启动前配置好。尤其是Jupyter Notebook场景,如果你在创建LLM实例之后才设置环境变量,部分组件可能已经缓存了配置,导致trace不生效。我的习惯是第一时间、在代码文件的最顶部配置,或者直接在启动命令里注入。第二,注意控制trace的存储压力。LangSmith免费版有trace数量限制,个人开发小规模使用基本够。但如果你在调试循环里频繁跑测试,会产生大量低质量trace。我的做法是开发调试时用单独的项目名,不和生产项目混在一起;数据集回归时,跑完就批量清理无关run,避免项目页堆满垃圾数据。第三,metadata一定要从一开始就规划好。业务ID、用户ID、项目版本号、环境标识,这些字段一开始就挂上,后面筛选、搜索、跨团队协作才谈得上。等到线上出问题才想到要加,那之前的关键trace已经找不回来了。第四,重视trace隐私与敏感信息。LangSmith会把模型输入的完整内容上报到服务端,涉及用户隐私或商业敏感信息的场景,要提前做好脱敏处理。我一般会选择把query里明显的个人信息摘掉,再上报链路数据,避免把原始隐私数据完整暴露在观测平台里。LangSmith这套链路追踪体系,本质上解决的是Agent开发里不可见的难题。当你把一次执行拆成一层层清晰可见的run,把这棵trace树融入日常开发和排障流程,很多以前要猜、要试、要碰运气才能定位的问题,都会变成打开浏览器点几下就能确认的例行工作。对正在做Agent应用、尤其是已经进入生产环境的团队来说,这套能力不只是一个调试工具,更是一套值得尽早建立起来的开发和运维基础设施。
阅读完成 · 觉得有帮助?
咨询建站