Structured Output이란 말 그대로 구조화된 출력으로, 정해진 규칙과 규격 내에서 LLM이 답변하게 만드는 방법이다.
LangChain 뿐만 아니라 OpenAI SDK, Google Gemini SDK, Anthropic의 Claude SDK에서도 구조화된 출력을 지원한다.
여기서는 LangChain의 구조화된 출력을 알아보는데, JSON과 Pydantic을 중심으로 알아본다.
LangChain
LangChain의 Output Parser 문서 (링크)를 보면 JSON, CommaSeparatedList, XML 출력 파서들을 지원한다.
Docs의 Structured Output 파트 (링크)를 보면 Pydantic, TypedDict, JSON schema를 지원한다.
LangChain의 출력 파서 Wikidocs (링크) 2024년 7월 버젼.
공통점:
구조화된 출력 생성
차이점:
output parser는 문자열 (string)의 결과를 파싱해서 원하는 구조로 변경
strucutred output은 api의 모델 레벨에서 원하는 구조로 출력을 한다.
여기서는 게임 캐릭터를 예시로 들어본다.
캐릭터의 직업, 레벨, 스킬을 구조적인 출력으로 내뱉고자 한다.
LLM의 with structured output 사용
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from dotenv import load_dotenv
load_dotenv()
# 1. 데이터 구조 정의
class GameCharacter(BaseModel):
job: str = Field(description="직업")
level: int = Field(description="레벨")
skills: list[str] = Field(description="스킬 리스트")
llm = ChatOpenAI(model="gpt-4o")
# 2. 구조화된 출력 설정
structured_llm = llm.with_structured_output(GameCharacter)
# 3. 실행
result = structured_llm.invoke("RPG 게임의 전사 캐릭터 정보를 생성해.")
print(result)
print(result.job) # '전사'
print(result.level) # 10
print(result.skills) # ['검기', '회피', '강화']
job='Warrior' level=35 skills=['Slash', 'Shield Bash', 'War Cry', 'Charge', 'Power Strike', 'Battle Tactics']
-> pydantic model의 결과.
Warrior
35
['Slash', 'Shield Bash', 'War Cry', 'Charge', 'Power Strike', 'Battle Tactics']
Parser 사용
Pydantic Parser
Pydantic model로 출력을 생성한다.
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from dotenv import load_dotenv
load_dotenv()
# 1. 구조 정의
class GameCharacter(BaseModel):
job: str = Field(description="직업")
level: int = Field(description="레벨")
skills: list[str] = Field(description="스킬 리스트")
# 2. 파서 초기화
parser = PydanticOutputParser(pydantic_object=GameCharacter)
# 3. 프롬프트에 포맷 지시사항(format_instructions) 삽입
prompt = PromptTemplate(
template="질문에 답하세요.\n{format_instructions}\n{question}",
input_variables=["question"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
# 4. 체인 구성 (Prompt | LLM | Parser)
chain = prompt | ChatOpenAI() | parser
result = chain.invoke({"question": "RPG 게임의 전사 캐릭터 정보를 생성해."})
print(result.job) # '전사'
print(result.level) # 10
print(result.skills) # ['검기', '회피', '강화']
전사
50
['검술', '갑옷 착용', '전투 기술']
JSON Parser
JSON 형식으로 출력을 생성한다.
from langchain_core.output_parsers import JsonOutputParser
# 2. 파서 초기화
parser = JsonOutputParser(pydantic_object=GameCharacter)
# 3. 프롬프트에 포맷 지시사항(format_instructions) 삽입
prompt = PromptTemplate(
template="질문에 답하세요.\n{format_instructions}\n{question}",
input_variables=["question"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
# 4. 체인 구성 (Prompt | LLM | Parser)
chain = prompt | ChatOpenAI() | parser
result = chain.invoke({"question": "RPG 게임의 전사 캐릭터 정보를 생성해."})
print(type(result)) # <class 'dict'>
print(result)
<class 'dict'>
{'job': '전사', 'level': 50, 'skills': ['검술', '방패 수비', '강타']}
파이썬이라서 JSON과 유사한 딕셔너리 형식으로 반환됨을 확인할 수 있다.
이번에는 Google의 Gemini와 Claude 모델을 활용해서 구조적 출력을 생성하는 법을 알아본다.
Google Gemini
from langchain_google_genai import ChatGoogleGenerativeAI
from pydantic import BaseModel, Field
import os
from dotenv import load_dotenv
load_dotenv()
# 1. 출력 구조 정의
class MovieReview(BaseModel):
title: str = Field(description="영화 제목")
rating: int = Field(description="별점 (1-10)")
pros: list[str] = Field(description="장점 리스트")
# 2. 모델 초기화 (API 키 설정 필요)
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", api_key=os.getenv("GEMINI_API_KEY"))
# 3. 구조화된 출력 적용
structured_llm = llm.with_structured_output(MovieReview)
# 4. 실행
result = structured_llm.invoke("영화 '인셉션'에 대한 짧은 리뷰를 써줘.")
print(result)
title='인셉션' rating=9 pros=['독창적인 스토리와 설정', '시각적으로 압도적인 연출', '몰입감 넘치는 전개', '여운을 남기는 결말']
Anthropic Claude
from langchain_anthropic import ChatAnthropic
from pydantic import BaseModel, Field
from dotenv import load_dotenv
load_dotenv()
# 1. 출력 구조 정의
class CodeAnalysis(BaseModel):
language: str = Field(description="프로그래밍 언어")
complexity: str = Field(description="복잡도 (High, Medium, Low)")
fix_suggestion: str = Field(description="개선 제안")
# 2. 모델 초기화
llm = ChatAnthropic(model="claude-haiku-4-5-20251001")
# 3. 구조화된 출력 적용
structured_llm = llm.with_structured_output(CodeAnalysis)
# 4. 실행
result = structured_llm.invoke("for i in range(10): print(i) 이 코드 분석해줘.")
print(result)
답변 결과
language='Python' complexity='Low' fix_suggestion='코드는 기본적으로 정상이지만, 더 나은 스타일을 위해 다음과 같이 개선할 수 있습니다:\n\n1. 함수로 감싸기:\n```python\ndef print_numbers():\n for i in range(10):\n print(i)\n\nprint_numbers()\n```\n\n2. List comprehension 활용:\n```python\n[print(i) for i in range(10)]\n```\n\n3. enumerate 사용 (인덱스가 필요한 경우):\n```python\nfor index, value in enumerate(range(10)):\n print(value)\n```\n\n현재 코드는 0부터 9까지 출력하는 간단한 루프이며, 특별한 문제는 없습니다.'
OpenAI 모델과 큰 차이점이 없음을 알 수 있다.
JSON Nested 처리
JSON 내부에 JSON이 들어가는 복잡한 형태를 흔히 볼 수 있다.
여기서는 게임 캐릭터의 스킬을 더 별도의 JSON으로 설정한다.
스킬에는 이름, mp 사용량, 레벨이 있다고 가정하면 아래처럼 표시할 수 있다.
character = {
"job": string,
"level: integer,
"skiles":
[
{
"name": string,
"mp": int,
"level": int,
},
]
}
이제 이렇게 중첩된 JSON을 어떻게 Pydantic model로 나타낼 수 있는지만 파악하면 된다.
정답은 간단한데, skills는 skill 객체의 리스트이고, 이 skill 객체를 리스트로 갖는 game character 객체를 설정하면 된다.
from langchain_google_genai import ChatGoogleGenerativeAI
from pydantic import BaseModel, Field
import os
from dotenv import load_dotenv
load_dotenv()
# 1. 구조 정의
class Skill(BaseModel):
name: str = Field(description="스킬 이름")
mp: int = Field(description="마나 소모량")
level: int = Field(description="스킬 레벨")
class GameCharacter(BaseModel):
job: str = Field(description="직업")
level: int = Field(description="레벨")
skills: list[Skill] = Field(description="스킬 리스트")
# 2. 모델 초기화 (API 키 설정 필요)
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", api_key=os.getenv("GEMINI_API_KEY"))
# 3. 구조화된 출력 적용
structured_llm = llm.with_structured_output(GameCharacter)
# 4. 실행
result = structured_llm.invoke("RPG 게임의 전사 캐릭터 정보를 생성해.")
print(result)
기존이 skills = list[str]였던 부분이 list[Skill]로 변했음을 확인할 수 있다.
print(result.skills)
캐릭터의 skills를 출력해서 확인하면 이런 결과를 얻는다.
[Skill(name='돌진', mp=10, level=5), Skill(name='맹렬한 공격', mp=15, level=7), Skill(name='방어 태세', mp=5, level=3)]
이런식으로 계속해서 중첩해 나가면서, 복잡한 데이터를 구조적으로 다룰 수 있다.
'AI Codes' 카테고리의 다른 글
| GPT-OSS-120B을 실제로 실행하는 내용들 (2) | 2025.08.18 |
|---|---|
| 코드와 데이터 라이센스 관련 리서치 (0) | 2025.06.10 |
| IDE에 로컬 LLM 연결 시도 후기 (0) | 2025.04.29 |