本层判断标准

这次改动新增的依赖,它的名字、许可、替代方案、移除方式,能不能在合并请求说明里找到?

说明里找不到这四项,说明它是在没人注意的时候进来的。补法:把下面的五问模板补进说明;第 1 问和第 5 问答不出,评审人直接拒。后果是可以量化的:一个装进 site-packages 的包出了问题,要么连带它的一整棵依赖树一起换掉,要么继续用一个没人维护的库。

引入前回答的五个问题

五个问题不用写成长文档,但每问都要留一条命令或一段可查的记录,答案要有人看过。「助手说这个库很常用」不是答案。下面这份模板直接贴进合并请求的「新增依赖」一栏。

五问模板(贴进合并请求说明)

它是「填完才能提交」的检查表,不是问卷。空着的一栏等于没查。

## 新增依赖:fastcache-lite==0.4.2

1. 仓库里有没有同类库?
   - 搜索:grep -rn "cache" pyproject.toml requirements*.txt app/
   - 结论:无同类(仓库内仅用 functools.lru_cache,无持久化缓存)
2. 许可是什么?
   - 许可类型:MIT(包内 LICENSE 文件)
   - 与项目许可(MIT)是否冲突:否
3. 维护状态如何?
   - 最近一次发布:近 6 个月内
   - 是否单人维护:否
   - 停更是否超过 12 个月:否
4. 体积与传递依赖可接受吗?
   - 新增传递依赖:0 个(pipdeptree 查看)
   - 结论:可接受
5. 出问题怎么移除?
   - 引用范围:只在 app/clients/cache.py
   - 替换成本:改动 1 个文件,接口只有 get/set/delete

判据只有两处:答不出第 1 问的搜索命令,说明没查;答不出第 5 问的引用范围,说明这个依赖会渗进整个项目,移除成本高,要更谨慎。边界:只用标准库就能完成的改动,不必填这份模板,它只针对第三方包。

助手交出来的样子

## 新增依赖:fastcache-lite==0.4.2
1. 仓库里有没有同类库?  无(助手说这个库很常用)
2. 许可是什么?          MIT(助手说)
3. 维护状态如何?        近期有发布(助手说)
4. 传递依赖可接受吗?    0 个(助手说)
5. 出问题怎么移除?      改动 1 个文件(助手说)

认它的办法:装之前去注册表亲自查一次这个名字,看首次发布时间和下载量。五问全由助手代填、每问都是「助手说」,等于一问没答——页面上那句「『助手说这个库很常用』不是答案」说的就是它。差别在于查一次只要十秒,而这个包会一直留在依赖树里。

值得单独说的是:助手推荐的包名,可能是它编出来的。Spracklen 等人在 2025 年那篇测量里(USENIX Security 2025,arXiv:2406.10279)跑了 16 个模型、57.6 万次生成,19.7% 的包引用在现实中不存在——开源模型平均 21.7%,商业模型 5.2%。更难办的是其中 43% 的假名字在重复生成里稳定复现,攻击者抢注这些名字就能批量收割,这个手法叫 slopsquatting。查一次注册表同时挡住两件事:不存在的包,和刚被抢注的包。

依赖清单与锁文件

把它分成两样:人写的直接依赖,和工具生成的锁文件。前者评审时看,后者保证同一个版本在每台机器上装出同样的东西。锁文件必须进版本库,每次变更要在合并请求里看得见。

requirements.in(人写,只有直接依赖)

不写版本上限,只在出现不兼容时加约束。

flask
httpx
pytest

requirements.lock(工具生成,带哈希)

由 pip-compile 生成,含所有传递依赖与哈希。不要手工编辑它;要改就重新生成整份。

# 由 pip-compile --generate-hashes 生成,不要手工编辑
flask==3.0.3 --hash=sha256:...
httpx==0.27.0 --hash=sha256:...
pytest==8.3.2 --hash=sha256:...
# 传递依赖(节选)
click==8.1.7 --hash=sha256:...
certifi==2024.8.30 --hash=sha256:...

docs/licenses.md(许可清单)

它一次性回答「我们用了哪些许可」。由工具生成,不用手写。

# 依赖许可清单(由 pip-licenses 生成)

| 包 | 版本 | 许可 | 用途 |
|---|---|---|---|
| flask | 3.0.3 | BSD-3-Clause | Web 框架 |
| httpx | 0.27.0 | BSD-3-Clause | 调用外部模型服务 |
| pytest | 8.3.2 | MIT | 测试,不进入运行环境 |

边界:带哈希的锁文件保证「同一个版本装出同样的东西」,代价是升级一个包就要重新生成整份,冲突时还要再跑一遍。课程项目规模下这个代价很小。本组决定不要哈希,至少要把精确版本(==)锁住,不要用 >=——后者会让不同机器装出不同的依赖。

什么时候不该引入第三方库

下面六种情况里前三种最常见,出现任一条就不要引。

情况判据改用
要的功能标准库里有日期时间、JSON、压缩、HTTP 基本请求标准库,先查 python -c "import json" 一类能否满足
只用它一两个函数全项目对该库的调用点 ≤ 2 处自己写十几行,写进 app/utils/
会带进一棵依赖树传递依赖数量 > 10 个,或新增包体积明显变大标准库或自写;换更小的替代库
停更超过 12 个月最近一次发布距今 > 12 个月换维护中的库,或自写
许可与项目冲突许可类型与项目不一致,以 SPDX 标识为准不引入,换许可相容的替代
只是让代码看起来短说不出它在解决哪个具体问题不引入,保留现有实现
传递依赖数量上限
≤ 10 个
超过 10 个就换更小的库或自己写。查看命令:pipdeptree。
停更时间上限
≤ 12 个月
超过 12 个月不引入;确实用得上,就在说明里写明替代计划与接手的人。
全项目调用点上限
≤ 2 处
全项目只有两处调用的库,自己写通常更省事。
新增依赖审批
≥ 1 人
把「新增依赖」一栏空着提交,评审人直接退回。
可以这么做 先用标准库试一次;再用 pipdeptree 看它会带进来多少包;最后把引用收在一个模块里,只暴露必要的方法。
不要这么做 为了一个函数引入一整棵依赖树;在业务代码里到处 import 同一个库;把「这个库很常用」「能跑通」当成引入理由。

锁定与审计命令

五条命令:装工具、生成锁文件、安装、查漏洞、查许可。把它们全部写进 Makefile 或工作流,本地和持续集成跑同一条。

锁定与审计(可直接复制)

第一次要装两个工具。之后每次改依赖,只跑第二到第五条。

# 1. 装工具
pip install pip-tools pip-audit pipdeptree

# 2. 生成带哈希的锁文件(改动它等于改动依赖,要单独评审)
pip-compile --generate-hashes requirements.in -o requirements.lock

# 3. 按锁文件安装
pip install -r requirements.lock

# 4. 查已知漏洞(退出码非零即失败)
pip-audit -r requirements.lock

# 5. 查传递依赖与许可
pipdeptree --warn silence
pip-licenses --format=markdown --with-urls > docs/licenses.md

# 6. 看这次改动动了哪些依赖(评审用)
git diff origin/main -- requirements.lock docs/licenses.md
严重度处置时限
高危阻断合并,升级或替换该依赖立即
中危限期修复,超过时限自动升为阻断7 天内
低危记录,下次依赖变更时一并处理无单独时限

一条容易忽略的边界:扫描工具本身也是依赖。装一个扫描器不会让事情变好,让它跑起来才会——仓库里没有可复制命令的扫描项,不写进清单。密钥扫描要覆盖历史提交(gitleaks detect --redact):已经进过仓库的密钥一律轮换,只删掉那一行,它在历史里仍然可查,等于没处理。这一条与质量门禁共用同一套阈值。