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 (反映推断不确定性)
449 lines
14 KiB
Rust
449 lines
14 KiB
Rust
//! 反馈信号收集与信任度管理(Phase 5 反馈闭环)
|
||
//! 收集用户对进化产物(技能/Pipeline)的显式/隐式反馈
|
||
//! 管理信任度衰减和优化循环
|
||
//! 信任度记录通过 VikingAdapter 持久化
|
||
|
||
use std::collections::HashMap;
|
||
use std::sync::Arc;
|
||
|
||
use chrono::{DateTime, Utc};
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use crate::types::MemoryType;
|
||
use crate::viking_adapter::VikingAdapter;
|
||
|
||
/// 反馈信号类型
|
||
#[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>,
|
||
}
|
||
|
||
/// 反馈收集器
|
||
/// 管理反馈记录和信任度评分
|
||
/// 通过 VikingAdapter 持久化信任度记录(可选)
|
||
pub struct FeedbackCollector {
|
||
trust_records: HashMap<String, TrustRecord>,
|
||
viking: Option<Arc<VikingAdapter>>,
|
||
/// 是否已从持久化存储加载信任度记录
|
||
loaded: bool,
|
||
}
|
||
|
||
impl FeedbackCollector {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
trust_records: HashMap::new(),
|
||
viking: None,
|
||
loaded: false,
|
||
}
|
||
}
|
||
|
||
/// 创建带 VikingAdapter 的 FeedbackCollector
|
||
pub fn with_viking(viking: Arc<VikingAdapter>) -> Self {
|
||
Self {
|
||
trust_records: HashMap::new(),
|
||
viking: Some(viking),
|
||
loaded: false,
|
||
}
|
||
}
|
||
|
||
/// 从 VikingAdapter 加载已持久化的信任度记录
|
||
pub async fn load(&mut self) -> Result<usize, String> {
|
||
let viking = match &self.viking {
|
||
Some(v) => v,
|
||
None => return Ok(0),
|
||
};
|
||
|
||
// MemoryEntry::new("feedback", Session, artifact_id) 生成
|
||
// URI: agent://feedback/sessions/{artifact_id}
|
||
let entries = viking
|
||
.find_by_prefix("agent://feedback/sessions/")
|
||
.await
|
||
.map_err(|e| format!("Failed to load trust records: {}", e))?;
|
||
|
||
let mut count = 0;
|
||
for entry in entries {
|
||
match serde_json::from_str::<TrustRecord>(&entry.content) {
|
||
Ok(record) => {
|
||
// 只合并不覆盖:保留内存中的较新记录
|
||
self.trust_records
|
||
.entry(record.artifact_id.clone())
|
||
.or_insert(record);
|
||
count += 1;
|
||
}
|
||
Err(e) => {
|
||
tracing::warn!(
|
||
"[FeedbackCollector] Failed to deserialize trust record at {}: {}",
|
||
entry.uri,
|
||
e
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
tracing::debug!(
|
||
"[FeedbackCollector] Loaded {} trust records from storage",
|
||
count
|
||
);
|
||
Ok(count)
|
||
}
|
||
|
||
/// 将信任度记录持久化到 VikingAdapter
|
||
/// 首次调用时自动从存储加载已有记录,避免覆盖
|
||
pub async fn save(&mut self) -> Result<usize, String> {
|
||
// 首次保存前自动加载已有记录,防止丢失历史数据
|
||
if !self.loaded {
|
||
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
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
let viking = match &self.viking {
|
||
Some(v) => v,
|
||
None => return Ok(0),
|
||
};
|
||
|
||
let mut saved = 0;
|
||
for record in self.trust_records.values() {
|
||
let content = match serde_json::to_string(record) {
|
||
Ok(c) => c,
|
||
Err(e) => {
|
||
tracing::warn!(
|
||
"[FeedbackCollector] Failed to serialize trust record {}: {}",
|
||
record.artifact_id,
|
||
e
|
||
);
|
||
continue;
|
||
}
|
||
};
|
||
let entry = crate::types::MemoryEntry::new(
|
||
"feedback",
|
||
MemoryType::Session,
|
||
&record.artifact_id,
|
||
content,
|
||
)
|
||
.with_importance((record.trust_score * 10.0) as u8);
|
||
|
||
match viking.store(&entry).await {
|
||
Ok(_) => saved += 1,
|
||
Err(e) => {
|
||
tracing::warn!(
|
||
"[FeedbackCollector] Failed to save trust record {}: {}",
|
||
record.artifact_id,
|
||
e
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
tracing::debug!(
|
||
"[FeedbackCollector] Saved {} trust records to storage",
|
||
saved
|
||
);
|
||
Ok(saved)
|
||
}
|
||
|
||
/// 提交一条反馈
|
||
pub fn submit_feedback(&mut self, entry: FeedbackEntry) -> TrustUpdate {
|
||
let record = self
|
||
.trust_records
|
||
.entry(entry.artifact_id.clone())
|
||
.or_insert_with(|| TrustRecord {
|
||
artifact_id: entry.artifact_id.clone(),
|
||
artifact_type: entry.artifact_type.clone(),
|
||
trust_score: 0.5,
|
||
total_feedback: 0,
|
||
positive_count: 0,
|
||
negative_count: 0,
|
||
last_updated: Utc::now(),
|
||
});
|
||
|
||
// 更新计数
|
||
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.get(artifact_id)
|
||
}
|
||
|
||
/// 获取所有需要优化的产物(信任度 < 0.4)
|
||
pub fn get_artifacts_needing_optimization(&self) -> Vec<&TrustRecord> {
|
||
self.trust_records
|
||
.values()
|
||
.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
|
||
.values()
|
||
.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
|
||
.values()
|
||
.filter(|r| r.trust_score >= 0.8)
|
||
.collect()
|
||
}
|
||
|
||
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();
|
||
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);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_save_and_load_roundtrip() {
|
||
let viking = Arc::new(crate::VikingAdapter::in_memory());
|
||
|
||
// 写入阶段
|
||
let mut collector = FeedbackCollector::with_viking(viking.clone());
|
||
collector.submit_feedback(make_feedback("skill-a", Sentiment::Positive));
|
||
collector.submit_feedback(make_feedback("skill-a", Sentiment::Positive));
|
||
collector.submit_feedback(make_feedback("skill-b", Sentiment::Negative));
|
||
|
||
let saved = collector.save().await.unwrap();
|
||
assert_eq!(saved, 2); // 2 个 artifact
|
||
|
||
// 读取阶段:新 collector 从存储加载
|
||
let mut collector2 = FeedbackCollector::with_viking(viking);
|
||
let loaded = collector2.load().await.unwrap();
|
||
assert_eq!(loaded, 2);
|
||
|
||
let record_a = collector2.get_trust("skill-a").unwrap();
|
||
assert_eq!(record_a.positive_count, 2);
|
||
assert_eq!(record_a.total_feedback, 2);
|
||
|
||
let record_b = collector2.get_trust("skill-b").unwrap();
|
||
assert_eq!(record_b.negative_count, 1);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_load_without_viking_returns_zero() {
|
||
let mut collector = FeedbackCollector::new();
|
||
let loaded = collector.load().await.unwrap();
|
||
assert_eq!(loaded, 0);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_save_without_viking_returns_zero() {
|
||
let mut collector = FeedbackCollector::new();
|
||
let saved = collector.save().await.unwrap();
|
||
assert_eq!(saved, 0);
|
||
}
|
||
}
|