一次与AI的"设计对话":审计日志降级方案从1000行到350行

4 阅读7分钟

一次与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沉默了。

是的——pendingarchivingarchived 本质上就是同一个文件的不同阶段。文件存在就代表待补偿,文件不存在就代表已完成。

用目录区分状态,有点多余。

我又问:"状态字段是不是也多余了?"

"状态字段确实可以用来标记 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            # 唯一索引

核心设计:

  1. 一个Properties文件,记录 文件名=已补偿行数
  2. 所有 .log 文件一视同仁,不需要区分状态
  3. 滚动时原子迁移索引
  4. 当前文件也能被实时补偿

最终成果:

  • 代码量:~1000 行 → ~350 行(减少 65%)
  • 索引结构:JSON对象 → Properties键值对
  • 状态管理:3个目录+状态字段 → 无状态

08

写完这个方案,我复盘了一下,发现一件事:

AI给了很多方案,但每一个关键决策,都是我做的。

  • 三目录要不要?→ 我决定不要
  • 断点续传要不要?→ 我决定不要
  • 当前文件要不要补偿?→ 我决定要
  • 滚动时迁不迁移索引?→ 我决定迁移

AI的价值在于快速探索可能性,而人的价值在于判断哪个可能性是真正值得追求的


09

回顾整个设计过程,我经历了三个阶段:

第一阶段:过度设计

  • 试图解决所有可能的问题
  • 复杂的状态机和索引结构
  • 教训:完整性 ≠ 可靠性

第二阶段:逐步简化

  • 质疑每个设计决策的必要性
  • 去掉冗余的状态和字段
  • 洞察:简单性本身就是可靠性

第三阶段:极简确立

  • 单一索引、无状态文件
  • 滚动时迁移、实时补偿
  • 成果:350 行代码实现零丢失保障

10

这篇是系列第一篇,讲的是一次真实的AI协作设计过程。

下篇文章(《从这次实战聊聊:AI辅助设计,为什么人的判断力比以往更值钱》)我想聊聊:在这整个过程中,是什么让我做出了那些判断?AI时代,工程师真正稀缺的能力到底是什么?


这个系列是关于AI辅助设计的实战复盘和方法思考。如果你也用AI编程工具,希望我的经历能给你一些参考。