This commit is contained in:
Logan committed 2026-07-09 14:16:56 +08:00
1 parent 0690285147
commit 5618cd2738
154 files changed
+5021 -3161

No files matched your search

+132 -24
View File
@@ -116,6 +116,7 @@ type Agent struct {
evolver *evolver.EvolverEngine
callback EventCallback
totalTokens int
cache map[string]string
mu sync.Mutex
}
@@ -130,7 +131,7 @@ func NewAgent(client llm.Client, registry *tools.Registry, cfg *Config, sc *safe
if cfg.MaxContextTokens <= 0 {
cfg.MaxContextTokens = 8000
}
return &Agent{llm: client, registry: registry, config: cfg, safetyCtl: sc, evolver: ev}
return &Agent{llm: client, registry: registry, config: cfg, safetyCtl: sc, evolver: ev, cache: make(map[string]string)}
}
// SetCallback 注入事件回调 // Logan.Li
@@ -293,6 +294,14 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
continue
}
if msg := a.suppressBroadKubernetesGet(tc.Function.Name, args, history); msg != "" {
a.emit(Event{Type: EventToolCall, Phase: PhaseToolRunning, Round: round, ToolName: tc.Function.Name, ToolArgs: args, 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: args, Risk: risk.Display(), Summary: "安全检查"})
@@ -315,6 +324,16 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
// 执行
a.emit(Event{Type: EventToolCall, Phase: PhaseToolRunning, Round: round, ToolName: tc.Function.Name, ToolArgs: args, 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: args, 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)
@@ -346,6 +365,7 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
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, args, output, success)
records = append(records, evolver.ToolCallRecord{
ToolName: tc.Function.Name, Args: args, Output: output,
@@ -369,6 +389,82 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
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 {
@@ -522,34 +618,46 @@ func (a *Agent) trimMessages(msgs []llm.Message) []llm.Message {
return ensureToolMessageIntegrity(result)
}
// ensureToolMessageIntegrity 确保 tool 消息前面有对应 assistant(tool_calls) // Logan.Li
// ensureToolMessageIntegrity 确保 assistant(tool_calls) 后紧跟完整 tool 结果组 // Logan.Li
func ensureToolMessageIntegrity(msgs []llm.Message) []llm.Message {
validIDs := make(map[string]bool)
for _, m := range msgs {
if m.Role == "assistant" {
for _, tc := range m.ToolCalls {
validIDs[tc.ID] = true
}
}
}
out := make([]llm.Message, 0, len(msgs))
for i, m := range msgs {
for i := 0; i < len(msgs); i++ {
m := msgs[i]
if m.Role == "tool" {
if !validIDs[m.ToolCallID] || i == 0 || msgs[i-1].Role != "assistant" {
continue
}
matched := false
for _, tc := range msgs[i-1].ToolCalls {
if tc.ID == m.ToolCallID {
matched = true
break
}
}
if !matched {
continue
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
}
}
out = append(out, m)
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
}
+98
View File
@@ -0,0 +1,98 @@
package core
import (
"testing"
"ezops/pkg/agentcli/llm"
)
func TestEnsureToolMessageIntegrityKeepsCompleteMultiToolGroup(t *testing.T) {
msgs := []llm.Message{
{Role: "system", Content: "sys"},
{Role: "user", Content: "查看异常副本"},
{Role: "assistant", Content: "并行检查", ToolCalls: []llm.ToolCall{
{ID: "call-1", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}},
{ID: "call-2", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}},
{ID: "call-3", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}},
}},
{Role: "tool", ToolCallID: "call-1", Name: "kubernetes_get", Content: "deployments"},
{Role: "tool", ToolCallID: "call-2", Name: "kubernetes_get", Content: "statefulsets"},
{Role: "tool", ToolCallID: "call-3", Name: "kubernetes_get", Content: "pods"},
}
got := ensureToolMessageIntegrity(msgs)
if len(got) != len(msgs) {
t.Fatalf("完整多工具结果组应全部保留,got=%d want=%d", len(got), len(msgs))
}
for i := 3; i < len(got); i++ {
if got[i].Role != "tool" || got[i].ToolCallID == "" {
t.Fatalf("第 %d 条工具结果异常: %+v", i, got[i])
}
}
}
func TestEnsureToolMessageIntegrityDropsIncompleteToolGroup(t *testing.T) {
msgs := []llm.Message{
{Role: "system", Content: "sys"},
{Role: "assistant", Content: "并行检查", ToolCalls: []llm.ToolCall{
{ID: "call-1", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}},
{ID: "call-2", Type: "function", Function: llm.FunctionCall{Name: "kubernetes_get", Arguments: "{}"}},
}},
{Role: "tool", ToolCallID: "call-1", Name: "kubernetes_get", Content: "deployments"},
{Role: "user", Content: "继续"},
}
got := ensureToolMessageIntegrity(msgs)
if len(got) != 2 {
t.Fatalf("不完整工具结果组应整体丢弃,got=%d want=2: %+v", len(got), got)
}
if got[0].Role != "system" || got[1].Role != "user" {
t.Fatalf("剩余消息异常: %+v", got)
}
}
func TestSuppressBroadKubernetesGetAfterDiagnose(t *testing.T) {
agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil)
history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx"}`: 1}
msg := agent.suppressBroadKubernetesGet("kubernetes_get", map[string]interface{}{"resource": "pods"}, history)
if msg == "" {
t.Fatal("expected broad pods query to be suppressed after diagnose")
}
}
func TestSuppressBroadKubernetesGetBlocksAllPodAliasesAfterDiagnose(t *testing.T) {
agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil)
history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx","focus":"all"}`: 1}
for _, resource := range []string{"pods", "pod"} {
msg := agent.suppressBroadKubernetesGet("kubernetes_get", map[string]interface{}{"resource": resource}, history)
if msg == "" {
t.Fatalf("expected broad %s query to be suppressed after diagnose", resource)
}
}
}
func TestSuppressBroadKubernetesGetAllowsFilteredQuery(t *testing.T) {
agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil)
history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx"}`: 1}
msg := agent.suppressBroadKubernetesGet("kubernetes_get", map[string]interface{}{
"resource": "pods", "status_filter": "NotRunning",
}, history)
if msg != "" {
t.Fatalf("expected filtered pods query to pass, got %q", msg)
}
}
func TestSuppressBroadKubernetesGetAllowsStatusFilteredPodsAfterDiagnose(t *testing.T) {
agent := NewAgent(nil, nil, &Config{Mode: "kubernetes"}, nil, nil)
history := map[string]int{`kubernetes_diagnose:{"namespace":"astralx","focus":"pods"}`: 1}
cases := []map[string]interface{}{
{"resource": "pods", "status_filter": "NotRunning"},
{"resource": "pod", "status_filter": "CrashLoopBackOff"},
}
for _, args := range cases {
msg := agent.suppressBroadKubernetesGet("kubernetes_get", args, history)
if msg != "" {
t.Fatalf("expected status_filter query to pass for args=%v, got %q", args, msg)
}
}
}