EverOSとMilvusを活用したエージェントの長期記憶の構築
EverOSは、AIエージェント向けのMarkdownファースト型記憶システムです。会話から永続的な記憶を抽出し、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 Server に接続します。EverOS も、同じ URI およびトークン設定を通じてZilliz Cloudをサポートしています。その 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 のメモリモードを採用することで、この例ではユーザーのメモリに焦点を当てています。MilvusのコレクションとそのスキーマはEverOSが管理するため、ユーザー自身が作成する必要はありません。
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
}
}
レスポンスには追加のヘルス状態フィールドが含まれています。このチュートリアルで重要な値は、status: "ok" 、llm: true 、embed: true 、およびcascade.healthy: true です。
プロジェクトの会話の追加
以下の Python プログラムは、10 件の独立した会話を EverOS に送信します。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 呼び出しにより、そのセッションの終了がマークされ、1回の抽出がトリガーされます。その後、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によって抽出されるため、正確な表現、識別子、およびタイムスタンプは異なる場合があります。元のMarkdownファイルが信頼できる恒久的な情報源であり、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件の無関係な会話よりも先に、2件のAtlasの会話が返されています。EverOSはクエリをOpenAIのエンベディングエンドポイントに送信し、Mayaのアプリケーションおよびプロジェクトの範囲内でBM25およびベクトル候補をMilvusに要求し、2つの結果リストを統合します。
Milvusのコレクションを確認する
EverOSは、サポートされている各派生メモリ種別ごとに1つのコレクションを作成します。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 ServerエンドポイントまたはZilliz Cloudを使用するには、EVEROS_MILVUS__URI を更新します。エンドポイントで認証が必要な場合は、EVEROS_MILVUS__TOKEN を設定してください。データ取り込みおよび検索のコードは変更不要です。
まとめ
EverOSとMilvusを組み合わせることで、会話を永続的な記憶として保存し、キーワードや意味的シグナルを通じてそれらを呼び出すことができます。この同じパターンを応用することで、アシスタントやその他のエージェント型アプリケーションに、独自のユーザー、プロジェクト、ワークフロー向けの長期記憶機能を持たせることができます。