상세 컨텐츠

본문 제목

LangGraph를 이용한 서비스 구현

AI 관련/Langgraph

by rakyun 2025. 8. 5. 16:32

본문

랭그래프의 탄생 이유

  • RAG(Retrieval-Augmented Generation)을 사용하면서 우리는 아래의 문제를 마주하게 된다.
    • LLM이 생성한 답변이 Hallucination인 경우
    • RAG를 적용하여 받은 답변이 문서에는 없는 "사전지식"으로 답변한 경우
      • 즉 Vector DB에 없는 LLM을 만들때 적용된 사전지식
    • 문서 검색에서 원하는 내용이 없을 경우
      • 인터넷 혹은 논문에서 부족한 정보를 검색하여 지식을 보강할 수는 없을까?

즉 langchain을 사용하여 RAG를 진행하면서 ai가 잘못된 정보를 가져오는 상황이 생기게 된다. 그럴때마다 그 답변을 처리하는 로직을 추가하게 되면 RAG 코드가 너무 길어지게 되고 유지보수가 힘들어지게 된다. 이를 해결하기 위해 랭그래프가 나오게 되었다.

또한 langchain은 비순환 그래프 구조로 작동한다. 무슨 말이냐면 A -> B -> C처럼 정해진 파이프라인을 따라 순서대로만 흘러가게 된다. 이 구조는 간단한 RAG 애플리케이션을 만드는 데는 효율적이지만, 나온 아웃풋을 검증하고 자료가 없으면 새로 인터넷에서 검색하는 등의 분기처리, 순환 구조에서는 코드를 작성하기가 까다롭다.

ToolCalling

LLM을 외부 기능이나 데이터에 접근할 수 있게 해줌으로써 LLM의 한계를 극복하는 방법
  • LLM이 작업을 하던 중에 외부 자료가 필요하다면 외부 자료를 검색하는 도구를 추가해줄 수 있다.
  • 간단하게 2 * 3을 연산하는 함수를 하나 정의해놓고 "2 * 3은 ?" 이라는 유저의 질문에 2 * 3을 연산하는 함수를 도구처럼 사용하는 것이다.

Tavily

  • 웹 검색 도구
  • AI 기반의 웹 검색 API를 제공하는 서비스
  • 인증키: 환경변수 TAVILY_API_KEY를 설정해줘야 함
tool = TavilySearch(max_results=3, description="인터넷 검색 기능")
tools = [tool]


llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.0)
llm_with_tools = llm.bind_tools(tools)
  • 위 코드와 같이 사용할 도구를 정의하고 그 도구를  llm.bind_tools() 함수를 사용해서 바인딩 해준다.
  • 만약 도구가 여러 개라면 각 도구의 description을 읽고 가장 알맞은 도구를 찾아 실행한다.

Few-shot

  • 모델에게 몇 가지 예시를 제공하여 원하는 출력 형식이나 작업 수행 방식을 보여주는 기법
  • 모델에게 도구를 어떻게 사용해야 하는지 예시를 통해 보여주는 목적으로도 사용
examples = [
    HumanMessage("트러플 리조또의 가격과 특징, 그리고 어울리는 와인에 대해 알려주세요.", name="example_user"),
    AIMessage("메뉴 정보를 검색하고, 위키피디아에서 추가 정보를 찾은 후, 어울리는 와인을 검색해보겠습니다.", name="example_assistant"),
    AIMessage("", name="example_assistant", tool_calls=[{"name": "search_menu", "args": {"query": "트러플 리조또"}, "id": "1"}]),
    ToolMessage("트러플 리조또: 가격 ₩28,000, 이탈리아 카나롤리 쌀 사용, 블랙 트러플 향과 파르메산 치즈를 듬뿍 넣어 조리", tool_call_id="1"),
    AIMessage("트러플 리조또의 가격은 ₩28,000이며, 이탈리아 카나롤리 쌀을 사용하고 블랙 트러플 향과 파르메산 치즈를 듬뿍 넣어 조리합니다. 이제 추가 정보를 위키피디아에서 찾아보겠습니다.", name="example_assistant"),
    AIMessage("", name="example_assistant", tool_calls=[{"name": "wiki_summary", "args": {"query": "트러플 리조또", "k": 1}, "id": "2"}]),
    ToolMessage("트러플 리조또는 이탈리아 요리의 대표적인 리조또 요리 중 하나로, 고급 식재료인 트러플을 사용하여 만든 크리미한 쌀 요리입니다. 주로 아르보리오나 카나롤리 등의 쌀을 사용하며, 트러플 오일이나 생 트러플을 넣어 조리합니다. 리조또 특유의 크리미한 질감과 트러플의 강렬하고 독특한 향이 조화를 이루는 것이 특징입니다.", tool_call_id="2"),
    AIMessage("트러플 리조또의 특징에 대해 알아보았습니다. 이제 어울리는 와인을 검색해보겠습니다.", name="example_assistant"),
    AIMessage("", name="example_assistant", tool_calls=[{"name": "search_wine", "args": {"query": "트러플 리조또에 어울리는 와인"}, "id": "3"}]),
    ToolMessage("트러플 리조또와 잘 어울리는 와인으로는 주로 중간 바디의 화이트 와인이 추천됩니다. 1. 샤르도네: 버터와 오크향이 트러플의 풍미를 보완합니다. 2. 피노 그리지오: 산뜻한 산미가 리조또의 크리미함과 균형을 이룹니다. 3. 베르나차: 이탈리아 토스카나 지방의 화이트 와인으로, 미네랄리티가 트러플과 잘 어울립니다.", tool_call_id="3"),
    AIMessage("트러플 리조또(₩28,000)는 이탈리아의 대표적인 리조또 요리 중 하나로, 이탈리아 카나롤리 쌀을 사용하고 블랙 트러플 향과 파르메산 치즈를 듬뿍 넣어 조리합니다. 주요 특징으로는 크리미한 질감과 트러플의 강렬하고 독특한 향이 조화를 이루는 점입니다. 고급 식재료인 트러플을 사용해 풍부한 맛과 향을 내며, 주로 아르보리오나 카나롤리 등의 쌀을 사용합니다. 트러플 리조또와 잘 어울리는 와인으로는 중간 바디의 화이트 와인이 추천됩니다. 특히 버터와 오크향이 트러플의 풍미를 보완하는 샤르도네, 산뜻한 산미로 리조또의 크리미함과 균형을 이루는 피노 그리지오, 그리고 미네랄리티가 트러플과 잘 어울리는 이탈리아 토스카나 지방의 베르나차 등이 좋은 선택이 될 수 있습니다.", name="example_assistant"),
]
  • 사고 과정과 행동 순서를 보여주는 '모범 답안', 즉 LLM에게 어떤 식으로 사고해라를 정의해놓는것.
  • 정의 해놓은 examples을 ChatPromptTemplate()에 넣어주면 ai가 답변을 생성할때 미리 예시를 참고해서 생성하게 된다.

랭그래프 서비스 구현

 

  • 사용자의 인풋을 참고하고 미리 저장해둔 예시 프롬프트를 활용하여 ai 아웃풋을 만드는 서비스

  • 프로그램 시작시에 참고해야할 예시 프롬프트가 160개 정도 되기에 vector DB에 임베딩 시켜서 저장한다.

  • 사용자의 request로 DB에서 임베딩 된 프롬프트를 조회 하는 prepare 노드 생성

  • 위에서 few-shot 프롬프트를 state에 담아 넘겨주면 아웃풋을 생성하는 generate 노드 생성

  • Hallucination이거나 request에 없는 내용으로(few-shot 프롬프트에만 있는 내용) 결과를 생성하는 것을 방지하기 위해 validate 노드 생성

  • validate 후에 잘못된 부분을 수정하기 위한 fix 노드 생성
    • generate 노드로 다시 되돌릴 수 있지만 fix 노드를 둔 이유는 확장성 때문이다.
      fix 노드는 generate 노드보다 조금 더 어려운 작업을 수행하기에 더 똑똑한 LLM을 호출해야 할 수도 있다. 그리고 여러 문장 중 한 문장만 잘못되었는데 모든 문장을 재생성 하는 generate로 가면 토큰의 낭비가 심하기에 fix 노드를 두었다.

랭그래프 서버

langgraph up
또는
langgraph dev
  • 위 명령어를 통해 랭그래프 서버를 띄울 수 있다.

이렇게 랭그래프 서버를 띄우면 좋은 점이 여러가지 있다.

  • 상태를 저장해주는 postgreSQL db와의 연동
  • 들어오는 작업을 처리해주는 큐를 만드는 redis와의 연동
  • langsmith에서 토큰 사용량 및 노드 간의 결과와 상태값 확인 가능
  • 이것들 외에도 많은 이점이 있어서 langgraph up을 해서 사용하는 것이 권장된다.

그러나 조금 불편한 점도 있다.

  • 기본 랭그래프 서버의 설계 사상은 대화의 상태(thread_id)를 관리하는 주체가 API를 호출하는 클라이언트라고 가정한다.
    그래서 클라이언트가 원할 때 스레드를 만들고(POST /threads), 원할 때 실행하며(POST /runs), 상태를 조회할 수 있도록 각 기능별로 API를 분리해서 제공하게 된다.

  • 이러면 랭그래프 서버를 호출하는 앞단에서는 thread 생성 api 호출 한 번, thread 실행 api 호출 한 번, 상태 조회 api 호출 한 번으로 총 3번의 api 호출을 하게 된다.

  • 또한 랭그래프 서비스 호출에 인증 단계를 추가하거나 api 엔드포인트를 임의로 정하거나 response를 임의로 정하는 것은 불가능하다.

해결 방안

  • 랭그래프 서버를 호출하는 앞단에서 이렇게 3번의 api 호출과 각 호출마다 예외를 처리해야하는 코드가 쌓이게 되는 것이 좋지 않다고 생각을 해서 나는 랭그래프 서버를 호출하고 결과를 반환해줄 프록시 fast api 서버를 구축했다.

  • 이렇게 해서 엔드포인트도 마음대로 정의가 가능하고 랭그래프에 넘겨줄 데이터도 가공이 가능하고 리턴 값 또한 마음대로 정의할 수 있다. 

실행 결과

  • 지금 첨부된 사진을 보면 generate 노드에서 6초 validate 노드에서 5초 fix 노드에서 다시 6초 총 17초의 시간이 걸리는 것을 볼 수 있다. 내 생각에는 시간이 좀 오래 걸린다고 생각이 들었다.

    그래서 generate 노드에서 90퍼센트 이상의 확실한 결과를 내고 validate 노드에서는 정말 크리티컬한 에러만 잡아내도록 프롬프트 템플릿을 고도화 하여 validate 노드에서 리젝을 당하지 않고 generate 노드만으로 생성된 결과를 리턴하도록 하였다.

  • generate 노드의 프롬프트를 더 고도화하여 validate 노드에서는 정말 크리티컬한 문제만 잡도록 수정했다. 그 결과 17초에서 -> 13초 정도로 감소 되었다.

  • 토큰의 소요량도 감소했다. ai가 하는 일은 프롬프트를 확실하게 정의해서 최대한 한 번에 확실한 결과를 뽑아내게끔 만드는 것이 효율적인 것 같다.

'AI 관련 > Langgraph' 카테고리의 다른 글

랭그래프 참고자료  (0) 2025.11.06

관련글 더보기