Files
aiops/backend/pkg/agentcli/evolver/engine.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

272 lines
9.3 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.
// engine.go - 自进化引擎(Evolver)核心 // Logan.Li
// 保留 opsxcli 13 步进化循环的思想,精简为核心步骤:
// OBSERVE → CLASSIFY → LEARN → STORE → MEMORIZE → PREDICT(注入下次 Prompt)。
// 异步执行、失败优雅降级(LLM 不可用退化为规则生成),绝不影响主 ReAct 循环。
package evolver
import (
"context"
"fmt"
"strings"
"time"
"ezops/pkg/agentcli/llm"
)
// ToolCallRecord 工具调用记录(供 Evolver 观察学习) // Logan.Li
type ToolCallRecord struct {
ToolName string `json:"tool_name"`
Args map[string]interface{} `json:"args"`
Output string `json:"output"`
Duration time.Duration `json:"duration"`
Success bool `json:"success"`
RiskLevel int `json:"risk_level"`
}
// TaskExecution 一次任务的完整执行记录 // Logan.Li
type TaskExecution struct {
Query string `json:"query"`
ToolCalls []ToolCallRecord `json:"tool_calls"`
TotalSteps int `json:"total_steps"`
TotalTokens int `json:"total_tokens"`
Duration time.Duration `json:"duration"`
Success bool `json:"success"`
FinalAnswer string `json:"final_answer"`
Timestamp time.Time `json:"timestamp"`
ServerHost string `json:"server_host"` // 用于环境记忆
}
// EvolveResult 进化结果(会话级回灌 Prompt) // Logan.Li
type EvolveResult struct {
TaskType string `json:"task_type"`
LearnedHint string `json:"learned_hint"`
ToolSequence []string `json:"tool_sequence"`
ExperienceAdded bool `json:"experience_added"`
Consolidated bool `json:"consolidated"`
}
// EvolverEngine 自进化引擎 // Logan.Li
type EvolverEngine struct {
mem *Memories
llm llm.Client // 可选,用于提炼经验/事实
memDir string
}
// NewEvolverEngine 创建进化引擎(llm 可为 nil,退化为规则学习) // Logan.Li
func NewEvolverEngine(memDir string, client llm.Client) (*EvolverEngine, error) {
if memDir == "" {
memDir = "data/agentcli"
}
if err := ensureDir(memDir); err != nil {
return nil, fmt.Errorf("初始化记忆目录失败: %w", err)
}
return &EvolverEngine{
mem: NewMemories(memDir),
llm: client,
memDir: memDir,
}, nil
}
// Evolve 执行一次进化(观察任务 → 学习经验 → 存储) // Logan.Li
// 设计为幂等、可降级:任何子步骤失败都不返回 error 给调用方(仅影响学习质量)。
func (e *EvolverEngine) Evolve(ctx context.Context, exec *TaskExecution) *EvolveResult {
if exec == nil {
return nil
}
result := &EvolveResult{}
// Step 1: OBSERVE —— 提取工具序列
for _, tc := range exec.ToolCalls {
result.ToolSequence = append(result.ToolSequence, tc.ToolName)
}
// Step 2: CLASSIFY —— 任务类型分类(关键词规则,无需 LLM)
result.TaskType = classifyTask(exec.Query, exec.ToolCalls)
// Step 3: LEARN —— 生成经验 hint(优先 LLM,降级规则)
result.LearnedHint = e.learnHint(ctx, exec, result.TaskType)
// Step 4: STORE —— 存入经验记忆 + 环境记忆(主机)
exp := ExperienceEntry{
TaskType: result.TaskType,
Query: truncate(exec.Query, 200),
Hint: result.LearnedHint,
ToolSequence: result.ToolSequence,
Success: exec.Success,
Timestamp: exec.Timestamp.Unix(),
}
if err := e.mem.AddExperience(exp); err == nil {
result.ExperienceAdded = true
}
if exec.ServerHost != "" {
_ = e.mem.AddHost(exec.ServerHost) // 失败忽略
}
// Step 5: MEMORIZE —— LLM 提炼持久事实(可选,失败降级)
if e.llm != nil && exec.Success && exec.FinalAnswer != "" {
go e.memorizeFact(ctx, exec) // 异步,不阻塞进化主流程
}
return result
}
// learnHint 生成经验提示(LLM 优先,降级规则) // Logan.Li
func (e *EvolverEngine) learnHint(ctx context.Context, exec *TaskExecution, taskType string) string {
tools := strings.Join(uniqueStrings(resultToolNames(exec.ToolCalls)), " → ")
// 尝试用 LLM 提炼
if e.llm != nil {
if hint := e.llmLearnHint(ctx, exec, taskType, tools, exec.Success); hint != "" {
return hint
}
}
// 降级:规则生成
if exec.Success {
return fmt.Sprintf("完成「%s」类任务时,工具序列 %s 最终成功,可作为参考。", taskType, tools)
}
return fmt.Sprintf("尝试「%s」类任务时,工具序列 %s 未成功,下次可换用不同命令或思路。", taskType, tools)
}
// llmLearnHint 调用 LLM 提炼一句经验 // Logan.Li
func (e *EvolverEngine) llmLearnHint(ctx context.Context, exec *TaskExecution, taskType, tools string, success bool) string {
result := "失败"
if success {
result = "成功"
}
prompt := fmt.Sprintf(
"你是运维经验提炼器。根据以下任务执行记录,用一句中文(不超过80字)提炼经验提示,"+
"说明下次做同类任务的关键要点或推荐工具顺序。只返回经验文本,不要多余解释。\n"+
"任务: %s\n工具序列: %s\n结果: %s",
truncate(exec.Query, 150), tools, result)
resp, err := e.llm.Complete(ctx, &llm.CompletionRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil || resp == nil {
return ""
}
return strings.TrimSpace(truncate(resp.Message.Content, 120))
}
// memorizeFact 异步提取持久事实存入事实记忆 // Logan.Li
func (e *EvolverEngine) memorizeFact(ctx context.Context, exec *TaskExecution) {
defer func() { _ = recover() }()
prompt := fmt.Sprintf(
"从以下运维任务记录中,提炼 1 条值得长期记住的事实(如某主机的固定配置、已知问题、约定)。"+
"如果没有明显可沉淀的事实,回复「无」。只返回事实本身,一句话。\n任务: %s\n结论: %s",
truncate(exec.Query, 150), truncate(exec.FinalAnswer, 300))
ctx2, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
resp, err := e.llm.Complete(ctx2, &llm.CompletionRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil || resp == nil {
return
}
fact := strings.TrimSpace(resp.Message.Content)
if fact == "" || strings.HasPrefix(fact, "无") {
return
}
_ = e.mem.AddFact(FactEntry{Fact: truncate(fact, 150), Source: truncate(exec.Query, 80)})
}
// GetContextForPrompt 为下一次任务构建记忆上下文(注入 system prompt) // Logan.Li
func (e *EvolverEngine) GetContextForPrompt(query string) string {
taskType := classifyTask(query, nil)
var sb strings.Builder
if exps := e.mem.FindExperiences(taskType, 3); len(exps) > 0 {
sb.WriteString(fmt.Sprintf(" 【%s 类历史经验】\n", taskType))
for _, exp := range exps {
mark := "成功"
if !exp.Success {
mark = "未成功"
}
sb.WriteString(fmt.Sprintf(" - (%s) %s\n", mark, exp.Hint))
}
}
if facts := e.mem.RelevantFacts(query, 3); len(facts) > 0 {
sb.WriteString(" 【已知事实】\n")
for _, f := range facts {
sb.WriteString(fmt.Sprintf(" - %s\n", f.Fact))
}
}
if hosts := e.mem.Hosts(); len(hosts) > 0 {
sb.WriteString(fmt.Sprintf(" 【已知主机】%s\n", strings.Join(hosts, ", ")))
}
return sb.String()
}
// GetStats 进化统计(供审计/展示) // Logan.Li
func (e *EvolverEngine) GetStats() map[string]interface{} {
return map[string]interface{}{
"enabled": true,
"memory": e.mem.CountStats(),
}
}
// ---- 任务分类(关键词规则) ---- // Logan.Li
// classifyTask 按 query + 工具调用关键词分类任务类型 // Logan.Li
func classifyTask(query string, calls []ToolCallRecord) string {
text := strings.ToLower(query)
for _, c := range calls {
if cmd, ok := c.Args["command"].(string); ok {
text += " " + strings.ToLower(cmd)
}
}
rules := []struct {
typ string
kws []string
}{
{"磁盘空间", []string{"disk", "空间", "df", "du", "inode", "磁盘", "满了"}},
{"网络", []string{"network", "网络", "ping", "netstat", "ss ", "端口", "连接", "tcp", "dns"}},
{"服务管理", []string{"service", "服务", "systemctl", "nginx", "mysql", "redis", "restart", "启动", "停止"}},
{"软件安装", []string{"install", "安装", "yum", "apt", "docker", "部署", "deploy", "pip", "npm"}},
{"进程排查", []string{"process", "进程", "ps ", "kill", "cpu", "top", "卡死", "hang"}},
{"内存排查", []string{"memory", "内存", "mem", "free", "oom", "swap"}},
{"日志排查", []string{"log", "日志", "journalctl", "tail", "grep"}},
{"权限问题", []string{"permission", "权限", "chmod", "chown", "denied", "拒绝", "forbidden"}},
{"性能调优", []string{"performance", "性能", "负载", "load", "慢", "slow", "latency", "延迟"}},
{"配置管理", []string{"config", "配置", "conf", "vim", "sed"}},
}
for _, r := range rules {
for _, kw := range r.kws {
if strings.Contains(text, kw) {
return r.typ
}
}
}
return "通用运维"
}
// ---- 辅助 ---- // Logan.Li
func truncate(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n]) + "..."
}
func resultToolNames(calls []ToolCallRecord) []string {
out := make([]string, 0, len(calls))
for _, c := range calls {
out = append(out, c.ToolName)
}
return out
}
func uniqueStrings(in []string) []string {
seen := make(map[string]bool)
out := make([]string, 0, len(in))
for _, s := range in {
if !seen[s] {
seen[s] = true
out = append(out, s)
}
}
return out
}