文章目录
- 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 写不花钱的单测。