feat: Evolution Engine Phase 3-5 — WorkflowComposer+FeedbackCollector+EvolutionMiddleware+反馈闭环
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
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
Phase 3: - EvolutionMiddleware (priority 78): 管家对话中注入进化确认提示 - GrowthIntegration.check_evolution() API 串入 Phase 4: - WorkflowComposer: 轨迹工具链模式聚类 + Pipeline YAML prompt 构建 + JSON 解析 - EvolutionEngine.analyze_trajectory_patterns() L3 入口 Phase 5: - FeedbackCollector: 反馈信号收集 + 信任度管理 + 推荐(Optimize/Archive/Promote) - EvolutionEngine 反馈闭环方法: submit_feedback/get_artifacts_needing_optimization 新增 12 个测试(111→123),全 workspace 701 测试通过。
This commit is contained in:
298
crates/zclaw-growth/src/feedback_collector.rs
Normal file
298
crates/zclaw-growth/src/feedback_collector.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
//! 反馈信号收集与信任度管理(Phase 5 反馈闭环)
|
||||
//! 收集用户对进化产物(技能/Pipeline)的显式/隐式反馈
|
||||
//! 管理信任度衰减和优化循环
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 反馈信号类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum FeedbackSignal {
|
||||
/// 用户直接表达的意见
|
||||
Explicit,
|
||||
/// 从使用行为推断
|
||||
ImplicitUsage,
|
||||
/// 使用频率
|
||||
UsageCount,
|
||||
/// 任务完成率
|
||||
CompletionRate,
|
||||
}
|
||||
|
||||
/// 情感倾向
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum Sentiment {
|
||||
Positive,
|
||||
Negative,
|
||||
Neutral,
|
||||
}
|
||||
|
||||
/// 进化产物类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum EvolutionArtifact {
|
||||
Skill,
|
||||
Pipeline,
|
||||
}
|
||||
|
||||
/// 单条反馈记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FeedbackEntry {
|
||||
pub artifact_id: String,
|
||||
pub artifact_type: EvolutionArtifact,
|
||||
pub signal: FeedbackSignal,
|
||||
pub sentiment: Sentiment,
|
||||
pub details: Option<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 信任度记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrustRecord {
|
||||
pub artifact_id: String,
|
||||
pub artifact_type: EvolutionArtifact,
|
||||
pub trust_score: f32,
|
||||
pub total_feedback: u32,
|
||||
pub positive_count: u32,
|
||||
pub negative_count: u32,
|
||||
pub last_updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 反馈收集器
|
||||
/// 管理反馈记录和信任度评分
|
||||
pub struct FeedbackCollector {
|
||||
/// 信任度记录表(内存,可持久化到 SQLite)
|
||||
trust_records: Vec<TrustRecord>,
|
||||
}
|
||||
|
||||
impl FeedbackCollector {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
trust_records: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交一条反馈
|
||||
pub fn submit_feedback(&mut self, entry: FeedbackEntry) -> TrustUpdate {
|
||||
let record = self.get_or_create_record(&entry.artifact_id, &entry.artifact_type);
|
||||
|
||||
// 更新计数
|
||||
record.total_feedback += 1;
|
||||
match entry.sentiment {
|
||||
Sentiment::Positive => record.positive_count += 1,
|
||||
Sentiment::Negative => record.negative_count += 1,
|
||||
Sentiment::Neutral => {}
|
||||
}
|
||||
|
||||
// 重新计算信任度
|
||||
let old_score = record.trust_score;
|
||||
record.trust_score = Self::calculate_trust_internal(
|
||||
record.positive_count,
|
||||
record.negative_count,
|
||||
record.total_feedback,
|
||||
record.last_updated,
|
||||
);
|
||||
record.last_updated = Utc::now();
|
||||
|
||||
let new_score = record.trust_score;
|
||||
let total = record.total_feedback;
|
||||
let action = Self::recommend_action_internal(new_score, total);
|
||||
|
||||
TrustUpdate {
|
||||
artifact_id: entry.artifact_id.clone(),
|
||||
old_score,
|
||||
new_score,
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取信任度记录
|
||||
pub fn get_trust(&self, artifact_id: &str) -> Option<&TrustRecord> {
|
||||
self.trust_records.iter().find(|r| r.artifact_id == artifact_id)
|
||||
}
|
||||
|
||||
/// 获取所有需要优化的产物(信任度 < 0.4)
|
||||
pub fn get_artifacts_needing_optimization(&self) -> Vec<&TrustRecord> {
|
||||
self.trust_records
|
||||
.iter()
|
||||
.filter(|r| r.trust_score < 0.4 && r.total_feedback >= 2)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 获取所有应该归档的产物(信任度 < 0.2 且反馈 >= 5)
|
||||
pub fn get_artifacts_to_archive(&self) -> Vec<&TrustRecord> {
|
||||
self.trust_records
|
||||
.iter()
|
||||
.filter(|r| r.trust_score < 0.2 && r.total_feedback >= 5)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 获取所有高信任产物(信任度 >= 0.8)
|
||||
pub fn get_recommended_artifacts(&self) -> Vec<&TrustRecord> {
|
||||
self.trust_records
|
||||
.iter()
|
||||
.filter(|r| r.trust_score >= 0.8)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_or_create_record(
|
||||
&mut self,
|
||||
artifact_id: &str,
|
||||
artifact_type: &EvolutionArtifact,
|
||||
) -> &mut TrustRecord {
|
||||
let exists = self
|
||||
.trust_records
|
||||
.iter()
|
||||
.any(|r| r.artifact_id == artifact_id);
|
||||
if !exists {
|
||||
self.trust_records.push(TrustRecord {
|
||||
artifact_id: artifact_id.to_string(),
|
||||
artifact_type: artifact_type.clone(),
|
||||
trust_score: 0.5, // 初始信任度
|
||||
total_feedback: 0,
|
||||
positive_count: 0,
|
||||
negative_count: 0,
|
||||
last_updated: Utc::now(),
|
||||
});
|
||||
}
|
||||
self.trust_records
|
||||
.iter_mut()
|
||||
.find(|r| r.artifact_id == artifact_id)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn calculate_trust_internal(
|
||||
positive: u32,
|
||||
negative: u32,
|
||||
total: u32,
|
||||
last_updated: DateTime<Utc>,
|
||||
) -> f32 {
|
||||
if total == 0 {
|
||||
return 0.5;
|
||||
}
|
||||
let positive_ratio = positive as f32 / total as f32;
|
||||
let negative_penalty = negative as f32 * 0.1;
|
||||
let days_since = (Utc::now() - last_updated).num_days().max(0) as f32;
|
||||
let time_decay = 1.0 - (days_since * 0.005).min(0.5);
|
||||
(positive_ratio * time_decay - negative_penalty).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn recommend_action_internal(trust_score: f32, total_feedback: u32) -> RecommendedAction {
|
||||
if trust_score >= 0.8 {
|
||||
RecommendedAction::Promote
|
||||
} else if trust_score < 0.2 && total_feedback >= 5 {
|
||||
RecommendedAction::Archive
|
||||
} else if trust_score < 0.4 && total_feedback >= 2 {
|
||||
RecommendedAction::Optimize
|
||||
} else {
|
||||
RecommendedAction::Monitor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FeedbackCollector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// 信任度更新结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrustUpdate {
|
||||
pub artifact_id: String,
|
||||
pub old_score: f32,
|
||||
pub new_score: f32,
|
||||
pub action: RecommendedAction,
|
||||
}
|
||||
|
||||
/// 建议动作
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RecommendedAction {
|
||||
/// 继续观察
|
||||
Monitor,
|
||||
/// 需要优化
|
||||
Optimize,
|
||||
/// 建议归档(降级为记忆)
|
||||
Archive,
|
||||
/// 建议提升为推荐技能
|
||||
Promote,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_feedback(artifact_id: &str, sentiment: Sentiment) -> FeedbackEntry {
|
||||
FeedbackEntry {
|
||||
artifact_id: artifact_id.to_string(),
|
||||
artifact_type: EvolutionArtifact::Skill,
|
||||
signal: FeedbackSignal::Explicit,
|
||||
sentiment,
|
||||
details: None,
|
||||
timestamp: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initial_trust() {
|
||||
let collector = FeedbackCollector::new();
|
||||
assert!(collector.get_trust("skill-1").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_positive_feedback_increases_trust() {
|
||||
let mut collector = FeedbackCollector::new();
|
||||
collector.submit_feedback(make_feedback("skill-1", Sentiment::Positive));
|
||||
let record = collector.get_trust("skill-1").unwrap();
|
||||
assert!(record.trust_score > 0.5);
|
||||
assert_eq!(record.positive_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_feedback_decreases_trust() {
|
||||
let mut collector = FeedbackCollector::new();
|
||||
collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative));
|
||||
let record = collector.get_trust("skill-1").unwrap();
|
||||
assert!(record.trust_score < 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_feedback() {
|
||||
let mut collector = FeedbackCollector::new();
|
||||
collector.submit_feedback(make_feedback("skill-1", Sentiment::Positive));
|
||||
collector.submit_feedback(make_feedback("skill-1", Sentiment::Positive));
|
||||
collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative));
|
||||
let record = collector.get_trust("skill-1").unwrap();
|
||||
assert_eq!(record.total_feedback, 3);
|
||||
assert!(record.trust_score > 0.3); // 2/3 positive
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recommend_optimize() {
|
||||
let mut collector = FeedbackCollector::new();
|
||||
// 2 negative → trust < 0.4
|
||||
collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative));
|
||||
let update = collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative));
|
||||
assert_eq!(update.action, RecommendedAction::Optimize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_needs_optimization_filter() {
|
||||
let mut collector = FeedbackCollector::new();
|
||||
collector.submit_feedback(make_feedback("bad-skill", Sentiment::Negative));
|
||||
collector.submit_feedback(make_feedback("bad-skill", Sentiment::Negative));
|
||||
collector.submit_feedback(make_feedback("good-skill", Sentiment::Positive));
|
||||
|
||||
let needs = collector.get_artifacts_needing_optimization();
|
||||
assert_eq!(needs.len(), 1);
|
||||
assert_eq!(needs[0].artifact_id, "bad-skill");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_promote_recommendation() {
|
||||
let mut collector = FeedbackCollector::new();
|
||||
for _ in 0..5 {
|
||||
collector.submit_feedback(make_feedback("great-skill", Sentiment::Positive));
|
||||
}
|
||||
let recommended = collector.get_recommended_artifacts();
|
||||
assert_eq!(recommended.len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user