-
ProviderStrategy vs ToolStrategy — 구조화 출력 전략 선택IT 2026. 6. 23. 22:00
LLM에게 "JSON으로 줘"라고 말하는 것과, 스키마를 강제로 씌워 정확한 타입으로 받는 것은 전혀 다른 이야기다. 구조화 출력(Structured Output)은 후자다. 모델 응답을 미리 정의한 Pydantic 스키마 형태로 돌려받아, 이후 코드가 타입 안전하게 처리할 수 있도록 보장한다. 그런데 이 구조화 출력을 구현하는 방법이 하나가 아니다. LangChain에는 두 가지 전략이 존재한다 — ProviderStrategy와 ToolStrategy. 두 전략은 모양은 비슷하지만 내부 동작이 전혀 다르고, 잘못 선택하면 성능 저하나 예상치 못한 실패로 이어진다.
구조화 출력이란 무엇인가
보통 LLM의 응답은 자유 형식 텍스트다. 그러나 에이전트나 파이프라인에서는 모델 출력을 다음 단계 코드가 바로 소비해야 한다. "영화 별점을 1~5 사이 정수로 줘"라고 프롬프트를 잘 써도 모델이 "약 3.5점 정도"라고 답하면 파싱이 깨진다. 구조화 출력은 응답 자체를 Pydantic 모델로 강제해서 이 문제를 원천 차단한다.
from pydantic import BaseModel, Field class FeedbackAnalysis(BaseModel): category: str = Field(description="피드백 카테고리") star_rating: int = Field(description="영화 별점 1~5", ge=1, le=5) keywords: list[str] = Field(description="핵심 키워드")위 스키마를 정의하고 모델에 전달하면, 모델 응답은 반드시
category,star_rating(1~10 정수),keywords(리스트)를 갖춘 객체로 돌아온다. 문제는 이 "강제"를 어떤 방식으로 구현하느냐에 따라 동작이 달라진다는 점이다.ProviderStrategy: 모델 자체가 구조화를 책임진다
ProviderStrategy는 모델 제공사(Provider)의 네이티브 기능을 그대로 활용한다. OpenAI의
response_format이나 Anthropic의 tool use 네이티브 JSON 모드처럼, 모델 자체가 스키마를 이해하고 구조화된 응답을 생성한다. LangChain은 스키마만 전달하고 나머지는 모델 API에 맡긴다.흐름이 단순하다. 스키마를 API 파라미터에 실어 보내면 모델이 처음부터 구조에 맞춰 응답한다. 검증에서 실패하면 예외를 던지고 끝이다 — 자동 재시도 없이. 이것이 ProviderStrategy의 장점이자 단점이다. 모델이 네이티브 지원을 하는 경우에는 빠르고 깔끔하지만, 지원하지 않는 모델에서는 아예 사용할 수 없다.
# ProviderStrategy: 모델이 네이티브 지원할 때 agent_provider = create_agent( model=model, tools=[], response_format=FeedbackAnalysis, # 스키마만 전달, 나머지는 모델이 처리 )코드가 간결하다.
response_format에 Pydantic 모델을 넘기는 것이 전부다. 그러나 이 코드가 동작하려면model이 해당 기능을 지원해야 한다. 지원 여부는 LangChain이 런타임에 판단하며, 지원하지 않으면 에러가 발생한다.ToolStrategy: 도구 호출로 구조화를 흉내낸다
ToolStrategy는 접근 방법이 다르다. 스키마를 "도구"로 변환해서 모델이 도구 호출 형태로 응답하게 만든다. 도구 호출은 사실상 모든 최신 모델이 지원하므로 모델 종속성 없이 구조화 출력을 구현할 수 있다. 그리고 한 가지 핵심 기능이 추가된다 — 자동 에러 수정 재시도.
핵심은 검증 실패 시의 루프다. 스키마 검증이 실패하면 ToolStrategy는 예외를 던지는 대신 실패 원인을 메시지로 만들어 모델에게 돌려보낸다. "star_rating이 문자열인데 정수여야 합니다" 같은 피드백을 받은 모델은 스스로 수정해서 다시 응답을 생성한다. 이 과정이 검증을 통과할 때까지 반복된다.
# ToolStrategy: 모든 모델에서 동작, 자동 에러 수정 포함 from langchain.agents.structured_output import ToolStrategy agent_tool = create_agent( model=model, tools=[], response_format=ToolStrategy(FeedbackAnalysis), # 스키마를 ToolStrategy로 감싸기 )ToolStrategy로 스키마를 감싸는 것이 전부다. 코드 레벨에서는 단순하지만, 내부에서는 도구 변환 → 도구 호출 → 검증 → (실패 시) 피드백 → 재시도 루프가 돌아간다.복잡한 스키마일수록 ToolStrategy가 느린 이유
ToolStrategy의 자동 재시도는 양날의 검이다. 단순 스키마에서는 대부분 한 번에 통과하므로 오버헤드가 적다. 그러나 스키마가 복잡해질수록 — 중첩 객체, 열거형 제약, 범위 검증, 조건부 필드 등 — 첫 번째 모델 응답이 모든 제약을 만족하지 못할 확률이 높아진다. 검증-피드백-재시도 루프가 여러 번 돌면 LLM 호출이 그만큼 늘어나고, 각 호출은 수백 밀리초에서 수 초 단위의 지연을 만들어낸다.
어떤 전략을 선택해야 하는가
결정 기준은 세 가지다. 첫째, 모델이 네이티브 구조화 출력을 지원하는가. 지원하지 않으면 선택지는 ToolStrategy뿐이다. 둘째, 스키마가 복잡한가. 단순 스키마라면 ProviderStrategy가 빠르고 예측 가능하다. 셋째, 자동 에러 수정이 필요한가. 파이프라인에서 첫 응답 실패가 치명적이고 재시도 비용을 감수할 수 있다면 ToolStrategy의 자동 수정 루프가 안전망이 된다.
Union 타입으로 다중 스키마 지원
하나의 에이전트가 여러 종류의 응답 스키마를 다뤄야 할 때가 있다. 사용자 입력이 "연락처 정보"일 수도 있고 "일정 정보"일 수도 있는 경우, 고정된 단일 스키마로는 두 상황을 모두 처리할 수 없다. 이럴 때 Python의 Union 타입을
response_format에 전달하면 에이전트가 입력에 따라 적절한 스키마를 자동으로 선택한다.from typing import Union from pydantic import BaseModel class ContactInfo(BaseModel): name: str # 이름 phone: str # 전화번호 class EventDetails(BaseModel): title: str # 일정 제목 date: str # 날짜 (예: 2026-06-08) # 두 스키마 중 하나를 자동 선택 — 입력 내용에 따라 모델이 판단 agent = create_agent( model=model, tools=[], response_format=Union[ContactInfo, EventDetails], )Union[ContactInfo, EventDetails]를 전달하면 모델은 사용자 입력을 보고 두 스키마 중 더 적합한 것을 선택해 채운다. "홍길동, 010-1234-5678"이 들어오면ContactInfo로, "팀 회의, 2026-06-10"이 들어오면EventDetails로 응답한다. 단, Union 타입 사용 시에는 ProviderStrategy와 ToolStrategy의 지원 여부가 달라질 수 있다. 모델이 Union을 네이티브로 처리하지 못하는 경우 ToolStrategy가 각 스키마를 별도 도구로 변환해 선택지를 제공하는 방식으로 처리한다. 여러 스키마를 다루는 파이프라인일수록 ToolStrategy의 자동 수정 루프가 더 중요해지는 이유이기도 하다.Union 타입의 장점은 단일 에이전트로 다양한 입력 유형을 처리할 수 있다는 점이다. 입력 유형마다 별도 에이전트를 만들거나 분기 로직을 작성할 필요 없이, 스키마 정의만으로 모델이 분기를 담당한다. 다만 Union 멤버가 많아지거나 스키마 간 필드가 겹치면 모델이 잘못된 스키마를 선택하는 경우가 생길 수 있으므로, 각 스키마의
Field(description=...)를 명확하게 작성해 모델의 판단을 돕는 것이 중요하다.정리
구조화 출력 전략은 "둘 중 하나가 항상 좋다"는 결론이 없다. ProviderStrategy는 빠르고 단순하지만 모델 종속적이고 실패 시 방어막이 없다. ToolStrategy는 느릴 수 있지만 어디서나 동작하고 모델 스스로 출력을 고치는 루프를 내장한다. 내가 쓰는 모델이 무엇인지, 스키마가 얼마나 복잡한지, 파이프라인이 첫 시도 실패를 어떻게 처리하는지를 함께 고려해야 올바른 선택에 이른다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
Retriever를 에이전트 도구로 — RAG 패턴 구현 (0) 2026.06.24 LLM에 코드 실행 능력 붙이기 — PythonAstREPLTool (0) 2026.06.24 Pydantic Field로 LLM 출력 스키마를 제약하는 방법 (0) 2026.06.23 LangChain ToolRuntime으로 런타임 컨텍스트 주입하기 (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