fix(growth): Evolution Engine 穷尽审计 3CRITICAL + 3HIGH 全部修复
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

C-01: ExperienceExtractor 接入 ExperienceStore
- GrowthIntegration.new() 创建 ExperienceExtractor 时注入 ExperienceStore
- 经验持久化路径打通:extract_combined → persist_experiences → ExperienceStore

C-02+C-03: 进化触发链路全链路接通
- create_middleware_chain() 注册 EvolutionMiddleware (priority 78)
- MemoryMiddleware 持有 Arc<EvolutionMiddleware> 共享引用
- after_completion 中调用 check_evolution() → 推送 PendingEvolution
- EvolutionMiddleware 在下次对话前注入进化建议到 system prompt

H-01: FeedbackCollector loaded 标志修复
- load() 失败时保留 loaded=false,下次 save 重试
- 日志级别 debug → warn

H-03: FeedbackCollector 内部可变性
- EvolutionEngine.feedback 改为 Arc<Mutex<FeedbackCollector>>
- submit_feedback() 从 &mut self → &self,支持中间件 &self 调用路径
- GrowthIntegration.initialize() 从 &mut self → &self

H-05: 删除空测试 test_parse_empty_response (无 assert)

H-06: infer_experiences_from_memories() fallback
- Outcome::Success → Outcome::Partial (反映推断不确定性)
This commit is contained in:
iven
2026-04-19 00:43:02 +08:00
parent 72b3206a6b
commit e94235c4f9
7 changed files with 107 additions and 58 deletions

View File

@@ -42,7 +42,7 @@ impl Default for EvolutionConfig {
/// 进化引擎中枢
pub struct EvolutionEngine {
viking: Arc<VikingAdapter>,
feedback: FeedbackCollector,
feedback: Arc<tokio::sync::Mutex<FeedbackCollector>>,
config: EvolutionConfig,
}
@@ -50,7 +50,9 @@ impl EvolutionEngine {
pub fn new(viking: Arc<VikingAdapter>) -> Self {
Self {
viking: viking.clone(),
feedback: FeedbackCollector::with_viking(viking),
feedback: Arc::new(tokio::sync::Mutex::new(
FeedbackCollector::with_viking(viking),
)),
config: EvolutionConfig::default(),
}
}
@@ -61,7 +63,9 @@ impl EvolutionEngine {
let viking = experience_store.viking().clone();
Self {
viking: viking.clone(),
feedback: FeedbackCollector::with_viking(viking),
feedback: Arc::new(tokio::sync::Mutex::new(
FeedbackCollector::with_viking(viking),
)),
config: EvolutionConfig::default(),
}
}
@@ -144,18 +148,21 @@ impl EvolutionEngine {
// -----------------------------------------------------------------------
/// 提交反馈并获取信任度更新,自动持久化
pub async fn submit_feedback(&mut self, entry: FeedbackEntry) -> TrustUpdate {
let update = self.feedback.submit_feedback(entry);
pub async fn submit_feedback(&self, entry: FeedbackEntry) -> TrustUpdate {
let mut feedback = self.feedback.lock().await;
let update = feedback.submit_feedback(entry);
// 非阻塞持久化:失败仅打日志,不影响返回值
if let Err(e) = self.feedback.save().await {
if let Err(e) = feedback.save().await {
tracing::warn!("[EvolutionEngine] Failed to persist trust records: {}", e);
}
update
}
/// 获取需要优化的进化产物
pub fn get_artifacts_needing_optimization(&self) -> Vec<String> {
pub async fn get_artifacts_needing_optimization(&self) -> Vec<String> {
self.feedback
.lock()
.await
.get_artifacts_needing_optimization()
.iter()
.map(|r| r.artifact_id.clone())
@@ -163,8 +170,10 @@ impl EvolutionEngine {
}
/// 获取建议归档的进化产物
pub fn get_artifacts_to_archive(&self) -> Vec<String> {
pub async fn get_artifacts_to_archive(&self) -> Vec<String> {
self.feedback
.lock()
.await
.get_artifacts_to_archive()
.iter()
.map(|r| r.artifact_id.clone())
@@ -172,22 +181,21 @@ impl EvolutionEngine {
}
/// 获取推荐产物
pub fn get_recommended_artifacts(&self) -> Vec<String> {
pub async fn get_recommended_artifacts(&self) -> Vec<String> {
self.feedback
.lock()
.await
.get_recommended_artifacts()
.iter()
.map(|r| r.artifact_id.clone())
.collect()
}
/// 获取反馈收集器的引用(用于高级查询)
pub fn feedback(&self) -> &FeedbackCollector {
&self.feedback
}
/// 启动时加载已持久化的信任度记录
pub async fn load_feedback(&mut self) -> Result<usize> {
pub async fn load_feedback(&self) -> Result<usize> {
self.feedback
.lock()
.await
.load()
.await
.map_err(|e| zclaw_types::ZclawError::Internal(e))

View File

@@ -501,7 +501,7 @@ fn infer_experiences_from_memories(
pain_pattern: m.category.clone(),
context: content.clone(),
solution_steps: Vec::new(),
outcome: crate::types::Outcome::Success,
outcome: crate::types::Outcome::Partial,
confidence: m.confidence * 0.7, // 降低推断置信度
tools_used: m.keywords.clone(),
industry_context: None,

View File

@@ -137,13 +137,18 @@ impl FeedbackCollector {
pub async fn save(&mut self) -> Result<usize, String> {
// 首次保存前自动加载已有记录,防止丢失历史数据
if !self.loaded {
if let Err(e) = self.load().await {
tracing::debug!(
"[FeedbackCollector] Auto-load before save failed (non-fatal): {}",
e
);
match self.load().await {
Ok(_) => {
self.loaded = true;
}
Err(e) => {
// 加载失败时保留 loaded=false下次 save 会重试
tracing::warn!(
"[FeedbackCollector] Auto-load before save failed, will retry next save: {}",
e
);
}
}
self.loaded = true;
}
let viking = match &self.viking {

View File

@@ -252,6 +252,12 @@ impl Kernel {
growth = growth.with_llm_driver(driver.clone());
}
// Evolution middleware — shared with MemoryMiddleware for pushing evolution candidates
let evolution_mw = std::sync::Arc::new(
zclaw_runtime::middleware::evolution::EvolutionMiddleware::new()
);
chain.register(evolution_mw.clone());
// Compaction middleware — only register when threshold > 0
let threshold = self.config.compaction_threshold();
if threshold > 0 {
@@ -269,10 +275,11 @@ impl Kernel {
chain.register(Arc::new(mw));
}
// Memory middleware — auto-extract memories after conversations
// Memory middleware — auto-extract memories + check evolution after conversations
{
use std::sync::Arc;
let mw = zclaw_runtime::middleware::memory::MemoryMiddleware::new(growth);
let mw = zclaw_runtime::middleware::memory::MemoryMiddleware::new(growth)
.with_evolution(evolution_mw);
chain.register(Arc::new(mw));
}

View File

@@ -13,7 +13,7 @@
use std::sync::Arc;
use zclaw_growth::{
AggregatedPattern, CombinedExtraction, EvolutionConfig, EvolutionEngine,
ExperienceExtractor, GrowthTracker, InjectionFormat,
ExperienceExtractor, ExperienceStore, GrowthTracker, InjectionFormat,
LlmDriverForExtraction, MemoryExtractor, MemoryRetriever, PromptInjector,
RetrievalResult, UserProfileUpdater, VikingAdapter,
};
@@ -79,14 +79,15 @@ impl GrowthIntegration {
let retriever = MemoryRetriever::new(viking.clone());
let injector = PromptInjector::new();
let tracker = GrowthTracker::new(viking.clone());
let evolution_engine = Some(EvolutionEngine::new(viking));
let evolution_engine = Some(EvolutionEngine::new(viking.clone()));
Self {
retriever,
extractor,
injector,
tracker,
experience_extractor: ExperienceExtractor::new(),
experience_extractor: ExperienceExtractor::new()
.with_store(Arc::new(ExperienceStore::new(viking))),
profile_updater: UserProfileUpdater::new(),
profile_store: None,
evolution_engine,
@@ -120,10 +121,8 @@ impl GrowthIntegration {
///
/// **注意**FeedbackCollector 内部已实现 lazy-load首次 save() 时自动加载),
/// 所以此方法为可选优化 — 提前加载可避免首次反馈提交时的延迟。
/// 在中间件持有 GrowthIntegration 的场景中,由于 `&self` 限制无法调用此方法,
/// lazy-load 机制会兜底处理。
pub async fn initialize(&mut self) -> Result<()> {
if let Some(ref mut engine) = self.evolution_engine {
pub async fn initialize(&self) -> Result<()> {
if let Some(ref engine) = self.evolution_engine {
match engine.load_feedback().await {
Ok(count) => {
if count > 0 {

View File

@@ -11,14 +11,17 @@ use async_trait::async_trait;
use zclaw_types::Result;
use crate::growth::GrowthIntegration;
use crate::middleware::{AgentMiddleware, MiddlewareContext, MiddlewareDecision};
use crate::middleware::evolution::EvolutionMiddleware;
/// Middleware that handles memory retrieval (pre-completion) and extraction (post-completion).
///
/// Wraps `GrowthIntegration` and delegates:
/// - `before_completion` → `enhance_prompt()` for memory injection
/// - `after_completion` → `process_conversation()` for memory extraction
/// - `after_completion` → `extract_combined()` for memory extraction + evolution check
pub struct MemoryMiddleware {
growth: GrowthIntegration,
/// Shared EvolutionMiddleware for pushing evolution suggestions
evolution_mw: Option<std::sync::Arc<EvolutionMiddleware>>,
/// Minimum seconds between extractions for the same agent (debounce).
debounce_secs: u64,
/// Timestamp of last extraction per agent (for debouncing).
@@ -29,11 +32,18 @@ impl MemoryMiddleware {
pub fn new(growth: GrowthIntegration) -> Self {
Self {
growth,
evolution_mw: None,
debounce_secs: 30,
last_extraction: std::sync::Mutex::new(std::collections::HashMap::new()),
}
}
/// Attach a shared EvolutionMiddleware for pushing evolution suggestions.
pub fn with_evolution(mut self, mw: std::sync::Arc<EvolutionMiddleware>) -> Self {
self.evolution_mw = Some(mw);
self
}
/// Set the debounce interval in seconds.
pub fn with_debounce_secs(mut self, secs: u64) -> Self {
self.debounce_secs = secs;
@@ -52,6 +62,49 @@ impl MemoryMiddleware {
map.insert(agent_id.to_string(), now);
true
}
/// Check for evolvable patterns and push suggestions to EvolutionMiddleware.
async fn check_and_push_evolution(&self, agent_id: &zclaw_types::AgentId) {
let evolution_mw = match &self.evolution_mw {
Some(mw) => mw,
None => return,
};
match self.growth.check_evolution(agent_id).await {
Ok(patterns) if !patterns.is_empty() => {
for pattern in &patterns {
let trigger = pattern
.common_steps
.first()
.cloned()
.unwrap_or_else(|| pattern.pain_pattern.clone());
evolution_mw.add_pending(
crate::middleware::evolution::PendingEvolution {
pattern_name: pattern.pain_pattern.clone(),
trigger_suggestion: trigger,
description: format!(
"基于 {} 次重复经验,自动固化技能",
pattern.total_reuse
),
},
).await;
}
tracing::info!(
"[MemoryMiddleware] Pushed {} evolution candidates for agent {}",
patterns.len(),
agent_id
);
}
Ok(_) => {
tracing::debug!("[MemoryMiddleware] No evolvable patterns found");
}
Err(e) => {
tracing::debug!(
"[MemoryMiddleware] Evolution check failed (non-fatal): {}", e
);
}
}
}
}
#[async_trait]
@@ -65,11 +118,6 @@ impl AgentMiddleware for MemoryMiddleware {
ctx.user_input.chars().take(50).collect::<String>()
);
// Retrieve relevant memories and inject into system prompt.
// The SqliteStorage retriever now uses FTS5-only matching — if FTS5 finds
// no relevant results, no memories are returned (no scope-based fallback).
// This prevents irrelevant high-importance memories from leaking into
// unrelated conversations.
let base = &ctx.system_prompt;
match self.growth.enhance_prompt(&ctx.agent_id, base, &ctx.user_input).await {
Ok(enhanced) => {
@@ -88,7 +136,6 @@ impl AgentMiddleware for MemoryMiddleware {
Ok(MiddlewareDecision::Continue)
}
Err(e) => {
// Non-fatal: retrieval failure should not block the conversation
tracing::warn!(
"[MemoryMiddleware] Memory retrieval failed (non-fatal): {}",
e
@@ -99,7 +146,6 @@ impl AgentMiddleware for MemoryMiddleware {
}
async fn after_completion(&self, ctx: &MiddlewareContext) -> Result<()> {
// Debounce: skip extraction if called too recently for this agent
let agent_key = ctx.agent_id.to_string();
if !self.should_extract(&agent_key) {
tracing::debug!(
@@ -113,8 +159,6 @@ impl AgentMiddleware for MemoryMiddleware {
return Ok(());
}
// Combined extraction: single LLM call produces both memories and structured facts.
// Avoids double LLM extraction ( process_conversation + extract_structured_facts).
match self.growth.extract_combined(
&ctx.agent_id,
&ctx.messages,
@@ -127,12 +171,14 @@ impl AgentMiddleware for MemoryMiddleware {
facts.len(),
agent_key
);
// Check for evolvable patterns after successful extraction
self.check_and_push_evolution(&ctx.agent_id).await;
}
Ok(None) => {
tracing::debug!("[MemoryMiddleware] No memories or facts extracted");
}
Err(e) => {
// Non-fatal: extraction failure should not affect the response
tracing::warn!("[MemoryMiddleware] Combined extraction failed: {}", e);
}
}

View File

@@ -335,22 +335,6 @@ mod tests {
assert!(!is_extraction_driver_configured());
}
#[test]
fn test_parse_empty_response() {
// We cannot create a real LlmDriver easily in tests, so we test the
// parsing logic via a minimal helper.
struct DummyDriver;
impl TauriExtractionDriver {
fn parse_response_test(
&self,
response_text: &str,
extraction_type: MemoryType,
) -> Vec<ExtractedMemory> {
self.parse_response(response_text, extraction_type)
}
}
}
#[test]
fn test_parse_valid_json_response() {
let response = r#"```json