Files
aiops/backend/pkg/agentcli/evolver/memory.go
T
attackerandClaude 92a2247390 feat: 新增 Agent CLI — Web 版 AI 运维 Agent(参考 opsxcli 移植)
后端引擎 backend/pkg/agentcli/(纯引擎库,零业务依赖,剥离 CLI/TUI 外壳):
- llm: OpenAI 兼容客户端,复用 model.LLMConfig
- tools: ssh_execute(复用 pkg/remoteexec)+ analyze_output + 风险评估黑名单
- safety: 三级安全模式 + 五级风险 + WebSocket 异步确认
- prompt: 五层 System Prompt
- core: ReAct 16 轮循环 + 自适应检查点 + 循环检测
- evolver: 四层记忆 + 进化循环,data/agentcli/ 文件存储

集成层:
- controller/agent_cli.go: WebSocket 控制器(事件流推流 + 高危确认 + 会话持久化)
- model/agent_cli.go: agent_cli_sessions/messages 表 + AutoMigrate
- 路由 /ws/agent-cli + /api/v1/aiops/agent-cli/sessions
- seed 权限 aiops:agent-cli / aiops:llm-config

前端:
- AgentCLI.tsx: 对话流 + 工具块 + 高危确认 + 四模块 tips 面板
- LLMConfig.tsx: 大模型设置独立页
- api/agentCli.ts + Sidebar/App 菜单路由

文档:
- docs/agent/ 11 篇(opsxcli 同名文档适配改写 + 00-README 映射总览)

其他:
- CLAUDE.md 补充 agent-cli 模块说明
- 修正 .gitignore 根级二进制规则(前导 /,避免误伤 frontend/src/components/aiops/)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-27 09:55:17 +08:00

247 lines
6.4 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
}
// 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),
}
}