티스토리 뷰

반응형

LangChain 출력 제어하기 — Structured Output으로 JSON 응답을 안정적으로 받는 방법

LangChain을 처음 공부할 때, 많은 사람이 여기서 한 번 멈춥니다.
저도 그랬어요.

모델이 답은 잘하는데…
막상 서비스에 붙이려니까 애매한 거예요.

프론트엔드는 이런 걸 원하죠.

  • 제목
  • 요약
  • 난이도
  • 태그 목록
  • 추천 액션

그런데 모델은 이렇게 답합니다.

“좋습니다! 아래처럼 정리할 수 있습니다…”
그리고 중간에 설명도 붙고, 줄바꿈도 섞이고, 가끔 키 이름도 바뀝니다.

이때 딱 느끼게 됩니다.
아, 사람이 읽기 좋은 답변이랑 서비스가 쓰기 좋은 답변은 완전히 다르구나.

그래서 필요한 게 Structured Output입니다.
LangChain 문서는 structured output을 자연어를 직접 파싱하는 대신, JSON 객체·Pydantic 모델·dataclass 같은 예측 가능한 형식의 데이터를 얻는 방식으로 설명합니다. 또 에이전트에서는 response_format으로 구조화된 응답을 다룰 수 있고, 모델 레벨에서는 별도의 structured output 기능을 사용할 수 있다고 안내해요. (LangChain Docs)


왜 이게 중요한가

주니어 때는 “일단 답만 잘 나오면 되지” 싶습니다.
근데 백엔드 API나 실제 서비스로 가면 바로 달라져요.

예를 들어 블로그 도우미 API를 만든다고 해보죠.
우리가 원하는 건 이런 응답입니다.

{
  "title": "LangChain Structured Output 이해하기",
  "summary": "자연어 응답 대신 JSON 형태로 안정적으로 받는 방법",
  "difficulty": "beginner",
  "tags": ["LangChain", "Structured Output", "Python"]
}

이런 형태가 나오면 프론트에서 그대로 쓰기 쉽습니다.

  • 카드 UI 렌더링 가능
  • DB 저장 가능
  • 검증 가능
  • 실패 처리 가능
  • API 스펙 문서화 가능

반대로 그냥 일반 텍스트로 받으면 매번 파싱 지옥이 열립니다.
OpenAI 공식 문서도 structured outputs는 모델 최종 응답을 특정 스키마에 맞춰 받고 싶을 때 적합하다고 설명하고 있어요. (OpenAI Developers)


오늘 글에서 다룰 것

이번 글에서는 2가지를 나눠서 볼 겁니다.

  1. 모델 직접 호출에서 structured output 쓰기
  2. 에이전트에서 structured output 쓰기

이 둘은 비슷해 보이지만, 용도가 조금 다릅니다.

  • 모델 직접 호출: 그냥 “결과를 JSON/Pydantic으로 받고 싶다”
  • 에이전트: 툴 사용까지 포함한 흐름에서 마지막 결과를 구조화하고 싶다

LangChain 문서도 에이전트의 structured output 페이지와 모델 관련 structured output 사용처를 구분해서 안내합니다. 에이전트에서는 ProviderStrategy와 ToolStrategy 개념이 나오고, 모델 쪽은 .with_structured_output() 같은 방식을 중심으로 설명됩니다. (LangChain Docs)


1. 가장 먼저 알아야 할 개념: “JSON처럼 보이는 텍스트”는 부족하다

이건 정말 중요합니다.

많은 초보 예제가 이렇게 갑니다.

prompt = "아래 형식의 JSON으로만 답해줘: { ... }"

그리고 모델이 JSON 비슷하게 주면 성공했다고 생각해요.
근데 실제론 이 방식이 불안정합니다.

왜냐면 모델이 가끔 이렇게 하거든요.

  • 앞에 설명 문장 추가
  • 뒤에 부연 설명 추가
  • 키 이름 살짝 변경
  • 배열 대신 문자열 반환
  • enum 값 이상하게 생성

그래서 structured output의 핵심은
**“JSON으로 써달라고 부탁하는 것”**이 아니라
**“스키마에 맞는 결과를 강제하고 검증하는 것”**입니다.

LangChain 문서도 structured output의 목적을 “예측 가능한 형식”이라고 분명히 설명하고 있고, ToolStrategy는 provider-native structured output이 없거나 신뢰하기 어려울 때 사용할 수 있다고 안내합니다. (LangChain Docs)


2. Pydantic 모델로 출력 스키마 정의하기

반응형

Python에서 이 작업을 할 때 가장 실무적인 방법 중 하나가 Pydantic입니다.
왜냐면 백엔드에서 원래 많이 쓰는 검증 도구이기도 하거든요.

예를 들어 블로그 초안 요약 결과를 이런 구조로 받고 싶다고 해보겠습니다.

from pydantic import BaseModel, Field
from typing import Literal

class BlogSummary(BaseModel):
    title: str = Field(description="글 제목")
    summary: str = Field(description="한두 문장 요약")
    difficulty: Literal["beginner", "intermediate", "advanced"] = Field(
        description="독자 난이도"
    )
    tags: list[str] = Field(description="추천 태그 목록")

이렇게 해두면 좋은 점이 많습니다.

  • 어떤 필드가 필요한지 명확함
  • 타입이 정해짐
  • enum처럼 제한 가능
  • 결과 검증 가능

즉, 이제부터 모델 응답은 “그냥 텍스트”가 아니라
검증 가능한 데이터 계약(contract) 이 됩니다.


3. 모델 직접 호출에서 structured output 사용하기

모델 직접 호출은 가장 먼저 익혀야 합니다.
LangChain에서 모델은 invoke()로 호출하는 것이 기본이고, structured output은 모델 래퍼나 통합 기능을 통해 스키마에 맞는 결과를 받도록 구성할 수 있습니다. LangChain 문서는 모델 호출의 기본 메서드로 invoke를 설명하고, structured output은 모델 레벨에서도 사용할 수 있다고 안내합니다. (LangChain Docs)

아래는 바로 따라칠 수 있는 예제입니다.

pip install -U "langchain[openai]" pydantic
import os
from typing import Literal

from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model


class BlogSummary(BaseModel):
    title: str = Field(description="글 제목")
    summary: str = Field(description="한두 문장 요약")
    difficulty: Literal["beginner", "intermediate", "advanced"] = Field(
        description="독자 난이도"
    )
    tags: list[str] = Field(description="추천 태그 목록")


def main() -> None:
    if not os.environ.get("OPENAI_API_KEY"):
        raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")

    model = init_chat_model("gpt-5-nano", model_provider="openai")
    structured_model = model.with_structured_output(BlogSummary)

    result = structured_model.invoke(
        "LangChain PromptTemplate을 처음 배우는 주니어 개발자를 위한 블로그 글 개요를 만들어줘."
    )

    print(result)
    print(type(result))
    print(result.model_dump())


if __name__ == "__main__":
    main()

이 코드의 핵심은 이 부분입니다.

structured_model = model.with_structured_output(BlogSummary)

즉, 모델에게
“이제부터 너의 응답은 BlogSummary 스키마에 맞춰서 내놔”
라고 걸어두는 거예요.

이 방식은 일반 텍스트 대신, Pydantic 모델 인스턴스처럼 다룰 수 있는 결과를 돌려받는 게 핵심입니다. structured output 문서도 Pydantic 모델이나 JSON 객체처럼 애플리케이션이 바로 사용할 수 있는 형식을 얻는다고 설명합니다. (LangChain Docs)


4. 이게 왜 진짜 편한가

이제부터 결과를 문자열 파싱하지 않아도 됩니다.

예를 들면 이런 식으로 바로 씁니다.

print(result.title)
print(result.difficulty)
print(result.tags)

그리고 API 응답으로 내보낼 때도 훨씬 깔끔하죠.

return result.model_dump()

이게 정말 큽니다.
예전에는 모델 응답에서 억지로 json.loads() 하다가 실패하고,
예외 처리하고,
가끔 문자열 앞뒤 잘라내고…
진짜 보기 싫은 코드가 계속 붙었거든요.

Structured output을 쓰면 그 싸움을 많이 줄일 수 있습니다.


5. enum, 리스트, 필수 필드가 왜 중요한가

서비스에서는 “대충 비슷한 답”보다
정해진 타입이 훨씬 중요합니다.

예를 들어 difficulty를 문자열 자유 입력으로 두면 이런 문제가 생길 수 있어요.

  • "easy"
  • "beginner"
  • "초급"
  • "입문"
  • "매우 쉬움"

사람은 이해하지만, 서비스는 피곤해집니다.

그래서 이런 건 enum 제한이 좋습니다.

difficulty: Literal["beginner", "intermediate", "advanced"]

이렇게 하면 프론트 필터, DB 저장, 분석 대시보드까지 다 편해집니다.

또 tags: list[str]처럼 타입을 명확히 해두면
배열로 와야 할 자리에 긴 문자열 한 줄이 오는 문제를 많이 줄일 수 있습니다.


6. 실무형 예제: 블로그 글 분석 API 응답 만들기

이제 조금 더 백엔드다운 예제를 보겠습니다.
사용자가 글 초안을 넣으면, 구조화된 분석 결과를 돌려주는 겁니다.

import os
from typing import Literal

from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model


class BlogAnalysis(BaseModel):
    title: str = Field(description="추천 제목")
    one_line_summary: str = Field(description="한 줄 요약")
    target_reader: str = Field(description="추천 독자층")
    difficulty: Literal["beginner", "intermediate", "advanced"] = Field(
        description="글 난이도"
    )
    key_points: list[str] = Field(description="핵심 포인트 목록")
    recommended_tags: list[str] = Field(description="추천 태그")
    needs_code_example: bool = Field(description="코드 예제가 필요한지 여부")


def main() -> None:
    if not os.environ.get("OPENAI_API_KEY"):
        raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")

    draft = """
    LangChain은 프롬프트, 모델, 툴, 리트리벌을 조합해
    생성형 AI 애플리케이션을 더 구조적으로 개발하게 도와준다.
    """

    model = init_chat_model("gpt-5-nano", model_provider="openai")
    structured_model = model.with_structured_output(BlogAnalysis)

    result = structured_model.invoke(
        f"""
        아래 블로그 초안을 분석해서 구조화된 결과를 반환해줘.

        초안:
        {draft}
        """
    )

    print(result.model_dump())


if __name__ == "__main__":
    main()

이런 식으로 받아두면
이제 프론트엔드는 title, recommended_tags, needs_code_example을 바로 쓸 수 있습니다.

여기서 중요한 건,
모델이 글을 “잘 쓰는 것”보다도
서비스가 사용 가능한 데이터로 잘 내주는 것입니다.


7. 에이전트에서 structured output은 어떻게 다를까

여기부터는 한 단계 올라갑니다.

에이전트는 단순 답변 모델이 아니라,
툴을 쓰고, 중간 판단을 하고, 마지막 응답을 만드는 흐름입니다.

LangChain 에이전트 문서는 structured output에서 ProviderStrategy와 ToolStrategy를 설명합니다. ToolStrategy는 인공적인 tool calling 방식으로 구조화 출력을 만들며, provider-native structured output이 없거나 신뢰하기 어려울 때 사용된다고 안내합니다. 또 create_agent(..., response_format=...) 형태로 structured output을 설정할 수 있다고 설명합니다. (LangChain Docs)

즉, 에이전트에서는
“툴을 쓴 뒤 마지막 결과를 어떤 형식으로 반환할 것인가”
가 중요해집니다.

아래 예제를 보죠.

from pydantic import BaseModel, Field
from langchain.agents import create_agent


class WeatherAnswer(BaseModel):
    city: str = Field(description="도시 이름")
    weather_summary: str = Field(description="날씨 요약")
    recommendation: str = Field(description="추천 행동")


def get_weather(city: str) -> str:
    # 실제 서비스에서는 외부 API 호출
    fake_data = {
        "Seoul": "맑음, 14도",
        "Busan": "흐림, 12도",
    }
    return fake_data.get(city, "날씨 정보 없음")


agent = create_agent(
    model="openai:gpt-5-nano",
    tools=[get_weather],
    response_format=WeatherAnswer,
)

result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "서울 날씨를 알려주고 외출 팁도 함께 정리해줘.",
            }
        ]
    }
)

print(result["structured_response"])

여기서 핵심은 이 부분입니다.

response_format=WeatherAnswer

그리고 최종 구조화 응답은 보통:

result["structured_response"]

이 위치에서 꺼내게 됩니다.
LangChain structured output 문서도 에이전트의 structured response가 상태의 structured_response 키에 담긴다고 설명합니다. (LangChain Docs)


8. 모델 직접 호출 vs 에이전트 structured output

이건 꼭 구분해서 기억하면 좋습니다.

모델 직접 호출

이럴 때 좋습니다.

  • 그냥 결과를 구조화해서 받고 싶다
  • 툴 호출은 필요 없다
  • 단일 기능 API를 만들고 있다
  • 요약, 분류, 추출, 태깅 같은 작업

대표 예:

  • 블로그 글 요약
  • 상품 리뷰 분석
  • 이메일 분류
  • 문서 메타데이터 추출

에이전트

이럴 때 좋습니다.

  • 툴 호출이 필요하다
  • DB, 검색, API 조회 후 결과를 구조화해야 한다
  • 단순 응답이 아니라 행동이 포함된다

대표 예:

  • 주문 조회 도우미
  • 날씨+일정 추천 어시스턴트
  • 문서 검색+업무 액션 처리 에이전트

즉, 처음에는 모델 직접 structured output부터 익히는 게 좋고,
툴이 붙기 시작하면 에이전트로 확장하는 게 자연스럽습니다.


9. 실무에서 자주 터지는 문제들

이건 경험상 꼭 적어야 합니다.

1) “JSON으로만 답해줘” 프롬프트에 너무 의존함

이건 초반엔 되지만, 서비스에선 불안정합니다.
가능하면 스키마 기반 방식으로 가는 게 낫습니다. structured output의 목적 자체가 스키마에 맞는 예측 가능한 응답을 얻는 데 있기 때문입니다. (LangChain Docs)

2) 스키마를 너무 복잡하게 만듦

중첩이 지나치게 깊고 optional 필드가 너무 많으면 디버깅이 힘들어집니다.
처음엔 작게 시작하는 게 좋습니다.

3) 사람이 읽기 좋은 텍스트와 서비스용 데이터를 한 번에 다 하려 함

예를 들어 본문 텍스트, 설명, 태그, 점수, 추천 액션을 전부 한 응답에 억지로 넣으려 하면 구조가 흔들리기 쉽습니다.
이럴 땐 분리하는 게 낫습니다.

  • 구조화 데이터용 응답
  • 사용자 표시용 자연어 응답

4) 스키마를 안 바꾸고 프롬프트만 계속 만짐

문제가 응답 품질이 아니라 스키마 설계인 경우도 많습니다.
예를 들어 difficulty를 자유 문자열로 둔 게 원인일 수 있죠.


10. 지금 단계에서 가장 추천하는 연습

이건 정말 효과 좋습니다.

지금 여러분이 만든 지난 예제들을 전부
“자연어 응답”에서 “구조화 응답”으로 바꿔보세요.

예를 들어:

  • LangChain 설명 → title, summary, difficulty
  • PromptTemplate 설명 → concept, example, warning
  • 문서 요약 → summary, keywords, action_items

이 연습을 해보면 바로 감이 옵니다.
“아, 이제 진짜 백엔드 API처럼 보이네.”


11. 이번 글의 전체 실행 코드

모델 직접 structured output 예제를 다시 한 번 실행용으로 정리해둘게요.

import os
from typing import Literal

from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model


class ArticlePlan(BaseModel):
    title: str = Field(description="추천 글 제목")
    summary: str = Field(description="글 요약")
    reader_level: Literal["beginner", "intermediate", "advanced"] = Field(
        description="추천 독자 수준"
    )
    keywords: list[str] = Field(description="핵심 키워드 목록")
    include_code: bool = Field(description="코드 예시 포함 여부")


def main() -> None:
    if not os.environ.get("OPENAI_API_KEY"):
        raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")

    model = init_chat_model("gpt-5-nano", model_provider="openai")
    structured_model = model.with_structured_output(ArticlePlan)

    result = structured_model.invoke(
        """
        LangChain Structured Output을 처음 배우는 개발자를 위한
        블로그 글 기획안을 만들어줘.
        """
    )

    print("=== Pydantic object ===")
    print(result)
    print()

    print("=== dict ===")
    print(result.model_dump())


if __name__ == "__main__":
    main()

마무리

오늘 글이 조금 덜 화려하게 느껴질 수도 있습니다.
RAG처럼 멋있어 보이진 않으니까요.

근데 진짜 서비스 개발에서는 이런 게 훨씬 오래 갑니다.

저는 Structured Output을 처음 제대로 써봤을 때
“와, 드디어 모델 응답을 사람 말이 아니라 데이터로 다루는 느낌이다”
이 생각이 들었어요.

그전까지는 AI가 좀 변덕스러운 동료 같았다면,
그 이후부터는 그래도 계약 가능한 인터페이스처럼 느껴졌습니다.

그 차이가 꽤 큽니다. 진짜로.


다음 글 예고

다음 글에서는
“LangChain 체인 연결하기 — Prompt | Model | Parser 흐름을 코드처럼 조립하는 법”
으로 이어가겠습니다.

이제부터는 점점 더 LangChain다운 감각이 들어옵니다.
각 구성요소를 이어 붙여서, 재사용 가능한 파이프라인처럼 만드는 단계로 넘어갈 거예요.


출처

  • LangChain Structured Output 문서: structured output은 JSON 객체, Pydantic 모델, dataclass 같은 예측 가능한 형식을 반환하며, 에이전트에서는 structured_response 키에 담긴다고 설명. (LangChain Docs)
  • LangChain Agents 문서: ToolStrategy는 provider-native structured output이 없거나 신뢰하기 어려울 때 tool calling 기반으로 구조화 출력을 생성한다고 설명. (LangChain Docs)
  • LangChain Models 문서: 모델의 기본 호출 메서드는 invoke()이며, LangChain이 주요 모델 제공자를 지원한다고 설명. (LangChain Docs)
  • LangChain create_agent 레퍼런스: create_agent(..., response_format=...) 형태의 에이전트 생성 API 제공. (LangChain Reference)
  • OpenAI Structured Outputs 가이드: structured outputs는 모델의 최종 응답을 특정 스키마에 맞게 받고 싶을 때 적합하다고 설명. (OpenAI Developers)


LangChain, Structured Output, Pydantic, LangChain Python, JSON 응답, 생성형AI, LLM개발, AI Agent, 백엔드개발, 주니어개발자

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함
반응형