DevLog @ 2025.04.14

들어가며

지난번에는 AIRI의 기억 시스템을 이야기했습니다. 오늘은 이렇게 복잡한 기억 시스템을 어떻게 구현할지 더 깊이 파고들고 앞으로의 전망도 살펴보겠습니다.

검색 엔진에서 시작하기

검색 엔진은 검색 성능 요구가 높습니다. 이를 위해 시스템은 2단계 정렬 과정을 구현합니다:

  • 기본 정렬 (Coarse Ranking, 조 정렬)
  • 비즈니스 정렬 (Fine Ranking, 정밀 정렬)

기본 정렬은 1차 선별 역할로, 검색 결과에서 양질의 문서를 빠르게 골라 상위 N 개를 추출합니다. 그다음 정밀 정렬로 세밀하게 점수를 매겨 최종적으로 최적의 결과를 사용자에게 돌려줍니다.

즉 기본 정렬은 성능에 큰 영향을 주고, 비즈니스 정렬은 최종 랭킹 품질에 영향을 줍니다.

따라서 기본 정렬은 최대한 단순하고 효과적이어야 하며, 비즈니스 정렬에서 핵심 요소만 뽑아 와야 합니다. 현재 기본 정렬과 비즈니스 정렬 모두 정렬 표현식으로 구성됩니다.

OpenSearch / Wentian 엔진 DSL [1]

Neko가 많이 써 온 알리바바 클라우드 OpenSearch를 예로 들어 보겠습니다. 검색 엔진에는 재정렬을 위한 내장 함수들이 있습니다:

static_bm25

정적 텍스트 연관도. 전통적인 NLP 방식으로, 질의와 문서의 일치 정도를 측정합니다. RAG의 _similarity score_와 비슷합니다. 값 범위: 0~1

exact_match_boost

사용자가 지정한 질의어의 최대 가중치를 얻습니다. score boost 함수라고도 합니다. 입력한 키워드가 토크나이즈 전에 문서 필드(제목, 본문 등)의 "내용"에 적중하는 경우에 해당합니다. 예를 들어 "How to make Neurosama"를 검색할 때, "Neurosama"라는 정확한 구절을 담은 문서와 페이지가 "Neuro"와 "sama"가 따로 나오는 문서보다 높은 점수를 받아야 합니다.

timeliness, timeliness_ms

시의성 점수. 더 새로운 내용일수록 연관도가 높습니다.

데이터는 어떻게 저장되나?

알리바바 클라우드의 OpenSearch, Grafana 내장 Loki, 그 이전의 ElasticSearch 엔진(일부 영상 사이트는 ElasticSearch 기반으로 개발됐습니다) 등 어떤 검색 엔진이든, 사용하기 전에 데이터를 그 검색 엔진 안의 별도 데이터 구조로 재가공해야 합니다.

이 재가공은 어떻게 구현할까요? 여기에 DTS가 필요합니다.

DTS [2]

DTS라는 개념을 좀 더 소개하겠습니다.

Data Transformation Services는 비즈니스 데이터베이스와 검색 엔진 인스턴스 사이의 통신과 데이터 동기화를 담당하는 시스템입니다.

구현 원리: MySQL과 Postgres의 네이티브 watch/subscribe 이벤트 기능으로 테이블 변경을 감지한 뒤 데이터를 검색 엔진으로 동기화합니다. 이 과정에서 데이터는 원하는 형식으로 직렬화되며 데이터 구조 변환(ETL: extract, transform, load)을 거칩니다.

조 정렬 검색을 수행할 때, 데이터베이스의 에서 검색하는 것과 비슷하지 않나요? 가상 테이블처럼요. 어느 정도 맞는 이해입니다. 다만 뷰는 보통 데이터베이스와 같은 하부 자료구조(B+ 트리)를 쓰는 반면, 검색 엔진은 그래프나 특화된 인덱스 키-값 데이터베이스 같은 다양한 전용 자료구조를 쓸 수 있습니다.

토크나이제이션?

전통적인 검색 엔진에서 중국어 문서를 입력하면 이런 과정을 거칩니다:

  • 문장 분할 (큰 문단을 문장으로 쪼개기)
  • 단어 분할 (문장을 단어/글자, 명사, 동사 등으로 쪼개기)
  • 병음 변환
  • 현재 사전 커버리지 설정에 따라 앞의 결과를 매핑하고 덮어쓰기
  • 기본적인 벡터화와 특징 추출 수행
  • 저장 계층에 기록

영어도 토크나이제이션이 필요하지만 훨씬 간단합니다. 공백이 단어 경계 역할을 하니까요.

성능은 어떻게 최적화하나?

  • 연산 집약적
  • 내부 작업 스케줄러를 여러 개 두고 데이터를 천천히 인덱싱
  • 해밍 거리, 코사인 거리 같은 전통 NLP 기법은 미리 계산해 저장 가능
  • 인기 검색어는 토크나이제이션과 정렬 결과를 캐시 가능
  • 데이터 레이크하우스? AWS에서 흔히 쓰이며 보통 여러 데이터베이스나 데이터 소스를 아우르는 집계 질의에 씁니다. 매우 느려서 사실상 데이터 분석과 BI 에만 사용합니다

Recall이란?

Recall(재현/검색)은 키워드를 입력했을 때 기대한 문서를 실제로 찾아낼 수 있는지를 말합니다.

Search와의 차이는? Search는 "사용자가 시작하는 동작"이고, Recall은 "검색에 응답하기 위해 기계가 하는 일"입니다.

Reranking이란?

Reranking의 의미는 이렇습니다. 임베딩 모델 벡터를 기반으로 ANN(Approximate Nearest Neighbor)과 KNN(K-Nearest Neighbor)의 벡터 거리 정렬에만 의존하면 실제로는 편향이 생깁니다.

앞서 OpenSearch에서 소개한 exact_match_boost와 timeliness 함수가 더는 존재하지 않게 되기 때문입니다.

검색된 문서에 다른 필드와 단계를 기준으로 한 정렬을 추가하고 싶다면 어떻게 할까요?

RAG는 이제 reranking model이라는 새로운 과정을 대중화했습니다. 본질적으로 별도의 전문가 모델을 써서 1차로 검색된 데이터를 자동으로 재정렬하는 것입니다.

하지만 reranking 으로도 기억 계층의 여러 문제를 풀 수는 없습니다. 망각 곡선, 기억 강화, 무작위 기억 회상, 감정이 영향을 미치는 재정렬 점수 같은 것들은 reranking 모델이 다룰 수 있는 영역이 아닙니다.

AIRI를 위한 좋은 기억 계층을 만들려면 RAG의 기본 능력과 과거 검색 엔진의 재정렬 경험을 결합해 좋은 재정렬 메커니즘을 세워야 합니다.

기억 계층 실험 플랫폼

Project AIRI Memory Driver @duckdb/duckdb-wasm Playground

왼쪽에 강조된 "half life"는 기억의 반감기입니다.

기본값으로 시간은 1초 = 1일 로 흐르므로, 7초가 지나면 기억 점수가 절반이 됩니다.

기억 점수란 무엇일까요? 기억 점수는 주로 이것으로 제어됩니다:

그 결과로 나온 점수가 현재 점수입니다.

original은 무엇일까요? 초기화 시점의 점수입니다.

예: 원래 점수가 523 인데 현재 점수는 실제로 서서히 줄어들고 있습니다:

계속하기 전에 짚고 넘어가면, 이 망각 곡선 SQL은 무상태(stateless)입니다.

무상태란 무슨 뜻일까요? 점수를 갱신하기 위해 데이터베이스에서 실시간 작업을 돌릴 필요 없이, "현재 시각"을 기준으로 망각 함수를 바로 적용해 점수를 계산한다는 뜻입니다.

그럼 현재 점수가 떨어지면 어떻게 할까요? 이 문제를 풀려면 기억을 강화하는 방법이 필요합니다.

인간의 기억 시스템에 빗대어

간격 반복에서 언급되는 망각 곡선과 심리학의 기억 시스템 기본 작동 원리를 바탕으로 [3]

인간의 기억은 몇 가지로 나눌 수 있다는 걸 압니다:

  • 작업 기억
  • 단기 기억
  • 장기 기억
  • 근육 기억

작업 기억은 기억해 둘 필요성이 가장 낮습니다.

단기 기억은 망각 곡선에 따라 강도(점수)가 점차 감쇠합니다. 이 시점에는 이 과정을 모델링할 단기 기억 시뮬레이션 함수가 필요합니다.

장기 기억은 중요하며 반감기가 길고, 단기 기억에서 진화한 것입니다.

마지막으로 근육 기억은 기억의 한 종류라기보다 이미 형성된 조건 반사에 가깝습니다.

AIRI는 어떻게 설계해야 할까?

여기서 AIRI의 구현 원리를 엿볼 수 있습니다:

  • 작업 기억은 messages 배열 같은 것
  • 단기 기억은 잘 회상되지 않는 RAG 기억 항목 같은 것으로, 새로운 것일수록 회상되기 쉬움
  • 장기 기억은 쉽게 회상되지만 흐릿해지는 RAG 항목 같은 것으로, 과거 회상 횟수가 많을수록 회상되기 쉬움
  • 근육 기억은 고정된 패턴 같은 것으로, A가 나타나면 ActionA와 MemoryA가 함께 나타나는, 정확한 매칭 메커니즘에 가까움

그런데 이 설계가 맞을까요?

분명 여기서는 시간적 연관도와 검색 횟수라는 두 차원만 도입했습니다. 더 복잡한 시스템을 추구하기 시작하면 이것만으로는 한계에 부딪힙니다.

빠른 복습

DevLog에서 언급한 정렬 표현식을 복습해 보면 이해에 도움이 될 겁니다.

코사인 거리는 "연관도" 이며, 가장 기본적인 조 정렬입니다:

이제 시간을 개입시켜야 하므로 시간 거리를 저장할 필드를 하나 더 추가하고, 결합 점수 (1.2 * similarity) + (0.2 * time_relevance)를 저장할 별도 필드를 만듭니다. 여기서 의미적 연관도는 1.2배 가중치(증폭 계수이며 1 미만일 필요는 없습니다), 시간 거리 연관도는 0.2배 가중치를 갖습니다.

이렇게 하면 무상태 다중 필드 연관도 정렬 SQL을 깔끔하게 구현하면서 매개변수(1.2와 0.2)로 조정도 할 수 있습니다.

기억 상세 카드에서는 "simulate retrieval"을 클릭해 기억 회상을 능동적으로 트리거할 수 있습니다.

현재 데모에서는 원본 테이블의 검색 횟수 필드에 UPDATE 문으로 +1을 더하는 단순한 방식으로 구현되어 있습니다.

여기에 숨은 함정이 있습니다. 이건 여전히 단일 차원 계산이라 "회상 = 강화" 와 같습니다.

하지만 현실 세계는 그렇지 않습니다. 기억은 슬플 수도 기쁠 수도 있고, 슬픔은 부정적 피드백을, 기쁨은 긍정적 피드백을 가져옵니다.

그래서 이 부분은 아직 완성하지 못했습니다.

감정?

https://drizzle-orm-duckdb-wasm.netlify.app/#/memory-simulator

이 새 시뮬레이터에는 감정 관련 시뮬레이션이 포함되어 있습니다:

감정은 기억과 관련이 있을까?

사탕을 먹고 싶은데 못 먹는 건 단순한 문제입니다. 못 먹으면 당연히 기분이 나쁘죠.

그러다 보면 감정이 사실 기억과 관련되어 있다는 걸 알게 됩니다.

"과거의 어떤 기억이 즐거워서 다시 겪고 싶은데", "그 기억 속 상황을 지금은 재현할 수 없어서", 그래서 "얻지 못해 기분이 나쁜" 것이죠.

기억 데이터베이스에 "기쁨" 과 "혐오" 점수를 저장할 수 있습니다:

PTSD?

PTSD에는 보통 "trigger"와 "flashback"이라는 두 단어가 따라옵니다. 분명 PTSD 관련 기억은 억제되어야 하고, 혐오와 트라우마 점수가 높아야 합니다.

하지만 실제로 PTSD 관련 기억은 갑자기 떠오를 수 있습니다. 생체 모방과 데이터 시뮬레이션 관점에서는 난수를 써서 이 효과를 구현할 수 있습니다.

https://yutsuki.moe/2019/09/a0d0fa1b/ 의 감정 모델을 참고할 수 있습니다.

아직 할 일이 많습니다...

예를 들어 지금 ReLU의 감정은 어떤가요? ReLU는 누군가에 대한 나쁜 기억을 갖고 있을까요?

기억은 기쁨과 슬픔이 함께 극성을 이루는 항목으로 나타날까요?

욕구는요? 소원 시스템을 만들어야 할까요?

백그라운드 작업 과 비슷하게, 발생한 기억을 하나씩 처리하고 인덱싱하면서 최근 경험을 바탕으로 과거 기억의 여러 점수를 수정하는 꿈꾸기 에이전트나 잠재의식 에이전트를 만들 수도 있습니다.

하지만 반드시 "꿈꾸기" 과정이 필요한 건 아니고, 그냥 "백그라운드 작업" 이면 됩니다.

재인덱싱 관점에서 보면 꿈꾸기 에이전트와 잠재의식 에이전트는 인덱스를 다시 만드는 일과 같습니다.

여기까지 오면 Mem0Zep Memory 같은 라이브러리가 롤플레잉과 감정 AI에서는 전혀 쓸모없다는 걸 알게 됩니다 😦

갈 길이 멀고, 저희는 계속 노력해야 합니다.

참고 문헌


  1. https://help.aliyun.com/zh/open-search/industry-algorithm-edition/rough-sort-functions ↩︎

  2. https://help.aliyun.com/zh/open-search/industry-algorithm-edition/configure-dts-real-time-synchronization ↩︎

  3. https://zh.wikipedia.org/wiki/遗忘曲线 ↩︎