LANGCHAIN_TRACING_V2 与 LANGSMITH_TRACING:新旧变量并存时谁生效

旧教程让你写 export LANGCHAIN_TRACING_V2=true,官方新文档写的是 LANGSMITH_TRACING=true。两套名字在代码里都还有效,所以最常见的问题不是“写错了”,而是 .env、CI 变量、Dockerfile 或 IDE 运行配置里同时留着新旧两个,而你以为后写的、或者名字更新的那个会生效。

先看结论:开关上旧名字优先。已经设了 LANGSMITH_TRACING=false,只要环境里还留着 LANGCHAIN_TRACING_V2=true,追踪就仍然开着,每次调用都会挂上 LangChainTracer,在密钥和网络可用时,输入输出会照常发往 LangSmith。项目名、密钥和服务地址正好相反,新名字优先。这个结果在 langchain==0.3.27 和 langchain==1.4.7 下完全一致,升级到 v1 既不会让旧变量失效,也不会改变这个优先级。

适用范围

环境已安装的关键包用途
0.3.27(旧环境)langchain==0.3.27、langchain-core==0.3.86、langsmith==0.14.12旧教程当年的环境
1.4.7langchain==1.4.7、langchain-core==1.7.0、langsmith==0.14.12升级后的环境
  • Python 3.12.12;

  • 官方来源:Trace with LangChain (opens in a new tab) 和 Conditional tracing (opens in a new tab);

  • 实验代码:站点仓库 labs/langsmith-tracing-env/;脚本不联网,不启动任何运行,也不需要 API Key。

  • 覆盖:开关、项目名、密钥和服务地址这四类变量在新旧名字并存时取哪一个,以及代码里 tracing_context 和 ls.configure 两种覆盖方式;

  • 未覆盖:真实的 LangSmith 服务(上报是否成功、密钥权限、采样);其他 langsmith 版本;空字符串或只含空白的取值(只读过源码,没有放进实验);部署平台自行注入的变量。

新旧变量名对照

用途旧教程里的名字官方新文档的名字两者并存时
打开或关闭追踪LANGCHAIN_TRACING_V2LANGSMITH_TRACING旧名字优先
项目名LANGCHAIN_PROJECTLANGSMITH_PROJECT新名字优先
密钥LANGCHAIN_API_KEYLANGSMITH_API_KEY新名字优先
服务地址LANGCHAIN_ENDPOINTLANGSMITH_ENDPOINT新名字优先

只设置其中一个名字时,新旧都生效,两个环境下一致。所以旧教程里的变量在 v1 里不会报错,也不会失效,问题只出在并存。官方的两篇文档都只使用 LANGSMITH_ 开头的名字,没有讨论旧名字并存时怎么取舍,这部分是对照实验的结果。

开关:旧名字盖过新名字

下面是 env_precedence.py 在 1.4.7 环境里的输出,每一行是一组环境变量。enabled 是 langsmith 判断追踪是否开启的结果,tracer 表示 LangChain 的回调管理器有没有挂上 LangChainTracer,也就是这次调用会不会被记录:

langchain-core 1.7.0 | langsmith 0.14.12

[1] tracing switch
case                                                          enabled  tracer
(nothing set)                                                 False    off
LANGSMITH_TRACING=true                                        True     on
LANGCHAIN_TRACING_V2=true                                     True     on
LANGSMITH_TRACING=false + LANGCHAIN_TRACING_V2=true           True     on
LANGSMITH_TRACING=true + LANGCHAIN_TRACING_V2=false           False    off
LANGSMITH_TRACING=True                                        False    off
LANGSMITH_TRACING=1                                           False    off
LANGCHAIN_TRACING_V2=true + tracing_context(enabled=False)    False    off
LANGCHAIN_TRACING_V2=true + ls.configure(enabled=False)       False    off

[2] project, key and endpoint
case                                                                                    value
LANGCHAIN_PROJECT=old-proj                                                              old-proj
LANGSMITH_PROJECT=new-proj + LANGCHAIN_PROJECT=old-proj                                 new-proj
LANGCHAIN_API_KEY=key-old                                                               key-old
LANGSMITH_API_KEY=key-new + LANGCHAIN_API_KEY=key-old                                   key-new
LANGCHAIN_ENDPOINT=http://localhost:9/old                                               http://localhost:9/old
LANGSMITH_ENDPOINT=http://localhost:9/new + LANGCHAIN_ENDPOINT=http://localhost:9/old   http://localhost:9/new

0.3.27 环境的输出除了第一行的版本号,与上面逐行相同。重点看第四行和第五行:

  • LANGSMITH_TRACING=false 加上 LANGCHAIN_TRACING_V2=true,追踪是开着的。 这是线上最危险的一种:你按隐私要求把开关设成了 false,旧变量却把它盖掉了,数据会继续外发(密钥和网络可用时),而且没有任何报错;
  • 反过来 LANGSMITH_TRACING=true 加上 LANGCHAIN_TRACING_V2=false,追踪是关着的。 你按新文档打开了追踪,却一条 trace 也看不到,原因同样是旧变量。

这个优先级来自 langsmith 的 tracing_is_enabled 函数。它在最后一步是这样读环境变量的:

# langsmith/utils.py (0.14.12), last step of tracing_is_enabled()
var_result = get_env_var("TRACING_V2", default=get_env_var("TRACING", default=""))
return var_result == "true"

get_env_var 对每个名字依次查 LANGSMITH_ 和 LANGCHAIN_ 两个前缀。于是先查的是带 _V2 的名字:LANGSMITH_TRACING_V2,再是 LANGCHAIN_TRACING_V2。只有这两个都没设置,才去查不带 _V2 的 LANGSMITH_TRACING 和 LANGCHAIN_TRACING。带 _V2 的名字排在前面,与前缀新旧无关,所以旧教程里的 LANGCHAIN_TRACING_V2 总是压过新的 LANGSMITH_TRACING。

取值只认小写 true

表里的第六行和第七行:LANGSMITH_TRACING=True 和 LANGSMITH_TRACING=1 都不会打开追踪,enabled 是 False,也没有异常。判断条件是取值恰好等于小写的 true。在 .env 文件、CI 配置或编排文件里手写这个值时,不要写成 True、TRUE、1 或 yes。

项目、密钥、端点:新名字优先

另外三类变量各自只有一个名字,get_env_var 先查 LANGSMITH_ 前缀,没有再查 LANGCHAIN_。同一个输出的第二部分:

  • 只设置旧名字时,旧名字生效,所以旧教程的 LANGCHAIN_PROJECT、LANGCHAIN_API_KEY 和 LANGCHAIN_ENDPOINT 照常工作;
  • 新旧都设置时,取新名字。表里脚本用的 key-old、key-new 是占位字符串,不是真实密钥。

开关取旧名字,其余取新名字,没有一条规则能同时记住两种情况。清理环境时,最稳妥的办法是把四类旧变量一起删掉,只保留 LANGSMITH_ 开头的名字。

在自己的环境里确认

下面的脚本回答两个问题:当前进程里追踪到底开没开,以及四个开关变量名各是什么值。它要在出问题的那个环境里运行,也就是同一个 shell、虚拟环境、容器或 IDE 运行配置:

check_tracing.py
"""Report whether tracing is on in this process and which variable names are set.
 
Run it in the same shell, virtual environment or container as the failing process. To reproduce a
leftover variable without touching your shell, pass NAME=VALUE pairs: they are applied before
langsmith is imported.
"""
import os
import sys
 
for pair in sys.argv[1:]:
    name, _, value = pair.partition("=")
    os.environ[name] = value
 
from langsmith.utils import tracing_is_enabled  # noqa: E402  (must run after the variables are applied)
 
for name in ("LANGSMITH_TRACING", "LANGCHAIN_TRACING_V2", "LANGSMITH_TRACING_V2", "LANGCHAIN_TRACING"):
    print(f"{name:<22}{os.environ.get(name, '(unset)')!r}")
print("tracing_is_enabled():", tracing_is_enabled())

为了在不改动你的 shell 的前提下复现“新旧并存”,脚本接受 NAME=VALUE 参数,并在导入 langsmith 之前写入环境。下面是传入 LANGSMITH_TRACING=false LANGCHAIN_TRACING_V2=true 的输出:

LANGSMITH_TRACING     'false'
LANGCHAIN_TRACING_V2  'true'
LANGSMITH_TRACING_V2  '(unset)'
LANGCHAIN_TRACING     '(unset)'
tracing_is_enabled(): True

看到 LANGCHAIN_TRACING_V2 是 'true',而 LANGSMITH_TRACING 是 'false',最后一行仍然是 True,就是旧变量在起作用。找到来源的办法是在项目目录和部署配置里搜索 LANGCHAIN_:shell 配置文件、.env 文件、Dockerfile 的 ENV、编排文件、CI 的变量设置、IDE 的运行配置,以及代码里的 os.environ[...] = ...。

要可靠地关掉追踪

下表中的 ls 指 import langsmith as ls。

做法在表里的依据适合的场景
删除所有 LANGCHAIN_ 开头的追踪变量,只保留 LANGSMITH_TRACING第二、三行:只设一个名字时,新旧名字都能控制开关长期做法,配置里只剩一套名字
代码里用 with ls.tracing_context(enabled=False): 包住调用第八行:即使 LANGCHAIN_TRACING_V2=true 也关闭某个租户、某类请求单独关闭
进程启动时调用 ls.configure(enabled=False)第九行:即使 LANGCHAIN_TRACING_V2=true 也关闭无法立即清理环境变量时的兜底

官方的 Conditional tracing 页给出的优先级是:tracing_context(enabled=...) 最高,其次是 ls.configure(enabled=...),环境变量最低。表里第八、九行与这个顺序一致。代码层面的开关能压住环境变量,但它掩盖了环境里仍有旧变量的事实,换一种启动方式、换一个进程,旧变量还会生效,所以兜底之后仍然要清理。

失败、回滚与排障

现象常见原因处理
设了 LANGSMITH_TRACING=false,LangSmith 里仍有新的 trace环境里还留着 LANGCHAIN_TRACING_V2=true运行 check_tracing.py 确认,删除旧变量
设了 LANGSMITH_TRACING=true,却一条 trace 也没有LANGCHAIN_TRACING_V2=false 压住了它;或者取值写成了 True、1运行 check_tracing.py,统一成小写 true
trace 出现在意料之外的项目里新旧项目变量并存时取 LANGSMITH_PROJECT只保留 LANGSMITH_PROJECT
两个变量都核对过了,仍然没有 trace实验没有覆盖服务端:密钥权限、网络、采样都可能是原因按追踪上线前检查用本地接收端抓包,看请求有没有发出

删除旧变量前,先记下它们现在的取值(密钥不要写进工单或聊天记录)。删除之后如果追踪消失,说明原来的配置靠的是旧名字,补上 LANGSMITH_TRACING=true 即可。需要退回时,把旧变量放回去,行为就恢复成删除之前的样子。

复现脚本

env_precedence.py 是上面两个表的来源。每组变量都在一个全新的子进程里运行,因为 langsmith 会缓存部分环境变量的读取结果;子进程只询问库“会怎么做”,不启动任何运行,也不联网:

env_precedence.py
"""Which LangSmith environment variable wins when old (LANGCHAIN_*) and new (LANGSMITH_*) names are both set?
 
Every case runs in a fresh child process with a clean environment, because langsmith caches some
environment lookups. The child only asks the libraries what they would do: it never starts a run
and never touches the network.
"""
import json
import os
import subprocess
import sys
from importlib.metadata import version
 
CHILD = r'''
import json
import sys
import warnings
 
warnings.simplefilter("ignore")
 
import langsmith as ls
from langchain_core.callbacks.manager import CallbackManager
from langsmith import utils
 
 
def snapshot():
    handlers = [type(h).__name__ for h in CallbackManager.configure().handlers]
    return {
        "tracing": utils.tracing_is_enabled(),
        "tracer": "LangChainTracer" in handlers,
        "project": utils.get_tracer_project(),
        "api_key": utils.get_api_key(None),
        "api_url": utils.get_api_url(None),
    }
 
 
mode = sys.argv[1]
if mode == "context_off":
    with ls.tracing_context(enabled=False):
        result = snapshot()
elif mode == "configure_off":
    ls.configure(enabled=False)
    result = snapshot()
else:
    result = snapshot()
print(json.dumps(result))
'''
 
# (label, environment variables, how the code overrides them)
SWITCH_CASES = [
    ("(nothing set)", {}, "env"),
    ("LANGSMITH_TRACING=true", {"LANGSMITH_TRACING": "true"}, "env"),
    ("LANGCHAIN_TRACING_V2=true", {"LANGCHAIN_TRACING_V2": "true"}, "env"),
    ("LANGSMITH_TRACING=false + LANGCHAIN_TRACING_V2=true", {"LANGSMITH_TRACING": "false", "LANGCHAIN_TRACING_V2": "true"}, "env"),
    ("LANGSMITH_TRACING=true + LANGCHAIN_TRACING_V2=false", {"LANGSMITH_TRACING": "true", "LANGCHAIN_TRACING_V2": "false"}, "env"),
    ("LANGSMITH_TRACING=True", {"LANGSMITH_TRACING": "True"}, "env"),
    ("LANGSMITH_TRACING=1", {"LANGSMITH_TRACING": "1"}, "env"),
    ("LANGCHAIN_TRACING_V2=true + tracing_context(enabled=False)", {"LANGCHAIN_TRACING_V2": "true"}, "context_off"),
    ("LANGCHAIN_TRACING_V2=true + ls.configure(enabled=False)", {"LANGCHAIN_TRACING_V2": "true"}, "configure_off"),
]
 
# (label, environment variables, snapshot field that the case is about)
SETTING_CASES = [
    ("LANGCHAIN_PROJECT=old-proj", {"LANGCHAIN_PROJECT": "old-proj"}, "project"),
    ("LANGSMITH_PROJECT=new-proj + LANGCHAIN_PROJECT=old-proj", {"LANGSMITH_PROJECT": "new-proj", "LANGCHAIN_PROJECT": "old-proj"}, "project"),
    ("LANGCHAIN_API_KEY=key-old", {"LANGCHAIN_API_KEY": "key-old"}, "api_key"),
    ("LANGSMITH_API_KEY=key-new + LANGCHAIN_API_KEY=key-old", {"LANGSMITH_API_KEY": "key-new", "LANGCHAIN_API_KEY": "key-old"}, "api_key"),
    ("LANGCHAIN_ENDPOINT=http://localhost:9/old", {"LANGCHAIN_ENDPOINT": "http://localhost:9/old"}, "api_url"),
    (
        "LANGSMITH_ENDPOINT=http://localhost:9/new + LANGCHAIN_ENDPOINT=http://localhost:9/old",
        {"LANGSMITH_ENDPOINT": "http://localhost:9/new", "LANGCHAIN_ENDPOINT": "http://localhost:9/old"},
        "api_url",
    ),
]
 
 
def run_case(extra: dict, mode: str) -> dict:
    """Run one case in a child process whose LangSmith variables are exactly `extra`."""
    env = {k: v for k, v in os.environ.items() if not k.startswith(("LANGSMITH_", "LANGCHAIN_"))}
    env.update(extra)
    proc = subprocess.run([sys.executable, "-c", CHILD, mode], env=env, capture_output=True, text=True, timeout=60)
    if proc.returncode != 0:
        raise SystemExit(f"child failed for {extra}: {proc.stderr[-400:]}")
    return json.loads(proc.stdout.strip().splitlines()[-1])
 
 
def main() -> None:
    print(f"langchain-core {version('langchain-core')} | langsmith {version('langsmith')}")
    print()
    print("[1] tracing switch")
    print(f"{'case':<62}{'enabled':<9}tracer")
    for label, extra, mode in SWITCH_CASES:
        got = run_case(extra, mode)
        print(f"{label:<62}{got['tracing']!s:<9}{'on' if got['tracer'] else 'off'}")
    print()
    print("[2] project, key and endpoint")
    print(f"{'case':<88}value")
    for label, extra, field in SETTING_CASES:
        print(f"{label:<88}{run_case(extra, 'env')[field]}")
 
 
if __name__ == "__main__":
    main()

依据与复现

  • Trace with LangChain (opens in a new tab):官方示例使用 LANGSMITH_TRACING 和 LANGSMITH_PROJECT;
  • Conditional tracing (opens in a new tab):tracing_context、ls.configure 与环境变量的优先级;
  • 复现:先执行 labs/setup-envs.sh legacy v1 重建环境,再执行 python3 labs/run.py --lab langsmith-tracing-env;
  • 输出范围:文中输出全部来自站点仓库 labs/langsmith-tracing-env/ 在“适用范围”所列版本下的实际运行;优先级的原因来自对 langsmith/utils.py 中 tracing_is_enabled 与 get_env_var 的阅读;
  • 局限:结果对应 langsmith==0.14.12,其他版本的取舍顺序可能不同,升级后可以重新运行脚本确认;
  • 官方来源核对于 2026-10-27。

继续阅读