AI 辅助编程(二):从提问到项目
AI-Assisted Programming II: From Prompt to Project
L8 教的是"AI 写一个函数,你怎么审"。这一讲是"你要做一个项目,AI 怎么参与"——以及它在哪些地方一定会让你失望。
本讲结束后你应当能
- 把一句话需求拆成模块清单,并用接口先行的方式交给 AI
- 说出 AI 在项目尺度上做得好和做不好的事
- 用 AI 写测试、做评审、写文档,同时守住"人在环内"
- 在团队协作中约定 AI 的使用方式
- 判断什么时候必须自己写
本页目录
这一讲和 L8 的区别
L8 处理的是一个函数的尺度:AI 给你一段代码,你怎么判断它对不对。
这一讲跳到一个项目的尺度。当代码有几百上千行、分成多个模块、由两个人协作时,问题变了:
| L8 的问题 | 本讲的问题 |
|---|---|
| 这段代码对吗 | 这个设计对吗 |
| 这个函数有没有边界 bug | 这些模块的接口合不合理 |
| 怎么验证一个返回值 | 怎么验证一个系统 |
| 我和 AI 谁写这一行 | 我和 AI 谁做哪一层 |
你正在做的团队项目(Lab 14 的简化版 NumPy)就是这个尺度。
从本周起 AI 政策进入 Level 2:完全开放。 但”开放”不等于”交出判断”。
一、从写函数到做项目
先拆,再问
假设需求是一句话:“做一个简化版 NumPy。”
直接把这句话丢给 AI,你会得到几百行代码——读不完,也就验证不了。L8 说过:读不完的代码等于没验证。
正确的第一步是你自己拆:
Array 类
├── 数据存储 ← 内部表示,最重要的决定
├── 形状管理 ← shape / ndim / size / reshape / transpose
├── 索引 ← __getitem__ / __setitem__
├── 逐元素运算 ← __add__ / __sub__ / __mul__ / __truediv__
├── 归约 ← sum / mean / max / min(带 axis)
└── 表示 ← __repr__ / __eq__
模块级函数
├── 构造 ← zeros / ones / arange
└── 矩阵乘 ← dot
这一步是你的工作,不是 AI 的。 因为拆分方式决定了整个程序的结构,而 AI 不知道你的取舍——你打算做几维?性能重要吗?要不要支持广播?
拆完之后,你有了一堆可以独立提问的小任务。
接口先行
拆完之后,不要描述功能,写出签名:
class Array:
def __init__(self, data: list) -> None:
"""从嵌套列表构造。data 必须是规整的(每行等长),否则抛 ValueError。
内部用扁平列表 self._data 加 self._shape 元组存储。
"""
def reshape(self, *shape: int) -> "Array":
"""返回一个新 Array,形状改为 shape,数据顺序按行优先。
元素总数必须匹配,否则抛 ValueError。
不修改原对象。
>>> Array([[1,2],[3,4]]).reshape(4).shape
(4,)
"""
这几行说清了:输入类型、输出类型、错误条件、内部表示、是否原地修改、一个例子。AI 拿到这个,几乎不可能理解错。
对比一下你如果只说”写个 reshape”——它得猜内部表示、猜要不要原地改、猜形状不匹配时怎么办。猜错了你还得重来。
这就是 L4 说类型提示的第三个用途、L8 说”签名是可执行的规格”的兑现。
给它上下文
项目尺度上,AI 需要知道你已经有什么。
一个有效的做法是,每次提问时附上:
- 相关的已有代码(不是全部,是这个任务会用到的部分)
- 你的约定:命名风格、错误处理方式、内部表示
- 已有的测试,让它知道什么算对
这是我们项目里 Array 的构造和内部表示:
class Array:
def __init__(self, data):
self._data = [...] # 扁平列表,行优先
self._shape = (...) # 形状元组
现在请实现 transpose(),要求:
- 返回新对象,不改原来的
- 只需支持一维和二维
- 一维时直接返回自身的副本
- 用我们现有的 _data / _shape 表示
现有测试(应该通过):
assert Array([[1,2,3],[4,5,6]]).transpose().shape == (3, 2)
assert Array([1,2,3]).transpose().shape == (3,)
一次一小步,每步验证
这是 L7 分治思想在协作上的应用:
- 问一个函数
- 读一遍——不理解就问它解释,或者要求换个写法
- 测一遍——用你自己准备的用例
- 通过了再问下一个
二、AI 做得好和做不好的事
做得好
| 任务 | 为什么适合 |
|---|---|
| 样板代码 | 机械、重复、模式固定 |
| 格式转换 | 输入输出都明确 |
| 写测试用例 | 能穷举常见情况(但边界要你补) |
| 写文档字符串 | 代码在眼前,描述它是它擅长的 |
| 解释报错 | 见过海量同类错误 |
| 重构 | “把这段改成用推导式”这类明确指令 |
| 查 API 用法 | 比翻文档快(但要核实) |
做不好
一、性能与复杂度判断。
这是最值得警惕的一类。AI 倾向于给出能跑的方案,而不是能用的方案。
# 一个 AI 很可能给你的 dot 实现
def dot_naive(a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
n, m, p = len(a), len(b), len(b[0])
result = []
for i in range(n):
row = []
for j in range(p):
s = 0.0
for k in range(m):
s += a[i][k] * b[k][j]
row.append(s)
result.append(row)
return result
import time
size = 60
A = [[1.0] * size for _ in range(size)]
B = [[1.0] * size for _ in range(size)]
t = time.time()
dot_naive(A, B)
elapsed = time.time() - t
print(f"{size}×{size} 矩阵相乘耗时 {elapsed:.4f} 秒")
print("这是 O(n³):矩阵边长翻倍,耗时变 8 倍")
print(f"推算 600×600 大约需要 {elapsed * 1000:.1f} 秒")
代码完全正确。但如果你的项目要处理 1000×1000 的矩阵,这个实现是不可用的——而 AI 不会主动告诉你。
它不知道你的 n 有多大。 只有你知道。
import time
# 同一个问题,两种数据结构
def count_dups_list(items: list[int]) -> int:
seen: list[int] = []
count = 0
for x in items:
if x in seen: # O(n) 查找
count += 1
else:
seen.append(x)
return count
def count_dups_set(items: list[int]) -> int:
seen: set[int] = set()
count = 0
for x in items:
if x in seen: # O(1) 查找
count += 1
else:
seen.add(x)
return count
data = list(range(8000)) * 2
t = time.time(); count_dups_list(data); a = time.time() - t
t = time.time(); count_dups_set(data); b = time.time() - t
print(f"用列表: {a:.4f} 秒")
print(f"用集合: {b:.5f} 秒")
print(f"相差 {a / b:.0f} 倍。两段代码结果完全一样。")
二、跨模块的一致性。
你分十次问了十个函数,每次它都给出局部合理的代码。但合起来可能:
- 命名风格不统一(
get_shapevsshapevsgetShape) - 错误处理方式不一致(有的返回
None,有的抛异常) - 重复实现了同一个辅助函数三遍
- 对内部表示的假设不同
AI 每次只看到你给它的那一段上下文。 全局一致性只能靠你。
三、你的项目特有的约定。
“我们项目里所有形状都用元组表示”、“所有错误都抛 ArrayError 的子类”、“内部索引从 0 开始但对外从 1 开始”——这些它不可能知道,除非你每次都说。
四、判断需求本身对不对。
如果你的需求有问题,AI 会忠实地实现一个有问题的东西。它不会说”你确定要这样吗”。
三、用 AI 做质量工作
让它写测试
这是 AI 很擅长的事——但有一条铁律。
为下面这个函数写 pytest 测试:
def reshape(self, *shape: int) -> "Array":
"""..."""
要求覆盖:正常情况、元素数不匹配、一维转二维、二维转一维、
空数组、单元素数组。
它能很快给你一批用例。但是:
让它做代码评审
把 L8 的 PEP 8 检查表交给它:
按 PEP 8 审查这段代码,重点看:
- 命名是否符合 snake_case / PascalCase
- 有没有遮蔽内置名
- 函数是否过长、嵌套是否过深
- 类型提示是否完整
- 有没有可变默认参数
只报告问题,不要重写。
“只报告问题,不要重写” 这一句很重要。否则它会直接给你一个改好的版本,而你失去了逐条判断的机会。
也别忘了工具:ruff 比 AI 更快、更确定、免费,而且不会幻觉。能用工具查的,别用 AI 查。
让它写文档
为这个类写 README 的「用法」一节,包含:
- 三个由简到繁的例子
- 每个例子的输出
- 一段说明内部表示的注意事项
文档是 AI 的强项:代码在眼前,把它翻译成散文是它做得最好的事之一。
但要核实例子的输出——它经常把输出”想”出来而不是算出来。
四、团队协作中的 AI
你们两人一组做项目。加上 AI,实际是三方协作。
先约定,再动手
在写第一行代码之前,两个人应该先定下:
| 事项 | 为什么要先定 |
|---|---|
| 内部表示 | 中途改要推倒重来 |
| 命名风格 | 事后统一改名很痛苦 |
| 错误处理约定 | 抛异常还是返回 None,必须一致 |
| 代码格式化工具 | black + ruff,配置进项目 |
| 测试怎么组织 | 文件放哪、怎么运行 |
把这些写进 README 的开头。每次向 AI 提问时把这段贴上去,一致性问题就少一大半。
谁对 AI 生成的代码负责
提交它的人。 不是写提示词的人,不是 AI。
这意味着:如果你把 AI 给的代码合并进项目,你就要能解释它。批改时问到那一段、期末考到那个概念,“那是 AI 写的”不是答案。
依赖与许可证
AI 有时会建议引入某个第三方库。引入之前问三件事:
- 真的需要吗? 标准库能不能做?团队项目还明确禁止用 NumPy 实现
- 它的许可证是什么? 有些许可证对使用方式有要求
- 它维护得怎么样? 上次更新是三年前的库,风险很高
五、什么时候必须自己写
不是出于诚信要求,而是出于效果——这些场合 AI 帮不上,或者帮了反而更慢:
一、你还不理解的东西。
如果你不懂矩阵乘法在做什么,AI 给你的 dot 你也审不了。先弄懂,再让它写。 否则你只是在积累自己无法维护的代码。
二、需要在多个方案里做取舍的地方。
“用嵌套列表还是扁平列表加步长”——这取决于你更在乎什么。AI 能列出两种方案的优缺点(这很有用),但选哪个是你的判断。
三、调试。
AI 看不到你的运行时状态。给它一段代码和一个报错,它会猜;而你有调试器(L8),能看到每个变量的实际值。
用调试器定位问题,用 AI 理解问题。 顺序不能反。
四、最后一公里的正确性。
代码 90% 对的时候,剩下 10% 往往是你的具体场景才有的边界。这部分 AI 帮不上,因为它不知道你的场景。
六、这门课结束之后
工具会继续变。你在这门课学的具体语法,五年后可能有一半过时了。
但有些东西不会:
拆解问题的能力。 把一个模糊的需求变成一组清晰的小任务——这是 L7 分治思想的日常版本,也是使用任何工具的前提。
判断对错的能力。 知道该测什么、边界在哪、什么情况下会崩。AI 越强,这个能力越值钱,因为它能生成的东西越多,你需要判断的就越多。
估算代价的能力。 看到嵌套循环就想到 n²,看到 in 就问是列表还是集合。这是 L11 教的,它和语言无关。
说清楚一件事的能力。 无论是写文档、写提示词、还是跟队友解释——能把一件事说明白的人,用什么工具都比别人快。
小结
- 项目尺度的问题不是”这段代码对吗”,而是”这个设计对吗”
- 先自己拆,拆到一个任务你能一口气读完为止——拆分是你的工作
- 接口先行:写出签名、类型、错误条件、例子,再让 AI 填实现
- 每次提问附上相关代码、项目约定、已有测试
- 一次一小步,每步验证。要一大坨然后”看起来能跑”是最危险的模式
- AI 做得好:样板、格式转换、写测试、写文档、解释报错、重构
- AI 做不好:性能与复杂度判断、跨模块一致性、你的项目约定、判断需求本身
- 提问时说清数据规模,命中率会明显提高
- AI 生成的测试不能验证 AI 生成的代码——边界情况必须你先列
- 让它评审时说”只报告问题,不要重写”;能用
ruff查的别问 AI - 团队先约定内部表示、命名、错误处理,再动手
- 谁提交,谁负责。“那是 AI 写的”不是答案
- 必须自己写的:你不懂的、需要取舍的、调试、最后一公里的边界
练习
- 拿你团队项目里还没实现的一个方法,写出完整的接口规格(签名 + 文档字符串 + 三个例子含边界),不写实现。把这份规格交给 AI,评价它给的实现。
- 让 AI 实现
dot(),然后测量它在 100×100 和 200×200 时的耗时。验证是不是 O(n³)。如果是,问它”如何优化”,评价它的建议。 - 先自己列出
reshape()的至少 8 个边界情况,再让 AI 生成测试用例,对比它漏了哪些。 - 把你和队友各自写的一个模块交给 AI,让它只报告不一致的地方(命名、错误处理、风格)。评价它找出的问题里有几条是真的。
- 找一段你队友写的代码,问 TA 一个问题:“这一行为什么这么写?“记录这次对话——如果对方答不上来,你们发现了什么?
- 写一份你们团队的 AI 使用约定,不超过半页,至少包含:什么可以问、提交前必须做什么、谁负责。