
当代码、测试和原型都能由 AI 快速生成,软件开发中最贵的部分就不再只是“把程序写出来”。不同参与者究竟想解决什么问题,什么行为算正确,需求变动后哪些决定必须重做,这些问题会直接决定生成出来的系统能否继续演化。
软件工程研究如何让意图进入规约,再落实为实现,并在维护中保持它们之间的联系。软件是逻辑制品,细微的不一致也可能让它失效;实现中的错误还会沿着依赖关系反过来暴露需求和设计的问题。AI 降低了实现成本,却也能更快地扩散未经理清的假设。需求、产品设计和架构因此成为影响软件走向的关键杠杆。
这些问题也不容易像算法性能那样放进统一实验里比较:缺陷预测能回答某些代码更可能出错,却不能直接判断客户真正需要什么、产品是否好用、某种架构几年后是否仍合适。越是难以量化的决定,越需要把假设、反馈和实际结果留下来,反复检验。
一、需求不是一张功能清单
教务系统把需求工程中的难题集中在一起。学生关心选课与毕业,教师需要排课和登记成绩,教务人员要执行培养方案并处理例外;这些角色的目标会在名额、先修要求和毕业审核上相遇。不同参与者可能各自说得很清楚,却仍需要有人决定冲突时系统应如何行动。
设想一份培养方案要求至少修满 160 学分,并分别满足通识、专业必修、选修和实践学分。数学要求可以由高数 A 满足,也可以由高数 B 加桥接课满足;缓考学生暂时可以选后续课程,但若开课前仍未通过就要撤销;经过批准的两门交换课程可以合抵一门必修课,却不能重复计入选修学分。转专业、重修、补考、课程替代和人工豁免会让这些规则继续交错。每条规则单独看并不复杂,难处在于它们落到具体学生和具体学期时会同时生效。
字段的边界同样会影响整个系统。若把一位学生的缺课次数从 0 改成 -1,依赖这条记录的多个选课信息页面都可能失败。一个看似局部的数值,可能已经成为许多功能共同依赖的前提;修补出错页面并不能代替对数据含义、有效范围和共享关系的理解。
系统还必须解释例外本身:谁批准了豁免、依据哪个版本的培养方案、适用于谁、理由是什么。若系统没有表达这些业务事实,工程师可能只能直接改数据库来解决眼前问题。结果也许暂时正确,但过后难以分辨这是一项正式审批,还是一次没有留下依据的救火操作。
让 AI 调查现有流程、整理需求目录、角色、术语、验收场景和追踪关系,能快速得到可讨论的起点。形式化的需求编号有助于把业务目标、需求项与测试联系起来,也能让团队分工和检查进度。但文档写得完整,不代表参与者已经达成一致;需求数量多,也不代表产品价值已经验证。工具可以整理已观察到的规则,却不能替业务各方裁定目标冲突。
规格还包括许多看似细小的行为:当前学期如何切换、用户有多种身份时先显示什么、按钮在什么时间段可用、拒绝操作时给出什么理由。把这些内容写成可观察的验收场景,才能检查不同角色是否理解一致;没有被确认的猜测,则应保留为待决事项。由同一份不完整规格生成实现、测试和演示界面,只会让相同的遗漏在多个产物里重复出现。
二、MVP 能发现需求,也会留下技术债
最小可行产品的价值,是用最少的实现尽快让用户看到、操作和批评一个结果。课表、选课入口或毕业审核原型可以暴露没有说出口的需求,例如交换课程是否需要被认定。原型把讨论从抽象描述带到真实使用,反馈往往比继续补写长文档更具体。
但原型通过正常路径,不代表系统已经理解现实中的例外。需求变动时,改动可能从界面一路穿过课程映射、学分计算、成绩单、审核逻辑和测试。比如把“一门交换课程替代一门本校课程”改为“两门交换课合抵一门必修课”,旧决定是否重算、已经批准的例外是否继续生效,也都需要回答。
每个早期决定都会为未来留下约束。AI 可能很快选择数据库、前后端结构、Excel 版本或数据布局;这些选择一旦进入代码、测试和持续对话的上下文,之后的每项需求都会以它们为前提。没有验证的决定越多,返工范围就越难预测。MVP 应优先验证最重要的价值,同时让未验证的假设保持可见,而不是把所有想得到的功能一并实现。
需求永远不可能一次说尽,也会随着组织、政策和使用经验变化。快速原型不是万能解法,瀑布式地把早期理解固定成完整规格也不是。变化不可避免,工程设计要做的是让变化有地方可去,使它尽量局限在少数明确边界中。
三、架构从几行代码开始
架构不是大型系统专属的框图,也不等于微服务、数据库和技术栈的选择。一个小程序按读取输入、解析、处理、输出分成阶段,就已经在决定职责和数据怎样流动。若每一步都偷偷修改同一组全局变量,表面上的函数边界并不能阻止变化扩散;真正的边界还要说明谁拥有状态、谁能依赖谁、错误由谁处理。
命令行参数解析也是一种架构选择。直接用一连串字符串比较处理参数,短选项、长选项和格式变化会散落到业务代码中;把解析集中到标准解析器,再把结果转换成明确的参数状态,便把一类复杂性关在边界内。编程语言的函数、类型、作用域和调用栈,也都能用于表达约束与责任。
架构决定复杂性放在哪里,也决定需求变化时影响哪些部分。拆成更多函数或文件并不会自动得到好架构;关键在于稳定接口、状态归属和依赖关系是否清楚。每次抽象都应能说清楚它隔离了哪种变化,又增加了什么维护责任。
四、汉诺塔说明表示方式会改变难度
非递归汉诺塔可以有几种不同的表达。第一种直接利用最优移动序列的数学规律:第几步移动哪个盘子,可以从步数二进制末尾连续零的个数得到;每个盘子的移动方向也固定。第二种把递归分解出的待办任务放进栈里,按顺序取出并执行。第三种则模拟函数调用,显式保存每一层调用的参数和程序执行到的位置。
这三种方案把复杂性放在不同地方:数学规律、任务分解或程序执行状态。第一种代码短,但需要理解并验证规律;任务栈更贴近递归本身;模拟调用栈则把操作语义转成状态机。选择哪一种,取决于要解释什么、希望验证什么,以及哪种表示能让人掌握问题。短小或高效的实现不必然是最容易维护、教学或修改的实现。
架构能力也来自跳出当前代码看问题:算法、数据结构、形式语义和他人如何使用系统,都会改变问题的表示方式。只在现有表示中不断补条件,往往会增加分支;换一个合适的模型,复杂性可能变得可控。
五、Excel 二维码需要的是编译器
在 Excel 单元格里输入字符串,并让表格公式生成对应二维码,是目标明确、结果可检查的需求。但二维码的编码算法很容易用普通编程语言表达,把算法硬塞进工作表公式却会生成冗长脆弱的表达式。公式一旦依赖特定 Excel 版本、单元格地址或布局,版本兼容、移动输出位置、添加输入等修改都可能迫使整片公式重写。
从问题外部看,这不是单纯的“多写一些公式”,而是把一种表示翻译成另一种表示。可以在算法和 Excel 之间设计中间表示(IR),用节点表示输入、常量、异或、查表和条件选择,再用边记录数据依赖。AI 可以按这种语言描述二维码计算;同一中间表示既能生成 Python 程序用于检查,也能生成 Excel 公式用于交付。
这个结构把变化分配给不同部分:算法描述编码逻辑,输入绑定处理单元格位置,布局模块决定中间结果放在哪里,目标后端负责生成特定版本的公式。以后更换 Excel 版本时,主要修改对应算子的生成方式,而不必从头改写算法。共享的中间表示也支持常量折叠、重复计算合并等优化。
验证仍要考虑共同错误:Python 和 Excel 若由同一份错误逻辑生成,两个结果一致也不能证明二维码正确。可以再用独立实现或二维码解码器核验内容;比较矩阵时还要固定编码分段、纠错等级和掩码等选项。架构让验证和变化更有位置,并不会让正确性自动成立。
六、只存当前状态,会丢掉决策的来路
传统 CRUD 系统通常维护当前状态:学生、课程、成绩、选课记录,以及由它们计算出的“是否满足毕业条件”。数据库范式能减少冗余并避免更新异常;对简单的信息维护,CRUD 直接、清楚,也常常足够。
困难在于,许多状态本身是计算结论。一个学生“允许毕业”,可能依赖课程成绩、培养方案版本、课程替代审批和人工豁免。只保存布尔值,便无法解释它为什么成立,也无法判断成绩更正后哪些审核需要复核。现实政策已经批准、教室正在维修或组织发生变化时,系统也未必及时记录这些事实,人工补丁便会逐渐形成无法解释的数据。
院系更名展示了历史语义的冲突。若把“计算机系”直接改成“计算机学院”,过去的成绩单可能被显示为学院时期;若新建一个组织 ID,学生、课程、权限和历史记录之间的承继关系又需要迁移。问题不只有名称怎么改,还包括组织身份是否延续、属性何时生效、旧记录按当时结构还是今天结构查询。代码可以重写,已经丢失的历史依据却不能事后可靠地补回。
七、事件溯源保存发生过什么
与只覆盖当前状态不同,事件溯源(Event Sourcing)按顺序保存业务事件,再将事件应用到初始状态,得到某个时点的当前视图。账本记录逐笔收支,余额只是从记录中算出的结果;同样,系统可以保存“选课申请被接受”“课程替代已批准”“成绩已登记”等事实,再分别投影成学生课表、教师名单或毕业审核结果。
保留事件使系统有机会回答过去发生了什么、某个决定依据什么,也能从历史重建当前视图。但只有事件名称还不够。若要解释某次毕业审核,还需保留当时的输入、规则版本、快照和审批依据;修正错误时应追加可追踪的更正,而不是把原始历史悄悄抹掉。业务规则和事件格式也会演化,重放历史时必须避免再次发送通知、扣款等外部副作用。
时间也有不同含义:一项豁免可能在 6 月 1 日生效,却到 6 月 3 日才录入系统。要判断 6 月 2 日它是否有效,和要解释系统在 6 月 2 日为什么拒绝毕业审核,分别需要业务生效时间与系统记录时间。把二者压成一个时间戳,会丢失解释历史决策所需的信息。
事件溯源带来存储、版本演进、重放成本和并发控制等责任。当前视图可通过增量更新或快照提高查询效率;只剩一个名额时,同时到达的两个选课申请仍需原子地竞争,追加事件本身不会自动解决并发冲突。对于不需要追溯和审计的简单数据,CRUD 可能更合适。架构要根据真正需要解释的业务历史来选,而不是为了模式名称采用复杂设计。
八、CQRS 与 DDD 解决的是不同问题
CQRS(命令与查询职责分离)把接受业务操作的写模型,与提供查询结果的读模型分开。写模型检查先修课、名额和时间冲突;学生课表、教师点名册、教务统计可以使用各自适合的读视图。读视图可以是预计算或冗余数据,也可以稍后更新;如果采用异步更新,界面就要清楚说明命令何时算成功,避免用户因为查询结果暂时未刷新而重复提交。
CQRS 可以与事件溯源组合,也能独立于事件溯源使用。它强调写入和查询各自的职责,并不规定必须用事件保存全部历史。DDD(领域驱动设计)则从业务概念和模型边界着手:在选课、财务和毕业审核等不同限界上下文中,同一个“学生”或“通过”可能有不同含义。把这些差异压成一个通用字段,会让系统难以说明某门课通过了、为什么却不能计入特定的毕业要求。
这些方法不是可以随意叠加的流行词。业务边界、查询负载、历史追踪和一致性要求不同,所需的结构也不同。简单场景可以保留直接的 CRUD;复杂场景则可先为关键决定记录规则版本与输入,再逐步建立可解释的投影和复核流程。
为下一次变化留出位置
从命令行解析、汉诺塔到教务系统,架构关心的始终是如何表示问题、隔离变化、保留必要的依据。开始编码前要明确关键业务决策由谁负责、数据归谁所有、哪些关系必须追踪、哪些状态必须立即一致;对还未验证的假设保持可修改,也比过早承诺复杂基础设施更有价值。
改造已有系统时,可以沿着一条重要业务链逐步补足事实、规则版本和审批依据,让新旧结果并行比较,先解释高风险案例,再切换权威写入入口。过程必须尊重真实数据的历史:旧记录能证明什么就记录什么,已经缺失的证据不能伪装成从未丢失。
AI 可以生成更多实现,也能协助整理需求和比较方案;软件能否承受下一次变化,仍取决于人是否看清业务含义、依赖关系和失败边界。代码越容易生成,少数影响深远的抽象和决策就越需要被认真选择。
