SoftWare Engineering in AI
SoftWare Engineering in AI
Serves as the lecture notes for SJTU-CS3604.
Introductions
软件工程是在现实生活中各类软件的构筑基石,包含上千个 feature,上百万行代码的大型软件的规划设计、开发、测试、部署、运维等需要成百上千人共同维护努力,是一项相当复杂的系统性工程。
随着大语言模型的迅速发展,基座模型的编码能力持续增长,在单个简单任务的场景下,AI 编码的速度、准确率、代码整洁度等方面显著超越人类水平,但是仍然在面对庞大 codebase、复杂需求,需要长程记忆和任务理解的复杂软件工程项目上无法胜任。
因此,在未来,程序员的使用不再是手动编写代码,而是拆解需求和技术框架,明确到每一个原子需求,并且保持 Human in the loop,保持对代码的理解和 Debug 能力。
It is the era of AI-driven programming.
在 AI 时代,软件工程会出现下面的变化趋势:
- 从编码为主导的开发导向到以需求为导向的开发导向
- 需求工程将会从人类可读的文档形态转向以上下文工程为主的形态。
软件工程的基本阶段

需求工程
产品经理:将从用户为导向的需求(我需要实现 XXX 功能)转向为技术人员为导向的需求。
具体而言,这个过程可以被分解为:
- 需求的获取 & 整理
- 需求的建模 & 转换
- 需求的撰写 & 验证 & 确认
- 需求的管理 & 变更
同时,需求本身存在层级性,可以生成一颗非常庞大的需求树。
- 需求的顶层主要是面向用户的接口
- 需求的底层是面向 AI 编程的接口设计问题
- 中间的需求设计需求的拆解和合并
软件设计
软件设计将需求转化为系统实现方案,决定了软件模块、接口、数据及其相互关系。
We need to design an interface:
- What I should provide
- How you will call the interface
- What the result you will ensure
For the intermediate steps, the code is programmed by AI!
软件测试
- 黑盒测试:只验证输入输出是否达到了预期
- 白盒测试:审查程序的具体接口和逻辑检查
从需求到测试:
- 业务需求(面向客户)
- 系统需求(用户端需求)
- 高层设计需求(比如不同模块的接口的设计)
- 底层的接口内部函数设计和编码
这四个不同层级的设计也有对应的测试实现!
- 底层设计:单元测试
- 高层设计:模块测试
- 系统需求:集成测试
- 业务需求:验收测试
Test-Driven Development
根据需求和设计出的接口实现测试脚本(黑盒测试)
软件维护
- 版本管理系统
- 去中心化,本地化,面向多人协作
- 问题追踪系统
- Issue Tracking Systems 问题追踪系统
- One single issue focus on one atomic tasks!
- 保证需求文档和软件代码能够实时同步,并且方便回滚回退,方便多人协作
- 持续集成系统(CICD)
- 和问题追踪系统高度耦合
- 降低风险,保证主干可以实时发布
以上三个自动化系统可以保证人类程序员在面对超长的 code-base 和 memory 的情况下可以提升效率,对 AI 来说,也同样如此。
软件工程的版本管理
I am quite familiar with Git and version control, thus we just skip that part!
需求测试和编码
测试驱动的开发和实践 Test Driven Development
在大语言模型,testcase 的设计天生为语言模型定义了良好的奖励机制,使语言模型的能力迅速上升。
在面向测试的 TDD 开发流程中,根据需求和场景确定设计接口和生成鲁棒准确全面的测试样例是开发的核心流程。(尤其是在 AI 时代)
软件需求测试
- 功能性需求:实现什么功能,验证可实现性
- 方便验证,集成
- 非功能性需求:例如兼容性需求,速度需求,性能需求,成本需求等
- 更复杂,更具体
需求场景
- Context 代表系统的初始状态和上下文
- Action 代表用户或系统执行的触发动作
- Then 代表系统应该产生的行为和输出
需求场景描述语言
Gherkin 是一种结构化的自然语言(Structured Natural Language),用于行为驱动开发(BDD)中描述软件系统的行为。它让需求文档既能被人阅读,又能被测试工具 Behave 执行。
1 | |
变体需求场景(Variant Scenario)是指:在一个功能需求下,围绕同一主
目标(Main Scenario),针对不同输入条件、异常情况或交互路径而产生的
分支场景(Alternative Scenarios)或异常场景(Exception Scenarios)。
变体需求场景有点类似于 Rust 中的 Happy Path
软件接口
接口的精髓在于形成通信或者数据交换的契约,即尽可能精炼的暴露有用的信息,而不暴露一些内部的细节信息,这样保证不同模块之间的高度复用和低耦合,提升开发效率。
- User Interface
- Application Programming Interface
UI 接口
主要提供 GUI 操作的相关测试。
- 设计目标
- 约束属性
完全从用户的角度出发,不暴露技术细节!
API 接口
RESTAPI 调用:一套标准化的 API 接口实现
- URL
- 请求参数(一个字典)
- 返回 JSON 格式
- 得到响应示例(一个 JSON 格式)
- 存在调用约束
对于一个需求场景,可以把它转换为一个接口!
- When 的触发条件即提供了 API 接口的输入和调用条件
- Then 代表 API 接口的执行结果
- Given 的验证被封装在 API 接口的内部验证
Example
1 | |
The source code for this example above has been open sourced into This Repo.
软件测试
软件测试从信息层面上是一种交叉验证的思想。
- 代码的实现源自于需求
- 测试的实现也源自于需求!
- 测试本身就可以看作是一种对真实需求的虚拟采样和交叉验证
但是采样本身为了便携性牺牲了安全性和质量,采样就意味着总有边边角角无法被覆盖到。因此,测试主要被分为:
- 黑盒测试:测试接口,不在于内部的实现细节,实现简单并且迅速
- 白盒测试:进行透明测试检查,但是比较复杂并且需要额外的管理,但是可以提升安全性。(Code Review)
V 模型
根据不同层级的需求,对应不同的测试细粒度:
- 底层设计:执行单元测试,测试最小单元的基本实现情况
- 高层设计:分成具体的若干模块,测试模块的功能(类),执行模块测试
- 系统需求:分解成细粒度的子需求(系统需求),执行集成测试
- 业务需求:完成从用户角度出发,模拟用户行为进行测试(验收测试)
单元测试 Unit Test
对软件系统中的最小可测试组件(通常是一个函数或方法)进行验证的一种测试方法。其目的是独立验证该组件的逻辑是否正确工作,并且在没有依赖其他部分的情况下满足设计和功能要求。
模块测试
聚焦于测试对个单元之间的组合和链接,例如测试多个相互依赖的测试和类。
集成测试
验证多个模块或系统组件组合在一起后是否能够正确协同工作。它通常在模块测试之后进行,通过测试模块之间的接口和交互,确保不同模块整合后在功能和性能上符合系统设计和需求规格。
验收测试
直接和用户进行交互,从用户的角度出发。
测试环境
- 受控环境测试:alpha Testing
- 不受控环境测试:beta Testing
- 测试软件面对生成环境的鲁棒性
- 测试异常处理和兼容性能力
白盒测试
- 程序证明:从理论层面进行白盒分析和测试
- 耗费时间长,但能 100% 保证安全性
- 黑盒测试:测试接口
- 耗费时间短,但是可能会漏掉 corner case
核心:覆盖
-
语句覆盖:选择足够多的测试数据,保证被测试程序的每个语句都被测试过
- 本质就是从底层的视角查看 corner case
- 只关心执行达到,但是不关心分支的执行情况
-
分支覆盖:不仅每个语句必须至少执行一次,而且每个判定的每种可能的结果都应该至少执行一次,也就是每个判定的每个分支都至少执行一次。
将代码本质上建模成一颗树或者流程图的情况。
-
条件覆盖:不仅每个语句至少执行一次,而且使判定表达式中的每个条件都取到各种可能的结果。(对分支覆盖的更细致的检查)
-
路径覆盖:检查每一个路径覆盖(含多个条件触发状态),保证状态上的鲁棒性。
- 建模成对代码流程图的遍历问题
循环测试
测试循环结构的有效性。
-
简单循环:测试最简单的循环功能,假设允许通过循环的最大次数是 次,则涉及的测试样例应该覆盖:
- 跳过循环
- 通过 1/2/m/n-1/n/n+1 次 ()
-
嵌套循环
- 更复杂的循环结构
- 从最内层循环开始测试,把所有其他循环都设置为最小值。
- 对最内层循环使用简单循环测试方法,而使外层循环的迭代参数(例如,循环计数器)取最小值,并为越界值或非法值增加一些额外的测试。
- 由内向外,对下一个循环进行测试,但保持所有其他外层循环为最小值,其他嵌套循环为“典型”值。
-
串接循环
- 独立的串接循环
- 依赖的串接循环:
- 核心思想:每次只测试一个简单循环,测试多次
- 使用测试嵌套循环的方法来测试串接循环
在真实软件开发中,循环出现的 bug 往往出现在一些特殊的 corner case,因此在循环测试中也会额外测试一些边界条件,提升软件测试的鲁棒性。
黑盒测试
白盒测试存在的问题:重视细节而非整体
- 本身测试复杂
- 不考虑性能问题,不考虑功能缺失问题
- 重视逻辑覆盖而不是场景覆盖
而黑盒测试依赖测试集的设计,从整体和用户的角度测试程序的功能正确性,完整性和性能等多方面数据。
如何保证测试用例尽可能覆盖到全面的样例?等价类划分 & 边界值界定
为了测试把数字串转变成整数的程序,除了用等价划分法设计出的测试方案外,还应该用边界值分析法再补充下述测试方案。
- 使输出刚好等于最小的负整数 ‘-32768’ -32768
- 使输出刚好等于最大的正整数 ‘32767’ 32767
- 使输出刚刚小于最小的负整数 ‘-32769’ 错误——无效输入
- 使输出刚刚大于最大的正整数 ‘32768’ 错误——无效输入
科研拓展:自动化的白盒测试
We hope LLM can be acted as a tester for automatically doing Black-Box and White-Box Testing.
符号执行
We want an algorithms to automatically let certain inputs for certain branches while doing white-box testing.
关键分支的约束条件和测试的输入。可以转换为逻辑表达式,进而转化为约束求解问题,可以计算出最终的可行解。
但是存在的问题是:
- SMT 解析算法时间复杂度高
- 但是复杂的边界判断逻辑会导致生成的约束条件迅速膨胀,难以求解
- 真实世界代码的复杂(底层代码)
基于搜索的软件测试
约束求解问题本身也可以看成是一个优化问题。
定义损失函数,每次 rollout 计算当前输入计算执行的分支和目标分支的距离,并在后续进行优化。(定义不同分支上的距离)
- It is the Guiding Optimization!
- 搜索空间本身就是不连续的,因此很难施加优化方法,很容易退化成随机测试。