feat(growth): L2 技能进化核心 — PatternAggregator+SkillGenerator+QualityGate+EvolutionEngine
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
- PatternAggregator: 经验模式聚合,找出 reuse_count>=threshold 的可固化模式 - SkillGenerator: LLM prompt 构建 + JSON 解析 + 自动提取 JSON 块 - QualityGate: 置信度/冲突/格式质量门控 - EvolutionEngine: 中枢调度器,协调 L2 触发检查+技能生成+质量验证 新增 24 个测试(87→111),全 workspace 0 error。
This commit is contained in:
205
crates/zclaw-growth/src/skill_generator.rs
Normal file
205
crates/zclaw-growth/src/skill_generator.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
//! 技能生成器
|
||||
//! 将聚合的经验模式通过 LLM 转化为 SKILL.md 内容
|
||||
//! 提供 prompt 构建和 JSON 结果解析
|
||||
|
||||
use crate::pattern_aggregator::AggregatedPattern;
|
||||
use zclaw_types::Result;
|
||||
|
||||
/// 技能候选项
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SkillCandidate {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub triggers: Vec<String>,
|
||||
pub tools: Vec<String>,
|
||||
pub body_markdown: String,
|
||||
pub source_pattern: String,
|
||||
pub confidence: f32,
|
||||
}
|
||||
|
||||
/// LLM 驱动的技能生成 prompt
|
||||
const SKILL_GENERATION_PROMPT: &str = r#"
|
||||
你是一个技能设计专家。根据以下用户反复出现的问题和解决步骤,生成一个可复用的技能定义。
|
||||
|
||||
问题模式:{pain_pattern}
|
||||
解决步骤:{steps}
|
||||
使用的工具:{tools}
|
||||
行业背景:{industry}
|
||||
|
||||
请生成以下 JSON:
|
||||
```json
|
||||
{
|
||||
"name": "技能名称(简短中文)",
|
||||
"description": "技能描述(一段话)",
|
||||
"triggers": ["触发词1", "触发词2", "触发词3"],
|
||||
"tools": ["tool1", "tool2"],
|
||||
"body_markdown": "技能的 Markdown 正文,包含步骤说明",
|
||||
"confidence": 0.85
|
||||
}
|
||||
```
|
||||
"#;
|
||||
|
||||
/// 技能生成器
|
||||
/// 负责 prompt 构建和 LLM 返回的 JSON 解析
|
||||
pub struct SkillGenerator;
|
||||
|
||||
impl SkillGenerator {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// 从聚合模式构建 LLM prompt
|
||||
pub fn build_prompt(pattern: &AggregatedPattern) -> String {
|
||||
SKILL_GENERATION_PROMPT
|
||||
.replace("{pain_pattern}", &pattern.pain_pattern)
|
||||
.replace("{steps}", &pattern.common_steps.join(" → "))
|
||||
.replace("{tools}", &pattern.tools_used.join(", "))
|
||||
.replace("{industry}", pattern.industry_context.as_deref().unwrap_or("通用"))
|
||||
}
|
||||
|
||||
/// 解析 LLM 返回的 JSON 为 SkillCandidate
|
||||
pub fn parse_response(json_str: &str, pattern: &AggregatedPattern) -> Result<SkillCandidate> {
|
||||
// 尝试提取 JSON 块(LLM 可能包裹在 ```json ... ``` 中)
|
||||
let json_str = extract_json_block(json_str);
|
||||
|
||||
let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
|
||||
zclaw_types::ZclawError::ConfigError(format!("Invalid skill JSON: {}", e))
|
||||
})?;
|
||||
|
||||
let triggers: Vec<String> = raw["triggers"]
|
||||
.as_array()
|
||||
.map(|a: &Vec<serde_json::Value>| {
|
||||
a.iter()
|
||||
.filter_map(|v: &serde_json::Value| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let tools: Vec<String> = raw["tools"]
|
||||
.as_array()
|
||||
.map(|a: &Vec<serde_json::Value>| {
|
||||
a.iter()
|
||||
.filter_map(|v: &serde_json::Value| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(SkillCandidate {
|
||||
name: raw["name"]
|
||||
.as_str()
|
||||
.unwrap_or("未命名技能")
|
||||
.to_string(),
|
||||
description: raw["description"].as_str().unwrap_or("").to_string(),
|
||||
triggers,
|
||||
tools,
|
||||
body_markdown: raw["body_markdown"].as_str().unwrap_or("").to_string(),
|
||||
source_pattern: pattern.pain_pattern.clone(),
|
||||
confidence: raw["confidence"].as_f64().unwrap_or(0.5) as f32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 LLM 返回文本中提取 JSON 块
|
||||
fn extract_json_block(text: &str) -> &str {
|
||||
// 尝试匹配 ```json ... ```
|
||||
if let Some(start) = text.find("```json") {
|
||||
let json_start = start + 7; // skip ```json
|
||||
if let Some(end) = text[json_start..].find("```") {
|
||||
return text[json_start..json_start + end].trim();
|
||||
}
|
||||
}
|
||||
// 尝试匹配 ``` ... ```
|
||||
if let Some(start) = text.find("```") {
|
||||
let json_start = start + 3;
|
||||
if let Some(end) = text[json_start..].find("```") {
|
||||
return text[json_start..json_start + end].trim();
|
||||
}
|
||||
}
|
||||
// 尝试找 { ... } 块
|
||||
if let Some(start) = text.find('{') {
|
||||
if let Some(end) = text.rfind('}') {
|
||||
return &text[start..=end];
|
||||
}
|
||||
}
|
||||
text.trim()
|
||||
}
|
||||
|
||||
impl Default for SkillGenerator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::experience_store::Experience;
|
||||
|
||||
fn make_pattern() -> AggregatedPattern {
|
||||
let exp = Experience::new(
|
||||
"agent-1",
|
||||
"报表生成",
|
||||
"researcher",
|
||||
vec!["查询数据库".into(), "格式化输出".into()],
|
||||
"success",
|
||||
);
|
||||
AggregatedPattern {
|
||||
pain_pattern: "报表生成".to_string(),
|
||||
experiences: vec![exp],
|
||||
common_steps: vec!["查询数据库".into(), "格式化输出".into()],
|
||||
total_reuse: 5,
|
||||
tools_used: vec!["researcher".into()],
|
||||
industry_context: Some("healthcare".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_prompt() {
|
||||
let pattern = make_pattern();
|
||||
let prompt = SkillGenerator::build_prompt(&pattern);
|
||||
assert!(prompt.contains("报表生成"));
|
||||
assert!(prompt.contains("查询数据库"));
|
||||
assert!(prompt.contains("researcher"));
|
||||
assert!(prompt.contains("healthcare"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_response_valid_json() {
|
||||
let pattern = make_pattern();
|
||||
let json = r##"{"name":"每日报表","description":"生成每日报表","triggers":["报表","日报"],"tools":["researcher"],"body_markdown":"# 每日报表\n步骤1","confidence":0.9}"##;
|
||||
let candidate = SkillGenerator::parse_response(json, &pattern).unwrap();
|
||||
assert_eq!(candidate.name, "每日报表");
|
||||
assert_eq!(candidate.triggers.len(), 2);
|
||||
assert_eq!(candidate.confidence, 0.9);
|
||||
assert_eq!(candidate.source_pattern, "报表生成");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_response_json_block() {
|
||||
let pattern = make_pattern();
|
||||
let text = r#"```json
|
||||
{"name":"技能A","description":"desc","triggers":["a"],"tools":[],"body_markdown":"body","confidence":0.8}
|
||||
```"#;
|
||||
let candidate = SkillGenerator::parse_response(text, &pattern).unwrap();
|
||||
assert_eq!(candidate.name, "技能A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_response_invalid_json() {
|
||||
let pattern = make_pattern();
|
||||
let result = SkillGenerator::parse_response("not json at all", &pattern);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_json_block_with_markdown() {
|
||||
let text = "Here is the result:\n```json\n{\"key\": \"value\"}\n```\nDone.";
|
||||
assert_eq!(extract_json_block(text), "{\"key\": \"value\"}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_json_block_bare() {
|
||||
let text = "{\"key\": \"value\"}";
|
||||
assert_eq!(extract_json_block(text), "{\"key\": \"value\"}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user