news 2026/10/13 4:54:47

MyBatis Plus实战:从零SQL CRUD到分页、逻辑删除与乐观锁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis Plus实战:从零SQL CRUD到分页、逻辑删除与乐观锁

1. 为什么都在用 MyBatis Plus:先看它解决了什么问题

做 Java 后端开发的人,对 MyBatis 肯定不陌生。Mapper 接口加 XML 文件,SQL 自己写,灵活是真灵活,但烦也是真烦。单表 CRUD 这类操作,几乎每个项目都要写一遍,而且写得大同小异:selectById、insert、updateById、deleteById,换个表就是换个字段名,纯体力活。更别说分页查询,每到一个项目就要重复配一次分页插件,遇到多表关联还得小心 SQL 别写错。

MyBatis Plus 的核心定位,就是把这些重复劳动砍掉。它在 MyBatis 之上做了一层增强,单表 CRUD 不用写 SQL,内置方法直接用;分页、逻辑删除、自动填充这些高频需求也内置了解决方案。它不是替代 MyBatis,而是在 MyBatis 的基础上帮你省事。

我当时上手这个框架,最直接的感受是:以前写一个用户模块,从建表到写完 Mapper 和 Service,怎么也要半天,用 MyBatis Plus 之后,建完表,实体类一写,继承几个基类,CRUD 接口基本就齐了。这也就是为什么现在很多新项目,尤其是中小规模项目,直接就用它来起步。

适合谁学?如果你是刚入门 Java 后端的学生、准备做毕设的开发者,或者公司项目里数据层还在靠手写大量重复 SQL 的 CRUD 场景,MyBatis Plus 是值得花几天时间掌握的。它的学习曲线比 MyBatis 原生要平缓得多,只要你掌握了 Spring Boot 的基本用法,剩下的就是照着文档调 API。

下面我不打算照着官方文档重新念一遍,而是按我实际用过之后的经验,把最关键的配置、最常用的功能、最容易踩的坑,从头到尾捋一遍。你跟着走一遍,基本就能在项目里正常使用了。

2. 快速上手前的准备:环境搭建与依赖引入

2.1 环境版本选择:Spring Boot 2 还是 3

这个选择现在影响挺大的。MyBatis Plus 从 3.5.4 之后,对 Spring Boot 3 的支持才算是比较稳定。如果你是新项目,直接用 Spring Boot 3.x 加 MyBatis Plus 3.5.x 以上版本,问题不大。如果项目还在用 Spring Boot 2.x,那么 MyBatis Plus 3.4.x 或 3.5.x 也都可以,但要注意 3.5.x 后续版本有些接口做了微调,升级时要留意兼容性。

以我常用的组合为例:Spring Boot 3.2.x,MyBatis Plus 3.5.7,JDK 17,MySQL 8.x。这个组合在现在的生产环境里很常见,也没有遇到过什么坑。如果你是为了学习,完全可以用这个组合起步。

2.2 Maven 依赖引入

用 Maven 管理依赖的话,核心只要加一个:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency>

注意,如果你用的是 Spring Boot 3,官方推荐引入mybatis-plus-spring-boot3-starter,而不是原来的mybatis-plus-boot-starter。这是个容易踩的坑,我之前就见过有人用 Spring Boot 3 引入了旧 starter,结果启动直接报错。

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency>

2.3 配置文件里的关键项

在application.yml里,最基础的就是配置数据源。然后是两个我个人强烈建议你一上来就打开的开关:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

log-impl这个配置,会把所有执行的 SQL 打印到控制台。调试阶段一定要开,不然你怎么死的都不知道。等上线前再关掉就行。

logic-delete-field是逻辑删除的全局配置,后面我会详细讲。这里先记住:如果你表里设计了一个deleted字段来标记是否删除,那这个配置能帮你省掉大量重复的 "where deleted = 0" 条件。

另外,实体类里字段的驼峰命名和下划线命名自动映射,MyBatis Plus 默认就是开启的,所以userName对应user_name这种场景,不需要额外配置。这一点相当省心,比原生 MyBatis 默认不开启要友好得多。

2.4 启动类注解

在 Spring Boot 启动类上,需要加一个注解:

@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

@MapperScan指定 Mapper 接口所在的包路径,这样就不用每个 Mapper 接口上都加@Mapper注解了。两种方式都行,但@MapperScan更简洁,尤其是 Mapper 数量多的时候。

3. 核心功能逐个拆解:从实体类到条件构造器

3.1 实体类注解:别只会 @TableName 和 @TableId

实体类是 MyBatis Plus 操作的基础。最基本的两个注解,一个是@TableName,指定这个实体类对应哪张表;另一个是@TableId,标记主键字段。但如果只知道这两个,还远远不够,实际项目里还有几个字段场景非常常见。

先看一个完整的例子:

@Data @TableName("user") public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; private String email; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableLogic private Integer deleted; }

这里面有几个点值得展开说:

  • @TableId(type = IdType.ASSIGN_ID):主键生成策略。ASSIGN_ID是雪花算法生成分布式 ID,适合分布式系统。如果用的是数据库自增主键,就用IdType.AUTO。我把这个选择放在优先级第一位,是因为很多新人在主键策略上选错,导致插入数据时主键冲突或者 ID 为空。
  • @TableField(fill = FieldFill.INSERT):自动填充标记。createTime这个字段希望在插入时自动填入当前时间,updateTime希望在插入和更新时都自动填入。这个配合后面说的 MetaObjectHandler 一起用。
  • @TableLogic:单字段逻辑删除标记。加上这个注解之后,MyBatis Plus 的删除操作会自动变成 update 语句,查询操作会自动加WHERE deleted = 0条件。

这里要提醒一句:如果你在配置文件里已经配了全局logic-delete-field,实体类上可以不用加@TableLogic。但如果你只想对某一张表做逻辑删除,其他表不做,那就在实体类字段上单独加注解,不配全局配置。两种方式按需选择。

还有一个比较容易忽略的问题:如果数据库表的字段名和 Java 字段名差别较大,比如表里叫user_name,类里叫name,那就需要@TableField("user_name")显式指定。但大多数情况下,只要遵循下划线转驼峰的命名规则,是可以省略这个注解的。

3.2 BaseMapper:零 SQL 完成单表 CRUD

实体类写完之后,Mapper 接口可以直接继承BaseMapper:

public interface UserMapper extends BaseMapper<User> { }

就这么一个空接口,你就能用这些方法了:

  • selectById(id):按主键查询
  • selectList(Wrapper):按条件查询列表
  • selectOne(Wrapper):按条件查询单条
  • insert(entity):插入一条
  • deleteById(id):按主键删除
  • updateById(entity):按主键更新

这些方法都是基于主键和实体类字段自动生成 SQL 的,不需要你写任何 XML。对于单表操作来说,覆盖了 80% 以上的场景。

3.3 条件构造器 Wrapper:动态 SQL 的核心

真正让 MyBatis Plus 变得好用,甚至可以说是灵魂的,是QueryWrapper和LambdaQueryWrapper。它解决了一个非常痛的问题:查询条件不确定,用户可能传 name,可能传 age,可能两个都传,也可能都不传。

以前写原生 MyBatis,这种场景要在 XML 里拼<if>标签,一不小心就 SQL 拼接错误。用条件构造器之后,直接在 Java 代码里动态组合条件:

public List<User> searchUsers(String name, Integer age) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(name), User::getName, name) .eq(age != null, User::getAge, age) .orderByDesc(User::getCreateTime); return userMapper.selectList(wrapper); }

这里的关键点:

  • like()的第一个参数是布尔条件。条件为 true 才把这个条件加进去,这样动态 SQL 的拼装变得非常优雅。
  • eq()和like()的组合,已经能覆盖绝大多数查询场景。
  • 用LambdaQueryWrapper而不是QueryWrapper,好处是方法引用User::getName可以避免硬编码字段名字符串。我强烈建议你只用 Lambda 版本的,不然后期重构实体类字段名时,字符串字段名不会跟着变,很容易查不出来问题。

再补充几个常用的条件方法:

  • ge/le:大于等于 / 小于等于,常用于时间范围和数值范围
  • between:闭区间范围
  • in:IN 查询
  • isNull/isNotNull:判空
  • last("LIMIT 1"):拼接原生 SQL 片段,但这个方法能不用就不用,因为你无法控制拼接的位置,容易出问题。如果真的只查一条,用selectOne加条件即可。

3.4 分页查询:PaginationInnerInterceptor 的配置与使用

分页查询是 MyBatis Plus 使用率最高的功能之一。但注意,它并不是开箱即用的。你需要先配置一个分页插件拦截器,否则你调分页方法,会发现查出来的数据并没有真正分页,甚至可能直接把全表查出来。

配置方式如下:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }

分页查询的使用方式:

Page<User> page = new Page<>(1, 10); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.orderByDesc(User::getCreateTime); Page<User> result = userMapper.selectPage(page, wrapper); // result.getRecords() 当前页数据 // result.getTotal() 总记录数 // result.getPages() 总页数 // result.getCurrent() 当前页码

参数new Page<>(1, 10)是第 1 页,每页 10 条。返回的result对象里,getRecords()拿当前页数据,getTotal()拿总条数,这就够前端渲染分页组件了。

这里有个非常隐蔽的性能问题值得注意:selectPage默认会执行一条 count 查询来统计总条数,如果你的表数据量很大,这个 count 查询可能比数据查询本身还慢。MyBatis Plus 3.5.x 之后支持了分页查询的COUNT优化,也支持通过Page的setSearchCount(false)跳过 count 查询。如果场景是"加载更多"类型的无限滚动,不需要总条数,建议直接把 searchCount 关掉。

3.5 Service 层:IService 和 ServiceImpl

很多项目里 Service 层代码就是简单调用 Mapper,自己手写的话,每张表都要写一遍,挺没意思的。MyBatis Plus 提供了 IService 接口和 ServiceImpl 基类,可以把这层也省掉。

public interface UserService extends IService<User> { } @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }

继承之后,你的 Service 层就有了getById、save、updateById、removeById、page这些现成方法。而且ServiceImpl内部还做了一件事:默认开启了批量插入的优化,用saveBatch批量插入时,内部会拼接多条 INSERT 语句,减少数据库交互次数。

我实际测过,插入 5000 条数据,循环单条插入可能要十几秒,用saveBatch两三秒就能完成。如果项目里有批量导入功能,这个提升是非常明显的。

4. 进阶功能深入:自动填充、逻辑删除与乐观锁

4.1 字段自动填充:别再把创建时间手动 set 了

很多表里都有create_time、update_time这类审计字段。以前的做法是在插入和更新前手动 set,代码多而且容易漏。MyBatis Plus 的自动填充功能,可以让你实现一次、全局生效。

先写一个 MetaObjectHandler 的实现类:

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

然后实体类对应字段上要加上前面说过的@TableField(fill = FieldFill.INSERT)或@TableField(fill = FieldFill.INSERT_UPDATE)。

这个功能的核心价值在于统一。任何一个实体类,只要有这些注解,插入和更新时时间字段就自动维护了,不需要在业务代码里再写一行 set。尤其是团队协作时,这个能规避"有人忘了填 create_time"的隐患。

4.2 逻辑删除:删除不是真删,是打标记

逻辑删除的意思很直白:数据不物理删除,而是把某个字段置为 1,查询时默认排除掉。这在很多业务场景里是一个硬性需求,比如订单记录、用户审计数据,真正删除会导致后续追溯困难。

用 MyBatis Plus 实现逻辑删除非常简单。你只需要在字段上加上@TableLogic。

之后:

  • 调用deleteById(1L)时,实际执行的 SQL 是UPDATE user SET deleted=1 WHERE id=? AND deleted=0
  • 调用selectById(1L)时,实际执行的 SQL 是SELECT * FROM user WHERE id=? AND deleted=0
  • 调用selectList(wrapper)时,会在原有条件上自动追加AND deleted=0

你会发现,所有框架内置的方法,都自动处理了逻辑删除条件。但如果你自己写 XML SQL,那么抱歉,MyBatis Plus 是无法干预你的自定义 SQL 的,你需要在 XML 里手动加上deleted = 0条件。这一点很多人容易忽略,导致逻辑删除后通过自定义 SQL 查到了已经逻辑删除的数据。

4.3 乐观锁:解决并发更新覆盖问题

具体场景是这样的:一个后台编辑功能,A 和 B 同时打开同一条记录的编辑页面。A 先保存了修改,把数据从 version 1 改成了 version 2。B 没有刷新页面,保存的时候仍然是基于 version 1 的旧数据,如果不加控制,B 的修改会把 A 的修改覆盖掉。

乐观锁的思路是在更新时校验版本号。具体到 MyBatis Plus,需要三步配置。

第一步,实体类里加版本字段:

@Version private Integer version;

第二步,配置乐观锁插件。这个插件要和分页插件一起注册:

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }

第三步,更新时通过updateById或update方法操作即可。

当 MyBatis Plus 执行更新时会自动拼接条件:

UPDATE user SET name='新的名字', version=2 WHERE id=1 AND version=1

如果影响行数为 0,说明版本不对,更新失败,你可以在业务代码里做出提示。

这里要特别注意一个坑:乐观锁只对 MyBatis Plus 内置的updateById和update(entity, wrapper)生效。你如果有自定义 SQL 的更新方法,@Version是不起作用的,需要自己在 SQL 里处理。这种复合场景,我更推荐在乐观锁冲突失败后,给用户一个明确的提示,比如"数据已被他人修改,请刷新后重试",而不是静默失败。

5. 实战流程全记录:从建表到接口实现的一次完整走查

5.1 建表和初始化数据

假设我们要做一个简单的用户管理系统,包含查询用户、新增用户、修改用户、删除用户和分页列表。先建表:

CREATE TABLE user ( id BIGINT PRIMARY KEY COMMENT '主键ID', name VARCHAR(30) NOT NULL COMMENT '姓名', age INT DEFAULT NULL COMMENT '年龄', email VARCHAR(50) DEFAULT NULL COMMENT '邮箱', version INT DEFAULT 1 COMMENT '乐观锁版本号', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除,0未删除,1已删除', create_time DATETIME DEFAULT NULL COMMENT '创建时间', update_time DATETIME DEFAULT NULL COMMENT '更新时间' ) COMMENT '用户表';

插入几条测试数据:

INSERT INTO user (id, name, age, email, version, deleted, create_time, update_time) VALUES (1, '张三', 25, 'zhangsan@example.com', 1, 0, NOW(), NOW()), (2, '李四', 30, 'lisi@example.com', 1, 0, NOW(), NOW()), (3, '王五', 28, 'wangwu@example.com', 1, 0, NOW(), NOW());

5.2 编写实体类、Mapper、Service

实体类和 Mapper 前面已经展示过,这里直接看 Service:

public interface UserService extends IService<User> { } @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }

这里不需要写任何具体实现,基础 CRUD 和分页已经由 IService 提供了。

5.3 开发 Controller 接口

Controller 层是直接面向前端和外部调用的。我们可以直接基于 MyBatis Plus 提供的分页能力来写接口:

@RestController @RequestMapping("/user") public class UserController { @Resource private UserService userService; @GetMapping("/page") public Result<Page<User>> page(@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String name) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(name), User::getName, name) .orderByDesc(User::getCreateTime); Page<User> pageResult = userService.page(new Page<>(current, size), wrapper); return Result.success(pageResult); } @GetMapping("/{id}") public Result<User> getById(@PathVariable Long id) { return Result.success(userService.getById(id)); } @PostMapping public Result<Boolean> save(@RequestBody User user) { return Result.success(userService.save(user)); } @PutMapping public Result<Boolean> update(@RequestBody User user) { return Result.success(userService.updateById(user)); } @DeleteMapping("/{id}") public Result<Boolean> delete(@PathVariable Long id) { return Result.success(userService.removeById(id)); } }

这里的Result是统一返回类型,实际项目里一般会定义一个包装类,包含状态码、消息和数据。这个和 MyBatis Plus 无关,是你项目自己的设计。

当代码写完,启动项目之后,访问GET /user/page?current=1&size=10,你将看到控制台打印出两条 SQL:

SELECT COUNT(*) AS total FROM user WHERE deleted=0; SELECT * FROM user WHERE deleted=0 ORDER BY create_time DESC LIMIT 10;

注意,每个查询都自动带上了deleted=0条件,这就是前面日志里配置的逻辑删除生效的表现。

接口调用也可以直接验证一下删除:

调用DELETE /user/1,控制台打印的 SQL 是:

UPDATE user SET deleted=1 WHERE id=1 AND deleted=0;

数据还在表里,但从接口结果和后续查询来看,这条记录已经"不存在"了。审计场景下,你可以随时手动把deleted改回 0 来恢复数据。

6. 我踩过的一些坑和排查思路

6.1 常见异常速查表

异常现象可能原因解决办法
启动报Invalid bound statement (not found)Mapper 接口没有扫描到,或者 XML 文件的 namespace 写错检查启动类的@MapperScan包路径;检查 XML 的 namespace 是否指向正确接口
插入数据主键冲突主键策略用的是ASSIGN_ID,但表主键设置了自增统一主键策略:要么数据库自增 +IdType.AUTO,要么雪花 ID + 表不设自增
分页查询返回全表数据没有配置PaginationInnerInterceptor注册分页插件,且注意数据库类型要一致
逻辑删除后,自定义 SQL 查到了已删除数据自定义 SQL 没有处理删除标记条件在 XML 或注解 SQL 里手动加deleted = 0条件
更新不生效且无报错,影响行数为 0乐观锁版本号不对即更新前先查一次最新版本,或者给用户刷新页面的提示

6.2 一个最容易困扰新人的问题:updateById 更新为 null 的问题

updateById的默认行为是:实体类里为 null 的字段不参与更新。也就是说,如果你只设置了name和age,其余字段为 null,那么生成的 SQL 只更新name和age,不会把其他字段置空。

大多数场景这个行为是合理的。但如果你就是想更新某个字段为 null,比如把用户的email清空,updateById是做不到的。这时候要改成 UpdateWrapper:

LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(User::getId, 1L) .set(User::getEmail, null); userMapper.update(null, updateWrapper);

这类需求很冷门,但遇到了确实容易懵,因为你不看 SQL 根本不知道 updateById 悄悄跳过了 null 字段。

6.3 自定义 SQL 和 MyBatis Plus 内置方法混用时的边界

MyBatis Plus 虽然提供了大量内置方法,但复杂查询还是得自己写 SQL。比如多表关联、子查询、复杂统计,这些场景用 Wrapper 硬拼反而不如写 XML 清晰。

我相信这种方式:

  • 单表常规操作走 MyBatis Plus 内置方法
  • 多表关联和报表统计走自定义 XML SQL
  • 自定义 SQL 的参数传递可以用@Param("ew")结合 Wrapper 来动态拼接条件,但这个进阶玩法对新手不友好,建议先直接用普通参数

如果你在同一个 Mapper 接口里既继承BaseMapper,又自己定义方法,那么唯一的硬性要求是:自己的方法要有对应的 XML 实现。否则启动时虽然不报错(MyBatis Plus 对非内置方法默认会尝试从 XML 找映射),但你一调用就会抛异常。

7. 一些个人经验总结

我实际用下来的体感是:MyBatis Plus 不是让你就不写 SQL 了,而是把重复的单表 SQL 从你手里解放出去,让你把精力放在那些真正复杂的查询和业务逻辑上。它的核心价值是效率,不是万能。

有一点我觉得挺重要的:如果你是团队开发,一定要在项目初期约定好 MyBatis Plus 的使用边界。哪些地方用内置方法,哪些地方必须手写 SQL,逻辑删除是全局开启还是只针对部分表,主键策略是什么,统一了规则之后,协作时才不会一个项目里出现两种风格。

用配置类里注册插件的时候,分页插件和乐观锁插件都注册在同一个MybatisPlusInterceptor里,顺序一般没有严格限制,但保持"先乐观锁、后分页"是官方文档里的推荐写法,跟着做不会有问题。

最后说一个我在实际项目里养成的习惯:所有 MyBatis Plus 内置方法执行完之后,我都会看一眼日志里的 SQL 是否正确,特别是条件有没有生效、逻辑删除条件有没有自动带上。不要嫌麻烦,SQL 错了,数据就错了,数据错了,业务逻辑再对也是白搭。这个习惯帮我抓出过不少隐蔽的 bug,很值得养成。

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

区间和转前缀和之差:USACO Range Reconstruction构造全解

做 USACO 的 Range Reconstruction 这题&#xff0c;最关键的一步就是把“区间和”四个字翻译成“前缀和之差”。这句话读起来很简单&#xff0c;但真到了考场上&#xff0c;很多人会被那个上三角矩阵唬住&#xff0c;以为要解方程组、甚至去搜回溯。实际上&#xff0c;这就是一…

作者头像 李华
网站建设 2026/10/13 4:54:08

n8n+AI Agent+Grist:构建Hacker News信息流自动化工作流

1. 先把需求讲清楚&#xff1a;为什么要把 Hacker News 和 Grist 交给一个智能体管最近在折腾信息流自动化&#xff0c;发现每天花大量时间刷 Hacker News 找技术选题、看热门讨论&#xff0c;效率实在太低了。后来把 n8n、Grist 和 AI Agent 串在一起&#xff0c;做了一个能自…

作者头像 李华
网站建设 2026/10/13 4:53:35

非凸优化不是缺陷,而是深度学习表征能力的根源

1. 这不是“退而求其次”&#xff0c;而是主动选择的升维策略你有没有在调参时盯着损失曲线发过呆&#xff1f;明明梯度下降还在稳步下降&#xff0c;但验证集准确率却卡在87%不动了&#xff1b;或者训练到第200轮&#xff0c;突然发现模型对某类样本的预测概率集体塌缩——不是…

作者头像 李华
网站建设 2026/10/13 4:53:29

Redis主从复制+哨兵架构详解:高可用方案实战

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

作者头像 李华
网站建设 2026/10/13 4:52:58

从Function Calling到MCP:大模型工具调用标准化实战指南

这两年做大模型应用&#xff0c;我有个特别深的感受&#xff1a;模型本身的对话能力已经很强了&#xff0c;真正的瓶颈在于它够不到外面的世界。数据存在数据库里、工具跑在服务端、文件散落在本地&#xff0c;模型再聪明也只能干瞪眼。MCP&#xff08;Model Context Protocol&…

作者头像 李华
网站建设 2026/10/13 4:52:58

一句“废话”让AI十分钟生成全栈系统:完整实操拆解

前两天我扔给AI一句话&#xff0c;它十分钟给我生成了一套能跑的全栈管理系统&#xff0c;前端页面、后台接口、数据库表、登录注册全齐。这句话乍一听像是废话&#xff0c;我大概说的是“帮我做一个带用户管理的后台系统&#xff0c;要有登录、权限、列表、增删改查&#xff0…

作者头像 李华