728x90
반응형

하하, 드디어 약 2개월 만에 테디노트의 RAG 비법노트 강의 후기를 다시 쓰게 되었네요.

 

취준도 병행하다 보니 미뤄지기도 했고, 8월부터는 그동안 들었던 내용을 잊지 않으려고 매일 복습을 이어가고 있었습니다.

 

이번 글에서는 지난 내용에 이어 OutputParser, 특히 PydanticOutputParser를 활용한 미니 프로젝트를 소개하려 합니다. 이 프로젝트에서는 메일 본문에서 핵심 내용을 추출하고, 메일 발신자와 관련된 추가 정보를 SerpAPI 검색을 통해 자동으로 보여주는 기능을 구현했습니다.

 

그럼 바로 시작해볼까요?


1. Pydantic이란?

먼저 이번 프로젝트의 핵심 중 하나인 Pydantic에 대해 간단히 짚고 넘어가겠습니다.

 

Pydantic은 Python에서 데이터 검증(validation)과 데이터 구조화(modeling)를 쉽게 해주는 라이브러리입니다. 예를 들어, API에서 받아온 JSON이나 사용자 입력값 같은 데이터를 다룰 때, 타입이 맞지 않거나 값이 누락되면 에러가 발생하기 쉽습니다.

 

Pydantic을 사용하면 이런 데이터를 Python 클래스 형태로 정의하고, 자동으로 타입 검증을 해주기 때문에 안정적으로 다룰 수 있습니다.

 

간단히 말해:

  • 데이터 검증: 문자열, 숫자, 리스트, 딕셔너리 등이 지정한 타입에 맞는지 확인
  • 자동 변환: "123" 같은 문자열도 int로 변환해주는 등 편리한 타입 캐스팅
  • 모델링: 데이터 스키마를 클래스 형태로 정의해서 직관적으로 관리 가능

 

예를 들어, 메일 데이터가 있다고 할 때, 보낸 사람, 제목, 내용을 Pydantic 모델로 정의해 두면, 들어온 JSON이 올바른 형식인지 한 번에 검증할 수 있습니다.

 

from pydantic import BaseModel

class Mail(BaseModel):
    sender: str
    subject: str
    content: str

mail = Mail(sender="test@example.com", subject="회의 공지", content="내일 2시에 회의 있습니다.")
print(mail)

 

이렇게 하면, Mail 클래스는 단순한 Python 객체가 아니라 검증된 데이터 구조가 됩니다. 덕분에 이후에 OutputParser와 결합할 때도 훨씬 깔끔하게 데이터를 다룰 수 있습니다.


2. OutputParser와 PydanticOutputParser

 

LangChain에서는 모델의 출력을 그냥 문자열 그대로 두지 않고, 구조화된 형태로 변환할 수 있는 기능을 제공합니다. 이게 바로 OutputParser입니다.

 

예를 들어 GPT에게 "메일 내용에서 핵심 키워드를 뽑아줘"라고 요청했을 때, 모델이 단순히 "회의, 일정, 내일" 같은 문자열을 반환할 수도 있지만, 우리가 원하는 건 정해진 형식의 JSON이나 객체일 때가 많습니다. 이럴 때 OutputParser를 쓰면, 모델이 생성한 텍스트를 자동으로 파싱해서 우리가 정의한 구조로 변환해줍니다.

 

그중에서도 가장 강력한 도구가 바로 PydanticOutputParser입니다. 앞서 설명한 Pydantic 모델을 OutputParser에 연결하면, 모델의 출력이 자동으로 해당 Pydantic 구조에 맞게 변환됩니다.

 

즉,

  • 📩 메일에서 보낸 사람, 제목, 핵심 내용을 뽑아내고,
  • 🛠️ 이걸 Mail 같은 Pydantic 클래스 구조로 곧바로 매핑

할 수 있는 거죠.

 

사용 예시

from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

class MailSummary(BaseModel):
    sender: str = Field(description="메일 발신자")
    subject: str = Field(description="메일 제목")
    summary: str = Field(description="메일 핵심 요약")

parser = PydanticOutputParser(pydantic_object=MailSummary)

 

 

이제 GPT가 "보낸 사람: John / 제목: Project Update / 내용: 내일 미팅 예정" 같은 결과를 내놓으면, parser가 이걸 MailSummary 객체로 바꿔줍니다.


3. SerpAPI란?

이제 메일 본문에서 핵심 정보(발신자, 제목, 요약 등)를 추출했으니, 남은 건 발신자에 대한 추가 정보를 자동으로 가져오는 일입니다. 여기서 활용할 수 있는 도구가 바로 SerpAPI입니다.

 

SerpAPI는 검색 엔진 결과 페이지(SERP: Search Engine Results Page)를 API 형태로 제공하는 서비스입니다.

 

즉, 구글이나 네이버처럼 직접 크롤링을 하지 않아도, API 호출 한 번으로 검색 결과를 구조화된 JSON 데이터로 받아올 수 있습니다.


SerpAPI의 장점

  • 🔍 자동화된 검색: 원하는 키워드로 구글, 유튜브, 아마존 등 다양한 엔진에서 검색 가능
  • 📦 구조화된 데이터: HTML 파싱할 필요 없이 JSON으로 바로 결과 활용
  • ⚙️ 맞춤 옵션: 언어(hl), 지역(gl), 검색 개수(num) 등 세부 파라미터 지정 가능

 

이번 프로젝트에서는 메일 발신자의 이름이나 이메일을 검색어로 넣어서, 관련된 공개 정보(예: 블로그, 소셜 계정, 관련 기사)를 가져오는 방식으로 활용했습니다.

 

SerpAPI 코드 설명

먼저, SerpAPI를 사용하려면 API Key를 발급받아 환경 변수로 등록해야 합니다.

 

import os

# 발급받은 SerpAPI 키를 환경 변수에 등록
os.environ["SERPAPI_API_KEY"] = "YOUR_API_KEY"

 

그다음, LangChain이 제공하는 SerpAPIWrapper를 사용해 검색 객체를 만듭니다.

from langchain_community.utilities import SerpAPIWrapper

# 검색 엔진 파라미터 설정
params = {
    "engine": "google",  # 검색 엔진
    "gl": "kr",          # 지역(한국)
    "hl": "ko",          # 언어(한국어)
    "num": "3"           # 검색 결과 개수
}

search = SerpAPIWrapper(params=params)

 

이제 search.run("검색어")를 실행하면, 구글에서 검색한 결과를 JSON 형태로 받아볼 수 있습니다.

result = search.run("정지원 AI 연구원")
print(result)

 

이 결과를 메일 발신자 정보와 함께 보여주면, 메일 요약 + 발신자 검색 결과까지 자동으로 얻을 수 있는 미니 프로젝트가 완성되는 거죠 🎉


5. 프로젝트: 메일 요약 + 발신자 검색 결과 결합

마지막으로, 지금까지 만든 체인(메일 → PydanticOutputParser → 구조화된 객체)과 SerpAPI를 연결하면, 메일 요약 + 발신자 검색 결과를 한 번에 얻을 수 있습니다.

 

그럼, 실제로 예시를 만들어서 진행해보겠습니다.


(1) 이메일 예시 준비

실제 이메일 내용을 하나 만들어서 실험에 사용합니다.

 

email_conversation = """From: jiwon jeong (jwjw9603@skku.edu)
To: 돌멩e 대리님 (dolmeng@mail.net)
Subject: 방송 방제 관련 문의

안녕하세요, 돌멩e 대리님,

저는 jiwon jeong입니다. 최근 방송에서 방제 관련 내용을 다루고 있어서 몇 가지 궁금한 점이 생겨 이렇게 이메일을 드립니다.
1. 방송에서 다룬 방제 방법 중 가장 효과적인 방법은 무엇인가요?
2. 방제 시 주의해야 할 점이나 팁이 있다면 알려주실 수 있을까요?
3. 추가로 참고할 만한 자료나 링크가 있다면 공유 부탁드립니다.   

jiwon jeong 드림"""

 

(2) 이메일 요약을 위한 Pydantic 모델 정의

 

메일에서 필요한 주요 엔티티(발신자, 수신자, 제목, 요약 등)를 담는 Pydantic 모델을 정의합니다.

 

# 이메일 본문으로부터 주요 엔티티 추출
from pydantic import BaseModel, Field

class EmailSummary(BaseModel):
    sender_name: str = Field(description="이메일을 보낸 사람")
    sender_email: str = Field(description="이메일을 보낸 사람의 이메일 주소")
    recipient_name: str = Field(description="이메일을 받은 사람")
    recipient_email: str = Field(description="이메일을 받은 사람의 이메일 주소")
    subject: str = Field(description="이메일 제목")
    meeting_date: str = Field(description="제안된 미팅 날짜")
    meeting_time: str = Field(description="제안된 미팅 시간")
    meeting_location: str = Field(description="제안된 미팅 장소")
    summary: str = Field(description="이메일의 주요 내용 요약")

 

(3) LLM과 OutputParser연결

 

PydanticOutputParser를 활용해 LLM이 뱉은 결과를 EmailSummary 구조로 변환합니다.

 

from langchain_core.output_parsers import PydanticOutputParser
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o", temperature=0)
output_parser = PydanticOutputParser(pydantic_object=EmailSummary)

 

(4) 프롬프트 설계 & 체인 실행

 

PromptTemplate을 활용해 이메일 본문을 LLM에 전달하고, 파싱된 결과를 받아옵니다.

 

 

from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate.from_template(
    """
    You are a helpful assistant. Please answer the following questions in KOREAN.
    
    #QUESTION:
    다음의 이메일 내용 중에서 주요 내용을 추출해 주세요.
    
    #EMAIL CONVERSATION:
    {email_conversation}
    
    #FORMAT:
    {format}
    """
)
prompt = prompt.partial(format=output_parser.get_format_instructions())

# 체인 생성
chain = prompt | llm | output_parser

# 체인 실행
answer = chain.invoke({"email_conversation": email_conversation})
print(answer)

 

출력 결과는 다음과 같습니다.

 

sender_name='jiwon jeong' sender_email='jwjw9603@skku.edu' recipient_name='돌멩e 대리님' recipient_email='dolmeng@mail.net' subject='방송 방제 관련 문의' meeting_date='' meeting_time='' meeting_location='' summary='jiwon jeong은 방송에서 다룬 방제 방법 중 가장 효과적인 방법, 방제 시 주의해야 할 점이나 팁, 추가로 참고할 만한 자료나 링크에 대해 문의하고 있습니다.'

 

(5) 발신자 정보 추가 검색 (SerpAPI)

 

이제 추출된 발신자 이름/이메일을 검색어로 넣어, 추가 정보를 자동으로 가져옵니다.

 

from langchain_community.utilities import SerpAPIWrapper
import os

os.environ["SERPAPI_API_KEY"] = "YOUR_API_KEY"

params = {"engine": "google", "gl": "kr", "hl": "ko", "num": "3"}
search = SerpAPIWrapper(params=params)

query = f"{answer.sender_email} {answer.sender_name}"
search_result = search.run(query)
print(search_result)

search_result = eval(search_result)
search_result_string = "\\n".join(search_result)

 

출력 결과는 다음과 같습니다:

 

['Contact : jwjw9603@g.skku.edu I am interested in Natural Language Processing and Commonsense Reasoning for the Next of QnA System. 할 수 있다!!!', 'Jiwon Jeong. Sungkyunkwan University (SKKU). g.skku.edu의 이메일 확인됨. Natural Language ProcessingLLMsLogical Fallacy. 학술자료인용. 제목. 정렬.', '• Jimin An, als398@skku.edu, Master, 2024. • Jiwon Jeong, jwjw9603@g.skku.edu, Master, 2024. • JunKoo Lee, dlwnsrn0727@g.skku.edu, Master, 2024. • HyunSung Kim ...']

 

(6) 최종 리포트 생성

 

마지막으로, 메일 요약 정보와 검색 결과를 합쳐 정리합니다.

 

from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

report_prompt = PromptTemplate.from_template(
    """당신은 이메일의 주요 정보를 바탕으로 요약 정리해 주는 전문가 입니다.
당신의 임무는 다음의 이메일 정보를 바탕으로 보고서 형식의 요약을 작성하는 것입니다.
주어진 정보를 기반으로 양식(format)에 맞추어 요약을 작성해 주세요.

#Information:
- Sender: {sender}
- Additional Information about sender: {additional_information}
- Recipient: {recipient}
- Subject: {subject}
- Meeting Date: {meeting_date}
- Meeting Time: {meeting_time}
- Meeting Location: {meeting_location}
- Summary: {summary}

#FORMAT(in markdown format):
🙇‍♂️ 보낸 사람:
- (보낸 사람의 이름, 회사 정보)

📧 이메일 주소:
- (보낸 사람의 이메일 주소)

😍 보낸 사람과 관련하여 검색된 추가 정보:
- (검색된 추가 정보)

✅ 주요 내용:
- (이메일 제목, 요약)

⏰ 일정:
- (미팅 날짜 및 시간)

#Answer:"""
)

report_chain = (
    report_prompt | ChatOpenAI(model="gpt-4-turbo", temperature=0) | StrOutputParser()
)

report_response = report_chain.invoke(
    {
        "sender": answer.sender_name,
        "additional_information": search_result_string,
        "recipient": answer.recipient_name,
        "subject": answer.subject,
        "meeting_date": answer.meeting_date,
        "meeting_time": answer.meeting_time,
        "meeting_location": answer.meeting_location,
        "summary": answer.summary,
    }
)
print(report_response)

 

최종 결과 예시

'
🙇‍♂️ 보낸 사람:
- Jiwon Jeong, Sungkyunkwan University (SKKU)

📧 이메일 주소:
- jwjw9603@g.skku.edu

😍 보낸 사람과 관련하여 검색된 추가 정보:
- Jiwon Jeong은 자연어 처리 및 상식 추론에 관심이 있으며, QnA 시스템의 다음 단계에 대한 연구를 진행 중입니다. 그는 성균관대학교에서 석사 과정을 밟고 있으며, 자연어 처리, LLMs, 논리적 오류에 대한 학술 자료를 인용하고 있습니다.

✅ 주요 내용:
- 제목: 방송 방제 관련 문의
- 요약: Jiwon Jeong은 방송에서 다룬 방제 방법 중 가장 효과적인 방법, 방제 시 주의해야 할 점이나 팁, 추가로 참고할 만한 자료나 링크에 대해 문의하고 있습니다.

⏰ 일정:
- 미팅 날짜 및 시간: 정보 없음

 

정확도는 다소 아쉽지만, 기대 이상으로 쓸 만한 정보들을 얻을 수 있었습니다.


마무리

이번 프로젝트에서는 LangChain의 OutputParserPydantic을 활용해 메일 본문에서 주요 정보를 깔끔하게 추출하고, 여기에 SerpAPI 검색 결과까지 결합해보았습니다.

  • 📩 PydanticOutputParser 덕분에 LLM 출력이 단순 텍스트가 아닌 구조화된 데이터 객체로 변환되어 훨씬 다루기 편해졌습니다.
  • 🔍 SerpAPI를 활용하면 발신자 정보를 자동으로 보강할 수 있어서, 이메일 분석 결과를 더 풍부하게 만들 수 있었습니다.
  • 🔗 두 기능을 연결해보니, 단순한 "LLM 답변"을 넘어서 외부 검색과 결합된 작은 RAG 파이프라인을 구현하는 경험을 할 수 있었습니다.

물론 검색 결과가 항상 100% 정확하거나 원하는 정보만 주는 건 아니지만, "LLM + 외부 도구 결합"의 가능성을 확인하기에는 충분했습니다.

앞으로는 이 아이디어를 확장해서,

  • 회사 내부 메일 자동 요약 + 인물/기업 정보 검색
  • 고객 문의 자동 분석 + 관련 자료 검색
  • 등 다양한 서비스로 응용할 수 있을 것 같습니다.

다음 시간에는 이 프로젝트를 Streamlit으로 구현해보도록 하겠습니다.

728x90
반응형

+ Recent posts