ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

hermes-agent:用大模型自动优化MySQL慢查询的实战指南

hermes-agent:用大模型自动优化MySQL慢查询的实战指南 hermes-agent 这个名字起得有点中二但它确确实实帮我少加了不少班。先说背景。我在一家做电商中台的公司既要写业务代码也兼着看数据库性能。最让人头疼的不是线上突然宕机而是每天一打开慢查询平台几千条慢日志堆在眼前大部分长得很像同样是查订单换个用户ID就慢一次。人肉分析这些日志是纯体力活但要是不分析等大促流量一压数据库 CPU 直接拉满到时候所有人都在群里你。hermes-agent 就是冲着这件事来的。它是一个基于大语言模型LLM的 MySQL 慢查询优化智能体核心能力是自动完成四件事读取慢查询日志、获取相关表结构和索引信息、对候选 SQL 执行 EXPLAIN、最后生成带根因分析和可执行方案的优化建议。它不会直接改线上库只负责把“数据库哪里不舒服”翻译成“业务代码该怎么改”。Hermes 在神话里是神的信使这个项目做的事也类似——在数据库和业务开发之间传话所以我给它起了这个名字。如果你也是后端开发、DBA 或者 SRE手头有大量慢 SQL 没空细看或者你正在研究怎么把大模型接进数据库运维流程这篇博文应该能给你不少参考。我会把设计思路、核心代码、配置细节和一些测试过程中的坑一次性全交代清楚。1. 整体设计与思路拆解为什么让大模型来管慢查询1.1 项目定位在数据库和业务之间当“翻译官”先讲清楚一个问题慢查询优化这件事难点到底在哪。单纯的“找出慢 SQL”并不难打开 slow log 或者用 pt-query-digest 扫一轮就能列出来。难的是接下来的三步第一判断这条 SQL 慢在哪是缺索引、索引失效、还是本身要扫描的数据量太大第二给出修改建议是加索引、改 SQL 写法、还是调整表结构第三让业务开发能看懂并愿意改。传统脚本能做到第一步顶多帮你把日志聚个类但第二步和第三步基本做不了。拿一个很常见的案例来说WHERE user_id ? ORDER BY created_at DESC这条 SQL 走了全表扫描原因是缺复合索引。脚本只能告诉你“这条 SQL 全表扫描涉及 10 万行”但它说不清为什么user_id上有单列索引却还是全表扫描也不知道要改成(user_id, created_at)这种复合索引才能同时满足过滤和排序。hermes-agent 的设计目标就是把这套“诊断加处方”的能力自动化。它面向的使用场景很具体日常巡检每天定时把慢日志喂给它输出一份当天需要处理的 TOP 问题清单。大促前体检压测前把线上历史慢 SQL 全部跑一遍提前发现索引缺口。SQL Review开发提测时把新 SQL 丢进来在还没上生产之前就发现隐患。适合用它的人不是指望它替代 DBA而是那些没有专职 DBA、或者 DBA 已经忙不过来的团队。你不需要懂 Prompt 工程也不需要懂索引优化原理只需要能把项目部署起来然后像跟同事请教一样问它“这条 SQL 为什么这么慢怎么改”1.2 为什么是 Agent不是普通的分析脚本一开始我想得很简单直接写个脚本把慢日志整理成文本一股脑丢给大模型 API让它返回优化建议。试了几天发现效果很不稳定原因在于这种“一次提问”的方式有一个致命缺陷模型拿不到数据库的实时信息。举个例子你让模型分析一条订单查询 SQL它可能凭经验猜“这里应该加索引”。但它不知道这张表现在已经有几个索引、数据量多大、status字段的区分度高不高。没有这些信息建议就只能是泛泛而谈甚至可能出现“我已经建了这个索引你怎么还说慢”的尴尬情况。这就是 Agent 和普通脚本的核心区别Agent 不是一次性提问而是让模型进入一个“观察—思考—行动—再观察”的循环。模型发现自己缺信息的时候可以主动调用工具去查比如查表结构、查索引、执行 EXPLAIN拿到结果之后再继续推理。这个循环在代码上就是多轮调用大模型接口直到模型觉得自己已经得出结论、不再请求更多工具为止。听起来复杂但用现在的 Function Calling 机制实现其实很清晰。模型不是真的会调用函数它只是输出一个结构化的“我想调用某个工具参数是什么”的请求由我们这边的代码真正执行数据库查询再把结果返回给模型继续推理。这个设计带来的好处是模型的所有判断都是基于真实数据而不是基于训练语料里那点“通用经验”。它说这条 SQL 该走索引是因为它真的看到possible_keys为空它说排序能用索引避免 filesort是因为它真的看到了Extra列里的Using filesort。这就让输出结果有了可验证性而不是模型凭空编。2. 核心细节解析从慢日志到优化建议的完整链路2.1 慢查询日志的采集与预处理Agent 要分析的第一手材料是慢查询日志但日志不能直接喂给大模型。原始慢日志文件里有大量无关信息比如连接 ID、查询耗时、锁等待时间而且一个下午的日志能有几万条直接塞进上下文窗口肯定爆炸。我的做法是先做两轮预处理。第一轮是采样和聚类。先用 pt-query-digest 这类工具按 SQL 指纹对慢日志做分组把“只是参数不同、结构相同”的 SQL 归成一类每类取一条代表 SQL。这样几万条日志通常能压缩成几十类再从里面挑出耗时占比最高的前十个类进入下一轮。第二轮是格式化。把代表 SQL 转成一行紧凑的文本同时附上它出现的次数、平均耗时、最大耗时这几个关键指标。最终喂给模型的结构大概是这样的慢查询分组 #1 SQL指纹: SELECT * FROM orders WHERE user_id ? AND status ? ORDER BY created_at DESC LIMIT ? 出现次数: 1247 平均耗时: 2.3s 最大耗时: 8.7s 样例SQL: SELECT * FROM orders WHERE user_id 12345 AND status PAID ORDER BY created_at DESC LIMIT 20这里有一个容易被忽略的细节SQL 指纹的生成规则要足够严谨否则聚类效果会大打折扣。比如数字字面量12345和字符串字面量PAID都要替换成占位符但表名、列名、关键字必须保留原样。我最初用简单的正则替换结果把LIMIT 20和LIMIT 100分成了两类后来改成按 token 解析才稳定下来。MySQL 侧还需要先确认慢查询日志是开启的。可以在 MySQL 里执行SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL log_queries_not_using_indexes ON;long_query_time设成 1即超过 1 秒的记录log_queries_not_using_indexes开启后即使执行很快但没用索引的 SQL 也会被记入日志这能协助发现潜在隐患。实际测试中这个参数很有用因为有些慢查询线上流量少的时候测不出来等流量上来才暴露但有它就能提前暴露没走索引的 SQL。2.2 Function Calling给 Agent 装上数据库工具Agent 能不能做出靠谱的判断很大程度上取决于它手里的工具好不好用。我在 hermes-agent 里设计了一组最小但够用的工具集每个工具都遵循一个原则输入参数少、返回结果小、只做只读操作。工具名作用返回内容备注get_table_schema获取表结构字段名、类型、是否可空、默认值参数只有表名get_table_indexes获取表索引索引名、索引列、是否唯一用于判断现有索引能否覆盖 SQLexplain_query执行 EXPLAIN访问类型、possible_keys、实际 key、扫描行数、Extra不执行原 SQL只执行 EXPLAINget_table_stats获取表统计信息行数、数据大小、索引大小帮助模型判断是否需要全表扫描工具定义用的是 OpenAI 兼容的 Function Calling 格式。核心代码骨架是这样的tools [ { type: function, function: { name: get_table_schema, description: 获取指定MySQL表的字段结构包括字段名、类型、是否可空、默认值, parameters: { type: object, properties: { table: {type: string, description: 表名} }, required: [table] } } }, { type: function, function: { name: explain_query, description: 对指定SQL执行EXPLAIN返回执行计划注意只执行EXPLAIN不会执行原SQL, parameters: { type: object, properties: { sql: {type: string, description: 完整的SQL语句} }, required: [sql] } } } ]工具实现部分直接调用 pymysql 执行查询import json import pymysql def execute_tool(name: str, arguments: str, conn) - str: args json.loads(arguments) cursor conn.cursor(pymysql.cursors.DictCursor) try: if name get_table_schema: cursor.execute(SHOW FULL COLUMNS FROM {}.format(args[table])) rows cursor.fetchall() return json.dumps(rows, ensure_asciiFalse, defaultstr) if name get_table_indexes: cursor.execute(SHOW INDEX FROM {}.format(args[table])) rows cursor.fetchall() return json.dumps(rows, ensure_asciiFalse, defaultstr) if name explain_query: cursor.execute(EXPLAIN args[sql]) rows cursor.fetchall() return json.dumps(rows, ensure_asciiFalse, defaultstr) if name get_table_stats: cursor.execute(SELECT table_rows, data_length, index_length FROM information_schema.tables WHERE table_schema DATABASE() AND table_name %s, args[table]) rows cursor.fetchall() return json.dumps(rows, ensure_asciiFalse, defaultstr) finally: cursor.close() return unknown tool这里有几个实际踩过的坑。第一个是SHOW FULL COLUMNS比DESC多一些信息包括字符集和权限注释对模型判断编码不一致导致的隐式转换很有帮助。第二个是 EXPLAIN 工具必须做 SQL 白名单校验只允许以 SELECT 开头的 SQL防止模型被“提示注入”后输出EXPLAIN DELETE FROM ...这种危险语句。第三个是返回结果里不要带 BLOB/TEXT 字段的实际内容只给类型和长度否则一个长文本字段就能把上下文窗口撑爆。2.3 提示词设计让模型既敢说想法又不敢乱执行工具是能力提示词是边界。提示词如果写得不够清楚模型就会出现两种情况要么唯唯诺诺什么都让你“仅供参考请以实际情况为准”要么过于自信直接给出“请执行以下 DDL”这种危险输出。我在 System Prompt 里固定了几条硬规则你是一名资深的MySQL数据库优化专家你的任务是分析给定的慢查询SQL并给出优化建议。 硬性规则 1. 只能做只读分析绝对不能执行INSERT、UPDATE、DELETE、ALTER、CREATE、DROP等任何非SELECT语句。 2. 分析过程中必须至少调用一次explain_query来获取真实执行计划禁止只凭经验猜测。 3. 如果发现缺少表结构或索引信息必须先调用get_table_schema和get_table_indexes补齐信息。 4. 最终输出必须包含四个部分问题定位、原因分析、优化建议、风险提示。 5. 优化建议必须基于实际查询到的执行计划不能凭空臆测。 6. 如果建议新增索引必须说明索引的具体列顺序并评估是否会影响现有写性能。这几条规则看着简单但每一条都是从实际测试废墟里捞出来的。第 2 条解决了“模型只看 SQL 文本就给出通用建议”的问题第 4 条解决了“输出结构每次都不一样、根本无法程序化解析”的问题第 6 条则专门应对数据库优化最经典的翻车现场——只看 SELECT 慢就建议加索引完全不管那张表是不是每秒几万次写入的流水表。还有一个值得细说的点模型能区分“事实”和“推测”。在提示词里加一条“如果无法从工具结果中确认原因请明确说‘无法确认’并列出可能的排查方向”能显著降低输出里武断结论的比例。实测下来加了这条之后生成的报告质量提升非常明显。3. 实操过程部署 hermes-agent 并跑通一次优化3.1 环境准备与数据库最小权限配置整个项目依赖非常简单Python 3.10 以上版本核心库四个openaiOpenAI 兼容接口的客户端、pymysql、pyyaml、pt-query-digest可选用于日志聚类。我通常把项目放在一个独立的虚拟环境里python3 -m venv venv source venv/bin/activate pip install openai pymysql pyyaml数据库账号是重中之重。给 Agent 用的账号必须有最小权限原则是“只能看不能动”。我建了一个专门的只读账号CREATE USER hermes_reader% IDENTIFIED BY change_me_to_your_password; GRANT SELECT, SHOW VIEW ON shop.* TO hermes_reader%; GRANT SELECT ON mysql.slow_log TO hermes_reader%; FLUSH PRIVILEGES;这里没有授予 INSERT/UPDATE/DELETE也没有授予 DDL 权限。即使模型真的被恶意 Prompt 注入想把“可以执行 DDL”变成现实它手里的账号也什么都干不了。这个环节是整个项目的安全底线绝对不能省。配置文件我放在config.yaml结构如下mysql: host: 127.0.0.1 port: 3306 user: hermes_reader password: change_me_to_your_password database: shop llm: base_url: https://your-llm-endpoint/v1 model: your-model-name api_key_env: LLM_API_KEY temperature: 0.2 agent: max_steps: 8 slow_log_path: /var/log/mysql/slow.log top_n: 5temperature我调到了 0.2因为数据库优化是事实推理任务不需要创造性温度越高越容易胡说。3.2 Agent 主循环代码骨架整个 Agent 的核心是一个循环把消息发给大模型模型返回要么是最终答案要么是工具调用请求如果是后者就执行工具、把结果追加进消息列表继续下一轮。代码骨架如下import json import os import pymysql from openai import OpenAI client OpenAI( api_keyos.environ.get(config[llm][api_key_env]), base_urlconfig[llm][base_url] ) conn pymysql.connect( hostconfig[mysql][host], portconfig[mysql][port], userconfig[mysql][user], passwordconfig[mysql][password], databaseconfig[mysql][database] ) def run_agent(sql_text: str) - str: messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: f请分析以下慢查询SQL\n{sql_text}} ] for _ in range(config[agent][max_steps]): response client.chat.completions.create( modelconfig[llm][model], messagesmessages, toolstools, tool_choiceauto ) msg response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for call in msg.tool_calls: result execute_tool(call.function.name, call.function.arguments, conn) messages.append({ role: tool, tool_call_id: call.id, name: call.function.name, content: result }) return 已达最大轮数未生成分析结果max_steps我设成 8。正常分析一条 SQL 需要 2 到 5 轮工具调用8 轮足够超过这个数说明模型陷入死循环及时止损。3.3 一次典型案例的完整优化过程我拿一个线上真实存在的场景来演示。有一张订单表orders大概 100 万行原始 SQL 是分页查询SELECT * FROM orders WHERE user_id 12345 AND status PAID ORDER BY created_at DESC LIMIT 20;Agent 拿到 SQL 后的第一轮推理通常会这样做先调用get_table_schema(orders)和get_table_indexes(orders)确认表上有哪些字段和索引。假设现有索引只有主键id和单列索引idx_user_id(user_id)。第二轮它会调用explain_query拿到真实执行计划。我把结果简化成实际常见的形态type: ALL possible_keys: idx_user_id key: NULL rows: 1000000 Extra: Using where; Using filesort看到这个执行计划Agent 的判断逻辑大致是typeALL说明全表扫描keyNULL说明优化器没有选择idx_user_id。这里有一个白皮书里不常写、但实战特别重要的知识点当user_id12345这个条件选择性较低比如该用户有几十万条订单优化器觉得回表成本太高会宁可全表扫。同时ORDER BY created_at没有索引可用Extra列里出现了Using filesort排序也得在内存里做。Agent 最终给出的建议是新增一个复合索引ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, created_at);索引列的顺序是有讲究的。等值条件的user_id和status放前面范围排序的created_at放最后这样既能等值匹配又能在索引内完成排序消除 filesort。加上索引后的执行计划变成type: ref key: idx_user_status_time rows: 156 Extra: Using index condition扫描行数从 100 万掉到 156查询耗时从秒级降到毫秒级。整个分析过程从丢入 SQL 到拿到结果实测在 10 秒左右。4. 常见问题与排查技巧实录4.1 模型幻觉给了不存在的字段怎么办最开始测试时模型的建议里出现过不存在的字段名。比如它建议ORDER BY pay_time DESC但表里实际只有created_at。原因很好理解模型在训练语料里见多了支付系统的订单表以为pay_time是通用字段就没查表结构直接脑补。解决方案分两层。第一层是在工具调用顺序上做限制提示词里写明“在给出任何字段建议前必须调用 get_table_schema 确认字段存在”。第二层是在结果输出后加一道程序化校验解析推荐 SQL 里出现的所有字段名跟表结构做一次比对不存在的直接标红拦截。这两个机制叠加之后这类问题基本绝迹。4.2 上下文爆炸慢日志太多怎么喂给模型另一个高频问题是单条慢日志如果包含超长的 WHERE 条件比如IN后面带了几千个 IDSQL 文本本身就超出了模型上下文限制。更常见的是分析一批慢日志时全部塞进上下文模型还没分析几条就已经到上限了。我最后的处理方式是“先聚类再抽样最后分片”。先按指纹聚类成若干组每组取一条样本把样本 SQL 做截断和归一化比如把超长IN列表折叠成IN (?...)。如果聚完类批次还是太大就按耗时占比挑 TOP N我通常用 5剩下的只保留一行统计摘要不进入模型分析列表。这样既控制了上下文占用又保证最耗时的 SQL 一定会被覆盖到。4.3 建议容易跑偏如何让结论聚焦问题模型还有个小毛病就是容易“发散”。分析一条 SQL 时一会儿说索引不够一会儿说分页太深一会儿又说建议做读写分离最后给出的结论像是大杂烩用户根本不知道该先做什么。我的处理方式是在提示词里加了一条价值判断规则只分析和当前慢查询直接相关的原因按照“影响最大、改动最小、收益最高”的优先级排序并且每条建议标注预期收益和风险。这让输出从“一堆正确的废话”变成了“一页能执行的清单”。实际操作中如果发现模型还是发散可以进一步把输出格式从纯文本改成结构化 JSON由前端渲染字段固定在问题、原因、建议、风险四个维度内。4.4 安全红线生产库连接的权限控制最后单独花点篇幅说安全因为这可能是整个项目里最不能出错的环节。我在部署文档里写了一条铁律Agent 的数据库连接必须使用只读账号且只能访问配置里指定的业务库不能有跨库访问权限。有人会问加索引不就是要执行 DDL 吗我的答案是Agent 负责出方案DDL 的执行必须走人工审核流程。在实际项目里我让 Agent 输出的建议里包含完整 SQL 和风险评估然后由值班 DBA 复制去执行。如果你想让流程自动化到“Agent 直接改库”的程度必须额外加两层保护一是配置中心里的开关默认关闭二是所有 DDL 操作记录审计日志并限制只能在工作时间窗口内执行。5. 这个项目还能怎么用场景与扩展方向5.1 从“事后救火”到“事前体检”做完了慢查询诊断我发现同样的 Agent 框架可以延伸出两个很有价值的使用场景。第一个是 SQL Review。团队里开发提测的 SQL 可以自动跑一遍 Agent在代码合并之前就发现潜在的性能问题。这比等上线后慢日志报警再处理要划算得多。具体做法是把 CI 里检测到的新 SQL 发给 Agent返回结果里带上“是否有全表扫描”“是否隐式转换”“是否命中索引”这几个标记不通过的直接阻塞合并。第二个是大促前的索引预检。每次大促前把去年同期的慢日志导出来用 Agent 全量跑一遍生成一份“当前索引缺口清单”。我连续做了两个季度效果非常好几乎把能提前踩的坑都踩完了。5.2 从单机脚本到团队服务平台单机脚本的问题在于每个开发想看分析报告都要来问你。后来我把 Agent 封装成了一个 Web 服务前端就一个输入框粘贴 SQL 点击分析后端起一个异步任务跑 Agent结果落库前端轮询展示。再往后加了简单的 Webhook 通知分析完成自动推送到群消息。再扩展一点可以把分析历史存下来按表、按 SQL 指纹维度做趋势统计看哪些表被反复报告慢查询帮 DBA 定位“问题最多的十张表”。这个统计维度对推动业务侧做表结构重构非常有用因为数据摆在那里比口头强调有说服力得多。整套项目做下来我最大的体会是大模型真正解决的不是“会不会分析”的问题而是“愿不愿意一遍一遍分析”的问题。MySQL 优化的方法论本身很成熟难的是坚持每天对每一条慢日志执行同样的标准流程。hermes-agent 把这套流程变成了自动化把人的时间从重复劳动里解放出来让我有精力去处理那些机器判断不了的疑难杂症。如果你也被慢查询困扰强烈建议照这个思路搭一个试试先用线上真实慢日志回放测试慢慢调整提示词和工具集跑顺之后你会回来感谢自己。
返回列表