LLM

구조화된 llm 응답받기: Langchain Structured Output

Luti 2025. 10. 5. 10:16

 

그동안 블로그 쓸 겨를이 없었고,

앞으로도 조오금 바쁠 것 같은데 괜히 글 하나 끄적이고 싶어서 몇 달 전에 다뤘던 내용에 대해 짧게 공유해 보겠습니다.

 

 

# 00. 배경 및 문제 상황

 

RAG 웹 애플리케이션 작업 중이었습니다.

LLM 컴포넌트에서 생성한 응답을 백엔드에서 받아 비즈니스 로직을 처리하고 프론트엔드로 전달해 응답에 따른 맞춤 UI를 렌더링해야 하는 상황이었는데, 문제는 LLM이 생성해 주는 응답은 하나의 긴 문자열이라는 것이었습니다.

 

few_shot 프롬프팅을 통해 LLM이 json 형태의 구조화된 응답을 강요한다고 하더라도 결국 응답을 전달받는 클라이언트 쪽에서 역직렬화하여 값을 파싱 해야 하는 번거로움과 위험성이 있었고,

파싱 로직이 완벽하다 한들 결국 프롬프팅으로 해결하는 방법은 LLM 모델 자체에 크게 의존하게 되는 것이라, 모델이 조금이라도 다른 형태로 응답을 출력한다면 서비스 장애로 이어질 수 있는 상황이었습니다.

 

 

# 01. with_strucutred_output()

 

이 문제를 저는 Langchain에서 제공하는 with_strucutred_output() 기능을 이용하여 해결하고자 했습니다.

문서의 개요가 딱 저의 상황과 일치해서 기대감을 가지고 시도했고요.

 

이 Structured Output의 핵심 아이디어는 "출력 구조를 사전에 스키마로 정의한다"입니다.

 

스키마를 정의하는 방식은 Pydantic과 json mode가 있습니다.

Pydantic은 파이썬 애플리케이션에서 데이터 유효성 검사를 쉽게 할 수 있도록 돕는 라이브러리이며,

대부분의 상황에서는 타입 안전하게 Pydantic을 이용해 스키마를 정의하는 게 좋다고 생각합니다만, 혹시나 프로젝트에서 Pydantic을 사용하지 않는 경우 직접 json 형태로 템플릿을 작성하여 스키마를 등록할 수 있습니다.

 

Pydantic을 이용해 구조화된 답변을 생성하는 과정을 가장 간소화한 예시코드를 작성해 보았습니다.

from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import Literal
import json
import os

os.environ["OPENAI_API_KEY"] = (
    
)


# Pydantic 스키마
class CharacterCreationSchema(BaseModel):
    """만화 캐릭터 만들기 스키마"""

    name: str = Field(description="캐릭터의 이름")
    age: str = Field(description="캐릭터의 나이")
    sex: Literal["남", "여"] = Field(description="캐릭터의 성별 ('남' 또는 '여')")
    job: str = Field(description="캐릭터의 직업 (예: 학생, 선생님, 개발자, 마법사 등)")
    background: str = Field(
        description="캐릭터의 배경 이야기 (예: 어디서 왔는지, 어떤 가족이 있는지 등)"
    )


# 프롬프트
prompt = """=
당신은 만화 캐릭터 생성기입니다.
아래 조건에 맞는 캐릭터를 만들어주세요.
조건:
- 이름: 하나의 단어로 된 한국어 이름
- 나이: 10대에서 30대 사이
- 성별: 남 또는 여
- 직업: 현실적이거나 판타지 세계에 어울리는 직업
- 배경 이야기: 간단한 배경 이야기 (2-3문장)
"""

# OpenAI 객체 생성
base_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 응답
structured_model = base_model.with_structured_output(CharacterCreationSchema)
pydantic_result: CharacterCreationSchema = structured_model.invoke(prompt)

# 확인
print("=== pydantic_result ===")
print(pydantic_result)
print("\n=== JSON 형태로 변환 ===")
print(json.dumps(pydantic_result.model_dump(), ensure_ascii=False, indent=2))

 

출력은 아래와 같습니다.

 

llm api 콜을 한 서버에서 직접 응답을 다룰 경우 result 자체를 객체처럼 이용하시면 되고, 응답을 받는 클라이언트가 따로 있을 경우 Pydantic의 model_dump() 함수를 이용해 json 형태로 변환하여 전달할 수 있습니다.

 

 

# 02. Tool Calling

 

추가적으로 bind_tools() 함수를 사용하여 Pydantic 스키마를 모델에 직접 등록하는 방법이 있는데, 위에서 소개해드린 with_structured_output() 방식이 도구 등록과 출력 파싱이 자동으로 되어 사용하기에 더 편리하지 않나 싶습니다.

 

Tool Calling 방식으로 구조화된 응답 생성하기 (Langchain - Structured Output 문서 예시 코드)

from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o", temperature=0)

# ResponseFormatter 스키마를 tool로 바인딩
model_with_tools = model.bind_tools([ResponseFormatter])

# 모델 호출
ai_msg = model_with_tools.invoke("What is the powerhouse of the cell?")

 

 

 

 

실제 프로젝트에서 작성된 코드는 아래 레포지토리를 확인해 주세요.

https://github.com/LeeEuyJoon/Carrer-HY

 

 


 

문제는 이 구조화된 답변 기능이 모든 llm 모델에 적용 가능한 것이 아니라는 점인데, 해당 프로젝트에서는 지금 다양한 RAG 파라미터를 조작해 가며 가장 성능이 좋은 조합으로 최종 서비스 형태를 결정하기로 했습니다.

혹여나, json 출력을 지원하지 않는 모델로 최종 결정이 된다면, 그때는 인트로에서 말씀드린 방법대로 프롬프팅에 의존해서 응답을 파싱 해야 할 듯싶습니다.

 

 

🌕 즐추 보내세요 🌕