diff --git a/docs/en/concepts/memory.mdx b/docs/en/concepts/memory.mdx
index efea552f3..954d5efe6 100644
--- a/docs/en/concepts/memory.mdx
+++ b/docs/en/concepts/memory.mdx
@@ -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. |
diff --git a/docs/en/learn/human-feedback-in-flows.mdx b/docs/en/learn/human-feedback-in-flows.mdx
index 60588657a..523c25fc5 100644
--- a/docs/en/learn/human-feedback-in-flows.mdx
+++ b/docs/en/learn/human-feedback-in-flows.mdx
@@ -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.
+
+
+`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.
+
+
+
## 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
diff --git a/docs/ko/concepts/memory.mdx b/docs/ko/concepts/memory.mdx
index bf87acd2a..ea4463eea 100644
--- a/docs/ko/concepts/memory.mdx
+++ b/docs/ko/concepts/memory.mdx
@@ -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 분석을 건너뜀. |
diff --git a/docs/ko/learn/human-feedback-in-flows.mdx b/docs/ko/learn/human-feedback-in-flows.mdx
index 6ba92c37e..23877007e 100644
--- a/docs/ko/learn/human-feedback-in-flows.mdx
+++ b/docs/ko/learn/human-feedback-in-flows.mdx
@@ -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`만 전달됩니다. 인코딩 파이프라인이 범위, 카테고리, 중요도를 자동으로 추론합니다.
+
+
+`learn=True`는 Flow에 메모리가 사용 가능해야 합니다. Flow는 기본적으로 자동으로 메모리를 얻지만, `_skip_auto_memory`로 비활성화한 경우 HITL 학습은 조용히 건너뜁니다.
+
+
+
## 관련 문서
- [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 학습에서 사용되는 통합 메모리 시스템
diff --git a/docs/pt-BR/concepts/memory.mdx b/docs/pt-BR/concepts/memory.mdx
index 7b3198b6d..3931ed6ab 100644
--- a/docs/pt-BR/concepts/memory.mdx
+++ b/docs/pt-BR/concepts/memory.mdx
@@ -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 informaçõ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. |
diff --git a/docs/pt-BR/learn/human-feedback-in-flows.mdx b/docs/pt-BR/learn/human-feedback-in-flows.mdx
index c847bf31a..b25af542b 100644
--- a/docs/pt-BR/learn/human-feedback-in-flows.mdx
+++ b/docs/pt-BR/learn/human-feedback-in-flows.mdx
@@ -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.
+
+
+`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.
+
+
+
## 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
diff --git a/lib/crewai/src/crewai/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py
index ca3925198..c3ac1ad72 100644
--- a/lib/crewai/src/crewai/flow/flow.py
+++ b/lib/crewai/src/crewai/flow/flow.py
@@ -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]
diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py
index 902a3d310..20a0373cb 100644
--- a/lib/crewai/src/crewai/llm.py
+++ b/lib/crewai/src/crewai/llm.py
@@ -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)
diff --git a/lib/crewai/src/crewai/memory/unified_memory.py b/lib/crewai/src/crewai/memory/unified_memory.py
index 432262048..a15f77afd 100644
--- a/lib/crewai/src/crewai/memory/unified_memory.py
+++ b/lib/crewai/src/crewai/memory/unified_memory.py
@@ -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]:
diff --git a/lib/crewai/tests/test_flow.py b/lib/crewai/tests/test_flow.py
index 2040e9e5b..0ec4b3063 100644
--- a/lib/crewai/tests/test_flow.py
+++ b/lib/crewai/tests/test_flow.py
@@ -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}"
+ )