CrewAI Memory

CrewAI Memory (통합 메모리 시스템)

크루가 이전에 했던 일을 기억하고, 다음 태스크에서 그 맥락을 다시 꺼내 쓰게 하려면 메모리가 필요해요. CrewAI는 단기·장기·엔티티·외부 메모리 같은 각각의 타입을 따로 두는 대신, 하나의 Memory 클래스로 전부 통합한 시스템을 제공합니다. 저장할 때 LLM이 내용을 분석해 스코프·카테고리·중요도를 정하고, 회상할 때는 의미 유사도·최신성·중요도를 섞은 복합 점수로 결과를 정렬해요. 이 페이지에서 메모리의 네 가지 사용 방식과 스코프, 슬라이스, 임베더 설정까지 정리합니다.

출처: 공식문서

본문

빠른 시작

from crewai import Memory

memory = Memory()

# Store -- the LLM infers scope, categories, and importance
memory.remember("We decided to use PostgreSQL for the user database.")

# Retrieve -- results ranked by composite score (semantic + recency + importance)
matches = memory.recall("What database did we choose?")
for m in matches:
    print(f"[{m.score:.2f}] {m.record.content}")

# Tune scoring for a fast-moving project
memory = Memory(recency_weight=0.5, recency_half_life_days=7)

# Forget
memory.forget(scope="/project/old")

# Explore the self-organized scope tree
print(memory.tree())
print(memory.info("/"))

remember()로 저장하면 LLM이 스코프·카테고리·중요도를 추론하고, recall()은 의미+최신성+중요도를 결합한 복합 점수로 결과를 정렬해요.

메모리 사용 네 가지 방식

메모리는 독립(standalone), Crew와 함께, Agent와 함께, Flow 안에서 네 가지 방법으로 쓸 수 있습니다.

독립 사용 — 스크립트·노트북·CLI 도구 또는 독립 지식베이스로, 에이전트나 크루 없이도 가능합니다.

from crewai import Memory

memory = Memory()

# Build up knowledge
memory.remember("The API rate limit is 1000 requests per minute.")
memory.remember("Our staging environment uses port 8080.")
memory.remember("The team agreed to use feature flags for all new releases.")

# Later, recall what you need
matches = memory.recall("What are our API limits?", limit=5)
for m in matches:
    print(f"[{m.score:.2f}] {m.record.content}")

# Extract atomic facts from a longer text
raw = """Meeting notes: We decided to migrate from MySQL to PostgreSQL
next quarter. The budget is $50k. Sarah will lead the migration."""

facts = memory.extract_memories(raw)
# ["Migration from MySQL to PostgreSQL planned for next quarter",
#  "Database migration budget is $50k",
#  "Sarah will lead the database migration"]

for fact in facts:
    memory.remember(fact)

Crew와 함께 — 기본 설정은 memory=True를 넘기고, 커스텀 동작이 필요하면 설정된 Memory 인스턴스를 넘깁니다.

from crewai import Crew, Agent, Task, Process, Memory

# Option 1: Default memory
crew = Crew(
    agents=[researcher, writer],
    tasks=[research_task, writing_task],
    process=Process.sequential,
    memory=True,
    verbose=True,
)

# Option 2: Custom memory with tuned scoring
memory = Memory(
    recency_weight=0.4,
    semantic_weight=0.4,
    importance_weight=0.2,
    recency_half_life_days=14,
)
crew = Crew(
    agents=[researcher, writer],
    tasks=[research_task, writing_task],
    memory=memory,
)

memory=True일 때 크루는 기본 Memory()를 만들고 크루의 embedder 설정을 자동 전달해요. 크루의 모든 에이전트는 자기 메모리가 없는 한 크루의 메모리를 공유합니다. 커스텀 embedder가 없으면 기본으로 OpenAI text-embedding-3-large 임베딩을 사용합니다. 각 태스크 후 크루는 태스크 출력에서 개별 사실을 자동 추출해 저장하고, 각 태스크 전에는 에이전트가 메모리에서 관련 맥락을 회상해 태스크 프롬프트에 주입해요.

Agent와 함께 — 에이전트는 크루의 공유 메모리를 쓰거나(기본), 스코프로 제한된 뷰를 받아 개인 맥락을 분리할 수 있습니다.

from crewai import Agent, Memory

memory = Memory()

# Researcher gets a private scope -- only sees /agent/researcher
researcher = Agent(
    role="Researcher",
    goal="Find and analyze information",
    backstory="Expert researcher with attention to detail",
    memory=memory.scope("/agent/researcher"),
)

# Writer uses crew shared memory (no agent-level memory set)
writer = Agent(
    role="Writer",
    goal="Produce clear, well-structured content",
    backstory="Experienced technical writer",
    # memory not set -- uses crew._memory when crew has memory enabled
)

이 패턴으로 연구자는 자신의 조사 결과를 개인적으로 가지되, 작가는 크루 공유 메모리를 읽게 됩니다.

Flow 안에서 — 모든 Flow에는 내장 메모리가 있습니다. 어떤 Flow 메서드 안에서든 self.remember(), self.recall(), self.extract_memories()를 쓸 수 있어요.

from crewai.flow.flow import Flow, listen, start

class ResearchFlow(Flow):
    @start()
    def gather_data(self):
        findings = "PostgreSQL handles 10k concurrent connections. MySQL caps at 5k."
        self.remember(findings, scope="/research/databases")
        return findings

    @listen(gather_data)
    def write_report(self, findings):
        # Recall past research to provide context
        past = self.recall("database performance benchmarks")
        context = "\n".join(f"- {m.record.content}" for m in past)
        return f"Report:\nNew findings: {findings}\nPrevious context:\n{context}"

계층적 스코프

메모리는 파일시스템처럼 스코프의 계층 트리로 조직됩니다. 각 스코프는 /, /project/alpha, /agent/researcher/findings 같은 경로예요.

/
  /company
    /company/engineering
    /company/product
  /project
    /project/alpha
    /project/beta
  /agent
    /agent/researcher
    /agent/writer

스코프는 맥락 의존 메모리를 제공합니다. 스코프 안에서 회상하면 그 트리 브랜치만 검색하므로 정밀도와 성능이 좋아집니다.

스코프 추론 방식

스코프를 지정하지 않고 remember()를 호출하면, LLM이 내용과 기존 스코프 트리를 분석해 가장 좋은 위치를 제안합니다. 맞는 스코프가 없으면 새로 만들죠. 시간이 지나면 스코프 트리는 콘텐츠 자체에서 유기적으로 자라납니다.

memory = Memory()

# LLM infers scope from content
memory.remember("We chose PostgreSQL for the user database.")
# -> might be placed under /project/decisions or /engineering/database

# You can also specify scope explicitly
memory.remember("Sprint velocity is 42 points", scope="/team/metrics")

스코프 트리 시각화

print(memory.tree())
# / (15 records)
#   /project (8 records)
#     /project/alpha (5 records)
#     /project/beta (3 records)
#   /agent (7 records)
#     /agent/researcher (4 records)
#     /agent/writer (3 records)

print(memory.info("/project/alpha"))
# ScopeInfo(path='/project/alpha', record_count=5,
#           categories=['architecture', 'database'],
#           oldest_record=datetime(...), newest_record=datetime(...),
#           child_scopes=[])

MemoryScope — 하위 트리 뷰

MemoryScope는 모든 연산을 트리의 한 브랜치로 제한합니다. 그 스코프를 쓰는 에이전트나 코드는 해당 하위 트리 안에서만 보고 쓸 수 있어요.

memory = Memory()

# Create a scope for a specific agent
agent_memory = memory.scope("/agent/researcher")

# Everything is relative to /agent/researcher
agent_memory.remember("Found three relevant papers on LLM memory.")
# -> stored under /agent/researcher

agent_memory.recall("relevant papers")
# -> searches only under /agent/researcher

# Narrow further with subscope
project_memory = agent_memory.subscope("project-alpha")
# -> /agent/researcher/project-alpha

스코프 설계 모범 사례

  • 처음엔 평평하게 시작하고 LLM이 정리하게 둔다. 스코프 계층을 처음부터 과하게 설계하지 마세요. memory.remember(content)로 시작해 LLM의 스코프 추론이 콘텐츠가 쌓이며 구조를 만들게 하면 됩니다.
  • /{entity_type}/{identifier} 패턴을 쓴다. /project/alpha, /agent/researcher, /company/engineering, /customer/acme-corp 같은 패턴에서 자연스러운 계층이 만들어져요.
  • 데이터 타입이 아니라 관심사(concern)로 스코프한다. /decisions/project/alpha 대신 /project/alpha/decisions를 쓰세요. 관련 콘텐츠가 함께 모입니다.
  • 깊이를 얕게(2~3단계) 유지한다. 너무 깊게 중첩하면 스코프가 너무 듬성해져요. /project/alpha/architecture는 좋지만 /project/alpha/architecture/decisions/databases/postgresql은 너무 깊습니다.
  • 아는 경우엔 명시적 스코프를, 모르는 경우엔 LLM에 맡긴다. 알고 있는 프로젝트 결정을 저장한다면 scope="/project/alpha/decisions"를 넘기고, 자유 형식 에이전트 출력을 저장한다면 스코프를 생략해 LLM이 알아서 하게 하세요.

메모리 슬라이스

MemorySlice는 여러 개의 서로 떨어진 스코프를 가로지르는 뷰입니다. 한 브랜치로 제한하는 스코프와 달리, 슬라이스는 여러 브랜치에서 동시에 회상할 수 있어요.

  • Scope: 에이전트나 코드가 한 하위 트리로 제한되어야 할 때. 예: /agent/researcher만 보는 에이전트.
  • Slice: 여러 브랜치의 맥락을 결합해야 할 때. 예: 자신의 스코프와 공유 회사 지식 양쪽을 읽는 에이전트.

읽기 전용 슬라이스 — 가장 흔한 패턴은 에이전트에게 여러 브랜치에 대한 읽기 접근을 주되, 공유 영역에는 쓰지 못하게 하는 것입니다.

memory = Memory()

# Agent can recall from its own scope AND company knowledge,
# but cannot write to company knowledge
agent_view = memory.slice(
    scopes=["/agent/researcher", "/company/knowledge"],
    read_only=True,
)

matches = agent_view.recall("company security policies", limit=5)
# Searches both /agent/researcher and /company/knowledge, merges and ranks results

agent_view.remember("new finding")  # Raises PermissionError (read-only)

읽기·쓰기 슬라이스 — read-only가 꺼져 있으면 포함된 스코프 중 어느 곳에나 쓸 수 있지만, 스코프를 명시적으로 지정해야 합니다.

view = memory.slice(scopes=["/team/alpha", "/team/beta"], read_only=False)

# Must specify scope when writing
view.remember("Cross-team decision", scope="/team/alpha", categories=["decisions"])

복합 점수 산정

회상 결과는 세 신호의 가중 결합으로 정렬됩니다.

composite = semantic_weight * similarity + recency_weight * decay + importance_weight * importance
  • similarity = 1 / (1 + distance) (벡터 인덱스에서, 0~1)
  • decay = 0.5^(age_days / half_life_days) — 지수 감쇠 (오늘은 1.0, 반감기 시점에 0.5)
  • importance = 레코드의 중요도 점수 (0~1, 인코딩 시점에 설정)

Memory 생성자에서 직접 설정할 수 있습니다.

# Sprint retrospective: favor recent memories, short half-life
memory = Memory(
    recency_weight=0.5,
    semantic_weight=0.3,
    importance_weight=0.2,
    recency_half_life_days=7,
)

# Architecture knowledge base: favor important memories, long half-life
memory = Memory(
    recency_weight=0.1,
    semantic_weight=0.5,
    importance_weight=0.4,
    recency_half_life_days=180,
)

MemoryMatch에는 match_reasons 리스트가 포함되어 어떤 이유로 그 순위가 됐는지 보여 줍니다 (예: ["semantic", "recency", "importance"]).

LLM 분석 레이어

Memory는 LLM을 세 가지 방식으로 사용합니다.

  1. 저장 시 — 스코프·카테고리·중요도를 생략하면 LLM이 내용을 분석해 스코프·카테고리·중요도·메타데이터(엔티티, 날짜, 토픽)를 제안합니다.
  2. 회상 시 — deep/auto 회상에서 LLM이 쿼리(키워드, 시간 힌트, 제안된 스코프, 복잡도)를 분석해 검색을 안내합니다.
  3. 메모리 추출extract_memories(content)가 원시 텍스트(예: 태스크 출력)를 개별 메모리 문장으로 쪼갭니다. 에이전트는 각 문장에 대해 remember()를 호출하기 전에 이것을 사용해 큰 덩어리 대신 원자적 사실을 저장합니다.

LLM 실패 시 모든 분석은 우아하게 단계적으로 저하됩니다.

메모리 통합

새 콘텐츠를 저장할 때 인코딩 파이프라인은 저장소의 유사한 기존 레코드를 자동 검사합니다. 유사도가 consolidation_threshold(기본 0.85)를 넘으면 LLM이 무엇을 할지 결정합니다.

  • keep — 기존 레코드가 여전히 정확하고 중복이 아님
  • update — 기존 레코드를 새 정보로 갱신(LLM이 병합 콘텐츠 제공)
  • delete — 기존 레코드가 낡았거나 대체·모순됨
  • insert_new — 새 콘텐츠도 별도 레코드로 삽입할지

이렇게 해서 중복이 쌓이지 않습니다. 예를 들어 "CrewAI ensures reliable operation"을 세 번 저장해도 통합이 중복을 인식하고 레코드 하나만 유지해요.

배치 내 중복 제거

remember_many()를 쓸 때 같은 배치 안의 항목들이 저장소에 닿기 전에 서로 비교됩니다. 두 항목의 코사인 유사도가 batch_dedup_threshold(기본 0.98) 이상이면 나중 항목이 조용히 버려집니다. LLM 호출 없이 순수 벡터 연산으로 배치 안의 정확·근사 중복을 걸러내는 거예요.

# Only 2 records are stored (the third is a near-duplicate of the first)
memory.remember_many([
    "CrewAI supports complex workflows.",
    "Python is a great language.",
    "CrewAI supports complex workflows.",  # dropped by intra-batch dedup
])

논블로킹 저장

remember_many()논블로킹입니다. 인코딩 파이프라인을 백그라운드 스레드에 제출하고 즉시 반환해서, 메모리가 저장되는 동안 에이전트가 다음 태스크로 이어갈 수 있어요.

# Returns immediately -- save happens in background
memory.remember_many(["Fact A.", "Fact B.", "Fact C."])

# recall() automatically waits for pending saves before searching
matches = memory.recall("facts")  # sees all 3 records

모든 recall() 호출은 검색 전에 자동으로 drain_writes()를 호출해, 쿼리가 항상 최신 영속 레코드를 보게 합니다. 크루가 끝나면 kickoff()finally 블록에서 대기 중인 메모리 저장을 모두 비우므로 백그라운드 저장이 진행 중이어도 손실되지 않아요. 크루 수명주기가 없는 스크립트·노트북에서는 memory.drain_writes()memory.close()를 명시적으로 호출하면 됩니다.

출처와 프라이버시

모든 메모리 레코드는 출처 추적을 위한 source 태그와 접근 제어를 위한 private 플래그를 가질 수 있습니다.

# Tag memories with their origin
memory.remember("User prefers dark mode", source="user:alice")
memory.remember("System config updated", source="admin")
memory.remember("Agent found a bug", source="agent:debugger")

# Recall only memories from a specific source
matches = memory.recall("user preferences", source="user:alice")

private=True 메모리는 source가 일치할 때만 회상에 보입니다. include_private=True를 넘기면 관리자처럼 source와 무관하게 모든 private 레코드를 볼 수 있습니다. 다중 사용자나 엔터프라이즈 배포에서 사용자별 메모리를 격리할 때 특히 유용합니다.

RecallFlow (Deep Recall)

recall()은 두 가지 깊이를 지원합니다.

  • depth="shallow" — 복합 점수 산정을 곁들인 직접 벡터 검색. 빠름(~200ms), LLM 호출 없음.
  • depth="deep" (기본) — 다단계 RecallFlow 수행: 쿼리 분석, 스코프 선택, 병렬 벡터 검색, 신뢰도 기반 라우팅, 낮은 신뢰도 시 선택적 재귀 탐색.

영리한 LLM 스킵: query_analysis_threshold(기본 200자)보다 짧은 쿼리는 deep 모드에서도 LLM 쿼리 분석을 완전히 건너뜁니다. "What database do we use?" 같은 짧은 쿼리는 이미 좋은 검색 문구라 LLM 분석이 크게 도움이 안 돼요. 이러면 일반적인 짧은 쿼리에서 회상당 ~1-3초를 아낄 수 있습니다. 긴 쿼리(예: 전체 태스크 설명)만 LLM 증류를 거쳐 목표 지향적인 하위 쿼리로 나눠집니다.

# Shallow: pure vector search, no LLM
matches = memory.recall("What did we decide?", limit=10, depth="shallow")

# Deep (default): intelligent retrieval with LLM analysis for long queries
matches = memory.recall(
    "Summarize all architecture decisions from this quarter",
    limit=10,
    depth="deep",
)

RecallFlow 라우터를 제어하는 신뢰도 임계값은 설정 가능합니다.

memory = Memory(
    confidence_threshold_high=0.9,   # Only synthesize when very confident
    confidence_threshold_low=0.4,    # Explore deeper more aggressively
    exploration_budget=2,            # Allow up to 2 exploration rounds
    query_analysis_threshold=200,    # Skip LLM for queries shorter than this
)

임베더 설정

Memory는 의미 검색을 위해 텍스트를 벡터로 변환하는 임베딩 모델이 필요합니다. 기본적으로 Memory()는 OpenAI text-embedding-3-large(3072차원 벡터)를 사용하며 OPENAI_API_KEY가 필요합니다.

text-embedding-3-small이나 text-embedding-ada-002처럼 1536차원 임베딩으로 만든 기존 로컬 메모리 저장소는 text-embedding-3-large 기본값과 호환되지 않을 수 있습니다. OpenAI·Azure OpenAI 프로바이더 모두 해당하며, Azure의 기본 임베딩 모델도 text-embedding-ada-002에서 text-embedding-3-large로 바뀌었어요. 로컬 테스트가 임베딩 차원 불일치로 실패하면 crewai reset-memories -m으로 메모리를 리셋하거나, 로컬 메모리 저장 디렉터리를 삭제하거나, 마이그레이션 전까지 더 오래된 임베더 모델을 명시적으로 설정하세요.

임베더는 세 가지 방식으로 설정할 수 있습니다.

from crewai import Memory

# As a config dict
memory = Memory(embedder={"provider": "openai", "config": {"model_name": "text-embedding-3-large"}})

# As a pre-built callable
from crewai.rag.embeddings.factory import build_embedder
embedder = build_embedder({"provider": "ollama", "config": {"model_name": "mxbai-embed-large"}})
memory = Memory(embedder=embedder)

memory=True를 쓸 때는 크루의 embedder 설정이 전달됩니다.

from crewai import Crew

crew = Crew(
    agents=[...],
    tasks=[...],
    memory=True,
    embedder={"provider": "openai", "config": {"model_name": "text-embedding-3-large"}},
)

지원 프로바이더 요약:

Provider Key Typical Model Notes
OpenAI openai text-embedding-3-large Default. Set OPENAI_API_KEY.
Ollama ollama mxbai-embed-large Local, no API key needed.
Azure OpenAI azure text-embedding-3-large Default model. Requires deployment_id.
Google AI google-generativeai gemini-embedding-001 Set GOOGLE_API_KEY.
Google Vertex google-vertex gemini-embedding-001 Requires project_id.
Cohere cohere embed-english-v3.0 Strong multilingual support.
VoyageAI voyageai voyage-3 Optimized for retrieval.
AWS Bedrock amazon-bedrock amazon.titan-embed-text-v1 Uses boto3 credentials.
Hugging Face huggingface all-MiniLM-L6-v2 Local sentence-transformers.
Jina jina jina-embeddings-v2-base-en Set JINA_API_KEY.
IBM WatsonX watsonx ibm/slate-30m-english-rtrvr Requires project_id.
Sentence Transformer sentence-transformer all-MiniLM-L6-v2 Local, no API key.
Custom custom -- Requires embedding_callable.

LLM 설정

Memory는 저장 분석(스코프·카테고리·중요도 추론), 통합 결정, deep 회상 쿼리 분석에 LLM을 사용합니다.

from crewai import Memory, LLM

# Default: gpt-4o-mini
memory = Memory()

# Use a different OpenAI model
memory = Memory(llm="gpt-4o")

# Use Anthropic
memory = Memory(llm="anthropic/claude-3-haiku-20240307")

# Use Ollama for fully local/private analysis
memory = Memory(llm="ollama/llama3.2")

# Use Google Gemini
memory = Memory(llm="gemini/gemini-3.7-flash")

# Pass a pre-configured LLM instance with custom settings
llm = LLM(model="gpt-4o", temperature=0)
memory = Memory(llm=llm)

LLM은 지연 초기화됩니다 — 처음 필요할 때만 생성돼요. 그래서 API 키가 설정돼 있지 않아도 Memory()는 생성 시점에 절대 실패하지 않습니다. 오류는 실제로 LLM을 호출할 때(예: 명시적 스코프·카테고리 없이 저장하거나 deep 회상 중)에만 표면화됩니다. 완전 오프라인/프라이빗 운영을 원하면 LLM과 임베더 둘 다 로컬 모델로 쓰면 됩니다.

memory = Memory(
    llm="ollama/llama3.2",
    embedder={"provider": "ollama", "config": {"model_name": "mxbai-embed-large"}},
)

저장 백엔드

  • 기본: LanceDB. ./.crewai/memory에 저장되고, 환경변수 $CREWAI_STORAGE_DIR/memory가 설정돼 있으면 그 경로(또는 storage="path/to/dir"로 넘긴 경로)를 사용합니다.
  • 커스텀 백엔드: StorageBackend 프로토콜(crewai.memory.storage.backend)을 구현한 인스턴스를 Memory(storage=your_backend)로 넘기세요.

탐색

memory.tree()                        # Formatted tree of scopes and record counts
memory.tree("/project", max_depth=2) # Subtree view
memory.info("/project")              # ScopeInfo: record_count, categories, oldest/newest
memory.list_scopes("/")              # Immediate child scopes
memory.list_categories()             # Category names and counts
memory.list_records(scope="/project/alpha", limit=20)  # Records in a scope, newest first

실패 동작

분석 중 LLM이 실패하면(네트워크 오류, 속도 제한, 잘못된 응답) 메모리는 우아하게 저하됩니다.

  • 저장 분석 — 경고를 로그로 남기고 기본 스코프 /, 빈 카테고리, 중요도 0.5로 그대로 저장합니다.
  • 메모리 추출 — 전체 콘텐츠를 단일 메모리로 저장해 아무것도 버리지 않습니다.
  • 쿼리 분석 — 회상이 단순 스코프 선택과 벡터 검색으로 폴백해 여전히 결과를 얻습니다.

이러한 분석 실패에 대해 예외는 발생하지 않고, 저장·임베더 실패만 예외를 던집니다.

프라이버시 참고

메모리 콘텐츠는 분석을 위해 설정된 LLM으로 전송됩니다(저장 시 스코프/카테고리/중요도, 쿼리 분석, 선택적 deep 회상). 민감한 데이터에는 로컬 LLM(예: Ollama)을 사용하거나 프로바이더가 규정 요구사항을 충족하는지 확인하세요.

메모리 이벤트

모든 메모리 연산은 source_type="unified_memory" 이벤트를 방출합니다. 타이밍·오류·콘텐츠를 들을 수 있어요.

Event Description Key Properties
MemoryQueryStartedEvent Query begins query, limit
MemoryQueryCompletedEvent Query succeeds query, results, query_time_ms
MemoryQueryFailedEvent Query fails query, error
MemorySaveStartedEvent Save begins value, metadata

더 알아보기