ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • LangChain ToolRuntime으로 런타임 컨텍스트 주입하기
    IT 2026. 6. 23. 21:00
    LangChain ToolRuntime으로 런타임 컨텍스트 주입하기

    LangChain 도구를 작성하다 보면 곧 이런 질문에 부딪힌다. "이 비서 에이전트가 현재 응대 중인 사용자의 이름을 알아야 하는데, 어디서 받아오지?" 함수 인자로 넘기면 에이전트가 LLM에게 인자를 생성하게 하므로, 사용자가 이름을 직접 말하지 않는 이상 값을 알 방법이 없다. 하드코딩하면 도구가 재사용 불가능해진다. ToolRuntime은 이 문제를 "LLM이 채우는 인자"와 "실행 시점에 주입되는 컨텍스트"를 구분하는 방식으로 해결한다.

    문제: 실행 시점 데이터를 어떻게 전달하나

    일반적인 도구 함수는 LLM이 인자를 생성한다. 사용자 ID나 이름처럼 "호출하는 쪽에서 미리 알고 있는" 데이터는 LLM이 생성하는 게 적절하지 않다 — LLM이 이 값을 모르기 때문이다. 그렇다고 전역 변수나 클로저를 쓰면 테스트와 재사용이 어려워진다.

    diagram

    이 다이어그램은 문제의 핵심을 보여준다. LLM은 도구를 언제 호출할지는 알지만, 실행 환경에 무슨 데이터가 있는지는 모른다. "추측"과 "주입"의 분기가 이 패턴이 존재하는 이유다. LLM이 값을 모른 채 인자로 넘기는 추측 쪽으로 가면 임의의 이름을 생성하거나 아예 인자 생성을 거부해 오류가 발생한다.

    해결: dataclass로 Context 스키마 정의

    ToolRuntime 패턴은 세 단계로 구성된다. 첫째, 컨텍스트 데이터를 담는 dataclass를 정의한다. 둘째, 도구 함수의 파라미터에 ToolRuntime[SecretaryContext]를 선언해 런타임 주입임을 표시한다. 셋째, 에이전트 생성 시 context_schema=SecretaryContext로 등록해 LangChain이 이 파라미터를 LLM 인자가 아닌 주입 대상으로 인식하게 한다.

    from dataclasses import dataclass
    from langchain_core.tools import tool
    from langchain_core.tools.base import ToolRuntime  # 런타임 주입 타입
    
    @dataclass
    class SecretaryContext:
        name: str       # 비서가 응대하는 사용자 이름 — 호출자가 세션 시작 시 주입
        user_id: str    # 세션 식별자 — LLM이 생성하는 게 아니라 호출자가 제공
    
    @tool
    def get_user_name(runtime: ToolRuntime[SecretaryContext]) -> str:
        """현재 사용자의 이름을 반환한다."""
        # SecretaryContext를 직접 참조하는 게 아니라, runtime 안의 .context 속성으로 접근
        name = runtime.context.name
        return f"사용자 이름은 {name}입니다"
    
    @tool
    def personalize_response(message: str, runtime: ToolRuntime[SecretaryContext]) -> str:
        """메시지를 현재 사용자 이름으로 개인화해 반환한다.
        message: 개인화할 메시지 내용 (LLM이 채운다)
        runtime: 실행 시점 컨텍스트 (LLM이 채우지 않는다)
        """
        # message는 LLM이 생성, runtime은 호출자가 주입
        return f"{runtime.context.name}님, {message}"
    

    personalize_response에서 보듯, 일반 인자(message)와 런타임 인자(runtime)를 한 함수에 섞을 수 있다. LangChain은 context_schema 등록 정보를 보고 ToolRuntime 타입 힌트가 붙은 파라미터는 JSON Schema에서 제외한다. 즉 LLM은 message만 채우고, runtime은 자동으로 주입된다.

    에이전트 등록과 invoke

    from langchain_core.agents import create_react_agent  # 또는 프로젝트에 맞는 에이전트
    
    agent = create_react_agent(
        model=model,
        tools=[get_user_name, personalize_response],
        context_schema=SecretaryContext,  # 이 선언이 없으면 ToolRuntime 주입이 동작하지 않는다
    )
    
    # 실행 시점에 context를 채워서 전달
    result = agent.invoke(
        {"messages": [{"role": "user", "content": "내 이름이 뭐야?"}]},
        context=SecretaryContext(name="홍길동", user_id="user-hong"),  # 호출자가 주입
    )
    

    전체 데이터 흐름

    diagram

    데이터가 invoke 진입점에서 시작해 에이전트 내부를 통과하는 경로를 따라간다. LLM은 도구를 선택하고 인자를 생성하는 단계에서 일반 인자만 채우고 runtime 파라미터는 건드리지 않는다. 그 앞의 에이전트 내부 ToolRuntime 생성 단계에서 LangChain 프레임워크가 context_schema를 참고해 ToolRuntime 객체를 만들고 context 필드에 invoke 시 전달된 SecretaryContext 인스턴스를 채워 넣는다. 이 연결이 성립하려면 context_schema=SecretaryContext 등록이 반드시 선행되어야 한다.

    context_schema 없을 때 vs 있을 때

    ❌ context_schema 미등록

    diagram

    ✅ context_schema 등록 후

    diagram

    두 흐름을 별도 블록으로 분리한 이유는 분기점이 에이전트 생성 시점(정적)이기 때문이다. 미등록 상태에서는 실행 시마다 오류가 발생하고, 등록 후에는 LangChain이 파라미터 분류를 한 번에 처리한다. 흔한 실수는 context_schema를 빠뜨린 채 ToolRuntime 타입 힌트만 선언하는 경우인데, 이때 에러 메시지가 타입 힌트 쪽을 가리켜 원인을 찾기 어렵다. 항상 context_schema 등록 여부를 먼저 확인해야 한다.

    핵심 오해 방지: runtime.context vs SecretaryContext

    가장 많이 헷갈리는 부분은 접근 경로다. 도구 함수 안에서 컨텍스트 값을 꺼낼 때 SecretaryContext.name이나 context.name이 아니라 반드시 runtime.context.name으로 접근해야 한다. ToolRuntime[SecretaryContext]는 제네릭 타입 선언으로, "이 runtime 객체의 .context 속성이 SecretaryContext 타입"임을 명시한다. SecretaryContext는 타입 힌트를 위한 정보이고, 실제 값은 runtime.context 안에 들어 있다.

    # 잘못된 접근 — SecretaryContext는 클래스 정의일 뿐, 인스턴스 값이 없음
    # name = SecretaryContext.name  # AttributeError
    
    # 올바른 접근 — runtime 객체의 .context 속성에서 꺼냄
    name = runtime.context.name
    

    정리

    ToolRuntime 패턴은 "LLM이 결정하는 것"과 "호출자가 제공하는 것"을 명확히 분리한다. dataclass로 컨텍스트 스키마를 정의하면 타입 안정성도 함께 얻는다. context_schema 등록은 이 패턴이 동작하는 스위치이고, runtime.context는 값을 꺼내는 단일 경로다. 이 세 가지 — dataclass 정의, context_schema 등록, runtime.context 접근 — 를 세트로 기억하면 런타임 의존성 주입을 안정적으로 구현할 수 있다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.