기억하는 시스템 — RAG는 지도의 한 도시였다
에이전트가 길게 일하려면 기억이 필요하다. 그런데 기억을 '필요할 때 꺼낸다'는 그 동작이 곧 RAG였다. 시리즈를 한 호흡에 되짚으며, RAG가 컨텍스트 생애주기의 한 토막임을 본다.
지난 글에서 우리는 판단과 반복이 모이면 에이전트가 된다고 했다. 그런데 에이전트가 한 번의 호출을 넘어 길게 일하려면, 빠진 조각이 하나 있다. 기억이다. LLM은 본래 기억이 없다 — 한 번의 호출이 끝나면 방금 한 일을 깨끗이 잊는다. 가중치에 새겨진 사전 지식은 있어도, 이 사용자가 지난주에 무슨 식단을 받았는지는 모른다.
이 글은 시리즈의 마지막 편이다. 토큰에서 출발해 좌표로 가는 길(글자에서 좌표로), 좌표 사이의 가까움을 재는 길, 가까운 것을 골라 모델에 먹이는 길, 생성된 것을 검사하고 행동으로 잇는 길을 차례로 걸었다. 마지막 길은 기억하는 길이다. 그리고 이 길 끝에서, 우리가 줄곧 걸어온 지도 전체가 한눈에 들어온다.
따라갈 질문 하나를 고정하자. 에이전트가 세션을 넘어 무언가를 기억한다는 게, 기계 안에서는 정확히 무슨 일인가.
두 종류의 기억 — 작업창과 그 너머
기억은 둘로 나뉜다.
단기 기억(working context)은 지금 이 작업창 안의 기억이다. 현재 대화에 쌓인 메시지, 방금 도구가 돌려준 결과, 지금 풀고 있는 문제의 맥락. 사실 이건 우리가 시리즈 내내 다룬 바로 그것 — 프롬프트에 담기는 컨텍스트다. 한계가 분명하다. 컨텍스트 창은 유한하고, 세션이 끝나면 증발한다. 도구 왕복이 길어질수록 이 작업창이 차오르고, 긴 컨텍스트의 중간이 묻히는 문제가 다시 고개를 든다.
장기 기억(long-term memory)은 세션을 넘어 살아남는 기억이다. 사용자의 선호(“매운 거 싫어함”), 과거의 사건(“지난주에 제육볶음 받음”), 작업의 상태. 이걸 어딘가에 저장해 두고, 지금 작업에 필요할 때 꺼내 단기 기억(프롬프트)에 넣는다.
여기서 시리즈 전체가 한 바퀴 돌아 제자리로 온다.
기억의 ‘회상’은 RAG의 retrieve-inject다
장기 기억을 ‘필요할 때 꺼낸다’는 그 동작 — 그게 정확히 RAG다. 저장된 기억이 수천 개면 전부 프롬프트에 넣을 수 없다. 그러니 지금 상황과 관련된 기억만 골라야 한다. 어떻게? 기억을 임베딩해 저장해 두고, 현재 맥락으로 유사도 검색을 하고, 가장 관련된 것을 추려 프롬프트에 주입한다.
문장 하나하나가 이 시리즈의 앞 장들이다. 임베딩으로 저장하는 건 좌표를 만드는 장이었고, 유사도 검색은 가까움을 재는 장이었고, 추려서 넣는 건 먹이는 장이었다. 메모리의 ‘회상’은 RAG의 retrieve-inject와 같은 기계다. 메모리와 RAG의 경계는 흐려진다. 문서를 찾으면 RAG, 과거의 나를 찾으면 메모리 — 부르는 이름이 다를 뿐 메커니즘은 하나다.
우리는 이 구분을 밥비서에서 이미 손으로 실천하고 있었다. 식단 생성 파이프라인에 이전에 만든 식단을 참조해 같은 메뉴 반복을 피하는 단계를 넣은 것 — 그게 장기 기억이다. 과거의 사건(지난 식단)을 저장해 뒀다가 새 식단을 짤 때 꺼내 참고했으니까. 비싼 후보 계산의 결과를 미리 캐시해 둔 것도, 한 번 치른 계산을 ‘기억’해 다음에 재사용한 셈이다. ‘메모리 시스템’이라는 말을 붙이기 전에, 우리는 무엇을 저장하고 언제 꺼낼지를 이미 설계하고 있었다.
가까움으로 안 풀리는 질문 — 관계를 따라가다
그런데 기억과 지식을 ‘의미적 가까움’으로만 찾는 게 늘 최선일까. 지금까지 우리의 검색은 한 가지 질문만 던졌다. “이것과 의미가 가까운 게 뭐지?” 벡터 공간에서 가장 가까운 점들을 줍는 일.
어떤 질문은 가까움으로 풀리지 않는다. “이 재료를 쓰면서 김치도 들어가고 30분 안에 되는 레시피”나 “이 재료의 대체재로 쓸 수 있는 것들” 같은 질문. 이건 의미적 거리가 아니라 관계를 따라가야 답이 나온다.
여기서 knowledge graph(지식 그래프)가 등장한다. 그래프는 세상을 점(노드)과 선(관계)으로 표현한다.
[제육볶음] --재료로 쓴다--> [돼지 앞다리살]
[돼지 앞다리살] --별칭--> [돈전지]
[돼지 앞다리살] --속한다--> [한식]
정보가 좌표가 아니라 연결망으로 산다. 벡터 검색과 그래프 검색은 묻는 방식부터 다르다.
| 벡터 검색 | 그래프 검색 | |
|---|---|---|
| 질문 | ”이것과 의미가 가까운 것" | "이것과 이 관계로 이어진 것” |
| 강점 | 모호한 의미·유사성 | 명확한 관계·다단계 추론 |
| 약점 | 관계를 못 따라감 | 그래프를 미리 구축해야 함 |
GraphRAG는 이 그래프를 RAG에 끌어들인 것이다. 질의에서 출발해 선을 타고 관련 노드들을 모은 뒤, 그 묶음을 프롬프트에 주입한다. 벡터 RAG가 “비슷한 문단들”을 주워온다면, GraphRAG는 “이 개체와 연결된 사실들”을 구조째 가져온다. 그래서 여러 사실을 연결해야 하는 질문에 강하다. 공짜는 아니다 — 그 그래프를 먼저 만들어야 한다는 큰 비용이 따른다.
그래프를 만드는 방법이 꼭 무거운 벡터 인프라일 필요는 없다. 우리 내부 시스템 중 하나는 컨텍스트의 관계를 무거운 벡터 DB가 아니라 마크다운 문서를 링크로 잇는 가벼운 그래프로 표현한다. 문서들을 링크로 잇고, 그 연결을 따라 맥락을 모으는 구조. 본격 GraphRAG의 경량판이라 볼 수 있다 — 임베딩 인프라 없이, 링크라는 가장 값싼 ‘선’으로 관계를 잇는 것. 트레이드오프는 분명하다. 의미적 유사 검색은 약하지만, 관계를 사람이 읽고 쓰고 따라가기엔 더없이 투명하고 가볍다. “벡터가 정답”이라는 기본값을 의심하고 문제에 맞는 더 가벼운 구조를 고른 것이다. 빌드타임 재료 정규화에 무거운 그래프 인덱스 대신 가벼운 인덱스를 골랐던 그 사고와 같은 결이다.
RAG는 더 큰 생애주기의 한 토막이다
이제 한 발 물러나 보자. 시리즈에서 따로 배운 조각들을 한 줄로 늘어놓으면 이렇다 — 찾기(retrieval), 먹이기(injection), 검사하기(judge), 기억하기(memory), 잇기(graph). 그런데 이들은 모두 더 큰 질문 하나의 부분들이다. 지금 이 작업에, 무엇을 컨텍스트로 넣을 것인가?
이 질문을 시간 축으로 펼치면 하나의 생애주기가 된다. 무엇을 저장할 것인가(모든 걸 기억할 순 없다). 언제 기억할 것인가. 언제 잊을 것인가(낡고 틀린 기억은 오히려 해롭다). 언제 다시 꺼낼 것인가(= retrieve). 무엇을, 어떤 순서로 주입할 것인가(= inject). 이게 context lifecycle(컨텍스트 생애주기)다.
그리고 여기서 시리즈의 결론이 나온다. RAG는 이 생애주기의 한 토막일 뿐이다. RAG는 “찾아서 넣는다”에서 멈춘다 — 무엇을 애초에 저장할지, 언제 잊을지는 묻지 않는다. 우리가 정적 RAG의 한계를 다루며 도달한 지점이 정확히 여기다. 유사도는 ‘지금 무엇이 비슷한가’에 답하지만, ‘지금 무엇이 여전히 유효한가’에는 답하지 못한다. 저장·망각·재주입까지 다루는 일은 retrieve-inject의 상위 문제이고, 우리는 그 상위 문제를 정면으로 다루는 일을 동적 컨텍스트 시스템이라 부른다.
그 글에서 우리는 “이건 그냥 RAG라 부르기엔 뭔가 더 큰데”라는 직감에서 출발했었다. 이 시리즈를 통과하고 나면 그 직감의 정체가 또렷해진다. RAG는 한 도시였고, 그 도시는 검색이라는 강과 주입이라는 다리, 판단이라는 관문을 가졌다. 그리고 그 도시는 메모리라는 더 큰 대륙의 한 지방이었다. 우리는 그 대륙을 이미 걷고 있었다.
지도를 다 그리고 나서
다시 월요일 아침이다. 한 사용자가 밥비서를 연다. 일주일치 저녁이 이미 차려져 있다. 이 시리즈의 처음에 우리는 이 식탁 앞에서 물었다 — 이 자동화는 어디서 왔는가. 이제 그 여정을 한 호흡에 되짚을 수 있다.
레시피 한 편이 토큰으로 잘리고, 임베딩 모델을 통과해 좌표가 되고, 재료들은 유사도와 가벼운 인덱스 위에서 대표 이름으로 묶이고, 코퍼스에 쌓였다. 사용자가 앱을 열면 구조화 필터가 후보를 거르고, 결정론 알고리즘이 여러 후보 식단을 짜면 LLM이 그중 가장 어울리는 한 벌을 고르고, 그 식단이 프롬프트에 주입되어 해설로 나온다. 생성된 것은 품질 게이트와 판단으로 검사받고, 도구 호출은 표준화된 인터페이스로 꽂히고, 판단과 반복이 모여 에이전트가 된다. 그리고 이 모든 것은, 무엇을 기억하고 언제 꺼낼지를 묻는 더 큰 생애주기 안에 있다.
이 지도에서 가장 중요한 건 길의 이름이 아니다. 우리가 이름을 알기 전에 이미 그 길을 걸었다는 사실이다. 표기가 다른 재료를 의미로 묶어야 한다는 걸 알았기에 임베딩을 썼고, 런타임에 매번 생성하면 비싸다는 걸 알았기에 코퍼스를 미리 짓고 알고리즘으로 조합했고, 모델이 틀린다는 걸 알았기에 검사하는 자리를 세웠다. 누가 가르쳐서가 아니라, 문제가 거기 있었기 때문에.
이 시리즈가 한 일을 한 줄로 줄이면 이렇다. 이름 없이 먼저 풀었고, 그다음 이름을 붙였고, 이제 같은 메커니즘을 다른 도메인으로 옮겨 심는다. 여기서 보여주고 싶은 것은 RAG를 정의할 줄 안다는 사실이 아니라, RAG를 사고할 줄 안다는 것이다 — 어디서 표준과 같고 어디서 의도적으로 갈라섰는지, 그 판단의 근거까지.