CrewAI 메모리 (Memory)¶
AI 에이전트를 만들다 보면 어느 순간 이런 고민이 찾아옵니다. "똑같은 사실을 매번 다시 설명해야 하나?" 과거 대화에서 PostgreSQL을 쓰기로 했는데, 새 태스크가 돌 때마다 그걸 잊어버리고 또 물어본다면 에이전트는 아무리 똑똑해도 정말 답답하겠죠. CrewAI의 메모리(Memory) 시스템이 바로 그 문제를 해결합니다.
통합된 메모리 시스템¶
CrewAI는 하나로 뭉친 통합 메모리 시스템을 제공합니다. 예전에는 단기 메모리(short-term), 장기 메모리(long-term), 엔티티 메모리(entity), 외부 메모리(external)처럼 여러 종류를 따로 관리해야 했는데, 이제는 단 하나의 Memory 클래스로 전부 대신합니다. 이 하나의 API가 지능적으로 동작하는 비결은 바로 LLM을 활용한다는 점이에요. 내용을 저장할 때 LLM이 그 내용을 분석해서 범위(scope), 카테고리(category), 중요도(importance)를 스스로 추론하고, 검색할 때는 의미 유사도(semantic similarity) + 최신성(recency) + 중요도(importance)를 섞은 복합 점수로 적응형 깊이 검색을 수행합니다.
메모리는 크게 네 가지 방식으로 쓸 수 있어요: 단독(standalone), 크루(Crew)와 함께, 에이전트(Agent)와 함께, 플로우(Flow) 안에서.
퀵 스타트¶
가장 기본적인 사용법부터 볼게요. from crewai import Memory로 불러와서 Memory() 인스턴스를 만들면 됩니다.
from crewai import Memory
memory = Memory()
# 저장하기 -- LLM이 범위, 카테고리, 중요도를 스스로 추론합니다
memory.remember("We decided to use PostgreSQL for the user database.")
# 검색하기 -- 결과는 복합 점수(의미 + 최신성 + 중요도) 순으로 정렬됩니다
matches = memory.recall("What database did we choose?")
for m in matches:
print(f"[{m.score:.2f}] {m.record.content}")
# 빠르게 변하는 프로젝트라면 점수를 조정해 보세요
memory = Memory(recency_weight=0.5, recency_half_life_days=7)
# 잊어버리기
memory.forget(scope="/project/old")
# 스스로 정리된 범위 트리 살펴보기
print(memory.tree())
print(memory.info("/"))
remember()로 저장하고 recall()로 검색하는 것이 핵심입니다. 저장 시점에 LLM이 범위·카테고리·중요도를 알아서 붙여 주기 때문에, 데이터베이스 스키마 같은 걸 미리 설계할 필요가 없어요.
메모리를 쓰는 네 가지 방법¶
단독(Standalone) 사용¶
크루나 에이전트가 전혀 필요 없습니다. 스크립트, 노트북, CLI 도구, 혹은 단독 지식 베이스로 그냥 쓸 수 있어요.
from crewai import Memory
memory = Memory()
# 지식을 쌓아갑니다
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.")
# 나중에 필요한 것만 꺼내 씁니다
matches = memory.recall("What are our API limits?", limit=5)
for m in matches:
print(f"[{m.score:.2f}] {m.record.content}")
# 긴 텍스트에서 원자적 사실(atomic fact) 단위로 추출합니다
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)
extract_memories()가 흥미로운데요. 긴 회의록 같은 텍스트를 통째로 저장하는 대신, 그 안에서 개별 사실 단위로 쪼개서 반환합니다. 그래서 한 덩어리 blob 대신 원자적인 사실(atomic fact)들이 따로따로 저장되죠.
크루(Crew)와 함께¶
크루 레벨에서 memory=True를 넘기면 기본 설정으로 메모리가 켜지고, 원하는 설정을 담은 Memory 인스턴스를 넘기면 커스텀하게 동작합니다.
from crewai import Crew, Agent, Task, Process, Memory
# 옵션 1: 기본 메모리
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process=Process.sequential,
memory=True,
verbose=True,
)
# 옵션 2: 튜닝된 점수를 가진 커스텀 메모리
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) 설정을 자동으로 그대로 넘겨줍니다. 크루 안의 모든 에이전트는 자기만의 메모리를 따로 갖지 않는 한 크루의 공유 메모리를 사용해요. 커스텀 임베더를 지정하지 않으면 기본값으로 OpenAI의 text-embedding-3-large 임베딩을 사용합니다.
동작 흐름도 짚고 갈게요. 각 태스크가 끝나면 크루가 태스크 결과에서 개별 사실을 자동으로 추출해 저장하고, 각 태스크가 시작되기 전에 에이전트가 메모리에서 관련 맥락을 회상해 태스크 프롬프트에 주입합니다.
에이전트(Agent)와 함께¶
에이전트는 기본적으로 크루의 공유 메모리를 쓰지만, 필요하면 범위가 제한된(private) 뷰를 받아서 독립적인 맥락을 가질 수도 있습니다.
from crewai import Agent, Memory
memory = Memory()
# 리서처는 개인 범위를 받아서 /agent/researcher 만 볼 수 있습니다
researcher = Agent(
role="Researcher",
goal="Find and analyze information",
backstory="Expert researcher with attention to detail",
memory=memory.scope("/agent/researcher"),
)
# 라이터는 크루 공유 메모리를 사용합니다 (에이전트 레벨 메모리 미설정)
writer = Agent(
role="Writer",
goal="Produce clear, well-structured content",
backstory="Experienced technical writer",
# memory 미설정 -- 크루 메모리가 켜져 있으면 crew._memory 를 사용합니다
)
이 패턴은 리서처가 자기 조사 결과를 독립적으로 간직하면서, 라이터는 크루 공유 메모리를 읽을 수 있게 해 줍니다. 정보의 공유 범위를 설계로 통제하는 거죠.
플로우(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):
# 과거 연구를 회상해서 맥락으로 제공합니다
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}"
플로우에서 메모리를 더 자세히 쓰는 법은 Flows 문서를 참고하면 됩니다.
계층적 범위(Hierarchical Scopes)¶
범위(Scope)란¶
메모리는 마치 파일시스템처럼 계층 구조의 범위 트리로 정리됩니다. 각 범위는 /, /project/alpha, /agent/researcher/findings 같은 경로 형태예요.
/
/company
/company/engineering
/company/product
/project
/project/alpha
/project/beta
/agent
/agent/researcher
/agent/writer
범위의 핵심 가치는 맥락 의존적 메모리(context-dependent memory)에 있어요. 특정 범위 안에서 회상하면 그 트리의 해당 브랜치만 검색하므로, 정확도와 성능이 함께 좋아집니다.
범위 추론(Scope Inference)이 동작하는 방식¶
remember()를 호출할 때 범위를 지정하지 않으면, LLM이 내용과 기존 범위 트리를 분석해 가장 적절한 위치를 제안합니다. 기존 범위에 맞는 게 없으면 새로 만듭니다. 그래서 시간이 지나면서 범위 트리는 내용 자체에서 유기적으로 자라납니다. 처음부터 스키마를 설계할 필요가 없다는 뜻이에요.
memory = Memory()
# LLM이 내용에서 범위를 추론합니다
memory.remember("We chose PostgreSQL for the user database.")
# -> /project/decisions 나 /engineering/database 아래에 놓일 수 있습니다
# 원하면 명시적으로 지정할 수도 있습니다
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=[])
tree()로 전체 구조를 보거나, info()로 특정 범위의 메타데이터(레코드 수, 카테고리, 최고·최신 레코드 시각, 하위 범위)를 확인할 수 있습니다.
MemoryScope: 서브트리 뷰¶
MemoryScope는 모든 동작을 트리의 한 브랜치로 제한합니다. 이걸 쓰는 에이전트나 코드는 해당 서브트리 안에서만 읽고 쓸 수 있어요.
memory = Memory()
# 특정 에이전트용 범위 생성
agent_memory = memory.scope("/agent/researcher")
# 모든 것이 /agent/researcher 에 상대적입니다
agent_memory.remember("Found three relevant papers on LLM memory.")
# -> /agent/researcher 아래에 저장됩니다
agent_memory.recall("relevant papers")
# -> /agent/researcher 아래에서만 검색합니다
# 서브스코프로 더 좁히기
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이 알아서 하게 두는 게 좋습니다.
사용 사례 예시¶
멀티 프로젝트 팀:
memory = Memory()
# 각 프로젝트마다 자기 브랜치를 가집니다
memory.remember("Using microservices architecture", scope="/project/alpha/architecture")
memory.remember("GraphQL API for client apps", scope="/project/beta/api")
# 전체 프로젝트에 걸쳐 회상
memory.recall("API design decisions")
# 또는 특정 프로젝트 안에서만
memory.recall("API design", scope="/project/beta")
에이전트별 개인 맥스트 + 공유 지식:
memory = Memory()
# 리서처는 개인 조사 결과를 가집니다
researcher_memory = memory.scope("/agent/researcher")
# 라이터는 자기 범위와 회사 공유 지식을 함께 읽습니다
writer_view = memory.slice(
scopes=["/agent/writer", "/company/knowledge"],
read_only=True,
)
고객 지원(고객별 맥락):
memory = Memory()
# 고객마다 격리된 맥락을 가집니다
memory.remember("Prefers email communication", scope="/customer/acme-corp")
memory.remember("On enterprise plan, 50 seats", scope="/customer/acme-corp")
# 공용 제품 문서는 모든 에이전트가 접근 가능합니다
memory.remember("Rate limit is 1000 req/min on enterprise plan", scope="/product/docs")
메모리 슬라이스(Memory Slices)¶
MemorySlice는 여러 개의, 어쩌면 서로 겹치지 않는(disjoint) 범위를 가로지르는 뷰(view)입니다. 범위(scope)가 하나의 서브트리로 제한하는 것과 달리, 슬라이스는 여러 브랜치에서 동시에 회상할 수 있게 해 줍니다.
슬라이스 vs 스코프, 언제 무엇을 쓸까¶
- 스코프(Scope): 에이전트나 코드 블록을 하나의 서브트리로 제한할 때 씁니다. 예를 들어
/agent/researcher만 보는 에이전트 같은 경우요. - 슬라이스(Slice): 여러 브랜치의 맥락을 합쳐야 할 때 씁니다. 자기 범위 + 회사 공유 지식을 함께 읽는 에이전트 같은 경우예요.
읽기 전용 슬라이스(Read-Only Slices)¶
가장 흔한 패턴입니다. 에이전트에게 여러 브랜치에 대한 읽기 접근을 주되, 공유 영역에 쓰기는 못 하게 하는 거죠.
memory = Memory()
# 에이전트는 자기 범위와 회사 지식을 회상할 수 있지만,
# 회사 지식에는 쓸 수 없습니다
agent_view = memory.slice(
scopes=["/agent/researcher", "/company/knowledge"],
read_only=True,
)
matches = agent_view.recall("company security policies", limit=5)
# /agent/researcher 와 /company/knowledge 둘 다 검색하고 결과를 합쳐 정렬합니다
agent_view.remember("new finding") # PermissionError 발생 (읽기 전용)
읽기-쓰기 슬라이스(Read-Write Slices)¶
읽기 전용을 끄면 포함된 범위 어디에나 쓸 수 있지만, 쓸 때는 반드시 어느 범위인지 명시해야 합니다.
view = memory.slice(scopes=["/team/alpha", "/team/beta"], read_only=False)
# 쓸 때는 범위를 반드시 지정해야 합니다
view.remember("Cross-team decision", scope="/team/alpha", categories=["decisions"])
복합 점수(Composite Scoring)¶
회상 결과는 세 가지 신호를 가중치로 합친 값으로 순위가 매겨집니다:
각 항목을 풀어보면:
- similarity = 벡터 인덱스로부터
1 / (1 + distance)(0~1) - decay =
0.5^(age_days / half_life_days)— 지수 감쇠 (오늘은 1.0, 반감기 시점에는 0.5) - importance = 레코드의 중요도 점수 (0~1), 인코딩 시점에 설정됨
이 값들은 Memory 생성자에서 직접 설정할 수 있습니다.
# 스프린트 회고: 최근 메모리를 우선, 반감기는 짧게
memory = Memory(
recency_weight=0.5,
semantic_weight=0.3,
importance_weight=0.2,
recency_half_life_days=7,
)
# 아키텍처 지식 베이스: 중요한 메모리를 우선, 반감기는 길게
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 분석 레이어¶
메모리는 LLM을 세 가지 방식으로 사용합니다:
- 저장 시 — scope·category·importance를 생략했을 때, LLM이 내용을 분석해 scope·category·importance와 메타데이터(엔티티, 날짜, 토픽)를 제안합니다.
- 회상 시 — deep/auto 회상에서 LLM이 쿼리를 분석(키워드, 시간 힌트, 제안 범위, 복잡도)해서 검색을 안내합니다.
- 메모리 추출 —
extract_memories(content)가 원시 텍스트(예: 태스크 출력)를 개별 메모리 문장으로 쪼갭니다. 에이전트는 각 문장에 대해remember()를 먼저 호출해서, 하나의 큰 덩어리 대신 원자적 사실이 저장되게 합니다.
모든 분석은 LLM이 실패해도 안전하게 점진적으로 저하됩니다. 자세한 건 Failure Behavior를 보세요.
메모리 통합(Memory Consolidation)¶
새 내용을 저장할 때 인코딩 파이프라인이 스토리지에서 유사한 기존 레코드를 자동으로 점검합니다. 유사도가 consolidation_threshold(기본 0.85)보다 높으면 LLM이 어떻게 할지 결정합니다:
- keep — 기존 레코드가 여전히 정확하고 중복이 아님
- update — 기존 레코드를 새 정보로 갱신 (LLM이 병합된 내용 제공)
- delete — 기존 레코드가 오래됐거나 대체/모순됨
- insert_new — 새 내용도 별도 레코드로 삽입할지 여부
이렇게 해서 중복이 쌓이는 걸 막아줍니다. 예를 들어 "CrewAI ensures reliable operation"이라는 내용을 세 번 저장해도, 통합이 중복을 알아채고 레코드 하나만 남깁니다.
배치 내부 중복 제거(Intra-batch Dedup)¶
remember_many()를 쓸 때 배치 안의 항목들은 스토리지에 도달하기 전에 서로 비교됩니다. 두 항목의 코사인 유사도가 batch_dedup_threshold(기본 0.98) 이상이면 나중 항목은 조용히 버려집니다. 이건 LLM 호출 없이 순수 벡터 연산으로, 단일 배치 안의 정확 혹은 거의 정확한 중복을 잡아냅니다.
# 3개 중 2개만 저장됩니다 (세 번째는 첫 번째와 거의 중복)
memory.remember_many([
"CrewAI supports complex workflows.",
"Python is a great language.",
"CrewAI supports complex workflows.", # 배치 내부 중복 제거로 버려짐
])
비차단 저장(Non-blocking Saves)¶
remember_many()는 비차단(non-blocking) 방식입니다. 인코딩 파이프라인을 백그라운드 스레드에 넘기고 즉시 반환하죠. 메모리가 저장되는 동안 에이전트는 다음 태스크로 계속 진행할 수 있습니다.
# 즉시 반환 -- 저장은 백그라운드에서 진행됩니다
memory.remember_many(["Fact A.", "Fact B.", "Fact C."])
# recall() 은 검색 전에 보류 중인 저장이 끝나기를 자동으로 기다립니다
matches = memory.recall("facts") # 3개 레코드 모두 보임
읽기 배리어(Read Barrier): 모든 recall() 호출은 검색 전에 drain_writes()를 자동으로 호출해서, 쿼리가 항상 최신 영속 레코드를 볼 수 있게 합니다. 투명하게 동작하므로 신경 쓸 필요가 없어요.
크루 종료(Crew Shutdown): 크루가 끝나면 kickoff()가 finally 블록에서 보류 중인 메모리 저장을 모두 비웁니다. 그래서 백그라운드 저장이 진행 중인 상태에서 크루가 끝나도 저장이 유실되지 않습니다.
단독 사용(Standalone Usage): 크루 라이프사이클이 없는 스크립트나 노트북이라면 drain_writes()나 close()를 직접 호출해야 합니다.
문제 해결 팁¶
메모리가 저장/회상되지 않나요?
- 크루를 쓸 때
memory=True또는memory=Memory(...)가 설정되어 있는지 확인하세요.
회상이 느린가요?
- 일상적인 에이전트 맥락에는
depth="shallow"를 쓰고, 복잡한 쿼리에만depth="deep"을 아끼세요. query_analysis_threshold를 올려서 더 많은 쿼리가 LLM 분석을 건너뛰게 하세요.
로그에 LLM 분석 오류가 보이나요?
- 메모리는 여전히 안전한 기본값으로 저장/회상합니다. 완전한 LLM 분석을 원한다면 API 키, rate limit, 모델 가용성을 점검하세요.
로그에 백그라운드 저장 오류가 보이나요?
- 메모리 저장은 백그라운드 스레드에서 돌아갑니다. 오류는
MemorySaveFailedEvent로 발생하지만 에이전트를 크래시시키지는 않아요. 로그에서 근본 원인(보통 LLM 또는 임베더 연결 문제)을 확인하세요.
임베딩 차원 불일치?
- 기존 로컬 메모리 스토어가 다른 임베딩 모델로 만들어졌을 수 있습니다. 기본 OpenAI 메모리 임베더는 이제
text-embedding-3-large(3072 차원)인데, 예전 스토어는 보통 1536 차원 임베딩을 썼습니다. 로컬 테스트에서는crewai reset-memories -m을 실행하거나, 로컬 메모리 스토리지 디렉터리를 삭제하거나, 이전 임베더 모델을 명시적으로 설정하면 됩니다.
동시 쓰기 충돌?
- LanceDB 작업은 공유 락(shared lock)으로 직렬화되고 충돌 시 자동 재시도됩니다. 같은 데이터베이스를 가리키는 여러
Memory인스턴스(예: 에이전트 메모리 + 크루 메모리)를 처리하므로 별도 조치가 필요 없어요.
터미널에서 메모리 브라우징:
메모리 리셋(예: 테스트용):
crew.reset_memories(command_type="memory") # 통합 메모리 리셋
# 또는 Memory 인스턴스에서:
memory.reset() # 모든 범위
memory.reset(scope="/project/old") # 해당 서브트리만
설정 참조(Configuration Reference)¶
모든 설정은 Memory(...)의 키워드 인자로 전달됩니다. 모든 파라미터는 합리적인 기본값을 가집니다.
| 파라미터 | 기본값 | 설명 |
|---|---|---|
llm |
"gpt-4o-mini" |
분석용 LLM (모델 이름 또는 BaseLLM 인스턴스) |
storage |
"lancedb" |
스토리지 백엔드 ("lancedb", 경로 문자열, 또는 StorageBackend 인스턴스) |
embedder |
None (OpenAI text-embedding-3-large) |
임베더 (설정 dict, callable, 또는 기본 OpenAI용 None) |
recency_weight |
0.3 |
복합 점수에서 최신성 가중치 |
semantic_weight |
0.5 |
복합 점수에서 의미 유사도 가중치 |
importance_weight |
0.2 |
복합 점수에서 중요도 가중치 |
recency_half_life_days |
30 |
최신성 점수가 절반으로 줄어드는 데 걸리는 일수 (지수 감쇠) |
consolidation_threshold |
0.85 |
저장 시 통합이 트리거되는 유사도. 1.0으로 설정하면 비활성화 |
consolidation_limit |
5 |
통합 중 비교할 최대 기존 레코드 수 |
default_importance |
0.5 |
LLM 분석이 생략되고 값이 없을 때 할당되는 중요도 |
batch_dedup_threshold |
0.98 |
remember_many() 배치 안에서 거의 중복을 버리기 위한 코사인 유사도 |
confidence_threshold_high |
0.8 |
이 회상 신뢰도 이상이면 결과를 직접 반환 |
confidence_threshold_low |
0.5 |
이 회상 신뢰도 미만이면 더 깊은 탐색 트리거 |
complex_query_threshold |
0.7 |
복잡한 쿼리에서 이 신뢰도 미만이면 더 깊게 탐색 |
exploration_budget |
1 |
딥 회상 중 LLM 주도 탐색 라운드 수 |
query_analysis_threshold |
200 |
이 길이(문자) 미만의 쿼리는 딥 회상에서 LLM 분석 생략 |
데이터스케쳐스 실무 관점¶
우리 팀에서도 크루를 몇 개 만들어 서로 다른 역할을 맡겨 돌려 보는 작업이 많아요. 그런데 에이전트가 태스크가 끝날 때마다 이전 맥락을 잊어버리면, 매번 같은 맥락을 프롬프트에 다시 넣어야 하는 반복이 생깁니다. CrewAI 메모리를 쓰면 이걸 팀 단위로 깔끔하게 해결할 수 있어요.
실무에서 가장 먼저 챙길 것은 임베딩 차원 문제입니다. 기본 임베더가 text-embedding-3-large(3072 차원)로 바뀌었다는 점은 우리처럼 기존 스토어를 쓰던 팀이라면 꼭 기억해야 할 함정이에요. 로컬에서 오래된 테스트를 돌리다가 차원 불일치 오류가 나면, crewai reset-memories -m으로 리셋하거나 이전 임베더를 명시적으로 지정하면 됩니다.
그다음은 범위(scope) 설계인데, 처음부터 정교하게 짜려고 하지 말고 /{entity_type}/{identifier} 정도의 얕은(2~3단계) 구조로 시작해서 LLM의 범위 추론에 맡기는 걸 권해요. 우리처럼 프로젝트·고객이 여러 개 얽힌 팀이라면, /project/<이름> 같은 브랜치로 분리하고, 공유 지식은 /company/knowledge에 두고 읽기 전용 슬라이스로 접근하게 하는 패턴이 특히 유용합니다. 공유 영역을 실수로 지워버리는 사고를 원천 차단할 수 있거든요.
마지막으로 운영 관점에서, remember_many()가 비차단이라 백그라운드에서 저장된다는 점을 알고 있어야 해요. 크루 kickoff()가 종료 시 보류 저장을 비워주긴 하지만, 스크립트나 노트북처럼 크루 라이프사이클이 없는 곳에서는 drain_writes()나 close()를 직접 불러야 저장이 유실되지 않습니다. 에이전트가 "기억했다"고 생각했는데 막상 저장이 안 된 상황을 피하려면 이 타이밍을 꼭 잡아두세요.