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
根因: Experience 结构体没有 tool_used 字段,PatternAggregator 从 context 字段提取工具名(语义混淆),导致工具信息不准确。 修复: - experience_store.rs: Experience 添加 tool_used: Option<String> 字段 (#[serde(default)] 兼容旧数据),Experience::new() 初始化为 None - experience_extractor.rs: persist_experiences() 从 ExperienceCandidate 的 tools_used[0] 填充 tool_used,同时填充 industry_context - pattern_aggregator.rs: 改用 tool_used 字段提取工具名,不再误用 context - store_experience() 将 tool_used 加入 keywords 提升搜索命中率
120 lines
3.9 KiB
Rust
120 lines
3.9 KiB
Rust
//! 结构化经验提取器
|
||
//! 从对话中提取 ExperienceCandidate(pain_pattern → solution_steps → outcome)
|
||
//! 持久化到 ExperienceStore
|
||
|
||
use std::sync::Arc;
|
||
|
||
use crate::experience_store::ExperienceStore;
|
||
use crate::types::{CombinedExtraction, Outcome};
|
||
|
||
/// 结构化经验提取器
|
||
/// LLM 调用已由上层 MemoryExtractor 完成,这里只做解析和持久化
|
||
pub struct ExperienceExtractor {
|
||
store: Option<Arc<ExperienceStore>>,
|
||
}
|
||
|
||
impl ExperienceExtractor {
|
||
pub fn new() -> Self {
|
||
Self { store: None }
|
||
}
|
||
|
||
pub fn with_store(mut self, store: Arc<ExperienceStore>) -> Self {
|
||
self.store = Some(store);
|
||
self
|
||
}
|
||
|
||
/// 从 CombinedExtraction 中提取经验并持久化
|
||
/// LLM 调用已由上层完成,这里只做解析和存储
|
||
pub async fn persist_experiences(
|
||
&self,
|
||
agent_id: &str,
|
||
extraction: &CombinedExtraction,
|
||
) -> zclaw_types::Result<usize> {
|
||
let store = match &self.store {
|
||
Some(s) => s,
|
||
None => return Ok(0),
|
||
};
|
||
|
||
let mut count = 0;
|
||
for candidate in &extraction.experiences {
|
||
if candidate.confidence < 0.6 {
|
||
continue;
|
||
}
|
||
let outcome_str = match candidate.outcome {
|
||
Outcome::Success => "success",
|
||
Outcome::Partial => "partial",
|
||
Outcome::Failed => "failed",
|
||
};
|
||
let mut exp = crate::experience_store::Experience::new(
|
||
agent_id,
|
||
&candidate.pain_pattern,
|
||
&candidate.context,
|
||
candidate.solution_steps.clone(),
|
||
outcome_str,
|
||
);
|
||
// 填充 tool_used:取 tools_used 中的第一个作为主要工具
|
||
exp.tool_used = candidate.tools_used.first().cloned();
|
||
exp.industry_context = candidate.industry_context.clone();
|
||
store.store_experience(&exp).await?;
|
||
count += 1;
|
||
}
|
||
Ok(count)
|
||
}
|
||
}
|
||
|
||
impl Default for ExperienceExtractor {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::types::{ExperienceCandidate, Outcome};
|
||
|
||
#[test]
|
||
fn test_extractor_new_without_store() {
|
||
let ext = ExperienceExtractor::new();
|
||
assert!(ext.store.is_none());
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_persist_no_store_returns_zero() {
|
||
let ext = ExperienceExtractor::new();
|
||
let extraction = CombinedExtraction::default();
|
||
let count = ext.persist_experiences("agent1", &extraction).await.unwrap();
|
||
assert_eq!(count, 0);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_persist_filters_low_confidence() {
|
||
let viking = Arc::new(crate::VikingAdapter::in_memory());
|
||
let store = Arc::new(ExperienceStore::new(viking));
|
||
let ext = ExperienceExtractor::new().with_store(store);
|
||
|
||
let mut extraction = CombinedExtraction::default();
|
||
extraction.experiences.push(ExperienceCandidate {
|
||
pain_pattern: "low confidence task".to_string(),
|
||
context: "should be filtered".to_string(),
|
||
solution_steps: vec!["step1".to_string()],
|
||
outcome: Outcome::Success,
|
||
confidence: 0.3, // 低于 0.6 阈值
|
||
tools_used: vec![],
|
||
industry_context: None,
|
||
});
|
||
extraction.experiences.push(ExperienceCandidate {
|
||
pain_pattern: "high confidence task".to_string(),
|
||
context: "should be stored".to_string(),
|
||
solution_steps: vec!["step1".to_string(), "step2".to_string()],
|
||
outcome: Outcome::Success,
|
||
confidence: 0.9,
|
||
tools_used: vec!["researcher".to_string()],
|
||
industry_context: Some("healthcare".to_string()),
|
||
});
|
||
|
||
let count = ext.persist_experiences("agent-1", &extraction).await.unwrap();
|
||
assert_eq!(count, 1); // 只有 1 个通过置信度过滤
|
||
}
|
||
}
|