온디맨드 스킬로 SQL 어시스턴트 만들기

온디맨드 스킬로 SQL 어시스턴트 만들기 (Build a SQL assistant with on-demand skills)

이 튜토리얼은 점진적 공개(progressive disclosure) — 에이전트가 모든 정보를 미리 로드하는 대신 온디맨드로 로드하는 컨텍스트 관리 기법 — 를 사용해 스킬(전문화된 프롬프트 기반 지시)을 구현하는 법을 보여줘요. 에이전트는 시스템 프롬프트를 동적으로 바꾸는 대신 도구 호출로 스킬을 로드하고, 각 작업에 필요한 스킬만 발견해서 불러옵니다.

출처: LangChain 공식 문서 — multi-agent/skills-sql-assistant

사용 사례: 대규모 엔터프라이즈에서 여러 비즈니스 버티컬에 걸쳐 SQL 쿼리 작성을 돕는 에이전트를 만든다고 상상해보세요. 조직이 버티컬마다 분리된 데이터스토어를 가질 수도, 수천 개의 테이블이 있는 단일 모놀리식 데이터베이스를 가질 수도 있어요. 어느 쪽이든 모든 스키마를 미리 로드하면 컨텍스트 윈도우가 압도됩니다. 점진적 공개는 필요할 때 관련 스키마만 로드해 이 문제를 해결하죠. 이 아키텍처는 서로 다른 제품 오너와 이해관계자가 자신의 비즈니스 버티컬에 대한 스킬을 독립적으로 기여하고 유지할 수 있게도 합니다.

만들 것: 두 개의 스킬(영업 분석 sales analytics, 재고 관리 inventory management)을 가진 SQL 질의 어시스턴트입니다. 에이전트는 시스템 프롬프트에서 가벼운 스킬 설명을 보고, 사용자 질의와 관련이 있을 때만 도구 호출로 전체 데이터베이스 스키마와 비즈니스 로직을 로드해요.

쿼리 실행·오류 수정·검증이 있는 SQL 에이전트의 완전한 예시는 SQL Agent 튜토리얼을 참고하세요. 이 튜토리얼은 어떤 도메인에도 적용 가능한 점진적 공개 패턴에 집중합니다. 점진적 공개는 확장 가능한 에이전트 스킬 시스템을 만드는 기법으로 Anthropic이 대중화했어요.

어떻게 동작하나요? (How it works)

사용자가 SQL 쿼리를 요청할 때의 흐름: 에이전트는 시스템 프롬프트에서 스킬 목록을 보고, 관련 스킬을 판단한 뒤 도구 호출로 해당 스킬의 전체 내용(스키마와 비즈니스 로직)을 로드합니다. 그 정보를 바탕으로 올바른 쿼리를 작성하죠.

왜 점진적 공개인가요?

  • 컨텍스트 사용량 감소: 모든 스킬이 아니라 작업에 필요한 스킬 2–3개만 로드합니다.
  • 팀 자율성: 서로 다른 팀이 전문화된 스킬을 독립적으로 개발할 수 있어요(다른 멀티 에이전트 아키텍처와 유사).
  • 효율적인 확장: 수십·수백 개의 스킬을 컨텍스트를 압도하지 않고 추가할 수 있어요.
  • 단순한 대화 기록: 단일 에이전트 + 단일 대화 스레드.

스킬이 뭔가요? Claude Code가 대중화한 스킬은 주로 프롬프트 기반입니다: 특정 비즈니스 작업을 위한 자체 포함된 전문화 지시 단위죠. Claude Code에서 스킬은 파일시스템의 파일이 있는 디렉토리로 노출되며 파일 작업으로 발견됩니다. 스킬은 프롬프트로 동작을 안내하고, 도구 사용에 대한 정보를 제공하거나 코딩 에이전트가 실행할 샘플 코드를 포함할 수 있어요.

점진적 공개가 있는 스킬은 **RAG(Retrieval-Augmented Generation)**의 한 형태로 볼 수 있어요. 각 스킬이 검색 단위이죠 — 다만 반드시 임베딩이나 키워드 검색이 아니라 콘텐츠를 탐색하는 도구(파일 작업, 이 튜토리얼에서는 직접 조회)에 기반합니다.

트레이드오프:

  • 지연 시간: 스킬을 온디맨드로 로드하려면 추가 도구 호출이 필요해서, 각 스킬이 필요한 첫 요청에 지연이 추가됩니다.
  • 워크플로 제어: 기본 구현은 프롬프트로 스킬 사용을 안내합니다. 커스텀 로직 없이는 "항상 스킬 A를 스킬 B보다 먼저 시도" 같은 하드 제약을 강제할 수 없어요.

직접 스킬 시스템 구현하기 (Implementing your own skills system)

이 튜토리얼처럼 직접 스킬 구현을 만들 때 핵심 개념은 점진적 공개 — 정보를 온디맨드로 로드하는 것 — 입니다. 그 외에는 구현에 완전한 자유가 있어요:

  • 저장 (Storage): 데이터베이스, S3, 인메모리 데이터 구조, 어떤 백엔드든 가능.
  • 발견 (Discovery): 직접 조회(이 튜토리얼), 대규모 스킬 컬렉션을 위한 RAG, 파일시스템 스캔, API 호출.
  • 로딩 로직: 지연 특성을 커스터마이즈하고 스킬 콘텐츠를 검색하거나 관련성을 랭킹하는 로직 추가.
  • 부수 효과 (Side effects): 스킬이 로드될 때 무엇이 일어날지 정의 — 예를 들어 그 스킬과 연관된 도구 노출(섹션 8에서 다룸).

이 유연성 덕분에 성능, 저장, 워크플로 제어에 대한 특정 요구사항에 맞게 최적화할 수 있어요.

설정 (Setup)

  • 설치: langchain 패키지 필요 (pip install langchain). 설치 가이드 참고.
  • LangSmith: 에이전트 내부를 살펴보도록 설정하고 LANGSMITH_TRACING, LANGSMITH_API_KEY 환경 변수 설정.
  • LLM: LangChain 통합에서 채팅 모델 선택 (예: OpenAI init_chat_model("gpt-5.5")).

1. 스킬 정의 (Define skills)

먼저 스킬의 구조를 정의합니다. 각 스킬은 이름, 간단한 설명(시스템 프롬프트에 표시), 전체 콘텐츠(온디맨드 로드)를 가져요.

from typing import TypedDict

class Skill(TypedDict):
    """A skill that can be progressively disclosed to the agent."""
    name: str  # Unique identifier for the skill
    description: str  # 1-2 sentence description to show in system prompt
    content: str  # Full skill content with detailed instructions

이제 SQL 질의 어시스턴트의 예시 스킬을 정의합니다. 스킬은 설명은 가볍게(에이전트에 미리 보임), 콘텐츠는 상세하게(필요할 때만 로드) 설계됩니다.

2. 스킬 로딩 도구 만들기 (Create skill loading tool)

전체 스킬 콘텐츠를 온디맨드로 로드하는 도구를 만듭니다.

from langchain.tools import tool

@tool
def load_skill(skill_name: str) -> str:
    """Load the full content of a skill into the agent's context.

    Use this when you need detailed information about how to handle a specific
    type of request. This will provide you with comprehensive instructions,
    policies, and guidelines for the skill area.

    Args:
        skill_name: The name of the skill to load (e.g., "expense_reporting", "travel_booking")
    """
    # Find and return the requested skill
    for skill in SKILLS:
        if skill["name"] == skill_name:
            return f"Loaded skill: {skill_name}\n\n{skill['content']}"

    # Skill not found
    available = ", ".join(s["name"] for s in SKILLS)
    return f"Skill '{skill_name}' not found. Available skills: {available}"

load_skill 도구는 스킬의 전체 콘텐츠를 문자열로 반환하며, 이 문자열이 ToolMessage로 대화의 일부가 됩니다.

3. 스킬 미들웨어 구축 (Build skill middleware)

스킬 설명을 시스템 프롬프트에 주입하는 커스텀 미들웨어를 만듭니다. 이 미들웨어는 전체 콘텐츠를 미리 로드하지 않고도 스킬을 발견 가능하게 만들어요. (미들웨어 개념과 패턴의 종합 가이드는 커스텀 미들웨어 문서를 참고하세요.)

from langchain.agents.middleware import ModelRequest, ModelResponse, AgentMiddleware
from langchain.messages import SystemMessage
from typing import Callable

class SkillMiddleware(AgentMiddleware):
    """Middleware that injects skill descriptions into the system prompt."""

    # Register the load_skill tool as a class variable
    tools = [load_skill]

    def __init__(self):
        """Initialize and generate the skills prompt from SKILLS."""
        # Build skills prompt from the SKILLS list
        skills_list = []
        for skill in SKILLS:
            skills_list.append(
                f"- **{skill['name']}**: {skill['description']}"
            )
        self.skills_prompt = "\n".join(skills_list)

    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        """Sync: Inject skill descriptions into system prompt."""
        # Build the skills addendum
        skills_addendum = (
            f"\n\n## Available Skills\n\n{self.skills_prompt}\n\n"
            "Use the load_skill tool when you need detailed information "
            "about handling a specific type of request."
        )

        # Append to system message content blocks
        new_content = list(request.system_message.content_blocks) + [
            {"type": "text", "text": skills_addendum}
        ]
        new_system_message = SystemMessage(content=new_content)
        modified_request = request.override(system_message=new_system_message)
        return handler(modified_request)

미들웨어가 스킬 설명을 시스템 프롬프트에 추가해, 전체 콘텐츠를 로드하지 않고도 에이전트가 사용 가능한 스킬을 알게 해줍니다. load_skill 도구는 클래스 변수로 등록되어 에이전트가 사용할 수 있어요.

프로덕션 고려사항: 이 튜토리얼은 단순함을 위해 __init__에서 스킬 목록을 로드해요. 프로덕션에서는 대신 before_agent 훅에서 스킬을 로드해, 새 스킬이 추가되거나 기존 스킬이 수정될 때 주기적으로 최신 상태를 반영하는 게 좋습니다.

4. 스킬 지원 에이전트 만들기 (Create the agent with skill support)

이제 스킬 미들웨어와 상태 지속을 위한 체크포인터가 있는 에이전트를 만듭니다.

from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

# Create the agent with skill support
agent = create_agent(
    model,
    system_prompt=(
        "You are a SQL query assistant that helps users "
        "write queries against business databases."
    ),
    middleware=[SkillMiddleware()],
    checkpointer=InMemorySaver(),
)

이제 에이전트는 시스템 프롬프트에서 스킬 설명에 접근할 수 있고, 필요할 때 load_skill을 호출해 전체 스킬 콘텐츠를 가져올 수 있어요. 체크포인터가 턴 사이의 대화 기록을 유지합니다.

5. 점진적 공개 테스트 (Test progressive disclosure)

스킬 특화 지식이 필요한 질문으로 에이전트를 테스트합니다.

from langchain_core.utils.uuid import uuid7

# Configuration for this conversation thread
thread_id = str(uuid7())
config = {"configurable": {"thread_id": thread_id}}

# Ask for a SQL query
result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": (
                    "Write a SQL query to find all customers "
                    "who made orders over $1000 in the last month"
                ),
            }
        ]
    },
    config
)

# Print the conversation
for message in result["messages"]:
    if hasattr(message, 'pretty_print'):
        message.pretty_print()
    else:
        print(f"{message.type}: {message.content}")

예상 출력: 에이전트는 시스템 프롬프트에서 가벼운 스킬 설명을 보고, 질문이 영업 데이터베이스 지식을 요구함을 인지한 뒤, load_skill("sales_analytics")를 호출해 전체 스키마와 비즈니스 로직을 가져오고, 그 정보로 데이터베이스 규약을 따르는 올바른 쿼리를 작성합니다. 비즈니스 로직(예: total_amount > 1000이 고액 주문, status = 'completed'만 매출 계산에 포함)이 스키마와 함께 로드되어 쿼리에 반영돼요.

6. 고급: 커스텀 상태로 제약 추가 (Advanced: Add constraints with custom state)

선택 사항으로, 로드된 스킬을 추적하고 도구 제약을 강제할 수 있습니다. 커스텀 상태 필드에 로드된 스킬을 기록해, 미들웨어가 어떤 스킬이 활성화되었는지에 따라 도구 가용성을 제어하는 방식입니다.

다음 단계 (Next steps)

  • 더 동적인 에이전트 동작을 위한 미들웨어 배우기
  • 에이전트 컨텍스트 관리를 위한 컨텍스트 엔지니어링 기법 탐색
  • 순차 워크플로를 위한 handoffs 패턴 탐색
  • 병렬 작업 라우팅을 위한 subagents 패턴 읽기
  • 전문화 에이전트의 다른 접근을 위한 멀티 에이전트 패턴 보기
  • LangSmith로 스킬 로딩 디버깅하고 모니터링하기

더 알아보기 (Learn more)