AI 시스템 설계 시리즈를 통해 우리는 Mailmind라는 가상의 AI 이메일 비서에 대해 다룹니다. 이 비서는 받은 편지함을 읽고, 메시지를 분류하며, 답장을 초안 작성합니다. 이것이 우리의 예시입니다.
읽기 전에: RAG가 존재하는 이유: 컨텍스트 문제와 임베딩 및 벡터 검색 해설하기를 참고하세요.
RAG(retrieval-augmented generation)는 관련 자료를 찾아 모델에 전달하여 그 내용을 바탕으로 답변하게 하는 것을 의미합니다. 이 '찾기' 단계는 키워드 검색, 벡터 검색, 데이터베이스 쿼리 또는 그래프 순회(graph traversal)가 될 수 있습니다. 본 기사에서는 사람들이 가장 많이 논쟁하는 두 가지 조회 방식인 벡터 검색과 지식 그래프 순회를 비교하며, 이 둘 모두 RAG에 데이터를 공급할 수 있습니다. 벡터 검색은 임베딩을 사용하여 비정형 텍스트 전반의 의미에 대한 모호한 질문(fuzzy questions)에 답합니다. 반면, 그래프 순회는 특정 개체들이 어떻게 연결되는지에 대한 정확하고 다단계적인 질문(multi-hop questions)에 답합니다. 우리의 AI 받은 편지함 비서인 Mailmind는 이 둘을 결합합니다. 즉, 그래프가 후보군을 좁히고, 그 좁혀진 범위 내에서 벡터 검색이 의미별로 순위를 매기는 방식입니다.
검색은 빠릅니다. 그래프 순회 역시 빠릅니다. 하지만 이들은 서로의 질문에 대해서는 취약점을 보입니다. 더 똑똑한 시스템은 더 좋은 인덱스를 가진 시스템이 아닙니다. 어떤 조회 방식을 언제 사용해야 하는지 아는 시스템입니다.
지난 에피소드의 프로덕션급 프롬프트 엔지니어링(prompt engineering)을 통해 Mailmind를 일관되게 작동하도록 만들 수 있습니다. 하지만 아무리 잘 지시된 에이전트라도 자신이 찾을 수 있는 것만으로 답변할 수 있습니다. Mailmind는 실제로 사용자의 받은 편지함에 대해 무엇을 알고 있을까요? 그리고 질문이 채팅 창의 마지막 몇 메시지를 넘어설 때, 어떻게 올바른 메모리를 찾아낼까요?
의미론적 검색은 데이터베이스 쿼리보다 언제 우월한가?
벡터 검색은 정확한 단어 단위가 아닌 '의미'로 구절을 검색합니다. 따라서 필요한 것을 설명할 수는 있지만 이름 붙일 수 없는 경우에 적합합니다.
이를 키워드가 아닌 아이디어를 색인화하는 검색 엔진이라고 생각해보세요. '납기일을 놓쳐 고객이 불만을 느꼈던 이메일'이라고 입력하면, 실제로 '불만족(unhappy)'이나 '납기일 지연(missed deadline)'이라는 단어는 사용하지 않았지만, '예상보다 시간이 오래 걸렸어요.'와 같은 표현으로 좌절감을 전달하는 메시지들이 검색됩니다. 이러한 능력은 텍스트가 저장되고 검색되는 방식에서 나옵니다.
Mailmind는 모든 이메일을 수백 개의 토큰으로 이루어진 중첩된 청크(chunk)로 분할합니다. 각 청크는 임베딩(embedding), 즉 그 의미를 포착하는 숫자 목록으로 변환되어, 이메일이 도착했을 때 단 한 번 생성됩니다. 이러한 임베딩들은 벡터 데이터베이스(vector database)에 저장되며, 이곳은 주어진 어떤 벡터와 가장 가까운 이웃을 찾아낼 수 있습니다. 질문이 들어오면, Mailmind는 그 질문을 임베딩하고 벡터 데이터베이스에 쿼리하여, 보통 코사인 유사도(cosine similarity)를 통해 벡터가 가장 근접한 청크들을 요청합니다. 상위 결과들이 LLM(대규모 언어 모델)에게 컨텍스트로 전달됩니다. RAG라는 이름은 Lewis 등이 위키피디아의 벡터 인덱스 위에 사전 학습된 신경망 검색기(neural retriever)와 텍스트 생성기를 결합하고, 검색기의 쿼리 측면과 생성기를 함께 미세 조정하면서 유래했습니다 (Lewis et al. 2020, Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks). 오늘날의 파이프라인들은 기성 임베딩 모델과 벡터 데이터베이스를 사용하여 동일한 아이디어를 적용하며, 보통 별도의 학습 과정 없이 작동합니다.
이러한 검색 경로는 개방형 발견(open-ended discovery)에 완벽합니다. 하지만 정확한 사실 정보에는 적합하지 않은 도구이며, 다음 섹션에서 대신 무엇을 사용해야 하는지 설명하겠습니다.
두 가지가 아닌 세 가지 조회 방식
그래프를 찾기 전에, 많은 '구조적' 질문들이 애초에 그래프 관련 질문이 아님을 주목하세요. 이메일은 이미 발신자, 수신자, 스레드, 날짜, 라벨과 같은 메타데이터(metadata)를 포함하고 있습니다. 세 가지 종류의 조회 방식만으로 거의 모든 것을 다룰 수 있습니다:
- 정확한 메타데이터(Exact metadata), SQL 또는 필터 사용. 이메일 자체에 저장된 단일 단계 사실들입니다: "이번 달 Sam에게서 온 이메일", "Sam이 Stratos 스레드에서 답장했는지 여부?" 일반적인 데이터베이스 쿼리가 이를 정확하게 답변합니다. 많은 벡터 데이터베이스 또한 각 벡터 옆에 메타데이터를 저장하고 검색 중에 필터링할 수 있으므로, 이는 종종 같은 시스템 내에 존재합니다.
- 벡터 검색(Vector search). 의미: "고객이 지연에 대해 좌절감을 느끼는 스레드."
- 그래프 순회(Graph traversal). 엔티티 간의 다단계 관계들입니다: "내가 답장하지 않은 Falcon 프로젝트의 사람들." 이 질문은 프로젝트에서 사람으로, 그리고 그들의 이메일로, 다시 당신의 답장까지 연결되며, 첫 번째 링크인 'Falcon에 종사하는 사람'은 어떤 단일 이메일에 쓰여 있지 않습니다.
관계형 데이터베이스(relational database)도 여러 조인(joins)을 통해 이러한 체인을 따라갈 수 있습니다. 하지만 그래프 데이터베이스는 관계의 체인을 쿼리하는 자연스러운 방식으로 만들고, 체인이 커져도 쿼리가 읽기 쉬운 상태를 유지합니다.
지식 그래프는 어떤 종류의 질문에 답할까요?
지식 그래프는 특정 실제 세계 엔티티들이 어떻게 연결되는지에 대한 질문에 답합니다. 당신은 사물들 간의 관계를 요청하고, 그것은 사실들의 네트워크를 따라가 정확한 답변을 제공합니다.
당신의 받은 편지함 속 사람들, 프로젝트, 스레드, 이메일, 주문 등의 지도를 상상해 보세요. 한 지점에서 다른 지점으로 경로를 추적하고 단계를 셀 수 있습니다. Mailmind는 노드(Person, Project, Thread, Email, Order)와 WORKS_ON, SENT, IN_THREAD, ABOUT, ORDERED, REPLIED_TO 같은 관계들로부터 이 지도를 구축합니다.
사용자가 "지난 7일 동안 내가 답장하지 않은 Falcon 프로젝트의 사람들은 누구인가요?"라고 질문하면, 그래프는 Falcon 프로젝트 노드에서 시작하여 WORKS_ON 간선(edges)을 따라 Person 노드로 이동하고, 그들이 해당 기간에 보낸 이메일을 SENT 간선을 따라가며, 이미 당신으로부터 답장이 있는 모든 이메일은 제외합니다. 이것이 바로 그래프 순회이며, 보통 Cypher와 같은 쿼리 언어로 작성됩니다.
In words: Falcon으로 시작하여 해당 프로젝트에서 일한 모든 사람에게로 이동하고, 그들이 지난 7일 동안 보낸 이메일을 가져온 다음, 당신으로부터 답장된 기록이 없는 이메일만 남기고 발신자를 반환합니다. 데이터베이스는 모든 행을 스캔하지 않습니다. 대신 간선(edges)을 따라 이동하므로, 벡터 검색과는 다른 비용 모델과 실패 모드를 가집니다. Graph Databases for AI: When Relationships Are the Data에서는 이 시리즈의 나중에 어떻게 그래프 데이터베이스가 작동하는지 다룹니다.
답변은 그래프 자체가 정확하다면 정확합니다. 만약 어떤 사람이 Falcon과 연결된 적이 없거나, 두 개의 이름으로 두 번 나타난다면, 쿼리는 알 수 없습니다. 그리고 그래프에 '불만 제기 같은 스레드'를 요청하면 아무것도 반환하지 않습니다. 이러한 실패 지점은 구조화된 데이터의 한계와, Mailmind가 조회(lookup) 기능을 결합하는 이유를 직접적으로 보여줍니다.
왜 어느 하나의 조회 방식도 모든 질문을 처리할 수 없을까요?
벡터 검색은 정확한 사실에 어려움을 겪습니다. 'Sam이 Stratos 스레드에 답장했나요?'는 의미에 대한 질문이 아니라, 일종의 조회입니다. 즉, 발신자가 Sam인 메시지가 그 스레드에 존재하는지 여부를 확인하는 것입니다. 벡터 검색은 Sam이 다른 맥락에서 Stratos를 언급한 청크(chunk)를 가져올 수 있으며, 모델은 이를 잘못하여 답장했다고 가정할 수 있습니다. 메타데이터 쿼리나 REPLIED_TO 간선 확인은 '예' 또는 '아니오'를 반환합니다.
반면 그래프는 설명과 같은 형태의 것에는 어려움을 겪습니다. '발신자가 배송에 대해 불만을 느꼈던 이메일을 찾아라'라는 요청은 '불만(upset)'이 노드나 관계가 아니기 때문에 실패합니다. 감정(Emotion)을 이메일(Email)과 연결하는 간선 자체가 없습니다. 그래프는 원시 텍스트에서 어조(tone)를 계산할 방법이 없으므로, 올바른 이메일이 존재하더라도 아무것도 반환하지 않습니다.
graph TD
A[
벡터 검색(Vector search)과 그래프(Graph)는 메모리의 두 가지 다른 축을 다룹니다. 한 축은 구조입니다. 즉, 누가 누구에게 무엇을 했는지에 대한 것입니다. 다른 축은 의미입니다. 이 메시지가 어떤 종류의 내용인지를 나타냅니다. 메타데이터(Metadata)는 각 이메일에 담긴 단순한 사실들을 다룹니다. 이 세 가지를 결합함으로써 Mailmind는 이 모든 것을 혼합하여 질문에 답할 수 있게 합니다.
## Mailmind는 두 가지 조회 방식을 어떻게 결합하는가?
Mailmind는 벡터 검색과 그래프 중 어느 하나를 선택하지 않습니다. 대신, 질문의 구조적 부분을 그래프로 보내 정확한 후보 목록을 얻고, 그 후 이 목록 내에서 의미에 따라 순위를 매기기 위해 벡터 검색을 사용합니다. 그래프는 '누구(who), 어디(where), 언제(when)'를 처리하고, 벡터 검색은 '어떤 종류(what kind of)'를 처리합니다.
'이번 달에 예산에 대해 논의하는 Falcon 프로젝트 스레드를 보여줘.'라는 질문을 고려해 봅시다. 그 과정은 다음과 같습니다:
1. 시스템은 질문을 구조적 부분(Falcon 프로젝트, 이번 달)과 의미적 부분(예산을 논의함)으로 구문 분석합니다.
2. 그래프는 Falcon 프로젝트에 관한 활동이 이번 달에 있었던 모든 스레드를 찾습니다. 예를 들어 14개의 스레드가 나온다고 가정합시다.
3. Mailmind는 질문의 의미적 부분(
## 빠른 참고 자료: 세 가지 조회 방식
| 속성 | 메타데이터 또는 SQL | 벡터 검색 (Vector search) | 그래프 순회 (Graph traversal) |
| :--- | :--- | :--- | :--- |
| 데이터 형태 | 각 이메일의 필드(보낸 사람, 스레드, 날짜) | 임베딩으로 저장된 텍스트 청크 | 개체 및 그들 간의 관계 |
| ... |
## 자주 묻는 질문 (Frequently Asked Questions)
**질문: 이미 벡터 데이터베이스가 있는데 왜 그래프를 추가해야 하나요?**
대부분의 경우 필요하지 않습니다. "4월에 Sam에게서 온 이메일"과 같은 정확한 사실은 많은 벡터 데이터베이스에서 지원하는 메타데이터 필터 또는 일반 데이터베이스에서 가져올 수 있습니다. 그래프는 질문이 여러 관계를 연결할 때 그 가치를 발휘합니다. 예를 들어, "4월에 Falcon 프로젝트와 관련된 어떤 연락처에게 이메일을 보냈나요?" 같은 질문은 먼저 누가 Falcon 프로젝트 소속인지 알아야 이메일을 검색할 수 있습니다. 그래프는 이러한 관계를 직접 저장하고, 개체가 정확하게 추출되고 해결되었다면 단 한 번의 순회(traversal)만으로 답변을 제공합니다.
**질문: 지식 그래프가 전체 이메일 텍스트를 저장하여 벡터 검색을 완전히 피할 수 있나요?**
노드에 속성으로 텍스트를 저장할 수는 있지만, 그래프 순회는 두 구절이 얼마나 유사한지 판단할 수 없습니다. 만약 사용자가 "이 불만과 어조가 일치하는 스레드를 찾아주세요"라고 요청하면, 순회 방식은 어조를 비교할 방법이 없어 아무것도 반환하지 않거나 모든 것을 반환하게 됩니다. 의미로 점수를 매기려면 여전히 벡터 검색이 필요합니다. 현재 일부 그래프 데이터베이스는 자체적인 벡터 인덱스를 포함하고 있는데, 예를 들어 Neo4j의 [벡터 인덱스](https://neo4j.com/docs/cypher-manual/current/indexes/semantic-indexes/vector-indexes/)에 대한 문서는 그래프 저장소 내부에서 동일한 유사성 검색을 실행하는 것을 설명합니다.
**질문: Mailmind에서 지식 그래프를 실시간 이메일 트래픽과 어떻게 동기화하나요?**
들어오는 모든 이메일은 작은 파이프라인을 작동시킵니다. NER(개체명 인식) 모델로 개체를 추출하고, 신원을 해결한 다음, 노드와 관계를 그래프에 upsert합니다. 업데이트와 삭제는 더 까다롭습니다. Mailmind는 쓰기 전 로그(write-ahead log)에서 이벤트를 재생하는 마이크로 배치 접근 방식을 사용합니다. 이 그래프는 정적인 덤프가 아니라, 받은 편지함의 지속적으로 업데이트되는 반영물입니다.
**질문: 지식 그래프를 구축하려면 엄격한 스키마가 필요한 것 아닌가요?**
현대의 속성 그래프 데이터베이스(property graph databases)는 스키마리스 유연성과 가벼운 제약 조건을 혼합할 수 있게 합니다. Mailmind의 그래프는 소수의 핵심 노드 레이블(Person, Project, Thread, Email, Order, Meeting)과 관계 유형을 정의하며, 다운타임 없이 새로운 것을 추가할 수 있습니다. 이메일 주소와 같은 식별 필드의 고유성 제약 조건은 그래프를 사용 가능하게 유지하면서도 특정 구조에 묶이지 않게 합니다.
**질문: 단순해 보이는 질문에서 벡터 검색이 실패하는 경우는 언제인가요?**
답변이 특정 사실이나 관계에 의존할 때, 벡터 검색은 두 개체(entity)가 모두 언급된 청크를 검색하지만 그 연결고리는 보여주지 못할 수 있습니다. 예를 들어,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39