调试跟踪嵌套
使用 LangSmith SDK、LangGraph 和 LangChain 进行追踪时,追踪功能应自动传播正确的上下文,从而确保在父追踪内执行的代码能在 UI 中显示于预期的位置。
如果你看到一个子任务(child run)跳转到一个独立的追踪记录(trace)中(并显示在顶层),这可能是由以下已知的“边界情况”之一导致的。
Python
以下列出了使用 Python 构建时导致“split”(拆分)追踪的常见原因。
使用 asyncio 进行上下文传播
在 Python 版本 < 3.11 中使用异步调用(尤其是流式传输)时,您可能会遇到跟踪嵌套问题。这是因为 Python 的 asyncio 直到 3.11 版本才完整支持上下文传递。
为什么
LangChain 和 LangSmith SDK 使用 上下文变量(contextvars) 来隐式传递追踪信息。在 Python 3.11 及更高版本中,此功能可无缝运行。但在早期版本(3.8、3.9、3.10)中,asyncio 任务缺乏完善的 contextvar 支持,可能导致追踪链断开。
解决
-
升级 Python 版本(推荐) 如有可能,请升级到 Python 3.11 或更高版本以支持自动上下文传播。
-
手动上下文传播 如果无法升级,则需要手动传播追踪上下文。具体方法取决于您的配置:
a) 使用 LangGraph 或 LangChain 将父级参数
config传递给子调用:import asyncio
from langchain_core.runnables import RunnableConfig, RunnableLambda
@RunnableLambda
async def my_child_runnable(
inputs: str,
# The config arg (present in parent_runnable below) is optional
):
yield "A"
yield "response"
@RunnableLambda
async def parent_runnable(inputs: str, config: RunnableConfig):
async for chunk in my_child_runnable.astream(inputs, config):
yield chunk
async def main():
return [val async for val in parent_runnable.astream("call")]
asyncio.run(main())b) 直接使用 LangSmith 直接传入运行树:
import asyncio
import langsmith as ls
@ls.traceable
async def my_child_function(inputs: str):
yield "A"
yield "response"
@ls.traceable
async def parent_function(
inputs: str,
# The run tree can be auto-populated by the decorator
run_tree: ls.RunTree,
):
async for chunk in my_child_function(inputs, langsmith_extra={"parent": run_tree}):
yield chunk
async def main():
return [val async for val in parent_function("call")]
asyncio.run(main())c) 将装饰后的代码与 LangGraph/LangChain 结合使用 采用多种技术进行手动交接:
import asyncio
import langsmith as ls
from langchain_core.runnables import RunnableConfig, RunnableLambda
@RunnableLambda
async def my_child_runnable(inputs: str):
yield "A"
yield "response"
@ls.traceable
async def my_child_function(inputs: str, run_tree: ls.RunTree):
with ls.tracing_context(parent=run_tree):
async for chunk in my_child_runnable.astream(inputs):
yield chunk
@RunnableLambda
async def parent_runnable(inputs: str, config: RunnableConfig):
# @traceable decorated functions can directly accept a RunnableConfig when passed in via "config"
async for chunk in my_child_function(inputs, langsmith_extra={"config": config}):
yield chunk
@ls.traceable
async def parent_function(inputs: str, run_tree: ls.RunTree):
# You can set the tracing context manually
with ls.tracing_context(parent=run_tree):
async for chunk in parent_runnable.astream(inputs):
yield chunk
async def main():
return [val async for val in parent_function("call")]
asyncio.run(main())
使用线程进行上下文传播
通常,我们会从启动追踪开始,并希望在单个追踪内对子任务应用某种并行处理。但 Python 标准库中的 ThreadPoolExecutor 默认会中断追踪。
为什么
Python 的 contextvars 在新线程中初始化为空。以下是两种保持追踪连续性的方法:
解决
-
使用 LangSmith 的 ContextThreadPoolExecutor
LangSmith 提供了一个
ContextThreadPoolExecutor,可自动处理上下文传播:from langsmith.utils import ContextThreadPoolExecutor
from langsmith import traceable
@traceable
def outer_func():
with ContextThreadPoolExecutor() as executor:
inputs = [1, 2]
r = list(executor.map(inner_func, inputs))
@traceable
def inner_func(x):
print(x)
outer_func() -
手动提供父运行树
或者,您可以手动将父运行树传递给内部函数:
from langsmith import traceable, get_current_run_tree
from concurrent.futures import ThreadPoolExecutor
@traceable
def outer_func():
rt = get_current_run_tree()
with ThreadPoolExecutor() as executor:
r = list(
executor.map(
lambda x: inner_func(x, langsmith_extra={"parent": rt}), [1, 2]
)
)
@traceable
def inner_func(x):
print(x)
outer_func()在此方法中,我们使用
get_current_run_tree()获取当前运行树,并通过langsmith_extra参数将其传递给内部函数。
这两种方法均能确保即使在独立线程中执行,内部函数调用也能被正确地聚合到初始的追踪栈下。