AI Agent가 복잡한 테이블 데이터 처리하는 방법

RAG와 SQL을 혼합한 Table Agent 개발기

Day • Machine Learning Engineer

  • 엔지니어링

안녕하세요 채널톡 AI 팀의 데이입니다!

현재 AI 에이전트 ALF는 현재 고객사에서 작성한 도큐먼트를 기반으로 답변을 제공하고 있습니다.

그러나 도큐먼트를 일일이 작성하지 않아도 엑셀 파일을 간단히 업로드하여 ALF가 상품 재고나 가격을 안내해주었으면 좋겠다는 수요가 꽤 많았습니다.

그래서, 오늘은 AI 에이전트에게 라는 자료구조를 잘 보여주기 위해 저희 팀에서 겪었던 고민들과 해결책들을 공유해보려 합니다.


Challenges

LLM의 성능이 나날이 좋아지고 있지만, 표는 여전히 LLM이 이해하기 까다로운 자료입니다. 그 이유는 LLM과 사람이 정보를 읽는 방식이 완전히 다르기 때문입니다.

LLM이 표를 이해하기 어려운 이유

사람은 시각 정보를 활용하여 2차원 정보를 받아들일 수 있습니다. 따라서 행과 열이 만나는 지점에 원하는 정보가 있을 것임을 직관적으로 알수가 있죠.

그러나 LLM은 1차원의 텍스트를 읽기 위해 학습된 모델이므로, 표를 보기 위해서는 1차원으로 펼쳐주는 작업이 필요한데요, 이 과정을 선형화(Linearization)라고 부릅니다.

표의 선형화 예시

LLM은 학습 과정에서 언어적 패턴을 귀납적으로 학습하게 되는데, 이것을 귀납 편향(Inductive Bias)라고 부릅니다. 귀납 편향은 딥러닝 모델들을 강력하게 만드는 원동력 중 하나인데요, 선형화된 표는 일반적인 줄글에서 나타나는 귀납적 패턴들을 파괴하여 LLM이 이해하기 어렵게 만듭니다.

  1. Proximity: 가까이 있는 단어는 서로 연관이 높다는 편향

    → 행이 바뀔 때 서로 관련이 없는 문자열이 이어붙게 됩니다.

... 색상: 블랙 | 가격: 40,000 | 행 번호 2 | 이름: 후드티 ...

  1. Causality: 이후에 오는 단어는 이전에 등장한 단어들에 영향을 받는다는 편향

    → 표의 행과 열들은 대부분 순서와 무관합니다.

"John loves Mary" ≠ "Mary loves John"

이렇듯 표는 눈으로 볼 때 가장 이해하기 쉬운 자료이며, 1차원의 줄글로 변환하면 이해하기가 어려워집니다. 게다가 표가 커지면 커질수록 이러한 어려움은 배로 늘어나게 됩니다. 수천, 수만 줄에 이르는 표를 선형화하여 모델의 컨텍스트에 넣어줄 수는 없는 노릇이죠. 그렇다면 RAG를 사용하면 어떨까요?

RAG에도... 표를 사용하기 어려운 이유

검색 증강 생성(Retrieval Augmented Generation, RAG)는 ALF를 비롯한 AI 에이전트들이 외부 지식을 활용하여 답변을 생성하기 위해 사용하는 기법입니다.

RAG 전처리 파이프라인

위 과정에 표를 활용할 경우, 두 가지 큰 어려움이 존재합니다.

우선, 청킹과 임베딩으로 이어지는 전처리 과정입니다. 청킹이란 문서를 여러 개의 조각들로 자르는 과정을 의미하고, 임베딩은 주어진 텍스트를 벡터로 변환하는 과정을 의미합니다. 이때 검색을 용이하게 하기 위해서는 각각의 벡터가 다른 벡터들과 충분히 구분 가능해야 하며, 그 말인 즉 각각의 조각이 구분 가능한 하나의 의미적 단위를 구성하는 것이 좋습니다. 그렇지 못한 경우, 벡터의 대표성이 떨어지고(흔히 벡터가 뭉툭하다고 합니다), 유사도 기반의 검색이 어려워집니다.

표의 행들을 기본 단위로 처리한다고 할 때, 각각의 행이 과연 하나의 의미적 단위를 구성한다고 말하기는 어렵습니다. 숫자 하나만 다를 뿐, 완전히 같은 행들도 얼마든지 존재하기 때문이죠.

또 다른 어려움은 검색 과정입니다. 표를 필요로 하는 문의 중에는 벡터 유사도 기반의 검색으로는 찾을 수 없거나, 찾기 매우 어려운 문의들이 많습니다. 가령, 아래와 같이 물어본다고 해 봅시다.

제일 비싼 상품이 뭐에요?

위 질문과 벡터상의 거리가 가까운 행들이 몇개 검색되긴 하겠지만, 그 상품들이 가장 비싼 상품이라는 보장은 없습니다. 숫자를 이용한 상대적인 비교가 아니라, 자연어 상의 절대적인 유사도를 기반으로 검색했기 때문이죠.

또한, 한 번의 검색으로 찾기 어려운 문의도 많습니다. 예를 들어,

15만원 이내의 테니스화 중에 흰색으로 아무거나 추천해주세요!

가격, 상품 종류, 색상까지 무려 3개의 조건이 포함된 문의입니다. 이러한 문의를 인공지능에서는 멀티-홉(Multi-hop) 질문이라고 일컫는데요, 정답에 이르기까지 여러 번의 점프를 해야 하기 때문입니다. 표를 기반으로 한 질문 중에는 멀티-홉 질문이 특히나 많은데요, 이런 경우 단 한번의 벡터 검색으로 완벽한 제품을 찾기보단 필터링을 걸어가며 서서히 범위를 좁혀가는 것이 더 자연스럽죠. 그러다 보니 자연스럽게 정렬과 필터링이 용이한 SQL로 시선을 돌리게 됩니다.

그렇다면 Text-to-SQL은?

프로그래밍에 능통한 요즘 LLM들은 아래와 같은 작업이 가능합니다.

Q: 제일 비싼 래쉬가드 얼마에요?

A:

SQL

SQL 쿼리를 작성하는 능력을 평가하기 위한 수많은 벤치마크들도 있고, 모델들의 성능은 나날이 올라가고 있습니다. 그러나, 만약 위 쇼핑몰에서 제공하는 상품의 이름이 "래쉬가드"가 아닌 "래시가드"였다면 어떨까요?

SQL만으로 모든 것을 검색하기에는, 현실에서 사용되는 상품명 및 각종 이름들이 그리 직관적이지 않습니다. 자유도도 너무 높죠. 회색을 표현하기 위해서 아이언 그레이, 차콜, 스페이스 그레이 등 너무 많은 이름들이 사용되곤 합니다. 그래서 저희는 아래와 같은 교훈을 얻었습니다.

RAG의 벡터 검색 능력과 SQL의 필터링 능력을 혼합해보자!


Table Agent

어떤 크기, 어떤 형식의 표가 들어와도 잘 읽는 에이전트를 만들기 위한 핵심 아이디어:

  • 필터링, 정렬 등의 세밀한 작업은 SQL 쿼리를 작성하기

  • 상품명, 색상, 옵션 이름 등 정확한 이름을 찾을 땐 벡터검색을 사용하기

데이터 전처리

이 에이전트가 벡터검색을 하는 목적은 단 한가지, 정확한 이름을 찾아서 SQL 쿼리를 잘 짜기 위해입니다. 따라서 벡터 데이터베이스에 적재되는 각각의 인덱스 단위는 하나의 행이 아니라, 하나의 로 정했습니다.

셀 하나하나를 전부 인덱싱한다니, 너무 많은 것은 아닐지 불안할 수 있으나, 정확한 이름을 찾기 위함이 목적이므로 중복을 제거하고, 숫자나 범주형 자료도 제외하고 나면 그리 많은 값이 아닙니다.

테이블의 전처리 과정 다이어그램

1. 열 분류

벡터 데이터베이스를 구성하기 위해, 우선 벡터 검색을 수행할 열을 선별해야 합니다. 가격, 재고와 같이 숫자 값이거나, 서로 다른 값이 5개가 채 되지 않는 범주형 자료는 간단하게 가려낼 수 있습니다.

다만 문자열로 된 열들 중에서 언어적 "의미"가 있는 열을 골라 내는 것은 간단한 휴리스틱으로는 어렵기 때문에, LLM을 동원하여 벡터검색이 필요한 열을 추출합니다. 이때 열의 이름과 열 내의 표본을 5개 정도 뽑아서 프롬프트에 넣어주면 간단하게 판별할 수 있습니다.

벡터 검색이 필요한 문자열: 상품명, 매장명, 색상 등

벡터 검색이 필요 없는 문자열: 상품 코드, 사이즈 등

2. 청킹 및 전처리

벡터 검색이 필요한 문자열들 중 몇몇 열은 하나의 벡터로 표현하기엔 그 길이가 너무 긴 경우가 있습니다. 이러한 열의 셀 값들은 기존 RAG에서 사용하는 청킹 알고리즘을 사용하여 적절한 길이의 조각들로 잘라줍니다.

이렇게 얻은 자연어 조각들은 중복 제거와 임베딩 모델을 거친 후, 벡터 데이터베이스에 적재됩니다. 내부적인 테스트를 거친 결과 일반적인 테이블은 중복되는 값이 매우 많아, 실제로 벡터 데이터베이스에 올라가는 벡터의 수는 전체 셀 수의 1~2% 정도에 불과했습니다.

에이전트 설계

초기에는 벡터 검색 → SQL 쿼리 작성으로 이루어진, 고정된 흐름의 에이전트를 설계했습니다.

초기 에이전트 플로우

이렇게만 해도 꽤 많은 문제를 풀 수 있었지만, 저희는 LLM 에이전트의 도구 사용(Tool Calling) 능력에 조금 더 기대보기로 했습니다. 원하는 순서대로 원하는 만큼 자유롭게 검색을 할 수 있도록, 루프를 만들어 주었죠.

개선된 에이전트 플로우

이렇듯 벡터 검색, SQL을 모두 LLM이 호출할 수 있는 함수로 정의한 후, 각각의 함수가 어떤 역할을 하는지, 어떨 때 호출해야 하는지를 상세하게 적어주었습니다. 또한, 그 외에도 몇 가지 유용한 함수들을 추가해 주었는데, 그 중 하나는 후처리 코드 작성입니다.

후처리 코드

SQL을 사용하면 데이터베이스의 일부분을 데이터프레임 형태로 받게 됩니다. 그러나 가끔 반환받은 데이터프레임이 너무 크거나, 불필요한 정보가 너무 많은 경우가 있습니다. 이럴 때를 위해, 아래와 같은 함수를 만들어 주었습니다.

JSON

쉽게 말하면, 함수를 만드는 함수입니다. LLM이 작성한 코드를 직접 실행하여, 표에서 필요한 정보만을 남길 수 있게 됩니다.

더 좋은 성능을 위한 추가 작업들

이렇게 만든 에이전트를 다양한 표에서 실험해보며 몇 가지 시행착오를 겪었고, 그 원인들과 해결책들에 대해 이야기해보려 합니다.

1. ReAct 프롬프팅

ReAct(Reasoning and Acting)은 에이전트가 도구를 호출하기 전후로 현재 상황에 대해 정리하고 앞으로의 동작을 간단히 메모하도록 유도하는 프롬프팅 기법입니다. 도구를 반복적으로 호출하는 상황에서 ReAct 프롬프팅 기법을 사용하면 조금 더 성능을 높일 수 있습니다.

가령, "7만원 이하의 가죽 샌들이 사고 싶어!" 라는 문의가 들어왔을 때, 아래와 같이 동작합니다.

YAML

2. 남은 턴 수 알려주기

에이전트가 도구 호출 루프를 돌 때, 무한 루프에 빠지는 것을 방지하기 위해 최대 호출 횟수를 지정해주게 됩니다(기본값은 7번입니다). 그러나 에이전트가 간혹 상품 하나하나를 따로 검색해보며 너무 여유롭게 행동하거나, 한 번의 검색에 모든 것을 해결하려고 행동할 때가 있었습니다. 몇 번의 도구 호출 기회가 남았는지 에이전트가 몰랐기 때문입니다.

이 문제를 해결하기 위해, 프롬프트 말미에 아래와 같은 문구를 추가해 주었습니다.

Python

3. 에러 핸들링

여러 번의 도구 호출 기회가 있다는 점은 에러가 발생했을 때 더 강건하게 고칠 수 있다는 장점도 있습니다. SQL 과정에서 에러가 발생할 경우, 에이전트 루프를 멈추지 않고 에이전트에게 에러 로그를 그대로 보여주면, 프로그래밍에 능통한 LLM들은 스스로 다음 차례에 개선된 쿼리를 작성할 수 있습니다.

또한, SQL을 통해 얻은 데이터프레임이 너무 커서 컨텍스트에 다 담기 부담스러운 경우, 상위 15개의 행만 보여준 후 아래와 같은 경고 메시지를 프롬프트에 삽입할 수 있습니다.

Python

마치며

지금까지 AI 에이전트가 라는 2차원 자료구조를 효과적으로 다루기 위해 우리가 겪은 고민과 해결 과정을 살펴보았습니다. 벡터 검색과 SQL의 강점을 극대화함으로써 큰 표나 복잡한 질문에도 유연하게 대응할 수 있었고, 이제는 더 강력한 AI 에이전트를 개발하기 위해 팀 내에서는 지금도 개선에 박차를 가하고 있습니다.

앞으로도 ALF가 더 많은 고객사의 복잡한 데이터 니즈를 해결할 수 있도록, 다양한 피드백과 아이디어를 기다리겠습니다. 감사합니다!

We Make a Future Classic Product