New Memory Improvements (#4484)

* better DevEx

* Refactor: Update supported native providers and enhance memory handling

- Removed "groq" and "meta" from the list of supported native providers in `llm.py`.
- Added a safeguard in `flow.py` to ensure all background memory saves complete before returning.
- Improved error handling in `unified_memory.py` to prevent exceptions during shutdown, ensuring smoother memory operations and event bus interactions.

* Enhance Memory System with Consolidation and Learning Features

- Introduced memory consolidation mechanisms to prevent duplicate records during content saving, utilizing similarity checks and LLM decision-making.
- Implemented non-blocking save operations in the memory system, allowing agents to continue tasks while memory is being saved.
- Added support for learning from human feedback, enabling the system to distill lessons from past corrections and improve future outputs.
- Updated documentation to reflect new features and usage examples for memory consolidation and HITL learning.

* Enhance cyclic flow handling for or_() listeners

- Updated the Flow class to ensure that all fired or_() listeners are cleared between cycle iterations, allowing them to fire again in subsequent cycles. This change addresses a bug where listeners remained suppressed across iterations.
- Added regression tests to verify that or_() listeners fire correctly on every iteration in cyclic flows, ensuring expected behavior in complex routing scenarios.
This commit is contained in:
João Moura
2026-02-14 23:57:56 -08:00
committed by GitHub
parent 18d266c8e7
commit 09e9229efc
10 changed files with 883 additions and 74 deletions

View File

@@ -380,22 +380,124 @@ Memory uses the LLM in three ways:
All analysis degrades gracefully on LLM failure -- see [Failure Behavior](#failure-behavior).
## RecallFlow (Deep Recall)
## Memory Consolidation
`recall()` supports three depths:
When saving new content, the encoding pipeline automatically checks for similar existing records in storage. If the similarity is above `consolidation_threshold` (default 0.85), the LLM decides what to do:
- **`depth="shallow"`** -- Direct vector search with composite scoring. Fast; used by default when agents load context.
- **`depth="deep"` or `depth="auto"`** -- Runs a multi-step RecallFlow: query analysis, scope selection, vector search, confidence-based routing, and optional recursive exploration when confidence is low.
- **keep** -- The existing record is still accurate and not redundant.
- **update** -- The existing record should be updated with new information (LLM provides the merged content).
- **delete** -- The existing record is outdated, superseded, or contradicted.
- **insert_new** -- Whether the new content should also be inserted as a separate record.
This prevents duplicates from accumulating. For example, if you save "CrewAI ensures reliable operation" three times, consolidation recognizes the duplicates and keeps only one record.
### Intra-batch Dedup
When using `remember_many()`, items within the same batch are compared against each other before hitting storage. If two items have cosine similarity >= `batch_dedup_threshold` (default 0.98), the later one is silently dropped. This catches exact or near-exact duplicates within a single batch without any LLM calls (pure vector math).
```python
# Fast path (default for agent task context)
# 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
])
```
## Non-blocking Saves
`remember_many()` is **non-blocking** -- it submits the encoding pipeline to a background thread and returns immediately. This means the agent can continue to the next task while memories are being saved.
```python
# 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
```
### Read Barrier
Every `recall()` call automatically calls `drain_writes()` before searching, ensuring the query always sees the latest persisted records. This is transparent -- you never need to think about it.
### Crew Shutdown
When a crew finishes, `kickoff()` drains all pending memory saves in its `finally` block, so no saves are lost even if the crew completes while background saves are in flight.
### Standalone Usage
For scripts or notebooks where there's no crew lifecycle, call `drain_writes()` or `close()` explicitly:
```python
memory = Memory()
memory.remember_many(["Fact A.", "Fact B."])
# Option 1: Wait for pending saves
memory.drain_writes()
# Option 2: Drain and shut down the background pool
memory.close()
```
## Source and Privacy
Every memory record can carry a `source` tag for provenance tracking and a `private` flag for access control.
### Source Tracking
The `source` parameter identifies where a memory came from:
```python
# 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 Memories
Private memories are only visible to recall when the `source` matches:
```python
# Store a private memory
memory.remember("Alice's API key is sk-...", source="user:alice", private=True)
# This recall sees the private memory (source matches)
matches = memory.recall("API key", source="user:alice")
# This recall does NOT see it (different source)
matches = memory.recall("API key", source="user:bob")
# Admin access: see all private records regardless of source
matches = memory.recall("API key", include_private=True)
```
This is particularly useful in multi-user or enterprise deployments where different users' memories should be isolated.
## RecallFlow (Deep Recall)
`recall()` supports two depths:
- **`depth="shallow"`** -- Direct vector search with composite scoring. Fast (~200ms), no LLM calls.
- **`depth="deep"` (default)** -- Runs a multi-step RecallFlow: query analysis, scope selection, parallel vector search, confidence-based routing, and optional recursive exploration when confidence is low.
**Smart LLM skip**: Queries shorter than `query_analysis_threshold` (default 200 characters) skip the LLM query analysis entirely, even in deep mode. Short queries like "What database do we use?" are already good search phrases -- the LLM analysis adds little value. This saves ~1-3s per recall for typical short queries. Only longer queries (e.g. full task descriptions) go through LLM distillation into targeted sub-queries.
```python
# Shallow: pure vector search, no LLM
matches = memory.recall("What did we decide?", limit=10, depth="shallow")
# Intelligent path for complex questions
# Deep (default): intelligent retrieval with LLM analysis for long queries
matches = memory.recall(
"Summarize all architecture decisions from this quarter",
limit=10,
depth="auto",
depth="deep",
)
```
@@ -406,6 +508,7 @@ 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
)
```
@@ -613,6 +716,45 @@ memory = Memory(embedder=my_embedder)
| Custom | `custom` | -- | Requires `embedding_callable`. |
## LLM Configuration
Memory uses an LLM for save analysis (scope, categories, importance inference), consolidation decisions, and deep recall query analysis. You can configure which model to use.
```python
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-2.0-flash")
# Pass a pre-configured LLM instance with custom settings
llm = LLM(model="gpt-4o", temperature=0)
memory = Memory(llm=llm)
```
The LLM is initialized **lazily** -- it's only created when first needed. This means `Memory()` never fails at construction time, even if API keys aren't set. Errors only surface when the LLM is actually called (e.g. when saving without explicit scope/categories, or during deep recall).
For fully offline/private operation, use a local model for both the LLM and embedder:
```python
memory = Memory(
llm="ollama/llama3.2",
embedder={"provider": "ollama", "config": {"model_name": "mxbai-embed-large"}},
)
```
## Storage Backend
- **Default**: LanceDB, stored under `./.crewai/memory` (or `$CREWAI_STORAGE_DIR/memory` if the env var is set, or the path you pass as `storage="path/to/dir"`).
@@ -685,11 +827,18 @@ class MemoryMonitor(BaseEventListener):
- When using a crew, confirm `memory=True` or `memory=Memory(...)` is set.
**Slow recall?**
- Use `depth="shallow"` for routine agent context. Reserve `depth="auto"` or `"deep"` for complex queries.
- Use `depth="shallow"` for routine agent context. Reserve `depth="deep"` for complex queries.
- Increase `query_analysis_threshold` to skip LLM analysis for more queries.
**LLM analysis errors in logs?**
- Memory still saves/recalls with safe defaults. Check API keys, rate limits, and model availability if you want full LLM analysis.
**Background save errors in logs?**
- Memory saves run in a background thread. Errors are emitted as `MemorySaveFailedEvent` but don't crash the agent. Check logs for the root cause (usually LLM or embedder connection issues).
**Concurrent write conflicts?**
- LanceDB operations are serialized with a shared lock and retried automatically on conflict. This handles multiple `Memory` instances pointing at the same database (e.g. agent memory + crew memory). No action needed.
**Browse memory from the terminal:**
```bash
crewai memory # Opens the TUI browser
@@ -721,7 +870,9 @@ All configuration is passed as keyword arguments to `Memory(...)`. Every paramet
| `consolidation_threshold` | `0.85` | Similarity above which consolidation is triggered on save. Set to `1.0` to disable. |
| `consolidation_limit` | `5` | Max existing records to compare during consolidation. |
| `default_importance` | `0.5` | Importance assigned when not provided and LLM analysis is skipped. |
| `batch_dedup_threshold` | `0.98` | Cosine similarity for dropping near-duplicates within a `remember_many()` batch. |
| `confidence_threshold_high` | `0.8` | Recall confidence above which results are returned directly. |
| `confidence_threshold_low` | `0.5` | Recall confidence below which deeper exploration is triggered. |
| `complex_query_threshold` | `0.7` | For complex queries, explore deeper below this confidence. |
| `exploration_budget` | `1` | Number of LLM-driven exploration rounds during deep recall. |
| `query_analysis_threshold` | `200` | Queries shorter than this (in characters) skip LLM analysis during deep recall. |

View File

@@ -73,6 +73,8 @@ When this flow runs, it will:
| `default_outcome` | `str` | No | Outcome to use if no feedback provided. Must be in `emit` |
| `metadata` | `dict` | No | Additional data for enterprise integrations |
| `provider` | `HumanFeedbackProvider` | No | Custom provider for async/non-blocking feedback. See [Async Human Feedback](#async-human-feedback-non-blocking) |
| `learn` | `bool` | No | Enable HITL learning: distill lessons from feedback and pre-review future output. Default `False`. See [Learning from Feedback](#learning-from-feedback) |
| `learn_limit` | `int` | No | Max past lessons to recall for pre-review. Default `5` |
### Basic Usage (No Routing)
@@ -576,6 +578,64 @@ If you're using an async web framework (FastAPI, aiohttp, Slack Bolt async mode)
5. **Automatic persistence**: State is automatically saved when `HumanFeedbackPending` is raised and uses `SQLiteFlowPersistence` by default
6. **Custom persistence**: Pass a custom persistence instance to `from_pending()` if needed
## Learning from Feedback
The `learn=True` parameter enables a feedback loop between human reviewers and the memory system. When enabled, the system progressively improves its outputs by learning from past human corrections.
### How It Works
1. **After feedback**: The LLM extracts generalizable lessons from the output + feedback and stores them in memory with `source="hitl"`. If the feedback is just approval (e.g. "looks good"), nothing is stored.
2. **Before next review**: Past HITL lessons are recalled from memory and applied by the LLM to improve the output before the human sees it.
Over time, the human sees progressively better pre-reviewed output because each correction informs future reviews.
### Example
```python Code
class ArticleReviewFlow(Flow):
@start()
@human_feedback(
message="Review this article draft:",
emit=["approved", "needs_revision"],
llm="gpt-4o-mini",
learn=True, # enable HITL learning
)
def generate_article(self):
return self.crew.kickoff(inputs={"topic": "AI Safety"}).raw
@listen("approved")
def publish(self):
print(f"Publishing: {self.last_human_feedback.output}")
@listen("needs_revision")
def revise(self):
print("Revising based on feedback...")
```
**First run**: The human sees the raw output and says "Always include citations for factual claims." The lesson is distilled and stored in memory.
**Second run**: The system recalls the citation lesson, pre-reviews the output to add citations, then shows the improved version. The human's job shifts from "fix everything" to "catch what the system missed."
### Configuration
| Parameter | Default | Description |
|-----------|---------|-------------|
| `learn` | `False` | Enable HITL learning |
| `learn_limit` | `5` | Max past lessons to recall for pre-review |
### Key Design Decisions
- **Same LLM for everything**: The `llm` parameter on the decorator is shared by outcome collapsing, lesson distillation, and pre-review. No need to configure multiple models.
- **Structured output**: Both distillation and pre-review use function calling with Pydantic models when the LLM supports it, falling back to text parsing otherwise.
- **Non-blocking storage**: Lessons are stored via `remember_many()` which runs in a background thread -- the flow continues immediately.
- **Graceful degradation**: If the LLM fails during distillation, nothing is stored. If it fails during pre-review, the raw output is shown. Neither failure blocks the flow.
- **No scope/categories needed**: When storing lessons, only `source` is passed. The encoding pipeline infers scope, categories, and importance automatically.
<Note>
`learn=True` requires the Flow to have memory available. Flows get memory automatically by default, but if you've disabled it with `_skip_auto_memory`, HITL learning will be silently skipped.
</Note>
## Related Documentation
- [Flows Overview](/en/concepts/flows) - Learn about CrewAI Flows
@@ -583,3 +643,4 @@ If you're using an async web framework (FastAPI, aiohttp, Slack Bolt async mode)
- [Flow Persistence](/en/concepts/flows#persistence) - Persisting flow state
- [Routing with @router](/en/concepts/flows#router) - More about conditional routing
- [Human Input on Execution](/en/learn/human-input-on-execution) - Task-level human input
- [Memory](/en/concepts/memory) - The unified memory system used by HITL learning

View File

@@ -7,9 +7,9 @@ mode: "wide"
## 개요
CrewAI는 **통합 메모리 시스템**을 제공합니다 -- 단기, 장기, 엔터티, 외부 메모리 유형을 하나의 지능형 API인 단일 `Memory` 클래스로 대체합니다. 메모리는 저장 시 LLM을 사용하여 콘텐츠를 분석하고(범위, 카테고리, 중요도 추론) 의미 유사도, 최신성, 중요도를 합한 복합 점수로 적응형 깊이 recall을 지원합니다.
CrewAI는 **통합 메모리 시스템**을 제공합니다 -- 단기, 장기, 엔터티, 외부 메모리 유형을 하나의 지능형 API인 단일 `Memory` 클래스로 대체합니다. 메모리는 저장 시 LLM을 사용하여 콘텐츠를 분석하고(범위, 카테고리, 중요도 추론) 의미 유사도, 최신성, 중요도를 합한 복합 점수로 적응형 깊이 recall을 지원합니다.
메모리를 네 가지 방법으로 사용할 수 있습니다: **독립 실행** (스크립트, 노트북), **Crew와 함께**, **에이전트와 함께**, 또는 **Flow 내부에서**.
메모리를 네 가지 방법으로 사용할 수 있습니다: **독립 실행**(스크립트, 노트북), **Crew와 함께**, **에이전트와 함께**, 또는 **Flow 내부에서**.
## 빠른 시작
@@ -21,7 +21,7 @@ memory = Memory()
# 저장 -- LLM이 scope, categories, importance를 추론
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}")
@@ -135,7 +135,7 @@ writer = Agent(
### Flow와 함께 사용
모든 Flow에는 내장 메모리가 있습니다. 모든 flow 메서드 내부에서 `self.remember()`, `self.recall()`, `self.extract_memories()`를 사용합니다.
모든 Flow에는 내장 메모리가 있습니다. 모든 flow 메서드 내부에서 `self.remember()`, `self.recall()`, `self.extract_memories()`를 사용하세요.
```python
from crewai.flow.flow import Flow, listen, start
@@ -149,7 +149,7 @@ class ResearchFlow(Flow):
@listen(gather_data)
def write_report(self, findings):
# 컨텍스트를 위해 과거 연구 recall
# 컨텍스트를 제공하기 위해 과거 연구 recall
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}"
@@ -377,25 +377,127 @@ memory = Memory(
2. **recall 시** -- deep/auto recall의 경우 LLM이 쿼리(키워드, 시간 힌트, 제안 scope, 복잡도)를 분석하여 검색을 안내합니다.
3. **메모리 추출** -- `extract_memories(content)`는 원시 텍스트(예: 작업 출력)를 개별 메모리 문장으로 나눕니다. 에이전트는 각 문장에 `remember()`를 호출하기 전에 이를 사용하여 하나의 큰 블록 대신 원자적 사실이 저장되도록 합니다.
모든 분석은 LLM 장애 시 우아하게 저하됩니다 -- [오류 시 동작](#오류-시-동작)을 참조하세요.
모든 분석은 LLM 실패 시 우아하게 저하됩니다 -- [오류 시 동작](#오류-시-동작)을 참조하세요.
## 메모리 통합
새 콘텐츠를 저장할 때 인코딩 파이프라인은 자동으로 스토리지에서 유사한 기존 레코드를 확인합니다. 유사도가 `consolidation_threshold`(기본값 0.85) 이상이면 LLM이 처리 방법을 결정합니다:
- **keep** -- 기존 레코드가 여전히 정확하고 중복이 아닙니다.
- **update** -- 기존 레코드를 새 정보로 업데이트해야 합니다 (LLM이 병합된 콘텐츠를 제공).
- **delete** -- 기존 레코드가 오래되었거나, 대체되었거나, 모순됩니다.
- **insert_new** -- 새 콘텐츠를 별도의 레코드로 삽입해야 하는지 여부.
이를 통해 중복이 축적되는 것을 방지합니다. 예를 들어, "CrewAI ensures reliable operation"을 세 번 저장하면 통합이 중복을 인식하고 하나의 레코드만 유지합니다.
### 배치 내 중복 제거
`remember_many()`를 사용할 때 동일 배치 내의 항목은 스토리지에 도달하기 전에 서로 비교됩니다. 두 항목의 코사인 유사도가 `batch_dedup_threshold`(기본값 0.98) 이상이면 나중 항목이 자동으로 삭제됩니다. 이는 LLM 호출 없이 순수 벡터 연산으로 단일 배치 내의 정확하거나 거의 정확한 중복을 잡아냅니다.
```python
# 2개의 레코드만 저장됨 (세 번째는 첫 번째의 거의 중복)
memory.remember_many([
"CrewAI supports complex workflows.",
"Python is a great language.",
"CrewAI supports complex workflows.", # 배치 내 중복 제거로 삭제
])
```
## 비차단 저장
`remember_many()`는 **비차단**입니다 -- 인코딩 파이프라인을 백그라운드 스레드에 제출하고 즉시 반환합니다. 이는 메모리가 저장되는 동안 에이전트가 다음 작업을 계속할 수 있음을 의미합니다.
```python
# 즉시 반환 -- 저장은 백그라운드에서 발생
memory.remember_many(["Fact A.", "Fact B.", "Fact C."])
# recall()은 검색 전에 보류 중인 저장을 자동으로 대기
matches = memory.recall("facts") # 3개 레코드 모두 확인 가능
```
### 읽기 배리어
모든 `recall()` 호출은 검색 전에 자동으로 `drain_writes()`를 호출하여 쿼리가 항상 최신 저장된 레코드를 볼 수 있도록 합니다. 이는 투명하게 작동하므로 별도로 신경 쓸 필요가 없습니다.
### Crew 종료
crew가 완료되면 `kickoff()`는 `finally` 블록에서 보류 중인 모든 메모리 저장을 드레인하므로, 백그라운드 저장이 진행 중인 상태에서 crew가 완료되더라도 저장이 손실되지 않습니다.
### 독립 실행 사용
crew 수명 주기가 없는 스크립트나 노트북에서는 `drain_writes()` 또는 `close()`를 명시적으로 호출하세요:
```python
memory = Memory()
memory.remember_many(["Fact A.", "Fact B."])
# 옵션 1: 보류 중인 저장 대기
memory.drain_writes()
# 옵션 2: 드레인 후 백그라운드 풀 종료
memory.close()
```
## 출처 및 개인정보
모든 메모리 레코드는 출처 추적을 위한 `source` 태그와 접근 제어를 위한 `private` 플래그를 가질 수 있습니다.
### 출처 추적
`source` 매개변수는 메모리의 출처를 식별합니다:
```python
# 메모리에 출처 태그 지정
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
matches = memory.recall("user preferences", source="user:alice")
```
### 비공개 메모리
비공개 메모리는 `source`가 일치할 때만 recall에서 볼 수 있습니다:
```python
# 비공개 메모리 저장
memory.remember("Alice's API key is sk-...", source="user:alice", private=True)
# 이 recall은 비공개 메모리를 볼 수 있음 (source 일치)
matches = memory.recall("API key", source="user:alice")
# 이 recall은 볼 수 없음 (다른 source)
matches = memory.recall("API key", source="user:bob")
# 관리자 액세스: source에 관계없이 모든 비공개 레코드 보기
matches = memory.recall("API key", include_private=True)
```
이는 서로 다른 사용자의 메모리가 격리되어야 하는 다중 사용자 또는 엔터프라이즈 배포에서 특히 유용합니다.
## RecallFlow (딥 Recall)
`recall()`은 가지 깊이를 지원합니다:
`recall()`은 가지 깊이를 지원합니다:
- **`depth="shallow"`** -- 복합 점수를 사용한 직접 벡터 검색. 빠름; 에이전트가 컨텍스트를 로드할 때 기본 사용.
- **`depth="deep"` 또는 `depth="auto"`** -- 다단계 RecallFlow 실행: 쿼리 분석, scope 선택, 벡터 검색, 신뢰도 기반 라우팅, 신뢰도가 낮을 때 선택적 재귀 탐색.
- **`depth="shallow"`** -- 복합 점수를 사용한 직접 벡터 검색. 빠름 (~200ms), LLM 호출 없음.
- **`depth="deep"` (기본값)** -- 다단계 RecallFlow 실행: 쿼리 분석, scope 선택, 병렬 벡터 검색, 신뢰도 기반 라우팅, 신뢰도가 낮을 때 선택적 재귀 탐색.
**스마트 LLM 건너뛰기**: `query_analysis_threshold`(기본값 200자)보다 짧은 쿼리는 deep 모드에서도 LLM 쿼리 분석을 완전히 건너뜁니다. "What database do we use?"와 같은 짧은 쿼리는 이미 좋은 검색 구문이므로 LLM 분석이 큰 가치를 더하지 않습니다. 이를 통해 일반적인 짧은 쿼리에서 recall당 ~1-3초를 절약합니다. 긴 쿼리(예: 전체 작업 설명)만 대상 하위 쿼리로의 LLM 분석을 거칩니다.
```python
# 빠른 경로 (에이전트 작업 컨텍스트 기본값)
# Shallow: 순수 벡터 검색, LLM 없음
matches = memory.recall("What did we decide?", limit=10, depth="shallow")
# 복잡한 질문용 지능형 경로
# Deep (기본값): 긴 쿼리에 대한 LLM 분석을 포함한 지능형 검색
matches = memory.recall(
"Summarize all architecture decisions from this quarter",
limit=10,
depth="auto",
depth="deep",
)
```
@@ -406,6 +508,7 @@ memory = Memory(
confidence_threshold_high=0.9, # 매우 확신할 때만 합성
confidence_threshold_low=0.4, # 더 적극적으로 깊이 탐색
exploration_budget=2, # 최대 2라운드 탐색 허용
query_analysis_threshold=200, # 이보다 짧은 쿼리는 LLM 건너뛰기
)
```
@@ -613,10 +716,49 @@ memory = Memory(embedder=my_embedder)
| Custom | `custom` | -- | `embedding_callable` 필요. |
## LLM 설정
메모리는 저장 분석(scope, categories, importance 추론), 통합 결정, 딥 recall 쿼리 분석에 LLM을 사용합니다. 사용할 모델을 설정할 수 있습니다.
```python
from crewai import Memory, LLM
# 기본값: gpt-4o-mini
memory = Memory()
# 다른 OpenAI 모델 사용
memory = Memory(llm="gpt-4o")
# Anthropic 사용
memory = Memory(llm="anthropic/claude-3-haiku-20240307")
# 완전한 로컬/비공개 분석을 위해 Ollama 사용
memory = Memory(llm="ollama/llama3.2")
# Google Gemini 사용
memory = Memory(llm="gemini/gemini-2.0-flash")
# 사용자 정의 설정이 있는 사전 구성된 LLM 인스턴스 전달
llm = LLM(model="gpt-4o", temperature=0)
memory = Memory(llm=llm)
```
LLM은 **지연 초기화**됩니다 -- 처음 필요할 때만 생성됩니다. 즉, API 키가 설정되지 않아도 `Memory()` 생성 시에는 실패하지 않습니다. 오류는 LLM이 실제로 호출될 때만 발생합니다(예: 명시적 scope/categories 없이 저장할 때 또는 딥 recall 중).
완전한 오프라인/비공개 운영을 위해 LLM과 embedder 모두에 로컬 모델을 사용하세요:
```python
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)`에 인스턴스 전달.
- **기본값**: LanceDB, `./.crewai/memory` 아래에 저장 (또는 환경 변수가 설정된 경우 `$CREWAI_STORAGE_DIR/memory`, 또는 `storage="path/to/dir"`로 전달한 경로).
- **사용자 정의 백엔드**: `StorageBackend` 프로토콜 구현하고(`crewai.memory.storage.backend` 참조) `Memory(storage=your_backend)`에 인스턴스 전달합니다.
## 탐색(Discovery)
@@ -685,11 +827,18 @@ class MemoryMonitor(BaseEventListener):
- crew 사용 시 `memory=True` 또는 `memory=Memory(...)`가 설정되었는지 확인하세요.
**recall이 느린가요?**
- 일상적인 에이전트 컨텍스트에는 `depth="shallow"`를 사용하세요. 복잡한 쿼리에만 `depth="auto"` 또는 `"deep"`을 사용하세요.
- 일상적인 에이전트 컨텍스트에는 `depth="shallow"`를 사용하세요. 복잡한 쿼리에만 `depth="deep"`을 사용하세요.
- 더 많은 쿼리에서 LLM 분석을 건너뛰려면 `query_analysis_threshold`를 높이세요.
**로그에 LLM 분석 오류가 있나요?**
- 메모리는 안전한 기본값으로 계속 저장/recall합니다. 전체 LLM 분석을 원하면 API 키, 속도 제한, 모델 가용성을 확인하세요.
**로그에 백그라운드 저장 오류가 있나요?**
- 메모리 저장은 백그라운드 스레드에서 실행됩니다. 오류는 `MemorySaveFailedEvent`로 발생하지만 에이전트를 중단시키지 않습니다. 근본 원인(보통 LLM 또는 embedder 연결 문제)은 로그를 확인하세요.
**동시 쓰기 충돌이 있나요?**
- LanceDB 연산은 공유 잠금으로 직렬화되며 충돌 시 자동으로 재시도됩니다. 이는 동일 데이터베이스를 가리키는 여러 `Memory` 인스턴스(예: 에이전트 메모리 + crew 메모리)를 처리합니다. 별도의 조치가 필요하지 않습니다.
**터미널에서 메모리 탐색:**
```bash
crewai memory # TUI 브라우저 열기
@@ -721,7 +870,9 @@ memory.reset(scope="/project/old") # 해당 하위 트리만
| `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` | recall 신뢰도가 이 값 이상이면 결과를 직접 반환. |
| `confidence_threshold_low` | `0.5` | recall 신뢰도가 이 값 미만이면 더 깊은 탐색 트리거. |
| `complex_query_threshold` | `0.7` | 복잡한 쿼리의 경우 이 신뢰도 미만에서 더 깊이 탐색. |
| `exploration_budget` | `1` | 딥 recall 중 LLM 기반 탐색 라운드 수. |
| `query_analysis_threshold` | `200` | 이 길이(문자 수)보다 짧은 쿼리는 딥 recall 중 LLM 분석을 건너뜀. |

View File

@@ -73,6 +73,8 @@ flow.kickoff()
| `default_outcome` | `str` | 아니오 | 피드백이 제공되지 않을 때 사용할 outcome. `emit`에 있어야 합니다 |
| `metadata` | `dict` | 아니오 | 엔터프라이즈 통합을 위한 추가 데이터 |
| `provider` | `HumanFeedbackProvider` | 아니오 | 비동기/논블로킹 피드백을 위한 커스텀 프로바이더. [비동기 인간 피드백](#비동기-인간-피드백-논블로킹) 참조 |
| `learn` | `bool` | 아니오 | HITL 학습 활성화: 피드백에서 교훈을 추출하고 향후 출력을 사전 검토합니다. 기본값 `False`. [피드백에서 학습하기](#피드백에서-학습하기) 참조 |
| `learn_limit` | `int` | 아니오 | 사전 검토를 위해 불러올 최대 과거 교훈 수. 기본값 `5` |
### 기본 사용법 (라우팅 없음)
@@ -576,6 +578,64 @@ async def on_slack_feedback_async(flow_id: str, slack_message: str):
5. **자동 영속성**: `HumanFeedbackPending`이 발생하면 상태가 자동으로 저장되며 기본적으로 `SQLiteFlowPersistence` 사용
6. **커스텀 영속성**: 필요한 경우 `from_pending()`에 커스텀 영속성 인스턴스 전달
## 피드백에서 학습하기
`learn=True` 매개변수는 인간 검토자와 메모리 시스템 간의 피드백 루프를 활성화합니다. 활성화되면 시스템은 과거 인간의 수정 사항에서 학습하여 출력을 점진적으로 개선합니다.
### 작동 방식
1. **피드백 후**: LLM이 출력 + 피드백에서 일반화 가능한 교훈을 추출하고 `source="hitl"`로 메모리에 저장합니다. 피드백이 단순한 승인(예: "좋아 보입니다")인 경우 아무것도 저장하지 않습니다.
2. **다음 검토 전**: 과거 HITL 교훈을 메모리에서 불러와 LLM이 인간이 보기 전에 출력을 개선하는 데 적용합니다.
시간이 지남에 따라 각 수정 사항이 향후 검토에 반영되므로 인간은 점진적으로 더 나은 사전 검토된 출력을 보게 됩니다.
### 예제
```python Code
class ArticleReviewFlow(Flow):
@start()
@human_feedback(
message="Review this article draft:",
emit=["approved", "needs_revision"],
llm="gpt-4o-mini",
learn=True, # HITL 학습 활성화
)
def generate_article(self):
return self.crew.kickoff(inputs={"topic": "AI Safety"}).raw
@listen("approved")
def publish(self):
print(f"Publishing: {self.last_human_feedback.output}")
@listen("needs_revision")
def revise(self):
print("Revising based on feedback...")
```
**첫 번째 실행**: 인간이 원시 출력을 보고 "사실에 대한 주장에는 항상 인용을 포함하세요."라고 말합니다. 교훈이 추출되어 메모리에 저장됩니다.
**두 번째 실행**: 시스템이 인용 교훈을 불러와 출력을 사전 검토하여 인용을 추가한 후 개선된 버전을 표시합니다. 인간의 역할이 "모든 것을 수정"에서 "시스템이 놓친 것을 찾기"로 전환됩니다.
### 구성
| 매개변수 | 기본값 | 설명 |
|-----------|--------|------|
| `learn` | `False` | HITL 학습 활성화 |
| `learn_limit` | `5` | 사전 검토를 위해 불러올 최대 과거 교훈 수 |
### 주요 설계 결정
- **모든 것에 동일한 LLM 사용**: 데코레이터의 `llm` 매개변수는 outcome 매핑, 교훈 추출, 사전 검토에 공유됩니다. 여러 모델을 구성할 필요가 없습니다.
- **구조화된 출력**: 추출과 사전 검토 모두 LLM이 지원하는 경우 Pydantic 모델과 함께 function calling을 사용하고, 그렇지 않으면 텍스트 파싱으로 폴백합니다.
- **논블로킹 저장**: 교훈은 백그라운드 스레드에서 실행되는 `remember_many()`를 통해 저장됩니다 -- Flow는 즉시 계속됩니다.
- **우아한 저하**: 추출 중 LLM이 실패하면 아무것도 저장하지 않습니다. 사전 검토 중 실패하면 원시 출력이 표시됩니다. 어느 쪽의 실패도 Flow를 차단하지 않습니다.
- **범위/카테고리 불필요**: 교훈을 저장할 때 `source`만 전달됩니다. 인코딩 파이프라인이 범위, 카테고리, 중요도를 자동으로 추론합니다.
<Note>
`learn=True`는 Flow에 메모리가 사용 가능해야 합니다. Flow는 기본적으로 자동으로 메모리를 얻지만, `_skip_auto_memory`로 비활성화한 경우 HITL 학습은 조용히 건너뜁니다.
</Note>
## 관련 문서
- [Flow 개요](/ko/concepts/flows) - CrewAI Flow에 대해 알아보기
@@ -583,3 +643,4 @@ async def on_slack_feedback_async(flow_id: str, slack_message: str):
- [Flow 영속성](/ko/concepts/flows#persistence) - Flow 상태 영속화
- [@router를 사용한 라우팅](/ko/concepts/flows#router) - 조건부 라우팅에 대해 더 알아보기
- [실행 시 인간 입력](/ko/learn/human-input-on-execution) - 태스크 수준 인간 입력
- [메모리](/ko/concepts/memory) - HITL 학습에서 사용되는 통합 메모리 시스템

View File

@@ -380,22 +380,124 @@ A memória usa o LLM de três formas:
Toda análise degrada graciosamente em caso de falha do LLM -- veja [Comportamento em Caso de Falha](#comportamento-em-caso-de-falha).
## RecallFlow (Recall Profundo)
## Consolidação de Memória
`recall()` suporta três profundidades:
Ao salvar novo conteúdo, o pipeline de codificação verifica automaticamente registros similares existentes no armazenamento. Se a similaridade estiver acima de `consolidation_threshold` (padrão 0.85), o LLM decide o que fazer:
- **`depth="shallow"`** -- Busca vetorial direta com pontuação composta. Rápido; usado por padrão quando agentes carregam contexto.
- **`depth="deep"` ou `depth="auto"`** -- Executa um RecallFlow em múltiplas etapas: análise da consulta, seleção de escopo, busca vetorial, roteamento baseado em confiança e exploração recursiva opcional quando a confiança é baixa.
- **keep** -- O registro existente ainda é preciso e não é redundante.
- **update** -- O registro existente deve ser atualizado com novas informões (o LLM fornece o conteúdo mesclado).
- **delete** -- O registro existente está desatualizado, substituído ou contradito.
- **insert_new** -- Se o novo conteúdo também deve ser inserido como um registro separado.
Isso evita o acúmulo de duplicatas. Por exemplo, se você salvar "CrewAI garante operação confiável" três vezes, a consolidação reconhece as duplicatas e mantém apenas um registro.
### Dedup Intra-batch
Ao usar `remember_many()`, os itens dentro do mesmo batch são comparados entre si antes de atingir o armazenamento. Se dois itens tiverem similaridade de cosseno >= `batch_dedup_threshold` (padrão 0.98), o posterior é silenciosamente descartado. Isso captura duplicatas exatas ou quase exatas dentro de um único batch sem chamadas ao LLM (pura matemática vetorial).
```python
# Caminho rápido (padrão para contexto de tarefa do agente)
# Apenas 2 registros são armazenados (o terceiro é quase duplicata do primeiro)
memory.remember_many([
"CrewAI supports complex workflows.",
"Python is a great language.",
"CrewAI supports complex workflows.", # descartado pelo dedup intra-batch
])
```
## Saves Não-Bloqueantes
`remember_many()` é **não-bloqueante** -- ele envia o pipeline de codificação para uma thread em background e retorna imediatamente. Isso significa que o agente pode continuar para a próxima tarefa enquanto as memórias estão sendo salvas.
```python
# Retorna imediatamente -- save acontece em background
memory.remember_many(["Fato A.", "Fato B.", "Fato C."])
# recall() espera automaticamente saves pendentes antes de buscar
matches = memory.recall("fatos") # vê todos os 3 registros
```
### Barreira de Leitura
Cada chamada `recall()` executa automaticamente `drain_writes()` antes de buscar, garantindo que a consulta sempre veja os registros mais recentes persistidos. Isso é transparente -- você nunca precisa pensar nisso.
### Encerramento da Crew
Quando uma crew termina, `kickoff()` drena todos os saves de memória pendentes em seu bloco `finally`, então nenhum save é perdido mesmo que a crew complete enquanto saves em background estão em andamento.
### Uso Standalone
Para scripts ou notebooks onde não há ciclo de vida de crew, chame `drain_writes()` ou `close()` explicitamente:
```python
memory = Memory()
memory.remember_many(["Fato A.", "Fato B."])
# Opção 1: Esperar saves pendentes
memory.drain_writes()
# Opção 2: Drenar e encerrar o pool de background
memory.close()
```
## Origem e Privacidade
Cada registro de memória pode carregar uma tag `source` para rastreamento de procedência e uma flag `private` para controle de acesso.
### Rastreamento de Origem
O parâmetro `source` identifica de onde uma memória veio:
```python
# Marcar memórias com sua origem
memory.remember("Usuário prefere modo escuro", source="user:alice")
memory.remember("Configuração do sistema atualizada", source="admin")
memory.remember("Agente encontrou um bug", source="agent:debugger")
# Recuperar apenas memórias de uma origem específica
matches = memory.recall("preferências do usuário", source="user:alice")
```
### Memórias Privadas
Memórias privadas só são visíveis no recall quando o `source` corresponde:
```python
# Armazenar uma memória privada
memory.remember("A chave de API da Alice é sk-...", source="user:alice", private=True)
# Este recall vê a memória privada (source corresponde)
matches = memory.recall("chave de API", source="user:alice")
# Este recall NÃO a vê (source diferente)
matches = memory.recall("chave de API", source="user:bob")
# Acesso admin: ver todos os registros privados independente do source
matches = memory.recall("chave de API", include_private=True)
```
Isso é particularmente útil em implantações multi-usuário ou corporativas onde memórias de diferentes usuários devem ser isoladas.
## RecallFlow (Recall Profundo)
`recall()` suporta duas profundidades:
- **`depth="shallow"`** -- Busca vetorial direta com pontuação composta. Rápido (~200ms), sem chamadas ao LLM.
- **`depth="deep"` (padrão)** -- Executa um RecallFlow em múltiplas etapas: análise da consulta, seleção de escopo, busca vetorial paralela, roteamento baseado em confiança e exploração recursiva opcional quando a confiança é baixa.
**Pulo inteligente do LLM**: Consultas com menos de `query_analysis_threshold` (padrão 200 caracteres) pulam a análise de consulta do LLM inteiramente, mesmo no modo deep. Consultas curtas como "Qual banco de dados usamos?" já são boas frases de busca -- a análise do LLM agrega pouco valor. Isso economiza ~1-3s por recall para consultas curtas típicas. Apenas consultas mais longas (ex.: descrições completas de tarefas) passam pela destilação do LLM em sub-consultas direcionadas.
```python
# Shallow: busca vetorial pura, sem LLM
matches = memory.recall("O que decidimos?", limit=10, depth="shallow")
# Caminho inteligente para perguntas complexas
# Deep (padrão): recuperação inteligente com análise LLM para consultas longas
matches = memory.recall(
"Resuma todas as decisões de arquitetura deste trimestre",
limit=10,
depth="auto",
depth="deep",
)
```
@@ -406,6 +508,7 @@ memory = Memory(
confidence_threshold_high=0.9, # Só sintetizar quando muito confiante
confidence_threshold_low=0.4, # Explorar mais profundamente de forma mais agressiva
exploration_budget=2, # Permitir até 2 rodadas de exploração
query_analysis_threshold=200, # Pular LLM para consultas menores que isso
)
```
@@ -613,6 +716,45 @@ memory = Memory(embedder=my_embedder)
| Custom | `custom` | -- | Requer `embedding_callable`. |
## Configuração de LLM
A memória usa um LLM para análise de save (inferência de escopo, categorias e importância), decisões de consolidação e análise de consulta no recall profundo. Você pode configurar qual modelo usar.
```python
from crewai import Memory, LLM
# Padrão: gpt-4o-mini
memory = Memory()
# Usar um modelo OpenAI diferente
memory = Memory(llm="gpt-4o")
# Usar Anthropic
memory = Memory(llm="anthropic/claude-3-haiku-20240307")
# Usar Ollama para análise totalmente local/privada
memory = Memory(llm="ollama/llama3.2")
# Usar Google Gemini
memory = Memory(llm="gemini/gemini-2.0-flash")
# Passar uma instância LLM pré-configurada com configurações customizadas
llm = LLM(model="gpt-4o", temperature=0)
memory = Memory(llm=llm)
```
O LLM é inicializado **lazily** -- ele só é criado quando necessário pela primeira vez. Isso significa que `Memory()` nunca falha no momento da construção, mesmo que chaves de API não estejam definidas. Erros só aparecem quando o LLM é realmente chamado (ex.: ao salvar sem escopo/categorias explícitos, ou durante recall profundo).
Para operação totalmente offline/privada, use um modelo local tanto para o LLM quanto para o embedder:
```python
memory = Memory(
llm="ollama/llama3.2",
embedder={"provider": "ollama", "config": {"model_name": "mxbai-embed-large"}},
)
```
## Backend de Armazenamento
- **Padrão**: LanceDB, armazenado em `./.crewai/memory` (ou `$CREWAI_STORAGE_DIR/memory` se a variável de ambiente estiver definida, ou o caminho que você passar como `storage="path/to/dir"`).
@@ -685,11 +827,18 @@ class MemoryMonitor(BaseEventListener):
- Ao usar uma crew, confirme que `memory=True` ou `memory=Memory(...)` está definido.
**Recall lento?**
- Use `depth="shallow"` para contexto rotineiro do agente. Reserve `depth="auto"` ou `"deep"` para consultas complexas.
- Use `depth="shallow"` para contexto rotineiro do agente. Reserve `depth="deep"` para consultas complexas.
- Aumente `query_analysis_threshold` para pular a análise do LLM em mais consultas.
**Erros de análise LLM nos logs?**
- A memória ainda salva/recupera com padrões seguros. Verifique chaves de API, limites de taxa e disponibilidade do modelo se quiser análise LLM completa.
**Erros de save em background nos logs?**
- Os saves de memória rodam em uma thread em background. Erros são emitidos como `MemorySaveFailedEvent` mas não derrubam o agente. Verifique os logs para a causa raiz (geralmente problemas de conexão com LLM ou embedder).
**Conflitos de escrita concorrente?**
- As operações do LanceDB são serializadas com um lock compartilhado e reexecutadas automaticamente em caso de conflito. Isso lida com múltiplas instâncias `Memory` apontando para o mesmo banco de dados (ex.: memória do agente + memória da crew). Nenhuma ação necessária.
**Navegar na memória pelo terminal:**
```bash
crewai memory # Abre o navegador TUI
@@ -721,7 +870,9 @@ Toda a configuração é passada como argumentos nomeados para `Memory(...)`. Ca
| `consolidation_threshold` | `0.85` | Similaridade acima da qual a consolidação é ativada no save. Defina `1.0` para desativar. |
| `consolidation_limit` | `5` | Máx. de registros existentes para comparar durante consolidação. |
| `default_importance` | `0.5` | Importância atribuída quando não fornecida e a análise LLM é pulada. |
| `batch_dedup_threshold` | `0.98` | Similaridade de cosseno para descartar quase-duplicatas dentro de um batch `remember_many()`. |
| `confidence_threshold_high` | `0.8` | Confiança de recall acima da qual resultados são retornados diretamente. |
| `confidence_threshold_low` | `0.5` | Confiança de recall abaixo da qual exploração mais profunda é ativada. |
| `complex_query_threshold` | `0.7` | Para consultas complexas, explorar mais profundamente abaixo desta confiança. |
| `exploration_budget` | `1` | Número de rodadas de exploração por LLM durante recall profundo. |
| `query_analysis_threshold` | `200` | Consultas menores que isso (em caracteres) pulam análise LLM durante recall profundo. |

View File

@@ -73,6 +73,8 @@ Quando este flow é executado, ele irá:
| `default_outcome` | `str` | Não | Outcome a usar se nenhum feedback for fornecido. Deve estar em `emit` |
| `metadata` | `dict` | Não | Dados adicionais para integrações enterprise |
| `provider` | `HumanFeedbackProvider` | Não | Provider customizado para feedback assíncrono/não-bloqueante. Veja [Feedback Humano Assíncrono](#feedback-humano-assíncrono-não-bloqueante) |
| `learn` | `bool` | Não | Habilitar aprendizado HITL: destila lições do feedback e pré-revisa saídas futuras. Padrão `False`. Veja [Aprendendo com Feedback](#aprendendo-com-feedback) |
| `learn_limit` | `int` | Não | Máximo de lições passadas para recuperar na pré-revisão. Padrão `5` |
### Uso Básico (Sem Roteamento)
@@ -576,6 +578,64 @@ Se você está usando um framework web assíncrono (FastAPI, aiohttp, Slack Bolt
5. **Persistência automática**: O estado é automaticamente salvo quando `HumanFeedbackPending` é lançado e usa `SQLiteFlowPersistence` por padrão
6. **Persistência customizada**: Passe uma instância de persistência customizada para `from_pending()` se necessário
## Aprendendo com Feedback
O parâmetro `learn=True` habilita um ciclo de feedback entre revisores humanos e o sistema de memória. Quando habilitado, o sistema melhora progressivamente suas saídas aprendendo com correções humanas anteriores.
### Como Funciona
1. **Após o feedback**: O LLM extrai lições generalizáveis da saída + feedback e as armazena na memória com `source="hitl"`. Se o feedback for apenas aprovação (ex: "parece bom"), nada é armazenado.
2. **Antes da próxima revisão**: Lições HITL passadas são recuperadas da memória e aplicadas pelo LLM para melhorar a saída antes que o humano a veja.
Com o tempo, o humano vê saídas pré-revisadas progressivamente melhores porque cada correção informa revisões futuras.
### Exemplo
```python Code
class ArticleReviewFlow(Flow):
@start()
@human_feedback(
message="Review this article draft:",
emit=["approved", "needs_revision"],
llm="gpt-4o-mini",
learn=True, # enable HITL learning
)
def generate_article(self):
return self.crew.kickoff(inputs={"topic": "AI Safety"}).raw
@listen("approved")
def publish(self):
print(f"Publishing: {self.last_human_feedback.output}")
@listen("needs_revision")
def revise(self):
print("Revising based on feedback...")
```
**Primeira execução**: O humano vê a saída bruta e diz "Sempre inclua citações para afirmações factuais." A lição é destilada e armazenada na memória.
**Segunda execução**: O sistema recupera a lição sobre citações, pré-revisa a saída para adicionar citações e então mostra a versão melhorada. O trabalho do humano muda de "corrigir tudo" para "identificar o que o sistema deixou passar."
### Configuração
| Parâmetro | Padrão | Descrição |
|-----------|--------|-----------|
| `learn` | `False` | Habilitar aprendizado HITL |
| `learn_limit` | `5` | Máximo de lições passadas para recuperar na pré-revisão |
### Decisões de Design Principais
- **Mesmo LLM para tudo**: O parâmetro `llm` no decorador é compartilhado pelo mapeamento de outcome, destilação de lições e pré-revisão. Não é necessário configurar múltiplos modelos.
- **Saída estruturada**: Tanto a destilação quanto a pré-revisão usam function calling com modelos Pydantic quando o LLM suporta, com fallback para parsing de texto caso contrário.
- **Armazenamento não-bloqueante**: Lições são armazenadas via `remember_many()` que executa em uma thread em segundo plano -- o flow continua imediatamente.
- **Degradação graciosa**: Se o LLM falhar durante a destilação, nada é armazenado. Se falhar durante a pré-revisão, a saída bruta é mostrada. Nenhuma falha bloqueia o flow.
- **Sem escopo/categorias necessários**: Ao armazenar lições, apenas `source` é passado. O pipeline de codificação infere escopo, categorias e importância automaticamente.
<Note>
`learn=True` requer que o Flow tenha memória disponível. Flows obtêm memória automaticamente por padrão, mas se você a desabilitou com `_skip_auto_memory`, o aprendizado HITL será silenciosamente ignorado.
</Note>
## Documentação Relacionada
- [Visão Geral de Flows](/pt-BR/concepts/flows) - Aprenda sobre CrewAI Flows
@@ -583,3 +643,4 @@ Se você está usando um framework web assíncrono (FastAPI, aiohttp, Slack Bolt
- [Persistência de Flows](/pt-BR/concepts/flows#persistence) - Persistindo estado de flows
- [Roteamento com @router](/pt-BR/concepts/flows#router) - Mais sobre roteamento condicional
- [Input Humano na Execução](/pt-BR/learn/human-input-on-execution) - Input humano no nível de task
- [Memória](/pt-BR/concepts/memory) - O sistema unificado de memória usado pelo aprendizado HITL

View File

@@ -1974,6 +1974,9 @@ class Flow(Generic[T], metaclass=FlowMeta):
return final_output
finally:
# Ensure all background memory saves complete before returning
if self.memory is not None and hasattr(self.memory, "drain_writes"):
self.memory.drain_writes()
if request_id_token is not None:
current_flow_request_id.reset(request_id_token)
if flow_id_token is not None:
@@ -2530,8 +2533,12 @@ class Flow(Generic[T], metaclass=FlowMeta):
return (None, None)
# For cyclic flows, clear from completed to allow re-execution
self._completed_methods.discard(listener_name)
# Also clear from fired OR listeners for cyclic flows
self._discard_or_listener(listener_name)
# Clear ALL fired OR listeners so they can fire again in the new cycle.
# This mirrors what _execute_start_method does for start-method cycles.
# Only discarding the individual listener is insufficient because
# downstream or_() listeners (e.g., method_a listening to
# or_(handler_a, handler_b)) would remain suppressed across iterations.
self._clear_or_listeners()
try:
method = self._methods[listener_name]

View File

@@ -419,8 +419,22 @@ class LLM(BaseLLM):
# FALLBACK to LiteLLM
if not LITELLM_AVAILABLE:
logger.error("LiteLLM is not available, falling back to LiteLLM")
raise ImportError("Fallback to LiteLLM is not available") from None
native_list = ", ".join(SUPPORTED_NATIVE_PROVIDERS)
error_msg = (
f"Unable to initialize LLM with model '{model}'. "
f"The model did not match any supported native provider "
f"({native_list}), and the LiteLLM fallback package is not "
f"installed.\n\n"
f"To fix this, either:\n"
f" 1. Install LiteLLM for broad model support: "
f"uv add litellm\n"
f"or\n"
f"pip install litellm\n\n"
f"For more details, see: "
f"https://docs.crewai.com/en/learn/llm-connections"
)
logger.error(error_msg)
raise ImportError(error_msg) from None
instance = object.__new__(cls)
super(LLM, instance).__init__(model=model, is_litellm=True, **kwargs)

View File

@@ -224,22 +224,30 @@ class Memory:
return future
def _on_save_done(self, future: Future[Any]) -> None:
"""Remove a completed future from the pending list and emit failure event if needed."""
with self._pending_lock:
try:
self._pending_saves.remove(future)
except ValueError:
pass # already removed
exc = future.exception()
if exc is not None:
crewai_event_bus.emit(
self,
MemorySaveFailedEvent(
value="background save",
error=str(exc),
source_type="unified_memory",
),
)
"""Remove a completed future from the pending list and emit failure event if needed.
This callback must never raise -- it runs from the thread pool's
internal machinery during process shutdown when executors and the
event bus may already be closed.
"""
try:
with self._pending_lock:
try:
self._pending_saves.remove(future)
except ValueError:
pass # already removed
exc = future.exception()
if exc is not None:
crewai_event_bus.emit(
self,
MemorySaveFailedEvent(
value="background save",
error=str(exc),
source_type="unified_memory",
),
)
except Exception: # noqa: S110
pass # swallow everything during shutdown
def drain_writes(self) -> None:
"""Block until all pending background saves have completed.
@@ -437,30 +445,49 @@ class Memory:
Both started and completed events are emitted here (in the background
thread) so they pair correctly on the event bus scope stack.
All ``emit`` calls are wrapped in try/except to handle the case where
the event bus shuts down before the background save finishes (e.g.
during process exit).
"""
crewai_event_bus.emit(
self,
MemorySaveStartedEvent(
value=f"{len(contents)} memories (background)",
metadata=metadata,
source_type="unified_memory",
),
)
start = time.perf_counter()
records = self._encode_batch(
contents, scope, categories, metadata, importance, source, private
)
elapsed_ms = (time.perf_counter() - start) * 1000
crewai_event_bus.emit(
self,
MemorySaveCompletedEvent(
value=f"{len(records)} memories saved",
metadata=metadata or {},
agent_role=agent_role,
save_time_ms=elapsed_ms,
source_type="unified_memory",
),
)
try:
crewai_event_bus.emit(
self,
MemorySaveStartedEvent(
value=f"{len(contents)} memories (background)",
metadata=metadata,
source_type="unified_memory",
),
)
except RuntimeError:
pass # event bus shut down during process exit
try:
start = time.perf_counter()
records = self._encode_batch(
contents, scope, categories, metadata, importance, source, private
)
elapsed_ms = (time.perf_counter() - start) * 1000
except RuntimeError:
# The encoding pipeline uses asyncio.run() -> to_thread() internally.
# If the process is shutting down, the default executor is closed and
# to_thread raises "cannot schedule new futures after shutdown".
# Silently abandon the save -- the process is exiting anyway.
return []
try:
crewai_event_bus.emit(
self,
MemorySaveCompletedEvent(
value=f"{len(records)} memories saved",
metadata=metadata or {},
agent_role=agent_role,
save_time_ms=elapsed_ms,
source_type="unified_memory",
),
)
except RuntimeError:
pass # event bus shut down during process exit
return records
def extract_memories(self, content: str) -> list[str]:

View File

@@ -1647,3 +1647,128 @@ class TestFlowAkickoff:
assert execution_order == ["begin", "route", "path_a"]
assert result == "path_a_result"
def test_cyclic_flow_or_listeners_fire_every_iteration():
"""Test that or_() listeners reset between cycle iterations through a router.
Regression test for a bug where _fired_or_listeners was not cleared when
cycles loop through a router/listener instead of a @start method, causing
or_() listeners to permanently suppress after the first iteration.
Pattern: router classifies → routes to ONE of several handlers → or_()
merge downstream → cycle back. Only one handler fires per iteration, but
the or_() merge must still fire every time.
"""
execution_order = []
class CyclicOrFlow(Flow):
iteration = 0
max_iterations = 3
@start()
def begin(self):
execution_order.append("begin")
@router(or_(begin, "loop_back"))
def route(self):
self.iteration += 1
execution_order.append(f"route_{self.iteration}")
if self.iteration <= self.max_iterations:
# Alternate between handlers on each iteration
return "type_a" if self.iteration % 2 == 1 else "type_b"
return "done"
@listen("type_a")
def handler_a(self):
execution_order.append(f"handler_a_{self.iteration}")
@listen("type_b")
def handler_b(self):
execution_order.append(f"handler_b_{self.iteration}")
# This or_() listener must fire on EVERY iteration, not just the first
@listen(or_(handler_a, handler_b))
def merge(self):
execution_order.append(f"merge_{self.iteration}")
@listen(merge)
def loop_back(self):
execution_order.append(f"loop_back_{self.iteration}")
flow = CyclicOrFlow()
flow.kickoff()
# merge must have fired once per iteration (3 times total)
merge_events = [e for e in execution_order if e.startswith("merge_")]
assert len(merge_events) == 3, (
f"or_() listener 'merge' should fire every iteration, "
f"got {len(merge_events)} fires: {execution_order}"
)
# loop_back must have also fired every iteration
loop_back_events = [e for e in execution_order if e.startswith("loop_back_")]
assert len(loop_back_events) == 3, (
f"'loop_back' should fire every iteration, "
f"got {len(loop_back_events)} fires: {execution_order}"
)
# Verify alternating handlers
handler_a_events = [e for e in execution_order if e.startswith("handler_a_")]
handler_b_events = [e for e in execution_order if e.startswith("handler_b_")]
assert len(handler_a_events) == 2 # iterations 1 and 3
assert len(handler_b_events) == 1 # iteration 2
def test_cyclic_flow_multiple_or_listeners_fire_every_iteration():
"""Test that multiple or_() listeners all reset between cycle iterations.
Mirrors a real-world pattern: a router classifies messages, handlers process
them, then both a 'send' step (or_ on handlers) and a 'store' step (or_ on
router outputs) must fire on every loop iteration.
"""
execution_order = []
class MultiOrCyclicFlow(Flow):
iteration = 0
max_iterations = 3
@start()
def begin(self):
execution_order.append("begin")
@router(or_(begin, "capture"))
def classify(self):
self.iteration += 1
execution_order.append(f"classify_{self.iteration}")
if self.iteration <= self.max_iterations:
return "type_a"
return "exit"
@listen("type_a")
def handle_type_a(self):
execution_order.append(f"handle_a_{self.iteration}")
# or_() listener on router output strings — must fire every iteration
@listen(or_("type_a", "type_b", "type_c"))
def store(self):
execution_order.append(f"store_{self.iteration}")
# or_() listener on handler methods — must fire every iteration
@listen(or_(handle_type_a,))
def send(self):
execution_order.append(f"send_{self.iteration}")
@listen("send")
def capture(self):
execution_order.append(f"capture_{self.iteration}")
flow = MultiOrCyclicFlow()
flow.kickoff()
for method in ["store", "send", "capture"]:
events = [e for e in execution_order if e.startswith(f"{method}_")]
assert len(events) == 3, (
f"'{method}' should fire every iteration, "
f"got {len(events)} fires: {execution_order}"
)