一次与AI的"设计对话":审计日志方案从1000行到350行
我最近用AI编程工具做了一次系统设计,在原有审计日志系统之上增加了日志降级逻辑,具体内容可参考:2026最简最强操作日志实现
不是那种"AI帮我写代码我复制粘贴"的使用方式,而是真正把它当成一个设计伙伴,反复对话、质疑、追问。
结果挺有意思的:方案从1000行简化到350行,可靠性反而更高了。
我想把这个过程完整记录下来,既是复盘,也是想聊聊:在AI时代,工程师真正值钱的能力是什么。
01
先说背景。
我们有个审计日志组件,会丢数据。
你可能觉得,丢几条日志有什么大不了的?但如果我告诉你,这些日志是用来满足合规要求的,是审计追溯的重要依据——丢一条都可能说不清楚,你还会觉得无所谓吗?
反正我觉得有所谓。
所以我的目标很明确:任何情况下,日志都不能丢。
02
拿到需求,我的第一反应是什么?
整一套完整的降级体系。
我把需求描述给了AI编程工具,它很快给出了方案:
应用层
↓
内存队列(Disruptor/BlockingQueue)
↓ 满/异常
本地文件降级(WAL模式)
├─ pending/ # 待补偿文件
├─ archiving/ # 补偿中文件
└─ archived/ # 已归档文件
↓
补偿线程(定时扫描 + 批量重放)
↓
数据库
配套还有一个复杂的索引文件:
{
"version": "1.0",
"files": {
"audit_pending_20260416_001.log": {
"totalLines": 1000,
"compensatedLines": 800,
"status": "COMPENSATING",
"lastRetryTime": 1713254500000,
"retryCount": 2
}
}
}
看起来挺完善的,对吧?
但我没有直接接受。
03
我盯着这个方案看了一会儿,心里有个声音:这套东西是不是太复杂了?
于是我开始问AI:
"你真的需要三个目录吗?"
AI想了想,说:三目录是为了区分文件的不同阶段。
我又问:"但这三个阶段,真的需要用目录来区分吗?"
AI沉默了。
是的——pending、archiving、archived 本质上就是同一个文件的不同阶段。文件存在就代表待补偿,文件不存在就代表已完成。
用目录区分状态,有点多余。
我又问:"状态字段是不是也多余了?"
"状态字段确实可以用来标记 PENDING、COMPENSATING、COMPLETED 三种状态..."
但问题是:如果文件存在就代表待补偿或补偿中,文件不存在就代表已完成,那显式的 status 字段是不是就多余了?
AI承认了。
我又问:"索引需要这么复杂吗?每个文件记录5+个字段,JSON解析和维护成本都不低。"
"确实...可以简化为键值对格式。"
第一轮对话结束:方案从"三目录+状态机"简化成了"单目录+无状态"。
代码量直接少了40%。
关键洞察:我们陷入了"完整性陷阱"——试图在设计阶段解决所有可能的问题,导致系统过度复杂。
04
但简化还没停止。
我又问AI:
"需要支持断点续传吗?"
AI给了两个选项:
方案A(极简):不记录补偿进度,每次失败从头重试。
方案B(折中):记录补偿进度,支持断点续传。
我开始算账:
- 数据库保存成功率 > 99.9%
- 就算失败,10MB文件重新读取也就几百毫秒
- 但为了支持断点续传,代码复杂度会增加不少
我的判断:不要断点续传。
理由很简单——省掉的复杂度,远大于浪费的那点IO。这是个划算的买卖。
第二轮对话结束:去掉断点续传。
关键洞察:记录 compensatedLines 是为了避免重复读取,而非为了可靠性。
05
我以为差不多了,但AI主动提出了一个新问题:
"你有没有考虑过,当前正在写入的文件不在索引里,会不会漏补偿?"
这个问题我之前真没想到。
我赶紧分析了一下:
T1: 写入 audit_current.log (0 → 5000 行)
T2: 数据库恢复可用
T3: 补偿线程扫描目录
- 发现 audit_current.log
- 但索引中不存在该文件
- 跳过不处理 ❌
T4: 继续写入,直到达到 10000 行阈值才滚动
T5: 滚动后变成 audit_pending_001.log,才开始补偿
结果:这 5000 条日志延迟了很长时间才被补偿!
我的判断:允许补偿当前文件。
代价是需要用读写锁处理并发,但这个成本是可控的。
第三轮对话结束:当前文件也能被实时补偿。
06
还没完。
我又问了一个问题:
"文件滚动后,新文件名在索引里找不到,补偿时会不会从第0行开始读?"
这是一个容易忽略的边界情况。
如果不做处理:
T1: audit_current.log 写了 10000 行,索引记录 audit_current.log=10000
T2: 达到阈值,滚动文件
- rename: audit_current.log → audit_20260416_001.log
- 新建: audit_current.log
- 重置索引: audit_current.log=0
T3: 补偿线程扫描
- 发现 audit_20260416_001.log
- 索引中不存在该文件
- 从第 0 行开始补偿 ❌ 重复劳动!
T4: 启动时清理孤儿索引
- 发现 audit_current.log=10000 但文件不存在
- 删除该索引条目 ❌ 浪费
根本原因:滚动时没有同步更新索引中的键名。
我的判断:滚动时要同步迁移索引。
private void rotateFile() {
writer.close();
String newFileName = generateFileName();
currentFile.renameTo(new File(directory, newFileName));
// 关键:迁移索引
int compensatedLines = index.getCompensatedLines("audit_current.log");
index.atomicRenameKey("audit_current.log", newFileName);
currentFile = new File(directory, "audit_current.log");
writer = new BufferedWriter(new FileWriter(currentFile, true));
index.updateCompensatedLines("audit_current.log", 0);
index.flush();
}
第四轮对话结束:索引跟着文件走,避免重复劳动。
07
最终方案长这样:
audit-log-fallback/
├── audit_current.log # 当前活跃写入
├── audit_20260416_001.log # 历史文件
├── audit_20260416_002.log
└── .compensation_index # 唯一索引
核心设计:
- 一个Properties文件,记录
文件名=已补偿行数 - 所有
.log文件一视同仁,不需要区分状态 - 滚动时原子迁移索引
- 当前文件也能被实时补偿
最终成果:
- 代码量:~1000 行 → ~350 行(减少 65%)
- 索引结构:JSON对象 → Properties键值对
- 状态管理:3个目录+状态字段 → 无状态
08
写完这个方案,我复盘了一下,发现一件事:
AI给了很多方案,但每一个关键决策,都是我做的。
- 三目录要不要?→ 我决定不要
- 断点续传要不要?→ 我决定不要
- 当前文件要不要补偿?→ 我决定要
- 滚动时迁不迁移索引?→ 我决定迁移
AI的价值在于快速探索可能性,而人的价值在于判断哪个可能性是真正值得追求的。
09
回顾整个设计过程,我经历了三个阶段:
第一阶段:过度设计
- 试图解决所有可能的问题
- 复杂的状态机和索引结构
- 教训:完整性 ≠ 可靠性
第二阶段:逐步简化
- 质疑每个设计决策的必要性
- 去掉冗余的状态和字段
- 洞察:简单性本身就是可靠性
第三阶段:极简确立
- 单一索引、无状态文件
- 滚动时迁移、实时补偿
- 成果:350 行代码实现零丢失保障
10
这篇是系列第一篇,讲的是一次真实的AI协作设计过程。
下篇文章(《从这次实战聊聊:AI辅助设计,为什么人的判断力比以往更值钱》)我想聊聊:在这整个过程中,是什么让我做出了那些判断?AI时代,工程师真正稀缺的能力到底是什么?
这个系列是关于AI辅助设计的实战复盘和方法思考。如果你也用AI编程工具,希望我的经历能给你一些参考。