post: 30 days of agent churn kept 23.5万 of 51.8万 lines (EN + ZH)
Deploy / build (push) Successful in 56s
Deploy / build (push) Successful in 56s
The commit-churn post the owner read and approved today, published in the first-person voice he asked for. - Voice rewrite before publishing: the draft opened on "when managers or recruiters ask how productive an engineer is" and addressed the reader in the second person throughout (you/your 12 -> 2, both in the CTA; ZH 你 5 -> 2). It now reads as a log entry: "Last week I did something I almost never bother to do: I counted my own commits." Headings moved from essay titles to actions. Numbers, the 5-repo table and every claim are unchanged. - Title changed at the owner's request: "Why 500,000 Lines of Commit Churn Only Left 230,000 Lines in Production" -> "I Committed 518,000 Lines in 30 Days. Production Kept 235,000." (ZH: 30 天提交 51.8 万行,生产环境只留下 23.5 万行), pubDate 2026-10-18. - Fixed two real defects found while making the card: the draft claimed "four automated gates" and listed three; and og-gen split titles at the word-count midpoint, breaking at "…Lines in / 30 Days" — the splitter now prefers a sentence boundary near the middle and falls back to the midpoint. - og PNG regenerated for the new title, thumbs and banner WebP re-derived. Measured: card 2 title lines, overflowY 0, measure_og_banner PASS (tags 1 row, banner delta 2), build clean, /posts/ monotonic with this post on top. Also queued the post at the head of the dev.to drip (queue 69 -> 70).
This commit is contained in:
1 parent
2060aaa166
commit
0d7384224d
9 files changed
+325
-23
No files matched your search
+22
-20
@@ -176,9 +176,10 @@ the URL — the daily dev.to drip below is the first distribution channel outsid
|
||||
- [x] ✅ **dev.to cross-posting is automated (2026-10-17).** `scripts/devto-crosspost.py` in the Hermes scripts dir
|
||||
(`C:\Users\hoelee\AppData\Local\hermes\scripts\`) publishes **one queued post per day** with `canonical_url`
|
||||
pointing at the blog original, from a Hermes **no_agent cron** (`devto daily crosspost`, id `ce8fc8bd23e6`,
|
||||
`25 12 * * *`, deliver Telegram). Queue = **69 ranked titles / every live post except three** — `hello-world` (blog intro page),
|
||||
`25 12 * * *`, deliver Telegram). Queue = **70 ranked titles / every live post except two** — `hello-world` (blog intro page),
|
||||
`how-i-made-my-own-songs-with-suno-ai` (8 `<audio>` players + a lyric `<script>` that dev.to's sanitiser strips)
|
||||
and the held-for-review post — at **one per day ≈ 2.3 months**. `devto-state.json` makes it idempotent per slug;
|
||||
— at **one per day ≈ 2.3 months**. The post that had been held for review joined the queue on top on 2026-10-18,
|
||||
the day it published. `devto-state.json` makes it idempotent per slug;
|
||||
the API key lives in `~/.secrets/devto-api-key.txt`, never in the repo. **Two guards added the same day:** an
|
||||
entry is refused while its frontmatter still says `draft: true` or its blog URL is not HTTP 200 (a transient
|
||||
5xx after 3 retries aborts the whole run instead of skipping, so queue order is never reshuffled by a tunnel
|
||||
@@ -759,31 +760,32 @@ start, so publishing added no pages. The number in that message is wrong; the de
|
||||
carry **no prose links** — every post they would naturally cite is dated later, and a backdated post may only link
|
||||
backwards. Their "related posts" rows are the theme's automatic ones, which every backdated post already has.
|
||||
|
||||
**Step B2p — (unplanned) The commit-churn post, held for the read (written 2026-10-17 04:00).** 🔵
|
||||
**Step B2p — (unplanned) The commit-churn post.** ✅ Published 2026-10-18 (`55b1f08`), after the owner's read and a voice rewrite
|
||||
|
||||
| Slug | Category | `pubDate` | Source |
|
||||
|---|---|---|---|
|
||||
| `why-500000-lines-of-commit-churn-only-left-230000-in-production` | `engineering` | 2026-10-17 | Hermes session 2026-10-17 |
|
||||
| `why-500000-lines-of-commit-churn-only-left-230000-in-production` | `engineering` | 2026-10-18 | Hermes session 2026-10-17 |
|
||||
|
||||
- **What it argues:** thirty days of autonomous-agent work across five self-hosted Gitea repos = 865 commits, 518,233
|
||||
additions, 130,892 deletions — and production kept 235,000 lines. Verification gates and pruning, not generation
|
||||
speed, are what survive.
|
||||
- **State (all of it measured 2026-10-17, not carried over from the writing session):** EN + ZH written;
|
||||
og + banner generated and **measured** (`scripts/measure_og_banner.py` → OG `rows=1 overflow=0`, banner 7 rows
|
||||
`delta=2 overflow=0`); local build clean at **250 `index.html` pages** with both `dist/` pages present; the generator
|
||||
changes are **purely additive** (`git diff --numstat` 8/0 and 18/0, `node --check` OK on both).
|
||||
**Not committed, not pushed** — four files (EN + ZH `.md`, og + banner PNG, plus the generator delta) sit in the working
|
||||
tree on purpose: publication gate, the owner has not read it.
|
||||
- ⚠ **This entry is the record of that hold.** Without it the next session reads the untracked files as someone else's
|
||||
in-flight work (or as already-published) — the same failure the skill warns about.
|
||||
- **Preview for the owner:** `cd dist && python -m http.server 8899 --bind 127.0.0.1`, then
|
||||
`/posts/why-500000-lines-of-commit-churn-only-left-230000-in-production/` and its `/posts/zh/…` twin.
|
||||
- **To publish on his go-ahead:** confirm the real date (`date` + the newest `<time>` on the live `/posts/`), then
|
||||
`git add` the four explicit paths, commit, push `origin main` **and** `github main`, verify the four URLs 200 and the
|
||||
`/posts/` order still monotonic. The `TERMINALS`/`BANNERS` entries ride along (whole-file staging) and stay inert until
|
||||
that slug is generated.
|
||||
- ⚠ **Kill the 8899 preview server once he has read it** — a wedged `python -m http.server` from the writing session was
|
||||
still holding the port on 2026-10-17 and served an empty response.
|
||||
- **The owner's read changed two things (both before publishing):**
|
||||
1. **Voice.** The draft opened *"When managers or recruiters ask how productive an engineer is…"* and kept addressing
|
||||
the reader in the second person (`you have to move beyond the chat box`, `If your Git log shows…`) — it read like a
|
||||
LinkedIn think-piece rather than a log entry, and it framed the post as written *for* recruiters. Rewritten in the
|
||||
first person (EN `you/your` 12 → 2, both in the CTA; ZH 你 5 → 2), headings changed from essay titles to actions
|
||||
(`What the agents actually touch`, `What changed in how I work`). All numbers, the table and the claims are
|
||||
unchanged — this was a voice edit, not a new draft.
|
||||
2. **Title.** `Why 500,000 Lines of Commit Churn Only Left 230,000 Lines in Production` →
|
||||
**`I Committed 518,000 Lines in 30 Days. Production Kept 235,000.`** (ZH: `30 天提交 51.8 万行,生产环境只留下 23.5 万行`).
|
||||
- **Two real defects found while fixing the card:** the draft said *"four automated gates"* and then listed **three**
|
||||
(now fixed to three); and `og-gen`'s title splitter broke at the word-count midpoint, which put the break at
|
||||
`…Lines in / 30 Days` — the splitter now prefers a sentence boundary near the middle and falls back to the midpoint
|
||||
(`.og/gen` measured afterwards: 2 title lines, `overflowY 0`, `measure_og_banner.py` PASS · tags 1 row · banner delta 2).
|
||||
- **State:** EN + ZH written and rewritten; og PNG regenerated for the new title, thumbs + banner WebP re-derived
|
||||
(`optimize-images.py`), build clean, `/posts/` order monotonic with the post on top at 2026-10-18.
|
||||
- ⚠ The four files were held **untracked** for two days on purpose (publication gate). Nothing else is held now.
|
||||
|
||||
|
||||
**Step B2m — The next-session topic queue (user decision 2026-10-16).** ⬜
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -449,11 +449,26 @@ tags = stripped
|
||||
.filter(Boolean);
|
||||
|
||||
// ---------- build template substitutions ----------
|
||||
// split the long title into two balanced lines for readability
|
||||
// split the long title into two balanced lines for readability.
|
||||
// Prefer a sentence boundary near the middle (". " / "? " / "! " / " — " / ": ") so a
|
||||
// line never dangles on a preposition ("…Lines in / 30 Days") when the title states a
|
||||
// headline and then a punchline; fall back to the word-count midpoint.
|
||||
const words = title.split(' ');
|
||||
const mid = Math.ceil(words.length / 2);
|
||||
const line1 = words.slice(0, mid).join(' ');
|
||||
const line2 = words.slice(mid).join(' ');
|
||||
let line1 = words.slice(0, mid).join(' ');
|
||||
let line2 = words.slice(mid).join(' ');
|
||||
const boundary = /[.?!#—:]\s+/g;
|
||||
let best = -1;
|
||||
let bm;
|
||||
while ((bm = boundary.exec(title)) !== null) {
|
||||
const cut = bm.index + 1; // keep the punctuation on the first line
|
||||
if (cut < title.length * 0.3 || cut > title.length * 0.75) continue;
|
||||
if (best === -1 || Math.abs(cut - title.length / 2) < Math.abs(best - title.length / 2)) best = cut;
|
||||
}
|
||||
if (best > 0) {
|
||||
line1 = title.slice(0, best).trim();
|
||||
line2 = title.slice(best).trim();
|
||||
}
|
||||
// escape HTML
|
||||
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
// title size: short titles can be larger
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: "I Committed 518,000 Lines in 30 Days. Production Kept 235,000."
|
||||
description: "Over 30 days, autonomous coding agents added 518,000 lines across 5 repos, yet production kept 235,000. Here is why verification gates matter more than generation."
|
||||
pubDate: "2026-10-18"
|
||||
category: "engineering"
|
||||
tags: ["ai", "git", "devops", "testing", "refactoring"]
|
||||
ogImage: "/og/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
|
||||
banner: "/banners/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
|
||||
---
|
||||
|
||||
Last week I did something I almost never bother to do: I counted my own
|
||||
commits. Not the ones I remember writing — the ones the repositories
|
||||
actually recorded. Five production repos on my self-hosted Gitea, thirty
|
||||
days, one person: 865 commits, 518,233 lines added, 130,892 deleted.
|
||||
|
||||
Then I counted what survived. A clean scan of the current revision of my
|
||||
main production application returns 234,976 lines. The video transcoder that
|
||||
gained 58,240 lines in the same window is 43,050 lines long today.
|
||||
|
||||
So where did the other 280,000 lines go? Nothing was lost. They were
|
||||
written, run, judged wrong, and deleted — or replaced by something smaller
|
||||
that did the same job. That loop, not the writing, is what I want to write
|
||||
down, because it changed how I work this year more than any model update
|
||||
did.
|
||||
|
||||
## The numbers I didn't believe
|
||||
|
||||
Here is the exact commit breakdown across the five repositories, between 17
|
||||
September and 17 October 2026:
|
||||
|
||||
| Repository | Commits (30d) | Additions (+) | Deletions (-) | Net Change | Current Total LOC | Primary Stack |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **numerology-report** | 432 | +331,672 | -98,937 | +232,735 | **234,976** | PHP, Markdown, JSON |
|
||||
| **hevc-transcode** | 187 | +58,240 | -15,508 | +42,732 | **43,050** | Python, HTML, Shell |
|
||||
| **dsm-resource-management** | 132 | +58,234 | -14,076 | +44,158 | **368,793** | Python, TypeScript, Astro |
|
||||
| **hoelee-blog** | 88 | +23,603 | -709 | +22,894 | **55,933** | Astro, TypeScript, Markdown |
|
||||
| **hoelee-agent-dist** | 26 | +46,484 | -1,662 | +44,822 | *(Dist)* | Python, Shell, Markdown |
|
||||
| **Total** | **865** | **+518,233** | **-130,892** | **+387,341** | — | — |
|
||||
|
||||
Two lines are easy to check yourself; this is the whole measurement:
|
||||
|
||||
```bash
|
||||
git -C numerology-report log --since=2026-09-17 --until=2026-10-17 \
|
||||
--numstat --pretty=format: | awk '{add+=$1; del+=$2} END {print add, del}'
|
||||
```
|
||||
|
||||
In a human team, churn like that usually means trouble: requirements moving
|
||||
under the code, a rewrite nobody admits to, an architecture that keeps
|
||||
collapsing. None of those applied here. Every one of these five repos is
|
||||
something I run and depend on, and every commit was authored in a session
|
||||
where I could read the diff before it landed.
|
||||
|
||||
The churn is what refactoring looks like when generation is cheap. An agent
|
||||
drafts a slice of the change, I run the gates, the gates disagree, and the
|
||||
slice gets thrown away or rewritten smaller. On the days I got the gates
|
||||
right, the size drop came faster than the features arrived.
|
||||
|
||||
## What the agents actually touch
|
||||
|
||||
Most people's first experience of AI code is a browser tab: paste a function
|
||||
into a chat window, copy the answer back into an editor. The bottleneck
|
||||
there is the clipboard. It also keeps the model far away from the two things
|
||||
that matter — the codebase's own conventions and the ability to run what it
|
||||
wrote.
|
||||
|
||||
So mine run in a terminal, on the machine, with real tools:
|
||||
|
||||
1. **Filesystem and syntax tools.** They read existing modules, patch by
|
||||
fuzzy match instead of overwriting whole files, and run syntax or AST
|
||||
checks before anything is saved.
|
||||
2. **Test and execution runners.** PHPUnit suites, pytest harnesses, and
|
||||
headless Chrome probes, executed in the foreground with the output read
|
||||
back.
|
||||
3. **Version control.** They read `git diff`, stage explicit paths, and look
|
||||
at the history of a file to see why the previous decision was made.
|
||||
|
||||
That combination is fast — an order of magnitude faster than me typing the
|
||||
first draft. It is also, on its own, a way to make a codebase rot faster,
|
||||
which is the part nobody warns you about.
|
||||
|
||||
## Where the bloat comes from
|
||||
|
||||
Unsupervised models love to write code. Give one a small edge case and its
|
||||
instinct is to add a private helper class, clone an existing utility with
|
||||
one extra parameter, or wrap a library in three layers of abstraction that
|
||||
only make sense to the diff that introduced them. Left alone for a week,
|
||||
that is a few thousand lines of confident, plausible, unnecessary code.
|
||||
|
||||
I know because I have that week in my own history. Two things came out of
|
||||
reading these diffs every day:
|
||||
|
||||
- A new class has to earn its place against a standard-library call I
|
||||
already have.
|
||||
- Anything generated is guilty until a deterministic check says otherwise.
|
||||
"Looks correct" from a model is not evidence; a red test is.
|
||||
|
||||
## The gates that actually deleted the code
|
||||
|
||||
During the V2 architecture overhaul of my main production application, more
|
||||
than 98,000 lines went out in thirty days. Almost none of that was me
|
||||
sitting down to delete things. It went because three gates kept rejecting
|
||||
what came back:
|
||||
|
||||
### 1. Deterministic harnesses instead of "does this look right?"
|
||||
|
||||
Asking a model to review its own code mostly produces confidence, not
|
||||
correctness. The useful move is to wire the work to something that cannot be
|
||||
talked out of failing:
|
||||
|
||||
- In the PHP application, every public route parameter has to carry an
|
||||
explicit default, enforced by a reflection probe
|
||||
(`ControllerAutoRouteSafetyTest.php`) — that test exists because a bare
|
||||
route crashed with a 500 in production.
|
||||
- In the report pipeline, a headless PDF extractor counts the rendered
|
||||
Chinese characters before a build is allowed to pass, so a font or layout
|
||||
change that silently drops glyphs fails the build instead of shipping a
|
||||
broken PDF.
|
||||
- In the transcoder, the ledger compares exact byte counts rather than
|
||||
approximating in megabytes, because rounded numbers hid a size regression
|
||||
for weeks.
|
||||
|
||||
If a gate goes red, the agent either fixes the change or drops it. Nothing
|
||||
reaching a commit is exempt from that.
|
||||
|
||||
### 2. Pruning as a standing task, not a cleanup project
|
||||
|
||||
A portion of every session goes to deleting. Can a 200-line helper be
|
||||
replaced by a ten-line call into something that already exists? Is this new
|
||||
mapper duplicating a function that has lived in `app/Libraries/` for a year?
|
||||
The 130,000 deletions above are not an accident of a rewrite; they are
|
||||
scaffolding that outlived its purpose, migration layers with one caller, and
|
||||
logic that arrived twice under two names.
|
||||
|
||||
### 3. Keeping deterministic work deterministic
|
||||
|
||||
The trap I keep seeing in AI-assisted projects is a model being asked to do
|
||||
arithmetic it should never touch. If a shell pipeline, a SQL constraint, an
|
||||
idempotent script, or a cron job can answer it, an LLM has no business
|
||||
there.
|
||||
|
||||
Models are good at drafting an initial implementation, generating a
|
||||
three-language dictionary, or mapping a third-party API's quirks. They are
|
||||
not where I put state transitions, money, or queue scheduling, because a
|
||||
probabilistic answer to a deterministic question is a bug with a delay fuse.
|
||||
|
||||
## What changed in how I work
|
||||
|
||||
For most of my career the physical constraint was typing speed and
|
||||
remembering syntax. Output was the metric, and a productive week looked like
|
||||
a lot of new files.
|
||||
|
||||
Syntax is free now. I can get a 500-line migration with tests drafted while
|
||||
I read the diff of the previous one. So the constraint moved to the three
|
||||
things a model cannot check for me:
|
||||
|
||||
- **Boundary design** — where components separate, and which state is
|
||||
allowed to cross.
|
||||
- **Test architecture** — the harness that catches semantic drift the type
|
||||
checker never sees.
|
||||
- **Pruning discipline** — the willingness to delete 280,000 lines of
|
||||
exploratory, redundant, or dead code so 235,000 lines can go to
|
||||
production.
|
||||
|
||||
Which is why I no longer read a commit graph as a productivity chart. Half a
|
||||
million added lines in a month tells me the tools were running. The 130,000
|
||||
deleted lines, the refactor underneath them, and a suite that is still green
|
||||
tell me whether the month was worth anything.
|
||||
|
||||
## Building something that has to stay up?
|
||||
|
||||
I run **Hoelee Enterprise**, a website design and development studio for
|
||||
Malaysian SMEs. The agent workflow, the test harnesses and the self-hosted
|
||||
infrastructure in this post are the same setup I build and maintain client
|
||||
systems on.
|
||||
|
||||
If you need full-stack engineering, workflow automation, or infrastructure
|
||||
that survives its own maintenance:
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected])
|
||||
- **Website:** [hoelee.com](https://hoelee.com)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "30 天提交 51.8 万行,生产环境只留下 23.5 万行"
|
||||
description: "过去 30 天,自主编程 Agent 在 5 个仓库中累计新增了 51.8 万行代码,但最终留存仅 23.5 万行。为什么在 AI 时代,验证闸门与代码剪枝比生成速度重要十倍?"
|
||||
pubDate: "2026-10-18"
|
||||
category: "engineering"
|
||||
tags: ["ai", "git", "devops", "testing", "refactoring"]
|
||||
ogImage: "/og/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
|
||||
banner: "/banners/why-500000-lines-of-commit-churn-only-left-230000-in-production.png"
|
||||
---
|
||||
|
||||
上周我做了一件平时几乎不会做的事:数了数自己的提交。不是数我记得写过的那些,而是数仓库里真实记录下来的那些。自建 Gitea 上 5 个生产仓库、三十天、一个人:865 次 Commit、新增 518,233 行、删除 130,892 行。
|
||||
|
||||
然后我数了数活下来的部分。对主生产项目的最新版本跑一次干净扫描,有效代码总量是 234,976 行;同一时间窗口里净增 58,240 行的视频转码器,今天整个仓库只有 43,050 行。
|
||||
|
||||
那 28 万行进哪儿去了?没有丢。它们被写出来、跑过测试、被判为不对,然后删掉——或者被一个更小、做同一件事的东西替换掉。我想把这条循环记下来,因为它这一年对我工作方式的影响,比任何一次模型升级都大。
|
||||
|
||||
## 一开始我不信的这几个数字
|
||||
|
||||
以下是 2026 年 9 月 17 日至 10 月 17 日,名下 5 个活跃仓库的精确 Git 统计:
|
||||
|
||||
| 仓库名称 | 30 天 Commit | 累计新增 (+Add) | 累计删除 (-Del) | 净变动 (Net) | 当前存量有效代码/文本 | 核心技术栈 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **numerology-report** | 432 | +331,672 | -98,937 | +232,735 | **234,976** | PHP, Markdown, JSON |
|
||||
| **hevc-transcode** | 187 | +58,240 | -15,508 | +42,732 | **43,050** | Python, HTML, Shell |
|
||||
| **dsm-resource-management** | 132 | +58,234 | -14,076 | +44,158 | **368,793** | Python, TypeScript, Astro |
|
||||
| **hoelee-blog** | 88 | +23,603 | -709 | +22,894 | **55,933** | Astro, TypeScript, Markdown |
|
||||
| **hoelee-agent-dist** | 26 | +46,484 | -1,662 | +44,822 | *(Dist)* | Python, Shell, Markdown |
|
||||
| **合计** | **865** | **+518,233** | **-130,892** | **+387,341** | — | — |
|
||||
|
||||
其中两行你可以自己复算,这就是完整的量法:
|
||||
|
||||
```bash
|
||||
git -C numerology-report log --since=2026-09-17 --until=2026-10-17 \
|
||||
--numstat --pretty=format: | awk '{add+=$1; del+=$2} END {print add, del}'
|
||||
```
|
||||
|
||||
在人类团队里,这样的更替量通常意味着出事了:需求在代码底下不停移动、有人偷偷在重写、架构反复塌陷。这几种情况我一条都不占——这 5 个仓库都是我每天在跑、离不开的东西,每一次提交都发生在能读到 diff 的会话里。
|
||||
|
||||
生成成本变低之后,这种更替量就是重构的样子:Agent 起草一段改动,我跑闸门,闸门不同意,这段就被丢掉或者改小重写。凡是闸门写对的那几天,代码总量往下掉得比功能上来的速度还快。
|
||||
|
||||
## Agent 实际接手的是什么
|
||||
|
||||
大多数人第一次让 AI 写代码,是在一个浏览器标签页里:把函数粘进对话框,再把答案拷回编辑器。这条路的瓶颈是剪贴板,而且模型离真正重要的两样东西都很远——仓库自己的约定,以及运行它刚写出来的东西的能力。
|
||||
|
||||
所以我的是跑在终端里、跑在这台机器上的,手里有真工具:
|
||||
|
||||
1. **文件与语法工具**:直接读已有模块,用模糊匹配打补丁而不是整文件覆盖,保存前先过语法与 AST 校验。
|
||||
2. **测试与执行器**:PHPUnit 单元测试、pytest 套件、无头 Chrome 探针,在前台跑起来并把输出读回来。
|
||||
3. **版本控制上下文**:读 `git diff`、显式暂存指定路径、翻一个文件的历史看上一个决定为什么这么做。
|
||||
|
||||
这套组合很快,比我亲手打第一版快一个数量级;但它本身也是让代码库腐烂得更快的方法——这一半通常没人提醒。
|
||||
|
||||
## 臃肿是从哪里长出来的
|
||||
|
||||
不受约束的模型天生爱写代码。给它一个细小的边界情况,它的第一反应是新建一个私有辅助类、把一个已有工具函数复制一份改个参数,或者给一个库套上三层抽象——那三层只有在这段 diff 里才讲得通。放它自己跑一周,就是几千行语气笃定、看起来合理、其实没必要的代码。
|
||||
|
||||
我这么讲是因为我自己的历史里就有那一周。每天读这些 diff,最后落成两条规矩:
|
||||
|
||||
- 新建一个类,要跟一个我本来就有的标准库调用竞争位置。
|
||||
- 生成出来的东西默认有罪,直到某个确定性检查说它没问题。模型说「看起来对」不算证据,红的测试才算。
|
||||
|
||||
## 真正把代码删掉的是三道闸门
|
||||
|
||||
在最近一次主生产系统的 V2 架构重构中,三十天里删掉了 98,000 多行。这些几乎不是我坐下来专门去删的,而是三道闸门不断把回来的东西打回,最后留下的就是更小的版本:
|
||||
|
||||
### 1. 用确定性脚手架,而不是问「这段看着对不对」
|
||||
|
||||
让模型审查自己写的代码,多数时候产出的是自信,不是正确。有用的做法是把它接到一个没法被说服成绿灯的东西上:
|
||||
|
||||
- PHP 应用里,所有公开路由参数必须带显式默认值,由专用的反射探针(`ControllerAutoRouteSafetyTest.php`)守着——这个测试存在,是因为一条裸路由在生产上真的 500 过。
|
||||
- 报告生成管线里,无头 PDF 解析器先数一遍渲染出的汉字数量,数不对就不许构建通过;这样字体或排版改动如果悄悄漏字,就是构建失败,而不是发出一份坏 PDF。
|
||||
- 转码队列里,台账比的是精确字节数而不是 MB 估算,因为圆整过的数字把一次体积回退藏了好几个星期。
|
||||
|
||||
闸门红了,Agent 要么改到过,要么把这段撤掉。没有任何东西能豁免。
|
||||
|
||||
### 2. 把剪枝当成日常动作,而不是一次大扫除
|
||||
|
||||
每次会话都会分出一段时间专门删东西:这个 200 行的辅助类,能不能换成一行已有的原生调用?这个新的映射层,是不是在重复 `app/Libraries/` 里放了一年的某个函数?上面那 13 万行删除不是重写的副产品,而是活过头了的脚手架、只有一个调用方的迁移层,以及换了名字出现两遍的同一段逻辑。
|
||||
|
||||
### 3. 确定性的活留给确定性代码
|
||||
|
||||
我在 AI 项目里反复看到的坑,是让模型去做它根本不该碰的计算。如果一条 Shell 管道、一个 SQL 约束、一个幂等脚本或者一个 Cron 任务就能给出答案,那 LLM 就不该出现在那里。
|
||||
|
||||
模型擅长起草初始实现、生成三语词库、摸清三方 API 的脾气;但状态流转、钱、队列调度,我不会交给它——一个确定性问题上概率性的答案,是一颗带延时引信的 bug。
|
||||
|
||||
## 我的工作方式变了什么
|
||||
|
||||
我职业生涯的大部分时间里,物理瓶颈是打字速度和记语法。产出就是指标,一个高效的一周看起来就是新建了很多文件。
|
||||
|
||||
现在语法是免费的。我可以一边读上一次改动的 diff,一边让 Agent 起草一份带单元测试的 500 行迁移。于是瓶颈转移到了模型替我看不了的三件事上:
|
||||
|
||||
- **边界设计**:模块从哪里分开,哪些状态允许跨过去。
|
||||
- **验证体系**:那个能抓住类型检查看不见的语义漂移的脚手架。
|
||||
- **剪枝的决断力**:愿不愿意删掉那 28 万行探索性、冗余、已死掉的代码,只把 23.5 万行送进生产。
|
||||
|
||||
所以我现在不再把提交曲线当生产率图表看。一个月新增 50 万行,只能说明工具在转;真正说明这一个月值不值的是那删掉的 13 万行、底下的重构,和一套仍然全绿的测试。
|
||||
|
||||
## 需要一套能长期不倒的系统?
|
||||
|
||||
我运营 **Hoelee Enterprise**,为马来西亚中小企业做网站设计与系统开发。上面这套 Agent 工作流、测试脚手架和自托管基础设施,就是我给客户交付并长期维护系统时用的同一套东西。
|
||||
|
||||
如果你需要全栈系统研发、业务流程自动化,或者能扛住自己维护周期的自建基础设施:
|
||||
- **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- **Email:** [[email protected]](mailto:[email protected])
|
||||
- **官网:** [hoelee.com](https://hoelee.com)
|
||||
Reference in new issue
Block a user