导 言
在刚过去的春季学期,我们担任了彭鑫老师主讲的《软件工程(H)》课程助教,在彭老师的指导下全程参与了课程项目设计和验收。本文从助教的角度对AI时代软件工程课程教学特别是课程实践的开展方式进行了观察和思考,供大家一起探讨和交流。

背 景
过去,软件工程课程最直接的问题往往是:学生能不能把需求实现为代码,能不能完成一个能够真正运行的软件系统。
但这一学期,我们越来越强烈地感受到,这个问题正在发生变化。当 Claude Code、Cursor、Copilot、Codex等工具进入学生的日常开发流程之后,“把代码写出来”已经越来越不是问题了。学生可以在很短时间内生成 CRUD、DTO、接口骨架、页面、配置和文档,也可以让 AI 根据日志修复错误、根据需求补充功能。
于是,一个非常有挑战性的问题自然浮现出来了:当代码实现变得如此容易、软件开发似乎就是跟AI对话,软件工程课程究竟应该教什么?进一步,学生是否还需要软件工程能力的训练?带着这个问题,我们在彭老师的指导下一边摸索一遍开展本学期软件工程课程项目的设计和验收工作。
与以往一样,课程项目要求以迭代化的方式开发一个完整软件。本学期的开发任务是一个网盘系统,按照Lab1到Lab4以迭代化的方式,从基础的文件上传、下载和目录管理功能,逐步增加团队空间、私密空间、权限、配额、回收站和分享等功能;同时,在开发要求上,从高质量编码、单元和接口测试、版本管理和协作开发开始,逐渐增加软件设计、需求分析、系统测试以及持续集成要求。但与过去不同的是,AI工具已经进化到了一个新的高度,几乎所有同学都会使用AI完成课程作业,甚至很多同学重度依赖AI并相信AI的强大能力。于是乎,在课程允许四人组队的情况下,有同学自愿一人成组独立完成课程项目,还有的组组长一人信心满满自愿包揽全部工作。因此,本学期课程项目设计的一个主要挑战就是如何在AI编码能力如此之强的当下突出软件工程的重要性同时强化软件工程能力体验。正如彭老师所说,如果同学们到学期末都可以用AI轻松搞定课程项目,那么他们不会对软件工程有什么理解,甚至会质疑软件工程存在的意义。
为此,我们一方面加大了项目的开发难度特别是增量开发过程中的需求和设计纠缠,另一方面强化了对于实现质量和软件理解的要求。本文的分享来自我们对这一学期课程项目的观察,其中既包括同学们在Lab1 到 Lab4 持续迭代过程中的真实经历,也包括我们对13个小组、总计约 20万行代码所做的系统性评审。我们想讨论的不是“学生应不应该使用 AI”,而是当 AI 已经成为默认队友之后,工程师怎样才能继续掌控需求、架构、边界和复杂度。

AI Coding已经成为默认工作流
从本学期软件工程课程项目的布置到最终验收,最明显的感受是:AI coding 已经不再是学生开发过程中的辅助选项,而是几乎默认进入了他们所有的编程和软件开发任务中。
在 Lab1 到 Lab4 的迭代过程中,大部分小组都会使用 Coding Agent 完成需求拆解、代码生成、接口联调、测试用例生成和文档整理。尤其在前期开发中,AI对通用功能代码、CRUD接口、前后端页面骨架、测试框架和文档初稿的生成确实显著提高了效率。有的小组甚至会完全依赖AI,形成所谓的“Human Out of The Scope”环路,即:首先把开发要求转成 Markdown文档,再让 AI 根据要求生成 checklist、实现代码、读取日志、自查功能覆盖情况,从而形成一种“需求文档 → AI 实现 → AI 自查 → 人工验证 → 再交给 AI 修复”的循环。
但是,随着Lab的开发复杂度不断上升,尤其到了 Lab3,系统开始涉及团队空间、个人空间、私密空间、权限、配额、回收站、分享、VIP 规则、前后端联调等多种交叉逻辑之后,AI的短板也逐渐显现出来,那就是擅长完成局部明确的任务,但很难真正承担系统级的软件工程判断。
很多学生一开始会觉得“功能能跑起来”就说明AI已经完成了主要工作,但在验收和联调时才发现,单个接口正确并不代表整体流程正确,单个页面能展示并不代表业务语义一致,生成了大量代码也不代表系统仍然可维护。例如,有小组总结到,团队文件复制时后端接口已经预留了 teamId,但前端调用时没有传递,导致目标目录回退到个人空间,用户感知上就变成“团队文件只能复制到个人空间”。这类问题不是语法错误,而是跨模块、跨空间上下文没有被统一检查的问题。

验收中暴露出的五个问题
1.部分学生被 AI 带着走,而不是主动驾驭 AI(Lint 与 unsafe 用量)
学生能够通过 AI 快速生成大量代码,但未必理解这些代码为什么这样写、是否符合原始需求、是否保持了系统已有结构。
一个典型现象是,学生在修改和答辩时会变成“传话者”:Reviewer指出问题后,学生把意见转述给 AI,AI 再修改代码;如果再次不通过,学生继续搬运反馈。这样一来,真正做决策的似乎不是学生,而是 AI;学生反而失去了对代码的掌控权。
有的小组把这种现象概括为:
Who is the real agent?
当人在 AI 与 Review 之间机械搬运意见,却无法独立解释和修正代码时,AI 并没有增强学生的软件工程能力,而是在替代学生做决定。
2.AI生成的代码往往局部可用,但全局设计能力不足
在简单场景下,AI 能够快速生成三层架构、权限枚举、接口封装、表单校验等内容;但一旦涉及长期扩展和需求演进,问题就会暴露出来。
例如,有小组在早期假设“一个用户一套存储空间”,到了Lab3需要支持个人、团队、私密三种空间时,原有设计压力陡增,只能通过扩展数据库字段来补救,最后反思前期应当更重视扩展性评估。
还有的小组指出,AI 可能会生成看似合理的 RBAC 权限枚举,但把角色和权限硬编码在代码里,导致后续新增角色必须修改源码,失去了运行时动态扩展能力。
3.AI 不会主动质疑错误前提
如果前端同学在 Prompt 中假设“后端已经实现某个接口”,AI 通常不会反问这个接口是否真实存在,而是会顺着这个假设继续生成代码。它甚至可能绕过后端,直接读取浏览器缓存来模拟效果,然后告诉学生功能已经完成。
这个问题在实际开发中非常危险,因为它会把错误假设包装成“已完成的功能”。只有人工读代码、真实联调和验收时,才可能发现系统从来没有真正实现那条业务链路。
4.AI可以生成测试,但不能保证测试真正有效
很多学生会让 AI 根据业务代码生成单元测试、接口测试或 checklist,这在形式上提高了覆盖范围,但并不意味着验证一定可靠。
AI 擅长列举格式错误、分享过期、提取码错误、同名目录、配额不足等边界情况,但对于权限可见性、跨空间状态变化、真实数据操作后的副作用,仍然必须由学生自己运行系统、登录前端、逐项验证。
有小组也明确反思,自动化工具能发现重复代码、命名不规范、缺少 null 检查等问题,但很难判断业务逻辑是否正确、设计是否合理、是否符合课程验收要求。
5.AI放大了团队协作中的接口不一致和设计不统一
在多人小组中,如果每个成员都让 AI 独立生成自己负责的模块,就很容易出现命名、接口参数、返回格式、错误码、权限边界、数据库变更方式不一致的问题。
有前端同学在Lab3的反思中提到,团队空间、私密空间、个人空间三套入口在界面上看起来相似,但背后的权限、配额、回收站、分享规则并不完全一样。如果前端只是照着接口堆页面,很容易把不同空间的语义混在一起,直到验收时才发现“看起来能用,但逻辑不一致”。

代码审查结果
为了判断验收中发现的问题究竟是少数个案,还是 AI 辅助开发中更普遍的现象,我们设计了一套可量化的代码质量审查skill:先用静态工具统计代码规模、复杂度和重复,再由多个 agent 按清单分片检查代码,并通过文件摘要完成跨模块分析和覆盖校验。
主要分析代码中的结构清晰度、代码重复度、耦合、命名与领域建模、变更边界等问题。我们在 13 个项目中记录了 437 条有效发现,其中高严重度 97 条、中严重度 303 条、低严重度 37 条。各组代码综合分平均为 71.2 分。
最值得关注的是各维度之间的差异:

我们发现,越是可以在单个文件内识别和修正的问题,整体表现越好;越需要跨文件、跨模块、跨迭代判断的问题,AI 整体表现越差。
高频问题也呈现出相同趋势:职责过多的长函数出现 75 处,参数过多出现 56 处,基本类型偏执出现 45 处,普通代码块重复出现 44 处,重复的条件分发出现 31 处,大类或“上帝对象”出现 30 处,数据泥团出现 29 处。
这些项目往往并不缺少分层目录、DTO、Service、Mapper、统一响应对象或框架配置。真正的问题是:代码可以继续生成,功能可以继续接入,但系统的结构、边界和领域模型没有以同样速度演化。

代码审查中得到的五个洞察
洞察1:长函数、大类和参数过多非常普遍——AI让代码增长快于结构演化
结构是这批项目中平均得分最低的维度。这里的问题不是项目“没有结构”,而是代码量和功能持续增加时,原有的类、函数和模块划分没有随之调整:已有 Service 继续增加方法,已有函数继续增加分支,已有接口继续增加参数。越来越多的业务被塞进同一套旧结构。
例如,一个最初只负责上传的 FileService,经过几个迭代后可能同时负责下载、复制、移动、回收站、配额、分享失效和对象存储。上传方法也逐渐变成:
upload(userId,teamId,spaceType, parentId, fileName, content, conflictPolicy)方法内部依次完成权限校验、配额检查、重名处理、对象上传、数据库写入和审计记录。每一步都合理,但这些职责不应该由一个方法和一个类长期共同承担。
这类问题不容易通过“再生成一次”解决,因为根因不是单段代码写得不好,而是代码增长速度超过了结构演化速度。AI 擅长回答“怎样把这次需求接上去”,却很少主动提出“承载需求的结构是否已经需要改变”。如果人只验收本次功能是否完成,局部正确的代码就会不断叠加成全局难以维护的系统。
洞察2:重复性问题特别严重——AI 擅长复制成功模式,却不会主动建立唯一规则来源
重复的条件判断和成组出现的平行实现存在于全部 13 个项目,普通代码块重复也出现在 11 个项目中。这里的重复不只是多写了几行代码,更严重的是同一业务规则在不同模块中被重新解释。
一个典型场景是:
uploadPersonalFile → 检查配额 → 处理重名 → 上传对象 → 写数据库uploadTeamFile → 检查配额 → 处理重名 → 上传对象 → 写数据库uploadPrivateFile → 检查配额 → 处理重名 → 上传对象 → 写数据库
三套代码最初看起来只是必要的业务差异,随后却可能出现一处支持 rename,一处支持 overwrite,另一处仍然只会报错。
AI 会把已经成功工作的代码当成最可靠的上下文样例,因此“再生成一套相似实现”通常比“识别共同规则并重构现有实现”更容易。这使 AI 的局部一致性恰好制造了全局重复。高层来看,重复代码真正危险的地方,是系统失去了唯一事实来源:同一规则有三份实现,就可能产生三种行为。
洞察3:基本类型偏执和数据泥团非常普遍——AI容易把领域建模退化为字段传递
基本类型偏执,是指本应具有业务含义和约束的概念被表示为普通 String、数字或布尔值;数据泥团,则是userId、teamId、spaceType、parentId等一组字段总是在多个方法和 DTO 中一起出现,却没有形成一个明确的领域对象。
例如:
moveFile(userId, teamId, spaceType, fileId, targetParentId, role, conflictPolicy)
这个签名没有表达 spaceType与 teamId的约束,也没有保证role、conflictPolicy是合法状态。更清晰的方向可能是:
record MoveFileCommand(Actor actor,SpaceContext source,FileId file,DirectoryId destination,ConflictPolicy policy) {}
这使非法状态更难被表达。更高层的问题是,AI 常常把系统理解成一组需要接通的输入、输出和字段,而软件工程需要进一步识别字段背后的概念、约束和不变量。字段传对了,只能说明数据管道接通了,不代表领域模型是正确的。
洞察4:AI提高修改速度,也可能加速架构熵增
发散式变化和霰弹式修改的平均评级都只有 2.85/5。AI 可以迅速完成一次跨 Controller、Service、Mapper、DTO 和数据库脚本的修改,但“迅速完成”不等于“变化边界合理””。
例如,“文件进入回收站后,所有公开分享立即失效”可能被分别补进文件服务、回收站服务、分享服务、数据库查询和定时清理任务。当前需求很快完成了,但“文件生命周期变化”仍然没有唯一所有者。下一次加入批量删除或自动过期时,还要重新寻找全部修改点。
如果系统存在明确的领域事件或生命周期策略,例如 FileMovedToRecycleBin和统一的 ShareInvalidationPolicy,变化就更容易被收束。AI 能够快速执行跨文件修改,但是否应该跨这么多文件,必须由人先判断。
洞察5:为了快速完成任务,AI会选择绕过模块边界的捷径
跨模块依赖问题出现在11个项目中。常见模式是,分享服务为了完成当前接口,直接访问文件、团队和成员模块的数据访问层,并在分享模块中重新实现文件可见性和团队权限判断。
直接访问内部数据最容易让当前接口工作,却使分享模块开始了解其他模块的表结构并复制其规则。以后文件可见性或团队权限变化时,原模块已经更新,分享模块仍可能保留旧语义。
更稳妥的方式是通过 FileAccessService、TeamAuthorizationService等公开能力维护不变量。模块边界的价值不是让目录看起来整齐,而是规定谁有权解释某条业务规则。AI 选择捷径时减少的是当前任务的实现成本,增加的却是未来规则变更时的不一致风险。

从Lab1到Lab4:需求变化如何暴露设计问题
也正因为这些问题,本学期的项目布置方式对学生产生了比较明显的触动。
课程项目不是一次性的小作业,而是从 Lab1 到 Lab4不断叠加需求、引入变更、要求扩展已有系统。这样的设计让学生逐渐意识到:软件工程不是把某个功能写出来,而是在不断变化的需求下,让系统仍然保持清晰、可靠、可维护、可协作。
有的小组在总结中提到,Lab3 中需求变更带来了大量返工,例如回收站规则调整、分享文件被删除后分享链接失效等问题,都要求他们回溯修改之前的代码并补充验证。这让学生真切感受到,前期需求理解不清、接口契约不稳定、设计文档缺失,都会在后期变成真实的开发成本。
从最终验收来看,不少同学对软件工程的认识发生了变化。最开始,很多人会把AI当成“代码生成器”,认为只要 Prompt 写得好,就能快速完成项目;但经过多轮 Lab、联调、Review 和验收后,他们逐渐意识到,AI 更适合作为执行加速器,而不是架构决策者。
比较成熟的小组最后形成的共识是:人先定义需求、边界、接口和规则,AI 再补充骨架、重复代码、检查清单和文档;AI 可以参与 Review,但最终判断必须回到人工实测和业务语义本身。
因此,作为助教,对这一学期的整体感受可以概括为:AI coding 确实降低了代码实现门槛,但没有降低软件工程能力的门槛,反而把学生在需求理解、架构设计、验证、团队协作和代码掌控方面的短板暴露得更明显。
过去,我们可能通过“能不能写出来”判断学生是否掌握课程内容。但在 AI coding 普及之后,更重要的问题变成了:学生是否知道自己写了什么,是否能解释为什么这样设计,是否能发现 AI 生成代码中的隐患,是否能在需求变化时维护系统,是否能证明功能真的正确。

结语
AI coding 并没有让软件工程教育失去意义。恰恰相反,当实现代码变得更便宜,需求理解、架构判断、系统一致性、长期维护和团队协作就变得更加重要。
AI 可以生成代码,但不能替学生承担工程责任;AI 可以给出方案,但不能替学生做长期维护成本、权限安全、系统一致性和团队协作的取舍。
课程项目真正带给学生的,不应该只是一次前后端系统开发经历,而是让他们意识到:软件工程的核心价值不在于机械地产生代码,而在于通过需求分析、架构设计、规范实现、真实验证和团队协作,把一个复杂系统持续控制在人的理解和掌控之中。
当 AI 让代码生产变得越来越容易,软件工程教育更需要训练学生定义问题、建立边界、验证结果并承担取舍。会使用 AI 只是起点,能够让 AI 参与构建的系统始终保持可理解、可维护、可演进,才是新的工程能力。

本文作者

袁子诚
复旦大学
袁子诚,复旦大学计算与智能创新学院硕士生,导师为彭鑫教授。研究方向为云原生软件智能运维、AgentOps 等。

李展发
复旦大学
李展发,复旦大学计算与智能创新学院硕士生,导师为彭鑫教授。研究方向为云原生与智能体软件系统,包括云原生智能运维、智能体软件架构、AgentOps 等。
指导教师

彭鑫
复旦大学
复旦大学计算与智能创新学院副院长、教授,国家级高层次人才计划入选者。中国计算机学会(CCF)杰出会员、软件工程专委会副主任、开源发展委员会常务委员,中国汽车工程学会汽车基础软件分会副主任,《Journal of Software: Evolution and Process》联合主编(Co-Editor),《ACM Transactions on Software Engineering and Methodology》、《Empirical Software Engineering》、《Automated Software Engineering》、《软件学报》等期刊编委。主要研究方向包括软件智能化开发与测试、智能化软件系统构造与运行支撑、具身智能与自主无人系统软件等。研究工作多次获得《IEEE Transactions on Software Engineering》年度最佳论文奖、ICSM最佳论文奖、ACM SIGSOFT杰出论文奖、IEEE TCSE杰出论文奖等国际期刊及会议优秀论文奖。担任2022年与2023年CCF中国软件大会(CCF ChinaSoft)组织委员会主席与程序委员会共同主席,以及ICSE、FSE、ASE、ISSTA、ICSME、SANER等会议程序委员会委员。

夜雨聆风