package evolve // SkillLearner 让 Agent 将成功的工作流保存为可复用技能. // // 原项目中,技能是人类手写的 markdown 文件(.flyto/skills/). // C 方案让 Agent 自己发现和保存技能: // - Agent 完成一个复杂任务后,分析整个过程 // - 提取关键步骤,使用的工具,决策逻辑 // - 生成一个结构化的技能定义 // - 下次遇到类似问题时自动调用 // // 技能 vs 工具的区别: // - 工具是原子操作(执行一个命令,读一个文件) // - 技能是工作流模板(包含多个步骤,决策分支,提示词模板) // - 工具由 Engine 调度,技能由模型在系统提示中看到后自主执行 import ( "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "time" ) // SkillLearner 负责技能学习和管理. type SkillLearner struct { store *EvolutionStore maxPerSession int learned int } // NewSkillLearner 创建技能学习器. func NewSkillLearner(store *EvolutionStore, maxPerSession int) *SkillLearner { return &SkillLearner{ store: store, maxPerSession: maxPerSession, } } // SkillDefinition 是一个技能定义. type SkillDefinition struct { // Name 技能名称 Name string `json:"name"` // Description 简短描述 Description string `json:"description"` // WhenToUse 什么时候应该使用这个技能(模型看到此信息后自动判断) WhenToUse string `json:"when_to_use"` // Steps 技能的执行步骤 Steps []SkillStep `json:"steps"` // Prompt 完整的提示词模板(注入到系统提示中) Prompt string `json:"prompt"` // RequiredTools 需要的工具列表 RequiredTools []string `json:"required_tools,omitempty"` // Tags 分类标签 Tags []string `json:"tags,omitempty"` // LearnedFrom 学习来源 LearnedFrom *LearningSource `json:"learned_from,omitempty"` // SuccessRate 成功率(Agent 自我评估) SuccessRate float64 `json:"success_rate"` // UsageCount 被使用的次数 UsageCount int `json:"usage_count"` // Version 版本号 Version int `json:"version"` // CreatedAt 创建时间 CreatedAt time.Time `json:"created_at"` // UpdatedAt 最后更新时间 UpdatedAt time.Time `json:"updated_at"` } // SkillStep 是技能的一个执行步骤. type SkillStep struct { // Order 步骤顺序 Order int `json:"order"` // Description 步骤描述 Description string `json:"description"` // ToolName 使用的工具(可选) ToolName string `json:"tool_name,omitempty"` // InputTemplate 工具输入模板(支持变量替换) InputTemplate string `json:"input_template,omitempty"` // Condition 执行条件(可选,如"前一步失败时") Condition string `json:"condition,omitempty"` // Fallback 失败时的回退步骤 Fallback string `json:"fallback,omitempty"` } // LearningSource 记录技能是从哪里学来的. type LearningSource struct { // SessionID 来源会话 ID SessionID string `json:"session_id"` // TaskDescription 原始任务描述 TaskDescription string `json:"task_description"` // TurnCount 完成任务用了多少轮 TurnCount int `json:"turn_count"` // ToolsUsed 使用了哪些工具 ToolsUsed []string `json:"tools_used"` // Timestamp 学习时间 Timestamp time.Time `json:"timestamp"` } // Apply 执行技能学习提案. func (sl *SkillLearner) Apply(ctx context.Context, proposal *EvolutionProposal) error { if sl.learned >= sl.maxPerSession { return fmt.Errorf("skill_learner: session limit reached (%d/%d)", sl.learned, sl.maxPerSession) } data, err := json.Marshal(proposal.Content) if err != nil { return fmt.Errorf("skill_learner: marshal content: %w", err) } var def SkillDefinition if err := json.Unmarshal(data, &def); err != nil { return fmt.Errorf("skill_learner: unmarshal definition: %w", err) } if def.CreatedAt.IsZero() { def.CreatedAt = time.Now() } def.UpdatedAt = time.Now() if err := sl.save(&def); err != nil { return fmt.Errorf("skill_learner: save: %w", err) } sl.learned++ return nil } // save 持久化技能定义. func (sl *SkillLearner) save(def *SkillDefinition) error { data, err := json.MarshalIndent(def, "", " ") if err != nil { return err } path := filepath.Join(sl.store.dir, "skills", def.Name+".json") return os.WriteFile(path, data, 0644) } // LoadAll 加载所有已学习的技能. // // 升华改进(ELEVATED): 返回 ([]*SkillDefinition, []error) 双清单而非单一 error. // 原方案:部分失败只记录 firstSkipErr,有部分成功则静默丢弃错误, // 全部失败才返回单个错误--调用方无法知道哪些文件损坏. // 新方案:每个失败文件产生独立 error,成功和失败信息并行返回,调用方自行决策. // 替代方案:<返回 (results, error) 聚合> - 否决原因:单一 error 掩盖了具体哪些文件有问题, // 且无法区分"一个文件损坏"和"五个文件损坏". func (sl *SkillLearner) LoadAll() ([]*SkillDefinition, []error) { dir := filepath.Join(sl.store.dir, "skills") entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, []error{err} } var skills []*SkillDefinition var errs []error for _, entry := range entries { if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" { continue } data, err := os.ReadFile(filepath.Join(dir, entry.Name())) if err != nil { errs = append(errs, fmt.Errorf("read %s: %w", entry.Name(), err)) continue } var def SkillDefinition if err := json.Unmarshal(data, &def); err != nil { errs = append(errs, fmt.Errorf("parse %s: %w", entry.Name(), err)) continue } skills = append(skills, &def) } return skills, errs } // RecordUsage 记录技能被使用. func (sl *SkillLearner) RecordUsage(name string, success bool) error { skills, errs := sl.LoadAll() // 精妙之处(CLEVER): 即使有文件损坏,只要找到目标技能就可以继续操作. // 只有在完全无可用技能时才聚合上报错误,避免个别损坏文件阻断全部操作. if len(skills) == 0 && len(errs) > 0 { return errors.Join(errs...) } for _, skill := range skills { if skill.Name == name { skill.UsageCount++ // 简单的指数移动平均更新成功率 if success { skill.SuccessRate = skill.SuccessRate*0.8 + 0.2 } else { skill.SuccessRate = skill.SuccessRate * 0.8 } skill.UpdatedAt = time.Now() return sl.save(skill) } } return nil } // FormatForSystemPrompt 将所有技能格式化为系统提示词片段. // Engine 在构建系统提示时调用此方法,让模型知道有哪些可用技能. func (sl *SkillLearner) FormatForSystemPrompt() (string, error) { skills, errs := sl.LoadAll() if len(skills) == 0 { if len(errs) > 0 { return "", errors.Join(errs...) } return "", nil } result := "\n# Learned Skills\n\n" result += "The following skills were learned from previous sessions. Use them when applicable:\n\n" for _, skill := range skills { result += fmt.Sprintf("## %s\n", skill.Name) result += fmt.Sprintf("When to use: %s\n", skill.WhenToUse) result += fmt.Sprintf("Success rate: %.0f%% (%d uses)\n", skill.SuccessRate*100, skill.UsageCount) if skill.Prompt != "" { result += fmt.Sprintf("\n%s\n\n", skill.Prompt) } } return result, nil }