잊지 않는 AI 에이전트 구축하기

@akshay_pachaar
영어3개월 전 · 2026년 4월 13일
625K
1.9K
305
46
4.1K

TL;DR

단순한 목록에서 Cognee의 오픈 소스 그래프 벡터 엔진으로 진화하여 지속적이고 관계적인 추론을 가능하게 하는 AI 에이전트 메모리 시스템에 대한 기술적 심층 분석입니다.

에이전트 메모리에 대한 첫 원칙 기반 접근: Python 리스트부터 마크다운 파일, 벡터 검색, 그래프-벡터 하이브리드, 그리고 마지막으로 이 모든 것을 위한 깔끔한 오픈소스 솔루션까지

Akshay 🚀 - inline image

LLM은 기본적으로 상태 비저장(stateless)입니다. 모든 API 호출은 새롭게 시작됩니다. ChatGPT와 채팅할 때 느끼는 "메모리"는 매 요청마다 전체 대화 기록을 다시 전송함으로써 만들어지는 환상에 불과합니다.

이 트릭은 캐주얼한 채팅에는 통합니다. 하지만 실제 에이전트를 구축하려는 순간 무너집니다.

메모리를 건너뛰면 나타나는 7가지 실패 모드:

  1. 컨텍스트 기억 상실: 이미 제공한 정보를 에이전트가 다시 물어봄
  2. 개인화 제로: 모든 상호작용이 일반적으로 느껴짐
  3. 멀티스텝 작업 실패: 작업 중간에 중간 상태가 조용히 사라짐
  4. 반복되는 실수: 에피소드 기억이 없어 동일한 오류가 영원히 반복됨
  5. 지식 축적 불가: 모든 세션이 처음부터 시작됨
  6. 공백으로 인한 환각: 컨텍스트가 넘치면 모델이 지어냄
  7. 정체성 붕괴: 연속성 없음, 신뢰 없음

당연한 대응은 "더 많은 컨텍스트를 넣자"입니다. 그래서 128K와 200K 토큰 윈도우가 모든 것을 해결할 것처럼 느껴집니다.

하지만 그렇지 않습니다.

관련 정보가 긴 컨텍스트의 중간에 위치하면 정확도가 30% 이상 떨어집니다. 이것은 잘 문서화된 "중간에서 길을 잃음(lost in the middle)" 효과입니다.

컨텍스트는 공유 예산입니다: 시스템 프롬프트, 검색된 문서, 대화 기록, 출력 모두가 동일한 토큰을 두고 경쟁합니다.

100K 토큰에서도 지속성, 우선순위 지정, 중요성의 부재로 인해 원시 컨텍스트 길이만으로는 충분하지 않습니다.

Akshay 🚀 - inline image

메모리는 단순히 프롬프트에 더 많은 텍스트를 집어넣는 것이 아닙니다. 에이전트가 기억하는 내용을 구조화하여 중요한 것을 찾을 수 있도록 하는 것입니다.

실제로 도움이 되는 인지과학 프레임워크

Lilian Weng의 2023년 정립은 기본 프레임워크가 되었습니다:

에이전트 = LLM + 메모리 + 계획 + 도구 사용.

네 가지 동등한 기둥입니다.

그녀의 분류는 인지과학에서 차용했으며, 인간의 메모리는 세 가지 시스템으로 나뉩니다:

  • 감각 메모리는 원시 지각 입력을 포착하고 1초 미만 동안 유지합니다. 주의를 기울인 부분만 다음 단계로 전달됩니다.
  • 작업 메모리는 능동적 사고가 일어나는 곳입니다. 한 번에 약 7±2개 항목을 유지합니다(Miller의 1956년 발견). 집중을 잃으면 내용이 사라집니다.
  • 장기 메모리는 실질적인 용량 제한이 없는 내구성 있는 저장소입니다. 검색이 병목입니다: 수백만 개를 저장해도 필요한 하나를 기억하지 못할 수 있습니다.

각각은 현대 에이전트 아키텍처의 구성 요소에 직접 매핑됩니다:

Akshay 🚀 - inline image

장기 메모리는 다시 세분화됩니다:

  • 에피소드: 특정 과거 사건 ("화요일에 PostgreSQL 클러스터가 다운됨")
  • 의미: 사실과 개념 ("PostgreSQL은 관계형 데이터베이스입니다")
  • 절차: 기술과 워크플로우 ("사용자가 환불을 요청하면 먼저 구매 날짜를 확인하세요")

에피소드와 의미 사이의 다리는 메모리 통합입니다: 반복된 특정 사건이 일반 지식으로 증류됩니다. 수십 번의 상호작용에서 "사용자들이 일관되게 요약을 선호한다"는 것을 알아차린 에이전트는 이를 재사용 가능한 규칙으로 전환해야 합니다. 통합이 없으면 에이전트는 배우지 않고 개별 사건만 재생합니다.

Akshay 🚀 - inline image

최소 에이전트와 가장 먼저 깨지는 것

프레임워크를 벗기면 에이전트는 루프입니다: 인지, 사고, 행동.

python
1class Agent:
2 """최소 AI 에이전트: 인지, 사고, 행동"""
3 def __init__(self):
4 self.client = anthropic.Anthropic()
5 self.model = "claude-sonnet-4-20250514"
6
7 def run(self, user_input: str) -> str:
8 response = self.client.messages.create(
9 model=self.model,
10 max_tokens=1024,
11 messages=[{"role": "user", "content": user_input}],
12 )
13 return response.content[0].text

"사과 4개 있어"라고 말한 다음 "하나 먹었는데, 몇 개 남았지?"라고 물어보면 에이전트는 무슨 사과를 말하는지 전혀 모릅니다. 각 호출은 고립되어 존재합니다.

레이어 1: Python 리스트

모든 사람이 가장 먼저 시도하는 수정:

python
1class Agent:
2 def __init__(self):
3 self.client = anthropic.Anthropic()
4 self.messages = [] # 전체 "메모리"가 리스트 하나
5
6 def chat(self, user_input: str) -> str:
7 self.messages.append({"role": "user", "content": user_input})
8 response = self.client.messages.create(
9 model="claude-sonnet-4-20250514",
10 max_tokens=1024,
11 messages=self.messages, # 전체 기록이 매번 전송됨
12 )
13 reply = response.content[0].text
14 self.messages.append({"role": "assistant", "content": reply})
15 return reply

이제 멀티턴이 작동합니다. 사과 질문이 올바르게 답변되는 이유는 전체 대화가 매 호출마다 다시 전송되기 때문입니다.

두 가지 문제가 빠르게 나타납니다:

  • 리스트가 무제한으로 커집니다. 약 200턴이 되면 컨텍스트 한계에 도달하고 가장 오래된 메시지가 조용히 사라집니다. 1턴의 사용자 이름은 어제의 가벼운 농담보다 훨씬 먼저 사라집니다. 우선순위 없이 엄격한 시간 순서만 있습니다.
  • 모든 것이 RAM에 있습니다. Python 프로세스가 종료되는 순간 에이전트는 당신이 누군지 전혀 모릅니다.

레이어 2: 지속성을 위한 마크다운 파일

다음 단계는 메모리를 디스크에 쓰는 것입니다. 마크다운은 자연스러운 선택입니다: 사람이 읽을 수 있고, Git 친화적이며, 에이전트가 일반 텍스트로 다시 읽을 수 있습니다. Claude Code는 CLAUDE.mdMEMORY.md 파일로 정확히 이 패턴을 사용합니다.

python
1class MarkdownMemoryAgent:
2 def __init__(self):
3 self.client = anthropic.Anthropic()
4 self.history_file = Path("memory/conversation_history.md")
5 self.facts_file = Path("memory/known_facts.md")
6
7 def save_to_disk(self, role: str, content: str) -> None:
8 with open(self.history_file, "a") as f:
9 f.write(f"### {role} at {datetime.now().isoformat()}\n{content}\n\n")
10
11 def load_history(self) -> str:
12 if self.history_file.exists():
13 return self.history_file.read_text()
14 return ""
15
16 def chat(self, user_input: str) -> str:
17 self.save_to_disk("user", user_input)
18 history = self.load_history()
19 response = self.client.messages.create(
20 model="claude-sonnet-4-20250514",
21 max_tokens=1024,
22 system=f"Previous conversation:\n{history}",
23 messages=[{"role": "user", "content": user_input}],
24 )
25 reply = response.content[0].text
26 self.save_to_disk("assistant", reply)
27 return reply

지속성이 해결되었습니다. 스크립트를 다시 시작해도 대화가 디스크에 남아 있습니다. 에이전트가 시간이 지남에 따라 추출하는 별도의 사실 파일을 유지할 수도 있습니다:

text
1- 사용자 이름은 Sarah
2- Sarah는 Acme Corp에서 백엔드 팀을 관리함
3- Acme Corp는 B2B SaaS 회사
4- 현재 프로덕션 데이터베이스를 새 AWS 리전으로 마이그레이션 중

어떤 편집기에서든 파일을 열고 에이전트가 알고 있는 내용을 정확히 확인하고 수동으로 수정할 수 있습니다. 프로토타이핑에 진정으로 유용합니다.

4개의 사실로는 완벽하게 작동합니다. 전체 파일을 컨텍스트에 로드하면 LLM이 Sarah, 그녀의 회사 또는 업계에 관한 모든 질문을 처리합니다.

이제 3개월 후를 상상해보세요. 에이전트에 2,000개의 추출된 사실과 200개의 대화 로그가 있습니다. 디스크에 500K+ 토큰의 마크다운이 있고 컨텍스트 윈도우는 128K입니다.

더 이상 모든 것을 로드할 수 없습니다. 현재 쿼리와 관련된 사실만 선택적으로 검색해야 합니다. 플랫 파일에서는 키워드 검색만이 유일한 옵션입니다:

python
1# 사용자 질문: "클라우드 마이그레이션 상태는 어떻게 되나요?"
2grep("cloud migration", facts_file)
3# 반환: []
4# 디스크의 사실은 "프로덕션 데이터베이스를 새 AWS 리전으로 마이그레이션 중"
5# "cloud migration"이라는 단어는 어디에도 없음
6
7# 사용자 질문: "데이터베이스 작업은 어떤 팀이 담당하나요?"
8grep("database team", facts_file)
9# 반환: []
10# 한 사실은 Sarah가 "백엔드 팀을 관리"한다고 함. 다른 사실은 팀이
11# "프로덕션 데이터베이스를 마이그레이션 중"이라고 함. 하지만 "database"와
12# "team"이 함께 있는 단일 라인은 없음

작은 규모에서는 마크다운 파일이 작동합니다. 실제 규모에서는 키워드 검색을 강제하고, 키워드는 동의어, 의역 또는 사실 간의 연결을 처리할 수 없습니다.

정보는 디스크에 있습니다. 하지만 모두 로드할 수 없고, 키워드 검색은 올바른 조각을 찾기에는 너무 취약합니다.

OpenClaw를 사용해본 적이 있다면 이 상황을 본 적이 있을 것입니다. 메모리를 마크다운 체크포인트 파일로 저장하며, 수 주간의 일일 사용 후에는 컨텍스트가 축적되고 압축되면서 초기 사실들이 조용히 사라집니다. 저장소는 있지만 검색이 없습니다.

지능적인 검색 없는 저장소는 카탈로그가 없는 도서관입니다.

레이어 3: 벡터 검색과 그 한계

임베딩을 추가하세요. 마크다운을 청크로 나누고, 청크를 임베딩하고, 코사인 유사도로 검색하세요. 이제 "database"가 "PostgreSQL"과 일치하는 이유는 벡터가 임베딩 공간에서 가깝게 위치하기 때문입니다. 동의어 문제가 해결됩니다.

그러면 새로운 벽에 부딪힙니다. 벡터 DB에 있는 다음 세 가지 사실을 고려해보세요:

text
1- "Alice는 Project Atlas의 기술 리드입니다"
2- "Project Atlas는 기본 데이터 저장소로 PostgreSQL을 사용합니다"
3- "PostgreSQL 클러스터가 화요일에 중단되었습니다"

사용자 질문: "Alice의 프로젝트가 화요일 중단의 영향을 받았나요?"

쿼리에 Alice와 화요일 중단이 언급되어 있으므로 벡터 검색은 첫 번째와 세 번째 사실을 높게 순위 매깁니다. 하지만 중요한 연결고리인 "Project Atlas는 PostgreSQL을 사용합니다"는 Alice도 화요일도 언급하지 않습니다. 이것이 연결 조각이며, 표면화되지 않을 조각입니다.

각 사실은 임베딩 공간에서 고립된 점입니다. 이들을 연결하는 결합 조직은 벡터에 보이지 않습니다.

Akshay 🚀 - inline image

이것은 예외적인 경우가 아닙니다. 실제 질문의 일반적인 형태입니다. 비즈니스 지식은 본질적으로 관계형입니다: 사람은 팀에 속하고, 팀은 프로젝트를 소유하며, 프로젝트는 시스템에 의존하고, 시스템에는 장애가 있습니다. 두 개 이상의 홉을 넘나드는 모든 질문은 플랫 벡터 검색이 답할 수 있는 범위를 초과합니다.

역량 매트릭스

각 레이어는 이전의 고통을 해결하지만 더 깊은 고통을 드러냅니다:

Akshay 🚀 - inline image

단일 메모리 레이어에 지속성, 의미 이해, 관계 추론이 모두 필요합니다.

이것을 직접 구축하려면 벡터 데이터베이스, 그래프 데이터베이스, 관계형 저장소, 엔터티 추출기, 중복 제거 파이프라인, 엣지 가중치 시스템을 함께 연결해야 합니다. 에이전트 로직 한 줄을 작성하기 전에 몇 주간의 인프라 작업입니다.

저는 이 격차를 깔끔하게 메워주는 솔루션을 사용해 왔습니다. 완전히 오픈소스이며, 세 가지 저장소 패러다임을 하나의 지붕 아래에서 처리하고, 몇 분 안에 실행할 수 있습니다. Cognee에 대해 이야기해보겠습니다.

Cognee: 세 가지 저장소, 하나의 엔진, 네 번의 호출

Cognee는 에이전트 메모리를 위해 구축된 오픈소스 지식 엔진입니다. 벡터 검색을 지식 그래프 및 관계형 출처 레이어와 단일 시스템으로 결합합니다.

전체 API 표면은 네 개의 비동기 호출입니다:

python
1import cognee
2
3await cognee.add("여기에 문서를 입력하세요") # 모든 것을 수집
4await cognee.cognify() # 지식 그래프 + 임베딩 구축
5await cognee.memify() # 메모리 자체 개선
6await cognee.search("쿼리 입력") # 추론으로 검색

이 네 개의 호출 뒤에는 세 가지 저장소 아키텍처가 있습니다.

Akshay 🚀 - inline image

왜 하나가 아니라 세 가지 저장소인가?

각 저장소는 다른 저장소가 할 수 없는 지식의 차원을 포착합니다:

  • 관계형 저장소 → 출처: 데이터가 어디서 왔는지, 언제 수집되었는지, 누가 접근 권한이 있는지
  • 벡터 저장소 → 의미: 콘텐츠가 무엇을 의미하는지, 무엇과 유사한지
  • 그래프 저장소 → 관계: 엔터티가 어떻게 연결되는지, 무엇이 무엇을 일으키는지, 누가 누구에게 보고하는지

이 중 하나라도 평탄화하면 검색 정확도에 중요한 정보가 손실됩니다.

기본 스택은 SQLite + LanceDB + Kuzu로, 완전히 내장되어 있고 파일 기반입니다. pip install cognee와 LLM API 키만 있으면 실행됩니다.

Docker나 외부 서비스가 필요 없습니다.

프로덕션의 경우 SQLite를 Postgres로, LanceDB를 Qdrant/Pinecone/pgvector로, Kuzu를 Neo4j/FalkorDB/Neptune으로 교체하세요.

어느 쪽이든 동일한 네 개의 호출 API입니다.

cognify가 실제로 하는 일

cognee.cognify()는 원시 텍스트를 구조화되고 상호 연결된 지식으로 변환하는 다단계 파이프라인을 실행합니다:

  1. 문서 분류 유형 및 도메인별
  2. 권한 확인 멀티테넌트 접근 제어용
  3. 청크 추출 단락 구조를 존중 (고정 크기 절단이 아님)
  4. 엔터티 및 관계 추출 LLM을 통해, 콘텐츠 해싱을 통한 자동 중복 제거
  5. 요약 생성 효율적인 검색용
  6. 이중 인덱싱 벡터 저장소(임베딩) 및 그래프 저장소(엣지)로

중복 제거 단계는 생각보다 중요합니다. 동일한 엔터티가 50개 문서에 걸쳐 나타나면 Cognee는 이를 50개의 인바운드 엣지를 가진 단일 그래프 노드로 병합합니다. 에이전트는 더 이상 "Alice"를 50명의 다른 낯선 사람으로 보지 않습니다. 그리고 파이프라인은 기본적으로 증분식입니다: 새 파일이나 업데이트된 파일만 재처리됩니다.

Akshay 🚀 - inline image

모든 그래프 노드에는 해당 임베딩이 있습니다. 이 이중 표현이 핵심 트릭입니다: 벡터를 통해 진입(의미적으로 유사한 콘텐츠 찾기)하고 그래프를 통해 빠져나오기(연결된 엔터티로 관계 따라가기), 또는 그 반대입니다. 이것이 의미 검색을 희생하지 않고 멀티홉 쿼리를 가능하게 하는 것입니다.

Memify: 학습하는 메모리

memify()는 Cognee를 모든 "수집 및 검색" 도구와 구분짓는 것입니다. 그래프에 대해 RL에서 영감을 받은 최적화 패스를 실행합니다:

  • 강화 좋은 검색으로 이어진 유용한 경로
  • 가지치기 오래되어 건드리지 않은 노드
  • 자동 조정 실제 사용량 기반 엣지 가중치
  • 파생 사실 추가 암시적 관계 식별을 통해

고객 지원 에이전트의 그래프는 자연스럽게 제품 문서와 환불 정책을 통한 경로를 강화하고 거의 쿼리되지 않는 HR 엣지를 약화시킵니다. 그래프는 시간이 지남에 따라 자체적인 관련성 감각을 발전시킵니다.

Akshay 🚀 - inline image

14가지 검색 모드

Cognee는 14가지 검색 모드를 제공합니다. 실제로 사용하게 될 것들:

Akshay 🚀 - inline image

Cognee 메모리로 실제 에이전트 구축하기

다음은 Cognee를 인지-사고-행동 루프에 연결하는 완전한 패턴입니다:

python
1import cognee
2from cognee import SearchType
3
4class CogneeMemoryAgent:
5 """그래프-벡터 하이브리드 영구 메모리를 가진 에이전트."""
6
7 def __init__(self, session_id: str = "default"):
8 self.llm_client = OpenAI()
9 self.session_id = session_id
10
11 async def ingest(self, text: str, dataset: str = "main"):
12 await cognee.add(text, dataset)
13 await cognee.cognify([dataset])
14
15 async def recall(self, query: str) -> str:
16 results = await cognee.search(
17 query_text=query,
18 query_type=SearchType.GRAPH_COMPLETION,
19 session_id=self.session_id,
20 )
21 return results[0] if results else ""
22
23 async def chat(self, user_input: str) -> str:
24 context = await self.recall(user_input)
25 messages = [
26 {"role": "system", "content": "당신은 도움이 됩니다. 메모리 컨텍스트를 사용하세요."},
27 {"role": "system", "content": f"메모리 컨텍스트:\n{context}"},
28 {"role": "user", "content": user_input},
29 ]
30 response = self.llm_client.chat.completions.create(
31 model="gpt-4o-mini", messages=messages
32 )
33 reply = response.choices[0].message.content
34 await cognee.add(
35 f"사용자: {user_input}\n어시스턴트: {reply}",
36 "conversations"
37 )
38 await cognee.cognify(["conversations"])
39 return reply

메모리 사이클: 수집, 추출, 저장, 검색, 응답, 다시 저장. 각 턴이 지식 그래프를 풍부하게 하고, 증분 처리는 새 콘텐츠만 인덱싱하도록 합니다.

세션 메모리는 대명사 해결을 자동으로 처리합니다:

python
1await cognee.search(query_text="Alice는 어디에 살고 있나요?", session_id="conv_1")
2await cognee.search(query_text="그녀는 직업이 무엇인가요?", session_id="conv_1")
3# "그녀"는 세션 컨텍스트에서 Alice로 해결됨

멀티테넌시는 데이터셋별 권한(읽기, 쓰기, 삭제, 공유)으로 그래프 수준에 내장되어 있습니다. 네임스페이스 분리가 아닌 실제 그래프 수준 격리입니다.

실용적인 앞으로의 길

오늘날 에이전트를 구축하고 있다면 실제 시작 질문은 다음과 같습니다: "내 에이전트가 무엇을 기억해야 하고, 어떤 종류의 질문에 답할 것인가?"

쿼리에 유사성 검색만 필요하다면("이 대화와 비슷한 대화 찾기"), 벡터 전용 메모리로 충분합니다. 쿼리가 엔터티 경계를 넘나드는 순간("Alice의 프로젝트가 화요일 중단의 영향을 받았나요?"), 그래프 탐색이 필요합니다.

별도의 벡터, 그래프 및 관계형 저장소를 직접 연결할 있습니다. 이 경로를 선택하는 팀은 일반적으로 자체 사용량에서도 학습하지 않는 메모리 레이어를 위해 인프라에 몇 주를 소비합니다.

Cognee는 이를 네 개의 API 호출로 축소합니다. 내장된 기본값으로 몇 분 안에 실행할 수 있습니다. 교체 가능한 백엔드(Postgres, Qdrant, Neo4j)는 에이전트 코드를 변경하지 않고 프로덕션으로 이동시킵니다.

지능에는 저장소뿐만 아니라 구조가 필요합니다. 세 가지 저장소 패러다임(관계형, 벡터, 그래프)은 경쟁 옵션이 아닙니다. 동일한 메모리 시스템의 상호 보완적인 레이어입니다. 그렇게 취급하는 것이 상태 비저장 LLM 래퍼를 실제로 학습하는 무언가로 바꾸는 것입니다.

에이전트가 오늘 잊어버렸지만 내일 기억했으면 하는 다음 것은 무엇인가요? 거기서 시작하세요.

👉 GitHub에서 Cognee 확인하기 →, 별표를 눌러주시고, 다음 에이전트에 연결해보세요.

네 개의 비동기 호출, pip install 한 번이면 실행됩니다.

여기까지입니다!

이 글을 재미있게 읽으셨다면:

저를 찾아주세요 →@akshay_pachaar ✔️

매일 AI, 머신러닝, 그리고 바이브 코딩 모범 사례에 대한 튜토리얼과 인사이트를 공유합니다.

원클릭 저장

YouMind로 바이럴 글을 AI 심층 읽기

소스를 저장하고, 핵심 질문을 던지고, 주장을 요약해 바이럴 글을 다시 활용할 수 있는 노트로 바꾸세요. 하나의 AI 워크스페이스에서 모두 할 수 있습니다.

YouMind 둘러보기
크리에이터를 위해

당신의 Markdown을 깔끔한 𝕏 글로

직접 쓴 장문을 올릴 때 이미지, 표, 코드 블록을 𝕏에 맞게 정리하는 일은 번거롭습니다. YouMind는 전체 Markdown 초안을 깔끔하고 바로 게시할 수 있는 𝕏 글로 바꿔 줍니다.

Markdown → 𝕏 사용해 보기

분석할 패턴 더 보기

최근 바이럴 아티클

더 많은 바이럴 아티클 보기