1. 本章目标 #

第 29 章说清楚了「为什么要看」,这一章解决「怎么看得见」。

好消息是接入极简单——三行环境变量,一行代码都不用改。坏消息是,接入之后你多半会遇到一两个让人摸不着头脑的问题:界面上一条数据都没有、或者报一个 401 Invalid token 但密钥明明是对的。本章会把这些坑连同它们的排查过程一起讲完。

学完你应能:

前置依赖: 第 2 章(create_agent)、第 29 章(Trace / Run / Span 三个词)。

参考文档:

1.1 本章统一环境 #

langchain        1.3.18
langchain-core   1.6.1
langgraph        1.2.11
langsmith        0.12.1
模型             deepseek-v4-flash

本章所有结论都在这套环境下实测。特别是第 4 节那三个「常见说法」,实测结果和多数教程写的不一样,请以本章的实测为准,并在你自己的版本上复验。


2. 最小接入:三行环境变量 #

先注册账号拿密钥:打开 smith.langchain.com,注册后在 Settings → API Keys 里创建一个,格式是 lsv2_pt_ 开头的长字符串。

然后在项目根目录的 .env 里加三行:

# 总开关。只有它是 true 时才会上报
LANGSMITH_TRACING=true

# 你的密钥。注意是 LANGSMITH_ 前缀,不是 LANGCHAIN_
LANGSMITH_API_KEY=lsv2_pt_你的密钥

# 数据落到哪个项目。不填就落到名为 default 的项目
LANGSMITH_PROJECT=my-first-project

第四行是可选的,自建部署或换区域时才需要:

# 官方云默认就是这个地址,用官方云可以不写
LANGSMITH_ENDPOINT=https://api.smith.langchain.com

业务代码一行都不用改。 拿第 2 章那个 Agent 原封不动跑一遍:

# 读 .env。注意这一行的位置很关键,第 5 节会专门讲
from dotenv import load_dotenv

load_dotenv(override=True)

from langchain.agents import create_agent
from langchain_core.tools import tool


# 工具一:查订单
@tool
def get_order(order_id: str) -> str:
    """按订单号查询订单状态。"""
    import time
    time.sleep(0.4)          # 模拟数据库延迟,让 Trace 里的耗时看得出来
    data = {"A1001": "已发货,预计 3 天到达", "A1002": "待付款"}
    return data.get(order_id, f"未找到订单 {order_id}")


# 工具二:查政策
@tool
def get_policy(topic: str) -> str:
    """查询售后政策。topic 可选:退货 / 换货 / 发票。"""
    return {"退货": "签收后 7 天内可无理由退货",
            "换货": "质量问题 30 天内可换",
            "发票": "支持开具电子发票"}.get(topic, "未找到该政策")


agent = create_agent(
    model="deepseek:deepseek-v4-flash",
    tools=[get_order, get_policy],
    system_prompt="你是电商客服,用中文简洁回答。需要数据时调用工具。",
)

# 就这么调,跟没接 LangSmith 时完全一样
out = agent.invoke({"messages": [
    {"role": "user", "content": "订单 A1001 到哪了?另外多久内能退货?"}]})
print(out["messages"][-1].content)

跑完打开 smith.langchain.com,在项目列表里点进 my-first-project,就能看到第一条 Trace。

2.1 第一条 Trace 长什么样 #

界面上看到的树,用 API 拉回来是这样(这是实测输出,不是示意):

[chain ] LangGraph                   2.552s   1188tok
    [chain ] model                       1.409s    555tok
        [llm   ] ChatDeepSeek                1.342s    555tok
    [chain ] tools                       0.403s
        [tool  ] get_order                   0.401s
    [chain ] tools                       0.003s
        [tool  ] get_policy                  0.001s
    [chain ] model                       0.610s    633tok
        [llm   ] ChatDeepSeek                0.608s    633tok

共 9 个 span,run_type 分布: {'chain': 5, 'llm': 2, 'tool': 2}

对照第 29 章的概念检查一遍:

根 Run 上的汇总信息:

status       = 'success'
latency      = 2.552s
total_tokens = 1188 (prompt=1038, completion=150)
total_cost   = 6.43888e-05

接入到此为止。 剩下的内容都是让它更好用、以及避开那些让你怀疑人生的坑。


3. 三个环境变量各管什么 #

变量 作用 不设会怎样
LANGSMITH_TRACING 总开关,只认 true 不上报,其他变量全部失效
LANGSMITH_API_KEY 身份凭证 报 401,且报错信息会误导你(第 5 节)
LANGSMITH_PROJECT 数据落到哪个项目 落到 default 项目
LANGSMITH_ENDPOINT 服务地址 用官方云 https://api.smith.langchain.com

3.1 关于旧变量名 #

老教程里你会看到 LANGCHAIN_TRACING_V2 和 LANGCHAIN_API_KEY。它们仍然能用(为了向后兼容),但新代码统一用 LANGSMITH_ 前缀。

真正要注意的是别混着用:

# 别这样。两套前缀混用时优先级容易搞错
LANGCHAIN_TRACING_V2=true
LANGSMITH_API_KEY=lsv2_pt_xxx

3.2 代码里怎么确认开关状态 #

不要靠猜,有现成的函数:

from langsmith import utils

# 综合判断当前是否会上报(会考虑环境变量和 tracing_context)
print(utils.tracing_is_enabled())      # True

实测输出:

LANGSMITH_TRACING  = 'true'
LANGSMITH_ENDPOINT = 'https://api.smith.langchain.com'
LANGSMITH_PROJECT  = 'ls-course-ch30'
tracing_is_enabled() = True

这个函数比自己读 os.getenv("LANGSMITH_TRACING") 靠谱,因为它还会考虑第 9 节讲的 tracing_context 临时开关。


4. 三个常见说法,实测辨真伪 #

网上关于 LangSmith 接入有几个流传很广的说法。我把它们逐个做了对照实验,结果和多数教程写的不一样。

4.1 说法一:LANGSMITH_PROJECT 必须在 import 之前设置 #

这是最常见的一条建议,通常写成:

# 很多教程强调这个顺序
import os
os.environ["LANGSMITH_PROJECT"] = "my-project"   # 必须在这之前设!
from langsmith import traceable                   # 否则不生效

实测三种时机:

# 变体 A:import 之前设
os.environ["LANGSMITH_PROJECT"] = "ls-course-timing-before-import"
from langsmith import traceable

# 变体 B:import 之后设
from langsmith import traceable
os.environ["LANGSMITH_PROJECT"] = "ls-course-timing-after-import"

# 变体 C:连 Client 都建完了再设
from langsmith import traceable, Client
c = Client()
os.environ["LANGSMITH_PROJECT"] = "ls-course-timing-after-client"

每个变体跑一个 @traceable 函数,在函数内部打印 get_current_run_tree().session_name(Run 实际落到的项目):

变体 before-import   实际项目: ls-course-timing-before-import
变体 after-import    实际项目: ls-course-timing-after-import
变体 after-client    实际项目: ls-course-timing-after-client

最终落库确认:
ls-course-timing-before-import         存在=True  run 数=1
ls-course-timing-after-import          存在=True  run 数=1
ls-course-timing-after-client          存在=True  run 数=1

三种都正确。这个说法在 langsmith 0.12.1 上不成立。

原因是项目名是在每个 Run 开始的那一刻才去读环境变量的,不是 import 时缓存下来的。所以你随时改都来得及。

不过还是建议把它放在文件顶部——不是因为技术上必须,而是因为可读性。让读代码的人一眼看到这份代码往哪个项目发数据,比藏在中间某一行强。

4.2 说法二:项目要先手动建好 #

实测:用一个从没出现过的项目名直接跑。

os.environ["LANGSMITH_PROJECT"] = "ls-course-autocreate-412736"
创建前 project_id = None
函数返回 42
创建后 project_id = a39ae0a2-45c6-4a1a-a517-7556a0de352f(等了约 4s 自动出现)

会自动创建,4 秒左右就能查到。 不需要提前在界面上点「New Project」。

4.3 说法三:不调 flush() 会丢数据 #

上报是后台线程异步做的,所以「进程退出太快会丢数据」这个担心听起来很合理。实测:

# 跑完一个 traceable 函数,不调 flush,直接让脚本结束
f(21)
print("跑完了,不调 flush 直接退出")
跑完了,不调 flush 直接退出
→ 项目出现了(等了约 4s):说明进程退出时有 atexit 兜底

正常退出时不会丢,SDK 注册了 atexit 钩子会把队列排干。

但有两种情况仍然会丢,这时才需要手动 flush:

情况 为什么会丢
os._exit() / SIGKILL / 容器被强杀 绕过了 atexit
长期运行的服务(Web 后端) 进程不退出,数据一直在队列里等批量条件

手动 flush 的正确写法有个陷阱:

# ✗ 错的。这是新建了一个 Client,flush 的是它自己的空队列,
#   真正装着 trace 的那个全局 client 完全没动
from langsmith import Client
Client().flush()

# ✓ 对的。拿到 LangChain tracing 实际在用的那个 client
from langchain_core.tracers.langchain import get_client
get_client().flush()

如果你用的是 @traceable(不经过 LangChain),那 Client().flush() 是对的——因为 @traceable 默认就用全局缓存的 client。混着用时以 get_client() 为准。

4.4 小结 #

说法 实测结论
项目名必须在 import 前设 不成立,任何时候设都行;但放顶部更易读
项目要手动建 不成立,自动创建,约 4 秒可见
不 flush 会丢数据 正常退出不会丢;强杀和长期服务才需要手动 flush
Client().flush() 能刷 LangChain 的数据 不成立,要用 get_client().flush()

5. 排障实录:密钥是对的,却报 401 #

这一节是本章最值得读的部分。它是一次真实排查的完整记录,而且根因不在 LangSmith——这类问题最耗时间,因为报错信息把你往完全错误的方向引。

5.1 现象 #

.env 配好了,密钥从界面复制粘贴,确认无误。跑起来报:

langsmith.utils.LangSmithAuthError: Authentication failed for /datasets.
HTTPError('401 Client Error: Unauthorized for url:
https://api.smith.langchain.com/datasets', '{"detail":"Invalid token"}')

Invalid token。第一反应当然是密钥有问题。

5.2 排查过程 #

第一步:验证密钥本身。 绕开 SDK,用最原始的方式打一次:

import requests
from dotenv import dotenv_values

k = dotenv_values(".env")["LANGSMITH_API_KEY"]
r = requests.get("https://api.smith.langchain.com/sessions?limit=1",
                 headers={"x-api-key": k}, timeout=20)
print(r.status_code)      # 200

200。密钥完全有效。 那问题在 SDK 侧。

第二步:观察规律。 反复试,发现一个奇怪的模式:

uv run python -c "...同样的代码..."      → 成功
uv run python C:\Temp\ls\script.py       → 401

同一台机器、同一份代码、同一个密钥,内联执行成功,脚本文件执行失败。

这个现象排除了「网络问题」「配额用尽」「服务端限流」这些猜测——它们不可能只影响其中一种执行方式。

第三步:找差别。 两种方式唯一的区别是 __file__ 的位置。而代码里唯一和文件位置有关的是……

from dotenv import load_dotenv
load_dotenv(override=True)

第四步:验证。 同一份探测代码,放在两个位置分别跑:

import os
from dotenv import load_dotenv, find_dotenv

print("cwd           =", os.getcwd())
print("__file__ dir  =", os.path.dirname(os.path.abspath(__file__)))
print("find_dotenv() =", repr(find_dotenv()))
load_dotenv(override=True)
k = os.getenv("LANGSMITH_API_KEY")
print("key after load =", (k[:12] + "..." if k else None))
脚本在项目目录内  (D:\forever\docs\langrag\_probe.py)
  cwd            = D:\forever\docs\langrag
  __file__ dir   = D:\forever\docs\langrag
  find_dotenv()  = 'D:\\forever\\docs\\langrag\\.env'
  key after load = lsv2_pt_dc37...

脚本在 Temp 目录  (C:\Users\...\Temp\_probe.py)
  cwd            = D:\forever\docs\langrag        ← 工作目录是对的!
  __file__ dir   = C:\Users\...\Temp
  find_dotenv()  = ''                              ← 但没找到 .env
  key after load = None                            ← 密钥是 None

5.3 根因 #

load_dotenv() 是从「调用它的那个脚本所在目录」向上查找 .env,不是从当前工作目录查找。

脚本放在 Temp 目录时,find_dotenv() 一路往上找到 C:\ 都没有 .env,返回空字符串。load_dotenv() 于是什么都没加载,LANGSMITH_API_KEY 是 None。

而 SDK 拿着 None 去请求,服务端返回 401 {"detail":"Invalid token"}——它说的是实话,token 确实无效,因为压根没有 token。 误导人的是我们的假设:我们以为 token 是 .env 里那个。

python -c 之所以能成功,是因为内联代码没有 __file__,find_dotenv() 退化成从当前工作目录找,而工作目录恰好是项目根目录。

5.4 怎么避免 #

方案一:显式指定路径(推荐,最稳)

from pathlib import Path
from dotenv import load_dotenv

# 用 __file__ 算出项目根目录,不依赖脚本从哪儿启动
ROOT = Path(__file__).resolve().parent      # 按实际层级调整 .parent 个数
load_dotenv(ROOT / ".env", override=True)

方案二:加载后立刻断言

import os
from dotenv import load_dotenv

load_dotenv(override=True)

# 三行断言,能省下半天排查时间
assert os.getenv("LANGSMITH_API_KEY"), (
    "LANGSMITH_API_KEY 没读到。检查 .env 位置:"
    "load_dotenv 是从脚本所在目录往上找的,不是从 cwd"
)

方案三:确认 find_dotenv() 的结果

from dotenv import find_dotenv

print("找到的 .env:", find_dotenv() or "(没找到!)")

5.5 值得记住的三点 #

这个案例的价值不在于 dotenv 本身,而在于排查方法:

  1. 401 Invalid token 不一定代表密钥错了,也可能是密钥根本没传。 遇到认证错误,先确认凭证读到了没有,再怀疑它对不对。

  2. 能稳定复现的「随机故障」,一定有个被你忽略的变量。 一开始我以为是网络抖动,因为成功和失败交替出现——实际上是我在两种执行方式之间来回切。先找出「什么情况下必然成功、什么情况下必然失败」,比统计成功率有用得多。

  3. 绕开封装直接打原始请求,是最快的分界手段。 requests 一行就把问题范围从「LangSmith 整条链路」缩到了「SDK 侧」。


6. @traceable:给普通函数加 Trace #

不是所有代码都在 LangChain 里。你的检索前处理、业务规则校验、缓存查询都是普通 Python 函数,它们默认不会出现在 Trace 里——于是树上就有了断层:明明花了 3 秒,Trace 里只看到 1 秒的模型调用,另外 2 秒不知去向。

@traceable 就是补这个断层的。

from langsmith import traceable
from langsmith.run_helpers import get_current_run_tree


# 用法一:什么都不传。run_type 默认 'chain',显示名默认取函数名
@traceable
def clean(text: str) -> str:
    """文本预处理。"""
    return text.strip().lower()


# 用法二:指定类型和显示名。中文名也可以,界面上直接显示
@traceable(run_type="tool", name="查缓存")
def cache_lookup(key: str) -> str | None:
    return {"abc": "命中的值"}.get(key)


# 用法三:带标签和元数据。这两个之后可以用来筛选
@traceable(run_type="chain", tags=["ch30", "pipeline"],
           metadata={"env": "dev", "ver": "v1"})
def handle(text: str) -> dict:
    """外层函数。内层调用会自动成为它的子 Run,不用手工串联。"""
    rt = get_current_run_tree()          # 拿到当前 Run 对象
    print(f"当前 Run id={rt.id}")
    print(f"  trace_id     = {rt.trace_id}")
    print(f"  session_name = {rt.session_name}")    # 落到哪个项目
    k = clean(text)                      # ← 自动成为 handle 的子 Run
    v = cache_lookup(k)                  # ← 同上
    return {"key": k, "hit": v}


print(handle("  ABC  "))

实测输出:

当前 Run id=01a065b3-b522-71e2-936b-671846be0e90
  trace_id     = 01a065b3-b522-71e2-936b-671846be0e90
  session_name = ls-course-ch30
{'key': 'abc', 'hit': '命中的值'}

上报后在项目里查到的结构:

▸ handle    tags=['ch30', 'pipeline']  metadata={'env': 'dev', 'ver': 'v1'}

三点值得注意:

6.1 混用:@traceable 和 LangChain 在同一棵树上 #

两者会自动拼成一棵树,不需要额外配置:

@traceable(run_type="chain", name="完整问答流程")
def full_pipeline(question: str) -> str:
    # 这一段是纯 Python,靠 @traceable 才可见
    q = clean(question)

    # 这一段是 LangChain,它会自动挂到 full_pipeline 下面
    out = agent.invoke({"messages": [{"role": "user", "content": q}]})
    return out["messages"][-1].content

产生的树:

[chain] 完整问答流程
    [chain] clean
    [chain] LangGraph
        [chain] model
            [llm] ChatDeepSeek
        ...

7. 项目怎么划分 #

项目是 LangSmith 最粗的一层归类。常见的分法有三种:

分法 例子 适合
按环境 myapp-dev / myapp-staging / myapp-prod 默认选它
按功能 customer-service / rag-search / report-gen 一个系统里有多个独立 Agent
按人 alice-dev / bob-dev 团队共享一个 workspace,各自调试互不干扰

最常见的组合是「按环境 + 按功能」:

# 生产环境的客服 Agent
LANGSMITH_PROJECT=customer-service-prod

7.1 别用项目做细粒度切分 #

一个常见的误用是给每次实验、每个用户、每个版本各建一个项目。这会带来两个问题:

细粒度的切分应该用 tags 和 metadata(下一节),它们是在项目内部做过滤,随时能切换视角。

一个实用的判断标准:

需要「分开看」的用项目,需要「对比着看」的用 tags / metadata。


8. tags 和 metadata:更灵活的切分 #

不改代码结构,在调用时通过 config 传:

r = agent.invoke(
    {"messages": [{"role": "user", "content": "3 加 5 等于几"}]},
    config={
        # 改根 Run 的显示名。默认叫 'LangGraph',一堆都叫这个很难找
        "run_name": "算术-自定义名字",
        # 标签:一维的字符串列表,用来筛
        "tags": ["ch30", "arith", "v2"],
        # 元数据:键值对,能存结构化信息
        "metadata": {"user_id": "u_123", "release": "2026.09"},
    },
)

实测查回来:

▸ 算术-自定义名字   tags=['v2', 'ch30', 'arith']
                   metadata={'release': '2026.09', 'user_id': 'u_123'}

8.1 两者的区别 #

tags metadata
类型 list[str] dict,值可以是任意 JSON
适合 少量、可枚举的分类 高基数、结构化的信息
例子 ["prod", "v2", "rag"] {"user_id": "u_123", "tenant": "acme"}
查询语法 has(tags, "prod") eq(metadata_key, "value")

经验法则: 值的种类少于几十种用 tags,是标识符(用户 ID、订单号、租户 ID)用 metadata。

8.2 该记哪些 metadata #

这几个是排障时最常用的,建议默认都记上:

config = {
    "metadata": {
        "user_id": current_user.id,        # 出问题能找到是谁遇到的
        "session_id": session.id,          # 串起同一个会话的多次调用
        "release": os.getenv("GIT_SHA"),   # 定位是哪个版本引入的
        "prompt_version": "v3",            # 改了提示词能对比前后
    }
}

其中 release(版本号)最容易被忽略,也最有用。有它,「这个 bug 是什么时候开始的」就从翻聊天记录变成了一次筛选。

8.3 注意:LangGraph 会自动打标签 #

实际拉回来的 Run 上有一些你没打过的标签:

tags=['seq:step:1']
tags=['graph:step:3']

这些是 LangGraph 自动加的,标记节点在图里的执行序号。不要用 seq: 和 graph: 开头的标签名,避免混淆。


9. tracing_context:临时改项目或临时关掉 #

环境变量是全局的。有时你只想改一小段,tracing_context 就是干这个的:

from langsmith import tracing_context
from langsmith import utils

# 临时换项目:这个块里产生的 Run 落到另一个项目
with tracing_context(project_name="ls-course-ch30-tmp"):
    handle("  abc ")
    # ↑ 落到 ls-course-ch30-tmp,块外的还是原项目

# 临时关掉:函数照常执行,只是不上报
with tracing_context(enabled=False):
    print(utils.tracing_is_enabled())      # False
    handle(" xyz ")                        # 正常返回,但不产生 Run

print(utils.tracing_is_enabled())          # True,出块自动恢复

实测输出:

函数内可以拿到当前 Run: id=01a065b3-b5aa-73c3-b1d7-1bfbae579bcb
  session_name = ls-course-ch30-tmp   ← 落到了临时项目
↑ 这次落到了临时项目

enabled=False 块内 tracing_is_enabled() = False
get_current_run_tree() = None(tracing 没开)
↑ 这次完全不上报,但函数照常执行

离开块后 tracing_is_enabled() = True

注意第二段里 get_current_run_tree() 变成了 None。这就是第 6 节说的那个判空理由——如果你的代码里有 get_current_run_tree().id 这种写法,一旦有人在外面套了 enabled=False,它就会抛异常。这个 bug 只在关掉 tracing 时出现,很难在开发时发现。

9.1 什么时候用它 #

场景 写法
单元测试不想污染项目 在 fixture 里套 tracing_context(enabled=False)
批量回填历史数据 同上,避免几万条噪音 Run
同一份代码给不同租户 tracing_context(project_name=f"app-{tenant}")
只想追某一个可疑请求 反过来:全局关掉,只给这个请求套 enabled=True

9.2 关掉 tracing 的三种方式 #

按作用范围从大到小:

# ① 全局:环境变量。适合 CI 环境
os.environ["LANGSMITH_TRACING"] = "false"

# ② 块级:上下文管理器。适合测试 fixture
with tracing_context(enabled=False):
    ...

# ③ 单次调用:LangChain 的 config
agent.invoke(payload, config={"callbacks": []})

第三种要小心:callbacks=[] 会覆盖掉所有回调,包括你自己注册的。想精确关闭 LangSmith 上报,用第二种。


10. 敏感数据脱敏 #

Trace 会完整记录 inputs 和 outputs。这意味着用户的手机号、身份证、订单地址、内部文档原文,全都会上传到 LangSmith 的服务器。合规上这往往是不能接受的。

Client 提供了两个钩子,在上报前改写数据:

from langsmith import Client, traceable


def mask(inputs: dict) -> dict:
    """上报前改写 inputs。注意要返回新字典,别改原对象。"""
    out = dict(inputs)
    if "phone" in out:
        # 138****34 这种打码
        out["phone"] = out["phone"][:3] + "****" + out["phone"][-2:]
    return out


# hide_inputs / hide_outputs 都接收一个函数,返回改写后的内容
masked_client = Client(
    hide_inputs=mask,
    hide_outputs=lambda o: {"已隐藏": True},   # 输出干脆整个替换掉
)


# 用 client= 参数指定这个函数用哪个 client 上报
@traceable(client=masked_client)
def query_user(phone: str, name: str) -> dict:
    return {"name": name, "balance": 12345}


res = query_user(phone="13800001234", name="张三")
print("函数真实返回:", res)

实测:

函数真实返回: {'name': '张三', 'balance': 12345}

上报到 LangSmith 后查回来:
  inputs  = {'name': '张三', 'phone': '138****34'}
  outputs = {'已隐藏': True}

函数的真实返回值不受影响,只有上报的副本被改写了。业务逻辑完全不知道脱敏这回事。

10.1 全局生效的写法 #

上面那样每个函数都传 client= 很啰嗦。想让整个应用都脱敏,用环境变量:

# 最粗暴:完全不上报 inputs 和 outputs,只留结构和耗时
LANGSMITH_HIDE_INPUTS=true
LANGSMITH_HIDE_OUTPUTS=true

这个开关的代价是 Trace 的排障价值大幅下降——你能看到「调了哪个工具、花了多久」,但看不到「传了什么参数」。

折中方案是写一个统一的脱敏函数,只打掉真正敏感的字段:

import re

# 需要整个隐藏的字段名
SENSITIVE_KEYS = {"password", "token", "api_key", "id_card", "bank_card"}
# 需要打码的模式:手机号、邮箱
PHONE = re.compile(r"1[3-9]\d{9}")
EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")


def redact(obj):
    """递归脱敏。dict / list / str 都能处理。"""
    if isinstance(obj, dict):
        return {k: ("***" if k.lower() in SENSITIVE_KEYS else redact(v))
                for k, v in obj.items()}
    if isinstance(obj, list):
        return [redact(v) for v in obj]
    if isinstance(obj, str):
        s = PHONE.sub(lambda m: m.group()[:3] + "****" + m.group()[-2:], obj)
        return EMAIL.sub("***@***", s)
    return obj


client = Client(hide_inputs=redact, hide_outputs=redact)

10.2 三个提醒 #


11. 实用清单 #

接入完成后,建议按这个清单过一遍:

"""放在项目里当作接入自检脚本。"""
import os
from pathlib import Path

from dotenv import load_dotenv, find_dotenv

# ① 显式指定 .env 路径,别依赖查找(第 5 节的坑)
ROOT = Path(__file__).resolve().parent
load_dotenv(ROOT / ".env", override=True)

# ② 断言关键变量读到了
print("找到的 .env :", find_dotenv() or "(没找到)")
assert os.getenv("LANGSMITH_API_KEY"), "密钥没读到,检查 .env 位置"

# ③ 确认开关状态
from langsmith import utils
print("tracing 开启 :", utils.tracing_is_enabled())
print("目标项目     :", os.getenv("LANGSMITH_PROJECT", "default"))

# ④ 验证密钥有效(绕开 SDK,直接打)
import requests
r = requests.get(f"{os.getenv('LANGSMITH_ENDPOINT', 'https://api.smith.langchain.com')}/sessions?limit=1",
                 headers={"x-api-key": os.getenv("LANGSMITH_API_KEY")}, timeout=20)
print("密钥有效     :", r.status_code == 200, f"(HTTP {r.status_code})")

# ⑤ 发一条测试 Trace
from langsmith import traceable

@traceable(name="接入自检")
def smoke(x: int) -> int:
    return x * 2

print("测试调用返回 :", smoke(21))

from langsmith import Client
Client().flush()
print("已上报,去界面确认能看到一条名为「接入自检」的 Trace")

12. 练习 #

  1. 复现 401 那个坑。 把一个能正常工作的脚本复制到 C:\Temp(或 /tmp)下运行,观察报错。然后用 find_dotenv() 确认根因。亲手踩一遍,下次遇到能省几小时。

  2. 给非 LangChain 代码补上断层。 找你项目里一个「Trace 里耗时对不上」的地方,用 @traceable 把中间的普通函数标注出来,看看那段消失的时间去哪了。

  3. 设计你的 metadata 方案。 按第 8.2 节,列出你的系统排障时最需要的 5 个字段。想一想:如果只能记 3 个,砍掉哪两个?

  4. 写一个脱敏函数并测试它。 用第 10.1 节的 redact 做起点,加上你业务里的敏感字段。关键是测试它不会抛异常——构造一个含 None、嵌套列表、超长字符串的输入试试。

  5. 验证一个说法。 第 4 节推翻了三个说法,但那是 langsmith 0.12.1 的行为。在你自己的版本上跑一遍那三个实验,看结论是否一致。这个习惯比记住结论本身更有价值。


13. 小结 #

接入本身:

LANGSMITH_TRACING=true
LANGSMITH_API_KEY=lsv2_pt_xxx
LANGSMITH_PROJECT=my-project

三行,业务代码不改。

实测推翻的三个说法:

说法 实际
项目名必须在 import 前设 任何时候都行(但放顶部更易读)
项目要手动建 自动创建,约 4 秒可见
不 flush 会丢 正常退出不丢;强杀和常驻服务才需要

一个要记住的排障结论:

401 Invalid token 也可能是密钥根本没传。load_dotenv() 从脚本所在目录往上找 .env,不是从工作目录找。

四个工具:

需求 用什么
给普通函数加 Trace @traceable
临时改项目 / 临时关掉 tracing_context(...)
给单次调用打标记 config={"run_name", "tags", "metadata"}
上报前脱敏 Client(hide_inputs=..., hide_outputs=...)

一条设计原则:

需要分开看的用项目,需要对比着看的用 tags / metadata。

数据现在流起来了。下一章开始真正地读它——从一棵树里找出「哪一步开始不对劲」,走一遍完整的排障流程。