fix(growth): 记忆召回跨 agent fallback — IdentityRecall 全局 scope 检索
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

B14 根因: 记忆按 agent_id 隔离存储,用户换对话/Agent 后
新 agent_id scope 下无记忆可检索,导致"我叫什么"无法召回。

修复: retrieve_broad_identity 在当前 agent 无结果时 fallback
到 retrieve_by_scope_any_agent,跨所有 agent 检索身份相关
的 preference/knowledge 记忆(用户名、工作单位等)。

影响范围: 仅 IdentityRecall 路径("我是谁"/"我叫什么"类查询),
普通 keyword 检索仍按 agent_id scope 隔离。
This commit is contained in:
iven
2026-04-21 20:39:32 +08:00
parent b2908791f6
commit c5f98beb7c

View File

@@ -331,6 +331,36 @@ impl MemoryRetriever {
self.config.experience_budget, self.config.experience_budget,
).await?; ).await?;
// Fallback: if no results for this agent, search across ALL agents
// for identity-critical info (user name, workplace, preferences)
if preferences.is_empty() && knowledge.is_empty() && experience.is_empty() {
tracing::info!(
"[MemoryRetriever] No memories for agent {}, falling back to global scope",
agent_str
);
let global_prefs = self.retrieve_by_scope_any_agent(
MemoryType::Preference,
self.config.max_results_per_type,
self.config.preference_budget,
).await?;
let global_knowledge = self.retrieve_by_scope_any_agent(
MemoryType::Knowledge,
self.config.max_results_per_type,
self.config.knowledge_budget,
).await?;
let total: usize = global_prefs.iter()
.chain(global_knowledge.iter())
.map(|m| m.estimated_tokens())
.sum();
return Ok(RetrievalResult {
preferences: global_prefs,
knowledge: global_knowledge,
experience,
total_tokens: total,
});
}
let total_tokens = preferences.iter() let total_tokens = preferences.iter()
.chain(knowledge.iter()) .chain(knowledge.iter())
.chain(experience.iter()) .chain(experience.iter())
@@ -352,6 +382,43 @@ impl MemoryRetriever {
}) })
} }
/// Retrieve memories across ALL agents for a given type.
/// Used as fallback when agent-scoped retrieval returns nothing for identity recall.
async fn retrieve_by_scope_any_agent(
&self,
memory_type: MemoryType,
max_results: usize,
token_budget: usize,
) -> Result<Vec<MemoryEntry>> {
// Match any agent by using only the type suffix as scope pattern
let scope_pattern = format!("/{}", memory_type);
let options = FindOptions {
scope: None, // No scope filter — search all agents
limit: Some(max_results * 3),
min_similarity: None,
};
let entries = self.viking.find("", options).await?;
// Filter to only matching memory type
let mut filtered: Vec<MemoryEntry> = entries
.into_iter()
.filter(|e| e.uri.contains(&scope_pattern) || e.memory_type == memory_type)
.collect();
filtered.sort_by(|a, b| {
b.importance.cmp(&a.importance)
.then_with(|| b.access_count.cmp(&a.access_count))
});
let mut result = Vec::new();
let mut used_tokens = 0;
for entry in filtered {
let tokens = entry.estimated_tokens();
if used_tokens + tokens > token_budget { break; }
used_tokens += tokens;
result.push(entry);
if result.len() >= max_results { break; }
}
Ok(result)
}
/// Retrieve memories by scope only (no text search). /// Retrieve memories by scope only (no text search).
/// Returns entries sorted by importance and recency, limited by budget. /// Returns entries sorted by importance and recency, limited by budget.
async fn retrieve_by_scope( async fn retrieve_by_scope(