软件工程里最被低估的成本:Navigation Cost

作者:

软件工程领域已经花了几十年时间研究如何管理复杂度。

从 MVC 到 DDD,从单体架构到微服务,从分层设计到六边形架构,各种框架、设计模式和工程实践层出不穷。它们解决的问题大同小异:如何组织代码,如何降低耦合,如何让系统在规模不断扩张的情况下仍然保持可维护性。

然而有一个问题,却很少被单独拿出来讨论。

假设把一个二十万行代码的项目交给一个经验丰富的工程师,让他修复一个线上问题。你觉得最大的成本是什么?

通常人们会认为是理解业务逻辑、理解系统架构,或者理解历史设计决策。但实际情况往往并非如此。在很多项目中,最大的成本其实是找到代码。

这里的“找到代码”并不是指找到某个文件的位置。现代 IDE 已经足够强大,全局搜索、调用链分析、类型跳转都非常成熟。真正困难的是找到系统运行时的入口,找到某段逻辑是在什么条件下被触发的,以及它是如何进入当前执行链路的。

例如,订单支付成功后为什么会发放积分?这个逻辑可能存在于支付回调中,也可能存在于 MQ 消费者中,可能由 Spring Event 触发,也可能是定时补偿任务完成的。即使你知道积分相关代码位于哪个模块,也不意味着你知道它是在什么时候、由什么入口触发执行的。

对于一个陌生系统而言,代码的存储结构和代码的运行结构往往是两套不同的体系。目录结构、包结构、模块划分描述的是代码如何组织,而真正决定系统行为的,是 HTTP 请求、消息队列、定时任务、事件监听器、RPC 调用、WebSocket 消息以及各种启动任务构成的运行时入口。

有趣的是,这个问题在 AI Agent 时代变得更加明显。

前段时间,我使用 Qwen3-Coder-Next 对一个老项目进行优化。

需求其实非常简单。

两个优化点,没有新增功能,没有架构调整,甚至不涉及数据库变更。按照正常开发经验,这种级别的修改最终落地的代码可能不到一百行。

然而整个任务执行完成后,消耗了400万+ Token。

最开始我以为是模型推理效率的问题。后来复盘整个过程才发现,问题根本不在推理,而在寻找。

模型用了大量时间搜索关键词、打开文件、分析调用关系、理解执行链路,然后不断排除无关模块。

订单相关代码读了一遍,支付相关代码读了一遍,消息消费者读了一遍,定时任务读了一遍,事件监听器读了一遍。

最后发现真正需要修改的位置只有两个方法。

换句话说,400万Token 里,99%都不是花在“改代码”上,而是花在“找到代码”上。

这件事恰恰也是人类开发者每天都在做的事情。

只不过 Agent 消耗的是 Token,而人类消耗的是时间。

软件工程里有很多被广泛讨论的成本。

开发成本、维护成本、测试成本、部署成本、沟通成本。

但很少有人单独讨论导航成本(Navigation Cost)。

所谓导航成本,是指开发者或 Agent 为了找到正确代码位置而付出的成本。

它不创造业务价值,不产生功能,不修复 Bug,但几乎存在于所有开发活动中。

修复问题之前,需要先找到问题。

实现需求之前,需要先找到入口。

优化性能之前,需要先找到链路。

很多团队统计开发效率时,会关注编码时间、测试时间、发布时间,却很少统计:为了找到需要修改的位置,究竟浪费了多少时间。

而随着系统规模扩大,以及 Agent 开始参与开发流程,这部分成本正在变得越来越昂贵。

最近看到一个有意思的开源项目:codegraph

它会扫描整个仓库,分析目录结构、模块依赖、入口文件以及调用关系,然后生成一份代码地图。

很多人把它看成 Agent 的辅助工具。

但我更关注另一件事。

Codebase Map 的出现,本身就在证明一件事情:导航已经成为一个独立问题。

如果导航不是问题,为什么会有人专门开发一套工具来生成代码地图?

如果项目天然具备可导航性,为什么 Agent 需要先扫描几万行代码才能开始工作?

如果代码入口很容易定位,为什么每个接手项目的人都要花大量时间寻找系统从哪里开始运行?

Codebase Map 其实是在给 Agent 补导航。

它解决的问题不是代码理解,而是代码定位。

但它的工作方式是后置的。

Agent 进入项目。

扫描项目。

理解项目。

生成地图。

然后开始工作。

整个过程本质上是在重复发现已经存在的信息。

而这些信息其实从项目创建的第一天就已经存在。

HTTP 入口不会突然改变。

MQ 消费者不会凭空出现。

定时任务、RPC 服务、事件监听器也不是运行时才生成的。

既然所有后来者都需要这份地图,那么地图本身为什么不成为项目资产?

如果一个问题每个接手项目的人都会遇到,每个 Agent 也都会遇到,那么它本质上就不是个人问题,而是项目问题。

一个新人接手项目,第一件事是找入口;一个 Agent 接手项目,第一件事还是找入口。十个新人接手项目,会重复找十次;十个 Agent 执行任务,也会重复找十次。

而这些信息实际上并没有变化。

HTTP 请求入口在哪里,MQ 消费入口在哪里,哪些定时任务会执行,哪些 Spring Event 会被触发,哪些 RPC 服务对外暴露。

这些信息从项目创建开始就已经存在。

既然如此,为什么要让每一个后来者重新发现一遍?

软件工程里有一个很经典的原则:不要重复劳动。

但对于系统入口的发现过程,整个行业似乎一直在重复劳动。

新人重复寻找。

老员工重复解释。

Agent 重复搜索。

每一次接手项目,都要重新建立一遍运行时认知。

如果只讨论理念,这件事听起来有些抽象。

举个例子。

假设线上反馈:用户支付成功了,但积分没有到账。

对于接手项目的人来说,第一个问题并不是如何修复,而是积分逻辑从哪里进入系统。

是支付回调直接发放?

是 MQ 异步消费?

是 Spring Event?

还是定时补偿任务?

很多时候,真正消耗时间的不是修复逻辑,而是定位逻辑。

如果项目里存在这样一份入口索引:

# 系统入口索引

## HTTP接口

| 功能   | 路径                | 方法   | 实现类             | 方法名      |
| ---- | ----------------- | ---- | --------------- | -------- |
| 创建订单 | /api/order/create | POST | OrderController | create() |
| 取消订单 | /api/order/cancel | POST | OrderController | cancel() |

## MQ消费者

| 功能      | Topic                  | 实现类                    | 方法名                  |
| ------- | ---------------------- | ---------------------- | -------------------- |
| 订单创建后处理 | order.queue            | OrderEventProcessor    | handleOrderCreated() |
| 支付结果回调  | payment.callback.queue | PaymentCallbackHandler | onPaymentResult()    |

## 定时任务

| 功能     | Cron         | 实现类                   | 方法名              |
| ------ | ------------ | --------------------- | ---------------- |
| 清理超时订单 | 0 2 * * *    | CleanExpiredOrdersJob | execute()        |
| 生成日报   | 0 0 10 * * ? | DailyReportJob        | generateReport() |

## Spring事件

| 功能        | Event          | 实现类                | 方法名           |
| --------- | -------------- | ------------------ | ------------- |
| 支付完成后发放积分 | OrderPaidEvent | PointEventListener | onOrderPaid() |

## RPC服务

| 功能     | 服务             | 实现类                | 方法名           |
| ------ | -------------- | ------------------ | ------------- |
| 查询用户信息 | UserRpcService | UserRpcServiceImpl | getUserInfo() |

## WebSocket消息

| 功能     | 消息类型         | 实现类                  | 方法名         |
| ------ | ------------ | -------------------- | ----------- |
| 实时聊天消息 | chat.message | ChatWebSocketHandler | onMessage() |

## Startup初始化任务

| 功能   | 类型                | 实现类               | 方法名   |
| ---- | ----------------- | ----------------- | ----- |
| 缓存预热 | CommandLineRunner | CacheWarmupRunner | run() |

那么接手者面对“支付成功但积分没到账”这个问题时,根本不需要全局搜索。

他首先会查看支付回调和订单支付事件对应的入口实现,然后直接进入对应代码。

整个过程从“探索系统”变成了“验证逻辑”。

这里最重要的一点是,这份文档并不解释业务,不解释架构,不解释实现,甚至不记录参数。

它只负责回答一个问题:

系统有哪些地方会触发代码执行?

剩下的事情交给源码。

因为接手项目的人本来就应该具备阅读当前技术栈代码的能力。

这也是我开始思考 Runtime Entry Outline(REO) 的原因。

它并不试图替代架构文档,也不试图解释业务逻辑,甚至不关心代码实现。

它只记录一件事情:

系统有哪些运行时入口。

很多人看到这里,第一反应可能是:这不就是文档吗?

其实不完全是。

传统文档描述的是系统如何设计,而 Runtime Entry Outline(REO) 描述的是系统如何运行。

前者关注结构,后者关注入口。

前者回答的是“系统长什么样”。

后者回答的是“系统从哪里开始执行”。

从 Agent 的角度看,这种信息甚至比架构图更有价值。

因为 Agent 最昂贵的成本之一并不是理解代码,而是寻找代码。

如果入口已经被明确列出,Agent 可以直接定位到相关文件,而不是从整个仓库开始搜索。

对于人类开发者来说也是一样。

很多线上问题的修复时间只有几分钟。

真正耗费时间的,是找到需要修改的位置。

过去的软件工程关注的是代码资产。

源代码是资产,数据库设计是资产,架构设计是资产。

但随着系统规模越来越大,以及 AI Agent 开始参与开发流程,还有一种资产开始变得越来越重要:系统认知。

哪些地方会触发代码执行,哪些入口承担核心业务,哪些链路真正影响系统行为。

这些信息长期存在于老员工的大脑里,却很少以结构化的形式沉淀下来。

于是每一个新人都要重新探索一遍,每一个 Agent 也要重新搜索一遍。

如果一个知识会被反复获取,如果一个过程会被反复执行,那么它就不应该依赖经验传承,而应该成为项目本身的一部分。

如果每个新人、每个 Agent 都要重新构建同一份地图,那么这份地图本身就应该成为项目资产。

或许未来项目仓库里,除了 README 之外,还会存在另一份默认文件:

系统入口索引。

它不负责解释系统。

它只负责告诉后来者:

代码从哪里开始运行。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注