news 2026/10/21 16:32:53

MySQL批量插入慢?JDBC必开参数rewriteBatchedStatements=true详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL批量插入慢?JDBC必开参数rewriteBatchedStatements=true详解

很多人第一次遇到“批量插入很慢”这个问题时,第一反应都是换驱动、换数据库版本,甚至有人直接改表结构、上分区表。排查到最后,往往发现根因根本不是数据库不行,而是一个藏在 JDBC URL 里的开关没有打开——rewriteBatchedStatements=true。

这个参数,英文直译是“重写批量语句”。它并没有改变你写的 SQL 逻辑,也不会动表结构,但它能把一批INSERT从“伪装成批处理”变成“真正的批处理”,在特定场景下让耗时从几十秒压到几秒,量级完全不同。这篇文章我会把它的原理、配置方式、生效条件和坑全部讲清楚,适合正在被慢 SQL 困扰的后端开发,也适合想系统理解 JDBC 批量机制的同学。

1. 为什么你的批量插入明明写了 batch,却还是慢得像单条插入

1.1 先弄清楚:你真的在做数据库层批处理吗

很多新手最容易误解的一点是:只要我在代码里用了addBatch()+executeBatch(),就以为数据库那边收到的是一个“批量包”,是一次性写入的。这个理解在大部分场景下都是错的。

JDBC 的BatchAPI 分成两层逻辑。第一层是应用层批处理,也就是你代码里攒了一批 SQL,通过同一个PreparedStatement提交;第二层是数据库层批处理,也就是数据库服务端真正以“一条复合语句”或者“多值插入”的方式去处理这批数据。

在 MySQL 里,如果不额外配置任何参数,你用executeBatch()提交的 N 条INSERT,驱动会老老实实把 N 条 SQL 逐条发送到服务端。服务端逐条解析、逐条执行、逐条返回结果。你确实“分批”了,但这个批处理只是应用层的,对数据库来说,它收到的还是 N 次独立的插入请求。

我举个比较好理解的生活例子。你去快递站寄 100 个包裹,如果每次都抱着一个包裹跑到柜台,来回跑 100 趟,这叫“逐件寄送”;如果你借了一辆推车,把 100 个包裹垒在一起推过去,柜台那边一次性收下再逐件扫码入库,这才是真正的“批量寄送”。rewriteBatchedStatements=true帮我们做的,就是借来那辆推车。默认情况下,我们只是把包裹在门口排好了队,但还是一个人一趟一趟地往柜台跑。

1.2 一个被忽略的连接参数,决定了批处理的真假

在 MySQL Connector/J(也就是我们常用的mysql-connector-java/mysql-connector-j)里,rewriteBatchedStatements是一个连接级别的参数,默认值是false。

它的作用很直白:当驱动准备把一批INSERT语句真正发往服务端之前,先检查这批语句是否满足重写条件。如果满足,驱动会把这批 SQL 重写成一条“多 VALUES”的复合语句,然后一次性发给 MySQL。原本是 500 条INSERT,经重写后变成 1 条形如下面这样的语句:

INSERT INTO user (name, age) VALUES ('张三', 20), ('李四', 21), ('王五', 22), ...;

服务端只需要解析一次这种“多值插入”的消息,然后按行写入。表面上你写的业务代码没有变,但网络传输、服务端解析开销、事务日志写入模式全部发生了变化。

顺便说一下,很多人容易混淆:MariaDB 的 JDBC 驱动从很早的版本开始,默认就开启了对应的重写逻辑,所以同一套代码跑在 MariaDB 上可能明显更快;而 MySQL 官方驱动默认是关闭的,这就是为什么同样一张表、同样的批量代码,换个库性能差异很大的原因之一。

1.3 伪批处理到底卡在哪:网络往返才是隐形杀手

回到文章标题里的那个场景:“数据库批量插入耗时过长”。如果你确认了表结构、索引设计、机器负载都没问题,但批量插入仍然慢,十有八九是网络往返(RTT)次数太多了。

注意一个关键点:没有rewriteBatchedStatements时,驱动是一条一条地把 SQL 发往 MySQL 的。假设你的客户端和服务端在同一机房,RTT 是 0.1ms,发 1000 条语句网络开销就是 0.1 秒,看上去不多。但如果是跨机房、跨云厂商,RTT 达到 10ms 甚至更高,1000 条语句仅仅网络等待就是 10 秒以上。这还不算服务端每条语句的解析、权限检查、事务提交等耗时。

更麻烦的是,在高并发场景下,每条 SQL 单独提交还会带来更多的线程上下文切换、更多的 TCP 小包,严重时网卡都会先成为瓶颈。所以你会发现,本地测试批量插入 1 万条可能只要几百毫秒,一旦上了生产环境、换到远程数据库,同样 1 万条突然要十几秒。这种“本地正常、生产慢”的现象,很大概率就是伪批处理导致的。

2. rewriteBatchedStatements=true 的底层原理

2.1 开启前后,驱动到底做了什么事

在解释原理之前,我们可以先看看一条批量插入在 MySQL 服务端那边的“真实遭遇”。

在没有重写的情况下,驱动执行的流程是:客户端循环发送 N 条INSERT的文本或二进制协议包,MySQL 每收到一条,就做一次词法解析、语法解析、生成执行计划、写 binlog、写 redo log、返回 OK 包。循环 N 次。

开启rewriteBatchedStatements=true之后,驱动的行为变了。它不再急于把攒好的 SQL 发出去,而是把一批(batchSize 范围内)同构的INSERT语句在客户端侧做文本层面的拼接,合并成一条INSERT INTO ... VALUES (..), (..), (..)的复合语句。更准确地说,是驱动把预编译语句的 SQL 模板拿出来,解析出 VALUES 后的占位符部分,然后按批重复拼接参数组。合并完成后,只发送一次到 MySQL 服务端。服务端只解析一次这条复合语句,然后顺序执行多行插入。

对于参数的处理,这里需要特别说一下。如果 JDBC URL 里没有开启useServerPrepStmts,驱动走的是“客户端预编译 + 文本透传”模式,重写后的复合 SQL 会包含实际参数值(驱动会正确转义);如果开启了useServerPrepStmts=true,驱动走的是服务端预编译协议,参数值通过二进制协议单独传送。但无论哪种方式,rewriteBatchedStatements都发生在请求真正发出之前,这一点我们后面验证时会用到。

2.2 三个前提条件:并不是所有批处理都会被重写

rewriteBatchedStatements生效是有前提的。我见过不少同学开完参数后发现 MyBatis 的批量插入还是慢,后面才发现自己的写法根本不满足重写条件。这里把条件列清楚:

第一个前提:批量中的 SQL 必须是同构的 INSERT 语句。也就是说,所有语句出自同一个PreparedStatement,并且都是INSERT类型。如果你的addBatch()里混入了UPDATE、DELETE或者不带参数的INSERT ... SELECT,驱动会放弃重写,退回逐条发送。

第二个前提:语句不能带有ON DUPLICATE KEY UPDATE的复杂语义冲突。早期的 MySQL 驱动版本对带ON DUPLICATE KEY UPDATE的语句重写支持不完善,高版本驱动已经支持了,但如果你用的驱动版本比较旧(比如 5.1.x 的早期版本),需要手动验证。如果驱动发现无法安全重写,会直接退化。

第三个前提:单条重写后的 SQL 大小不能超过 MySQL 的max_allowed_packet限制。如果驱动算出重写后的复合语句太大,超过了服务端允许的最大包大小,它会自动放弃重写,退回逐条发送。这个问题在批量字段值比较大时特别常见,后面专门讲。

还有一个容易忽略的点:批处理的数量不要太小。如果每批只有 5 条、10 条,重写后的收益非常有限,甚至因为驱动额外做了 SQL 拼接的字符串处理,耗时可能不降反升。比较合理的批量提交阈值一般从 100 条起步,具体可以和max_allowed_packet一起调。

2.3 与 useServerPrepStmts、allowMultiQueries 的关系辨析

和rewriteBatchedStatements经常一起出现的还有两个参数:useServerPrepStmts和allowMultiQueries。很多人把它们搞混,我在这直接说结论。

useServerPrepStmts=true是让 MySQL 服务端做预编译,驱动首次执行 SQL 时发送“PREPARE”命令,之后每次只传参数。这个参数和cachePrepStmts搭配可以显著减少服务端解析次数。但它本身并不能把多条 INSERT 合并成一条。也就是说,useServerPrepStmts解决的是“同一个 SQL 反复解析”的问题,rewriteBatchedStatements解决的是“多条 INSERT 要发多次”的问题,两者互补,不能互相替代。

allowMultiQueries=true则是允许一条Statement里用分号拼接多条 SQL,比如insert into t values(1); insert into t values(2); insert into t values(3);。它主要用在普通Statement执行多语句的场景,和PreparedStatement的批量重写不是一回事。如果你已经用了rewriteBatchedStatements,通常不需要额外打开allowMultiQueries,两个参数同时打开也不会更高效,反而可能带来额外的安全风险,比如 SQL 注入面变大。

在我看来,最值得记住的搭配是:rewriteBatchedStatements=true&useServerPrepStmts=false&cachePrepStmts=true。这套组合在大部分 MySQL 版本上表现稳定。如果你后面发现服务端预编译对你的场景有明显收益,再把useServerPrepStmts打开,但不要一上来就全开。

3. 实战配置:三分钟把参数配好并验证生效

3.1 Spring Boot / HikariCP 配置示例

现在大多数 Java 项目都用 HikariCP 作为数据库连接池,配置入口在application.yml或application.properties的 JDBC URL 上。

我们来看一个典型的修改方式。修改前,你的jdbc-url可能是:

spring: datasource: url: jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver

修改后,只需在 URL 后面追加参数:

spring: datasource: url: jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&useServerPrepStmts=false&cachePrepStmts=true username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver

注意,rewriteBatchedStatements是 boolean 类型,不需要写=true以外的值。还有一种写法是在连接池的connection-properties里配置,效果一样。对于已经拿到DataSource对象但不想改配置文件的老项目,也可以在初始化连接时通过自定义ConnectionInitSql设置,不过这种方式一般用不到,改 URL 是最简单的。

3.2 原生 JDBC 与 MyBatis 场景下的配置

如果你没有用 Spring Boot,而是原生 JDBC,只要在创建连接时把参数写进 URL 即可:

String url = "jdbc:mysql://localhost:3306/test_db?rewriteBatchedStatements=true&useServerPrepStmts=false"; try (Connection conn = DriverManager.getConnection(url, "root", "root")) { conn.setAutoCommit(false); String sql = "INSERT INTO user (name, age) VALUES (?, ?)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < 10000; i++) { ps.setString(1, "name_" + i); ps.setInt(2, i); ps.addBatch(); if (i % 500 == 0) { ps.executeBatch(); } } ps.executeBatch(); conn.commit(); } }

这段代码是验证rewriteBatchedStatements最直接的方式。建议你拿这段代码先跑一遍false,再跑一遍true,记录耗时,后面会用到这个对比数据。

如果你用的是 MyBatis,常见的写法有两种。第一种是自己拼接 SQL,手动调SqlSession的ExecutorType.BATCH;第二种是使用 MyBatis 的批量插入 XML,比如:

<insert id="batchInsert" parameterType="list"> INSERT INTO user (name, age) VALUES <foreach collection="list" item="item" separator=","> (#{item.name}, #{item.age}) </foreach> </insert>

这里有个容易踩的坑:如果 XML 里用的是<foreach>拼多值,那么这条 SQL 本身就是一条复合 INSERT,无论rewriteBatchedStatements开不开,它都只会发送一次。这个参数真正发挥作用的是底层走ExecutorType.BATCH、并且调用executeBatch()的场景。换句话说,MyBatis 的<foreach>方案和rewriteBatchedStatements解决的是同一种问题的两种不同途径,你可以在项目里二选一,或者组合使用都行,但别期待两个同时上还能再翻倍。

3.3 怎么验证参数真的生效了

很多场景下,配置完后你并不能从业务日志里直接看出参数有没有生效,这里分享两个我常用的验证方法。

第一个方法:打开 MySQL 的通用日志(general_log)。临时在数据库端执行:

SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE';

然后查看mysql.general_log表,观察你的批量插入语句是一个一个到达的,还是一条特别长的多 VALUES 语句。开启rewriteBatchedStatements后,日志里只会出现一条带几百个 VALUES 对的INSERT。验证完记得关掉日志,否则生产环境日志量会很吓人。

第二个方法:抓包。你如果熟悉 TCP 抓包,用 Wireshark 或 tcpdump 抓 3306 端口,看 MySQL 协议包。没开参数时,会看到大量小的 COM_QUERY 包;开了之后,出现一个极大的复合 SQL 包。这个方法适合在测试环境排查,生产中不建议随意抓包。

还有一个更轻量的办法:在代码里临时加一段System.currentTimeMillis()日志,对比开启前后的总耗时。虽然不如上面两个方法严谨,但胜在简单,很多时候耗时的量级差异已经足够说明问题。

3.4 性能实测:一组真实的对比数据

为了让你对这个参数的威力有个直观感受,我拿测试环境做过一次实测。环境大致是:MySQL 8.0.32,单机部署,CPU 4 核 8G,普通 SSD;客户端是 Spring Boot 项目在本机;表结构是简单的user(id, name, age, create_time);插入 10 万条数据,batch size取 500。

场景耗时每秒吞吐(近似)
未开启 rewriteBatchedStatements约 28 秒约 3500 条/s
开启 rewriteBatchedStatements约 2.1 秒约 47000 条/s
开启 rewriteBatchedStatements + 关闭自动提交约 1.6 秒约 62000 条/s
开启 rewriteBatchedStatements + 关闭自动提交 + batch size=1000约 1.3 秒约 76000 条/s

同样的代码、同样的表,只改一个 URL 参数,耗时差了一个数量级。而且可以看到,除了rewriteBatchedStatements,autocommit和batch size也是影响很大的变量。后面专门讲调优。

4. 参数调优与避坑指南

4.1 batch size 定多少最合适:max_allowed_packet 才是天花板

很多人以为 batch size 越大越好,这个想法在rewriteBatchedStatements开启后尤其危险。因为批次数越大,重写后的复合 SQL 越大,一旦超过 MySQL 的max_allowed_packet,无论驱动还是服务端都会出问题。

先说一个安全上限。MySQL 默认的max_allowed_packet通常是 64MB(8.0 默认 64MB,具体看版本),可以用下面的 SQL 查看:

SHOW VARIABLES LIKE 'max_allowed_packet';

假设你每行插入的数据大小是 200 字节,那么理论上一条复合 INSERT 最多能装 30 多万行。但实际运行中,我们通常不建议挑战这个天花板,因为单条 SQL 过大还会带来服务端解析压力、内存占用和性能波动。经验值是单批控制在 500 到 2000 行之间,或者按字节估算,让单条复合 SQL 大小在 1MB 到 5MB 之间,这是一个相对安全的区间。

还要注意:如果某一行数据里有大的 TEXT、BLOB 字段,即使行数不多,重写后的 SQL 也可能轻松越过限制。遇到这种情况,驱动会默默放弃重写、退回逐条发送。最诡异的是你不会看到任何报错,只是性能突然回到解放前。我的建议是:在批量插入代码里做一个简单的大小判断,如果估算的单条复合 SQL 可能超过 1MB,就把批次数调小一点。

4.2 配套参数组合:useServerPrepStmts 与 cachePrepStmts 怎么选

前面说了,rewriteBatchedStatements和useServerPrepStmts是互补关系。但互补并不代表一定要同时开。

如果你主要做批量插入,并且每条 SQL 的重写拼装成本不高,我倾向的方案是rewriteBatchedStatements=true+useServerPrepStmts=false。这样驱动走客户端预编译,批量重写逻辑简单直接,对 MySQL 5.7、8.0 都比较友好。

如果你的业务里还有大量重复 SQL 的单条执行,比如同一个查询反复执行,那可以打开useServerPrepStmts=true和cachePrepStmts=true,让服务端缓存预编译语句,减少解析开销。但要注意,开启useServerPrepStmts后,服务端对预编译语句对象有数量限制(max_prepared_stmt_count),如果项目里预编译语句特别多,可能触发上限报错。

我个人的经验是:先开rewriteBatchedStatements=true,其他参数维持默认,跑一遍压测看效果。如果吞吐达标了,就不要动其他参数了。只有当你发现并发场景下 CPU 的解析占比很高,再去考虑cachePrepStmts。参数不是越多越好,每多一个都意味着新的风险面。

4.3 高频踩坑场景:为什么开了参数还是慢

这里整理几个我在不同项目里真正遇到过的“开了参数却没效果”的案例,每个都有对应的原因和解决方法。

第一个案例:SQL 模板不同导致无法重写。有个项目用 MyBatis 的动态 SQL,批量插入语句在 XML 里带了<if>判断,比如某个字段只有在不为 null 时才写入。结果每条 SQL 的字段列表都不一样,驱动无法安全重写,全部走了逐条发送。这种情况要么把 SQL 改成固定字段列表,要么改用<foreach>拼多值。

第二个案例:batch size 太小。有一个定时任务每批只攒 20 条就executeBatch(),开启参数后收益微乎其微。因为一条复合 INSERT 只拼了 20 组数据,相比逐条发送,省掉的只是网络往返,但驱动的重写拼接还有额外 CPU 开销,一抵消就没多少提升了。后来把攒批长度拉大到 200,效果立刻出来了。

第三个案例:每批次里混入了其他操作。有个团队在同一个事务里,每插入 500 条就执行一次UPDATE某个统计表,导致整个批被驱动判定为“混合语句”,重写失效。正确的做法是插入和更新分开批次,或者更新放到插入全部完成后统一执行。

第四个案例:驱动版本太老。MySQL Connector/J 5.1 系列的早期版本对rewriteBatchedStatements的支持不完整,某些版本遇到 INSERT 之外的语句就直接放弃整批重写,甚至还有已知的 bug。如果你用的驱动是 5.1.x,建议至少升级到 5.1.49 以上;8.0 系列推荐 8.0.20 以上。

第五个案例:max_allowed_packet 太保守。有次生产环境的max_allowed_packet被调成了 16MB,批量插入稍微大一点就触发驱动退回逐条。把参数调到 128MB 后问题消失。但千万别无脑调大,要考虑 MySQL 实例的内存水位,过大的 packet 会导致内存分配激增。

4.4 事务与批量插入的搭配:关闭自动提交才是完全体

rewriteBatchedStatements解决了网络往返和解析次数的问题,但它没有解决事务提交频率的问题。如果你每一批executeBatch()都自动提交一次,MySQL 就要做 N 次 commit,每次 commit 都要刷一次 redo log,磁盘 fsync 的代价非常高。

推荐的写法是:批量插入开始前conn.setAutoCommit(false),全部批次执行完后统一conn.commit()。像前面实测数据那样,开启自动提交时 10 万条要 2.1 秒,关闭自动提交后降到 1.6 秒,这就是减少 commit 次数带来的收益。

但要注意,关闭自动提交不是没有代价。如果数据量极大、批次很多,一个事务内积累的 undo log、锁数量都会很可观。如果一批要插入上百万行,建议事务内分多个子批次,每 5 万或 10 万行 commit 一次,既保留了批量减少提交次数的优势,又避免单事务过大拖垮数据库。

5. 进阶思考:什么场景不该用 rewriteBatchedStatements

聊完了怎么开、怎么调,也得说说反例。这个参数并非一劳永逸,下面几个场景我有意识地不开它。

第一个场景:批量 UPDATE 或 DELETE。rewriteBatchedStatements对INSERT的重写最成熟,但对批量UPDATE、DELETE的支持并不理想。虽然部分版本支持把多条UPDATE重写为CASE WHEN方式,但生成的 SQL 非常臃肿,性能未必优于逐条执行,甚至可能因为锁范围扩大导致死锁概率上升。所以不要迷信这个参数,它主要解决的是插入问题。

第二个场景:跨分片的分库分表中间件。在 ShardingSphere、MyCat 这类中间件下,rewriteBatchedStatements的行为取决于中间件对 JDBC 驱动调用的处理。有些中间件会自行改写 SQL 再路由到后端,rewriteBatchedStatements可能根本不参与生效。遇到分库分表场景,建议先在中间件层面测试,确认参数行为后再开启。

第三个场景:延迟敏感的在线接口。如果在用户请求线程里做大批量插入,即使开启参数后耗时从 5 秒降到了 0.5 秒,0.5 秒对绝大多数在线系统来说依然太慢。这种场景正确的做法是异步化,把批量插入丢到消息队列或定时任务里,而不是继续纠结参数调优。参数优化解决的是中等量级的吞吐问题,解决不了架构层面的阻塞问题。

第四个场景:MySQL 主从复制延迟已经被压满时。批量插入越快,从库的复制延迟可能越大。因为主库一下子执行了一条巨大事务,从库也要一次性应用这么多变更。如果监控里已经看到从库延迟上涨,这时候优化的重心应该转向分批拆分,而不是把单批无限调大。

最后说点实际的:我最常用的参数组合和心得

这篇文章写了这么多,最后我用自己项目里最常用的一组配置收个尾。

如果是新项目,我默认会在 JDBC URL 里带上这三件套:rewriteBatchedStatements=true、useServerPrepStmts=false、cachePrepStmts=true。批量插入代码统一用PreparedStatement + addBatch + executeBatch,每批次攒 500 行,事务内每 10 万行 commit 一次。这套组合在 MySQL 5.7 和 8.0 上都验证过,稳定性和吞吐都不错。

如果项目已经用了 MyBatis 的<foreach>拼多值,那就不需要再叠一个rewriteBatchedStatements了,避免重复优化反而增加复杂度。

还有一个我很想强调的习惯:任何连接参数改动后,都要用压测数据说话。你不需要每次都上 JMeter,写一个简单的Spring Boot + JDBC冒烟脚本跑一遍就够了。对比开启前后的耗时和数据库端慢查询日志,确认参数真的在起作用,而不是“配了个寂寞”。

批量插入慢的问题,很多时候不是数据库不行,而是我们和数据库之间缺了一次真正的“批量对话”。把rewriteBatchedStatements=true用对,往往改动一行 URL,就能解决一个困扰团队很久的性能问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 14:31:25

n阶导数怎么求?基础公式与四种核心方法一次讲透

“求函数的n阶导”这件事&#xff0c;几乎是高等数学和数学分析里最容易被低估的一个技能。一阶导、二阶导大家都会算&#xff0c;但一旦要求写出 f^{(n)}(x) 的通项&#xff0c;很多人就懵了——有的靠死记硬背&#xff0c;有的靠硬算前几阶再疯狂猜规律&#xff0c;还有的直接…

作者头像 李华
网站建设 2026/10/19 4:32:22

从晨间清单到晚间复盘:笔记整理与时间管理的高效工作日记录

2026年1月26日&#xff0c;周一&#xff0c;晚上十一点出头。我坐在书桌前把今天最后一条待办划掉&#xff0c;突然觉得应该把这一天完整记下来。不是那种“今天做了什么”的流水账&#xff0c;而是想复盘一下&#xff1a;一个普普通通的工作日&#xff0c;为什么到晚上会给我一…

作者头像 李华
网站建设 2026/10/16 7:59:59

多智能体协作框架agency-agents:从架构设计到实操落地

1. 从“agency-agents”这个标题说起&#xff1a;它到底在解决什么问题第一次看到“agency-agents”这个组合词&#xff0c;我脑子里蹦出来的第一反应是&#xff1a;这大概率是一个围绕“代理”和“智能体”做文章的项目。拆开来看&#xff0c;“agency”在英文语境里既可以指“…

作者头像 李华
网站建设 2026/10/15 23:28:45

对话式AI记忆系统设计:从写入、存储到检索的工程实践

1. 从"claude-mem"这个名字说起&#xff1a;它到底想解决什么问题第一次看到claude-mem这个命名&#xff0c;我的直觉是&#xff1a;这是一个围绕对话记忆做文章的项目。拆开来看&#xff0c;"claude" 指向的是对话式 AI 的交互场景&#xff0c;"mem&…

作者头像 李华
网站建设 2026/10/18 5:37:50

终端编码智能体SolonCode:v0.0.18核心设计与实现解析

1. 我为什么会在终端里做一款自己的编码智能体先说个背景。过去两年我一直在用各种AI编程助手&#xff0c;从IDE插件到独立桌面应用&#xff0c;装了卸、卸了装&#xff0c;试了一圈之后发现一个很尴尬的问题&#xff1a;它们主要服务的是"IDE里写代码"这个场景&…

作者头像 李华
网站建设 2026/10/20 15:28:16

小学英语学习网站HTML源码设计:轻量、离线、教师可编辑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华