-
LangChain ToolRuntime으로 런타임 컨텍스트 주입하기IT 2026. 6. 23. 21:00
LangChain 도구를 작성하다 보면 곧 이런 질문에 부딪힌다. "이 비서 에이전트가 현재 응대 중인 사용자의 이름을 알아야 하는데, 어디서 받아오지?" 함수 인자로 넘기면 에이전트가 LLM에게 인자를 생성하게 하므로, 사용자가 이름을 직접 말하지 않는 이상 값을 알 방법이 없다. 하드코딩하면 도구가 재사용 불가능해진다.
ToolRuntime은 이 문제를 "LLM이 채우는 인자"와 "실행 시점에 주입되는 컨텍스트"를 구분하는 방식으로 해결한다.문제: 실행 시점 데이터를 어떻게 전달하나
일반적인 도구 함수는 LLM이 인자를 생성한다. 사용자 ID나 이름처럼 "호출하는 쪽에서 미리 알고 있는" 데이터는 LLM이 생성하는 게 적절하지 않다 — LLM이 이 값을 모르기 때문이다. 그렇다고 전역 변수나 클로저를 쓰면 테스트와 재사용이 어려워진다.
이 다이어그램은 문제의 핵심을 보여준다. 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"), # 호출자가 주입 )전체 데이터 흐름
데이터가 invoke 진입점에서 시작해 에이전트 내부를 통과하는 경로를 따라간다. LLM은 도구를 선택하고 인자를 생성하는 단계에서 일반 인자만 채우고 runtime 파라미터는 건드리지 않는다. 그 앞의 에이전트 내부 ToolRuntime 생성 단계에서 LangChain 프레임워크가
context_schema를 참고해ToolRuntime객체를 만들고context필드에 invoke 시 전달된SecretaryContext인스턴스를 채워 넣는다. 이 연결이 성립하려면context_schema=SecretaryContext등록이 반드시 선행되어야 한다.context_schema 없을 때 vs 있을 때
❌ context_schema 미등록
✅ context_schema 등록 후
두 흐름을 별도 블록으로 분리한 이유는 분기점이 에이전트 생성 시점(정적)이기 때문이다. 미등록 상태에서는 실행 시마다 오류가 발생하고, 등록 후에는 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가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
Retriever를 에이전트 도구로 — RAG 패턴 구현 (0) 2026.06.24 LLM에 코드 실행 능력 붙이기 — PythonAstREPLTool (0) 2026.06.24 Pydantic Field로 LLM 출력 스키마를 제약하는 방법 (0) 2026.06.23 ProviderStrategy vs ToolStrategy — 구조화 출력 전략 선택 (0) 2026.06.23 LangChain @tool 데코레이터의 3요소 — 에이전트가 도구를 이해하는 방법 (0) 2026.06.22 LangChain이 LLM을 다루는 방식: 추상화, 팩토리, 체이닝 (0) 2026.06.22 LangChain이 등장한 이유 — LLM 시대의 새로운 개발 패러다임 (0) 2026.06.22 JSON-RPC의 id는 누가 정하고 충돌하면 어떻게 되나 (0) 2026.06.21 서브에이전트를 200% 활용하는 노하우 — description부터 병렬 실행까지 (0) 2026.06.20 플러그인으로 서브에이전트 배포하기 — /plugin install부터 마켓플레이스까지 (0) 2026.06.19