똑쿼리 고도화 일대기
똑쿼리는 전사 데이터를 LLM을 통해 조회할 수 있는 서비스입니다. 여기서 전사 데이터는 단순한 데이터 웨어하우스의 수치를 넘어서, 프로젝트 문서, API 문서, 사내 규정까지 포괄하는 개념입니다. 이 글에서는 똑쿼리가 어떤 배경에서 탄생했고, LangGraph 도입과 함께 어떻게 고도화되었는지를 공유합니다.
들어가며
비바리퍼블리카, 배달의 민족, 당근 등 다양한 빅테크 기업에서는 사내 AI 를 이미 도입했고 이에 영향을 받기도 했습니다. 그럼에도 불구하고, 조직의 규모가 엄청 크지도 않은 비브로스라는 조직에 왜 사내 AI 가 필요했는가를 되짚어보았습니다.
비브로스는 그리 크지 않은 조직임에도, 우리가 운영하는 "똑닥"이라는 서비스는 MAU 100만 이상의 큰 서비스입니다. 여기서 파생되는 다양한 정책, 데이터가 있었고 이에 대한 조회 내지 설명을 위해 특정 개인 또는 집단에게 의존하곤 했습니다. 가령, "멤버십 유저 중 4세 미만 자녀 연동이 된 유저는 몇 명일까?" 같은 질문은 백엔드팀 또는 데이터 담당자에게 물어봐야 했습니다.
이것은 개인에게 컨텍스트 스위칭을 요구하며 업무 효율을 낮출 뿐 아니라, 이를 인지하는 구성원은 데이터가 궁 금해도 쿼리에 대해 선뜻 물어보기 힘들었습니다. 데이터뿐만 아니라 정책, 사내 규정도 마찬가지입니다. 그것을 잘 숙지하는 구성원에게 물어보곤 했고, 만약 그 구성원 부재 중이라면 답변을 기약 없이 기다려야 했습니다.
똑쿼리는 이런 배경에서 탄생하였습니다. 궁극적으로는, 회사 내 모든 구성원이 필요한 정보를 오랜 기다림 없이, AI와의 자연스러운 대화를 통해 즉시 확인할 수 있는 환경을 만드는 것이 목표입니다.
똑쿼리 개발기
시작점: 채팅 에이전트
똑쿼리는 단순한 데이터 질의응답 기능을 제공하는 것을 출발점으로 삼았습니다.
인터페이스는 사내 구성원 누구나 쉽게 접근할 수 있도록 슬랙으로 시작하였습니다. 사용자는 슬랙에서 똑쿼리 봇을 멘션하여 질문을 입력하면, 해당 요청이 서버로 전달되어 응답이 생성되는 방식입니다.
초기 똑쿼리 서버는 내부적으로 ChatAgent를 중심으로 답변을 결정하였습니다. ChatAgent 는 시스템 프롬프트 기반의 Chain-of-Thought(CoT) 추론을 통해 쿼리를 생성하고 실행하여 최종 답변을 생성하였습니다.
CoT(Chain of Thought)은 복잡한 문제를 단계적으로 사고하도록 유도 하는 LLM 프롬프트 기법입니다. 즉, 모델이 곧바로 정답을 예측하는 대신, 중간 추론 과정을 먼저 언어로 설명하도록 유도하는 방식입니다.
아래는 당시 사용했던 시스템 프롬프트의 일부입니다:
SYSTEM_PROMPT = """
Respond to the human as helpfully and accurately as possible. You have access to the following tools:
{tools}
...(중략)
Follow this format:
Question: input question to answer
Thought: consider previous and subsequent steps
Action:
```
$JSON_BLOB
```
Observation: action result
... (repeat Thought/Action/Observation N times)
Thought: I know what to respond
Action:
{{ "action": "Final Answer", "action_input": "Final response to human" }}
...(생략)
"""
이 형태의 구현은 "데이터 질의 응답"이라는 단일 테스크만을 수행하는 데는 충분했습니다.
더욱 복잡한 기능을 추가하기엔 한계가 있었지만, 당시엔 Langchain 생태계에 막 적응하던 시기였기에 복잡한 라이브러리 도입은 부담스러웠습니다.
하지만 이 구현 방식에는 다음과 같은 문제점이 있었습니다:
- 유지보수가 어려웠습니다. 모델이 정해진 포맷을 지키지 않으면 에러가 발생하거나, 체인이 한 번 더 실행되는 비효율이 생겼습니다.
- CoT 지시로 인해 프롬프트가 길어지면서, 토큰 낭비와 응답 속도 저하 문제가 발생했습니다. 프롬프트의 가독성 또한 떨어졌습니다.
- 다양한 에이전트들과 통합하려 했지만, 확장성이 부족해 유연한 스케일 확장이 어려웠습니다.
이러한 한계를 구조적으로 해결하고, 더 복잡한 기능을 체계적으로 다룰 수 있도록 저희는 LangGraph를 도입하게 되었습니다.
랭그래프 도입
랭그래프란
랭그래프(LangGraph)는 상태 기반(stateful)의 LLM 워크플로우를 구성할 수 있게 도와주는 프레임워크입니다. 공식 문서에서는 랭그래프의 특징을 아래와 같이 설명하고 있습니다:
신뢰성과 제어 가능성
에이전트의 행동을 모더레이션 검사와 인간의 승인 절차를 통해 제어할 수 있습니다. LangGraph는 장기 실행 워크플로우의 컨텍스트를 지속적으로 유지하여, 에이전트가 올바른 경로를 따르 도록 돕습니다.
저수준 접근과 확장성
제약이 많은 추상화 없이, 완전하게 설명 가능한 저수준 프리미티브로 사용자 정의 에이전트를 구축할 수 있습니다. 각 에이전트가 특정 역할을 수행하도록 설계하여, 사용 사례에 최적화된 확장 가능한 멀티 에이전트 시스템을 설계할 수 있습니다.
일급 스트리밍 지원
토큰 단위의 스트리밍과 중간 단계의 실시간 스트리밍을 통해, LangGraph는 사용자가 에이전트의 추론 과정과 행동을 실시간으로 명확하게 파악할 수 있도록 도와줍니다.
쉽게 생각하면, 서버를 구성할 때 사용하는 if/else나 for/while 같은 제어 구조를 Langchain 환경에서도 명시적으로 구성할 수 있도록 해주는 프레임워크입니다. 복잡한 프롬프트로 모델에게 일일이 로직을 맡기기보다는, 애플리케이션 코드 수준에서 제어권을 직접 쥘 수 있도록 해줍니다.
랭그래프는 세 가지 요소로 이루어집니다.
State (상태)
State는 애플리케이션의 현재 상태를 담고 있는 공유 데이터 구조입니다. Python의 모든 타입을 사용할 수 있으나, 주로
TypedDict나Pydantic BaseModel을 활용합니다.Node (노드)
Node는 에이전트의 로직이 구현된 Python 함수입니다. 현재 상태(State)를 입력받아 필요한 연산이나 부수 효과를 수행하고, 갱신된 상태를 반환합니다.
Edge (엣지)
Edge는 현재 상태(State)를 기반으로 다음 실행할 Node를 결정하는 Python 함수입니다. 조건에 따른 분기나 미리 정의된 전이 규칙을 포함할 수 있습니다.
즉, LangGraph는 데이터의 흐름(State), 실행의 주 체(Node), 실행의 경로(Edge)를 분리해, 복잡한 에이전트 시스템도 구조적으로 명확하고 유연하게 구성할 수 있도록 도와줍니다.
아래 그림은 랭그래프의 간단한 예시입니다.

예를 들어, 유저가 "멤버십 구독 정책 알려줘" 라고 요청을 보내면, 워크플로우는 __start__ 에서 시작됩니다. 초기 State 는 다음과 같이 구성됩니다.
{
"messages": [{ "role": "user", "content": "멤버십 구독 정책 알려줘" }],
"today": "2025-06-19 15:00:00",
"thread_id": "%THREAD_ID%"
}
초기 상태는 먼저 pre_model_hook 을 거쳐 agent 로 전달됩니다.
agent 에서는 사내 문서를 탐색이 필요하다고 판단을 내리고, 데이터를 수집하기 위해 tools 로 이동합니다.
tools 는 데이터베이스에서 관련 문서를 탐색한 뒤, 그 결과를 State에 추가하고 다시 pre_model_hook 으로 흐름을 돌려보냅니다.
{
"messages": [
{ "role": "tool", "content": "멤버십 정책 설명: 블라블라" },
{ "role": "user", "content": "멤버십 구독 정책 알려줘" }
],
"today": "2025-06-19 15:00:00",
"thread_id": "%THREAD_ID%"
}
이후 pre_model_hook은 갱신된 상태를 다시 agent 에 전달합니다.
agent는 메시지를 기반으로 더 정보를 탐색할지, 혹은 결론을 도출할지를 판단합니다.
결론을 도출할 수 있다고 판단되면, 응답 생성을 위해 generate_structured_response 로 이동합니다. 이 노드에서는 최종 답변을 JSON 형태로 구성하고, 이를 마친 후 __end__ 노드로 전달하여 워크플로우를 종료합니다.
이처럼 LangGraph는 상태 기반의 흐름 제어를 통해 요청의 입력부터 응답 생성까지의 전 과정을 명시적이고 구조적으로 관리할 수 있도록 돕습니다.
랭그래프 구조
초기에는 단순한 질의응답에 초점을 맞췄지만, 사용자 니즈와 활용도가 높아지 면서 저희는 똑쿼리 기능을 다음과 같이 세 가지 영역으로 확장하였습니다:
- 슬랙 스레드 내용을 요약하여, 긴 대화를 빠르게 파악할 수 있는 기능
- 슬랙 스레드를 벡터화하여 검색에 활용할 수 있는 기능
- 사내 데이터(데이터 웨어하우스, 스키마, 문서, 정책 등)를 기반으로 한 자연어 질의응답 기능
이러한 기능들은 모두 슬랙봇을 멘션한 사용자 요청을 분석하여, 어떤 기능을 수행할지 결정하는 Slack Router를 통해 자동으로 분기 처리됩니다.

(1) 슬랙 스레드 요약
스레드에서 나눈 대화 내용을 LLM을 통해 요약하는 기능입니다. 슬랙 내 스레드는 길어지면 전체 맥락을 파악하기 어려운 경우가 많기 때문에, 요약 기능은 빠른 의사결정과 협업에 도움을 줍니다.

사용자가 "요약"이라는 키워드를 포함해 멘션하면, Slack Router는 이를 감지하고 고정된 흐름으로 Slack Summary Agent 노드로 분기합니다. 이 노드는 해당 스레드의 모든 메시지를 조회한 뒤, 요약에 적합한 형식(예: 핵심 발언, 주요 결정 사항 등)으로 정리하여 전달합니다.

(2) 슬랙 스레드 임베딩
스레드에서 나눈 대화 내용을 요약해서 벡터DB에 임베딩하는 기능입니다. Confluence 등 공식 문서에는 기록되지 않았지만, 실무 대화에서 의미 있는 정보가 오가는 경우가 많아 이를 벡터화하여 검색에 활용할 수 있도록 만든 기능입니다.


사용자가 "학습"이라는 키워드를 포함해 슬랙봇을 멘션하면, Slack Router는 해당 요청을 감지하여 Embedding Agent 노드로 워크플로우를 전환합니다.
이 노드는 해당 스레드의 모든 메시지를 조회한 뒤, 학습할만한 가치가 있는 내용이 있을 경우 요약 후 벡터화하여 저장합니다. 의미 있는 정보가 없다고 판단되면, 임베딩을 수행하지 않고 요청을 종료합니다.

(3) 사내 데이터를 기반으로 한 자연어 질의응답
슬랙봇을 통해 사내 데이터를 조회할 수 있는 기능입니다.
예를 들어 "어제 가입한 유저가 몇 명인지", "멤버십 무상 지원 정책은 무엇인지", "복지비는 어떻게 신청하는지"와 같은 질문을 자연어로 입력하면 응답을 받을 수 있습니다.
Slack Router 관점에서는, 요약 또는 임베딩과 같은 명시적 키워드가 없는 모든 요청은 일반 질의로 분류되어 Query Resolver Agent 노드로 전달됩니다.
이 기능은 LangGraph 기반으로 다시 구성되어, 복잡한 판단과 흐름을 유연하게 처리합니다.
Query Resolver Agent 내에서는 먼저 Query Graph Router가 동작하여, 해당 질문이 어떤 유형인지 판단합니다. 문서 탐색이 필요한지 / 데이터 조회가 필요한지 / 지라 티켓을 확인해야 하는지를 경량 LLM을 통해 분석합니다. 예를 들어 사용자가 "멤버십 정책 알려줘"라고 입력하면, Query Graph Router는 해당 질문이 문서 탐색에 해당한다고 판단하여 Document Agent 노드로 분기합니다.

Query Graph Router 하위의 각 에이전트 노드는 langgraph.prebuilt 의 create_react_agent 함수를 통해 생성됩니다.
from langgraph.prebuilt import create_react_agent
document_agent = create_react_agent(
model=claude,
prompt=RETRIEVE_DOCUMENT_PROMPT,
tools=[confluence_retriever_tool, notes_retriever_tool, api_documents_retriever_tool],
name="document_agent",
)
이 에이전트는 주어진 도구를 기반으로 문서를 검색하고, 응답 생성을 위한 추론을 수행합니다.
이러한 방식은 Reasoning and Acting (ReAct) 구조로 불리며, 기존에 프롬프트 기반으로 수행하던 Chain-of-Thought 추론에 비해 더 구조적이고 간결한 추론 흐름을 제공합니다.
에이전트 구성
사내 구성원의 다양한 질문 유형을 처리하기 위해, Query Resolver Agent는 다음과 같은 세 가지 하위 에이전트로 구성되어 있습니다:
- 문서 에이전트 (
Document Agent): Confluence, 정책 PDF 문서, 내부 API 문서 등에서 정보를 검색하고 요약합니다. - 데이터 에이전트 (
Data Agent): 데이터 웨어하우스, 테이블 스키마, 비즈니스 메트릭에 대한 질의를 처리합니다. - 지라 에이전트 (
Jira Agent): 지라 티켓을 조회하거나 특정 이슈의 상태, 담당자 등을 확인합니다.
