포스트

챗봇 서비스 구축기

사람인 데이터를 활용하여 구축한 LLM 기반의 챗봇 서비스의 개발 내용을 공유 합니다.

챗봇 서비스 구축기

1. 들어가며

기존 챗봇 서비스는 정해진 규칙에 따라 답변하는 ‘룰 기반(Rule-based)’ 방식이 대부분이었습니다. 그러던 중 OpenAI의 ChatGPT가 등장하며 큰 변화의 바람을 몰고 왔습니다.

저희는 이 기술을 활용해 상담사를 통해서만 가능했던 문의 대응을 자동화할 수 있지 않을까 하고 생각했습니다. 저희 목표는 수년간 축적된 사람인의 도움말, 채용 공고, 기업 정보와 같은 내부 데이터를 LLM과 결합하여 사용자의 1차적인 문의를 해결하는 것이었습니다.

이번 글에서는 위의 목표를 달성하기 위해 구현한 챗봇 서비스 개발 내용을 공유하고자 합니다.

2. 챗봇 서비스 전체 아키텍처

저희가 구축한 챗봇 서비스는 아래와 같이 구성되어 있으며, 사용자의 질문에 답변하기까지 크게 8단계를 거칩니다.

/img/link/image.png

  1. 사용자 질문이 채팅 시스템을 통해 Bot 시스템에 전달됩니다 (①).
  2. Bot 시스템은 먼저 Function Calling을 통해 OpenAI에 질문의 의도 파악과 컨텐츠 조회를 위한 필요한 검색 파라미터를 추출합니다 (②).
  3. 추출된 정보를 바탕으로 검색 서버(bot 시스템 내부에 있음)를 연동하여 HTTP API나 Vector DB에서 질문과 관련된 컨텐츠들을 조회합니다 (③, ④).
  4. 조회된 콘텐츠와 원본 질문, 그리고 미리 정의된 프롬프트를 조합하여 다시 OpenAI에 전달하여 최종 답변을 생성하도록 요청합니다 (⑤, ⑥).
  5. 생성된 답변은 사용자에게 전달되고(⑦), 전체 대화 내용은 이력으로 저장됩니다(⑧).

2-1. Function Calling

아키텍에서 핵심 적인 기술인 ‘Function Calling’에 대해 잠시 추가적으로 말씀드리겠습니다.

Function Calling은 LLM이 대화의 맥락을 이해하고, 스스로 판단하여 미리 정의된 외부 함수(Function)를 호출하도록 요청하는 기능입니다. 이를 통해 LLM은 학습 데이터에 없는 실시간 정보나 내부 시스템 데이터에 접근할 수 있게 됩니다.

개발자가 호출 가능한 함수 목록과 각 함수의 역할, 필요한 파라미터를 JSON 스키마 형태로 LLM에게 제공하면, LLM은 사용자 질문에 가장 적합한 함수가 무엇인지 판단하고 해당 함수를 호출하는데 필요한 파라미터(argument)를 담은 JSON 객체를 생성하여 반환합니다.

Function Calling은 아래의 흐름으로 이루어 지게 됩니다.

/img/link/image.png

▶Function Calling 흐름도 - 출처 : OpenAI Function Calling 가이드
  1. 호출할 수 있는 도구로 모델에게 요청을 보냅니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
     tools = [{
         "type": "function",
         "name": "get_weather",
         "description": "Get current temperature for provided coordinates in celsius.",
         "parameters": {
                 "type": "object",
                 "properties": {
                         "latitude": {"type": "number"},
                         "longitude": {"type": "number"}
                 },
                 "required": ["latitude", "longitude"],
                 "additionalProperties": False
         },
         "strict": True
     }]
    
     input_messages = [{"role": "user", "content": "What's the weather like in Paris today?"}]
    
     response = client.responses.create(model="gpt-4.1", input=input_messages, tools=tools,)
    
    
  2. 모델로부터 도구 호출을(type: funcation call) 받습니다.

    1
    2
    3
    4
    5
    6
    7
    
     [{
         "type": "function_call",
         "id": "fc_12345xyz",
         "call_id": "call_12345xyz",
         "name": "get_weather",
         "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
     }]
    
  3. 도구 호출의 데이터를 이용하여 애플리케이션 측에서 코드 실행(실제 함수 호출) 합니다.

    1
    2
    3
    4
    
     tool_call = response.output[0]
     args = json.loads(tool_call.arguments)
    
     result = get_weather(args["latitude"], args["longitude"])
    
  4. 실행한 코드 결과(함수 반환값)를 사용하여 모델에 두 번째 요청을 보냅니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     input_messages.append(tool_call)  # append model's function call message
     # append result message
     input_messages.append({                               
             "type": "function_call_output",
             "call_id": tool_call.call_id,
             "output": str(result)
     })
    
     response_2 = client.responses.create(model="gpt-4.1", input=input_messages,tools=tools,)
     print(response_2.output_text)
    
  5. 모델로부터 최종 응답을 받습니다.

    1
    
     "The current temperature in Paris is 14°C (57.2°F)."
    

3. RAG 기반 답변 생성기 구현

3.1. 콘텐츠 특성에 맞는 RAG 파이프라인 설계

LLM은 매우 강력한 도구지만, 비즈니스에 바로 적용하기에는 몇 가지 한계를 가지고 있습니다.

흔히 알려진 문제점으로는 아래와 같은 것들이 있습니다.

  • 환각(Hallucination): 근거 없는 정보를 사실처럼 생성합니다.
  • 정보의 시의성: 모델의 학습 시점 데이터에만 의존하여 최신 정보를 반영하지 못합니다.
  • 출처의 신뢰성: 답변이 어떤 정보에 기반했는지 확인할 수 없습니다.

‘사람인 온라인 상담사’를 목표로 하는 저희는 이러한 단점들을 없애고, 도메인에 특화된 정확하고 최신화된 정보를 제공하는 것이 무엇보다 중요했습니다.

이 문제 해결을 위해 검색 증강 생성(Retrieval-Augmented Generation, RAG) 기술을 도입했습니다.

RAG는 LLM이 답변을 생성하기 전, 신뢰할 수 있는 외부 데이터베이스에서 관련 정보를 먼저 검색하고 참조하는 방식입니다.

/img/link/image_aws.png

▶RAG 개념도 - 출처 : AWS 자료

RAG를 도입함으로써 저희는 다음과 같은 이점을 얻을 수 있었습니다.

  • 비용 효율적인 구현: LLM 모델 자체를 재 학습 시킬 필요 없이 RAG를 위한 데이터베이스만 최신으로 유지하면 되기에 비용 효율적입니다.
  • 최신 정보 반영: 특정 시점 까지의 데이터로 학습된 LLM과 달리 RAG는 실시간으로 업데이트되는 최신 데이터까지 답변에 활용할 수 있습니다.
  • 사용자 신뢰 강화: 답변의 근거가 된 출처를 함께 제시할 수 있어 사용자가 정보를 신뢰하고 직접 확인할 수 있습니다. (저희는 서비스 내부에서 사용하는 챗봇으로 별도로 출처 정보는 제공하지 않습니다.)
  • 개발자의 통제권 확보: 정보 소스를 직접 제어하기 때문에 잘못된 정보를 수정하거나 새로운 요구사항을 반영하기 쉽습니다.

✅ 1단계: VectorDB를 활용한 기본 RAG 구현

첫 번째 접근으로 저희는 일반적인 RAG 방식과 같이 VectorDB를 구축하여 정보를 검색하는 방식을 채택했습니다.

사람인 도움말, 내부 FAQ, 상품 정보 등의 정제된 데이터를 VectorDB에 저장하고, 변경 사항은 실시간으로 동기화 되도록 구성했습니다.

/img/link/image.png

▶VectorDB를 활용한 RAG◀

/img/link/image.png

▶컨텐츠에 대한 백터화 및 질문 관련 컨텐츠 검색 방법◀

사람인이 보유한 컨텐츠를 분석 했을 때 컨텐츠의 길이가 길지 않다는 점에 착안하여, 문서를 단순히 분할(split)하는 대신 '제목'과 '내용'을 한 쌍으로 다루기로 했습니다.

그리고 제목과 내용에 대해 각각 별도의 임베딩을 생성하여 저장했습니다.

PoC 과정에서 사용자의 질문 의도와 가장 관련성이 높은 문서를 찾을 때 토큰이 많은 ‘내용’ 본문보다 핵심 의미가 압축된 ‘제목’이 있는 경우 정확도가 더 높다는 사실을 확인 했기 때문입니다.

실제로 검색 시 제목과 내용의 유사도 가중치를 5:5로 동일하게 설정했을 때, 500여 개의 테스트 질문에 대해 정답 문서를 찾아내는 정확도가 87%로 가장 높게 나타났습니다.

최근에는 사람인 일부 도움말 콘텐츠가 Notion으로 관리되고 있는 상황을 반영하여, Notion API를 통해 해당 컨텐츠를 수집하고 벡터화하는 기능을 확장 적용하였습니다. 이를 통해 RAG를 위한 데이터를 좀 더 다양한 채널에서 수집 할 수 있도록 확장 하였습니다.

✅ 2단계: 실시간 API 연동으로 RAG 확장

VectorDB 방식은 매우 효과적이었지만, 사람인의 모든 컨텐츠를 VectorDB에 담는 것은 시간과 비용 측면에서 현실적인 한계가 있었습니다.

그리고 이미 잘 구축된 컨텐츠 제공 API가 있다면 활용하는 것이 더 좋은 방안이라고 생각했습니다.

그래서 저희는 기존에 사용하던 내부 컨텐츠 검색 API를 RAG 파이프라인에 통합하는 두 번째 단계를 진행했습니다.

이는 검증된 내부 리소스를 재활용하는 동시에, 별도의 데이터 적재 과정이 필요 없기 때문에 전체 아키텍처를 단순화하는 이점이 있었습니다.

/img/link/image.png

▶HTTP API를 활용한 RAG◀

컨텐츠 검색 API를 동적으로 호출하기 위해 LLM의 'Function Calling' 기능을 활용했습니다. 사용자의 질문을 LLM이 분석하여 어떤 API를 호출하고 어떤 파라미터를 넘겨야 할지 정의 하도록 하였습니다.

또한, API로부터 받은 응답 데이터를 정해진 형식으로 추출하기 위해 '추출 스키마(Extraction Schema)'를 정의하고, 이를 통해 API에서 LLM에 전달 해야 할 데이터를 필터링(비공개 정보 필터링, 텍스트 정보 추출 등)하고 LLM에 전달하는 정보를 제어 할 수 있도록 했습니다.

3.2. 프롬프트 엔지니어링을 통한 답변 품질 관리

프롬프트 엔지니어링에는 “쓰레기를 넣으면 쓰레기가 나온다(GIGO)”는 유명한 말이 있습니다.

저희는 이 말이 챗봇의 답변 품질과 직결되는 가장 중요한 부분이라고 생각했습니다.

저희 챗봇은 범용적인 대화가 아닌, 명확한 원칙을 따라야 했습니다.

  1. 사람인 컨텐츠에 기반해 정확히 답변
  2. 정보가 없으면 임의로 답변하면 안됨
  3. 서비스 범위를 벗어난 질문에는 답변하면 안됨

이러한 원칙을 LLM에게 명확히 전달하기 위해 저희는 몇 가지 규칙을 적용했습니다.

  • 요청을 짧고 명료하게 작성
  • 원하는 결과물의 예시를 전달
  • 구역을 나누어 설명
  • 해야 하는 작업에 대해서 순서대로 정리

/img/link/image.png

물론 서비스 오픈 초기 부터 프롬프트가 완벽한 것은 아니었습니다.

지속적인 모니터링을 통해 유사 서비스에 대한 답변을 막거나, 불필요한 정보는 노출하지 않도록 하는 등의 안전장치를 프롬프트에 추가하며 완성도를 높여갔습니다.

프롬프트 작성을 더 깊이 이해하는 데 도움이 될 만한 자료를 추가로 공유합니다.

▶ 참고 : 구글의 Prompt 작성 백서 - Prompt Engineering에 대한 모든 기법 소개

1. 구체적인 예시를 포함하세요 (Few-shot Prompting) : 3~5개 정도의 답변 예시를 프롬프트에 포함하면, LLM이 맥락을 더 잘 파악하고 유사한 품질의 결과물을 생성합니다.

2. 간결하게 작성하세요 : 지시는 복잡하지 않고 명료해야 합니다. 너무 많은 내용을 한 번에 전달하면 LLM이 핵심을 놓칠 수 있습니다.

3. 명령형 어조를 사용하세요 : ‘~해야 돼’와 같은 서술형보다 ‘~해줘’ 형태의 직접적인 명령이 더 효과적입니다. ‘분석해’, ‘분류해’, ‘비교해’ 와 같이 명확한 동사를 사용하는 것이 좋습니다.

4. 출력(Output) 형식을 명시하세요 : JSON, Markdown, 글머리 기호 등 원하는 결과물의 형식을 구체적으로 지정하면 그대로 출력해 줄 확률이 높아집니다.

5. ‘해야 할 일’에 집중하세요 : ‘~ 하지 말아’라는 부정적인 제약보다는 ‘~해야 해’라는 긍정적인 지시가 더 좋은 결과를 만듭니다.

글을 쓰면서 이전에 작성된 저희 프롬프트도 다시 한번 점검 해야 하겠다는 생각이 듭니다. 😅

3.3. 다중 제목을 활용한 검색 개선

컨텐츠의 ‘제목’을 포함하여 검색하는 방식은 RAG 성능 확보에 도움이 되었습니다.

하지만 운영 과정에서 사용자들이 같은 의도를 다른 단어로 표현할 때 컨텐츠를 잘못 검색하는 문제들을 발견했습니다.

예를 들어, 시스템에는 ‘이력서 등록 방법’ 이라는 도움말이 있지만, 많은 사용자는 ‘이력서 작성 방법’ 이라고 질문했습니다.

이 미묘한 차이로 인해 챗봇은 적절한 도움말을 찾지 못하고(이력서와 관련된 도움말들이 많아서 ‘등록 방법’ 컨텐츠가 후순위로 밀림) 답변을 하지 못했습니다.

이 문제를 해결하기 위해 하나의 콘텐츠가 여러 개의 제목을 가질 수 있도록 하는 ‘다중 제목(Multi-subject)’ 기능을 추가 하였습나다.

사용자의 질문에 정답 컨텐츠를 찾을 수 있는 방법을 여러 개 만들어 주는 것입니다.

/img/link/image.png

▶ 다중 제목 설정을 위한 Admin 화면 ◀

구체적으로는 아래 세 가지 유형의 제목을 등록 하여 사용 할 수 있습니다.

  • 제목: 컨텐츠의 원래 제목입니다.
  • 유사 제목: 운영자가 직접 ‘이력서 작성 방법’과 같은 유사 제목을 수동으로 추가합니다.
  • 요약 제목: LLM이 콘텐츠 본문을 분석하여 핵심 내용을 담은 새로운 제목을 자동으로 생성합니다.

이 방식을 통해 검색의 유연성과 정확도를 높일 수 있었습니다.

3.4. LangGraph로 복잡한 RAG 파이프라인 제어하기

많은 LLM 활용 서비스들이 사용하고 있고, 저희도 서비스를 구현하면서 사용했던 프레임워크인 LangGraph에 대해서 말씀 드리겠습니다.

▶ LangGraph란?

LangGraph는 복잡한 LLM 워크플로우를 ‘그래프(Graph)’ 형태로 구성하여, 상태를 기반으로 작업을 제어할 수 있도록 해주는 LangChain의 프레임워크 입니다.

LangGraph는 크게 3가지 요소로 구성됩니다.

  • State (상태)

    그래프의 전체 생명 주기 동안 공유되는 데이터 입니다.

    모든 작업(Node)의 입출력은 이 State로 이루어집니다.

  • Nodes (노드)

    실제 로직을 처리 하는 함수입니다.

    하나의 node는 현재 State를 입력으로 받아 로직 처리 후, 그 결과를 다시 State에 반영하여 반환하게 됩니다.

  • Edges (엣지)

    node간 연결을 통해 흐름을 제어하는 방법 입니다.

    특정 node의 작업이 끝난 후, Graph에 정의된 edge를 기반으로 다음에 어떤 node를 호출할지 결정됩니다. edge를 이용하여 단순 연결 및 조건부 분기, 순환(Cycle) 구조를 만들 수 있습니다.

▶ 도입 계기

저희는 초기의 RAG 파이프라인을 ‘질문 → Function Call → 컨텐츠 검색 → 답변’으로 이어지는 순차적인 구조로 개발 하였습니다.

하지만 이 방식은 고도화를 진행 하면서 여러가지 한계에 부딪혔습니다.

  • 복잡한 질문 처리 불가

    하나의 질문에 여러 컨텐츠(예: 공고와 기업 정보)가 필요할 때, 반복적인 Function call과 컨텐츠 검색을 처리 하기 어려웠습니다.

  • 병렬 작업의 한계

    답변을 생성하면서 동시에 다른 작업(ex.질문을 분류하고 연관된 링크 정보 추출)을 처리 하는 병렬적인 구현이 복잡 했습니다.

  • 오류 처리의 복잡성

    로직 처리를 위한 각 단계들이 분리 되어 있어 LLM 연동 실패시 재시도를 위한 로직 처리가 복잡했습니다.

  • 코드의 중복 발생

    응답 타입(일반, 스트림)에 따라 유사한 로직을 가진 코드가 중복으로 생성되었습니다.

이러한 문제들을 해결하기 위해 상태 기반의 분기 처리 및 순환 처리가 가능한 LangGraph를 이용하였습니다.

▶ 적용 사례 및 도입 효과

/img/link/image_langgraph.png

▶챗봇 서비스를 위한 Graph 시각화 ◀

LangGraph를 도입하여 RAG 파이프라인을 더욱 유연하게 만들 수 있었습니다.

주요 적용 사례와 효과는 다음과 같습니다.

  • 조건부 분기를 통한 효율적인 흐름 제어

    Conditional Edge를 활용하여 유효하지 않은 질문(짧은 질문, 비속어/욕설 등)은 LLM 호출 없이 고정 답변으로 처리하고, 의도가 있는 질문만 LLM을 연동하도록 흐름을 효과적으로 분기 처리 했습니다.

  • 기능별 노드화를 통한 모듈성 확보

    Function Calling, 프롬프트 조회, LLM 호출 등 유사한 기능들을 Node로 모듈화 하여 코드 재사용성을 높이고, 필요에 따라 반복적인 처리가 가능 하도록 순환 구조를 만들었습니다.

  • Subgraph를 활용한 복잡도 관리

    전체 워크플로우 내부에 LLM을 연동을 통한 답변 부분을 독립적인 Subgraph로 생성하여 전체 그래프의 복잡도를 낮추었습니다.

  • 손쉬운 병렬 처리 구현

    ‘답변을 생성 하기 위한 프로세스’와 ‘질문을 분류 하여 연관된 링크 정보를 탐색’하는 프로세스를 Graph의 Map Reduce 방식을 기반으로 손쉽게 병렬로 처리 할 수 있도록 하였습니다.

4. 안정적인 운영과 확장을 위한 시스템 설계

4.1. 독립적인 Bot 관리 시스템 구축

저희 챗봇 서비스는 사용자와의 상호작용을 담당하는 ‘채팅 시스템’과 질문에 대해서 LLM을 연동해 답변 하는 ‘Bot 시스템’으로 분리하여 설계하였습니다.

특히 Bot 시스템은 여러 서비스의 다양한 Bot을 독립적으로 생성하고 운영할 수 있는 ‘멀티테넌시(Multi-tenancy)’ 구조를 채택했습니다.

Admin을 통해 각 Bot이 참조할 데이터 소스(검색 서버), 데이터 소스별 프롬프트, LLM 모델 정보까지 모든 것을 동적으로 설정 할 수 있습니다.

현재 ‘사람인 Bot’, ‘노무 상담 Bot’, ‘비긴즈 서비스 Bot’, ‘Komate Bot’ 등 4종류의 Bot 을 서비스 하고 있으며, Rest API를 통해 Biz Logic에서도 Bot 시스템을 연동 가능 합니다.

/img/link/image.png

▶ Admin을 통한 Multi Bot 서비스 ◀
Bot 컨텐츠 연동 타입 검색 가능 컨텐츠 항목
사람인 Bot Vector DB 타입 • 사람인 도움말 컨텐츠
• 상품 정보 컨텐츠
  HTTP 타입 • 공고 컨텐츠
• 추천 공고 컨텐츠
• 기업정보 컨텐츠
• 모바일 메뉴 컨텐츠
Komate Bot Vector DB 타입 • Komate 도움말 컨텐츠
노무 상담 Bot HTTP 타입 • 인사/노무 정보 컨텐츠
비긴즈 Bot Vector DB 타입 • Begins FAQ 컨텐츠

더 나아가, 하나의 Bot이 여러 데이터 소스(검색 서버)를 동시에 참조하도록 설정할 수도 있습니다.

이 경우, 저희는 LLM의 Function Calling을 활용해 사용자의 질문 의도를 파악하고, 가장 적합한 데이터 소스(검색 서버)를 스스로 판단하여 검색하도록 구현했습니다.

/img/link/image.png

▶ Function Calling을 이용한 데이터 소스 선정 ◀

이러한 아키텍처 덕분에 저희는 높은 확장성과 유연성을 확보할 수 있었습니다.

새로운 서비스에 챗봇이 필요할 경우, 이제는 별도의 개발 리소스 투입 없이 Admin 설정 만으로 신규 Bot을 생성하고 즉시 서비스에 투입할 수 있습니다.

4.2. 모니터링을 통한 데이터 선순환 구조 마련

챗봇 서비스의 지속적인 개선을 위해 저희는 모든 질문/답변 데이터를 이력으로 저장하여 관리 하고 있습니다.

사용자의 질문과 챗봇의 답변은 물론, 참조한 콘텐츠, 사용된 프롬프트, 처리 소요 시간 등 다양한 메타 정보까지 함께 저장하여 분석의 기반으로 사용하고 있습니다.

이렇게 수집된 데이터는 서비스의 부하 상태나 답변 품질을 측정하는 기술적인 모니터링 뿐만 아니라, 사용자의 질문 유형과 주요 관심사를 파악하는 데에도 유용하게 활용됩니다.

/img/link/image.png

▶ 모니터링을 통한 컨텐츠 개선 ◀

특히 저희는 이 데이터를 ‘고객 지원팀’과 함께 분석합니다.

고객 지원팀은 “답변에 활용된 콘텐츠가 부족하지 않은지?”, “사용자들이 가장 궁금해하는 내용은 무엇인지?” 와 같은 질문을 던지며 사용자의 진짜 의도를 파악하고 콘텐츠의 빈틈을 찾아냅니다.

분석을 통해 얻은 인사이트를 바탕으로 기존 도움말 콘텐츠를 보강하거나 신규로 등록하여 서비스 오픈 이후 약 20%의 콘텐츠를 개선하는 효과를 얻었습니다.

5. 마치며…

챗봇 프로젝트를 시작할 때는 ‘LLM이 모든 것을 알아서 해결해 줄 것’이라는 막연한 기대감이 있었습니다.

하지만 실제 개발 과정은 환각(Hallucination) 현상을 제어하고, 한정된 데이터에서 가장 정확한 정보를 찾아내기 위한 현실적인 과제들의 연속이었습니다.

결국 LLM이라는 강력한 기술을 그대로 사용하기보다, RAG 아키텍처와 컨텐츠에 맞는 프롬프트를 통해 서비스에 맞는 데이터를 제공하고 기준을 세우는 과정이 무엇보다 중요하다고 생각하게 되었습니다.

현재 서비스에 머무르지 않고 앞으로도 팀원들과 함께 사용자들이 더 잘 사용할 수 있도록 고도화 해 나갈 계획입니다.

LLM 기반 서비스를 처음 시작하는 분들께 작은 도움이라도 되기를 바랍니다.

읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.