fix: 允许 Agent 持续自主诊断
This commit is contained in:
+27
-4
@@ -2,6 +2,7 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections import Counter
|
||||||
import httpx
|
import httpx
|
||||||
from backend.config import AI_API_BASE, AI_API_KEY, AI_MODEL
|
from backend.config import AI_API_BASE, AI_API_KEY, AI_MODEL
|
||||||
from backend.agent_tools import classify_kubectl_command, execute_kubectl
|
from backend.agent_tools import classify_kubectl_command, execute_kubectl
|
||||||
@@ -96,7 +97,7 @@ async def _complete_chat(messages: list[dict]) -> str:
|
|||||||
async def run_diagnostic_agent(
|
async def run_diagnostic_agent(
|
||||||
report: str,
|
report: str,
|
||||||
question: str = "",
|
question: str = "",
|
||||||
max_steps: int = 6,
|
max_steps: int = 30,
|
||||||
execution_mode: str = "ai",
|
execution_mode: str = "ai",
|
||||||
):
|
):
|
||||||
"""自主诊断循环:AI 模式执行只读命令,手动模式等待用户执行。"""
|
"""自主诊断循环:AI 模式执行只读命令,手动模式等待用户执行。"""
|
||||||
@@ -112,8 +113,11 @@ async def run_diagnostic_agent(
|
|||||||
{"role": "user", "content": user_content},
|
{"role": "user", "content": user_content},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
command_counts = Counter()
|
||||||
|
|
||||||
if execution_mode == "ai":
|
if execution_mode == "ai":
|
||||||
initial_command = "kubectl get nodes -o wide"
|
initial_command = "kubectl get nodes -o wide"
|
||||||
|
command_counts[initial_command] += 1
|
||||||
yield {
|
yield {
|
||||||
"type": "command", "step": 0, "command": initial_command,
|
"type": "command", "step": 0, "command": initial_command,
|
||||||
"reason": "建立集群节点状态基线,确保 AI 执行模式实际采集现场证据",
|
"reason": "建立集群节点状态基线,确保 AI 执行模式实际采集现场证据",
|
||||||
@@ -141,6 +145,23 @@ async def run_diagnostic_agent(
|
|||||||
|
|
||||||
command = action["command"]
|
command = action["command"]
|
||||||
reason = action.get("reason", "AI 需要更多集群证据")
|
reason = action.get("reason", "AI 需要更多集群证据")
|
||||||
|
command_counts[command] += 1
|
||||||
|
if command_counts[command] > 2:
|
||||||
|
messages.extend([
|
||||||
|
{"role": "assistant", "content": content},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": (
|
||||||
|
f"命令 {command!r} 已重复执行两次,不能再次执行。"
|
||||||
|
"请基于已有输出选择不同的只读诊断命令,或在证据充分时返回 final。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
yield {
|
||||||
|
"type": "command_rejected", "command": command,
|
||||||
|
"reason": "相同命令已执行两次,请更换诊断方向",
|
||||||
|
}
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
decision = classify_kubectl_command(command)
|
decision = classify_kubectl_command(command)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
@@ -185,9 +206,11 @@ async def run_diagnostic_agent(
|
|||||||
])
|
])
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
"type": "final",
|
"type": "error",
|
||||||
"content": f"已达到自主诊断最大轮次({max_steps}),请根据上方命令输出继续人工排查。",
|
"message": (
|
||||||
"model": AI_MODEL,
|
f"自主诊断已达到安全上限({max_steps} 轮),但 Agent 仍未形成有效结论。"
|
||||||
|
"系统已停止继续执行以避免无限循环,请检查是否重复执行相同命令或 AI 服务响应异常。"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,53 @@ class DiagnosticAgentTest(unittest.TestCase):
|
|||||||
self.assertEqual(action["action"], "command")
|
self.assertEqual(action["action"], "command")
|
||||||
self.assertEqual(action["command"], "kubectl get pods -A")
|
self.assertEqual(action["command"], "kubectl get pods -A")
|
||||||
|
|
||||||
|
def test_ai_mode_continues_beyond_six_commands_until_model_finishes(self):
|
||||||
|
command_count = 8
|
||||||
|
responses = iter([
|
||||||
|
*[
|
||||||
|
(
|
||||||
|
'{"action":"command","command":"kubectl get pods -A '
|
||||||
|
f'--field-selector metadata.name=pod-{index}","reason":"继续收集证据"}}'
|
||||||
|
)
|
||||||
|
for index in range(command_count)
|
||||||
|
],
|
||||||
|
'{"action":"final","analysis":"已完成根因分析"}',
|
||||||
|
])
|
||||||
|
executed = []
|
||||||
|
|
||||||
|
async def fake_completion(messages):
|
||||||
|
return next(responses)
|
||||||
|
|
||||||
|
async def fake_execute(command):
|
||||||
|
executed.append(command)
|
||||||
|
return {"command": command, "mode": "read", "returncode": 0, "stdout": "evidence", "stderr": ""}
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
with patch("backend.ai_agent._complete_chat", fake_completion), patch("backend.ai_agent.execute_kubectl", fake_execute):
|
||||||
|
return await collect_events(run_diagnostic_agent("report", execution_mode="ai", max_steps=20))
|
||||||
|
|
||||||
|
events = asyncio.run(run())
|
||||||
|
self.assertEqual(len(executed), command_count + 1)
|
||||||
|
self.assertEqual(events[-1]["type"], "final")
|
||||||
|
self.assertEqual(events[-1]["content"], "已完成根因分析")
|
||||||
|
self.assertNotIn("最大轮次", events[-1]["content"])
|
||||||
|
|
||||||
|
def test_safety_limit_reports_agent_error_instead_of_sending_user_to_manual_diagnosis(self):
|
||||||
|
async def fake_completion(messages):
|
||||||
|
return '{"action":"command","command":"kubectl get pods -A","reason":"继续调查"}'
|
||||||
|
|
||||||
|
async def fake_execute(command):
|
||||||
|
return {"command": command, "mode": "read", "returncode": 0, "stdout": "same result", "stderr": ""}
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
with patch("backend.ai_agent._complete_chat", fake_completion), patch("backend.ai_agent.execute_kubectl", fake_execute):
|
||||||
|
return await collect_events(run_diagnostic_agent("report", execution_mode="ai", max_steps=2))
|
||||||
|
|
||||||
|
events = asyncio.run(run())
|
||||||
|
self.assertEqual(events[-1]["type"], "error")
|
||||||
|
self.assertIn("安全上限", events[-1]["message"])
|
||||||
|
self.assertNotIn("人工排查", events[-1]["message"])
|
||||||
|
|
||||||
def test_stops_when_model_returns_invalid_action(self):
|
def test_stops_when_model_returns_invalid_action(self):
|
||||||
async def fake_completion(messages):
|
async def fake_completion(messages):
|
||||||
return "not-json"
|
return "not-json"
|
||||||
|
|||||||
Reference in New Issue
Block a user