我不知道你们有没有过这种经历:本地调一个 LangChain 智能体,跑起来很顺,工具调用一次到位,结果部署到测试环境之后就开始抽风。同样的输入,一会儿调这个工具,一会儿调那个工具,连报错都带随机性。我一开始是靠 print 大法和各种日志硬啃,直到我把 LangChain 项目的追踪链路公开分享给远程协作者之后,才真正意识到——智能体开发这个事,单靠"看代码"是看不出名堂的,你必须能看到每一次运行的完整轨迹,才能定位问题出在模型、出在工具、还是出在上下文拼接。
这篇文章就围绕 LangChain 智能体开发中的"追踪"展开,重点聊两个很多人忽视的操作:公开分享追踪记录,以及取消分享。你会在里面看到我把一个 agent 的 trace 链接丢给同事后发生了什么,也会看到我不小心把内部信息暴露在公共链接里之后的补救过程。如果你正在用 LangChain 做 agent 开发,或者被多步工具调用的调试搞得焦头烂额,这篇内容应该能帮你少走不少弯路。
1. 为什么 LangChain 智能体开发离不开"追踪"这一环
智能体开发跟传统后端开发最大的区别在于:传统后端函数的输入输出是确定的,出 bug 了跑一遍单测基本能复现;但 LangChain 智能体的行为由大模型动态决策,同样的输入,LLM 这次决定调 A 工具,下次可能决定调 B 工具,甚至可能决定不调工具直接给答案。这种不确定性让"复现 bug"变成了一个伪命题——你无法保证下一次运行会走同一条路径,因此也无法用传统的方式去定位问题。
我在一个内部知识库问答项目里做得最痛苦的一件事,就是排查 agent 为什么偶尔会跳过检索工具直接回答。代码逻辑看起来完全没问题:系统提示词里明确写了"必须先调用检索工具",工具列表里也正确注册了检索函数。但实际跑起来,10 次里总有那么一两次,LLM 就是"自作主张"直接生成答案。后来我发现,这个问题的根源是模型对系统提示词的遵循度并不绝对,加上某些用户 query 本身带有强烈的语气暗示,模型就倾向于直接回答。这个结论,单靠看代码是永远看不出来的,你必须把实际的运行轨迹翻出来看。
所谓追踪,在 LangChain 生态里就是指把每一次 agent 运行的完整过程记录下来:输入是什么、模型输出了什么中间结果、调用了哪个工具、工具返回了什么、最终输出是什么。LangChain 官方提供的 LangSmith 平台就是干这个的,它会把一次 agent 运行记录成一棵"运行树"(Run Tree),根节点是整个 agent 的执行,子节点包括 LLM 调用、工具调用、链(Chain)调用等。
- 根节点:AgentExecutor 或 Agent 的整体运行
- 子节点:每次 LLM 调用(含 Prompt、Response、Token 数)
- 孙节点:每个 Tool 的调用(含入参、出参、耗时、错误信息)
我认识不少做智能体开发的工程师,一开始嫌麻烦不接追踪平台,觉得"反正本地能跑通就行"。但一旦 agent 开始接入真实业务,涉及多个工具、多个步骤、多轮对话,没有追踪几乎等于闭着眼睛开车。尤其是工具调用链比较长的时候,比如"查数据库→调 API→算结果→再调另一个 API",中间任何一个环节出错,整个链路就断了。追踪的意义不仅仅是事后排查,它还能让你看到 LLM"为什么会这样做"——这是改进提示词和工具设计的第一手依据。
LangSmith 的追踪能力还有一个容易被忽视的点:它可以记录到单个 token 级别的延迟分析,告诉你 LLM 响应慢是因为输入太长、还是模型本身推理慢、还是工具返回的数据量太大。这些细粒度的数据,平时靠手工观察根本拿不到,但对于优化 agent 性能却是刚需。
所以我的建议是:只要是 LangChain 智能体项目,从第一行代码开始就把追踪接上。具体来说,设置两个环境变量就能搞定:
export LANGCHAIN_TRACING_V2=true export LANGCHAIN_API_KEY=ls_xxx固定住这两个变量之后,你的每一次 agent 运行都会自动出现在 LangSmith 的项目(Project)下面,按时间排好。到这一步,追踪只是"给自己看",接下来要说的才是重点——怎么把追踪分享给别人,以及分享出去之后怎么收回来。
2. 先搞明白:一条追踪记录里到底藏了多少信息
在把追踪链接甩给任何一个人之前,你一定要先搞清楚一条 trace 里能看到什么。我见过太多人,以为分享追踪只是分享一个"运行概览",结果对方点开链接之后,Prompt、工具入参、API 返回全文全暴露了。这在内部协作时问题不大,但如果链接被转发到外部,那就成了事故。
一条典型的 LangChain agent trace 包含以下几类信息:
| 信息类别 | 具体内容 | 风险等级 |
|---|---|---|
| 输入参数 | 用户的原始 query、系统提示词、外部变量 | 中 |
| LLM 中间输出 | 模型的完整响应、思考过程、ReAct 推理文本 | 中高 |
| 工具调用数据 | 工具名称、入参、出参、错误堆栈 | 高 |
| 元数据 | 模型名称、温度、token 用量、延迟、时间戳 | 低 |
| 环境信息 | 项目名、运行 ID、数据标注标签(Human feedback) | 低 |
这里风险最高的是工具调用数据。我在一个对接第三方支付服务的 agent 项目里发现,工具返回的 JSON 里经常包含订单号、手机号、甚至部分支付流水信息。虽然我们内部约定工具出参只保留必要字段,但有一次后端同事偷懒,直接把整个 API 响应对象抛给了工具,trace 里就完整记录了一条真实的用户支付流水。当时那条 trace 的关键信息还没被分享出去,但已经足以让我警醒:每一个工具的出参,都必须当成"会被公开展示"来设计。
更隐蔽的是 LLM 中间输出。你可能觉得模型"思考过程"无所谓,不就是一堆推理文本吗?但 ReAct 格式的中间输出里,模型会"自言自语"地描述它正在查看的工具返回,有时候甚至会复述敏感字段,比如"用户的邮箱是 xxx,接下来我要用这个邮箱查询订单"。这些内容全都会被记录在 trace 里,并且展开之后一目了然。
所以,在考虑"公开分享追踪"之前,先做三件事:
- 检查工具返回数据是否可能包含敏感字段,如果有,在代码层做截断或脱敏
- 设置 LangSmith 的隐藏规则(Hidden Rules),把包含特定关键词的输出自动打码
- 用一个非生产账号跑几条测试用例,亲自点开 trace 检查一遍
LangSmith 的隐藏规则配置起来很简单,在项目的 Settings 里可以添加基于正则表达式的字段屏蔽。比如我想屏蔽所有疑似邮箱的内容,就加一条规则:
pattern: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}这样在 trace 输出里,匹配到的内容会被替换为[hidden]。这个功能我从第一次事故之后就再也没关过,宁可误伤一些正常字段,也不能让敏感信息裸奔。
顺带说一句,如果你分享追踪链接是为了在技术社区提问或者发 issue,信息暴露风险就更高了。公开链接一旦发出去,搜索引擎的爬虫甚至可能收录它。我在 Edinburgh 的一个技术群里见过有人发 LangSmith 公共链接问问题,两个月后那个链接居然还能被搜索引擎检索到,里面的数据也还原封不动地挂着。你可以在 LangSmith 的分享设置里看到"Allow public access"选项,但这只管链接是否可访问,管不了搜索引擎的索引行为。所以严谨的做法是:问完问题、拿到答案之后,立刻取消分享。
3. 公开分享追踪:把一条 trace 变成一张"现场照片"
LangChain 智能体开发到后期,往往是多人协作。你遇到一个奇怪的工具调用行为,想要问同事"这里为什么要这么调",如果只靠文字描述,很容易遗漏上下文。但如果你丢过去一条公开分享的追踪链接,对方点开就能看到完整的调用链,一眼定位问题。这就是公开分享追踪的核心价值:把一次不可复现的运行,变成一张随取随用的"现场照片"。
LangSmith 公开分享操作路径是这样的:
- 进入任意一条 trace 的详情页
- 在右上角找到 "Share" 按钮
- 点击后 LangSmith 会生成一个
app.langsmith.com/public/xxxx格式的链接 - 点击 "Copy link" 复制分享链接
这个链接默认是公开的、无需登录即可查看,任何拿到链接的人都能看到完整 trace。LangSmith 还允许你设置链接的有效期,比如 24 小时、7 天或者永久有效。我的习惯是默认选 7 天,防止陈旧链接在互联网上长期存在。
分享之后,对方打开链接会看到什么?我可以用一次典型的 ReAct agent 运行来说明。假设你的 agent 收到了一个问题:"帮我查一下张三上周的订单总额",一次运行会展开成这样的树:
- AgentExecutor(根节点)
- LLM 调用:系统提示 + 用户提问
- 工具调用:search_orders(query="张三", date_range="上周")
- 工具返回:JSON 数组,包含 5 条订单,总金额 3421 元
- LLM 调用:根据工具返回生成最终答案
对于协作者来说,他不需要在本机跑任何代码,只需要看着这棵树,就能判断问题出在哪个环节:是 LLM 没调工具?是工具参数传错了?还是工具返回结果被模型误解了?这种可视化诊断的效率,比在微信里发几十条代码截图高出不止一个量级。
我还有一个用法:把公开分享链接作为 bug 报告的附件。以前同事跟我报 bug,都是"输入 xxx 的时候,返回了 xxx,我怀疑是工具的问题"。后来我定了规矩,报 bug 必须附 trace 链接,否则不排查。有了链接,我可以直接看到模型每一步的推理过程,根本不用问"是不是复现了",因为 trace 本身就是复现记录。配合 LangSmith 的反馈机制(在 trace 上可以直接标"对"或"错"),我还能把链接作为标注数据,为后续微调或者评估集的建设积累素材。
如果说有什么要注意的,那就是分享链接的时效意识。LangSmith 的"永久公开"选项,你最好永远不要选。我踩过一次坑:一个半年前分享出去的 trace 链接,某天突然被同一个项目的客户无意中点开,结果对方直接在邮件里问我们"为什么你们内部工具返回里有一个未落地的表结构"。那之后我把所有永久链接全部作废,统一改成 7 天有效期。这个习惯,算是用一次不大不小的尴尬换来的。
另外,如果你想分享的是整个项目(Project)而不是单条 trace,要注意区分。LangSmith 里 Share 按钮在项目级和 trace 级都有,项目级分享意味着该项目的所有新 trace 都会自动公开可见,这是一把双刃剑:用于给团队做实时演示非常方便,但如果你忘了关,以后所有调试信息都会被外部看到。我用过一次项目级分享来做客户 demo,演示完当天就立刻关闭了,这种分享方式适合"临时开、及时关"的场景,不建议长期开着。
4. 取消分享与权限回收:按下去的后悔药
公开分享追踪的功能让我爽了很久,直到那次事故之后我才开始认真研究"取消分享"。LangSmith 的取消分享操作藏在哪?其实就在生成分享链接的那个位置——同一个 Share 按钮,点开之后会变成 "Manage link",进去之后有 "Disable public link" 的选项。点击之后,这个链接立刻失效,任何通过原链接访问的人都会看到 "Access denied" 或 "This shared run is no longer available"。
这里有个细节值得注意:取消分享之后,如果对方浏览器里已经打开了那个页面,他还能不能继续看?实测下来,如果页面已经完整加载,取消分享不会主动清除已经渲染出来的内容。也就是说,对方已经看到的信息是撤不回来的——这也再次说明了分享前审查的重要性。但如果是取消之后才点开链接,那就一定会被拦下来。
取消分享还有一种情况:你删除了包含这条 trace 的项目。LangSmith 里删除 Project 的话,下面所有运行记录都会被清理,分享链接当然也一起失效。但要注意,如果你只删除了某一条 trace,而它所在的 Project 还在,分享链接是否会失效取决于链接是针对 trace 还是针对 project。针对单个 trace 的链接,删除 trace 后立即失效;如果是 project 级链接,删除其中一条 trace 不影响整个 project 链接的可访问性,这点我实测过。
除了手动取消,LangSmith 还支持通过 API 管理分享状态。比如在 CI/CD 流程里,跑完测试之后自动生成一批 trace,然后批量分享给质量团队验收,验收结束后再自动取消。官方 API 提供对应的接口,可以用下面这类方式调用:
from langsmith import Client client = Client() # 获取某个运行记录 run = client.read_run("your_run_id") # 检查是否已有公共链接 if not run.share_token: share_url = client.share_run(run.id) print(f"Shared: {share_url}") # 稍后取消分享 client.unshare_run(run.id)这个能力看起来不起眼,但放到工程化流程里就很值钱。我目前的一个做法是:每次跑完一批 agent 回归测试,自动把失败的用例对应的 trace 分享出来,生成一个汇总表,发给团队群里。等下一个迭代的测试结果出来之后,再自动取消上一批的分享。这样既方便协作,又不会让链接堆积成隐患。
取消分享还有一个容易忽略的环节:如果你在 LangSmith 的反馈标记页面上给某条 trace 打了标签或写了评论,取消分享之后这些标记也会一并对外隐藏吗?实测结果是:取消公共分享后,标记和评论不会出现在公共页面上,因为整个页面都已经不可访问了,不存在"部分隐藏"的问题。但如果你只是切换了权限模式(比如从 public 改成 organization-only),反馈内容仍然保留对内部可见。所以取消分享和调整权限范围,是两个不同的操作,别混淆。
至于为什么很多开发者不爱用取消分享——我自己的感受是,分享太顺手了,顺手到根本不会想"这玩意儿怎么收回"的问题。LangSmith 的 UI 把 Share 按钮做得特别显眼,Disable 的入口却相对隐蔽,这种不对称设计导致大量"僵尸公共链接"存在。现在我在团队里推行了一个小制度:每次分享之前,先在评论区里写下"这条 trace 为什么分享、预计什么时候关闭",给自己一个提醒。别小看这个习惯,它让我清理链接的主动性高了很多。
5. 追踪分享在团队协作中的几种实际打开方式
文本描述得再多,也不如给几个真实场景。分享一下我目前在 LangChain 智能体开发里,公开分享/取消分享追踪的几种固定用法。如果你也在做 agent 类项目,可以直接照搬。
场景一:外包协作和跨团队排查
我们有一个语音助手 agent,一部分工具是外部团队开发的。对方在调试接口匹配时,经常需要看我们 agent 到底传了什么参数过去。以前的做法是打日志、导文件、发压缩包,效率低且容易出错。现在方案简单了许多:我们跑一条测试用例,把 trace 公开分享给到对方,他们在链接里直接核对工具入参和出参,如果发现参数映射不对,再反手分享回他们的调用链。一来一回之间,链接成了双方的"共同语言"。
场景二:开源项目和技术社区提问
在 GitHub issue 或者技术社区里发 LangChain 相关问题,没有比附上 trace 链接更直接的方式了。提问格式我一般是这样:先简单描述问题和目标,然后贴出 trace 链接,说明"这是我跑了三次之后的其中一条失败轨迹"。回答者不需要任何前置环境,点开链接就能看到模型每一步的调用记录,很多问题一眼就能判断是 prompt 写得有歧义,还是工具返回结构不对。但这里再次强调,公开提问意味着信息完全公开,分享前记得把 trace 里的敏感字段处理干净,提问完尽快取消分享。
场景三:版本回归对比
LangChain 智能体在改 prompt 或者换模型之后,行为会发生明显变化,但"变化是否变好了"需要对比。我们可以把旧版本某次运行的 trace 分享出来,再分享一条新版本的,两者放在一起对比工具调用路径的差异。比如旧版本调用了 3 个工具才给出答案,新版本直接 1 个工具搞定,那改进就非常直观。这个对比在我们做 agent 性能优化复盘时特别常用,比截一堆聊天记录清晰多了。
场景四:给非技术角色看"智能体到底做了什么"
这个场景比较小众,但我觉得值得一说。我们给产品经理演示 agent 的流程时,光说"它会自己查询数据然后总结"不够有说服力。你直接把一条 trace 的公开链接发过去,让他在浏览器里展开那棵运行树,看到模型怎么一步步调用工具的,比任何 PPT 都直观。看完再取消分享,干净利落。如果产品经理愿意给你在 trace 上点一个"正确"的反馈标记,那还能直接变成数据集的标注结果,一举两得。
这几个场景里,分享和取消分享的节奏都很重要。我个人的经验是:所有分享链接都必须设置到期时间,即使 LangSmith 默认允许永久分享,我也不会选择它。公开分享是低频操作,取消分享是必须养成的肌肉记忆——就像你发了朋友圈会定期检查谁还能看到一样,trace 链接也需要定期体检。我大概每两周会去 LangSmith 的分享管理页里翻一次,把所有不用的公共链接全部关掉。
6. 追踪信息脱敏:分享之前必须养成的肌肉记忆
接着上面说的敏感信息问题,单独展开讲一下脱敏。做智能体开发的人,最容易犯的一个错误就是把 trace 当成"开发调试记录",觉得只有自己能看到。但一旦走了公开分享流程,这条记录就变成了可以在互联网上被任何人访问的页面。你在本地的 print 输出里看到手机号无所谓,但在公共链接里看到手机号就是事故。
LangSmith 本身提供了一些基础脱敏机制,但我强烈建议不要完全依赖它。最可靠的做法是在应用代码层就控制工具的输出内容。我给自己定了一条规则:凡是工具函数返回的数据,都经过一个 sanitize 函数统一清洗,把邮箱、手机号、身份证号、银行卡号等字段替换成掩码格式。这里的逻辑很简单——工具返回的数据不是给用户看的,而是给模型中间推理用的,掩码之后对推理影响不大,但规避了信息泄露的大头。
我做了一个非常简单的清洗逻辑,用的就是正则替换:
import re SENSITIVE_PATTERNS = { "email": re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"), "phone": re.compile(r"1[3-9]\d{9}"), "id_card": re.compile(r"\d{17}[\dXx]"), } def sanitize_output(text): if not isinstance(text, str): return text for name, pattern in SENSITIVE_PATTERNS.items(): text = pattern.sub(f"[{name}_hidden]", text) return text这个函数我放在工具调用的边界上,所有工具返回值先过一遍 sanitize_output 再返回给模型。这样 trace 里记录的就是清洗后的数据,就算分享出去,敏感字段也全部被替换了。有同事问我,这样会不会影响模型的分析效果?实测下来影响非常有限,因为大部分敏感字段本来就不参与核心逻辑判断,比如查询订单总金额的时候,模型根本不需要看到完整的手机号。
除了工具输出,Prompt 本身也可能包含敏感信息。比如你的系统提示词里拼接了用户信息,这些内容也会被 LangSmith 完整记录。这种情况我建议直接用 LangSmith 的隐藏规则处理,而不是改代码逻辑。因为 Prompt 里的变量往往是动态拼接的,你无法在源头逐条清洗干净,用平台的隐藏规则做兜底更现实。
隐藏规则的操作路径是:LangSmith 项目 Settings → Hidden Rules → 添加正则模式。我之前就配了一条把所有sk-[a-zA-Z0-9]{20,}格式的内容替换掉——这是 OpenAI API Key 的常见格式。凡是命中的内容,在 trace 页面里都会显示为[hiddden],分享出去也不怕 API Key 泄露。
最后再补充一句关于分享链接生命周期的管理。LangSmith 在最近的版本里,对公共链接的管理入口做了一些调整,分享设置面板里可以看到当前有效的活跃链接列表,并且支持一键批量关闭。我用的策略很简单:凡是超过 30 天没有被访问过的公共链接,全部取消分享,不管当时是因为什么目的分享出去的。这个方法可能有点粗暴,但对于控制信息暴露面来说,宁缺毋滥。
7. 实测总结:分享追踪的正确节奏与最终体会
聊到这里,公开分享和取消分享追踪的完整逻辑应该算是理清了。最后分享一下我现在的固定操作流程,也算是对这篇内容的一个收束。
跑 LangChain 智能体项目时,我每一轮调试基本按照这个节奏走:
- 本地环境统一开启 LangSmith 追踪,所有运行自动落库
- 需要外部协作者帮忙看问题时,才生成分享链接,有效期默认 7 天
- 分享前先在本地把 trace 从头到尾展开检查一遍,确认没有敏感信息
- 问题解决之后,立即进入分享管理页面取消对应链接
- 每两周清理一次所有存量公共链接,把超过 30 天未访问的全部回收
这个流程看起来简单,但真正执行下来,你会发现"分享"这件事从一个随手操作变成了一种可控的协作工具。我现在的体会是:追踪本身已经很成熟了,但大部分团队的痛点不在于"没有追踪",而在于"追踪了但不知道如何安全有效地分享出去、收回来"。公开分享让你能把不可复现的问题抛给任何人一起看,取消分享则确保了这个过程始终在你的控制范围之内。
LangChain 智能体开发的复杂度,决定了你一定会遇到自己看不出来的问题。与其在本地对着代码发呆,不如把一次运行记录变成一个链接,递给一个愿意帮你点开的人。看完、解决、关掉,这个闭环,就是智能体开发里最值得养成的调试习惯之一。