런타임

HanJu Han·2025년 12월 28일

런타임(Runtime)과 의존성 주입

1. 런타임(Runtime)이란 무엇인가?

우리가 연극 배우(Agent)를 무대에 세운다고 상상해 봅시다. 배우가 연기를 하려면 대본(LLM)만 있어서는 안 됩니다.

  • Context (맥락): 이 관객이 누구인지(User ID), 지금 무대가 어디인지(DB Connection) 알아야 합니다.
  • Store (저장소): 배우가 이전 공연에서 관객이 무엇을 좋아했는지 적어둔 수첩(Long-term Memory)이 필요합니다.
  • Stream Writer: 관객에게 대사를 실시간으로 전달하는 마이크가 필요합니다.

LangChain의 create_agent는 내부적으로 LangGraph의 런타임 위에서 동작하며, 이 모든 정보를 '런타임 객체'라는 가방에 담아 에이전트, 도구(Tools), 미들웨어(Middleware)가 공유하게 해줍니다.

왜 이것이 중요할까요?
전역 변수(Global Variable)를 쓰지 않고, 실행 시점(Invoke)에 필요한 정보(DB 접속 정보, 사용자 ID 등)를 주입할 수 있기 때문에 테스트가 쉽고 코드가 깔끔해집니다.


2. 시나리오: 이커머스 VIP 쇼핑 어시스턴트

온라인 쇼핑몰의 '개인화 쇼핑 에이전트'를 만기

  • 목표: 사용자의 등급(VIP/General)에 따라 말투를 바꾸고, DB에서 최근 주문 내역을 조회하며, 사용자의 취향(Store)을 기억해야 합니다.
  • 필요한 정보(Context):
    • user_id: 사용자 식별자
    • membership_level: 멤버십 등급
    • db_connection_str: 주문 조회용 DB 연결 문자열

3. 기초 구현: Context 정의하기

가장 먼저 해야 할 일은 이 에이전트가 실행될 때 "어떤 정보들이 가방(Runtime)에 들어있어야 하는지" 규격을 정하는 것입니다.

from dataclasses import dataclass

# 1. 런타임에 주입될 데이터의 구조(Schema)를 정의합니다.
@dataclass
class ShoppingContext:
    user_id: str
    user_name: str
    membership_level: str  # 예: "VIP", "GOLD", "SILVER"
    db_connection_str: str # 실제로는 DB 커넥션 객체일 수 있습니다.

4. 미들웨어(Middleware): 맥락 주입하기

에이전트가 생각(LLM 호출)을 시작하기 전에, 런타임 정보를 이용해 시스템 프롬프트를 동적으로 바꿔봅시다. VIP 고객에게는 더 정중하게 대해야 하니까요.

from langchain.agents.middleware import dynamic_prompt, ModelRequest

# 2. 동적 프롬프트 미들웨어
# 요청(request) 안에는 런타임(runtime) 정보가 들어있습니다.
@dynamic_prompt
def personalize_greeting(request: ModelRequest) -> str:
    # 런타임 컨텍스트에서 정보 꺼내기
    context = request.runtime.context
    
    base_prompt = "당신은 유능한 쇼핑 어시스턴트입니다."
    
    if context.membership_level == "VIP":
        # VIP를 위한 특별 지침 추가
        return f"{base_prompt} 현재 대화 중인 {context.user_name}님은 VIP 고객입니다. 극존칭을 사용하고 배송비 무료 혜택을 강조하세요."
    else:
        return f"{base_prompt} 사용자 {context.user_name}님에게 친절하게 응대하세요."

5. 도구(Tools): 손발이 움직일 때 맥락 사용하기

이제 에이전트가 실제로 데이터를 조회할 때(도구 사용), 런타임 정보를 어떻게 쓰는지 봅니다. 여기서 ToolRuntime을 사용합니다.

from langchain.tools import tool, ToolRuntime

# 3. 도구(Tool) 정의
# ToolRuntime[ShoppingContext]를 통해 타입 힌팅과 함께 런타임에 접근합니다.
@tool
def check_order_status(order_id: str, runtime: ToolRuntime[ShoppingContext]) -> str:
    """사용자의 주문 상태를 확인합니다."""
    
    # 런타임에서 DB 접속 정보와 사용자 ID를 가져옵니다.
    # (함수 인자로 받지 않아도 런타임에서 꺼내 쓸 수 있어 보안에 좋습니다)
    db_conn = runtime.context.db_connection_str
    user_id = runtime.context.user_id
    
    # 가상의 DB 조회 로직
    print(f"[시스템] DB({db_conn})에 접속하여 User({user_id})의 주문({order_id}) 조회 중...")
    
    return f"주문 번호 {order_id}은 현재 '배송 중'입니다."

@tool
def save_user_preference(preference: str, runtime: ToolRuntime[ShoppingContext]) -> str:
    """사용자의 쇼핑 취향(예: 붉은색 선호)을 장기 기억(Store)에 저장합니다."""
    
    user_id = runtime.context.user_id
    
    # 런타임의 Store(장기 기억) 기능 사용
    if runtime.store:
        # ("users", user_id) 라는 키 경로에 취향 저장
        runtime.store.put(("users", user_id), "shopping_pref", {"value": preference})
        return "고객님의 취향을 메모해 두었습니다."
    
    return "기억 저장소(Store)가 연결되지 않았습니다."

6. 조립 및 실행 (Assembly & Invocation)

이제 정의한 스키마, 미들웨어, 도구를 합쳐 에이전트를 만들고, 실제 데이터를 주입하여 실행합니다.

from langchain.agents import create_agent

# 4. 에이전트 생성
agent = create_agent(
    model="gpt-4o", # 예시 모델
    tools=[check_order_status, save_user_preference],
    middleware=[personalize_greeting], # 미들웨어 등록
    context_schema=ShoppingContext     # 컨텍스트 구조 등록
)

# 5. 실행 (Invoke) - 여기서 의존성 주입이 일어납니다!
# 김철수 VIP 고객이 접속했다고 가정합니다.
vip_context = ShoppingContext(
    user_id="user_999",
    user_name="김철수",
    membership_level="VIP",
    db_connection_str="jdbc:mysql://prod-db:3306/shop"
)

response = agent.invoke(
    {"messages": [{"role": "user", "content": "내 주문 1234번 어디쯤 왔어?"}]},
    context=vip_context  # <--- 핵심: 런타임 컨텍스트 주입
)

# 결과 출력 (가정)
# 미들웨어 덕분에 "김철수 VIP 고객님, 주문하신..." 형태로 답변이 생성됩니다.
print(response["messages"][-1].content)

7. 시각화: 런타임 데이터의 흐름 (Mermaid)

이 모든 과정이 어떻게 흘러가는지 한눈에 파악할 수 있도록 도식화해 보겠습니다.


8. 요약

  1. 의존성 주입(Dependency Injection): 에이전트 코드 내부에 DB 주소나 사용자 ID를 하드코딩하지 마세요. context_schemainvoke(context=...)를 통해 실행 시점에 넣어주세요.
  2. 유연성: 같은 에이전트 코드라도 Context만 바꾸면 '일반 고객용 봇'이 되기도 하고 'VIP 전용 봇'이 되기도 합니다.
  3. 접근성:
    • 미들웨어: request.runtime.context로 접근하여 프롬프트를 제어합니다.
    • 도구: runtime: ToolRuntime[...] 인자를 통해 함수 내부에서 안전하게 외부 자원(DB 등)에 접근합니다.
  4. 상태 관리: 단순 변수는 Context, 영구적으로 기억해야 할 데이터는 Store를 활용하세요.

이 구조를 이해하면, 단순히 말만 잘하는 AI가 아니라 실제 비즈니스 로직과 깊게 연동된 지능형 애플리케이션을 구축할 수 있습니다.

profile
시리즈를 기반으로 작성하였습니다.

0개의 댓글