fix(growth): HIGH-6 修复 extract_combined 合并提取空壳
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

根因: growth.rs 构造 CombinedExtraction 时硬编码 experiences: Vec::new()
和 profile_signals: default(),导致 L1 结构化经验不被提取、L2 技能进化
没有输入数据、整个进化引擎无法端到端工作。

修复:
- extractor.rs: 添加 COMBINED_EXTRACTION_PROMPT 统一 prompt,单次 LLM 调用
  同时输出 memories + experiences + profile_signals
- extractor.rs: 添加 parse_combined_response() 解析 LLM JSON 响应
- LlmDriverForExtraction trait: 添加 extract_with_prompt() 方法(默认不支持,
  退化到现有 extract() + 启发式推断)
- MemoryExtractor: 添加 extract_combined() 方法,优先单次调用,失败则退化
- growth.rs: extract_combined() 使用新的合并提取替代硬编码空值
- TauriExtractionDriver: 实现 extract_with_prompt()
- ProfileSignals: 添加 has_any_signal() 方法
- types.rs: ProfileSignals 无 structural 变化(字段已存在)

测试: 4 个新测试(parse_combined_response_full/minimal/invalid +
extract_combined_fallback),11 个 extractor 测试全部通过
This commit is contained in:
iven
2026-04-18 22:56:42 +08:00
parent cb727fdcc7
commit 3c6581f915
4 changed files with 536 additions and 29 deletions

View File

@@ -15,7 +15,7 @@ use zclaw_growth::{
AggregatedPattern, CombinedExtraction, EvolutionConfig, EvolutionEngine,
ExperienceExtractor, GrowthTracker, InjectionFormat,
LlmDriverForExtraction, MemoryExtractor, MemoryRetriever, PromptInjector,
ProfileSignals, RetrievalResult, UserProfileUpdater, VikingAdapter,
RetrievalResult, UserProfileUpdater, VikingAdapter,
};
use zclaw_memory::{ExtractedFactBatch, Fact, FactCategory, UserProfileStore};
use zclaw_types::{AgentId, Message, Result, SessionId};
@@ -263,8 +263,8 @@ impl GrowthIntegration {
Ok(count)
}
/// Combined extraction: single LLM call that produces both stored memories
/// and structured facts, avoiding double extraction overhead.
/// Combined extraction: single LLM call that produces stored memories,
/// structured experiences, and profile signals — all in one pass.
///
/// Returns `(memory_count, Option<ExtractedFactBatch>)` on success.
pub async fn extract_combined(
@@ -277,25 +277,28 @@ impl GrowthIntegration {
return Ok(None);
}
// Single LLM extraction call
let extracted = self
// 单次 LLM 提取memories + experiences + profile_signals
let combined = self
.extractor
.extract(messages, session_id.clone())
.extract_combined(messages, session_id.clone())
.await
.unwrap_or_else(|e| {
tracing::warn!("[GrowthIntegration] Combined extraction failed: {}", e);
Vec::new()
CombinedExtraction::default()
});
if extracted.is_empty() {
if combined.memories.is_empty()
&& combined.experiences.is_empty()
&& !combined.profile_signals.has_any_signal()
{
return Ok(None);
}
let mem_count = extracted.len();
let mem_count = combined.memories.len();
// Store raw memories
self.extractor
.store_memories(&agent_id.to_string(), &extracted)
.store_memories(&agent_id.to_string(), &combined.memories)
.await?;
// Track learning event
@@ -304,14 +307,9 @@ impl GrowthIntegration {
.await?;
// Persist structured experiences (L1 enhancement)
let combined_extraction = CombinedExtraction {
memories: extracted.clone(),
experiences: Vec::new(), // LLM-driven extraction fills this later
profile_signals: ProfileSignals::default(),
};
if let Ok(exp_count) = self
.experience_extractor
.persist_experiences(&agent_id.to_string(), &combined_extraction)
.persist_experiences(&agent_id.to_string(), &combined)
.await
{
if exp_count > 0 {
@@ -324,9 +322,7 @@ impl GrowthIntegration {
// Update user profile from extraction signals (L1 enhancement)
if let Some(profile_store) = &self.profile_store {
let updates = self
.profile_updater
.collect_updates(&combined_extraction);
let updates = self.profile_updater.collect_updates(&combined);
let user_id = agent_id.to_string();
for update in updates {
if let Err(e) = profile_store
@@ -342,8 +338,9 @@ impl GrowthIntegration {
}
}
// Convert same extracted memories to structured facts (no extra LLM call)
let facts: Vec<Fact> = extracted
// Convert extracted memories to structured facts
let facts: Vec<Fact> = combined
.memories
.into_iter()
.map(|m| {
let category = match m.memory_type {