项目二今天开工。叫 ai-knowledge-base,一个能传文档、切分、检索、带引用回答的小知识库。Day 1 的目标只有两条:四张表建出来,项目能启动。
听起来是纯体力活。结果是体力活里的环境部分把我卡了半天,业务代码一行没写。
一、四张表
建表脚本放docs/schema.sql,在 IDEA 里全选执行。表是照着「上传 → 切分 → 检索 → 问答」这条线画的:
CREATEDATABASEIFNOTEXISTSai_knowledge_baseDEFAULTCHARACTERSETutf8mb4COLLATEutf8mb4_0900_ai_ci;-- ① 文档表CREATETABLEIFNOTEXISTSdoc(idBIGINTNOTNULLAUTO_INCREMENT,user_idBIGINTNOTNULLCOMMENT'上传者',nameVARCHAR(255)NOTNULLCOMMENT'原始文件名',sizeBIGINTNOTNULLDEFAULT0COMMENT'字节数',chunk_countINTNOTNULLDEFAULT0COMMENT'切分块数,上传时先置 0',statusTINYINTNOTNULLDEFAULT0COMMENT'0待解析 1解析中 2完成 3失败',created_atDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,PRIMARYKEY(id),KEYidx_user_created(user_id,created_at))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='文档';-- ② 分块表(核心表)CREATETABLEIFNOTEXISTSchunk(idBIGINTNOTNULLAUTO_INCREMENT,doc_idBIGINTNOTNULLCOMMENT'所属文档',seqINTNOTNULLCOMMENT'块内序号,从 0 开始',contentTEXTNOTNULLCOMMENT'块正文',char_countINTNOTNULLDEFAULT0,embedding_json JSONNULLCOMMENT'向量 JSON 数组,未向量化时为 NULL',created_atDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,PRIMARYKEY(id),KEYidx_doc_seq(doc_id,seq))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='文档分块';qa_log和eval_case略,思路一样。qa_log那张我单独说一句:它不只是日志,以后算召回率、算每次问答花多少 token、算 P95 延迟,全靠它。现在嫌它多余,第 5 周就得回来补。
跑完show tables四张都在,utf8mb4_0900_ai_ci。
二、索引我没照开工卡写
开工卡给的方案是 chunk 上建idx_doc_id(doc_id)。写之前我停了一下,因为以后取块的 SQL 基本长这样:
select*fromchunkwheredoc_id=?orderbyseq一个等值条件,一个排序字段。只建 doc_id 的话,MySQL 靠索引把这批行找出来,再自己排一遍序。
我拿了个临时库aikb_probe_1020,两张一模一样的表,一张只建 doc_id,一张建 (doc_id, seq),各插 2 万行,再把 1 号文档补到 3100 块,EXPLAIN 对着看:
--- A: 只有 doc_id --- type key key_len rows Extra ref idx_doc_id 8 3100 Using filesort --- B: (doc_id, seq) --- type key key_len rows Extra ref idx_doc_seq 8 3100 NULLUsing filesort就这么没了。
不过有一点得说清楚:耗时差得很少。3100 行这个量级,A 是 1.957ms,B 是 1.0965ms,不到 1 毫秒。拿这个数字去说明"复合索引快一倍"是骗自己。能确定的是执行计划里少了一次排序,等一个文档有上万块,这个差别才会按行数放大。
还有个细节我一开始没看懂:两张表key_len都是 8。seq 明明在索引里,为什么不体现在 key_len 上。想了一下明白过来,seq 只负责排序,不参与等值定位,MySQL 只把用于定位的那部分算进 key_len。所以 key_len 相等,不代表索引白建。
三、pom 里第一个名字就不对
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>4.1.1</version></parent><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot4-starter</artifactId><version>3.5.17</version></dependency>Boot 4 把 autoconfigure 拆成了按技术分的模块,starter 名字跟着重排,web 那个现在叫spring-boot-starter-webmvc,测试配套叫-webmvc-test。我翻本地仓库确认过:org/springframework/boot下面没有spring-boot-starter-web的 4.1.1 目录,只有-webmvc。
MyBatis-Plus 同理,得用mybatis-plus-spring-boot4-starter,名字里带 boot4 的那个。
四、Maven Wrapper 拦了我两次
第一次是 IDEA 报了一句ArtifactHandlerManager was bound。我按本能去File → Settings → Build Tools → Maven把 Maven home 改成D:\apache-maven-3.9.16-bin\apache-maven-3.9.16,重启,照报。
后来才想明白方向错了:IDEA 的 Maven 设置有两份,全局一份,项目一份,项目那份在.idea/workspace.xml里,优先级更高。改全局等于没改。
第二次是翻.m2,看见这么一串:
C:\Users\lenovo\.m2\wrapper\dists\ └── apache-maven-3.10.0-bin\ └── 39q0qgmngdu329psje7bjgne79\ ├── apache-maven-3.10.0\ └── apache-maven-3.10.0-bin.zipmvnw自己去拉了个 3.10.0 下来。3.10 和 3.9 的插件装配不一致,报错又只丢类名不给人话,我没打算对着 wrapper 源码查这个。处理方式是删掉.mvn整个目录,让项目走本机那套:
Apache Maven 3.9.16 Maven home: D:\apache-maven-3.9.16-bin\apache-maven-3.9.16 Java version: 26.0.1删完就干净了。commit 里我留了一句「移除 Maven Wrapper,改用本机 Maven 3.9.16」。
讲清楚一点:ArtifactHandlerManager was bound是当时 IDEA 弹的原文,我没去复现它,删.mvn之后也没再出现。所以它没被修好,只是被绕过去了。哪天别人拿这个项目跑 mvnw 报同样的错,原因还是同一个。
五、改了配置,跑起来像没改
application.yaml 改完我直接点运行,端口还是老的。
去看文件时间:
src/main/resources/application.yaml 09:02 target/classes/application.yaml 14:26两份内容一模一样,但 target 那份是 Build 的时候复制过去的副本。JVM 启动读的是 target/classes 里的,改完 src 不 Build,跑的还是上一次的。
这条我专门写进了 day1 的日志。因为明天写上传接口就要往 yaml 里加 multipart 限制,那时候如果忘了 Build,现象会是"我明明配了 10MB,传 5MB 还是被拒",能查半小时。
六、怎么验证
别只看"启动没报错",下面这几条都能对照:
| 检查项 | 命令 | 我得到的结果 |
|---|---|---|
| 编译 | mvn -o clean compile | BUILD SUCCESS,javac [debug parameters release 17]编 1 个源文件 |
| 启动 | java -jar target/*.jar | Started AiKnowledgeBaseApplication in 0.966 seconds |
| Web 容器 | 看上一条日志 | Tomcat 11.0.24 |
| 表 | show tables | 4 行:chunk / doc / eval_case / qa_log |
| 索引 | show index from chunk | idx_doc_seq两列,Seq_in_index 1=doc_id、2=seq |
| 密码没进仓库 | git log -p搜 password | 无明文,yaml 里是${DB_PASSWORD:} |
| 空密码会被拒 | 用空密码连库 | MySQL Error 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO) |
最后那条我特意跑了一次。因为 yaml 里写的是${DB_PASSWORD:},冒号后面没默认值,环境变量不设就是空密码。启动时不报错(现在还没 mapper,压根不碰数据库),等到第一个查询才会炸。这种坑早发现比晚发现好。
七、报错对照表
| 看到的现象 | 原文 / 关键字 | 我以为的原因 | 实际原因 |
|---|---|---|---|
| IDEA 红字,项目跑不起来 | ArtifactHandlerManager was bound | 本机 Maven 版本太低 | wrapper 拉了 3.10.0,装配不一致 |
| 改了全局 Maven 设置没用 | Settings 改了、重启无效 | 设置没保存 | 项目级.idea/workspace.xml覆盖全局 |
| 改完 yaml 配置不生效 | 端口、参数都是旧值 | 配置写错了 | 运行读target/classes副本,没 Build |
| 依赖名找不到 4.x 版本 | spring-boot-starter-web无 4.1.1 | 镜像没同步 | Boot 4 已改名-webmvc |
| 启动日志一条黄色警告 | No MyBatis mapper was found in '[com.first.aikb]' package | 扫包配错了 | 还没写 mapper,属于预期 |
八、两个还没解决的
一,JDK 26 加 Lombok,编译时会刷三行警告:
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called WARNING: sun.misc.Unsafe::objectFieldOffset has been called by lombok.permit.Permit WARNING: Please consider reporting this to the maintainers of class lombok.permit.Permit开工卡里担心的是「Boot 4.1.1 在 JDK 26 上能不能跑」,这条现在有答案了:能编译(release 17),能启动,0.966 秒。但 Lombok 这条警告我没消掉,也没找到确定的影响范围。留个记号,以后出现莫名其妙的问题,第一个怀疑它。
二,mvn -o clean compile每次都要重新拷一遍资源,会打印:
[INFO] Copying 1 resource from src\main\resources to target\classes看着像废话,其实就是第五节那个坑的现场。知道它在拷,才知道不 Build 就是没改。
九、下一篇
Day 2 写上传接口:MultipartFile落盘,然后往 doc 表插一条status=0的记录。
有四个决策要先定,我打算先想清楚再写代码:文件存相对路径还是绝对路径、要不要 UUID 重命名、先落盘还是先写库、multipart 的 10MB 限制配在哪。这四个问题里任何一个想反了,后面切分层都要返工。