最近几天,Graphite 一口气合了十几个 PR。Cross-graph Search、Large Corpus Gate、Topology OOM、Query Budget、Parallel Search、Memory、Cancellation、Timeout,几乎都在做性能优化和 Benchmark。

一开始看得很爽。发现一个慢 Query,Agent 找到 Hot Path,改实现、补测试、跑 Benchmark,然后交出 36 倍甚至 87 万倍的提升。以前要折腾几天的 Performance Optimization,现在几个小时就能跑完一轮。

但 PR 越合越多,越看越觉得不对劲:为什么每个“优化完成”后面,都紧跟着另一个 PR 补 Benchmark?Graphite 每个主要模块的 Unit Test Line Coverage 门槛已经是 98%,到底还有什么没测到?

一开始只想把 Query 跑快

这一串从 PR #91 开始。Production 里有一条六个 Property 做模糊匹配的 Broad Discovery Query,Generic Path 会反序列化所有 Node,逐个判断、Projection、去重,最后才应用 LIMIT。问题看起来很清楚:把这条 Query 跑快。

Agent 很快加了 Mapped Index Fast Path,随后又在 PR #95 继续优化。同一个 590 万 Node 的 Android Graph,Broad Keyword Search 快了 36 倍,UNWIND labels(n) 从 8.7 秒降到 0.01 毫秒,快了 87 万倍。

这套 Loop 很容易让人上瘾。Slow Query 是 Input,JMH 数字是 Output,中间的 Source Code 交给 Agent 搜索。Full Scan 可以换成 Index,Multi-graph Search 可以加 Bounded Parallelism,DATAFLOW Query 可以改成 Lazy Traversal。实现一版接一版地换,写代码几乎不再构成阻力。

当时看起来,剩下的事情只是让 Agent 继续找 Hot Path。

每次以为结束都会冒出下一个场景

第一个问题出在 Benchmark 的世界太小。之前的测试根本没有在真实场景里跑过,连千万级 Node 都没遇到过。PR #92 开始把原来不到 100 万 Node 的 Elasticsearch Fixture 换成 Tika、Hive 和 Kotlin Compiler,并把 Build、Save、Mapped Load、Cypher Query 串成 4 GiB 的 End-to-end Gate。

有了真实 Corpus,比较方法又不够可靠。PR #93 把 Base 和 Candidate 放到同一台 GitHub Runner 上,遇到疑似 Regression 还要反转执行顺序再跑一遍。

但规模仍然没有补全。三个 Corpus 最大也只有 590 多万 Node,而真实 Deployment 已经到了 8000 万 Node。PR #94 再把 40 个 Android-scale Graph 同时打开,逻辑规模超过 2.3 亿 Node,Topology Query 直接在 3 GiB Heap 下 OOM。几个 Large Corpus 能通过,不代表 Multi-graph Topology 也安全。

到了 PR #98,连“Latency 更低”这件事都要重新定义。被计时的 Query 必须先成功,Row Count、顺序、Value 和 Graph Provenance 必须和 Baseline 完全一致。否则提前失败、Budget Exhausted 或只返回部分结果,都可能比正确答案更快。

后面还有更多。Parallel Search 开到 NCPU 反而 Regression,最后只保留两个 Worker;Filtered DATAFLOW Query 要单独限制 Memory;Client 断开以后要 Cancellation;HTTP Server 里的 Work Budget 最后又换成 Timeout。

最难受的地方在于,这些优化大多没有撒谎。它们在各自的 Benchmark 里确实更快,只是每个 Benchmark 都只切中了 Production 的一个截面。Graph、Query、并发、缓存、Heap 或失败方式一换,原来的结论就可能失效。

98% Coverage 依然没见过世面

Graphite 的 Unit Test Line Coverage 门槛是 98%。这个数字很容易造成一种错觉:大部分代码都跑过,剩下的风险应该只是少数 Edge Case。

最近这串 PR 把这种错觉打碎了。一个小 Graph、一种 Node、一次 Query,就可以跑过和 Production 相同的代码。换成千万级 Node、40 个 Graph、混合 Node、模糊搜索、并发请求和有限 Heap,执行的还是那些 Line,系统表现却完全不同。

所以问题不是 Test Code 太少。它们只是一直在实验室环境里跑,没跑过 Production 的规模、数据分布和失败方式。说白了,没见过世面。

Coverage 回答的是“哪些代码被执行过”。Test Case 还要回答另一组问题:在什么 Corpus、Query Shape、并发度和资源约束下,什么结果才算正确,怎样才算足够快,失败时应该发生什么。

Test Code 可以继续生成。Agent 很擅长给一条新 Branch 补 Coverage,也能把 JUnit 和 JMH 写得很完整。它无法从 98% 这个数字里猜出下一个没见过的 Production 场景。

Source Code 只是答案 Test Case 才是问题

到这里,最近这些 PR 的共同点才显出来:它们在优化性能,也在一笔一笔暴露 Case Debt。

87 万倍不是假数字。它准确描述了一个 590 万 Node Fixture 上,一种 UNWIND labels(n) Query 从 Full Scan 切到 Metadata Fast Path 之后的变化。问题在于,“这个 Case 快了 87 万倍”很容易被读成“Graphite 快了 87 万倍”。

Agent 优化的不是“软件”这个抽象概念,而是给定的 Case。 Case 只写 Latency,它就会沿着 Latency 搜索;没有固定 Expected Result,失败也可能成为最快路径;没有 Production Corpus,Synthetic Data 上的最优解就可能被当成产品最优解。

到这里,Source Code 和 Test Case 的价值开始分开了。Source Code 是这一版实现给出的答案:Full Scan、Index Fast Path、Work Budget、Timeout,下一轮都可以继续换。Test Case 保存的是问题和通过标准:面对这组 Corpus 和 Query,在这个资源边界里,必须给出什么结果。

Source Code 已经不再稀缺,Agent 几个小时就能换一版。真正难补的是那些没跑过、没见过,也没有人做过取舍的 Case。最近这一串 PR 最后留下的结论是:Test Case 才是软件的核心资产。

真的吗?

先拿 Graphite 反过来想。如果删掉一段实现,只要 Graph Model、Architecture 和这些 Case 还在,Agent 可以再写一版。如果删掉 Case,只留下 Source Code,它只能看见过去怎么做,看不出为什么要支持 8000 万 Node,为什么部分结果不能算成功,也不知道哪些 Latency 和 Heap 上限是产品承诺。一个奇怪分支究竟是规则、Bug 还是偶然行为,Source Code 自己不会解释。

Source Code 会被下一版 Source Code 替换。Test Case 不跟某一版实现绑定,它把 Production 里已经付过代价才知道的约束,带到下一版实现里。Architecture 决定系统的形状,Harness 负责把约束跑起来,Test Case 则保存具体场景里什么必须继续成立。

Agent 真的需要 TDD 吗?里写过,测试和实现由同一个 Agent 根据同一句模糊需求生成,很容易共享同一份误解。Graphite 这次把问题推进了更具体的一层:即使 Test Code 很多、Coverage 很高、Benchmark 也是真的,只要 Case 没有覆盖真实场景,Agent 仍然会把局部答案做得无比漂亮。

Test Code 也会跟着实现一起漂

到了 PR #104,问题又往前走了一层。它同样没改 Production Code,只做了一件事:防止 Benchmark Gate 自己被 PR 改松。

Corpus 集合必须完全一致,缺失、重复、无效或漂移的数据全部 Fail Closed;Comparator、Fixture 和 Workload 配置从 PR 的 Base SHA 读取;Gate 自己维护的文件再交给 CODEOWNERS 保护。如果 Agent 可以同时修改实现、Benchmark 和通过标准,那片绿色没有裁决能力。

所以标题里的 Test Case 不是某个 *Test.kt 文件。JUnit、JMH、Runner 和 GitHub Actions 都是实现,Agent 一样可以重写。不能跟着 PR 一起漂移的是问题本身:用哪组 Corpus,跑什么 Query,Expected Result 是什么,最多允许多少时间和内存,这些答案又来自哪次 Production 观察。

Graphite 的 Bytecode Graph 也在做同样的记账。80 个 Java Fixture 会被编译、建图,再和 11000 多条 Stable Fact 的 Baseline 比较。新的 Graph Fact 可以增加,旧的 Fact 少一条都不行。但这份 Baseline 也只能保护已经见过的 80 个 Fixture,不能替没见过的 Bytecode 和 Query 做保证。

Case 不可能一次写完。每次 Production 暴露新场景,再把 Input、Expected Behavior、资源边界和 Provenance 记回来,这本账才会越来越接近产品本身。

这个判断在 Graphite 里说得通。但要反证它,只要找到一种不需要额外准备 Case,仅靠现成 Code 就能让 AI 重写的软件。

Compiler 恐怕是唯一的反例

现实世界里已经有海量 Source Code。把它们交给新旧 Compiler,能不能编译、输出什么诊断、编译后的程序跑出什么结果,都是现成的 Verification Signal。甚至不需要专门为这次重写设计 Input,已有的 Codebase 就是 Test Case。Code is Test Case。

但这里的 Code 不是 Compiler 自己的 Source Code,而是拿来编译的 Source Code。前者仍然可以被 AI 整体替换,后者作为 Input Corpus,负责告诉新 Compiler 哪些行为必须保留下来。

Compiler 这个反例没有推翻结论,反而把边界说清楚了。它之所以几乎不需要额外写 Case,是因为整个软件生态积累的 Source Code 都在替它充当 Case。大多数软件没有这种条件。Graphite 的一次 Query 被拒绝,究竟是 Resource Policy 还是 Bug;结果变少,究竟是优化还是数据被截断,旧实现和外部代码都给不了答案。只有在真实场景里做过取舍,再把 Corpus、Expected Result 和资源边界记下来,它们才会成为下一版实现的 Case。

SaaS 的护城河不是 Source Code

回头再看这几天的 Graphite,36 倍和 87 万倍最先抓住眼球。现在反而更该看 PR #92PR #104:前者开始把真实 Corpus 带进测试,后者保证裁判不会跟着选手一起移动。

即便如此,Tika、Hive、Kotlin Compiler 也只是三个固定 Corpus,最大不过 590 多万 Node,离 8000 万 Node 的 Deployment 还有一个数量级。它们只是让原本没见过世面的测试开始接近真实世界,离完整还很远。

过去一年,资本市场一直在重新审视 SaaS。Morgan Stanley 把这一轮称为软件估值的历史性修正:连续几个季度的下跌抹掉了约 2 万亿美元市值,背后的担忧很直接,AI 会不会打穿传统 SaaS 的商业模式?

Claude Code 让这种担忧变得具体。它在 2025 年 5 月公开发布,六个月就做到了 10 亿美元 Run-rate Revenue。AI 在 Coding 这条路上的价值已经被产品和收入证明。既然 Code 可以大规模生成,一个 SaaS 产品是不是也能在几个月里被复制?

Anthropic 对约 40 万次 Claude Code Session 的分析也看到了同一条边界:人主要决定做什么,Claude 主要决定怎么做;使用者越懂自己的业务,成功率越高。Graphite 这串 PR 里,Source Code 是“怎么做”,Test Case 才是“做什么算对”。

AI 已经能生成 Code,也能根据已有实现补出很多 Test Code。它还不能凭空生成一个成熟 SaaS 多年积累下来的 Production Case:哪些脏数据必须兼容,哪种权限组合不能越界,一次失败应该重试还是立即终止,哪个看起来多余的分支其实背着客户承诺。这些 Case 不是从 Source Code 里自然长出来的,它们来自一次次上线、事故、投诉和复盘。

这才是 SaaS 的护城河。Source Code 可以被重新生成,前人踩过的坑不能。没有 Test Case,AI 复制出来的只是功能列表。想在短时间里复制一个成熟软件,前人趟过的坑一样还得再趟一遍。