AI에게 컨텍스트를 반복해서 전달하는 것을 그만둔 이유
요약
사용자가 흩어진 대화 기록과 노트를 연락처 중심으로 구조화하여 브리핑을 제공하는 MeetMind 개발 사례를 소개합니다. Flask, Groq API, LLaMA 3 및 Hindsight 메모리 레이어를 활용하여 효율적인 컨텍스트 추출 시스템을 구축했습니다.
핵심 포인트
- 연락처 중심의 구조화된 메모리 레이어 구축
- Groq와 LLaMA 3를 활용한 빠른 브리핑 생성
- Hindsight를 통한 효율적인 정보 검색 및 지속성 확보
- 미니멀한 UI/UX를 통한 타이트한 메모 루프 설계
모든 클라이언트 통화, 모든 인터뷰, 모든 네트워킹 대화 전에 저는 항상 똑같은 15분짜리 의식을 치르곤 했습니다. 예전 노트를 뒤지고, Slack 스레드를 거슬러 올라가며, 이미 한 번 읽었던 문서들을 다시 읽는 일 말이죠. 정보는 존재했습니다. 단지 제가 곧 만나게 될 '사람'을 중심으로 정리되어 있지 않았을 뿐입니다. 데이터가 누락된 것이 아니라, 데이터가 흩어져 있어서 발생하는 그 마찰(friction)이 결국 MeetMind를 만드는 계기가 되었습니다.
내가 이것을 만든 이유
문제는 노트가 없다는 것이 아니었습니다. 어떤 도구도 그 노트들을 '연락처(contact)'별로 정리해주지 않는다는 것이 문제였습니다. 저는 이름을 입력하면 즉시 다음과 같은 정보를 얻고 싶었습니다:
- 과거 대화에서 나온 관련 컨텍스트 (context)
- 내가 약속했거나 상대방이 약속한 사항들
- 내가 기록해둔 후속 조치 사항 (follow-up items)
- 자연스러운 대화를 시작할 수 있는 몇 가지 주제
그리 큰 요구사항은 아닙니다. 하지만 제가 시도했던 그 어떤 것도 실제로 이를 수행하지 못했습니다. CRM은 너무 무거웠고, 노트 앱에는 검색/추출 레이어 (retrieval layer)가 없었습니다. 일반 텍스트 파일은 도구가 해야 할 일을 제가 직접 하게 만들었습니다. 그래서 저는 MeetMind를 만들었습니다. Flask 기반의 웹 애플리케이션으로, 이름만 입력하면 Groq의 API와 LLaMA 3를 사용하여 단 몇 초 만에 해당 인물에 대한 자신의 기억으로부터 추출된 구조화된 브리핑 (briefing)을 제공합니다.
흥미로운 점은 이것이 작동한다는 사실 그 자체가 아닙니다. 시스템의 거의 모든 설계 결정이 메모리 레이어 (memory layer)로서 Hindsight를 초기부터 채택함으로써 형성되었다는 점입니다.
MeetMind의 실제 모습
랜딩 페이지는 한 문장으로 핵심을 전달합니다: '결코 준비되지 않은 상태로 회의에 들어가지 마세요.' 이것이 제품의 전부입니다.
UI는 두 가지 모드(Briefing 및 Save Notes)를 가진 단일 페이지 Flask 앱입니다. 회의 전에는 연락처의 이름을 입력하고 생성(generate)을 누릅니다. 회의 후에는 무슨 일이 있었는지 짧은 노트를 작성하고 저장합니다. 이 루프는 의도적으로 매우 타이트하게 설계되었습니다. 캘린더 연동도 없고, 녹음 기능도 없으며, 주변 소리 자동 캡처 (ambient capture) 기능도 없습니다. 무엇을 기억할 가치가 있는지는 사용자가 직접 결정합니다.
히어로 섹션(hero section)에 떠 있는 메모리 카드는 단순히 장식용이 아닙니다. 이는 시스템이 정보를 표면화하는 방식을 보여주는 실제 사례입니다. 특정 연락처에 대한 노트를 저장해 두었다면, MeetMind는 전체 브리핑(briefing)을 생성하기도 전에 빠른 미리보기를 보여줍니다. API가 반환하는 has_memory 불리언(boolean) 값이 이를 제어합니다. 즉, 저장된 이력이 있는 연락처는 처음 조회하는 연락처와는 다른 시각적 처리를 받게 됩니다.
노트 작성 흐름 또한 매우 미니멀합니다. 연락처 이름 필드, _"예산 논의, 후속 조치, 선호도, 주요 결정 사항..."_이라는 플레이스홀더(placeholder)가 있는 자유 형식 텍스트 영역, 그리고 "메모리에 저장(Save to memory)" 버튼이 전부입니다. 이것이 입력 인터페이스의 전체 모습입니다.
시스템의 구성 방식
내부적으로는 세 가지 중요한 파일이 있습니다: app.py (Flask 라우팅 및 오케스트레이션 (orchestration)), memory_vault.py (지속성 및 검색 (persistence and retrieval)), 그리고 ai_brain.py (Groq를 통한 LLM 통합)입니다. 전체 오케스트레이션 레이어(orchestration layer)는 단 두 줄로 이루어져 있습니다:
# app.py — 오케스트레이션 레이어
history = hindsight_db.recall(contact)
briefing_text = generate_meeting_briefing(contact, history)
한 번의 호출로 문자열 리스트를 검색하고, 다른 한 번의 호출로 해당 리스트를 형식화된 브리핑으로 변환합니다. 두 함수는 서로의 존재를 알지 못합니다. 모든 복잡성은 각 함수 내부에 캡슐화(encapsulated)되어 있습니다.
모든 것을 결정지은 선택: Hindsight의 인터페이스를 중심으로 설계하기
이 프로젝트에서 가장 중대한 선택은 모델이나 프롬프트(prompt), 또는 저장 형식이 아니었습니다. 그것은 초기 단계에서 로컬 메모리 레이어를 Hindsight의 retain 및 recall API를 모델로 삼기로 결정한 것이었습니다.
Hindsight는 정확히 이 문제를 위해 구축된 메모리 라이브러리입니다. 즉, LLM 기반 애플리케이션이 세션 전반에 걸쳐 컨텍스트(contextual) 정보를 저장하고 검색할 수 있는 방법을 제공합니다. 핵심 프리미티브(primitive)는 매우 깔끔합니다. 텍스트를 retain하고, 나중에 쿼리(query)가 주어지면 관련 텍스트를 recall하는 방식입니다. 벡터 인덱스(vector indices)나 유사도 임계값(similarity thresholds)을 노출하지 않습니다. 그저 관련 있는 정보를 돌려줄 뿐입니다.
저는 해당 인터페이스를 정확히 모방하는 로컬 구현체를 구축했습니다:
class MockHindsightClient:
def retain(self, text):
# Parse and persist a note about a contact
...
이 인스턴스의 이름은 memory_store도, notes_manager도 아닌 hindsight_db로 명명되었습니다. 이러한 명명은 의도적이었습니다. 코드베이스의 나머지 부분이 이를 Hindsight로 취급하게 함으로써, 나중에 로컬 구현체를 실제 클라이언트(client)로 교체할 때 단 한 파일만 수정하면 되도록 하고 싶었습니다. 인터페이스(interface)가 계약(contract)이며, 그 뒤의 구현(implementation)은 세부 사항일 뿐입니다.
에이전트 메모리(agent memory)가 실제로 수행해야 하는 일에 대해 깊이 생각해보니 유용한 점 하나가 명확해졌습니다. 여기서 검색 모델(retrieval model)은 의도적으로 단순하게 설계되었습니다. 정확한 이름 키 기반의 조회(lookup) 방식입니다. "Rahul"은 .lower() 호출을 거쳐 "rahul"로 매핑되며, 그러면 그의 노트(notes)를 가져옵니다. 이 계층에서는 퍼지 매칭(fuzzy matching), 임베딩(embeddings), 또는 시맨틱 검색(semantic retrieval)을 사용하지 않습니다. 이러한 단순함은 하나의 기능(feature)입니다. 복잡성은 해당 노트로 무엇을 _하느냐_에 달려 있으며, 그것이 바로 LLM의 역할입니다.
프롬프트 구조(Prompt Structure)가 진짜 핵심 작업이었다
LLM 통합 코드는 45줄에 불과합니다. 그중 대부분은 프롬프트(prompt)입니다. 저는 Groq를 통해 llama-3.3-70b-versatile을 temperature=0.6으로 사용했습니다. 자연스럽게 들릴 만큼 충분히 따뜻하면서도, 컨텍스트(context)에 없는 세부 사항을 즉흥적으로 만들어내지 않을 만큼 충분히 차가운 설정입니다. 실제 API 호출은 네 줄뿐입니다. 진짜 반복적인 작업(iteration)이 필요했던 부분은 출력 스키마(output schema)였습니다.
system_instruction = (
"You are an elite executive assistant AI. Your goal is to prepare a brief, "
"high-impact, bulleted meeting preparation guide for the user."
...
모델에게 자유로운 형식을 허용할 때마다, 모델은 저에게 가장 중요한 정보가 "자신감을 가지고 회의에 임하세요"라고 말하는 것이라고 결정하곤 했습니다. 클라이언트와의 통화 전인 오전 8시 45분에 그런 종류의 미사여구(filler)는 쓸모가 없습니다. 출력을 세 개의 라벨이 붙은 섹션으로 고정함으로써 이러한 이탈(drift)을 거의 완전히 제거했습니다. 모델은 구조(structure)를 제공하면 그 구조를 따릅니다. 이것은 새로운 통찰은 아니지만, 새로운 프롬프트를 작성할 때마다 매번 다시 발견하게 되는 사실입니다.
formatted_history 처리 또한 실제로 중요한 역할을 수행합니다. 프롬프트에 가공되지 않은 문자열 (raw strings)을 그대로 전달하는 대신, 불렛 포인트 (bullet points) 형태로 형식을 맞춥니다. 그리고 데이터가 없는 (null) 경우에 대해서도 명시적인 처리를 수행합니다.
if past_history_list:
formatted_history = "\n".join([f"- {note}" for note in past_history_list])
else:
...
여기서 else 분기는 방어적 프로그래밍 (defensive programming)을 위한 것이 아닙니다. 기능적인 목적을 가집니다. 이 처리가 없다면, 모델은 실제 컨텍스트 (context)가 없음에도 불구하고 마치 실제 컨텍스트에서 나온 것처럼 들리는 모호한 요약본을 생성합니다. 모델에게 "이것은 첫 미팅입니다"라고 알려주면 출력 내용이 도입부 문구로 전환되는데, 이것이 정확히 의도한 바입니다.
실제 세션의 모습
두 개의 저장된 노트가 있는 연락처를 예로 들어보겠습니다. 예를 들어, "React 웹사이트 필요, 예산 ₹50,000, 월요일에 후속 조치 필요, 오전 미팅 선호"와 "Tailwind CSS 확정, 3주 일정, 50% 선불"이라는 내용이 있다면, 브리핑은 긴밀한 상호작용 요약, 후속 조치 및 결제 구조를 표시하는 리마인더 (reminders) 섹션, 그리고 일반적인 표현 대신 프로젝트 이름을 참조하는 대화 시작 문구와 함께 돌아옵니다. 출력은 구조적으로는 결정론적 (deterministic)이지만, 언어 표현은 다양하며, 사용자의 실제 저장된 히스토리 (history)에 근거합니다.
/get_briefing 엔드포인트는 브리핑과 함께 has_memory 플래그를 반환합니다.
return jsonify({
"briefing": briefing_text,
"has_memory": len(history) > 0
...
프론트엔드 (frontend)는 이전 노트의 존재 여부에 따라 브리핑을 다르게 렌더링하는 데 이 플래그를 사용합니다. 첫 만남의 브리핑은 두 달 치의 노트가 뒷받침된 브리핑과는 모습과 내용이 다릅니다. 이는 작은 차이지만, 이 도구가 단순히 보여주기식 (performative)이 아닌 진실되게 느껴지도록 만드는 요소입니다.
빌더의 경험
Flask 라우트 (routes), Groq API 연결, 메모리 인터페이스 (interface), 프롬프트 반복 작업 (prompt iteration) 등 이 모든 코드를 GitHub에 푸시하는 과정은 진정으로 기분이 좋았습니다. 단순히 "기능을 출시했다"는 느낌이 아니라, "제대로 작동하며 온전히 나의 것인 무언가를 만들었다"는 느낌이었습니다.
그 느낌은 매우 특별합니다. 실제로 겪고 있는 문제와 그 문제를 해결하는 시스템 사이의 간극을 메웠을 때 일어나는 현상입니다. 백엔드 (Backend)가 API와 통신했습니다. 메모리 레이어 (Memory layer)가 상태 (State)를 유지했습니다. 브리핑 (Briefing)은 구조화되고 근거가 있는 상태로 돌아왔습니다. 이것이 바로 어떤 튜토리얼도 가르쳐주지 않는 개발의 영역입니다. 당신이 작성한 시스템이 당신을 위해 실제로 무언가를 해내는 순간 말입니다.
저는 이제 회의 전에 예전 노트를 다시 읽는 일을 그만두었습니다. MeetMind를 열고 이름을 입력하면, 컨텍스트 (Context)가 이미 그곳에 있습니다.
내가 배운 것들
메모리 인터페이스 (Memory interface)가 이후의 모든 과정을 결정합니다. 초기에 Hindsight의 retain/recall 추상화 (Abstraction)를 채택한 덕분에 나머지 코드가 단순해졌습니다. LLM 레이어 (LLM layer)는 노트가 어디에서 왔는지 알 필요가 없습니다. Flask 레이어 (Flask layer)는 검색 (Retrieval)이 어떻게 작동하는지 알 필요가 없습니다. 이러한 경계(Boundaries)를 설정하는 것은 초기에 하면 비용이 적게 들지만, 나중에 추가하려면 비용이 많이 듭니다.
프롬프트 구조 (Prompt structure)는 곧 출력 스키마 (Output schema)입니다. 번호가 매겨진 섹션과 명시적인 라벨 (Labels)은 스타일의 선택이 아니라 기능적인 제약 조건입니다. 구조화되지 않은 프롬프트는 구조화되지 않은 출력을 생성하며, 구조화되지 않은 출력은 회의실에 들어가기 직전의 상황에서 파싱 (Parse)하기 어렵고 신뢰하기 어렵습니다.
프롬프트에서의 명시적인 null 처리 (Null handling)는 선택 사항이 아닙니다. LLM은 그럴듯하게 들리는 출력을 내놓는 경향이 있습니다. 이전 컨텍스트가 없다고 말해주지 않으면, 모델은 무언가를 지어낼 것입니다. 빈 상태 (Empty-state) 케이스는 프롬프트 내에서 별도의 코드 경로 (Codepath)로 다뤄져야 하며, 예외 케이스 (Edge case)가 아닙니다.
내부 객체의 이름을 그것이 구현하는 인터페이스 (Interface)의 이름을 따서 지으세요. notes_manager 대신 hindsight_db라고 이름 지음으로써, 이를 임포트 (Import)하는 모든 파일에서 의도된 아키텍처 (Architecture)를 확인할 수 있었습니다. 주석 (Comments)은 부패하지만, 이름은 부패하지 않습니다.
온도 (Temperature)는 기본값이 아니라 결정 사항입니다. 환각 (Hallucination)을 통한 구체적인 정보가 실제 비용을 초래할 수 있는 도구(예: 고객과 실제로 논의하지 않은 내용을 다시 언급하게 될 위험)에서는 0.6이라는 수치가 의도적인 선택이었습니다. 약간 무미건조한 언어는 지어낸 사실보다 훨씬 저렴한 실패 모드 (Failure mode)입니다.
이 시스템은 150줄 미만의 Python 코드, 몇 개의 HTML 템플릿, 그리고 Hindsight를 기반으로 하는 지속성 메모리 계층 (Persistent memory layer)으로 구성되어 있습니다. 메모리를 어떻게 추상화할지, 프롬프트 (Prompt)를 어떻게 구성할지, Null 케이스를 어떻게 처리할지와 같은 설계 결정 사항들은 모두 실제 프로덕션 구현 (Production implementation)으로 직접 확장 가능합니다. 로컬 메모리 클라이언트를 실제 Hindsight 통합 버전으로 교체하는 것은 단 하나의 파일만 수정하면 되는 작업입니다. 시스템의 나머지 부분은 이러한 변화가 일어났는지 알 필요조차 없습니다.
이것이 바로 지향할 가치가 있는 아키텍처 (Architecture)입니다. 그리고 솔직히 말해서, 사용할 가치가 있는 도구이기도 합니다.
링크
- MeetMind 라이브 체험하기: meetmind-iatt.onrender.com
- GitHub의 MeetMind 소스 코드: github.com/sickme78/MeetMind
- Hindsight — 에이전트 메모리 라이브러리 (Agent memory library): github.com/vectorize-io/hindsight
- Hindsight 문서: hindsight.vectorize.io
- 에이전트 메모리란 무엇인가: vectorize.io/what-is-agent-memory
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기