feat: 移除 etcdutl 引用,增加 ctr/nerdctl 示例,实现 Agent 断点续传

- AI Agent Prompt 移除不存在的 etcdutl 命令引用
- Prompt 增加 containerd(ctr/nerdctl) 和 Linux 基础调优命令示例
- processing_runs 表新增 context_json 字段,保存 Agent 执行上下文
- 每次命令执行后自动保存上下文到 DB
This commit is contained in:
Your Name
2026-07-25 14:34:05 +08:00
parent a5399bd69d
commit b19975c369
3 changed files with 169 additions and 4 deletions
+79 -3
View File
@@ -29,8 +29,10 @@ AGENT_SYSTEM_PROMPT = """你是 Kubernetes 自主诊断 Agent。你的首要职
可用工具:
1. kubectl 命令 - 查询集群内资源状态。例如: kubectl get pods -A, kubectl describe node <name>, kubectl logs <pod> -n <ns>
2. 系统诊断 shell 命令 - 查询节点宿主机状态。例如: systemctl status kubelet, journalctl -u kubelet -n 100 --no-pager, crictl ps, df -h, free -m, ss -tlnp, ip addr
3. etcd 诊断命令 - 检查 etcd 集群健康与数据。例如: etcdctl endpoint status, etcdctl member list, etcdctl alarm list, etcdctl get /kubernetes --prefix, etcdutl snapshot status <file>
2. 系统诊断 shell 命令 - 查询节点宿主机状态。例如: systemctl status kubelet, journalctl -u kubelet -n 100 --no-pager, crictl ps, df -h, free -m, ss -tlnp, ip addr, top, du -sh, curl, tcpdump
3. containerd 命令 - 查询容器运行时状态。例如: ctr containers list, ctr images list, ctr tasks ls, nerdctl ps -a, nerdctl logs <container>, nerdctl stats
4. etcd 诊断命令 - 检查 etcd 集群健康与数据。例如: etcdctl endpoint status, etcdctl endpoint health, etcdctl member list, etcdctl alarm list, etcdctl get /kubernetes --prefix
5. Linux 基础调优命令 - 例如: iostat -xz 1 3, vmstat 1 3, mpstat -P ALL 1 3, pidstat -d 1 3, lsof -i :<port>, ethtool <iface>, tcpdump -i any -c 10 -nn
行为要求:
1. 如果证据不足,必须返回 command 动作并逐条调查;至少检查与故障资源直接相关的状态、describe、events 或 logs。
@@ -112,6 +114,8 @@ async def run_diagnostic_agent(
question: str = "",
max_steps: int = 30,
execution_mode: str = "ai",
run_id: str = "",
history_store=None,
):
"""自主诊断循环:AI 模式执行只读命令,手动模式等待用户执行。"""
if execution_mode not in {"ai", "manual"}:
@@ -151,6 +155,8 @@ async def run_diagnostic_agent(
command_counts=command_counts,
report=report,
question=question,
run_id=run_id,
history_store=history_store,
):
yield event
@@ -163,6 +169,8 @@ async def _run_agent_loop(
command_counts: Counter,
report: str,
question: str,
run_id: str = "",
history_store=None,
):
invalid_response_count = 0
step = start_step
@@ -266,6 +274,9 @@ async def _run_agent_loop(
"command": command, "reason": reason, "mode": decision.mode,
"message": "请在终端手动执行命令,并根据结果继续排查",
}
# 手动模式也保存上下文,支持断点续传
self_save_context(run_id, history_store, messages, command_counts, step, step + 1,
max_steps, report, question, execution_mode, content)
return
if decision.requires_approval:
@@ -274,15 +285,19 @@ async def _run_agent_loop(
"command": command,
"reason": reason,
"created_by": AI_MODEL,
"run_id": "",
"run_id": run_id,
"report": report,
"question": question,
"execution_mode": execution_mode,
"history_store": history_store,
"messages": messages + [{"role": "assistant", "content": content}],
"next_step": step + 1,
"max_steps": max_steps,
"command_counts": dict(command_counts),
}
# 保存上下文到 DB,避免浏览器关闭/刷新丢失进度
self_save_context(run_id, history_store, messages, command_counts, step, step + 1,
max_steps, report, question, execution_mode, content)
yield {
"type": "approval_required", "approval_id": approval_id,
"command": command, "reason": reason,
@@ -296,6 +311,10 @@ async def _run_agent_loop(
{"role": "user", "content": "命令实际执行结果:\n" + json.dumps(result, ensure_ascii=False)},
])
# 每次执行后保存上下文,支持断点续传
self_save_context(run_id, history_store, messages, command_counts, step, step + 1,
max_steps, report, question, execution_mode)
step += 1
if max_steps > 0:
@@ -319,6 +338,8 @@ async def resume_diagnostic_agent(pending: dict, result: dict):
+ json.dumps(result, ensure_ascii=False)
),
})
run_id = pending.get("run_id", "")
history_store = pending.get("history_store")
async for event in _run_agent_loop(
messages=messages,
execution_mode=pending.get("execution_mode", "ai"),
@@ -327,6 +348,8 @@ async def resume_diagnostic_agent(pending: dict, result: dict):
command_counts=Counter(pending.get("command_counts") or {}),
report=pending.get("report", ""),
question=pending.get("question", ""),
run_id=run_id,
history_store=history_store,
):
yield event
@@ -335,6 +358,59 @@ def take_pending_approval(approval_id: str) -> dict | None:
return _PENDING_APPROVALS.pop(approval_id, None)
def self_save_context(
run_id: str, history_store,
messages: list, command_counts: Counter,
step: int, next_step: int,
max_steps: int, report: str, question: str,
execution_mode: str,
last_response_content: str = "",
):
"""保存 Agent 执行上下文到 DB,支持断点续传。"""
if not run_id or not history_store:
return
context = {
"messages": messages,
"command_counts": dict(command_counts),
"step": step,
"next_step": next_step,
"max_steps": max_steps,
"report": report,
"question": question,
"execution_mode": execution_mode,
"last_response": last_response_content,
}
history_store.update_context(run_id, context)
async def resume_from_context(
ctx: dict,
run_id: str = "",
history_store=None,
):
"""从 DB 保存的上下文恢复 Agent 诊断,不重跑历史命令,直接进入下一轮。"""
messages = list(ctx.get("messages") or [])
command_counts = Counter(ctx.get("command_counts") or {})
next_step = int(ctx.get("next_step", 1))
max_steps = int(ctx.get("max_steps", 0))
report = ctx.get("report", "")
question = ctx.get("question", "")
execution_mode = ctx.get("execution_mode", "ai")
async for event in _run_agent_loop(
messages=messages,
execution_mode=execution_mode,
max_steps=max_steps,
start_step=next_step,
command_counts=command_counts,
report=report,
question=question,
run_id=run_id,
history_store=history_store,
):
yield event
async def _stream_chat(messages: list[dict]):
"""通用流式请求 - yield 文本 chunk"""
# connect=10s 快速失败; read=90s 防止 AI 服务无响应时前端无限卡死