Python Vibe Coding 实操:两个考题的完整交付记录
本文是《Python Vibe Coding 实操》考核的实作说明。 考核核心不在"会不会手写代码",而在能否把需求讲清楚、把任务拆得开、把过程管得住、把结果验得出。
- 考题一 · Excel 报表生成脚本 → 仓库:ArenBase/excel-report
- 考题二 · FastAPI 记账接口 → 仓库:ArenBase/fastapi-ledger
每个仓库内含完整源码、测试、交付文档与调研报告。本文只做导读与要点说明,细节请点进仓库。
一、考核要求回顾
命题方给出的五维评分标准是「结果 + 过程 + 验证 + 风险 + 复盘」。我把这五个词翻译成了可执行的动作:
| 维度 | 我的落地方式 |
|---|---|
| 结果 | 能一键跑通,且数值经得起人工手算核对 |
| 过程 | 交给 AI 之前先书面澄清需求;拆成小步;每步立即运行验证;保留对话与 diff |
| 验证 | 断言具体数值而非"没抛异常";边界/异常用例覆盖远超最低要求 |
| 风险 | 列出风险清单,并把其中关键的几条升级为自动化断言 |
| 复盘 | 写清哪些交 AI、哪些自己拍板、哪些人工审查 |
约束条件:Python ≥ 3.10、可用主流第三方库、须能一键运行、必须完整保留 AI 对话记录与代码修改轨迹。
二、考题一:Excel 报表生成脚本
仓库 → excel-report
问题
某团队每月有一份 sales_raw.xlsx(字段:日期、销售员、产品、数量、单价、地区),里面有空值、重复行、负数量等脏数据。要写脚本清洗并统计,输出 report.xlsx,含「明细清洗后 / 按销售员 / 按地区 / 总览」四个工作表。
成果
一键生成报表。下面是实测输出(样例数据 143 行):

[OK] 读取: data/raw/sales_raw.xlsx (原始 143 行)
[OK] 清洗后: 128 行 (剔除 15 行)
- 关键字段缺失 : 4 行
- 数量<=0 : 3 行
- 全字段重复 : 5 行
- 非法数值/日期: 3 行
[OK] 总销售额: 622,141.20
[OK] 月度区间: 2023-12 ~ 2024-06 (共 7 个月)
[OK] 报表已生成: output/report.xlsx (4 个工作表)
剔除对账:4 + 3 + 5 + 3 = 15 = 143 − 128 ✅ 金额对账:7 个月度合计 = 622,141.20 = 总销售额 ✅
三个值得说的设计决策
1. 空白字符串也算"缺失",不能只用 dropna()
最直观的写法是 df.dropna(subset=关键字段)。但 pandas 的 isna() 不认为 " " 是缺失值。Excel 里"看起来是空的"单元格常常是纯空格串(系统导出很常见),只靠 dropna() 会漏掉,报表里就会出现一个销售员名为空的诡异分组,而且剔除行数与人工核对不上。
所以我补了一层显式判定,并为它单独写了用例。
2. 用"守恒不变量"代替逐条断言
我没有只断言"剔除数量 = 5",而是加了一条更强的检查:
原始行数 == 清洗后行数 + 缺失 + 数量≤0 + 重复 + 异常数值/日期
这条不变量能**一次性抓住"某行被静默吞掉"**这类最危险的 bug——包括我没想到的场景。实测在 4 组随机数据集上均成立。
3. Excel 序列日期的 1900 闰年缺陷
被导出成数字的日期(如 45292)需要按 Excel 序列号解析。我最初用的公式对现代日期正确,但对 1900 年 1–2 月会差一天——因为 Excel 错误地把 1900 当作闰年(存在不存在的 1900-02-29,序列号 60),导致 <60 与 ≥60 需要相差一天的两个纪元偏移。
发现方式很关键:我把测试从"4 个现代日期采样点"扩到"与 openpyxl 官方实现在 1~73415 全区间逐点比对",才暴露出来。修正后全区间 0 处不一致。
教训:采样点测试很容易"恰好"避开缺陷区间。凡"转换/映射"类函数,优先做全区间或性质测试。
验证
| 项目 | 结果 |
|---|---|
| 测试 | 109 passed |
| 原始文件只读 | SHA256 + mtime 运行前后均不变 |
| 输出幂等 | 逐字节一致(SHA256 D5691244…) |
| 三方对账 | 明细 = 按销售员 = 按地区 = 月度合计 |
| 异常输入 | 6 类异常各有中文提示与退出码(2/3/3/4/5/6) |
交付物:源码 + 109 测试 + 7 份文档 + 5 张运行截图 + 调研报告。
三、考题二:FastAPI 记账接口
仓库 → fastapi-ledger
问题
实现一个简易记账 HTTP 服务:
POST /records新增流水,校验金额 > 0、类型合法、日期格式GET /records按类型/分类/日期区间筛选并分页GET /summary返回区间内收入合计、支出合计与结余- 非法参数返回 4xx 及结构化错误信息
验收标准里有一条特别值得注意:并发写入不产生数据错误(能说明如何保证即可)。
成果
| 接口 | 说明 |
|---|---|
GET /health |
健康检查 |
POST /records |
新增流水 |
GET /records |
筛选 + 分页(size 上限 100) |
GET /records/{id} |
单条回查 |
GET /summary |
区间汇总 |
GET /docs |
OpenAPI 交互文档 |
三个关键决策
1. 金额全链路 Decimal,且 JSON 里返回字符串
记账最不能容忍的是"汇总对不上"。而浮点数天然有这个问题:
naive = 0.0
for _ in range(10):
naive += 0.1
# -> 0.9999999999999999 (差 1 分钱)
所以我做了三件事:校验用 Decimal、SQLite 列类型用 TEXT 存 str(Decimal)(REAL 是 IEEE-754 浮点,存 0.1 就有表示误差)、响应里金额序列化为字符串(避免前端 JS 的 Number 命中 53 位精度上限)。
2. 刻意不用 SQL SUM() —— 两个正确决策会互相拆台
这是整个考题里我最有收获的一条。/summary 需要求和,教科书做法是 SELECT SUM(amount),更快更省内存。但本工程为了精度把 amount 存成了 TEXT,而 SQLite 计算 SUM(TEXT) 时会转成 REAL —— 精度保证被性能优化直接摧毁。
于是最终方案是:SQL 只负责筛选,求和交给 Python 侧的 Decimal。代价是区间内的金额行会被取到内存(本题数据量下完全可接受,已如实写进 README 的"不保证项"),并且我把这条决策写成了静态审查断言,防止日后被"优化"回去。
教训:AI 很擅长在单一维度上优化(更快、更省内存、更简洁),但不会主动检查这次优化是否与另一个已定决策冲突。这种跨决策的一致性检查必须由人来做。
3. 并发正确性交给数据库,而不是自己写锁
考题说"数据用内存或 SQLite 存储均可"。内存 dict 看起来更简单,但要满足验收标准就得自己实现并发控制——而这正是最容易写错、错误又最难复现的地方。
我选择 SQLite,并把并发正确性交给成熟机制。具体五层:
| # | 机制 | 解决什么 |
|---|---|---|
| ① | 每请求独立连接 | sqlite3.Connection 默认不可跨线程;不共享即从结构上规避 |
| ② | 每连接设 busy_timeout |
该 PRAGMA 是每连接生效的,漏设会让锁竞争直接报错 |
| ③ | 写事务用 BEGIN IMMEDIATE |
默认 DEFERRED 隔离级别在写时才升级锁,并发下会产生 SQLITE_BUSY |
| ④ | 锁竞争有界重试 | 兜住 busy_timeout 覆盖不到的场景 |
| ⑤ | WAL 模式 | 读写不互相阻塞 |
实证(不是口头说明):
$ python scripts/concurrency_check.py --count 200 --workers 50
成功响应数 : 200
库中实际条数: 200
不同 id 数 : 200
汇总收入 : 246.00
[PASS] 无失败响应
[PASS] 无丢写(库中条数 == 成功数)
[PASS] 无重复写(id 全唯一)
[PASS] 金额精确(== 期望总额)
结论: 并发写入一致 ✅
脚本用 Barrier 让 50 个线程尽量同时冲入临界区,并用直接 SQL 查库作为独立证据源——因为如果只用 API 验证 API,API 的 bug 会同时污染被测对象和验证工具。
明确不保证的部分
我在 README 里主动写了一段"不保证":
- 写入顺序 ≠ id 顺序(并发下先拿到锁的先写,流水语义不依赖到达顺序)
- SQLite 是单写者模型,多进程写会串行化;更高并发应换 PostgreSQL
/summary会把区间金额行取到内存(已说明代价与演进方向)
声明边界不是示弱,是专业——评审者需要据此判断方案是否适合自己的场景。
验证
| 项目 | 结果 |
|---|---|
| 测试 | 155 passed(Python 3.12 与 3.14 两套解释器各一次) |
| 并发一致性 | 200 条 / 50 线程,无丢写无重复,金额精确 |
| 真实服务验证 | 26/26(含中文 UTF-8 往返) |
| 端到端验收演示 | 34/34 |
| 错误结构一致性 | 400/404/405/422 结构完全相同,500 不泄露堆栈 |
四、AI 是怎么参与的,哪些是我自己拍板的
考核明确说:“仅『让 AI 生成、跑通就交』按初级用法处理,过程分上限受限。” 所以我把分工划得很清楚。
交给 AI 的
| 环节 | 为什么可以交 |
|---|---|
| 资料检索 | 两个考题共 21 个 query 跨 4 个检索通道,快速拿到带出处的技术来源(详见下一节) |
| 样板代码 | argparse、Pydantic 校验器、openpyxl 样式 API、异常处理器骨架 |
| 测试批量生成 | 264 个用例的框架与参数化(但断言强度我逐条审) |
| 文档草稿 | README、CHANGELOG、复盘的章节结构 |
用 AnySearch 做检索优化
“让 AI 帮我查资料"听起来很简单,实际做下来发现检索本身也需要方法。我用的 AnySearch 提供四类通道,把它们组合起来比单发一句自然语言问题精确得多:
| 通道 | 用途 | 本次实际用法 |
|---|---|---|
| 垂直领域发现 | 先查"有哪些专门的数据源和必需参数” | 查 code 域时发现有两个子域:code.doc(按库名查官方文档)与 code.snippet(在 GitHub 真实代码里做正则检索) |
| 库文档检索 | 按"库名 + 主题"精确定位官方文档 | pandas read_excel dropna、pandas to_numeric errors coerce、pytest parametrize fixtures、ruff pyproject configuration |
| 真实代码检索 | 在 100 万+ 公开仓库里按正则 + 语言 + 路径过滤 | 用 ExcelWriter、groupby(.*)agg、drop_duplicates 搜 lang=Python, path=src/,捞到 llama-cookbook、chinese-fund-crawler 等项目的实际写法 |
| 通用 Web 检索 | 补中文生态与工程实践 | SQLite 并发与锁、Decimal 金额实践、FastAPI 全局异常处理 |
| 正文抽取 | 对关键页面取全文,而不是只看摘要 | 抽取 SQLite 官方论坛帖、PyPA 打包指南、Ruff 配置文档 |
几个具体的优化收益:
① “先发现子域,再检索"显著提升了精度。 直接问"pandas 怎么清洗数据"只会得到泛泛的结果;而先查子域、再用 code.doc 指定 library=pandas,拿到的就是官方文档里对应的 API 段落。做垂直检索之前先探明可用数据源,比反复改写自然语言问题有效得多。
② 分批并行检索,一次拿全一个主题。 我把 21 个 query 按主题分批并行发出(例如"pandas 读取与清洗 / pandas 多表写出 / openpyxl 样式"一批),而不是串行追问。好处是上下文一次性对齐,避免来回追问导致前置条件丢失。
③ 真实代码检索补上了官方文档没有的工程细节。 官方文档讲 API 语义,但"实际项目怎么写"要看代码。比如 sheet_name 必须截断到 31 字符这个 Excel 限制,是从 llama-cookbook 的 os.path.splitext(f)[0][:31] 里学到的;load_workbook + writer.book 做样式二次加工的标准写法来自一个中文项目。
④ 正文抽取救回了一个差点被误用的结论。 检索到一篇 SQLite 官方论坛帖,标题是「WAL 模式下用 BEGIN IMMEDIATE 会 database is locked,设 busy_timeout 也没用」。只看标题的话,结论就是"这两个不能一起用”。我抽取了全文,发现真实原因是发帖人在 CLI 里执行完 pragma 就退出了进程,超时设置随连接一起消失——论坛回复明确指出该 PRAGMA 是每连接生效的。
这直接变成了一条实现规范:每条连接都要设 busy_timeout,而不是放弃这个组合。如果当时只看摘要,就会去掉并发兜底机制。
⑤ 检索结果必须"落成规范"才算用完。 检索本身不产生价值,我要求每一条采纳的结论都能落到代码或文档里。本次检索到落地的映射:
| 检索到的结论 | 落地位置 |
|---|---|
busy_timeout 每连接生效;WAL 下读者也可能短暂 BUSY |
storage.py 的 _connection(),每条连接第一步就设 |
sqlite3 默认 DEFERRED 隔离级别并发下会 SQLITE_BUSY |
写事务改用 BEGIN IMMEDIATE |
with conn: 在 isolation_level=None 下不按预期工作 |
显式写 BEGIN/COMMIT/ROLLBACK |
SQL SUM(TEXT) 会被转成 REAL 计算 |
汇总改为 Python 侧 Decimal 累加 |
dropna() 不识别空白字符串 |
补显式空白判定 + 专门用例 |
| openpyxl 列宽按字符数算会截断中文 | 列宽按东亚字符宽度 ×2 计算 |
两条检索经验:
① 检索的"标题结论"与"正文结论"可能相反,而且 AI 倾向于转述标题。 凡是会改变架构的资料,必须回到正文验证。
② 检索出来的规范不能照搬,要判断它是否与本题约束冲突。 例如 PyPA 官方推荐 src layout,但其代价是"必须先安装才能运行",与考核"一键运行"直接冲突。 我的处理不是照搬也不是放弃,而是用官方文档给出的
__main__.pyworkaround 做显式调和 ——既保留 src layout 的工程规范性,又满足零安装运行。
我自己拍板、不让 AI 起草的
| 环节 | 理由 |
|---|---|
| 需求澄清清单 | 让 AI 写需求 = 让 AI 定义验收标准,“过程可控"就无从谈起 |
| 测试用例设计 | 让 AI 同时写代码和用例 = 自己出题自己判卷 |
| 关键口径 | 哪些列算"关键字段”、日期区间开闭、空区间返回 404 还是 0 —— 这些直接决定数值,业务后果要人承担 |
| 架构取舍 | 拒绝连接池、拒绝 ORM、拒绝加载账接口 —— 每一项都能说清"拒绝的理由" |
我纠正 AI 的 5 次
这是本次最值得记录的部分。5 次里没有一次是"AI 写不出代码",全部是"AI 写得能跑、但结论或方向是错的":
| # | AI 的判断 | 实测结论 |
|---|---|---|
| 1 | 整数日期交给 pd.to_datetime 即可 |
pd.to_datetime(45292) 不报错,而是返回 1970-01-01.000045292(整数被当纳秒)→ 月度分组全落 1970 年 |
| 2 | 测试失败是因为"pandas 3.0 dtype 排序异常" | 实测 str 与 object 排序一致。真正错的是我的测试期望值,代码一直是对的 |
| 3 | 混合日期格式交给整列推断 | 整列推断失败会静默把 "2024/3/15" 变成 NaT,等于凭空少一笔销售额 |
| 4 | 输出哈希不同是因为"数据不确定" | 真因是 openpyxl 在 _save() 里硬编码 datetime.now()(core.xml + 每个 zip 成员 mtime) |
| 5 | 并发建表报错是因为 busy_timeout 太短 |
把超时加到 5 秒仍然立即失败 → 根本不是超时问题,BEGIN IMMEDIATE 取锁失败时没走等待路径 |
还有一次是技术资料的标题党:检索到 SQLite 官方论坛帖标题写着「设 busy_timeout 没用」,AI 转述了这个结论。我读完整帖才发现真实原因是发帖人在 CLI 里设完 pragma 就退出了进程——该 PRAGMA 是每连接生效的。若照标题做,就会去掉并发兜底机制。
共同点:AI 给的是"最可能的原因",不是"已验证的原因"。 这些错误的共同特征是——都能跑通、听起来都合理。
两轮"AI 在单点上对、在全局上错"
- 考题一:
Decimal存金额对、SQL SUM()求和也对 → 但两者叠加会摧毁精度 - 考题二:
TEXT存金额对、busy_timeout该设也对 → 但叠加后仍有覆盖不到的场景
这让我形成了一个判断:AI 更像"高产的初级工程师"——手快、知识面广、局部判断通常正确,但不会主动做跨决策的一致性检查。
所以人的角色从"写代码"变成了两件事:定义验收标准,以及设计能自动抓错的检查。
五、把"约定"变成"断言"
两轮下来我最坚持的一件事:凡是设计约定,都写成 pytest 断言,而不是文档里的一句话。
因为写在文档里的约定,会被下一次"顺手重构"破坏;写成断言后,机器每次都会检查。
| 约定 | 对应的断言 |
|---|---|
考题一:loader 只读 |
test_loader_module_has_no_write_api(AST 审查源码不含写文件 API) |
| 考题一:输出可复现 | test_output_has_no_wall_clock_timestamps |
| 考题一 / 二:依赖不许越界 | test_no_forbidden_runtime_dependencies(禁止 xlsxwriter/polars/SQLAlchemy 等) |
| 考题二:金额不许用 float | test_money_module_never_uses_float(AST 只查 float(...) 调用) |
考题二:不许用 SQL SUM() |
test_storage_does_not_use_sql_sum(去掉 docstring 后检查) |
| 考题二:只有一处碰 SQL | test_only_storage_module_touches_sqlite |
| 考题二:版本三处同步 | test_pyproject_version_matches_package |
顺带踩了个有趣的坑:静态审查最初直接搜源码全文,结果
storage.py的 docstring 里刻意写着"不用SQL SUM()“来解释设计取舍,被误判为违规。规范说明本身会触发规范检查 —— 后来改成用ast去掉文档字符串再检查。
六、可复现(每个仓库都能独立跑通)
两个仓库都是零安装、一键运行,并且不修改任何已有数据。
# 考题一
cd excel-report
pip install -r requirements.txt # 运行时依赖(pandas / openpyxl / python-dateutil)
pip install pytest # 测试依赖
python scripts/make_sample_data.py
python src/excel_report --input data/raw/sales_raw.xlsx --output output/report.xlsx
python -m pytest -q # 109 passed
# 考题二
cd fastapi-ledger
pip install -r requirements.txt # 运行时依赖(fastapi / uvicorn / pydantic)
pip install -r requirements-dev.txt # 测试依赖(pytest + httpx2)
python src/ledger --port 8000 # 打开 http://127.0.0.1:8000/docs
python -m pytest -q # 155 passed
python scripts/smoke_demo.py # 34 项端到端验收
python scripts/concurrency_check.py --count 200 --workers 50
两个仓库都把运行时依赖与测试依赖分开声明: 考题一的测试依赖只有
pytest;考题二因为用TestClient做接口测试, 还需要httpx2(starlette ≥ 1.8 的TestClient优先用它, 两个都没装会直接报RuntimeError)。 这些依赖选择与踩坑记录都写在各自 README 的「常见问题」里。
关于 python -m pytest 而不是 pytest:用户级安装时 pytest.exe 落在
%APPDATA%\Python\PythonXX\Scripts,该目录默认不在 PATH,直接敲 pytest 会报
“无法识别为 cmdlet”。python -m pytest 由解释器定位模块,不依赖 PATH。两种写法等价。
七、总结:五维自查
| 维度 | 落点 | 客观证据 |
|---|---|---|
| 结果 | 两题均可一键运行 | 考题一 4 工作表 + 三方对账;考题二 5 接口 + 结构一致 |
| 过程 | 先澄清、拆小步、留痕 | 每题的 docs/01 需求清单(人工主写)、docs/04 AI 协作记录、docs/05 代码 diff |
| 验证 | 断言具体数值,含边界与异常 | 264 个测试(109 + 155);并发与精度用独立脚本举证 |
| 风险 | 风险清单 + 自动化断言 | 考题一 R-01 |
| 复盘 | 讲清分工、教训与改进项 | 每题的 docs/07,含"若重做会改进什么” |
如果只留一句话
能跑通不代表对。 本次 264 个测试抓出的缺陷里,没有一个是"跑不起来"的—— 全部是"跑得很顺、但结果是错的":整数日期落进 1970 年、混合格式静默丢数据、 1900 闰年差一天、浮点少一分钱、并发丢写、锁竞争绕过超时。
这些缺陷读代码看不出来,只能靠实跑 + 断言具体数值抓出来。 这大概就是这门考核想考的东西。
附:仓库与文档索引
考题一 · Excel 报表生成脚本
| 文档 | 内容 |
|---|---|
README.md |
一键运行、清洗规则、统计口径、退出码 |
docs/01-需求澄清清单.md |
交给 AI 之前先写清的目标/边界/验收标准/决策记录/风险清单 |
docs/02-报表用例.md |
29 条用例设计 |
docs/03-开发方案与技术选型.md |
分层架构、模块接口、版本规则 |
docs/04-AI协作过程记录.md |
协作轨迹与人工干预节点 |
docs/05-代码修改diff.md |
9 项关键修改的前后对比与根因 |
docs/06-测试用例与运行结果.md |
109 测试结果与人工核对证据 |
docs/07-复盘.md |
文字复盘 |
research/01-网络深度查询调研报告.md |
22 条引用来源 |
考题二 · FastAPI 记账接口
| 文档 | 内容 |
|---|---|
README.md |
接口契约、错误码表、并发方案、常见问题 |
docs/01-需求澄清清单.md |
接口契约、存储设计、并发不保证项、12 条决策、9 条风险 |
docs/02-接口用例.md |
62 条用例设计 |
docs/03-开发方案与技术选型.md |
分层架构与并发方案到代码的映射 |
docs/04-AI协作过程记录.md |
协作轨迹与两次纠正 AI 错误归因 |
docs/05-代码修改diff.md |
8 项修改(含 4 项用例缺陷修正) |
docs/06-测试用例与运行结果.md |
155 测试 + 并发举证 + 真实服务验证 |
docs/07-复盘.md |
文字复盘 |
research/01-网络深度查询调研报告.md |
含"标题党结论"的验证过程 |
