Files
aiops/backend/pkg/agentcli/evolver/memory.go
T
Logan 1acb0c49af feat: Agent CLI 与终端审计回放全面优化
- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏
- 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复
- 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示
- 审计历史页只读(移除删除),移除回放弹框录制搜索
2026-07-01 12:13:53 +08:00

262 lines
6.8 KiB
Go
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.
// Package evolver 实现 Agent CLI 的自进化引擎(Evolver)。 // Logan.Li
// 设计参考 opsxcli internal/agent/evolver,保留四层记忆 + 进化循环思想,精简适配为后端库:
// - 记忆持久化用文件(data/agentcli/*.jsonl),与 opsxcli 原生一致,零 DB 依赖。
// - 进化循环异步执行,失败优雅降级,绝不影响主 ReAct 循环。
// - LLM 可用时用于提炼经验/事实;不可用时退化为规则生成。
package evolver
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
// ---- 基础文件存储 ---- // Logan.Li
// ensureDir 确保目录存在 // Logan.Li
func ensureDir(dir string) error { return os.MkdirAll(dir, 0o755) }
// readJSONL 读取 JSONL 文件为切片 // Logan.Li
func readJSONL[T any](path string) []T {
raw, err := os.ReadFile(path)
if err != nil || len(raw) == 0 {
return nil
}
var out []T
for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
var item T
if json.Unmarshal([]byte(line), &item) == nil {
out = append(out, item)
}
}
return out
}
// appendJSONL 追加一行 JSON // Logan.Li
func appendJSONL[T any](path string, item T) error {
b, err := json.Marshal(item)
if err != nil {
return err
}
b = append(b, '\n')
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(b)
return err
}
// readJSON 读取整个 JSON 文件(失败返回默认值) // Logan.Li
func readJSON[T any](path string, def T) T {
raw, err := os.ReadFile(path)
if err != nil {
return def
}
var v T
if json.Unmarshal(raw, &v) != nil {
return def
}
return v
}
// writeJSON 写入整个 JSON 文件 // Logan.Li
func writeJSON[T any](path string, v T) error {
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, b, 0o644)
}
// ---- 四层记忆 ---- // Logan.Li
// ExperienceEntry 经验记忆条目(最核心,直接注入下次 Prompt) // Logan.Li
type ExperienceEntry struct {
TaskType string `json:"task_type"`
Query string `json:"query"`
Hint string `json:"hint"` // 经验提示
ToolSequence []string `json:"tool_sequence"` // 推荐工具序列
Success bool `json:"success"`
Timestamp int64 `json:"timestamp"`
}
// FactEntry 事实记忆条目(持久事实) // Logan.Li
type FactEntry struct {
Fact string `json:"fact"`
Source string `json:"source"`
Timestamp int64 `json:"timestamp"`
}
// SkillEntry 程序记忆条目(运维技能/流程) // Logan.Li
type SkillEntry struct {
Name string `json:"name"`
When string `json:"when"`
Steps []string `json:"steps"`
Timestamp int64 `json:"timestamp"`
}
// Memories 四层记忆集合(线程安全) // Logan.Li
type Memories struct {
dir string
mu sync.Mutex
expPath string // experience.jsonl
facPath string // factual.jsonl
sklPath string // procedural.jsonl
envPath string // environment.json
}
// NewMemories 创建四层记忆(dir 为 data/agentcli) // Logan.Li
func NewMemories(dir string) *Memories {
return &Memories{
dir: dir,
expPath: filepath.Join(dir, "experience.jsonl"),
facPath: filepath.Join(dir, "factual.jsonl"),
sklPath: filepath.Join(dir, "procedural.jsonl"),
envPath: filepath.Join(dir, "environment.json"),
}
}
// AddExperience 追加经验 // Logan.Li
func (m *Memories) AddExperience(e ExperienceEntry) error {
m.mu.Lock()
defer m.mu.Unlock()
if e.Timestamp == 0 {
e.Timestamp = time.Now().Unix()
}
return appendJSONL(m.expPath, e)
}
// FindExperiences 按 task_type 查找最近的经验(最多 n 条,优先成功) // Logan.Li
func (m *Memories) FindExperiences(taskType string, n int) []ExperienceEntry {
all := readJSONL[ExperienceEntry](m.expPath)
var matched []ExperienceEntry
for _, e := range all {
if taskType == "" || e.TaskType == taskType {
matched = append(matched, e)
}
}
// 优先成功经验,按时间倒序取前 n
successFirst := make([]ExperienceEntry, 0, len(matched))
others := make([]ExperienceEntry, 0, len(matched))
for _, e := range matched {
if e.Success {
successFirst = append(successFirst, e)
} else {
others = append(others, e)
}
}
// 简单逆序(文件本身就是时间顺序,取末尾最新)
pick := func(s []ExperienceEntry) []ExperienceEntry {
if len(s) <= n {
return s
}
return s[len(s)-n:]
}
out := append(pick(successFirst), pick(others)...)
if len(out) > n {
out = out[len(out)-n:]
}
return out
}
// AddFact 追加事实 // Logan.Li
func (m *Memories) AddFact(f FactEntry) error {
m.mu.Lock()
defer m.mu.Unlock()
if f.Timestamp == 0 {
f.Timestamp = time.Now().Unix()
}
return appendJSONL(m.facPath, f)
}
// RelevantFacts 返回与 query 关键词相关的事实(最多 n 条) // Logan.Li
func (m *Memories) RelevantFacts(query string, n int) []FactEntry {
all := readJSONL[FactEntry](m.facPath)
if len(all) == 0 {
return nil
}
kw := strings.ToLower(query)
var out []FactEntry
for _, f := range all {
if strings.Contains(strings.ToLower(f.Fact), kw) || strings.Contains(kw, strings.ToLower(f.Fact)) {
out = append(out, f)
}
}
if len(out) > n {
out = out[len(out)-n:]
}
return out
}
// AddSkill 追加技能 // Logan.Li
func (m *Memories) AddSkill(s SkillEntry) error {
m.mu.Lock()
defer m.mu.Unlock()
if s.Timestamp == 0 {
s.Timestamp = time.Now().Unix()
}
return appendJSONL(m.sklPath, s)
}
// KnownHosts 已知主机(环境记忆) // Logan.Li
type KnownHosts struct {
Hosts []string `json:"hosts"`
}
// AddHost 记录已知主机(去重) // Logan.Li
func (m *Memories) AddHost(host string) error {
if host == "" {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
env := readJSON(m.envPath, KnownHosts{})
for _, h := range env.Hosts {
if h == host {
return nil
}
}
env.Hosts = append(env.Hosts, host)
return writeJSON(m.envPath, env)
}
// Hosts 返回已知主机列表 // Logan.Li
func (m *Memories) Hosts() []string {
return readJSON(m.envPath, KnownHosts{}).Hosts
}
// FindSkills 按任务类型查找程序记忆(运维流程,最多 n 条) // Logan.Li
func (m *Memories) FindSkills(taskType string, n int) []SkillEntry {
all := readJSONL[SkillEntry](m.sklPath)
var matched []SkillEntry
for _, s := range all {
if taskType == "" || s.When == taskType {
matched = append(matched, s)
}
}
if len(matched) > n {
matched = matched[len(matched)-n:]
}
return matched
}
// CountStats 统计各层记忆条目数 // Logan.Li
func (m *Memories) CountStats() map[string]int {
return map[string]int{
"experience": len(readJSONL[ExperienceEntry](m.expPath)),
"factual": len(readJSONL[FactEntry](m.facPath)),
"skill": len(readJSONL[SkillEntry](m.sklPath)),
"hosts": len(readJSON(m.envPath, KnownHosts{}).Hosts),
}
}