ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LangSmith 实战:链跑错了,怎么查

LangSmith 实战:链跑错了,怎么查
个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <

文章目录

  • LangSmith 实战:链跑错了,怎么查
    • 一、它解决什么问题
    • 二、五分钟接上
      • 2.1 拿到 key
      • 2.2 配环境变量
      • 2.3 跑一次就有数据
    • 三、让 trace 更好查:tags 和 metadata
    • 四、给步骤起名字
    • 五、数据集与评测
      • 5.1 从 trace 加进数据集
      • 5.2 跑评测
    • 六、线上采样与隐私
      • 6.1 全量追踪太贵怎么办
      • 6.2 敏感数据
    • 七、不用 LangSmith 的替代方案
    • 八、常见坑
    • 九、小结

LangSmith 实战:链跑错了,怎么查

链一嵌套,出错就成了黑盒:
只知道最终结果不对,不知道是哪一步错了、当时的输入是什么。

LangSmith 就是解决这个的——把链里每一步的输入输出都录下来,
像调试程序一样调试 LLM 应用。

一、它解决什么问题

没有 LangSmith 时,你的排查方式是:

print(inputs)print(intermediate_1)print(intermediate_2)print(result)

手动插桩,改一次代码插一次 print,非常低效。

有了 LangSmith:

  • 自动记录每一步(哪个 prompt、传了什么、返回什么、几个 token、耗时);
  • 可视化成一棵树,一眼看到哪一步慢、哪一步错;
  • 可以重跑:改了 prompt 后,用历史数据重新跑一遍对比效果;
  • 可以做数据集和评测。

二、五分钟接上

2.1 拿到 key

去 smith.langchain.com 注册,创建一个 API key。

2.2 配环境变量

exportLANGCHAIN_TRACING_V2=trueexportLANGCHAIN_API_KEY=lsv2_xxxexportLANGCHAIN_PROJECT=my-rag-app# 项目名,随意

Windows:

$env:LANGCHAIN_TRACING_V2="true"$env:LANGCHAIN_API_KEY="lsv2_xxx"$env:LANGCHAIN_PROJECT="my-rag-app"

或者在代码里设(要在 import 之前):

importos os.environ["LANGCHAIN_TRACING_V2"]="true"os.environ["LANGCHAIN_API_KEY"]="lsv2_xxx"os.environ["LANGCHAIN_PROJECT"]="my-rag-app"

2.3 跑一次就有数据

chain.invoke({"question":"报销标准是多少?"})

打开 LangSmith 网页,就能看到这次调用的完整 trace:

RunnableSequence (1.8s) ├── ChatPromptTemplate (0ms) │ └── input: {question: "报销标准是多少?"} │ └── output: [SystemMessage..., HumanMessage...] ├── ChatOpenAI (1.7s) │ └── input: [完整消息列表] │ └── output: "根据上下文…" │ └── tokens: 245 (in 200 / out 45) └── StrOutputParser (0ms)

每一步的输入、输出、耗时、token 全都有。

三、让 trace 更好查:tags 和 metadata

默认的 trace 只有数据,不好筛选。加上标记:

chain.invoke({"question":q},config={"tags":["rag","v2.1","tenant-a"],"metadata":{"user_id":"u-123","session_id":"s-456","doc_version":"2024",},},)

然后在网页上就能:

  • 按tenant-a筛出某个租户的所有调用;
  • 按v2.1对比新旧版本的效果;
  • 搜user_id=u-123找到用户投诉的那次调用。

强烈建议生产环境每次调用都带——
这是"用户说答错了"时唯一的定位手段。

四、给步骤起名字

复杂链里,默认名字可能是RunnableLambda、RunnableParallel,
看不出是哪一步。手动命名:

fromlangchain_core.runnablesimportRunnableLambdadefretrieve_fn(x):returnretriever.invoke(x["question"])retrieve_step=RunnableLambda(retrieve_fn,name="检索文档")defformat_fn(docs):return"\n\n".join(d.page_contentfordindocs)format_step=RunnableLambda(format_fn,name="格式化上下文")chain=({"docs":retrieve_step,"question":RunnablePassthrough()}|RunnableLambda(lambdax:{"context":format_step(x["docs"]),"question":x["question"]},name="组装输入")|prompt|llm|StrOutputParser())

trace 里就显示成"检索文档 → 格式化上下文 → 组装输入",一目了然。

或者用with_config:

step=my_lambda.with_config(run_name="检索文档")

五、数据集与评测

LangSmith 最有价值的进阶功能:把真实调用沉淀成测试集。

5.1 从 trace 加进数据集

在网页上看到一次满意的回答,点"Add to Dataset",
就存成一条{input, expected_output}。

攒够 20~50 条,你就有了真实分布的测试集——
比自己拍脑袋编的问题靠谱得多。

5.2 跑评测

fromlangsmithimportClientfromlangsmith.evaluationimportevaluate client=Client()defcorrectness(run,example):"""自定义评分函数"""pred=run.outputs.get("output","")expected=example.outputs.get("answer","")# 用 LLM 判断,或用字符串匹配return{"score":1ifexpected.strip()inpredelse0}results=evaluate(lambdainputs:{"output":chain.invoke(inputs)},data="my-dataset",evaluators=[correctness],experiment_prefix="v2.1",)

输出的对比表格能直接看到"改了 prompt 之后,哪些问题变好了、哪些变差了"。

这正是前面评测那篇讲的"放进 CI"——LangSmith 帮你把测试集管理、
结果对比可视化做好了。

六、线上采样与隐私

6.1 全量追踪太贵怎么办

生产环境调用量大,全录会占空间。可以按比例采样:

importrandomdefbuild_config(user_id):cfg={"metadata":{"user_id":user_id}}ifrandom.random()<0.1:# 只采样 10%cfg["callbacks"]=[]# 走全局 traceelse:# 关掉这次的追踪...

更简单的方式是用 LangSmith 自带的采样率配置(企业版)。

6.2 敏感数据

trace 会把完整的 prompt 和回复上传到 LangSmith 服务器。
如果你的数据含个人信息、商业机密,要注意:

fromlangchain_core.callbacksimportBaseCallbackHandlerclassRedactingTracer(BaseCallbackHandler):"""上传前脱敏"""defon_llm_start(self,serialized,prompts,**kwargs):# 只记录长度,不记录内容self._len=sum(len(p)forpinprompts)defon_llm_end(self,response,**kwargs):logging.info(f"llm call, prompt_len={self._len}")

或者只在预发环境开 LangSmith,生产环境用自己的日志系统。

七、不用 LangSmith 的替代方案

如果公司不允许数据外传,可以自己搭:

importjsonimporttimefrompathlibimportPathclassFileTracer(BaseCallbackHandler):def__init__(self,path="traces.jsonl"):self.path=Path(path)def_write(self,record):withself.path.open("a",encoding="utf-8")asf:f.write(json.dumps(record,ensure_ascii=False)+"\n")defon_chain_start(self,serialized,inputs,*,run_id,**kwargs):self._write({"t":time.time(),"run_id":str(run_id),"event":"start","name":serialized.get("name"),"inputs":str(inputs)[:2000]})defon_chain_end(self,outputs,*,run_id,**kwargs):self._write({"t":time.time(),"run_id":str(run_id),"event":"end","outputs":str(outputs)[:2000]})defon_chain_error(self,error,*,run_id,**kwargs):self._write({"t":time.time(),"run_id":str(run_id),"event":"error","error":str(error)})

核心是"每一步都记下来",用文件、ES、还是 LangSmith 只是载体不同。
哪怕只是把 trace 写进日志文件,排查效率也比 print 强十倍。

八、常见坑

现象原因解法
网页上看不到 trace环境变量没设对检查LANGCHAIN_TRACING_V2=true(注意是 V2)
设了环境变量还是没有在 import 之后才设必须在 import langchain 之前
trace 里看不出是哪一步没起名字用name=或run_name
找不到某次调用没带 metadata每次调用带 user_id / trace_id
数据传出去不合规开了全量追踪脱敏或改用本地日志

九、小结

  • 三个环境变量接上:LANGCHAIN_TRACING_V2=true、
    LANGCHAIN_API_KEY、LANGCHAIN_PROJECT,必须在 import 前设;
  • trace 记录每一步的输入、输出、耗时、token,可视化成树;
  • 每次调用带tags和metadata(user_id、版本号),
    这是定位线上问题的唯一手段;
  • 给关键步骤起名(name=/run_name),否则 trace 里全是 RunnableLambda;
  • 把真实调用沉淀成数据集,改动后跑评测看哪些变好哪些变差;
  • 敏感数据要脱敏或改本地日志;核心是"每步都记",载体不重要。

下一篇讲测试——怎么用 Fake LLM 写不花钱的单测。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进