Files
aiops/backend/pkg/agentcli/core/agent.go
T
Logan.Li 1c98c25c74 feat(agentcli): 运行结果 incomplete 语义与契约文档
RunResult 增加 Outcome/Reason;触顶标记 incomplete:max_steps;
WebSocket finish 区分 incomplete 与 fatal error。补充 ai-agent docs 13 与挂点索引。
2026-10-19 03:53:09 +08:00

680 lines
25 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 core 实现 Agent CLI 的 ReAct 核心引擎。 // Logan.Li
// 设计参考 opsxcli internal/agent/core,剥离 TUI(Bubble Tea),改用 EventCallback 推送事件流
// (由 controller 序列化为 WebSocket 消息推给前端 + 持久化到 DB)。
// 核心算法保留:16 轮 ReAct 主循环、自适应检查点、循环检测(同参>3 次拒绝)、连续失败追踪、Evolver 集成。
package core
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"ezops/pkg/agentcli/evolver"
"ezops/pkg/agentcli/guardrail"
"ezops/pkg/agentcli/llm"
"ezops/pkg/agentcli/prompt"
"ezops/pkg/agentcli/safety"
"ezops/pkg/agentcli/tools"
)
// EventType 事件类型(驱动前端渲染与 DB 持久化) // Logan.Li
type EventType string
const (
EventStatus EventType = "status" // 生命周期状态(accepted/context/thinking/...)
EventThought EventType = "thought" // AI 思考/推理文本(流式结束后的完整内容)
EventThoughtDelta EventType = "thought_delta" // AI 思考流式增量 token
EventToolCall EventType = "tool_call" // 即将执行的工具(名/参数/风险)
EventToolResult EventType = "tool_result" // 工具执行结果(输出/成败/耗时)
EventProgress EventType = "progress" // 轮次进度 + token
EventDone EventType = "done" // 任务完成(最终答案)
EventError EventType = "error" // 错误(含达到最大轮次)
)
const (
PhaseAccepted = "accepted"
PhaseContext = "context"
PhaseThinking = "thinking"
PhasePlanning = "planning"
PhaseSafety = "safety_check"
PhaseApproval = "approval_required"
PhaseToolRunning = "tool_running"
PhaseObserving = "observing"
PhaseValidating = "validating"
PhaseSummarizing = "summarizing"
PhaseDone = "done"
PhaseBlocked = "blocked"
PhaseError = "error"
)
// Event 引擎产生的事件(controller 序列化后通过 WebSocket 推送) // Logan.Li
type Event struct {
Type EventType `json:"type"`
Phase string `json:"phase,omitempty"`
Summary string `json:"summary,omitempty"`
Round int `json:"round,omitempty"`
Thought string `json:"thought,omitempty"`
ToolName string `json:"tool_name,omitempty"`
ToolArgs map[string]interface{} `json:"tool_args,omitempty"`
Risk string `json:"risk,omitempty"`
Output string `json:"output,omitempty"`
Success bool `json:"success,omitempty"`
Duration string `json:"duration,omitempty"`
OutputSize int `json:"output_size,omitempty"`
Tokens int `json:"tokens,omitempty"`
Answer string `json:"answer,omitempty"`
Message string `json:"message,omitempty"`
}
// EventCallback 事件回调(controller 注入) // Logan.Li
type EventCallback func(Event)
// RunResult ReAct 执行结果(含对话历史供多轮追问) // Logan.Li
type RunResult struct {
Success bool
Output string
History []llm.Message // system 之后的对话历史
TotalTokens int
// Outcome/Reason 对齐三产品契约(completed/incomplete/...) // Logan.Li
Outcome string
Reason string
}
// Config Agent 运行配置 // Logan.Li
type Config struct {
Mode string // host | kubernetes
MaxIterations int
Temperature float64
ToolTimeout time.Duration
MaxTokens int
SafetyMode safety.Mode
OutputMaxLength int
MaxContextTokens int
MemoryDir string // Evolver 记忆目录
RoundOffset int // 轮次偏移(多轮追问时接续上一轮编号) // Logan.Li
}
// DefaultConfig 默认配置 // Logan.Li
func DefaultConfig() *Config {
return &Config{
MaxIterations: 16,
Temperature: 0.3,
ToolTimeout: 120 * time.Second,
MaxTokens: 4096,
SafetyMode: safety.ModeBalanced,
OutputMaxLength: 10000,
MaxContextTokens: 8000,
MemoryDir: "data/agentcli",
}
}
// Agent ReAct 核心引擎 // Logan.Li
type Agent struct {
llm llm.Client
registry *tools.Registry
config *Config
safetyCtl *safety.Controller
evolver *evolver.EvolverEngine
callback EventCallback
totalTokens int
cache map[string]string
mu sync.Mutex
}
// NewAgent 创建引擎。evolver 为 nil 时降级(无自学习)。 // Logan.Li
func NewAgent(client llm.Client, registry *tools.Registry, cfg *Config, sc *safety.Controller, ev *evolver.EvolverEngine) *Agent {
if cfg == nil {
cfg = DefaultConfig()
}
if sc == nil {
sc = safety.NewController(cfg.SafetyMode)
}
if cfg.MaxContextTokens <= 0 {
cfg.MaxContextTokens = 8000
}
return &Agent{llm: client, registry: registry, config: cfg, safetyCtl: sc, evolver: ev, cache: make(map[string]string)}
}
// SetCallback 注入事件回调 // Logan.Li
func (a *Agent) SetCallback(cb EventCallback) { a.callback = cb }
// emit 安全触发回调 // Logan.Li
func (a *Agent) emit(e Event) {
if a.callback != nil {
a.callback(e)
}
}
// Run 执行单次任务(ReAct 循环 + Evolver 集成);prior 为不含 system 的历史对话 // Logan.Li
func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.Message, priorTokens int) (*RunResult, error) {
a.totalTokens = priorTokens
start := time.Now()
a.emit(Event{Type: EventStatus, Phase: PhaseAccepted, Summary: "任务已接收"})
// Evolver PREDICT:获取历史经验注入 system prompt
evolveCtx := ""
if a.evolver != nil {
evolveCtx = a.evolver.GetContextForPrompt(query)
}
// 工具描述
toolDescs := a.buildToolDescriptions()
systemPrompt := prompt.BuildSystemPrompt(a.config.Mode, serverInfo, toolDescs, evolveCtx)
a.emit(Event{Type: EventStatus, Phase: PhaseContext, Summary: fmt.Sprintf("mode=%s tools=%d prior_messages=%d", a.config.Mode, len(a.registry.List()), len(prior))})
messages := []llm.Message{{Role: "system", Content: systemPrompt}}
if len(prior) > 0 {
messages = append(messages, prior...)
}
messages = append(messages, llm.Message{Role: "user", Content: query})
llmTools := a.registry.ToLLMTools()
toolCallHistory := make(map[string]int) // 循环检测
var toolRecords []evolver.ToolCallRecord
var consecutiveFailures int
var totalToolCalls int
for iter := 0; iter < a.config.MaxIterations; iter++ {
// 检查中断
if err := ctx.Err(); err != nil {
return nil, err
}
a.emit(Event{Type: EventProgress, Round: a.roundNum(iter)})
// 自适应检查点
if hint := a.checkAdaptiveCheckpoint(iter, toolCallHistory, consecutiveFailures, totalToolCalls); hint != "" {
messages = append(messages, llm.Message{Role: "user", Content: hint})
}
// 裁剪历史
trimmed := a.trimMessages(messages)
// ReAct 须用 Complete:Stream 无法组装 tool_calls/tool_use(GLM/Claude 等会只回文字不执行) // Logan.Li
round := a.roundNum(iter)
a.emit(Event{Type: EventStatus, Phase: PhaseThinking, Round: round, Summary: "LLM 推理中"})
resp, err := a.llm.Complete(ctx, &llm.CompletionRequest{
Messages: trimmed,
Tools: llmTools,
Temperature: a.config.Temperature,
MaxTokens: a.config.MaxTokens,
})
if err != nil {
a.emit(Event{Type: EventError, Phase: PhaseError, Message: "LLM 调用失败: " + err.Error()})
return nil, fmt.Errorf("LLM调用失败: %w", err)
}
a.totalTokens += resp.Usage.TotalTokens
a.emit(Event{Type: EventProgress, Tokens: a.totalTokens})
messages = append(messages, resp.Message)
// 无工具调用:首轮若仅描述计划未动手,注入提示重试一次 // Logan.Li
if len(resp.Message.ToolCalls) == 0 && totalToolCalls == 0 && iter < 2 {
// 计划阶段可展示简短思考,便于用户感知进度 // Logan.Li
if thought := strings.TrimSpace(resp.Message.Content); thought != "" {
a.emit(Event{Type: EventThought, Round: round, Thought: thought})
}
messages = append(messages, llm.Message{
Role: "user",
Content: fmt.Sprintf("【系统提示】请立即调用 %s 工具获取实际结果,不要仅用文字描述计划。", a.primaryExecTool()),
})
continue
}
// 无工具调用 → 最终答案
if len(resp.Message.ToolCalls) == 0 {
answer := strings.TrimSpace(resp.Message.Content)
// 部分模型最终轮 content 为空,回退到最近工具输出或默认文案 // Logan.Li
if answer == "" {
if fallback := lastToolOutput(messages); fallback != "" {
answer = "任务已执行完成,命令输出如下:\n" + fallback
} else {
answer = "任务已完成。"
}
}
a.emit(Event{Type: EventStatus, Phase: PhaseSummarizing, Round: round, Summary: "汇总最终答案"})
a.emit(Event{Type: EventDone, Phase: PhaseDone, Answer: answer, Tokens: a.totalTokens})
a.triggerEvolve(ctx, query, serverInfo, answer, toolRecords, true, start)
return &RunResult{
Success: true, Output: answer, Outcome: "completed", History: a.extractHistory(messages), TotalTokens: a.totalTokens,
}, nil
}
// 中间轮思考(后续还有 tool_calls,与终局 done 分开,避免重复输出) // Logan.Li
if thought := strings.TrimSpace(resp.Message.Content); thought != "" {
a.emit(Event{Type: EventThought, Phase: PhasePlanning, Round: round, Thought: thought})
}
// 处理工具调用
observations, records, failInc := a.processToolCalls(ctx, resp.Message.ToolCalls, toolCallHistory, round)
toolRecords = append(toolRecords, records...)
messages = append(messages, observations...)
totalToolCalls += len(resp.Message.ToolCalls)
consecutiveFailures = a.updateConsecutiveFailures(observations, consecutiveFailures) + failInc
}
// 达到最大轮次 → incomplete:max_steps(契约 13) // Logan.Li
msg := fmt.Sprintf("incomplete:max_steps: 达到最大迭代次数(%d),任务未完成。可加大预算或 continueTask 续跑。", a.config.MaxIterations)
a.emit(Event{Type: EventError, Phase: PhaseError, Message: msg, Tokens: a.totalTokens})
a.triggerEvolve(ctx, query, serverInfo, "", toolRecords, false, start)
return &RunResult{
Success: false, Output: msg, History: a.extractHistory(messages), TotalTokens: a.totalTokens,
Outcome: "incomplete", Reason: "max_steps",
}, fmt.Errorf("%s", msg)
}
// processToolCalls 处理工具调用,返回 observation 消息 + 记录 // Logan.Li
func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, history map[string]int, round int) ([]llm.Message, []evolver.ToolCallRecord, int) {
obs := make([]llm.Message, 0, len(calls))
records := make([]evolver.ToolCallRecord, 0, len(calls))
var failInc int
for _, tc := range calls {
key := tc.Function.Name + ":" + tc.Function.Arguments
history[key]++
// 循环检测
if history[key] > 3 {
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name,
Content: "错误: 检测到循环调用,相同参数已调用超过3次,请换一种方式"})
continue
}
tool, err := a.registry.Get(tc.Function.Name)
if err != nil {
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name,
Content: fmt.Sprintf("错误: 工具 '%s' 未找到", tc.Function.Name)})
continue
}
var args map[string]interface{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name,
Content: "错误: 参数解析失败: " + err.Error()})
continue
}
safeArgs := guardrail.SanitizeMap(args)
if msg := a.suppressBroadKubernetesGet(tc.Function.Name, args, history); msg != "" {
a.emit(Event{Type: EventToolCall, Phase: PhaseToolRunning, Round: round, ToolName: tc.Function.Name, ToolArgs: safeArgs, Risk: tools.RiskSafe.Display(), Summary: "工具执行中"})
a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name,
Output: msg, Success: true, Duration: "0s", OutputSize: len(msg)})
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name, Content: msg})
continue
}
risk := a.toolRisk(tool, args)
a.emit(Event{Type: EventStatus, Phase: PhaseSafety, Round: round, ToolName: tc.Function.Name, ToolArgs: safeArgs, Risk: risk.Display(), Summary: "安全检查"})
// 安全检查(高危触发 confirmFn,由 controller 异步确认)
approved, err := a.safetyCtl.Check(tool, args)
if err != nil {
a.emit(Event{Type: EventStatus, Phase: PhaseError, Round: round, ToolName: tc.Function.Name, ToolArgs: safeArgs, Risk: risk.Display(), Message: guardrail.SanitizeText(err.Error())})
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name,
Content: "安全检查失败: " + err.Error()})
continue
}
if !approved {
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name,
Content: "操作已被用户拒绝执行"})
a.emit(Event{Type: EventStatus, Phase: PhaseBlocked, Round: round, ToolName: tc.Function.Name, ToolArgs: safeArgs, Risk: risk.Display(), Summary: "用户拒绝执行"})
a.emit(Event{Type: EventToolResult, Round: round, ToolName: tc.Function.Name,
Phase: PhaseObserving, Output: "(用户拒绝执行)", Success: false, OutputSize: len("(用户拒绝执行)")})
continue
}
// 执行
a.emit(Event{Type: EventToolCall, Phase: PhaseToolRunning, Round: round, ToolName: tc.Function.Name, ToolArgs: safeArgs, Risk: risk.Display(), Summary: "工具执行中"})
if cached, ok := a.cachedToolResult(tc.Function.Name, args); ok {
a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name,
Output: cached, Success: true, Duration: "0s", OutputSize: len(cached)})
records = append(records, evolver.ToolCallRecord{
ToolName: tc.Function.Name, Args: safeArgs, Output: cached,
Duration: 0, Success: true, RiskLevel: int(risk),
})
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name, Content: cached})
continue
}
toolStart := time.Now()
toolCtx, cancel := context.WithTimeout(ctx, a.config.ToolTimeout)
result, execErr := tool.Execute(toolCtx, args)
cancel()
dur := time.Since(toolStart)
var output, errMsg string
success := false
if result != nil {
output, errMsg, success = result.Output, result.Error, result.Success
}
if execErr != nil {
errMsg = execErr.Error()
success = false
}
// 工具结果必须先脱敏,再进入自动分析、事件、缓存、Evolver 和模型上下文。 // Logan.Li
output = guardrail.SanitizeText(output)
errMsg = guardrail.SanitizeText(errMsg)
// 超长命令输出自动分析,避免 token 爆炸 // Logan.Li
if (tc.Function.Name == "ssh_execute" || tc.Function.Name == "pod_execute") && success && len(output) > a.autoAnalyzeThreshold() {
if analyzed := a.autoAnalyzeOutput(ctx, output); analyzed != "" {
output = analyzed
}
}
// 输出截断
if len(output) > a.config.OutputMaxLength {
output = output[:a.config.OutputMaxLength] +
fmt.Sprintf("\n[输出已截断,原始 %d 字符]", len(output))
}
a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name,
Output: output, Success: success, Duration: dur.Truncate(time.Millisecond).String(), OutputSize: len(output)})
a.storeToolResult(tc.Function.Name, safeArgs, output, success)
records = append(records, evolver.ToolCallRecord{
ToolName: tc.Function.Name, Args: safeArgs, Output: output,
Duration: dur, Success: success, RiskLevel: int(risk),
})
// 构建 observation
var content string
switch {
case execErr != nil:
content = "执行错误: " + errMsg
failInc++
case !success:
content = fmt.Sprintf("执行失败: %s\n输出: %s", errMsg, output)
failInc++
default:
content = output
}
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name, Content: content})
}
return obs, records, failInc
}
// cachedToolResult 返回同一任务内只读工具的缓存结果,避免 LLM 重复消耗 token。 // Logan.Li
func (a *Agent) cachedToolResult(toolName string, args map[string]interface{}) (string, bool) {
if !cacheableTool(toolName) {
return "", false
}
key := toolCacheKey(toolName, args)
if key == "" {
return "", false
}
if out, ok := a.cache[key]; ok {
return "已复用本轮任务缓存结果;不要重复查询同一对象,请基于首次结果总结,或改用更具体的 name/status_filter/pod 参数补查。\n\n" + out, true
}
return "", false
}
// storeToolResult 缓存成功的只读工具输出。 // Logan.Li
func (a *Agent) storeToolResult(toolName string, args map[string]interface{}, output string, success bool) {
if !success || !cacheableTool(toolName) {
return
}
key := toolCacheKey(toolName, args)
if key == "" {
return
}
a.cache[key] = output
}
func cacheableTool(toolName string) bool {
switch toolName {
case "kubernetes_diagnose", "kubernetes_get", "pod_logs", "aiops_asset_search", "aiops_alert_list", "aiops_metric_summary":
return true
default:
return false
}
}
func toolCacheKey(toolName string, args map[string]interface{}) string {
b, err := json.Marshal(args)
if err != nil {
return ""
}
return toolName + ":" + string(b)
}
// suppressBroadKubernetesGet 在聚合诊断后拦截全量 K8s 查询,迫使模型按异常对象下钻。 // Logan.Li
func (a *Agent) suppressBroadKubernetesGet(toolName string, args map[string]interface{}, history map[string]int) string {
if a.config.Mode != "kubernetes" && a.config.Mode != "k8s" {
return ""
}
if toolName != "kubernetes_get" || !hasDiagnoseHistory(history) {
return ""
}
resource := strings.ToLower(strings.TrimSpace(fmt.Sprint(args["resource"])))
name := strings.TrimSpace(fmt.Sprint(args["name"]))
labelSelector := strings.TrimSpace(fmt.Sprint(args["label_selector"]))
statusFilter := strings.TrimSpace(fmt.Sprint(args["status_filter"]))
if name != "" && name != "<nil>" || labelSelector != "" && labelSelector != "<nil>" || statusFilter != "" && statusFilter != "<nil>" {
return ""
}
switch resource {
case "pods", "pod", "deployments", "deployment", "deploy", "services", "service", "svc", "nodes", "node", "events", "event":
return "已拦截全量 Kubernetes 查询:本轮已经执行过 kubernetes_diagnose,里面包含异常 Workload、异常 Pod、Top Events 和节点摘要。请不要再次拉全量资源;如需补证据,请按诊断结果中的具体对象调用 kubernetes_get(name/status_filter) 或 pod_logs(pod)。"
default:
return ""
}
}
func hasDiagnoseHistory(history map[string]int) bool {
for key, count := range history {
if count > 0 && strings.HasPrefix(key, "kubernetes_diagnose:") {
return true
}
}
return false
}
// triggerEvolve 异步触发进化(带超时 + panic 恢复,绝不阻塞主循环) // Logan.Li
func (a *Agent) triggerEvolve(ctx context.Context, query, serverHost, answer string, records []evolver.ToolCallRecord, success bool, start time.Time) {
if a.evolver == nil {
return
}
exec := &evolver.TaskExecution{
Query: query, ToolCalls: records, TotalSteps: len(records),
TotalTokens: a.totalTokens, Duration: time.Since(start),
Success: success, FinalAnswer: answer, Timestamp: time.Now(), ServerHost: serverHost,
}
go func() {
defer func() { _ = recover() }()
eCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = a.evolver.Evolve(eCtx, exec)
}()
}
// roundNum 计算对外展示的 ReAct 轮次(含多轮追问偏移) // Logan.Li
func (a *Agent) roundNum(iter int) int {
return a.config.RoundOffset + iter + 1
}
// primaryExecTool 返回当前模式下的主执行工具名 // Logan.Li
func (a *Agent) primaryExecTool() string {
if a.config.Mode == "kubernetes" || a.config.Mode == "k8s" {
return "kubernetes_get"
}
return "ssh_execute"
}
// lastToolOutput 取最近一条工具 observation,供最终答案为空时回退展示 // Logan.Li
func lastToolOutput(messages []llm.Message) string {
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Role == "tool" {
if out := strings.TrimSpace(messages[i].Content); out != "" {
return out
}
}
}
return ""
}
// extractHistory 提取 system 之后的对话历史(供多轮追问) // Logan.Li
func (a *Agent) extractHistory(messages []llm.Message) []llm.Message {
if len(messages) <= 1 {
return nil
}
out := make([]llm.Message, len(messages)-1)
copy(out, messages[1:])
return out
}
// autoAnalyzeThreshold 触发自动 analyze_output 的输出长度阈值 // Logan.Li
func (a *Agent) autoAnalyzeThreshold() int {
t := a.config.OutputMaxLength / 2
if t < 4000 {
return 4000
}
return t
}
// autoAnalyzeOutput 对超长命令输出自动调用 analyze_output // Logan.Li
func (a *Agent) autoAnalyzeOutput(ctx context.Context, output string) string {
tool, err := a.registry.Get("analyze_output")
if err != nil {
return ""
}
r, err := tool.Execute(ctx, map[string]interface{}{"output": output})
if err != nil || r == nil || !r.Success || strings.TrimSpace(r.Output) == "" {
return ""
}
return r.Output + "\n[系统自动分析超长输出]"
}
// buildToolDescriptions 工具描述文本 // Logan.Li
func (a *Agent) buildToolDescriptions() string {
var sb strings.Builder
for _, t := range a.registry.List() {
sb.WriteString(fmt.Sprintf("- %s: %s\n", t.Name(), t.Description()))
}
return sb.String()
}
// toolRisk 取工具风险(动态优先) // Logan.Li
func (a *Agent) toolRisk(tool tools.Tool, args map[string]interface{}) tools.RiskLevel {
if dt, ok := tool.(tools.DynamicRiskTool); ok {
return dt.RiskLevelForArgs(args)
}
return tool.RiskLevel()
}
// checkAdaptiveCheckpoint 自适应检查点(iter 3/7/11 注入引导) // Logan.Li
func (a *Agent) checkAdaptiveCheckpoint(iter int, history map[string]int, consecFail, totalCalls int) string {
switch iter {
case 3:
repeat := 0
for _, c := range history {
if c > 1 {
repeat += c - 1
}
}
if totalCalls > 0 && (float64(repeat)/float64(totalCalls) > 0.5 || consecFail >= 3) {
return "【系统提示】检测到重复使用相同工具或连续失败,请尝试换一种思路或不同命令。"
}
case 7:
return "【系统提示】已执行到中段,请总结进展。信息足够就直接给出最终答案,否则说明剩余步骤。"
case 11:
return fmt.Sprintf("【系统提示】仅剩 %d 步,请立即整理已有信息给出最终答案,不要再调用新工具。", a.config.MaxIterations-iter-1)
}
return ""
}
// updateConsecutiveFailures 连续失败计数 // Logan.Li
func (a *Agent) updateConsecutiveFailures(obs []llm.Message, current int) int {
errs := 0
for _, o := range obs {
c := o.Content
if strings.HasPrefix(c, "错误") || strings.HasPrefix(c, "失败") || strings.HasPrefix(c, "执行错误") || strings.HasPrefix(c, "执行失败") {
errs++
}
}
if errs == 0 {
return 0
}
return current + errs
}
// trimMessages 裁剪历史(保留 system + 最近消息,token 不超 90%) // Logan.Li
func (a *Agent) trimMessages(msgs []llm.Message) []llm.Message {
if len(msgs) <= 1 {
return msgs
}
maxT := a.config.MaxContextTokens * 9 / 10
sysT := estimateTokens(msgs[0].Content)
kept := make([]llm.Message, 0, len(msgs))
keptT := 0
for i := len(msgs) - 1; i >= 1; i-- {
t := estimateTokens(msgs[i].Content)
if sysT+keptT+t > maxT {
break
}
keptT += t
kept = append([]llm.Message{msgs[i]}, kept...)
}
// 最少保留最近 4 条,避免上下文过短
if len(kept) < 4 && len(msgs) > 4 {
kept = msgs[len(msgs)-4:]
}
result := append([]llm.Message{msgs[0]}, kept...)
return ensureToolMessageIntegrity(result)
}
// ensureToolMessageIntegrity 确保 assistant(tool_calls) 后紧跟完整 tool 结果组 // Logan.Li
func ensureToolMessageIntegrity(msgs []llm.Message) []llm.Message {
out := make([]llm.Message, 0, len(msgs))
for i := 0; i < len(msgs); i++ {
m := msgs[i]
if m.Role == "tool" {
continue
}
if m.Role != "assistant" || len(m.ToolCalls) == 0 {
out = append(out, m)
continue
}
required := make(map[string]bool, len(m.ToolCalls))
for _, tc := range m.ToolCalls {
if tc.ID != "" {
required[tc.ID] = true
}
}
if len(required) == 0 {
continue
}
seen := make(map[string]bool, len(required))
tools := make([]llm.Message, 0, len(required))
j := i + 1
for ; j < len(msgs) && msgs[j].Role == "tool"; j++ {
tm := msgs[j]
if !required[tm.ToolCallID] || seen[tm.ToolCallID] {
continue
}
seen[tm.ToolCallID] = true
tools = append(tools, tm)
}
if len(seen) == len(required) {
out = append(out, m)
out = append(out, tools...)
}
i = j - 1
}
return out
}
// estimateTokens 粗略 token 估算(中文约 1 字/token,英文约 1/4,折中 rune/3) // Logan.Li
func estimateTokens(s string) int {
return len([]rune(s))/3 + 1
}
// Close 释放资源(SSH 长连接由 Engine.Close 统一释放) // Logan.Li
func (a *Agent) Close() {}