Files
zclaw_openfang/crates/zclaw-growth/src/skill_generator.rs
iven cb727fdcc7
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
fix(growth): 二次审计修复 — 6项 CRITICAL/HIGH/MEDIUM 全部修复
CRITICAL-1/2: json_utils 花括号匹配改为括号平衡算法
  - 处理字符串字面量中的花括号和转义引号
  - 新增 5 个测试(平衡匹配、字符串内花括号、转义引号、extract_string_array)

HIGH-4: EvolutionMiddleware 只取第一个事件(remove(0)),不丢弃后续
HIGH-5: EvolutionMiddleware 先 read() 判空再 write(),减少锁竞争
HIGH-7: from_experience_store 使用传入 store 的 viking 实例(不再忽略参数)
  - ExperienceStore 新增 viking() getter

MEDIUM-9: skill_generator + workflow_composer JSON 数组解析去重
  - 新增 json_utils::extract_string_array() 共享函数
MEDIUM-14: EvolutionMiddleware 注入文本去除多余缩进空格

测试: zclaw-growth 133 tests, zclaw-runtime 87 tests, workspace 0 failures
2026-04-18 22:30:10 +08:00

165 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 技能生成器
//! 将聚合的经验模式通过 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,
/// 技能版本号,用于后续迭代追踪
pub version: u32,
}
/// 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> {
let json_str = crate::json_utils::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))
})?;
Ok(SkillCandidate {
name: raw["name"]
.as_str()
.unwrap_or("未命名技能")
.to_string(),
description: raw["description"].as_str().unwrap_or("").to_string(),
triggers: crate::json_utils::extract_string_array(&raw, "triggers"),
tools: crate::json_utils::extract_string_array(&raw, "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,
version: raw["version"].as_u64().unwrap_or(1) as u32,
})
}
}
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!(crate::json_utils::extract_json_block(text), "{\"key\": \"value\"}");
}
#[test]
fn test_extract_json_block_bare() {
let text = "{\"key\": \"value\"}";
assert_eq!(crate::json_utils::extract_json_block(text), "{\"key\": \"value\"}");
}
}