EverOS와 Milvus를 활용한 AI 에이전트의 장기 기억 구축
EverOS는 AI 에이전트를 위한 마크다운(Markdown) 우선 기억 시스템입니다. 이 시스템은 대화에서 지속 가능한 기억을 추출하고, 마크다운을 신뢰할 수 있는 원본 정보로 유지하며, 검색 가능한 파생 인덱스를 구축합니다.
이 튜토리얼에서는 서로 다른 대화에서 이루어진 출시 결정을 기억하는 프로젝트 어시스턴트를 구축해 보겠습니다. Project Atlas 출시에 관한 대화와 다른 프로젝트에 대한 관련 없는 대화를 함께 추가할 것입니다. EverOS는 LLM을 사용하여 기억을 추출하고, Milvus는 하이브리드 검색에 사용되는 BM25 및 벡터 인덱스를 저장합니다.
Conversations
|
v
EverOS + LLM ------> Markdown memory files
|
| embedding model
v
Milvus ------> BM25 + vector hybrid search
LLM과 임베딩 모델은 서로 다른 역할을 담당합니다. LLM은 대화를 구조화된 기억으로 변환합니다. 임베딩 모델은 이러한 기억과 이후의 검색 쿼리를 벡터로 변환합니다. 이 튜토리얼에서 다루는 기본적인 하이브리드 검색에는 재순위 지정 모델이 필요하지 않습니다.
필수 조건
다음이 필요합니다:
- Python 3.12 이상
uv- 실행 중인 Milvus 서버
- OpenAI API 키
이 튜토리얼에서는 http://localhost:19530 주소의 Milvus 서버에 연결합니다. EverOS는 동일한 URI 및 토큰 설정을 통해 Zilliz Cloud도 지원합니다. EverOS의 Milvus 백엔드는 원격 엔드포인트를 요구하며, Milvus Lite 파일 경로는 허용하지 않습니다.
EverOS 설치
로컬 프로젝트를 생성하고, 선택 사항인 Milvus 종속성을 포함하여 EverOS를 설치합니다:
mkdir everos-milvus-demo
cd everos-milvus-demo
uv init --bare --python 3.12
uv add "everos[milvus]"
이 명령어는 의도적으로 버전을 고정하지 않으므로, 새로 설치할 경우 호환되는 최신 EverOS 릴리스가 자동으로 적용됩니다.
튜토리얼을 위해 별도의 메모리 루트를 초기화합니다:
export EVEROS_ROOT="$PWD/everos-data"
uv run everos init --root "$EVEROS_ROOT"
EverOS는 이 디렉터리 아래에 everos.toml 및 ome.toml 를 생성합니다. 또한 추출된 메모리도 이곳에 기록합니다.
OpenAI 및 Milvus 구성
환경 변수를 통해 OpenAI API 키를 설정하고 EverOS를 구성합니다:
export OPENAI_API_KEY="YOUR_OPENAI_API_KEY"
export MILVUS_URI="http://localhost:19530"
export EVEROS_INDEX__BACKEND="milvus"
export EVEROS_MILVUS__URI="$MILVUS_URI"
export EVEROS_MILVUS__COLLECTION_PREFIX="everos_bootcamp"
export EVEROS_LLM__MODEL="gpt-5.4-mini"
export EVEROS_LLM__API_KEY="$OPENAI_API_KEY"
export EVEROS_LLM__BASE_URL="https://api.openai.com/v1"
export EVEROS_EMBEDDING__MODEL="text-embedding-3-small"
export EVEROS_EMBEDDING__API_KEY="$OPENAI_API_KEY"
export EVEROS_EMBEDDING__BASE_URL="https://api.openai.com/v1"
export EVEROS_EMBEDDING__DIMENSIONS="1024"
export EVEROS_MEMORIZE__MODE="chat"
EverOS는 메모리 추출과 임베딩 모두에 OpenAI를 사용합니다. text-embedding-3-small 는 기본적으로 1536 차원을 반환하지만, EverOS는 구성된 dimensions 값을 OpenAI로 전달합니다. 이 튜토리얼에서는 EverOS가 관리하는 Milvus 스키마와 일치하도록 1024 차원을 요청합니다.
chat 메모리 모드를 사용함으로써 이 예제에서는 사용자 메모리에 초점을 맞춥니다. EverOS가 Milvus 컬렉션과 해당 스키마를 관리하므로, 사용자가 직접 생성할 필요가 없습니다.
EverOS 시작
EverOS HTTP 서버를 시작합니다:
uv run everos server start --root "$EVEROS_ROOT"
이 터미널 창을 열어 둡니다. EverOS는 시작 시 Milvus에 연결하여 구성된 접두사를 가진 7개의 파생 인덱스 컬렉션을 생성합니다.
동일한 프로젝트 디렉터리에서 다른 터미널을 열고 서비스를 확인합니다:
curl http://127.0.0.1:8000/health
참조 출력:
{
"status": "ok",
"version": "1.3.0",
"capabilities": {
"llm": true,
"embed": true,
"rerank": false,
"multimodal_llm": false,
"parser": true
},
"cascade": {
"healthy": true,
"pending": 0
}
}
응답에는 추가적인 상태(health) 필드가 포함되어 있습니다. 이 튜토리얼에서 중요한 값은 status: "ok", llm: true, embed: true 및 cascade.healthy: true 입니다.
프로젝트 대화 추가
다음 Python 프로그램은 EverOS에 10개의 독립적인 대화를 전송합니다. Atlas에는 별도의 시작 및 롤백 논의가 있습니다. 다른 프로젝트에 대한 8개의 대화는 주의 분산 요소 역할을 하여, 이후 검색에서 올바른 프로젝트 기억을 식별해야 합니다.
다음 코드를 ‘ add_memories.py ’라는 이름으로 저장하세요:
import json
import time
from urllib.request import Request, urlopen
API_URL = "http://127.0.0.1:8000/api/v2/memory"
NOW = int(time.time() * 1000)
conversations = [
(
"atlas-release",
[
{
"sender_id": "maya",
"sender_name": "Maya",
"role": "user",
"timestamp": NOW,
"content": (
"For Project Atlas, we decided to launch with a 10% canary "
"on September 30. Promote to all users only after the checkout "
"error rate stays below 1% for 30 minutes."
),
},
{
"sender_id": "assistant",
"role": "assistant",
"timestamp": NOW + 1_000,
"content": (
"Understood. I will remember the Atlas launch date, canary "
"percentage, and promotion gate."
),
},
],
),
(
"atlas-rollback",
[
{
"sender_id": "maya",
"sender_name": "Maya",
"role": "user",
"timestamp": NOW + 10_000,
"content": (
"The Atlas rollback owner is Priya. Roll back immediately if "
"checkout errors exceed 2% for five minutes, and keep the "
"previous container image available for 24 hours."
),
},
{
"sender_id": "assistant",
"role": "assistant",
"timestamp": NOW + 11_000,
"content": (
"Got it. Priya owns rollback, with the 2% five-minute trigger "
"and a 24-hour image retention window."
),
},
],
),
(
"orion-pricing",
[
{
"sender_id": "maya",
"sender_name": "Maya",
"role": "user",
"timestamp": NOW + 20_000,
"content": (
"Project Orion will test annual billing with the education "
"segment. The pricing review is scheduled for October 12."
),
},
{
"sender_id": "assistant",
"role": "assistant",
"timestamp": NOW + 21_000,
"content": (
"I will remember Orion's annual billing experiment and October "
"pricing review."
),
},
],
),
(
"vega-mobile",
[
{
"sender_id": "maya",
"sender_name": "Maya",
"role": "user",
"timestamp": NOW + 30_000,
"content": (
"For Project Vega, the mobile team chose offline drafts as the "
"next milestone. Elena will review the interaction design on "
"October 18."
),
},
{
"sender_id": "assistant",
"role": "assistant",
"timestamp": NOW + 31_000,
"content": (
"Noted. Vega's next milestone is offline drafts, followed by "
"Elena's design review."
),
},
],
),
(
"nova-warehouse",
[
{
"sender_id": "maya",
"sender_name": "Maya",
"role": "user",
"timestamp": NOW + 40_000,
"content": (
"Project Nova will migrate the analytics warehouse to Iceberg. "
"Marcus owns the checksum rehearsal scheduled for October 22."
),
},
{
"sender_id": "assistant",
"role": "assistant",
"timestamp": NOW + 41_000,
"content": (
"I will remember Nova's warehouse migration and Marcus's "
"checksum rehearsal."
),
},
],
),
(
"helios-support",
[
{
"sender_id": "maya",
"sender_name": "Maya",
"role": "user",
"timestamp": NOW + 50_000,
"content": (
"Project Helios needs weekend support coverage for the APAC "
"region. Imani will publish the rotation schedule on November 1."
),
},
{
"sender_id": "assistant",
"role": "assistant",
"timestamp": NOW + 51_000,
"content": (
"Noted. Helios needs APAC weekend coverage, and Imani owns the "
"rotation schedule."
),
},
],
),
(
"luna-onboarding",
[
{
"sender_id": "maya",
"sender_name": "Maya",
"role": "user",
"timestamp": NOW + 60_000,
"content": (
"Project Luna will replace the onboarding tour with a checklist. "
"The localized copy is due from the content team on October 25."
),
},
{
"sender_id": "assistant",
"role": "assistant",
"timestamp": NOW + 61_000,
"content": (
"I will remember Luna's checklist approach and the localization "
"deadline."
),
},
],
),
(
"aurora-observability",
[
{
"sender_id": "maya",
"sender_name": "Maya",
"role": "user",
"timestamp": NOW + 70_000,
"content": (
"Project Aurora will retain detailed telemetry for 30 days. "
"The operations team should alert after three consecutive "
"heartbeat misses."
),
},
{
"sender_id": "assistant",
"role": "assistant",
"timestamp": NOW + 71_000,
"content": (
"Understood. Aurora keeps 30 days of telemetry and alerts after "
"three missed heartbeats."
),
},
],
),
(
"comet-invoices",
[
{
"sender_id": "maya",
"sender_name": "Maya",
"role": "user",
"timestamp": NOW + 80_000,
"content": (
"Project Comet will add downloadable invoice PDFs for enterprise "
"accounts. Finance will approve the tax-field layout on October 28."
),
},
{
"sender_id": "assistant",
"role": "assistant",
"timestamp": NOW + 81_000,
"content": (
"Noted. Comet covers enterprise invoice PDFs and an October tax "
"layout review."
),
},
],
),
(
"solstice-research",
[
{
"sender_id": "maya",
"sender_name": "Maya",
"role": "user",
"timestamp": NOW + 90_000,
"content": (
"Project Solstice is prototyping voice notes for field researchers. "
"The research team will interview 12 participants in November."
),
},
{
"sender_id": "assistant",
"role": "assistant",
"timestamp": NOW + 91_000,
"content": (
"I will remember Solstice's voice-note prototype and the planned "
"participant interviews."
),
},
],
),
]
def post(path, payload):
request = Request(
f"{API_URL}/{path}",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urlopen(request, timeout=300) as response:
return json.load(response)["data"]
for session_id, messages in conversations:
added = post(
"add",
{
"session_id": session_id,
"app_id": "project-assistant",
"project_id": "launch-planning",
"messages": messages,
"defer_extraction": True,
},
)
flushed = post(
"flush",
{
"session_id": session_id,
"app_id": "project-assistant",
"project_id": "launch-planning",
},
)
print(f"{session_id}: {added['status']} -> {flushed['status']}")
프로젝트 디렉터리에서 실행하십시오:
uv run python add_memories.py
참고 출력:
atlas-release: accumulated -> extracted
atlas-rollback: accumulated -> extracted
orion-pricing: accumulated -> extracted
vega-mobile: accumulated -> extracted
nova-warehouse: accumulated -> extracted
helios-support: accumulated -> extracted
luna-onboarding: accumulated -> extracted
aurora-observability: accumulated -> extracted
comet-invoices: accumulated -> extracted
solstice-research: accumulated -> extracted
defer_extraction 를 true 로 설정하면, LLM에 경계 감지를 요청하지 않고 각 대화를 내구성 있는 버퍼에 저장합니다. 다음 /flush 호출은 해당 세션의 끝을 표시하고 한 번의 추출을 트리거합니다. 그러면 EverOS는 추출된 에피소드를 Markdown 형식으로 작성하고 Milvus 인덱스를 위해 비동기적으로 임베딩합니다.
Markdown 메모리 확인
생성된 에피소드 파일은 애플리케이션, 프로젝트 및 사용자 범주 아래에 저장됩니다:
find "$EVEROS_ROOT/project-assistant/launch-planning/users/maya/episodes" \
-type f -name "*.md"
참조 출력(파일 이름의 날짜는 예제를 실행한 시점을 나타냅니다):
everos-data/project-assistant/launch-planning/users/maya/episodes/episode-2026-09-08.md
파일을 열어 LLM이 추출한 기억 내용을 확인하세요. 축약된 발췌문은 다음과 같습니다:
## ep_20260908_00000001
**owner_id**: maya
**session_id**: atlas-release
**sender_ids**: [maya, assistant]
### Subject
Maya's Project Atlas Launch Decision: September 30 Canary and Promotion Criteria
### Content
Maya decided that Project Atlas would launch with a 10% canary on September 30.
The promotion to all users would occur only after the checkout error rate remained
below 1% for 30 minutes.
기억 내용은 LLM에 의해 추출되므로 정확한 문구, 식별자 및 타임스탬프는 달라질 수 있습니다. 원본 마크다운 파일이 여전히 신뢰할 수 있는 영구적인 출처이며, 이를 통해 Milvus 인덱스를 재구축할 수 있습니다.
기억 내용 검색하기
Atlas가 정식 가동되기 전에 무엇을 기억해야 하는지 확인하려면 하이브리드 검색을 사용하세요. 다음 코드를 search_memories.py 로 저장하세요:
import json
import time
from urllib.request import Request, urlopen
URL = "http://127.0.0.1:8000/api/v2/memory/search"
payload = {
"user_id": "maya",
"app_id": "project-assistant",
"project_id": "launch-planning",
"query": "What should I remember before Atlas goes live?",
"method": "hybrid",
"top_k": 4,
}
def search():
request = Request(
URL,
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urlopen(request, timeout=300) as response:
return json.load(response)["data"]["episodes"]
expected_sessions = {"atlas-release", "atlas-rollback"}
for _ in range(30):
episodes = search()
top_results = episodes[:2]
if {episode["session_id"] for episode in top_results} == expected_sessions:
break
time.sleep(2)
else:
raise RuntimeError("The expected Atlas memories were not indexed in time")
for rank, episode in enumerate(top_results, start=1):
print(f"{rank}. {episode['session_id']} | score={episode['score']:.3f}")
print(f" {episode['subject']}")
검색 실행:
uv run python search_memories.py
결과 참조 (점수와 문구는 다를 수 있음):
1. atlas-release | score=0.492
Project Atlas Launch Plan: 10% Canary Rollout on September 30 with Error Rate Gate
2. atlas-rollback | score=0.400
Atlas Rollback Plan Details: Priya as Owner, 2% Error Trigger, 24-Hour Image Retention
관련 없는 8개의 대화보다 두 개의 Atlas 대화가 먼저 반환됩니다. EverOS는 쿼리를 OpenAI 임베딩 엔드포인트로 전송하고, Milvus에 Maya의 애플리케이션 및 프로젝트 범위 내의 BM25 및 벡터 후보를 요청한 후, 두 결과 목록을 통합합니다.
Milvus 컬렉션 확인
EverOS는 지원되는 각 파생 메모리 유형에 대해 하나의 컬렉션을 생성합니다. MilvusClient 를 사용하여 각 컬렉션의 행 수를 확인해 보세요:
import os
from pymilvus import MilvusClient
prefix = "everos_bootcamp"
client = MilvusClient(uri=os.environ.get("MILVUS_URI", "http://localhost:19530"))
memory_kinds = [
"agent_case",
"agent_skill",
"atomic_fact",
"episode",
"foresight",
"knowledge_topic",
"user_profile",
]
for kind in memory_kinds:
name = f"{prefix}_{kind}"
if client.has_collection(collection_name=name):
result = client.query(
collection_name=name,
filter="",
output_fields=["count(*)"],
)
print(f"{kind}: {result[0]['count(*)']} rows")
client.close()
검증된 실행 결과 참조:
agent_case: 0 rows
agent_skill: 0 rows
atomic_fact: 50 rows
episode: 10 rows
foresight: 0 rows
knowledge_topic: 0 rows
user_profile: 1 rows
원자적 사실의 정확한 개수는 LLM 출력에 따라 달라질 수 있습니다. 10개의 에피소드 행은 플러시된 10개의 대화에 해당합니다. 나머지 컬렉션은 이 예제에서 다루지 않는 EverOS 메모리 모드 및 기능을 위해 제공됩니다.
다른 Milvus 배포 환경 사용
다른 Milvus 서버 엔드포인트나 Zilliz Cloud를 사용하려면 EVEROS_MILVUS__URI 파일을 수정하십시오. 엔드포인트에 인증이 필요한 경우 EVEROS_MILVUS__TOKEN 를 설정하십시오. 데이터 수집 및 검색 코드는 변경되지 않습니다.
결론
EverOS와 Milvus를 결합하면 대화를 영구적인 기억으로 전환하고, 키워드 및 의미적 신호를 통해 이를 검색할 수 있습니다. 동일한 패턴을 적용하여 어시스턴트 및 기타 에이전트형 애플리케이션에 사용자, 프로젝트, 워크플로우에 대한 장기 기억 기능을 제공할 수 있습니다.