1일 차: 메모리를 사후 고려 사항으로 취급하는 것을 그만둔 날
요약
AI 에이전트 구축 시 메모리 시스템을 사후에 추가하는 대신, 초기 설계 단계부터 통합하는 중요성을 강조합니다. Cognee를 활용하여 단순 벡터 검색을 넘어 지식 그래프 기반의 하이브리드 메모리 레이어를 구축하는 방법을 소개합니다.
핵심 포인트
- 단순 벡터 스토어의 평면적 한계를 지식 그래프로 극복
- 엔티티와 관계 추출을 통한 멀티 홉(multi-hop) 추론 가능
- Cognee를 활용한 하이브리드 그래프-벡터 메모리 구현
- AI 에이전트의 맥락 이해도를 높이는 메모리 설계 전략
저는 AI 앱을 구축할 때 모든 개발자가 저지르는 것과 거의 동일한 실수를 저지를 뻔했습니다.
노트북을 열고, FastAPI 서버를 구동하고, LLM을 연결하고, 응답 스트리밍을 시작한 다음, 아마도 끝날 때쯤인 새벽 2시경에 어떤 종류의 메모리 시스템을 사후 고려 사항 (afterthought)으로 급하게 덧붙이려던 참이었습니다. 여기에는 딕셔너리(dictionary) 하나, 저기에는 데이터베이스 테이블(database table) 하나. 어쩌면 마지막 다섯 개의 메시지를 컨텍스트 윈도우 (context window)에 밀어 넣고 그것을 "메모리"라고 부를 수도 있었겠죠. 저도 전에도 이렇게 해본 적이 있습니다. 명백히 문제가 생기기 전까지는 항상 괜찮게 느껴지곤 하죠.
이번에는 스스로를 붙잡았습니다.
우리가 여기까지 오게 된 과정
우리는 WeMakeDevs Hangover Part AI 해커톤을 위해 Continuum이라는 프로젝트를 구축하고 있습니다. 이 해커톤의 전제는 거의 불쾌할 정도로 단순합니다: 당신의 AI가 어젯밤의 기억이 없는 채로 라스베이거스에서 깨어났습니다. 잊어버리지 않는 AI를 만드세요. 그들이 사용하길 원하는 도구는 AI 에이전트 (AI agents)를 위한 하이브리드 그래프-벡터 메모리 레이어 (hybrid graph-vector memory layer)인 Cognee입니다. 그리고 심사는 말 그대로 당신이 이를 얼마나 깊이 있게 사용하는지에 무게를 둡니다.
그래서 저는 1일 차에 한 가지 규칙을 가지고 앉았습니다: 튜터 로직 (tutor logic)을 건드리지 말고, LLM 프롬프트 (LLM prompts)를 건드리지 말고, 프론트엔드 (frontend)를 건드리지 마라. 그저 Cognee를 작동시키고 다른 모든 것이 그 위에 올라갈 레이어를 구축하라.
지루하게 들릴 수도 있습니다. 하지만 그것은 이번 주 제가 내린 최고의 결정이었습니다.
Cognee란 대체 무엇인가
솔직히 말해서, Cognee에 대한 저의 첫 반응은 "회의론"이었습니다. 또 다른 메모리 라이브러리라고? 그건 그냥 단계가 몇 개 더 추가된 벡터 스토어 (vector store) 아닌가?
아닙니다. 그리고 그 차이는 제가 예상했던 것보다 더 중요합니다.
벡터 스토어 (vector store)는 시맨틱 검색 (semantic search)을 제공합니다. 텍스트를 임베딩 (embed)하고, 임베딩을 저장한 다음, 나중에 이를 쿼리 (query)하여 쿼리와 시맨틱하게 유사한 청크 (chunks)를 돌려받는 방식입니다. 유용하긴 하지만, 그것은 평면적 (flat)입니다. 모든 정보 조각은 다른 모든 정보 조각과 동일한 관계를 가집니다: 즉, 아무런 관계가 없다는 것입니다.
Cognee는 그 위에 지식 그래프 (knowledge graph)를 구축합니다. 텍스트 조각과 함께 **remember()**를 호출하면, 단순히 임베딩 (embedding)만 하는 것이 아닙니다. 엔티티 (entities)와 관계 (relationships)를 추출하는 추출 패스 (extraction pass)를 실행하여, 이를 그래프의 노드 (nodes)와 엣지 (edges)로 저장합니다. 따라서 나중에 **recall()**을 호출할 때, 단순히 벡터 유사도 검색 (vector similarity search)만 수행하는 것이 아니라 그래프 순회 (graph traversal)도 함께 수행합니다. 관계의 사슬을 따라갈 수 있고, 멀티 홉 (multi-hop) 질문에 답할 수 있으며, 서로 다른 시점에 입력한 서로 다른 정보들을 연결할 수 있습니다.
튜터링 앱의 경우, 이는 "학생이 4번 문제를 틀렸다" (평면적인 사실)와 "학생은 인수분해에서 부호 오류 오개념을 가지고 있으며, 이는 완전제곱식을 만드는 데 필요한 선행 조건이고, 학생은 현재 그 부분에서 막혀 있다" (관계의 사슬) 사이의 차이와 같습니다. 두 번째 방식이 실제로 더 나은 교육을 할 수 있도록 도와주는 방식입니다.
Cognee는 네 가지 연산을 노출하며, 해커톤은 명시적으로 이 연산들을 중심으로 설계되었습니다:
**remember()**는 텍스트, 파일 또는 URL을 수집하여 지식 그래프로 구조화합니다. **recall()**은 자연어 질문으로 해당 그래프를 쿼리하며, 의미론적 유사도 (semantic similarity)와 그래프 순회를 모두 사용하여 관련 결과를 반환합니다. **improve()**는 수집 후 강화 패스 (post-ingestion enrichment pass)를 실행하여 노드의 가중치를 재조정하고 오래되거나 중복된 데이터를 가지치기 (pruning) 합니다. **forget()**은 다른 모든 것을 지우지 않고 그래프에서 데이터를 정밀하게 제거합니다.
네 가지 연산. 완전한 회상 (Total recall). 이것이 그들의 슬로건이며, 솔직히 말해 정확한 표현입니다.
내가 가장 먼저 한 일은 이것이 실제로 작동함을 증명하는 것이었다
애플리케이션 코드를 한 줄도 쓰기 전에, 나는 일회용 테스트 파일을 작성했습니다. 유닛 테스트 (unit test)도 아니고, pytest 피스처 (fixture)도 아니었습니다. 그저 세 가지 작업을 수행하는 _test_cognee.py_라는 평범한 파이썬 (Python) 스크립트였습니다:
- 가상의 학생 상호작용을 설명하는 한 문장과 함께 **remember()**를 호출합니다.
- 해당 상호작용에 대한 질문과 함께 **recall()**을 호출합니다.
- 결과를 출력합니다.
그런 다음 나는 그것을 실행했습니다.
설정을 올바르게 맞추기 위해 Cognee 문서를 읽는 시간을 포함하여 약 40분 정도가 소요되었습니다. 구체적으로는 remember() 과정 중 그래프 추출 (graph extraction)을 수행하기 위해 LLM API 키가 필요하다는 사실을 알아내는 데 시간이 걸렸습니다. 이것이 저를 당황하게 만든 유일한 문제였습니다. 저는 단순히 임베딩 모델 (embedding model)만 있으면 되는 순수 벡터 저장소 (vector store)처럼 작동할 것이라고 예상했지만, 그래프 추출 단계에서 내부적으로 LLM을 호출하더군요. .env 파일에 이를 설정하고 나니 제대로 작동했습니다.
그리고 **recall()**이 방금 저장한 내용과 실제로 관련된 응답을 반환했을 때, 무언가 깨달음이 왔습니다. 이것은 단순한 장난감이 아니었습니다. 그래프 순회 (graph traversal)는 제가 10초 전에 저장한 단 하나의 사실로부터 정확한 정보를 끌어내고 있었습니다. 수백 개의 저장된 상호작용 속에서 이것이 어떻게 작동할지 벌써 그려졌습니다.
저는 테스트 파일을 삭제했습니다. 막혔던 부분이 풀렸습니다.
제가 이 단계를 강조하는 이유는 이 과정을 건너뛰고 싶은 유혹이 매우 강하기 때문입니다. 여러분은 무언가를 만들고 싶어 합니다. 앉아서 다섯 줄짜리 테스트 스크립트를 실행하는 것은 마치 미루기 (procrastination)처럼 느껴집니다. 하지만 다른 모든 것이 그 위에 구축된 상태에서, 3일 차에 메모리 레이어 (memory layer) 전체가 작동하지 않는다는 것을 발견하는 것은 5일간의 해커톤에서 진정으로 재앙적인 일입니다. 1일 차의 한 시간 동안 진행하는 생존 확인 (proof-of-life)은 3일 차의 8시간 디버깅 (debugging)보다 가치 있습니다.
왜 FastAPI인가
저는 백엔드 개발자입니다. Flask, Django, 그리고 FastAPI를 사용해 보았습니다. 이 프로젝트에서 FastAPI는 명백한 선택이었으며, 단순히 "빠르다"는 점 외에 그 이유를 설명하고 싶습니다.
이 특정 프로젝트를 위한 핵심 기능은 비동기 (async) 지원입니다. Cognee의 작업들인 remember(), recall(), improve(), **forget()**은 모두 비동기 함수 (async functions)입니다. 이들은 I/O 작업을 수행합니다. LLM을 호출하고, 데이터베이스에 기록하며, 그래프를 순회합니다. 만약 비동기 작업 위에 동기식 (synchronous) 서버를 구축한다면, 모든 것을 차단(block)하거나, 4일 차 밤 11시에 눈물이 날 정도로 이벤트 루프 (event loops)와 씨름하게 될 것입니다.
FastAPI는 async-native (비동기 네이티브)입니다. 모든 엔드포인트 (endpoint)는 async 함수입니다. 별도의 복잡한 절차 없이 라우트 핸들러 (route handlers) 내부에서 Cognee 호출을 직접 await 할 수 있습니다. 멘탈 모델 (mental model)이 깔끔하며, 그 깔끔함이 유지됩니다.
그 외에도: 프론트엔드 팀원이 당신의 설명 없이도 API를 이해할 수 있게 해주는 자동 OpenAPI 문서, Pydantic을 통한 타입 검증 (type validation), 그리고 서버가 요청을 받기 전에 환경 설정 (environment config)을 검증할 수 있는 startup lifespan hook (시작 수명 주기 훅) 등이 있습니다. 마지막 항목이 중요한 이유는, 데모 도중 모호한 500 에러와 함께 API 키 누락을 발견하는 것보다, 시작 단계에서 명확한 에러와 함께 이를 잡아내는 것이 훨씬 더 낫기 때문입니다.
메모리 라이프사이클 서비스 (The Memory Lifecycle Service): 내가 하루 동안 만든 가장 중요한 것
Cognee가 작동한다는 것을 증명한 후, 나는 전체 백엔드의 중추가 된 파일 하나를 만들었습니다: memory.py.
개념은 간단합니다. 코드베이스의 다섯 군데 서로 다른 곳에서 **cognee.remember()**를 직접 호출하는 대신, 모든 Cognee 작업은 이 단일 모듈을 통하게 됩니다. 다른 모든 서비스는 오직 여기서만, 그리고 여기서만 임포트 (import) 합니다.
왜일까요? 세 가지 이유가 있습니다.
디버깅 (Debugging). 메모리와 관련하여 무언가 문제가 생겼을 때 (반드시 무언가는 생기게 되어 있습니다), 확인해야 할 곳은 정확히 한 곳뿐입니다. 튜터링 엔진 (tutoring engine), 채점 서비스 (grading service), 전략 선택기 (strategy selector), 그리고 두 개의 라우터 (routers)에 흩어져 있지 않습니다. 단 하나의 파일입니다.
데모 (The demo). remember(), recall(), improve(), 그리고 **forget()**에 대한 모든 호출은 타임스탬프 (timestamp), 학생 ID, 데이터셋, 그리고 무엇이 이를 트리거했는지에 대한 평이한 영어 설명과 함께 JSON 파일에 기록됩니다. 데모 중에 심사위원들에게 시스템이 수행한 모든 메모리 작업에 대한 문자 그대로의 타임스탬프가 찍힌 영수증을 보여줄 수 있습니다. 이것은 단순히 멋진 UI 디테일이 아니라,
규율 (Discipline)입니다. 이것을 먼저 구축함으로써 팀의 규범을 설정하게 됩니다. 중앙 모듈이 존재하고 그곳이 명백히 올바른 위치이기 때문에, 아무도 서비스를 우회하여 Cognee를 직접 호출하지 않습니다. 이는 모두가 빠르게 움직이며 지름길(shortcuts)이 쌓여가는 해커톤에서 발생하는 아키텍처 드리프트 (architectural drift) 현상을 방지합니다.
이 모듈에는 네 가지 Cognee 작업에 직접 매핑되는 다섯 가지 함수와, 로그를 다시 읽어오는 여섯 번째 유틸리티가 있습니다. **remember_interaction()**은 학생의 시도에 대한 구조화된 설명을 받아 저장합니다. **recall_student_context()**는 학생 ID와 쿼리 문자열을 받아 관련 이력을 반환합니다. **improve_student_memory()**는 학생의 데이터셋에 대한 풍부화 (enrichment) 패스를 트리거합니다. **forget_resolved_misconception()**은 해결된 오개념 (misconceptions)을 가지치기(prunes)하고, 그래프를 깨끗하게 유지하기 위해 improve를 다시 실행합니다. 마지막으로 **get_lifecycle_log()**는 학생 ID로 필터링 가능한 이벤트 이력을 반환합니다.
이 다섯 가지 함수를 작성하는 데는 아마 두 시간 정도 걸렸을 것입니다. 하지만 일주일의 남은 기간 동안 이 함수들을 사용함으로써 우리는 문제의 한 범주 전체를 피할 수 있었습니다.
1일 차 종료 시점의 모습
하루를 마치고 리포지토리 (repo)에 푸시했을 때, 다음과 같은 것들이 존재했습니다:
네 개의 의존성 (dependencies)이 포함된 가상 환경 (virtual environment). 팀원들이 설정을 어떻게 하는지 물어볼 필요가 없도록 만든 .env.example 파일. 모든 환경 변수를 중앙 집중화하고 시작 시 검증하는 config.py. 네 가지 작업과 로깅이 포함된 memory.py 서비스. 기억(remember), 회상(recall), 개선(improve), 망각(forget)의 전체 라이프사이클 (lifecycle)을 실행하고 로그에 네 가지 이벤트가 모두 포함되어 있는지 확인하는 tests/test_memory_service.py. 작동하는 헬스 체크 (health check) 엔드포인트를 가진 main.py 내의 FastAPI 앱. 그리고 서버를 실행하는 정확한 방법을 설명하는 README 섹션.
실제 튜터링 로직에 관한 것은 아직 아무것도 존재하지 않았습니다. 질문 생성도, 채점도, 전략 선택도 없었습니다. 그리고 그것이 정답이었습니다.
기초가 탄탄했습니다. 향후 4일 동안 그 위에 구축된 모든 것들은 발을 디딜 자리를 확보하고 있었습니다.
내가 계속해서 되돌아오게 되는 지점
이 프로젝트의 또 다른 버전이 있었습니다. 그 버전에서는 1일 차에 프롬프트 템플릿 (prompt templates)을 작성하고, GPT-4o를 쓸지 Claude를 쓸지 논쟁하며, 작동은 하지만 실질적인 메모리 (memory)는 없는 채팅 엔드포인트 (chat endpoint)를 구축하는 데 시간을 보냈습니다. 저는 그런 프로젝트를 본 적이 있습니다. 데모 중에 "사용자를 어떻게 기억하나요?"라는 질문에 "음, 마지막 몇 개의 메시지를 컨텍스트 (context)로 전달합니다"라고 대답해야만 하는 그런 프로젝트 말입니다.
대신 제가 내놓은 답변은 이렇습니다: 이 시스템은 학생별 지식 그래프 (knowledge graph)를 구축합니다. 모든 상호작용을 구조화된 메모리 (structured memory)로 저장합니다. 단순히 키워드 매칭 (keyword matching)을 사용하는 것이 아니라, 그래프 탐색 (graph traversal)을 사용하여 관련 히스토리 (history)를 회상합니다. 매 세션이 끝날 때마다 그래프를 개선합니다. 해결된 오개념 (misconceptions)은 방치하여 오래된 잘못된 데이터가 향후 학습을 오염시키게 두는 대신, 이를 망각합니다.
그러한 답변이 가능했던 이유는 우리가 1일 차에 구축한 것들 덕분입니다.
메모리 (memory)는 마지막에 추가하는 기능이 아닙니다. AI 에이전트 (AI agent)에게 메모리는 곧 아키텍처 (architecture)입니다. 그것을 먼저 제대로 구축한다면, 그 외의 모든 것은 이미 탄탄한 기초가 마련된 집 안에 방을 만드는 것과 같습니다.
이 글은 WeMakeDevs Hangover Part AI 해커톤 (2026년 6월 29일 – 7월 5일) 기간 동안 구축된 Continuum의 빌드 로그 1일 차 기록입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기