해결된 질문
작성
·
56
·
수정됨
0
안녕하세요 강사님, 강의 잘 듣고 있습니다.
개발 병행하면서 강의 수강 중인데, 끝까지 다 듣지 못한 점 미리 양해드립니다.
LangGraph 공식 문서를 보면, Graph의 상태 스키마를 정의할 때 Pydantic의 BaseModel
보다 TypedDict
나 dataclass
를 사용하는 걸 더 권장하는 것처럼 보입니다.
강사님께서도 강의에서 주로 TypedDict
를 쓰시는 걸 확인했습니다.
그런데 개발을 하다 보니, TypedDict
는 런타임 유효성 검사나 구조화 파싱 기능이 없어서
LLM 응답이 해당 스키마에 맞게 출력되었는지 보장할 수 없는 점이 불편하게 느껴졌습니다.
예를 들어, PydanticOutputParser(pydantic_object=MyModel)
처럼 출력 형식을 강제할 수 있는 기능은TypedDict
에는 없어서, 결국 출력 파싱이 명확하지 않거나 "```json ... ```"
처럼 마크다운이 붙는 문제도 자주 발생합니다.
물론 TypedDict
는 속도 면에서 이점이 있고 LangGraph state로는 잘 어울린다는 것도 알고 있지만,
이런 이유 때문에 결국 스키마를 TypedDict
와 Pydantic
두 번 정의해야 하는 상황이 종종 생깁니다.
그래서 질문드리고 싶은 건 다음과 같습니다:
하나의 스키마 정의만으로 상태 관리와 LLM 출력 파싱까지 모두 깔끔하게 처리하는 더 좋은 방법은 없을까요?
혹은 실무에서는 이런 문제를 보통 어떻게 해결하고 계신지도 궁금합니다.
감사합니다!
답변 2
0
답변 감사합니다!
Chain에 PydanticOutputParser
로 output parser를 이어 붙이면, LLM의 답변이 원하는대로 parsing이 됩니다. 그러나 그외의 방식으로 쓰게 되면, 파싱이 자주 실패합니다. Gemini 모델을 쓰는 경우 답변 앞뒤로"```json ... ```"
가 붙게 되어서 결국 이미 존재하는 TypedDict
용 StrOutputParser
를 써도 파싱이 실패하고, 결국 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 해줘야하는 문제가 발생합니다.
0
안녕하세요! 제가 질문을 잘 이해한건지 살짝 혼란스럽지만 답변을 드려보자면,
상태를 정의할 때는 말씀하신 것처럼 TypedDict
로 충분해서 딱히 고민을 해본적은 없습니다. 출력 파싱이 LLM의 실행결과를 의미하는 거라면, LangGraph에서 관리되는 상태와는 연관짓지 않고 지금 작업하시는 것처럼 하는게 맞는 것 같습니다.
LangGraph의 상태는 에이전트가 활용하는 messages
를 제와하고는 개발자가 수동으로 업데이트 하게되기 때문에, 만약 사용자의 최종 답변으로 활용할 용도로 상태안에 dict
와 같은 값을 넣으셨다면, 해당 값을 직접 파싱해서 넣어주는게 더 LangGraph-스러운 방법인 것 같아요. LLM 실행 결과는 말씀하신 것처럼 output parser를 활용하시는 편이 더 자연스러운 것 같아요
혹시 제가 질문을 잘못이해한거라면 다시 답글로 달아주세요!
설명이 조금 미흡했던 부분이 있는 것 같아서 보충해서 말씀드리겠습니다.
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를 사용중인데, 말씀하신것처럼 모두 AIMessage
의 content
에 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
인스턴스만 교체하고 기존의 파이프라인을 유지하면 되는데, 모델별로 커스텀을 하게되면 나중에 확장성이 떨어져서 불편하실 것 같아요. 다만 이건 개인적인 의견이고 팀마다 추구하는 바가 다를 수도 있으니 팀에서 정해서 하시면 될 것 같습니다
답변 감사합니다 제공해주신 링크 덕분에 큰 도움이 되었습니다. 제가 ChatGoogleGenerativeAI 와 GoogleGenerativeAI 를 혼동 했었네요. ChatModels의 경우에는 AIMessage로 리턴되는것을 확인했습니다. 채팅 위주의 애플리케이션을 개발하지 않더라도 일정한 output을 기대하려면 가급적이면 ChatModel이나 pydantic으로 파싱해서 쓰는게 좋을 것 같네요.
아 네네 이해하신바가 맞습니다. PydanticOutputParser가 동작하는 방식이 처음 작성하신 예시코드라서 TypedDict하고는 연동이 안될거에요.
그리고 파이썬 특성상 response: LogState와 같이 작성해도 타입을 강제할 수도 없어서 처음에 작성하신 형태로 진행하셔야합니다.
State는 랭그래프에서 활용되는 값이고 output parser는 LLM 실행 결과를 어떻게 받고싶냐는 용도이기 때문에 분리해서 생각하시는 편이 좋을 것 같습니다