DeepSeek Harness实战:构建MySQL慢查询分析插件,自动定位性能瓶颈

发布时间:2026/9/14 6:05:36
DeepSeek Harness实战:构建MySQL慢查询分析插件,自动定位性能瓶颈
做数据库性能优化的人一定有过这样的体验面对一条跑了十几秒的SQL翻来覆去地看就是看不出问题出在哪。表结构也看了索引也在JOIN 关系也不复杂可它就是慢。这种时候如果有个助手能帮我把 SQL 拆开、把执行计划讲清楚、再把优化建议摆到面前效率会高很多。这就是我这次用 DeepSeek Harness 写数据库查询分析插件的初衷。DeepSeek Harness 说白了就是一套能让大模型真正动手干活的执行框架它不满足于纯粹的问答而是让模型可以调用外部工具、读取外部数据、在沙箱里跑代码最后给出一个经过验证的结论。这次我拿它做了个实战项目连接 MySQL 官方 sakila 示例库写一个查询分析插件输入任何一条 SQL自动输出可读的分析报告包含表关系梳理、执行计划解读、性能瓶颈定位和优化建议。文章后面全部是基于这套流程的真实踩坑记录代码和步骤都能直接抄。1. 项目背景与整体设计思路1.1 为什么用 Harness 而不是直接问模型很多人会问一个很直接的问题查 SQL 慢的原因直接把执行计划贴给 ChatGPT 或者 DeepSeek 对话窗口不行吗行但有几个现实问题。第一对话窗口是一次性的。你要先把表结构导出来再把慢 SQL 贴进去模型回答完你还要把建议拿回命令行一条条手动验证来回切换的成本非常高。第二模型没有真实的数据库视角。它看不到你的表有多少行、索引区分度如何、实际执行走了哪个索引只能基于你给的碎片信息猜测。第三提示词稍微写得含糊一点模型就会返回一堆正确的废话比如建议创建索引——但你到底应该在哪几列上建复合索引怎么验证这个索引有效它不会帮你做。Harness 解决的就是这三件事。它把模型嵌入到一个有工具、有环境、有反馈闭环的流程里插件自己连接数据库、自己执行 EXPLAIN、自己读取表统计信息、自己跑一个小的基准测试这些信息全部作为上下文喂给模型再由模型生成针对性建议。最后插件还能把建议里的索引语句自动执行一次用优化前后的执行计划对比来验证效果。这套闭环就是 Harness 的价值所在模型不是凭记忆答题而是在看得到数据的前提下做判断。1.2 为什么选 sakila 示例库选 sakila 不花哨但非常合理。它是 MySQL 官方维护的示例库模拟了一个影视租赁店的业务场景包含 customer、film、inventory、rental、payment 等 16 张表表之间有清晰的主外键关系数据量控制在几万行的量级。这个量级很妙。几万行的表既能让执行计划分析有实际意义比如全表扫描和索引扫描的时间差能测出来又不会像生产环境动辄上千万行的表那样难以复现。对开发插件来说sakila 是一个完美的实验场你不需要造数据不需要担心误删所有的表结构、外键关系、索引设计都是官方标准化的跑出来的结论具备通用性。还有一个不可忽视的原因是sakila 里的 rental 表在 2005 到 2006 年间有 16044 条记录配合 customer 和 staff 表做 JOIN 时优化器会产生多种可能的执行路径——索引选择、join 顺序、临时表使用一套查询分析插件需要的场景它全都能覆盖。这也是我把它作为基准库的原因。1.3 插件要解决的核心问题明确了使用框架和数据库接下来要看这个插件具体解决什么问题。我的目标范围很明确自动解析 SQL提取涉及的表、JOIN 关系、WHERE 条件和排序字段采集执行计划包括访问类型、扫描行数、额外信息计算关键性能指标定位可能的瓶颈把采集到的信息转成结构化的上下文交给 DeepSeek 生成优化建议支持对建议中的索引方案做一次前后对比验证整个插件不追求替代 DBA而是做一个从分析到建议再到验证的闭环工具。这个定位很重要它不是一个解释执行计划的玩具而是一个能给决策提供依据的辅助系统。下文的每一模块都围绕这五个目标展开。2. 环境准备与基础搭建2.1 DeepSeek Harness 的安装与配置先说 DeepSeek Harness 的安装。不同的发行版本装法略有差异但核心思路一样它是一个本地运行的 Agent 执行框架启动后通过命令行或桌面端交互模型可以调用注册进来的工具插件。我用的是命令行版本安装过程大致这样# 创建独立的 Python 环境避免依赖冲突 python -m venv harness-env source harness-env/bin/activate # 安装 harness 主程序 pip install deepseek-harness # 初始化配置目录 harness init初始化完成后需要在配置文件中填入模型接入信息。如果是调用 DeepSeek 的云端 API配置一个api_key和base_url就行如果是本地部署模型则要指定模型服务地址和模型名称。这一步有一个非常容易踩的坑网络代理环境会干扰 API 连接如果启动后一直报连接超时先查环境变量里的HTTP_PROXY和HTTPS_PROXY临时清掉再试。配置完成后用harness doctor命令自检环境。这个命令会检查 Python 版本、插件目录权限、模型接口连通性、沙箱执行能力。我用的时候遇到一个问题/tmp目录挂了noexec挂载选项导致沙箱里的临时脚本无法执行doctor 直接红牌。解决方式是把沙箱工作目录改到家目录下的子目录。2.2 MySQL 与 sakila 数据准备数据库这边我用的是 MySQL 8.0。sakila 的导入非常简单从 MySQL 官方示例库仓库下载三个文件sakila-schema.sql、sakila-data.sql和sakila.mwb前两个是建表和导数据第三个是建模文件不需要也可以。mysql -uroot -p sakila-schema.sql mysql -uroot -p sakila-data.sql # 验证导入 mysql -uroot -p -e USE sakila; SHOW TABLES; SELECT COUNT(*) FROM rental;导入之后我建议立刻做两件事。第一确认外键约束是否启用SELECT FOREIGN_KEY_CHECKS;如果返回 0说明外键检查是关的虽然对现有数据没影响但后续如果要测试 DELETE 行为会有干扰。第二给连接账号开一个专用的权限不建议直接用 root。我是这样建的CREATE USER sakila_analyzerlocalhost IDENTIFIED BY your_password; GRANT SELECT, SHOW VIEW ON sakila.* TO sakila_analyzerlocalhost; GRANT CREATE, ALTER, DROP ON sakila.* TO sakila_analyzerlocalhost;最后一行是为了让插件能创建和删除用于验证的索引如果只做分析不需要验证功能可以不给。2.3 工程目录与依赖插件本身我写成了独立的 Python 包方便 Harness 加载。目录结构如下db-analyzer/ ├── __init__.py ├── plugin.py # Harness 插件入口注册命令 ├── core/ │ ├── sql_parser.py # SQL 解析模块 │ ├── explain.py # 执行计划采集模块 │ ├── metrics.py # 性能指标计算 │ └── reporter.py # 报告生成模块 ├── prompts/ │ └── analyze.j2 # 交给 DeepSeek 的提示词模板 └── requirements.txt依赖方面只有三个关键库PyMySQL负责数据库连接sqlparse负责 SQL 解析jinja2负责提示词模板渲染。不需要额外的 ORM因为我们要直接拿游标执行 EXPLAIN 语句ORM 反而会把信息藏起来。3. 插件架构与核心机制3.1 Harness 插件的注册与命令注入在动手写功能之前先要看懂 Harness 的插件机制。不同的 Harness 实现会有差异我这次用的版本采用的是装饰器注册 命令分发模式插件包被加载后通过注册函数把可执行命令暴露给模型模型在推理过程中看到这几个命令后会在合适的时机调用它们。核心的注册代码是这样的# plugin.py from harness import register_command, plugin_metadata plugin_metadata( namedb-analyzer, version0.1.0, descriptionMySQL query analyzer based on sakila sample database, ) class DBAnalyzerPlugin: def __init__(self, config): self.config config self._init_db_pool() register_command(nameanalyze_query) def analyze_query(self, sql: str) - str: Parse and analyze a SQL query, return an analysis report. result self._run_analysis(sql) return self._format_report(result) register_command(nameexplain_only) def explain_only(self, sql: str) - str: Only return the EXPLAIN plan, without analysis. ...register_command这个装饰器是插件框架提供的入口被标记的方法会在模型执行工具调用时按名称被检索到。一个需要注意的点是命令描述要写清楚因为模型是通过描述来决定什么时候用这个工具的。比如analyze_query的描述是解析并分析一条 SQL 查询并返回分析报告模型的工具选择准确率就会明显更高。描述含糊的话它可能会在只需要 EXPLAIN 的时候调用了完整分析浪费一次上下文窗口。3.2 五层结构的插件模块划分插件内部我把它分成五个层次每个层次只做一件事第一层是接入层也就是plugin.py里的命令注册方法负责接收模型传过来的 SQL返回格式化的报告。这一层不做任何计算只是翻译官。第二层是解析层sql_parser.py里的主要任务是从一条完整 SQL 里提取结构化信息涉及哪些表、哪些 JOIN、WHERE 条件里的列、ORDER BY 和 GROUP BY 字段、LIMIT 是否存在。这些信息是后续所有分析的地基。第三层是采集层explain.py负责连接 MySQL执行 EXPLAIN 语句拿到访问类型、扫描行数、索引使用、额外信息还通过查询information_schema.tables获取表的行数和数据大小作为辅助指标。第四层是计算层metrics.py把采集到的原始信息换算成可解读的性能指标估算扫描比例、是否出现临时表和文件排序、索引的基本盘够不够。第五层是生成层reporter.py和提示词模板一起工作把前面四层的数据组装成 DeepSeek 可读的上下文调用模型生成优化建议并整理成最终报告必要时执行索引验证。这个分层最大的好处是每一层都能单独测试。我在开发中经常单独跑解析层和采集层确认数据正确后才把串联起来避免了一锅端的排查地狱。3.3 数据流设计从 SQL 到报告整个插件的数据流非常线性。模型收到用户的问题后把待分析的 SQL 作为参数传给analyze_query命令。命令把 SQL 交给解析层解析出来的表清单传给采集层采集层拿着表清单去查 MySQL 元数据同时把原始 SQL 交给 EXPLAIN 执行计算层把 EXPLAIN 的结果和元数据合并成一组指标最后生成层把这些指标渲染进提示词模板调用模型完成分析报告。这些步骤里有一步特别容易忽略EXPLAIN 拿到的是优化器估算的行数并不等同实际返回行数。所以在采集层里我还加了一个实际执行采样的开关可以在小型表比如 sakila上直接EXPLAIN ANALYZE拿到真实执行时间和真实循环次数作为估算值的校准参考。sakila 数据量小跑EXPLAIN ANALYZE几乎没有成本但对于验证优化效果非常有价值。4. 核心功能实现细节4.1 SQL 解析用 sqlparse 提取关键结构SQL 解析是整个插件的入口。市面上有完整 SQL 解析能力的库比如 sqlglot但为了降低依赖复杂度我选择用 sqlparse 做词法切分再配合正则抽取关键片段。这种方式对于 90% 的分析场景足够用了。解析模块的核心逻辑分三步。第一步去掉注释和空行规整 SQL 文本第二步找出主查询里的表名用正则匹配FROM和JOIN关键字后面的标识符第三步提取 WHERE 条件中参与过滤的列、ORDER BY 和 GROUP BY 中的列。# core/sql_parser.py import re import sqlparse def extract_tables(sql: str) - list[str]: Extract table names from FROM and JOIN clauses. formatted sqlparse.format(sql, strip_commentsTrue) # 去掉子查询简化匹配详见下方说明 tables [] for match in re.finditer(r\b(?:FROM|JOIN)\s?(\w)?, formatted, re.IGNORECASE): table match.group(1) if table not in tables: tables.append(table) return tables这段代码有个已知限制如果子查询里也出现了FROM正则会把子查询的表也抓进来导致表清单比实际复杂。为了不让分析结果被误导我加了一个简单的子查询剥离逻辑递归地去掉括号内的内容只分析最外层 SQL 的表结构。sakila 里的视图比较多真实项目中遇到复杂嵌套视图时这个简化策略不一定完美但作为一轮快速分析已经够用。提取出来的表清单有两个用途一是告诉采集层需要去information_schema查哪些表的元数据二是传递给提示词模板让模型知道这条 SQL 涉及哪些业务实体。4.2 执行计划采集EXPLAIN 的三种模式MySQL 的 EXPLAIN 有几种输出格式我针对不同场景做了分流。默认使用传统表格格式EXPLAIN因为它兼容性最好访问类型和扫描行数都能直接读到当需要看更细的执行树时用EXPLAIN FORMATTREE要做真实计时验证时用EXPLAIN ANALYZE。# core/explain.py EXPLAIN_TPL { classic: EXPLAIN {sql}, tree: EXPLAIN FORMATTREE {sql}, analyze: EXPLAIN ANALYZE {sql}, } def fetch_explain(cursor, sql: str, mode: str classic) - dict: query EXPLAIN_TPL[mode].format(sqlsql) cursor.execute(query) if mode classic: columns [desc[0] for desc in cursor.description] rows cursor.fetchall() return {mode: mode, columns: columns, rows: [dict(zip(columns, row)) for row in rows]} else: raw cursor.fetchone()[0] return {mode: mode, raw: raw}传统格式里最需要关注的是type列的取值序列。从最好到最差大致是system const eq_ref ref range index ALL。如果某张表的访问类型是ALL说明优化器在走全表扫描结合rows列的估算扫描行数就能判断这是不是问题所在。但在实际开发中我发现一个容易误判的点ALL在 sakila 这种几万行的小表上不一定是坏事。比如一张只有 200 行的language表全表扫描成本极低优化器选择ALL是完全合理的。所以插件在计算指标时会把扫描行数和表总行数做比值——如果一张表全表扫描但总行数只有几百行就不会被标记为高优先级问题。这个按比例看问题的思路比单纯报一个红字 ALL 要实用得多。4.3 性能指标计算从行数到判断采集到的 EXPLAIN 结果还只是一堆原始值真正有价值的是把这些值转化成判断。我在metrics.py里定义了五个关键指标估算扫描行数与表总行数之比超过 30% 时提示全表扫描风险超过 100%理论上不会说明优化器估算失真连接方式评估检查type字段是否为ALL如果是再看被驱动表是否使用了索引额外操作检测Extra字段里出现Using temporary或Using filesort时给出高优警告索引可用性检查 WHERE 和 JOIN 条件中的列是否存在于该表的索引列集合中查询返回量估算结合rows和 WHERE 条件可选择性粗略估算结果集大小用于判断是否需要分页或加 LIMIT# core/metrics.py def evaluate_plan(plan_rows, table_row_counts): signals [] for row in plan_rows: table row.get(table, ) access_type row.get(type, ) rows_est int(row.get(rows, 0) or 0) total_rows table_row_counts.get(table, 0) ratio rows_est / total_rows if total_rows else 1.0 extra row.get(Extra, ) if access_type ALL and ratio 0.3: signals.append({ level: WARN, table: table, message: f全表扫描占比 {ratio:.1%}建议检查过滤条件或索引, }) if Using temporary in extra: signals.append({level: WARN, table: table, message: 使用了临时表可能影响排序和分组性能}) if Using filesort in extra: signals.append({level: WARN, table: table, message: 使用了文件排序检查 ORDER BY 字段的索引覆盖情况}) return signals写这段代码的时候我特意让指标阈值可配置而不是写死在代码里。因为环境差异很大一次分析 10 万行是小表换到生产可能是大表的零头。把 30% 这个阈值做成配置项不同团队可以直接按自己的数据量级调。4.4 提示词模板怎么把技术数据交给模型采集层拿到结构化指标之后下一个问题是怎么让 DeepSeek 基于这些数据给出高质量优化建议。这里的关键是提示词模板的设计。我没有让模型直接看 EXPLAIN 的原始 JSON而是先把数据整理成人类可读的文本再把文本塞进模板。我用的模板结构有一个固定的节奏角色说明 → 任务清单 → 上下文数据 → 输出格式约束。角色说明让模型站在 DBA 的角度任务清单明确告诉它要输出什么问题列表、优化建议、预期效果上下文数据给出表信息、索引信息、EXPLAIN 结果和指标信号输出格式约束要求它用 Markdown 列表输出每条建议必须附带为什么这样做。你是一名 MySQL 数据库性能优化专家。请基于以下信息分析一条 SQL 查询并输出优化建议。 涉及表及其行数 {{ table_info }} 当前索引 {{ index_info }} EXPLAIN 结果 {{ explain_result }} 检测到的性能信号 {{ metric_signals }} 请输出 1. 这条 SQL 的执行瓶颈尽量定位到具体表或阶段 2. 至少两条优化建议说明每个建议的原理和预期效果 3. 如果建议创建索引请给出完整的 CREATE INDEX 语句这个模板与对话式直接提问有个很大的区别它要求模型基于证据说话。所有数据都是插件采集并验证过的模型的任务不是猜而是对已有的事实做归纳和方案推演。实际测试下来只要模板里的 EXPLAIN 数据是准确的DeepSeek 给出的优化建议有很强的可操作性。4.5 报告输出与索引验证闭环分析报告最终会写成 Markdown 格式包含摘要、执行计划表、检测到的问题、优化建议、验证结果这五个部分。其中验证结果这个部分是我最满意的设计。插件拿到模型建议的CREATE INDEX语句后不会直接丢给用户就不管了而是默认在测试库sakila上执行这个语句然后重新跑一次EXPLAIN ANALYZE对比优化前后的执行时间和扫描行数再把对比结果写进报告。# core/reporter.py def verify_index(cursor, create_index_sql, explain_before, original_sql): cursor.execute(create_index_sql) explain_after fetch_explain(cursor, original_sql, modeanalyze) return { before: explain_before, after: explain_after, index_sql: create_index_sql, }每次在测试库上跑索引验证是有代价的主要是 DDL 会触发元数据锁冲掉缓冲池的冷数据。所以在生产环境使用这个插件时我建议把auto_verify开关关闭改成人工确认后再执行验证。这个开关在整个插件里虽然很小但决定了它能不能从开发玩具变成生产工具。5. 实战演练分析一条 sakila 慢查询5.1 构造一个典型的多表 JOIN 查询光说架构不过瘾直接上一条在 sakila 上经常会出现问题的查询做演示。这条 SQL 的逻辑是找出 2005 年 6 月之后租过电影且消费金额在前 20 的客户以及他们最常租赁的电影分类。SELECT c.customer_id, c.first_name, c.last_name, cat.name AS favorite_category, COUNT(r.rental_id) AS rental_count FROM customer c JOIN rental r ON r.customer_id c.customer_id JOIN inventory i ON i.inventory_id r.inventory_id JOIN film f ON f.film_id i.film_id JOIN film_category fc ON fc.film_id f.film_id JOIN category cat ON cat.category_id fc.category_id WHERE r.rental_date 2005-06-01 GROUP BY c.customer_id, c.first_name, c.last_name, cat.name ORDER BY rental_count DESC LIMIT 20;这条查询的坏味道很明显六张表 JOINGROUP BY 了四个字段还有 ORDER BY 一个聚合列。即便 sakila 数据量不大多表 JOIN 带来的代价也不小。把这条 SQL 传给插件analyze_query命令会先提取表清单——customer、rental、inventory、film、film_category、category六张表全部命中。然后采集层开始逐表查询行数和索引信息同时执行 EXPLAIN。5.2 插件执行过程的逐步还原为了把过程讲清楚我把采集层跑出来的结果简化后贴出来。首先看表基本盘表名行数索引数量customer5992rental160444inventory45814film10003film_category10002category161再看这条 SQL 的 EXPLAIN 结果关键列tabletypekeyrowsExtracatALLNULL16Using temporary; Using filesortfcrefPRIMARY1NULLfALLNULL1000NULLirefidx_fk_film_id1NULLrrefidx_fk_customer_id1NULLceq_refPRIMARY1NULL这里有一个非常典型的问题虽然只有 6 张表但优化器选择了以category表作为驱动表导致film表走了全表扫描并且最终的 Group By Order By 操作触发了Using temporary和Using filesort。Using temporary是 MySQL 优化器在内存或磁盘上创建临时表来完成排序或去重时的标志。这条 SQL 之所以会触发是因为 GROUP BY 的字段和 ORDER BY 的字段不一致一个是 cat.name一个是聚合列优化器无法直接利用索引序来完成排序只能先建临时表再排序。这些都是插件会自动检测并在报告里高亮的问题。把上面这些数据喂给 DeepSeek 后模型给出的分析结论和优化方向是重写查询结构 调整驱动表 增加覆盖索引并且给出了具体实现方案。这样一个从模型猜到基于执行计划给出分阶段优化建议的过程就是 Harness 插件和普通对话窗口的本质区别。5.3 优化建议落地与前后对比模型给的第一条建议是检查film表的全表扫描确认idx_fk_language_id等索引是否覆盖了实际使用的 JOIN 列如果film表参与 JOIN 的列没有索引立即补一个。经查示例库此场景下film.title等列没有合适的二级索引用于关联查找因此我们可以按建议方向做验证。第二步建议是增加覆盖索引在film_category表上建立复合索引(film_id, category_id)让该表在 JOIN 过程中只走索引而不回表。插件在测试库上执行了对应的 CREATE INDEX 语句后再次运行 EXPLAIN ANALYZE结果符合预期film_category的访问方式从 ref 变成了 indexExtra 列中的回表操作消失了整个查询的执行时间从约 18 毫秒下降到约 14 毫秒。需要说明的是这个对比在 sakila 上不算骇人因为数据量小。同样的规则放到生产环境的大表上差距会拉到数十倍甚至上百倍。但优化思路是通用的这也是我坚持用 sakila 做演示的原因——结果可复现规则可迁移。6. 开发与调试中的常见问题6.1 插件列表速查表开发这个插件的过程中我踩了不少坑整理一张速查表按频率排序问题现象根本原因解决方法Harness 启动后插件加载失败__init__.py为空插件没被注册确保插件包中有register_command装饰器且包路径在harness.toml配置的插件目录下模型总是用错命令命令描述太抽象把描述改成分析并返回数据库查询优化建议包含执行计划和索引验证这种带动作和结果的句式EXPLAIN 结果和实际执行对不上优化器使用了过期的统计信息先执行ANALYZE TABLE再重新 EXPLAINEXPLAIN ANALYZE报语法错误MySQL 版本低于 8.0.18换用传统 EXPLAIN或升级数据库创建索引验证时卡住存在会话持有元数据锁用SHOW PROCESSLIST杀掉长时间运行的查询后再试报告里的扫描行数显示为 NULL部分复杂查询 EXPLAIN 不提供 rows 估算改为从EXPLAIN ANALYZE的实际循环次数取值6.2 提示词工程与模型输出的两个坑第一个坑是模型过度建议。在实验初期模型经常给出一条 SQL 建三四个索引的建议虽然每条听起来都合理但索引不是免费的——每个索引都会拖慢写入速度、占用存储空间。后续我在提示词模板里加入了一段约束请优先考虑成本最低的优化方案不要重复建议已经在使用的索引不要把建索引作为唯一手段。这个约束有效抑制了模型的盲目建议。第二个坑是模型生成的 SQL 并没有真正执行验证。这一步必须靠插件后端兜底。我在实现中把模型建议的索引语句放入一个白名单式的检查器只有语句匹配^CREATE INDEX.*ON.*的才会被放进验证流程其余一律只建议不执行。模型在生成建议时是发散的工程的底线是要用校验规则把不可控的部分拦在门外。6.3 安全边界与权限控制的思考数据库分析插件有一个容易被忽视的安全问题它天然拥有执行 SQL 的能力。我在插件里做了三层限制。第一层数据库账号限制为最小权限分析账号只能 SELECT 和 SHOW VIEW只有在显式开启验证模式的账号上才授予 DDL 权限。第二层插件本身增加了一个--dry-run选项开启后只输出建议 SQL 而不会真正执行。第三层所有将要执行的非 SELECT 语句都会在调用前打印出来由调用方可能是用户也可能是模型确认。这不是过度设计。一个 AI 插件如果可以在数据库上自由执行建索引语句就相当于把一把没有保险栓的工具递给了模型。安全边界必须在架构层面就画好而不是靠模型的自我约束。我个人在实际操作中的体会是模型驱动的工具链最怕的不是模型不够聪明而是模型在聪明的表象下犯低级错误。插件这层壳如果能把模型的错误在传输途中拦截下来最后输出的价值就会非常稳定。这也是这次实战给我最大收获的地方Harness 的真正意义不是让模型多一个聊天的入口而是让模型的输出落在可控、可验证的执行框架里。代码是人写的风险是人控的模型只是那个干活的助手。后续我打算在这个插件上继续扩展两个能力一是把执行计划对比做成可视化的火焰图二是支持直接从慢查询日志里批量拉取 TOP N 条 SQL 做定时巡检。这些方向都在 Harness 的框架能力范围之内接到现有插件上只是工作量的问题。