引言
不知道你有没有这种感觉------现在用 Claude Code 这类 AI 工具时,总觉得它像变魔术一样。你丢一个问题进去,它就能自动定位代码 bug、修改文件、甚至操作服务器。前阵子我做了一个 MySQL 诊断 Agent,用户只需要敲一行命令,它就能自己查慢查询、看系统负载、分析锁等待,最后给你一份完整的诊断报告。
今天这篇文章,我就把这个项目拆解开,跟你聊聊 Agent 开发的那些事儿。相信我,理解了背后的原理,你会发现 AI 工具其实没那么神秘。
智能体(Agent)
Agent(智能体/代理)是一种基于大语言模型(LLM)的自主AI系统。它能感知环境、规划目标、自主决策,并通过调用外部工具(API、数据库、浏览器)执行任务,将传统的"人机交互"提升为能独立完成复杂工作的"数字员工"或合作伙伴。
其实开发 Agent 说复杂也复杂,说简单也简单。拆开来看,核心就是三个部分:
- LLM - 大脑,负责做决策
- Tools - 手脚,负责执行具体操作
- Prompt - 灵魂,告诉 LLM 它是谁、能做什么
80% 的开发工作在写 Tools。 LLM 和 Prompt 通常很快就定了。
MySQL-Agent开发实战
作为开发者或 DBA,你是不是也遇到过这些情况:
- 数据库慢了,要查一堆命令:先看慢查询,再看 QPS,然后查进程列表,还要看系统负载...
- 每次排查都要重复同样的操作,记不住那么多命令和指标
- 新手想排查问题,根本不知道从何下手
- 想做自动化诊断,但硬编码的规则又太死板
这就是我开发这个 Agent 的初衷------让 AI 来帮我们完成这些繁琐的排查工作。
LLM:Agent 的大脑
LLM 是整个 Agent 的决策中心。在这个项目里,我用的是 SiliconFlow 的 DeepSeek-V3.2 API。
为什么用 API 而不是直接用 Claude Code 界面?因为通过 API,我们可以把 LLM 集成到我们的程序里,让它按我们的逻辑来工作。
调用 LLM 的代码其实很简单:
python
def _call_llm(self, user_message: str) -> ToolCall:
# 构建请求消息
self.messages.append({"role": "user", "content": user_message})
# 调用 API
payload = {
"model": self.model,
"messages": [{"role": "system", "content": self.SYSTEM_PROMPT}] + self.messages,
"temperature": 0.3, # 温度低一点,输出更稳定
"max_tokens": 3000,
}
with httpx.Client(timeout=120) as client:
response = client.post(url, headers=headers, json=payload)
result = response.json()
# 解析返回的工具调用决策
return parse_tool_call(result)
就这么简单------把上下文发给 LLM,它返回一个决策,告诉我们下一步该用什么工具、做什么操作。
但这里有个关键点:LLM 本身不会直接操作数据库或服务器,它只会"说"要做什么。真正执行的是我们接下来要讲的 Tools。
Tools:Agent 的手脚(这才是 80% 的工作)
很多人以为开发 Agent 的重点是调 LLM、写 Prompt。其实我的体会正好相反------80% 的工作都在写 Tools。
为什么?因为 LLM 虽然聪明,但它"说"的东西不一定能直接执行。你需要把它的想法转换成真正安全、可靠的代码。
在这个项目里,我写了三个核心工具:
工具一:SSHTool - 安全的 Linux 命令执行
第一个工具是 SSH 执行器。但这里有个大问题------你敢让 LLM 随便执行 Linux 命令吗?要是它说个 rm -rf / 怎么办?
所以安全是第一位的。我的做法是白名单机制:
python
class SSHTool:
# 只允许这些只读命令
ALLOWED_COMMANDS = {
"top", "free", "df", "du", "ps", "netstat", "ss", "uptime",
"vmstat", "iostat", "mpstat", "sar", "dmesg", "ls", "cat",
# ... 更多只读命令
}
# 绝对禁止这些危险子串
BLOCKED_SUBSTRINGS = [
">", ">>", "<", "<<", "|", ";", "&&", "||", # 重定向和管道
"rm -rf", "mkfs", "dd if=", # 危险操作
"exec", "eval", "sudo", "su ", # 权限提升
# ... 更多
]
def _validate_command(self, command: str) -> Tuple[bool, str]:
# 先检查有没有危险子串
for blocked in self.BLOCKED_SUBSTRINGS:
if blocked in command.lower():
return False, f"命令包含禁止的子串: {blocked}"
# 再检查命令名是否在白名单里
cmd_name = command.strip().split()[0]
if "/" in cmd_name:
cmd_name = cmd_name.split("/")[-1]
if cmd_name not in self.ALLOWED_COMMANDS:
return False, f"命令不在允许列表中: {cmd_name}"
return True, ""
这样一来,LLM 只能执行我们允许的只读命令,完全不用担心安全问题。
工具二:MySQLTool - 只读的数据库查询
第二个工具是 MySQL 查询器。同样,我们也需要严格的安全控制。
除了白名单,我还加了一个实用的功能------支持多条 SQL 执行。因为 LLM 有时候会想一次查多个指标,比如:
sql
SHOW GLOBAL STATUS LIKE 'Slow_queries';
SHOW VARIABLES LIKE 'long_query_time';
SHOW GLOBAL STATUS LIKE 'Questions';
默认情况下,PyMySQL 不支持一次执行多条 SQL。所以我写了一个解析器:
python
def _split_multi_query(self, sql: str) -> List[str]:
"""智能拆分多条 SQL,正确处理字符串中的分号"""
queries = []
current_query = []
in_string = False
string_char = None
escape_next = False
for char in sql:
if escape_next:
current_query.append(char)
escape_next = False
continue
if char == "\\":
current_query.append(char)
escape_next = True
continue
if char in ("'", '"'):
# 处理字符串边界
if not in_string:
in_string = True
string_char = char
elif char == string_char:
in_string = False
string_char = None
current_query.append(char)
continue
if char == ";" and not in_string:
# 只有不在字符串中的分号才是语句分隔符
query = "".join(current_query).strip()
if query:
queries.append(query)
current_query = []
continue
current_query.append(char)
# 处理最后一条语句
query = "".join(current_query).strip()
if query:
queries.append(query)
return queries
这个函数会正确处理 SQL 字符串中的分号,不会把 SELECT 'a;b' FROM t 拆成两条语句。
工具三:ReportWriter - 报告生成器
第三个工具是报告生成器。它的工作是把每一轮的观察结果记录下来,最后生成一份漂亮的 Markdown 报告。
python
class ReportWriter:
def add_observation(self, tool: str, action: str,
input_data: str, observation: str, round_num: int):
"""记录一轮观察"""
self.observations.append({
"round": round_num,
"tool": tool,
"action": action,
"input": input_data,
"observation": observation,
"timestamp": datetime.now().isoformat(),
})
def generate_markdown(self, final_answer: str,
metadata: Optional[Dict] = None) -> str:
"""生成完整的 Markdown 报告"""
# ... 组织报告内容
# 包含:配置信息、最终诊断、每一轮的详细观察
# ...
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
return filepath
好的工具设计应该是这样的:每个工具只做一件事,但把这件事做扎实。
Prompt:Agent 的灵魂
有了 LLM 和 Tools,还需要告诉 LLM 怎么用它们。这就是 Prompt 的作用。
Prompt 是 Agent 的"使用说明书",它要回答三个问题:
- 你是谁?(角色定位)
- 你能做什么?(工具说明)
- 你应该怎么做?(任务指南)
在这个项目里,我的 System Prompt 是全中文的,因为目标用户是中文用户。我把它分成了几个部分:
第一部分:角色定位
python
SYSTEM_PROMPT = """你是一位专业的 MySQL 数据库和 Linux 系统性能诊断专家。
你的任务是通过收集和分析信息,诊断数据库和系统的性能问题。
"""
先给 LLM 一个清晰的角色------你是专家,不是助手。这样它会更专业、更自信地做决策。
第二部分:工具说明
这部分要详细,因为 LLM 需要准确理解每个工具的能力:
python
【可用工具】
你有以下三个工具可以使用:
1. mysql_query - 执行只读 MySQL 查询
- 输入: SQL 查询语句(仅允许 SELECT, SHOW, DESCRIBE, EXPLAIN)
- 用途: 检查 MySQL 状态、变量、进程列表、慢查询、锁信息等
- 提示: 可以一次执行多条用分号分隔的查询
- 常用查询示例:
* SHOW GLOBAL STATUS LIKE 'Slow_queries'
* SHOW VARIABLES LIKE 'long_query_time'
...
注意,我还特意加了"常用查询示例"。这很重要,因为 LLM 有时候不知道具体该用什么命令,给几个例子能帮它快速上手。
第三部分:诊断流程建议
我还给了 LLM 一个诊断流程的建议,但强调"可以根据实际情况调整":
python
【诊断流程建议】
请按以下思路进行诊断,但可以根据实际情况调整:
第1阶段 - 基础检查(建议第1-3轮):
1. 慢查询统计 - Slow_queries, long_query_time
2. MySQL 运行时间 - Uptime
3. 查询量统计 - Questions, 计算 QPS
...
这就像给新手医生一个检查清单------既提供指导,又不限制灵活处理。
第四部分:响应格式
最后,必须明确告诉 LLM 如何输出它的决策:
python
【响应格式】
你的回答必须是纯 JSON 格式,不要包含其他文字:
{
"tool": "mysql_query|ssh_exec|final_answer",
"action": "具体动作描述",
"input": "要执行的查询或命令"
}
格式约定是 Agent 开发中最容易出问题的地方。LLM 有时候会在 JSON 外面加一些解释,或者用 Markdown 代码块包裹。所以在代码里,我加了一些清理逻辑:
python
# 清理响应 - 有时会有额外的文本或 markdown 格式
assistant_message = assistant_message.strip()
if assistant_message.startswith("```json"):
assistant_message = assistant_message[7:]
if assistant_message.startswith("```"):
assistant_message = assistant_message[3:]
if assistant_message.endswith("```"):
assistant_message = assistant_message[:-3]
assistant_message = assistant_message.strip()
parsed = json.loads(assistant_message)
这些细节虽然琐碎,但能让你的 Agent 稳定很多。
Agent 的主循环
现在三大组件都有了,怎么把它们拼起来呢?核心就是一个主循环:
python
def run(self) -> str:
"""运行完整的诊断流程"""
logger.info("诊断 Agent 启动...")
self.current_round = 0
# 初始上下文
user_message = self._build_initial_context()
while self.current_round < self.MAX_ROUNDS:
self.current_round += 1
logger.info(f"--- 第 {self.current_round} 轮诊断 ---")
# 步骤 1:让 LLM 决定下一步
tool_call = self._call_llm(user_message)
logger.info(f"决策: {tool_call.tool} - {tool_call.action}")
# 步骤 2:检查是否结束诊断
if tool_call.tool == "final_answer":
final_answer = tool_call.input_data
self.report_writer.add_observation(
tool="agent",
action="final_answer",
input_data="",
observation=final_answer,
round_num=self.current_round,
)
logger.info("已得出最终诊断。")
return final_answer
# 步骤 3:执行工具
observation = self._execute_tool(tool_call)
# 步骤 4:记录观察
self.report_writer.add_observation(
tool=tool_call.tool,
action=tool_call.action,
input_data=tool_call.input_data,
observation=observation,
round_num=self.current_round,
)
# 步骤 5:准备下一轮消息
user_message = f"""来自 {tool_call.tool}.{tool_call.action} 的观察结果:
...
"""
这个循环就是 Agent 的核心。每一轮都是:观察 → 决策 → 行动 → 再观察,直到 LLM 认为信息足够了。



完整代码:
python
#!/usr/bin/env python3
"""
MySQL/Linux 远程只读诊断 Agent
这是一个自主的 MySQL 和 Linux 系统性能诊断工具,使用 LLM 自动决策
需要收集哪些信息,最终生成综合诊断报告。
【功能特性】
- 自主诊断: LLM 自动决定下一步查询什么
- 只读安全: 严格的白名单机制,只允许只读操作
- 双端检查: 同时检查 MySQL 数据库和 Linux 系统
- 详细报告: 生成完整的 Markdown 格式诊断报告
【使用方式】
python main.py
"""
import os
import sys
import logging
from dotenv import load_dotenv
from src.mysql_diagnostic_agent import (
SSHTool,
MySQLTool,
ReportWriter,
DiagnosticAgent,
)
def setup_logging() -> None:
"""
配置日志系统
设置日志格式和输出方式,便于调试和监控。
"""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.StreamHandler(sys.stdout),
],
)
def load_config() -> dict:
"""
从环境变量加载配置
读取 .env 文件中的配置信息。
Returns:
配置字典
"""
load_dotenv()
return {
# SiliconFlow API 配置
"api_key": os.getenv("SILICONFLOW_API_KEY"),
"base_url": os.getenv("SILICONFLOW_BASE_URL", "https://api.siliconflow.cn/v1/"),
"model": os.getenv("SILICONFLOW_MODEL", "deepseek-ai/DeepSeek-V3.2"),
# SSH 连接配置
"ssh_host": os.getenv("SSH_HOST"),
"ssh_port": int(os.getenv("SSH_PORT", "22")),
"ssh_user": os.getenv("SSH_USER", "root"),
"ssh_password": os.getenv("SSH_PASSWORD"),
"ssh_key_path": os.getenv("SSH_KEY_PATH"),
# MySQL 连接配置
"mysql_host": os.getenv("MYSQL_HOST"),
"mysql_port": int(os.getenv("MYSQL_PORT", "3306")),
"mysql_user": os.getenv("MYSQL_USER", "root"),
"mysql_password": os.getenv("MYSQL_PASSWORD"),
"mysql_database": os.getenv("MYSQL_DATABASE"),
# 诊断配置
"slow_query_threshold": float(os.getenv("SLOW_QUERY_THRESHOLD", "0.5")),
}
def validate_config(config: dict) -> None:
"""
验证必需的配置是否存在
Args:
config: 配置字典
Raises:
ValueError: 当缺少必需配置时
"""
required = [
("api_key", "SILICONFLOW_API_KEY"),
("ssh_host", "SSH_HOST"),
("mysql_host", "MYSQL_HOST"),
]
missing = []
for key, env_var in required:
if not config.get(key):
missing.append(env_var)
if missing:
raise ValueError(f"缺少必需的环境变量: {', '.join(missing)}")
def main() -> int:
"""
主入口函数
协调整个诊断流程:
1. 加载配置
2. 初始化工具
3. 测试连接
4. 运行 Agent 诊断
5. 生成报告
Returns:
退出码 (0=成功, 非0=失败)
"""
setup_logging()
logger = logging.getLogger(__name__)
try:
# ==================== 欢迎信息 ====================
logger.info("=" * 60)
logger.info(" MySQL/Linux 远程只读诊断 Agent")
logger.info("=" * 60)
# ==================== 加载配置 ====================
config = load_config()
validate_config(config)
# ==================== 初始化工具 ====================
logger.info("正在初始化工具...")
ssh_tool = SSHTool(
host=config["ssh_host"],
port=config["ssh_port"],
username=config["ssh_user"],
password=config["ssh_password"],
key_path=config["ssh_key_path"],
)
mysql_tool = MySQLTool(
host=config["mysql_host"],
port=config["mysql_port"],
username=config["mysql_user"],
password=config["mysql_password"],
database=config["mysql_database"],
)
report_writer = ReportWriter(output_dir="reports")
# ==================== 测试连接 ====================
logger.info("正在测试连接...")
ssh_tool.connect()
logger.info("SSH 连接: 正常")
mysql_tool.connect()
logger.info("MySQL 连接: 正常")
# ==================== 初始化并运行 Agent ====================
logger.info("正在初始化诊断 Agent...")
agent = DiagnosticAgent(
ssh_tool=ssh_tool,
mysql_tool=mysql_tool,
report_writer=report_writer,
api_key=config["api_key"],
base_url=config["base_url"],
model=config["model"],
slow_query_threshold=config["slow_query_threshold"],
)
# 运行诊断
logger.info("开始自主诊断...")
final_answer = agent.run()
# ==================== 生成报告 ====================
logger.info("正在生成报告...")
report_path = report_writer.generate_markdown(
final_answer=final_answer,
metadata={
"目标 SSH": f"{config['ssh_host']}:{config['ssh_port']}",
"目标 MySQL": f"{config['mysql_host']}:{config['mysql_port']}",
"使用模型": config["model"],
"慢查询阈值": f"{config['slow_query_threshold']}秒",
},
)
# ==================== 输出摘要 ====================
print()
print(report_writer.generate_summary_text(final_answer))
print()
print(f"完整报告已保存至: {report_path}")
print()
# ==================== 清理 ====================
ssh_tool.disconnect()
mysql_tool.disconnect()
logger.info("诊断完成!")
return 0
except KeyboardInterrupt:
logger.info("诊断被用户中断。")
return 130
except Exception as e:
logger.error(f"诊断失败: {e}", exc_info=True)
return 1
if __name__ == "__main__":
sys.exit(main())
.env
# SiliconFlow API Configuration
SILICONFLOW_API_KEY=sk-xxxxxxxx
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/
SILICONFLOW_MODEL=deepseek-ai/DeepSeek-V3.2
# SSH Configuration
SSH_HOST=172.20.20.15
SSH_PORT=22
SSH_USER=root
SSH_PASSWORD=xxxxxx# or use SSH_KEY_PATH
#SSH_KEY_PATH=~/.ssh/id_rsa
# MySQL Configuration
MYSQL_HOST=172.20.20.15
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=xxxxx@
MYSQL_DATABASE=ownit
# Diagnostic Configuration
SLOW_QUERY_THRESHOLD=0.5
src\mysql_diagnostic_agent\diagnostic_agent.py
python
"""
诊断 Agent - 核心编排逻辑
使用 LLM 自主决定需要收集哪些信息,
何时可以得出诊断结论。提供中文界面和提示词。
"""
import os
import logging
from typing import Dict, Any, List, Optional
from dotenv import load_dotenv
import httpx
from .ssh_tool import SSHTool
from .mysql_tool import MySQLTool
from .report_writer import ReportWriter
logger = logging.getLogger(__name__)
load_dotenv()
class ToolCall:
"""
工具调用决策
表示从 LLM 返回的工具调用决策。
"""
def __init__(self, tool: str, action: str, input_data: str):
"""
初始化工具调用
Args:
tool: 工具名称 (mysql_query, ssh_exec, final_answer)
action: 动作描述
input_data: 输入数据(SQL 或命令)
"""
self.tool = tool
self.action = action
self.input_data = input_data
def to_dict(self) -> Dict[str, str]:
"""转换为字典格式"""
return {
"tool": self.tool,
"action": self.action,
"input": self.input_data,
}
class DiagnosticAgent:
"""
自主 MySQL/Linux 诊断 Agent
使用 LLM 来决定收集什么信息,何时可以结束诊断。
会自动检查 MySQL 和系统两方面的指标。
"""
# 最大诊断轮次,防止无限循环
MAX_ROUNDS = 30
# ==================== 系统提示词(中文) ====================
SYSTEM_PROMPT = """你是一位专业的 MySQL 数据库和 Linux 系统性能诊断专家。
你的任务是通过收集和分析信息,诊断数据库和系统的性能问题。
【可用工具】
你有以下三个工具可以使用:
1. mysql_query - 执行只读 MySQL 查询
- 输入: SQL 查询语句(仅允许 SELECT, SHOW, DESCRIBE, EXPLAIN)
- 用途: 检查 MySQL 状态、变量、进程列表、慢查询、锁信息等
- 提示: 可以一次执行多条用分号分隔的查询
- 常用查询示例:
* SHOW GLOBAL STATUS LIKE 'Slow_queries'
* SHOW VARIABLES LIKE 'long_query_time'
* SHOW GLOBAL STATUS LIKE 'Questions'
* SHOW GLOBAL STATUS LIKE 'Uptime'
* SHOW FULL PROCESSLIST
* SHOW ENGINE INNODB STATUS
* SHOW GLOBAL STATUS LIKE 'Threads_%'
* SHOW GLOBAL STATUS LIKE 'Connections'
* SHOW GLOBAL STATUS LIKE 'Aborted_%'
* SHOW GLOBAL VARIABLES
* SHOW OPEN TABLES WHERE In_use > 0
2. ssh_exec - 通过 SSH 执行只读 Linux 命令
- 输入: Linux 命令(仅允许只读命令)
- 用途: 检查系统资源:CPU、内存、磁盘、网络、进程等
- 常用命令示例:
* top -bn1 | head -50
* free -h
* df -h
* uptime
* vmstat 1 3
* iostat -x 1 3
* netstat -tuln
* netstat -an | grep ESTABLISHED | wc -l
* ps aux --sort=-%cpu | head -20
* ps aux --sort=-%mem | head -20
* dmesg | tail -50
* ss -s
3. final_answer - 结束诊断并给出结论
- 输入: 综合诊断报告和优化建议
- 使用时机: 当你收集了足够的信息,可以给出完整诊断时
【诊断流程建议】
请按以下思路进行诊断,但可以根据实际情况调整:
第1阶段 - 基础检查(建议第1-3轮):
1. 慢查询统计 - Slow_queries, long_query_time
2. MySQL 运行时间 - Uptime
3. 查询量统计 - Questions, 计算 QPS
4. 连接数统计 - Threads_connected, Max_used_connections
5. 系统基本信息 - uptime, free, df
第2阶段 - 深入检查(建议第4-10轮):
1. 进程列表 - SHOW FULL PROCESSLIST
2. InnoDB 状态 - SHOW ENGINE INNODB STATUS
3. 锁等待信息
4. CPU 使用率 - top, vmstat
5. 内存使用 - free, ps
6. 磁盘 I/O - iostat, df
7. 网络连接 - netstat, ss
第3阶段 - 针对性分析(按需进行):
1. 表缓存状态
2. 查询缓存(如果启用)
3. 临时表使用情况
4. 排序统计
5. 特定慢查询的 EXPLAIN
【重要规则】
1. 只能使用上面列出的三个工具
2. 所有 MySQL 查询必须是只读的(SELECT, SHOW, DESCRIBE, EXPLAIN)
3. 所有 SSH 命令必须是只读的(不能修改系统)
4. 从基础检查开始,然后根据发现深入挖掘
5. 必须全面检查:既要检查 MySQL,也要检查系统资源
6. 当你认为已经收集了足够信息时,使用 final_answer
【响应格式】
你的回答必须是纯 JSON 格式,不要包含其他文字:
{
"tool": "mysql_query|ssh_exec|final_answer",
"action": "具体动作描述",
"input": "要执行的查询或命令"
}
【最终诊断报告要求】
当使用 final_answer 时,请提供包含以下内容的综合报告:
1. 总体评价 - 健康状态评分(0-100分)
2. 慢查询分析 - 数量、阈值、占比
3. QPS 和负载分析
4. 连接和线程状态
5. 锁等待分析
6. 内存使用状况
7. CPU 使用状况
8. 磁盘空间和 I/O
9. 主要问题列表(按严重程度排序)
10. 具体的优化建议
现在,请开始诊断工作!"""
# ==================== 初始化 ====================
def __init__(
self,
ssh_tool: SSHTool,
mysql_tool: MySQLTool,
report_writer: ReportWriter,
api_key: str,
base_url: str,
model: str,
slow_query_threshold: float = 0.5,
):
"""
初始化诊断 Agent
Args:
ssh_tool: SSH 工具实例
mysql_tool: MySQL 工具实例
report_writer: 报告生成器实例
api_key: API 密钥
base_url: API 基础 URL
model: 模型名称
slow_query_threshold: 慢查询阈值(秒)
"""
self.ssh_tool = ssh_tool
self.mysql_tool = mysql_tool
self.report_writer = report_writer
self.api_key = api_key
self.base_url = base_url
self.model = model
self.slow_query_threshold = slow_query_threshold
self.messages: List[Dict[str, str]] = []
self.current_round = 0
# ==================== LLM 交互 ====================
def _build_initial_context(self) -> str:
"""
构建初始上下文
Returns:
初始上下文消息
"""
return f"""MySQL/Linux 诊断会话开始
【配置信息】
- 慢查询阈值: {self.slow_query_threshold} 秒
- 目标: MySQL 和 Linux 系统健康检查
请开始诊断过程。建议按以下顺序进行:
1. 检查慢查询统计信息
2. 检查 MySQL 状态和配置变量
3. 检查系统资源使用情况
请确保收集足够全面的信息后再给出最终诊断。
"""
def _call_llm(self, user_message: str) -> ToolCall:
"""
调用 LLM 获取下一步工具决策
Args:
user_message: 用户消息
Returns:
工具调用决策
"""
self.messages.append({"role": "user", "content": user_message})
# 构建 API 请求
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
payload = {
"model": self.model,
"messages": [
{"role": "system", "content": self.SYSTEM_PROMPT}
] + self.messages,
"temperature": 0.3, # 较低的温度使输出更稳定
"max_tokens": 3000,
}
url = f"{self.base_url.rstrip('/')}/chat/completions"
logger.debug(f"调用 LLM API: {url}")
try:
with httpx.Client(timeout=120) as client:
response = client.post(url, headers=headers, json=payload)
response.raise_for_status()
result = response.json()
# 解析响应
assistant_message = result["choices"][0]["message"]["content"]
self.messages.append({"role": "assistant", "content": assistant_message})
# 解析工具调用 JSON
import json
# 清理响应 - 有时会有额外的文本或 markdown 格式
assistant_message = assistant_message.strip()
if assistant_message.startswith("```json"):
assistant_message = assistant_message[7:]
if assistant_message.startswith("```"):
assistant_message = assistant_message[3:]
if assistant_message.endswith("```"):
assistant_message = assistant_message[:-3]
assistant_message = assistant_message.strip()
parsed = json.loads(assistant_message)
return ToolCall(
tool=parsed["tool"],
action=parsed.get("action", parsed["tool"]),
input_data=parsed["input"],
)
except Exception as e:
logger.error(f"LLM 调用失败: {e}", exc_info=True)
# 降级处理 - 如果无法解析,直接结束诊断
return ToolCall(
tool="final_answer",
action="结束诊断",
input_data=f"诊断因错误中断: {e}\n\n请查看报告中的观察数据进行手动分析。",
)
# ==================== 工具执行 ====================
def _execute_tool(self, tool_call: ToolCall) -> str:
"""
执行工具调用并返回结果
Args:
tool_call: 工具调用决策
Returns:
工具执行结果
"""
try:
if tool_call.tool == "mysql_query":
result = self.mysql_tool.query(tool_call.input_data)
# 格式化结果以便阅读
if result:
# 尝试格式化为表格
lines = []
headers = list(result[0].keys())
# 表头
lines.append(" | ".join(str(h) for h in headers))
# 分隔线
lines.append("-+-".join("-" * len(str(h)) for h in headers))
# 数据行
for row in result:
lines.append(" | ".join(str(row.get(h, "")) for h in headers))
return "\n".join(lines)
return "查询执行成功,无结果返回。"
elif tool_call.tool == "ssh_exec":
return self.ssh_tool.execute(tool_call.input_data)
elif tool_call.tool == "final_answer":
return "收到最终诊断结果。"
else:
return f"未知工具: {tool_call.tool}"
except Exception as e:
logger.error(f"工具执行失败: {e}", exc_info=True)
return f"错误: {str(e)}"
# ==================== 主诊断流程 ====================
def run(self) -> str:
"""
运行完整的诊断流程
Returns:
最终诊断字符串
"""
logger.info("诊断 Agent 启动...")
self.current_round = 0
# 初始上下文
user_message = self._build_initial_context()
while self.current_round < self.MAX_ROUNDS:
self.current_round += 1
logger.info(f"--- 第 {self.current_round} 轮诊断 ---")
# 获取 LLM 决策
tool_call = self._call_llm(user_message)
logger.info(f"决策: {tool_call.tool} - {tool_call.action}")
# 检查是否结束诊断
if tool_call.tool == "final_answer":
final_answer = tool_call.input_data
self.report_writer.add_observation(
tool="agent",
action="final_answer",
input_data="",
observation=final_answer,
round_num=self.current_round,
)
logger.info("已得出最终诊断。")
return final_answer
# 执行工具
observation = self._execute_tool(tool_call)
# 记录观察
self.report_writer.add_observation(
tool=tool_call.tool,
action=tool_call.action,
input_data=tool_call.input_data,
observation=observation,
round_num=self.current_round,
)
# 准备下一轮消息
user_message = f"""来自 {tool_call.tool}.{tool_call.action} 的观察结果:
【输入】
{tool_call.input_data}
【结果】
{observation}
接下来你想做什么?如果已经收集了足够的信息,请使用 final_answer 给出诊断结论。
"""
# 达到最大轮次,强制结束
final_answer = "诊断已达到最大轮次限制仍未结束。请查看已收集的观察数据进行手动分析。"
logger.warning("已达到最大诊断轮次。")
return final_answer
src\mysql_diagnostic_agent\mysql_tool.py
python
"""
MySQL 工具模块 - 只读 MySQL 查询
提供安全的 MySQL 连接和只读查询功能,
包含 SQL 白名单和安全检查机制,同时支持批量查询。
"""
import logging
from typing import Optional, List, Tuple, Any
import pymysql
from pymysql.cursors import DictCursor
logger = logging.getLogger(__name__)
class MySQLTool:
"""
MySQL 工具类,用于执行只读查询
该类提供安全的 MySQL 连接管理和查询执行功能,
所有执行的 SQL 都会经过白名单验证。
支持一次执行多条用分号分隔的查询。
"""
# ==================== 安全配置 ====================
# 允许的只读语句类型
ALLOWED_STATEMENTS = {
"SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN", "USE",
"HELP", "CHECKSUM", "CHECK", "ANALYZE"
}
# 需要阻止的关键字(用于只读安全保护)
BLOCKED_KEYWORDS = [
# 数据修改
"INSERT", "UPDATE", "DELETE", "REPLACE",
# 数据定义
"DROP", "TRUNCATE", "ALTER", "CREATE", "RENAME",
# 权限管理
"GRANT", "REVOKE",
# 变量设置
"SET",
# 数据加载
"LOAD",
# 锁操作
"LOCK", "UNLOCK",
# 事务控制
"START", "COMMIT", "ROLLBACK", "SAVEPOINT", "RELEASE",
# 预处理语句
"PREPARE", "EXECUTE", "DEALLOCATE",
# 表维护
"OPTIMIZE", "REPAIR", "USE FRM",
# 备份恢复
"BACKUP", "RESTORE", "IMPORT", "EXPORT",
# 服务器控制
"FLUSH", "RESET", "SHUTDOWN", "KILL", "PURGE", "CHANGE",
# 插件管理
"INSTALL", "UNINSTALL", "PLUGIN"
]
# ==================== 初始化和连接管理 ====================
def __init__(
self,
host: str,
port: int = 3306,
username: str = "root",
password: str = "",
database: Optional[str] = None,
):
"""
初始化 MySQL 工具
Args:
host: MySQL 服务器地址
port: MySQL 端口,默认 3306
username: MySQL 用户名,默认 root
password: MySQL 密码
database: 默认数据库(可选)
"""
self.host = host
self.port = port
self.username = username
self.password = password
self.database = database
self._connection: Optional[pymysql.Connection] = None
def connect(self) -> None:
"""
建立 MySQL 连接
如果已有连接则不重复建立。
"""
if self._connection:
return
logger.info(f"正在连接 MySQL: {self.username}@{self.host}:{self.port}")
self._connection = pymysql.connect(
host=self.host,
port=self.port,
user=self.username,
password=self.password,
database=self.database,
cursorclass=DictCursor,
charset="utf8mb4",
connect_timeout=30,
read_timeout=60,
write_timeout=60,
)
def disconnect(self) -> None:
"""
关闭 MySQL 连接
安全地关闭连接并清理资源。
"""
if self._connection:
self._connection.close()
self._connection = None
logger.info("MySQL 连接已断开")
# ==================== SQL 验证 ====================
def _validate_query(self, query: str) -> Tuple[bool, str]:
"""
验证单条查询是否为只读且安全
Args:
query: 待验证的 SQL 查询
Returns:
(是否允许, 错误信息) 的元组
"""
query_upper = query.strip().upper()
if not query_upper:
return False, "空查询"
# ==================== 清理注释 ====================
lines = []
for line in query_upper.split("\n"):
# 移除行内注释
line = line.split("--", 1)[0].split("#", 1)[0].strip()
if line:
lines.append(line)
clean_query = " ".join(lines)
# ==================== 检查禁止关键字 ====================
for keyword in self.BLOCKED_KEYWORDS:
# 检查是否为完整单词
if f" {keyword} " in f" {clean_query} ":
return False, f"查询包含禁止的关键字: {keyword}"
if clean_query.startswith(f"{keyword} "):
return False, f"查询以禁止关键字开头: {keyword}"
# ==================== 获取第一个单词 ====================
first_word = clean_query.split()[0] if clean_query else ""
# ==================== 特殊处理 ====================
# ANALYZE TABLE 特殊处理 - 允许用于诊断(只读分析)
if first_word == "ANALYZE" and "TABLE" in clean_query:
return True, ""
# ==================== 白名单检查 ====================
if first_word not in self.ALLOWED_STATEMENTS:
return False, f"语句类型不在允许列表中: {first_word}"
return True, ""
def _split_multi_query(self, sql: str) -> List[str]:
"""
将多条用分号分隔的 SQL 拆分为单条语句
Args:
sql: 包含多条 SQL 的字符串
Returns:
SQL 语句列表
"""
queries = []
current_query = []
in_string = False
string_char = None
escape_next = False
for char in sql:
if escape_next:
current_query.append(char)
escape_next = False
continue
if char == "\\":
current_query.append(char)
escape_next = True
continue
if char in ("'", '"'):
if not in_string:
in_string = True
string_char = char
elif char == string_char:
in_string = False
string_char = None
current_query.append(char)
continue
if char == ";" and not in_string:
query = "".join(current_query).strip()
if query:
queries.append(query)
current_query = []
continue
current_query.append(char)
# 处理最后一条
query = "".join(current_query).strip()
if query:
queries.append(query)
return queries
# ==================== 查询执行 ====================
def query(self, sql: str, params: Optional[Tuple] = None) -> List[dict]:
"""
执行只读 MySQL 查询
支持一次执行多条用分号分隔的查询,会返回所有查询的合并结果。
Args:
sql: SQL 查询(可以包含多条用分号分隔的语句)
params: 查询参数(仅用于单条查询)
Returns:
查询结果列表,每行是一个字典
Raises:
ValueError: 查询验证失败
RuntimeError: 查询执行失败
"""
# 分割多条查询
queries = self._split_multi_query(sql)
if not queries:
return []
# 如果是多条查询,分别执行
if len(queries) > 1:
all_results = []
for i, single_query in enumerate(queries, 1):
logger.info(f"执行多查询 [{i}/{len(queries)}]: {single_query}")
results = self._execute_single_query(single_query)
if results:
all_results.extend(results)
return all_results
# 单条查询
return self._execute_single_query(queries[0], params)
def _execute_single_query(self, sql: str, params: Optional[Tuple] = None) -> List[dict]:
"""
执行单条查询(内部方法)
Args:
sql: 单条 SQL 查询
params: 查询参数
Returns:
查询结果列表
"""
# 验证查询安全性
is_valid, error_msg = self._validate_query(sql)
if not is_valid:
raise ValueError(f"查询验证失败: {error_msg}")
# 确保已连接
if not self._connection:
self.connect()
logger.info(f"执行 MySQL 查询: {sql}")
try:
with self._connection.cursor() as cursor:
cursor.execute(sql, params or ())
result = cursor.fetchall()
return list(result) if result else []
except Exception as e:
logger.error(f"查询执行失败: {e}")
raise RuntimeError(f"执行查询失败: {e}")
def query_one(self, sql: str, params: Optional[Tuple] = None) -> Optional[dict]:
"""
执行查询并返回单行结果
Args:
sql: SQL 查询
params: 查询参数
Returns:
单行结果字典,如果没有结果返回 None
"""
results = self.query(sql, params)
return results[0] if results else None
def query_value(self, sql: str, params: Optional[Tuple] = None) -> Any:
"""
执行查询并返回第一列的单个值
Args:
sql: SQL 查询
params: 查询参数
Returns:
第一列的值,如果没有结果返回 None
"""
row = self.query_one(sql, params)
if row:
return next(iter(row.values()))
return None
# ==================== 便捷方法 ====================
def show_variables(self, like: Optional[str] = None) -> dict:
"""
获取 MySQL 系统变量
Args:
like: 可选的 LIKE 过滤条件
Returns:
变量名到值的字典
"""
if like:
sql = "SHOW VARIABLES LIKE %s"
rows = self.query(sql, (like,))
else:
rows = self.query("SHOW VARIABLES")
return {row["Variable_name"]: row["Value"] for row in rows}
def show_status(self, like: Optional[str] = None) -> dict:
"""
获取 MySQL 状态信息
Args:
like: 可选的 LIKE 过滤条件
Returns:
状态名到值的字典
"""
if like:
sql = "SHOW GLOBAL STATUS LIKE %s"
rows = self.query(sql, (like,))
else:
rows = self.query("SHOW GLOBAL STATUS")
return {row["Variable_name"]: row["Value"] for row in rows}
def show_processlist(self) -> List[dict]:
"""
获取当前进程列表
Returns:
进程列表
"""
return self.query("SHOW FULL PROCESSLIST")
def show_engine_status(self) -> Optional[dict]:
"""
获取 InnoDB 引擎状态
Returns:
InnoDB 状态信息
"""
rows = self.query("SHOW ENGINE INNODB STATUS")
if rows:
return rows[0]
return None
# ==================== 上下文管理器支持 ====================
def __enter__(self):
"""进入上下文时建立连接"""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""退出上下文时关闭连接"""
self.disconnect()
src\mysql_diagnostic_agent\report_writer.py
python
"""
报告生成器模块 - 生成诊断报告
负责收集诊断过程中的观察结果,并生成
详细的 Markdown 格式诊断报告。
"""
import os
import logging
from datetime import datetime
from typing import Dict, Any, List, Optional
logger = logging.getLogger(__name__)
class ReportWriter:
"""
诊断报告生成器
用于记录诊断过程中的每一轮观察,并最终生成
完整的 Markdown 格式报告。
"""
def __init__(self, output_dir: str = "reports"):
"""
初始化报告生成器
Args:
output_dir: 报告输出目录,默认为 "reports"
"""
self.output_dir = output_dir
self.observations: List[Dict[str, Any]] = []
self.start_time = datetime.now()
self._ensure_output_dir()
def _ensure_output_dir(self) -> None:
"""
确保输出目录存在
如果目录不存在则创建。
"""
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir)
logger.info(f"已创建输出目录: {self.output_dir}")
def add_observation(
self,
tool: str,
action: str,
input_data: str,
observation: str,
round_num: int,
) -> None:
"""
添加一条观察记录到报告
Args:
tool: 使用的工具 (ssh, mysql, agent)
action: 执行的动作描述
input_data: 工具的输入数据
observation: 观察结果/输出
round_num: 诊断轮次编号
"""
self.observations.append({
"round": round_num,
"tool": tool,
"action": action,
"input": input_data,
"observation": observation,
"timestamp": datetime.now().isoformat(),
})
logger.info(f"[第 {round_num} 轮] {tool}.{action} - 观察已记录")
def _generate_filename(self) -> str:
"""
生成唯一的报告文件名
基于时间戳生成唯一的文件名。
Returns:
报告文件名
"""
timestamp = self.start_time.strftime("%Y%m%d_%H%M%S")
return f"diagnostic_report_{timestamp}.md"
def _format_code_block(self, content: str, language: str = "") -> str:
"""
将内容格式化为 Markdown 代码块
如果内容过长会自动截断。
Args:
content: 要格式化的内容
language: 代码语言(可选)
Returns:
Markdown 代码块字符串
"""
if not content:
return ""
# 截断过长的内容以保持报告可读性
if len(content) > 5000:
content = content[:5000] + "\n... [已截断]"
return f"```{language}\n{content}\n```"
def generate_markdown(
self,
final_answer: str,
metadata: Optional[Dict[str, Any]] = None,
) -> str:
"""
生成 Markdown 格式的完整报告
Args:
final_answer: 最终诊断结论
metadata: 要包含的额外元数据(可选)
Returns:
生成的报告文件路径
"""
end_time = datetime.now()
duration = (end_time - self.start_time).total_seconds()
filename = self._generate_filename()
filepath = os.path.join(self.output_dir, filename)
# ==================== 构建 Markdown 内容 ====================
lines = []
# 标题
lines.append("# MySQL/Linux 诊断报告")
lines.append("")
lines.append(f"- **生成时间**: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
lines.append(f"- **总耗时**: {duration:.2f} 秒")
lines.append(f"- **诊断轮次**: {max((o['round'] for o in self.observations), default=0)}")
lines.append("")
# 元数据部分
if metadata:
lines.append("## 配置信息")
lines.append("")
for key, value in metadata.items():
lines.append(f"- **{key}**: {value}")
lines.append("")
# 最终诊断结论
lines.append("## 最终诊断")
lines.append("")
lines.append(final_answer)
lines.append("")
# 按轮次组织的观察记录
lines.append("## 诊断过程")
lines.append("")
if not self.observations:
lines.append("*没有记录观察数据。*")
lines.append("")
else:
# 按轮次分组
rounds: Dict[int, List[Dict[str, Any]]] = {}
for obs in self.observations:
r = obs["round"]
if r not in rounds:
rounds[r] = []
rounds[r].append(obs)
# 为每一轮生成内容
for round_num in sorted(rounds.keys()):
lines.append(f"### 第 {round_num} 轮")
lines.append("")
for obs in rounds[round_num]:
lines.append(f"**工具**: `{obs['tool']}.{obs['action']}`")
lines.append("")
lines.append("**输入**:")
lines.append("")
lines.append(self._format_code_block(str(obs["input"])))
lines.append("")
lines.append("**观察结果**:")
lines.append("")
lines.append(self._format_code_block(str(obs["observation"])))
lines.append("")
# 页脚
lines.append("---")
lines.append("")
lines.append("*由 MySQL 诊断 Agent 自动生成*")
# ==================== 写入文件 ====================
content = "\n".join(lines)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
logger.info(f"报告已写入: {filepath}")
return filepath
def generate_summary_text(self, final_answer: str) -> str:
"""
生成简洁的文本摘要
Args:
final_answer: 最终诊断结论
Returns:
摘要文本
"""
lines = []
lines.append("=" * 60)
lines.append(" MySQL/Linux 诊断报告摘要")
lines.append("=" * 60)
lines.append("")
lines.append("最终诊断:")
lines.append("-" * 40)
lines.append(final_answer)
lines.append("")
lines.append("=" * 60)
lines.append(f"总诊断轮次: {max((o['round'] for o in self.observations), default=0)}")
return "\n".join(lines)
src\mysql_diagnostic_agent\ssh_tool.py
python
"""
SSH 工具模块 - 只读 Linux 命令执行
提供安全的 SSH 连接和只读命令执行功能,
包含命令白名单和安全检查机制。
"""
import os
import logging
from typing import Optional, Tuple
import paramiko
# 获取模块日志记录器
logger = logging.getLogger(__name__)
class SSHTool:
"""
SSH 工具类,用于执行只读的 Linux 命令
该类提供安全的 SSH 连接管理和命令执行功能,
所有执行的命令都会经过白名单验证。
"""
# ==================== 安全配置 ====================
# 允许执行的只读命令白名单
ALLOWED_COMMANDS = {
# 系统信息
"top", "uptime", "uname", "hostname", "date", "ls", "cat",
# 资源监控
"free", "df", "du", "vmstat", "iostat", "mpstat", "sar",
# 进程管理
"ps", "pstree", "pgrep", "pidof", "lsof", "fuser",
# 网络工具
"netstat", "ss", "ip", "ifconfig", "ping", "traceroute",
"nslookup", "dig", "nethogs", "iftop",
# 系统日志
"dmesg", "journalctl",
# 系统状态
"lscpu", "lsmem", "lsblk",
# MySQL 相关只读命令
"mysqladmin", "mysqldump",
# 文本处理
"grep", "awk", "sed", "cut", "sort", "uniq", "wc", "head", "tail",
# 性能监控
"iotop", "htop", "atop",
# 服务管理
"systemctl",
}
# 需要阻止的危险子串列表
BLOCKED_SUBSTRINGS = [
# 重定向和管道
">", ">>", "<", "<<", "|",
# 命令分隔符
";", "&&", "||",
# 命令替换
"`", "$(", "${",
# 危险操作
"rm -rf", "mkfs", "dd if=",
# Fork 炸弹
":(){ :|:& };:",
# 权限修改
"chmod 777", "chown -R",
# 破坏性操作
"mv /", "cp /dev/null", "> /dev/sd",
# 代码执行
"exec", "eval", "source", ". ",
# 权限提升
"sudo", "su ",
# 网络下载
"wget ", "curl ",
# 包管理
"apt-get", "yum ", "dnf ", "pip ",
# 版本控制
"git ",
# 容器/虚拟化
"docker ", "kube", "virsh",
]
# ==================== 初始化和连接管理 ====================
def __init__(
self,
host: str,
port: int = 22,
username: str = "root",
password: Optional[str] = None,
key_path: Optional[str] = None,
):
"""
初始化 SSH 工具
Args:
host: SSH 服务器地址
port: SSH 端口,默认 22
username: SSH 用户名,默认 root
password: SSH 密码(可选)
key_path: SSH 私钥路径(可选)
"""
self.host = host
self.port = port
self.username = username
self.password = password
self.key_path = key_path
self._client: Optional[paramiko.SSHClient] = None
def connect(self) -> None:
"""
建立 SSH 连接
如果已有连接则不重复建立。
优先使用密钥认证,其次使用密码认证。
"""
if self._client:
return
# 创建 SSH 客户端并配置自动添加主机密钥
self._client = paramiko.SSHClient()
self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 构建连接参数
connect_kwargs = {
"hostname": self.host,
"port": self.port,
"username": self.username,
}
# 优先使用密钥认证
if self.key_path and os.path.exists(self.key_path):
connect_kwargs["key_filename"] = self.key_path
elif self.password:
connect_kwargs["password"] = self.password
logger.info(f"正在连接 SSH: {self.username}@{self.host}:{self.port}")
self._client.connect(**connect_kwargs, timeout=30)
def disconnect(self) -> None:
"""
关闭 SSH 连接
安全地关闭连接并清理资源。
"""
if self._client:
self._client.close()
self._client = None
logger.info("SSH 连接已断开")
# ==================== 命令验证 ====================
def _validate_command(self, command: str) -> Tuple[bool, str]:
"""
验证命令是否为只读且安全
Args:
command: 待验证的命令字符串
Returns:
(是否允许, 错误信息) 的元组
"""
cmd_lower = command.lower()
# 检查是否包含危险子串
for blocked in self.BLOCKED_SUBSTRINGS:
if blocked in cmd_lower:
return False, f"命令包含禁止的子串: {blocked}"
# 提取命令名
cmd_parts = command.strip().split()
if not cmd_parts:
return False, "空命令"
cmd_name = cmd_parts[0]
# 处理包含路径的情况,如 /usr/bin/top
if "/" in cmd_name:
cmd_name = cmd_name.split("/")[-1]
# ==================== 特殊命令处理 ====================
# mysqladmin 特殊处理 - 只允许只读子命令
if cmd_name == "mysqladmin":
allowed_mysqladmin = {
"ping", "status", "version", "extended-status",
"processlist", "variables", "info"
}
for subcmd in allowed_mysqladmin:
if subcmd in command:
return True, ""
return False, "mysqladmin 子命令不在允许列表中"
# mysqldump 特殊处理 - 只允许无数据导出
if cmd_name == "mysqldump":
if "--no-data" not in command and "-d" not in command:
return False, "mysqldump 必须使用 --no-data 或 -d 参数"
return True, ""
# ==================== 白名单检查 ====================
if cmd_name not in self.ALLOWED_COMMANDS:
return False, f"命令不在允许列表中: {cmd_name}"
return True, ""
# ==================== 命令执行 ====================
def execute(self, command: str, timeout: int = 60) -> str:
"""
执行只读命令
Args:
command: 要执行的命令
timeout: 命令超时时间(秒),默认 60
Returns:
命令输出结果
Raises:
ValueError: 命令验证失败
RuntimeError: 命令执行失败
"""
# 首先验证命令安全性
is_valid, error_msg = self._validate_command(command)
if not is_valid:
raise ValueError(f"命令验证失败: {error_msg}")
# 确保已连接
if not self._client:
self.connect()
logger.info(f"执行 SSH 命令: {command}")
try:
# 执行命令
stdin, stdout, stderr = self._client.exec_command(command, timeout=timeout)
# 读取输出(处理编码问题)
output = stdout.read().decode("utf-8", errors="replace")
error = stderr.read().decode("utf-8", errors="replace")
# 获取退出码
exit_code = stdout.channel.recv_exit_status()
if exit_code != 0:
logger.warning(f"命令退出码 {exit_code}: {error}")
# 合并输出和错误信息
result = output
if error and not output:
result = error
elif error:
result = output + "\n\nSTDERR:\n" + error
return result.strip()
except Exception as e:
logger.error(f"命令执行失败: {e}")
raise RuntimeError(f"执行命令失败: {e}")
# ==================== 上下文管理器支持 ====================
def __enter__(self):
"""进入上下文时建立连接"""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""退出上下文时关闭连接"""
self.disconnect()
生成的日志
# MySQL/Linux 诊断报告
- **生成时间**: 2026-03-16 23:12:23
- **总耗时**: 125.44 秒
- **诊断轮次**: 16
## 配置信息
- **目标 SSH**: 172.20.20.15:22
- **目标 MySQL**: 172.20.20.15:3306
- **使用模型**: deepseek-ai/DeepSeek-V3.2
- **慢查询阈值**: 0.5秒
## 最终诊断
根据收集的信息,以下是MySQL和Linux系统的综合诊断报告:
1. 总体评价 - 健康状态评分:95/100
- 系统运行正常,负载极低,资源充足
- MySQL配置合理,无慢查询,连接数正常
2. 慢查询分析
- 慢查询数量:0(Slow_queries = 0)
- 慢查询阈值:10秒(long_query_time = 10.000000)
- 注意:当前阈值设置为10秒,远高于目标阈值0.5秒,建议调整为0.5秒以捕获更多潜在慢查询
3. QPS和负载分析
- MySQL运行时间:1623秒(约27分钟)
- 总查询数:Questions = 35
- 平均QPS:35/1623 ≈ 0.022 QPS(极低负载)
- 系统负载:0.00, 0.01, 0.05(非常健康)
4. 连接和线程状态
- 当前连接数:Threads_connected = 2
- 最大使用连接数:Max_used_connections = 2
- 最大允许连接数:max_connections = 151
- 线程运行数:Threads_running = 2
- 连接利用率:2/151 ≈ 1.3%(非常低)
5. 锁等待分析
- 表锁等待:Table_locks_waited = 0(无等待)
- InnoDB行锁等待:Innodb_row_lock_current_waits = 0(无等待)
- InnoDB行锁等待时间:Innodb_row_lock_time = 0
- 无锁争用问题
6. 内存使用状况
- 总内存:3.7G
- 已用内存:544M(14.7%)
- 可用内存:2.9G(78.4%)
- 缓存/缓冲:1.6G
- InnoDB缓冲池:16MB(1001页 * 16KB),使用率约12.2%
- 内存使用非常健康
7. CPU使用状况
- vmstat显示:用户态1%,系统态1%,空闲98%
- 无I/O等待(wa = 0%),无steal时间
- CPU资源充足
8. 磁盘空间和I/O
- 根分区:36G容量,14G已用(39%),22G可用
- 其他分区空间充足
- 无磁盘I/O统计数据(iostat未安装)
- 从vmstat看,bi/bo较低,I/O压力小
9. 其他重要观察
- 临时表:Created_tmp_disk_tables = 12,Created_tmp_tables = 25(磁盘临时表占比48%)
- 表缓存:Table_open_cache_hits = 3605,Table_open_cache_misses = 206(命中率94.6%)
- 连接错误:Aborted_clients = 1,Aborted_connects = 1(可忽略)
- InnoDB缓冲池命中率:(30229-845)/30229 ≈ 97.2%(良好)
10. 主要问题列表(按严重程度排序)
a) 慢查询阈值设置过高(10秒 vs 目标0.5秒)
b) 磁盘临时表使用比例较高(48%)
c) 系统监控工具不全(缺少iostat)
11. 具体的优化建议
a) 调整慢查询阈值:SET GLOBAL long_query_time = 0.5;(需在my.cnf中永久设置)
b) 优化查询以减少磁盘临时表:检查相关查询,增加tmp_table_size和max_heap_table_size
c) 安装系统监控工具:yum install sysstat(用于iostat等工具)
d) 考虑降低max_connections(当前151过高,可降至100左右)
e) 定期监控InnoDB缓冲池使用情况,考虑适当增加innodb_buffer_pool_size
f) 启用慢查询日志:SET GLOBAL slow_query_log = ON;
总体而言,系统处于非常健康的状态,负载极低,资源充足。主要需要调整监控配置以更好地捕获潜在问题。
## 诊断过程
### 第 1 轮
**工具**: `mysql_query.检查慢查询统计、MySQL运行状态和连接信息`
**输入**:
```
SHOW GLOBAL STATUS LIKE 'Slow_queries'; SHOW VARIABLES LIKE 'long_query_time'; SHOW GLOBAL STATUS LIKE 'Uptime'; SHOW GLOBAL STATUS LIKE 'Questions'; SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections'; SHOW GLOBAL VARIABLES LIKE 'max_connections';
```
**观察结果**:
```
Variable_name | Value
--------------+------
Slow_queries | 0
long_query_time | 10.000000
Uptime | 1553
Questions | 27
Threads_connected | 2
Max_used_connections | 2
max_connections | 151
```
### 第 2 轮
**工具**: `ssh_exec.检查系统负载、内存、磁盘和网络连接概览`
**输入**:
```
uptime; free -h; df -h; netstat -an | grep ESTABLISHED | wc -l; ss -s;
```
**观察结果**:
```
错误: 命令验证失败: 命令包含禁止的子串: |
```
### 第 3 轮
**工具**: `ssh_exec.分别检查系统负载、内存、磁盘和网络连接`
**输入**:
```
uptime
```
**观察结果**:
```
23:12:42 up 33 min, 2 users, load average: 0.00, 0.01, 0.05
```
### 第 4 轮
**工具**: `ssh_exec.检查内存使用情况`
**输入**:
```
free -h
```
**观察结果**:
```
total used free shared buff/cache available
Mem: 3.7G 544M 1.6G 11M 1.6G 2.9G
Swap: 3.9G 0B 3.9G
```
### 第 5 轮
**工具**: `ssh_exec.检查磁盘空间使用情况`
**输入**:
```
df -h
```
**观察结果**:
```
文件系统 容量 已用 可用 已用% 挂载点
/dev/mapper/centos-root 36G 14G 22G 39% /
devtmpfs 1.9G 0 1.9G 0% /dev
tmpfs 1.9G 0 1.9G 0% /dev/shm
tmpfs 1.9G 12M 1.9G 1% /run
tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup
/dev/sda1 1014M 142M 873M 14% /boot
tmpfs 378M 0 378M 0% /run/user/0
```
### 第 6 轮
**工具**: `ssh_exec.检查网络连接统计`
**输入**:
```
ss -s
```
**观察结果**:
```
Total: 589 (kernel 1020)
TCP: 13 (estab 4, closed 0, orphaned 0, synrecv 0, timewait 0/0), ports 0
Transport Total IP IPv6
* 1020 - -
RAW 0 0 0
UDP 6 3 3
TCP 13 7 6
INET 19 10 9
FRAG 0 0 0
```
### 第 7 轮
**工具**: `mysql_query.检查MySQL进程列表和InnoDB状态`
**输入**:
```
SHOW FULL PROCESSLIST; SHOW ENGINE INNODB STATUS;
```
**观察结果**:
```
Id | User | Host | db | Command | Time | State | Info
---+------+------+----+---------+------+-------+-----
5 | event_scheduler | localhost | None | Daemon | 1585 | Waiting on empty queue | None
9 | root | localhost | None | Sleep | 1368 | | None
12 | root | 172.20.20.1:5111 | ownit | Query | 0 | init | SHOW FULL PROCESSLIST
| | | | | | |
```
### 第 8 轮
**工具**: `ssh_exec.检查CPU使用率和进程信息`
**输入**:
```
top -bn1 | head -20
```
**观察结果**:
```
错误: 命令验证失败: 命令包含禁止的子串: |
```
### 第 9 轮
**工具**: `ssh_exec.检查系统CPU和进程状态`
**输入**:
```
vmstat 1 2
```
**观察结果**:
```
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 1653292 2108 1649372 0 0 70 371 177 230 1 1 98 0 0
2 0 0 1653292 2108 1649404 0 0 0 0 349 427 0 0 100 0 0
```
### 第 10 轮
**工具**: `ssh_exec.检查磁盘I/O统计`
**输入**:
```
iostat -x 1 2
```
**观察结果**:
```
bash: iostat: 未找到命令
```
### 第 11 轮
**工具**: `mysql_query.检查MySQL的锁等待和表使用情况`
**输入**:
```
SHOW OPEN TABLES WHERE In_use > 0; SHOW GLOBAL STATUS LIKE 'Table_locks_%'; SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';
```
**观察结果**:
```
错误: 执行查询失败: not enough arguments for format string
```
### 第 12 轮
**工具**: `mysql_query.分别检查MySQL的锁等待和表使用情况`
**输入**:
```
SHOW OPEN TABLES WHERE In_use > 0
```
**观察结果**:
```
查询执行成功,无结果返回。
```
### 第 13 轮
**工具**: `mysql_query.检查表锁和行锁状态`
**输入**:
```
SHOW GLOBAL STATUS LIKE 'Table_locks_%'; SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';
```
**观察结果**:
```
错误: 执行查询失败: not enough arguments for format string
```
### 第 14 轮
**工具**: `mysql_query.检查表锁状态`
**输入**:
```
SHOW GLOBAL STATUS LIKE 'Table_locks_%'
```
**观察结果**:
```
错误: 执行查询失败: not enough arguments for format string
```
### 第 15 轮
**工具**: `mysql_query.检查MySQL全局状态变量`
**输入**:
```
SHOW GLOBAL STATUS
```
**观察结果**:
```
Variable_name | Value
--------------+------
Aborted_clients | 1
Aborted_connects | 1
Acl_cache_items_count | 0
Binlog_cache_disk_use | 0
Binlog_cache_use | 4
Binlog_stmt_cache_disk_use | 0
Binlog_stmt_cache_use | 1
Bytes_received | 2761
Bytes_sent | 10275
Caching_sha2_password_rsa_public_key | -----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3zcuZv9VYFb51NWQa0Cj
PumXey9VY1mcnBpWbQhCRoltqMoec0pnxNV0D8xCirnlEcUhb7X8PtHdewOLqkEo
ZotxjdT+mQIERUTr45pImh6Wo2TcgyJVQzS0cGFoYCIRKczb/NgeGUCgucUcbpNy
uIJk1n1JYKVVwu/B4KxDphyPUgyY17Nn9y0anH6uAN7FLCvwuigIfRByENjV71MZ
SFjCsQImrJu+BU2/q+wJZhIIo+w5l2nE7OL3vslDW1sosrNrTwDNfKD89+xDpg9Q
JDu+KbVV+FgOrbku+jaE8JDqvHdMhMUDLs7y2ZzdKLCEKePF9PbwJ8UeqY86gMU5
bwIDAQAB
-----END PUBLIC KEY-----
Com_admin_commands | 0
Com_assign_to_keycache | 0
Com_alter_db | 0
Com_alter_event | 0
Com_alter_function | 0
Com_alter_instance | 0
Com_alter_procedure | 0
Com_alter_resource_group | 0
Com_alter_server | 0
Com_alter_table | 0
Com_alter_tablespace | 0
Com_alter_user | 4
Com_alter_user_default_role | 0
Com_analyze | 0
Com_begin | 0
Com_binlog | 0
Com_call_procedure | 0
Com_change_db | 1
Com_change_master | 0
Com_change_repl_filter | 0
Com_change_replication_source | 0
Com_check | 0
Com_checksum | 0
Com_clone | 0
Com_commit | 0
Com_create_db | 2
Com_create_event | 0
Com_create_function | 0
Com_create_index | 0
Com_create_procedure | 0
Com_create_role | 0
Com_create_server | 0
Com_create_table | 35
Com_create_resource_group | 0
Com_create_trigger | 0
Com_create_udf | 0
Com_create_user | 1
Com_create_view | 0
Com_create_spatial_reference_system | 0
Com_dealloc_sql | 0
Com_delete | 0
Com_delete_multi | 0
Com_do | 0
Com_drop_db | 0
Com_drop_event | 0
Com_drop_function | 0
Com_drop_index | 0
Com_drop_procedure | 0
Com_drop_resource_group | 0
Com_drop_role | 0
Com_drop_server | 0
Com_drop_spatial_reference_system | 0
Com_drop_table | 0
Com_drop_trigger | 0
Com_drop_user | 0
Com_drop_view | 0
Com_empty_query | 0
Com_execute_sql | 0
Com_explain_other | 0
Com_flush | 2
Com_get_diagnostics | 0
Com_grant | 1
Com_grant_roles | 0
Com_ha_close | 0
Com_ha_open | 0
Com_ha_read | 0
Com_help | 0
Com_import | 0
Com_insert | 0
Com_insert_select | 0
Com_install_component | 0
Com_install_plugin | 0
Com_kill | 0
Com_load | 0
Com_lock_instance | 0
Com_lock_tables | 0
Com_optimize | 0
Com_preload_keys | 0
Com_prepare_sql | 0
Com_purge | 0
Com_purge_before_date | 0
Com_release_savepoint | 0
Com_rename_table | 0
Com_rename_user | 0
Com_repair | 0
Com_replace | 0
Com_replace_select | 0
Com_reset | 0
Com_resignal | 0
Com_restart | 0
Com_revoke | 0
Com_revoke_all | 0
Com_revoke_roles | 0
Com_rollback | 0
Com_rollback_to_savepoint | 0
Com_savepoint | 0
Com_select | 0
Com_set_option | 10
Com_set_password | 0
Com_set_resource_group | 0
Com_set_role | 0
Com_signal | 0
Com_show_binlog_events | 0
Com_show_binlogs | 0
Com_show_charsets | 0
Com_show_collations | 0
Com_show_create_db | 0
Com_show_create_event | 0
Com_show_create_func | 0
Com_show_create_proc | 0
Com_show_create_table | 0
Com_show_create_trigger | 0
Com_show_databases | 0
Com_show_engine_logs | 0
Com_show_engine_mutex | 0
Com_show_engine_status | 1
Com_show_events | 0
Com_show_errors | 0
Com_show_fields | 0
Com_show_function_code | 0
Com_show_function_status | 0
Com_show_grants | 0
Com_show_keys | 0
Com_show_master_status | 0
Com_show_open_tables | 2
Com_show_plugins | 0
Com_show_privileges | 0
Com_show_procedure_code | 0
Com_show_procedure_status | 0
Com_show_processlist | 1
Com_show_profile | 0
Com_show_profiles | 0
Com_show_relaylog_events | 0
Com_show_replicas | 0
Com_show_slave_hosts | 0
Com_show_replica_status | 0
Com_show_slave_status | 0
Com_show_status | 8
Com_show_storage_engines | 0
Com_show_table_status | 0
Com_show_tables | 0
Com_show_triggers | 0
Com_show_variables | 3
Com_show_warnings | 0
Com_show_create_user | 0
Com_shutdown | 0
Com_replica_start | 0
Com_slave_start | 0
Com_replica_stop | 0
Com_slave_stop | 0
Com_group_replication_start | 0
Com_group_replication_stop | 0
Com_stmt_execute | 0
Com_stmt_close | 0
Com_stmt_fetch | 0
Com_stmt_prepare | 0
Com_stmt_reset | 0
Com_stmt_send_long_data | 0
Com_truncate | 0
Com_uninstall_component | 0
Com_uninstall_plugin | 0
Com_unlock_instance | 0
Com_unlock_tables | 0
Com_update | 0
Com_update_multi | 0
Com_xa_commit | 0
Com_xa_end | 0
Com_xa_prepare | 0
Com_xa_recover | 0
Com_xa_rollback | 0
Com_xa_start | 0
Com_stmt_reprepare | 0
Connection_errors_accept | 0
Connection_errors_internal | 0
Connection_errors_max_connections | 0
Connection_errors_peer_address | 0
Connection_errors_select | 0
Connection_errors_tcpwrap | 0
Connections | 12
Created_tmp_disk_tables | 12
Created_tmp_files | 5
Created_tmp_tables | 25
Current_tls_ca | ca.pem
Current_tls_capath |
Current_tls_cert | server-cert.pem
Current_tls_cipher |
Current_tls_ciphersuites |
Current_tls_crl |
Current_tls_crlpath |
Current_tls_key | server-key.pem
Current_tls_version | TLSv1.2
Delayed_errors | 0
Delayed_insert_threads | 0
Dela
... [已截断]
```
### 第 16 轮
**工具**: `agent.final_answer`
**输入**:
**观察结果**:
```
根据收集的信息,以下是MySQL和Linux系统的综合诊断报告:
1. 总体评价 - 健康状态评分:95/100
- 系统运行正常,负载极低,资源充足
- MySQL配置合理,无慢查询,连接数正常
2. 慢查询分析
- 慢查询数量:0(Slow_queries = 0)
- 慢查询阈值:10秒(long_query_time = 10.000000)
- 注意:当前阈值设置为10秒,远高于目标阈值0.5秒,建议调整为0.5秒以捕获更多潜在慢查询
3. QPS和负载分析
- MySQL运行时间:1623秒(约27分钟)
- 总查询数:Questions = 35
- 平均QPS:35/1623 ≈ 0.022 QPS(极低负载)
- 系统负载:0.00, 0.01, 0.05(非常健康)
4. 连接和线程状态
- 当前连接数:Threads_connected = 2
- 最大使用连接数:Max_used_connections = 2
- 最大允许连接数:max_connections = 151
- 线程运行数:Threads_running = 2
- 连接利用率:2/151 ≈ 1.3%(非常低)
5. 锁等待分析
- 表锁等待:Table_locks_waited = 0(无等待)
- InnoDB行锁等待:Innodb_row_lock_current_waits = 0(无等待)
- InnoDB行锁等待时间:Innodb_row_lock_time = 0
- 无锁争用问题
6. 内存使用状况
- 总内存:3.7G
- 已用内存:544M(14.7%)
- 可用内存:2.9G(78.4%)
- 缓存/缓冲:1.6G
- InnoDB缓冲池:16MB(1001页 * 16KB),使用率约12.2%
- 内存使用非常健康
7. CPU使用状况
- vmstat显示:用户态1%,系统态1%,空闲98%
- 无I/O等待(wa = 0%),无steal时间
- CPU资源充足
8. 磁盘空间和I/O
- 根分区:36G容量,14G已用(39%),22G可用
- 其他分区空间充足
- 无磁盘I/O统计数据(iostat未安装)
- 从vmstat看,bi/bo较低,I/O压力小
9. 其他重要观察
- 临时表:Created_tmp_disk_tables = 12,Created_tmp_tables = 25(磁盘临时表占比48%)
- 表缓存:Table_open_cache_hits = 3605,Table_open_cache_misses = 206(命中率94.6%)
- 连接错误:Aborted_clients = 1,Aborted_connects = 1(可忽略)
- InnoDB缓冲池命中率:(30229-845)/30229 ≈ 97.2%(良好)
10. 主要问题列表(按严重程度排序)
a) 慢查询阈值设置过高(10秒 vs 目标0.5秒)
b) 磁盘临时表使用比例较高(48%)
c) 系统监控工具不全(缺少iostat)
11. 具体的优化建议
a) 调整慢查询阈值:SET GLOBAL long_query_time = 0.5;(需在my.cnf中永久设置)
b) 优化查询以减少磁盘临时表:检查相关查询,增加tmp_table_size和max_heap_table_size
c) 安装系统监控工具:yum install sysstat(用于iostat等工具)
d) 考虑降低max_connections(当前151过高,可降至100左右)
e) 定期监控InnoDB缓冲池使用情况,考虑适当增加innodb_buffer_pool_size
f) 启用慢查询日志:SET GLOBAL slow_query_log = ON;
总体而言,系统处于非常健康的状态,负载极低,资源充足。主要需要调整监控配置以更好地捕获潜在问题。
```
---
*由 MySQL 诊断 Agent 自动生成*