Skip to main content
LangSmith SDK, LangGraph, LangChain으로 추적할 때는 올바른 컨텍스트가 자동으로 전파되어 부모 추적 내에서 실행되는 코드가 UI의 예상 위치에 렌더링되어야 합니다. 자식 실행이 별도의 추적으로 이동하여 최상위 레벨에 나타나는 경우, 다음과 같은 알려진 “엣지 케이스” 중 하나가 원인일 수 있습니다.

Python

다음은 Python으로 구축할 때 “분할된” 추적이 발생하는 일반적인 원인을 설명합니다.

asyncio를 사용한 컨텍스트 전파

Python 버전 3.11 미만에서 비동기 호출(특히 스트리밍 사용 시)을 사용할 때 추적 중첩 문제가 발생할 수 있습니다. 이는 Python의 asyncio가 버전 3.11에서야 컨텍스트 전달에 대한 완전한 지원을 추가했기 때문입니다.

원인

LangChain과 LangSmith SDK는 contextvars를 사용하여 추적 정보를 암시적으로 전파합니다. Python 3.11 이상에서는 이 기능이 원활하게 작동합니다. 하지만 이전 버전(3.8, 3.9, 3.10)에서는 asyncio 태스크가 적절한 contextvar 지원이 부족하여 추적이 분리될 수 있습니다.

해결 방법

  1. Python 버전 업그레이드(권장) 가능하다면 자동 컨텍스트 전파를 위해 Python 3.11 이상으로 업그레이드하세요.
  2. 수동 컨텍스트 전파 업그레이드가 불가능한 경우 추적 컨텍스트를 수동으로 전파해야 합니다. 방법은 설정에 따라 다릅니다: a) LangGraph 또는 LangChain 사용 부모 config를 자식 호출에 전달합니다:
    import asyncio
    from langchain_core.runnables import RunnableConfig, RunnableLambda
    
    @RunnableLambda
    async def my_child_runnable(
        inputs: str,
        # config 인수(아래 parent_runnable에 있음)는 선택 사항입니다
    ):
        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,
        # 실행 트리는 데코레이터에 의해 자동으로 채워질 수 있습니다
        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로 데코레이션된 함수는 "config"를 통해 전달될 때 RunnableConfig를 직접 받을 수 있습니다
        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):
        # 추적 컨텍스트를 수동으로 설정할 수 있습니다
        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는 새 스레드 내에서 비어 있는 상태로 시작됩니다. 추적 연속성을 유지하기 위한 두 가지 접근 방식이 있습니다:

해결 방법

  1. 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()
    
  2. 부모 실행 트리 수동 제공 또는 부모 실행 트리를 내부 함수에 수동으로 전달할 수 있습니다:
    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 매개변수를 사용하여 내부 함수에 전달합니다. 두 방법 모두 별도의 스레드에서 실행되더라도 내부 함수 호출이 초기 추적 스택 아래에 올바르게 집계되도록 보장합니다.
Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.
I