Inflearn brand logo image

인프런 커뮤니티 질문&답변

Seunggu Kang님의 프로필 이미지
Seunggu Kang

작성한 질문수

LangGraph를 활용한 AI Agent 개발 (feat. MCP)

2.6 SubGraph: LangGraph Agent를 Node로 활용하는 방법

schema 질문

해결된 질문

작성

·

56

·

수정됨

0

안녕하세요 강사님, 강의 잘 듣고 있습니다.


개발 병행하면서 강의 수강 중인데, 끝까지 다 듣지 못한 점 미리 양해드립니다.

 

LangGraph 공식 문서를 보면, Graph의 상태 스키마를 정의할 때 Pydantic의 BaseModel보다 TypedDictdataclass를 사용하는 걸 더 권장하는 것처럼 보입니다.
강사님께서도 강의에서 주로 TypedDict를 쓰시는 걸 확인했습니다.

그런데 개발을 하다 보니, TypedDict는 런타임 유효성 검사나 구조화 파싱 기능이 없어서
LLM 응답이 해당 스키마에 맞게 출력되었는지 보장할 수 없는 점이 불편하게 느껴졌습니다.

예를 들어, PydanticOutputParser(pydantic_object=MyModel)처럼 출력 형식을 강제할 수 있는 기능은
TypedDict에는 없어서, 결국 출력 파싱이 명확하지 않거나 "```json ... ```"처럼 마크다운이 붙는 문제도 자주 발생합니다.

물론 TypedDict는 속도 면에서 이점이 있고 LangGraph state로는 잘 어울린다는 것도 알고 있지만,
이런 이유 때문에 결국 스키마를 TypedDictPydantic 두 번 정의해야 하는 상황이 종종 생깁니다.

 

그래서 질문드리고 싶은 건 다음과 같습니다:

하나의 스키마 정의만으로 상태 관리와 LLM 출력 파싱까지 모두 깔끔하게 처리하는 더 좋은 방법은 없을까요?
혹은 실무에서는 이런 문제를 보통 어떻게 해결하고 계신지도 궁금합니다.

감사합니다!

답변 2

0

Seunggu Kang님의 프로필 이미지
Seunggu Kang
질문자

답변 감사합니다!

Chain에 PydanticOutputParser 로 output parser를 이어 붙이면, LLM의 답변이 원하는대로 parsing이 됩니다. 그러나 그외의 방식으로 쓰게 되면, 파싱이 자주 실패합니다. Gemini 모델을 쓰는 경우 답변 앞뒤로"```json ... ```" 가 붙게 되어서 결국 이미 존재하는 TypedDictStrOutputParser 를 써도 파싱이 실패하고, 결국 custom한 Parser를 만들어줘야 해서 번거롭네요.

코드로 예를 들어드리면, 다음과 같은 노드를 만든다고 했을 때 :

 

class LogState(BaseModel):
    project_name: str
    error_level: str
    time_period_hours: int
    environment: str

output_parser = PydanticOutputParser(pydantic_object=LogState)

def log_filter(state: UserQueryState) -> LogState:     
  user_input = state['user_input'] 
  chain = prompt | llm | output_parser     
  response: LogState = chain.invoke({'user_input': user_input})

이런 식으로 할 경우에는 chain 과 pydantic output parsing 에 의해 답변이 원하는 스키마 형태로(LogState) 잘 나오는데 반해,

 

class LogState(TypedDict):
    project_name: str
    error_level: str
    time_period_hours: int
    environment: str

from langchain_core.output_parsers import StrOutputParser

def log_filter(state: UserQueryState) -> LogState:     
  user_input = state['user_input'] 
  chain = prompt | llm | StrOutputParserser     
  response: LogState = chain.invoke({'user_input': user_input})

다음과 같이 쓴다면 response에서 파싱이 안됩니다. 답변 앞뒤로 "```json ... ```" 같은 형태가 붙기 때문입니다. OpenAI 모델을 사용하면 또 다른 형식으로 붙습니다.

결국, Pydantic BaseModel을 사용하면 간단하게 주어진 outputparser 만 끝에 붙이는걸로도 충분히 output 값이 보장되지만, TypedDict를 쓰는 경우에는 모델마다 output 값을 일일이 custom 해줘야하는 문제가 발생합니다.

강병진님의 프로필 이미지
강병진
지식공유자

아 네네 이해하신바가 맞습니다. PydanticOutputParser가 동작하는 방식이 처음 작성하신 예시코드라서 TypedDict하고는 연동이 안될거에요.

 

그리고 파이썬 특성상 response: LogState와 같이 작성해도 타입을 강제할 수도 없어서 처음에 작성하신 형태로 진행하셔야합니다.

 

State는 랭그래프에서 활용되는 값이고 output parser는 LLM 실행 결과를 어떻게 받고싶냐는 용도이기 때문에 분리해서 생각하시는 편이 좋을 것 같습니다

강병진님의 프로필 이미지
강병진
지식공유자

타입이 다른 경우에 IDE에서는 밑줄 그어지면서 오류나는 것처럼 보일 수는 있는데 실제로 동작하는데는 문제 없을거에용

0

강병진님의 프로필 이미지
강병진
지식공유자

안녕하세요! 제가 질문을 잘 이해한건지 살짝 혼란스럽지만 답변을 드려보자면,

상태를 정의할 때는 말씀하신 것처럼 TypedDict로 충분해서 딱히 고민을 해본적은 없습니다. 출력 파싱이 LLM의 실행결과를 의미하는 거라면, LangGraph에서 관리되는 상태와는 연관짓지 않고 지금 작업하시는 것처럼 하는게 맞는 것 같습니다.

LangGraph의 상태는 에이전트가 활용하는 messages를 제와하고는 개발자가 수동으로 업데이트 하게되기 때문에, 만약 사용자의 최종 답변으로 활용할 용도로 상태안에 dict 와 같은 값을 넣으셨다면, 해당 값을 직접 파싱해서 넣어주는게 더 LangGraph-스러운 방법인 것 같아요. LLM 실행 결과는 말씀하신 것처럼 output parser를 활용하시는 편이 더 자연스러운 것 같아요

혹시 제가 질문을 잘못이해한거라면 다시 답글로 달아주세요!

Seunggu Kang님의 프로필 이미지
Seunggu Kang
질문자

설명이 조금 미흡했던 부분이 있는 것 같아서 보충해서 말씀드리겠습니다.

Langchain 에서 제공하는 OpenAI를 쓰는 경우에는 response가 항상 AIMessage 로 감싸져서 나옵니다. 그래서 항상 결과값을 response.content 로 접근하면 원하는 결과값을 도출 할 수 있는데 반해, GeminiAI 같은 경우에는 완전히 다른 식으로 나옵니다. 따라서 Pydantic의 BaseModel로 output 포맷을 강제하지 않으면 두 모델별로 따로 별도의 output 파싱이 필요한데, pydantic 을 사용하면 둘다 '별도'의 output 파싱이 없어도 BaseModel에 맞게 리턴을 해줍니다. 그리고 이 BaseModel로도 충분히 Stateful 하게 노드들의 state로 사용할 수 있으니 BaseModel을 써도 충분 할 것 같지만, 앞서 말한것 처럼 퍼포먼스적인 이슈나 (BaseModel 내에 또 다른 BaseModel이 있는 경우, nested한 타입 체킹으로 인한 속도 저하), 공식문서에서도 TypedDict를 추천하는 점, 강사님께서도 LangGraph 스럽지 않다 하시는 점 등등으로 인해 실무에선 어떤식으로 쓰이는지 궁금했었습니다.

강병진님의 프로필 이미지
강병진
지식공유자

gemini도 ChatGoogleGenerativeAI 로 연동하시는거죠? LangChain에 있는 BaseModel을 상속받는 class 를 활용해서 연동하시는거면 말씀하신 것처럼 다 AIMessage로 감싸져서 나올거에요. 저는 회사에서 OpenAI, AWS Bedrock, Gemini, AzureOpenAI를 사용중인데, 말씀하신것처럼 모두 AIMessagecontent에 LLM 답변 생성 결과가 포함되어 있습니다.

https://python.langchain.com/docs/integrations/chat/google_generative_ai/

물론 개발팀에서 어떤 것을 추구하느냐에 따라 다르겠지만, 특정 모델을 썼을 때 parsing을 잘 해주니까 parsing을 잘 해주는 모델을 쓰기 보다는, 전체적으로 PydanticOutputParser로 감싸주는 편이 좋다고 생각합니다. 말씀하신 타입 체킹으로 인한 퍼포먼스 이슈는 p95, p99 측정했을 때 심각한 수준은 아닐 것 같아요. 프롬프트나 네트워크 속도 이슈등으로 인해 실제 답변을 생성하는데 아마 더 많은 시간이 소요될 것 같습니다

AI 기술은 앞으로 계속 발전할거고, 지금 작업중이신 use case에 어떤 모델이 더 좋은 결과를 낼지 알기 어려운 상황이거든요. 예를들면 지금은 gpt-4o나 gpt-4.1이 좋아보이지만, 내일 나올수도 있는 처음 들어보는 회사의 모델이 특정 use case에 있어서는 더 좋은 성과를 낼 수도 있습니다.

parser를 활용해서 output을 원하는 형태로 강제하면, 새로운 모델이 나올 경우 테스트를 할때도 BaseModel 인스턴스만 교체하고 기존의 파이프라인을 유지하면 되는데, 모델별로 커스텀을 하게되면 나중에 확장성이 떨어져서 불편하실 것 같아요. 다만 이건 개인적인 의견이고 팀마다 추구하는 바가 다를 수도 있으니 팀에서 정해서 하시면 될 것 같습니다

Seunggu Kang님의 프로필 이미지
Seunggu Kang
질문자

답변 감사합니다 제공해주신 링크 덕분에 큰 도움이 되었습니다. 제가 ChatGoogleGenerativeAIGoogleGenerativeAI 를 혼동 했었네요. ChatModels의 경우에는 AIMessage로 리턴되는것을 확인했습니다. 채팅 위주의 애플리케이션을 개발하지 않더라도 일정한 output을 기대하려면 가급적이면 ChatModel이나 pydantic으로 파싱해서 쓰는게 좋을 것 같네요.

Seunggu Kang님의 프로필 이미지
Seunggu Kang

작성한 질문수

질문하기