Files
pj0235-eai_agentplatform/bugs_and_errors.md
T
eaiadminandClaude Code bc30bfce28 docs(rules): 记录「dev 早期一包提交」——G14.5 例外 + E09 复盘
上一条提交(c1af86c)刻意混合了多条并行线,本提交把这次的教训沉淀下来:

- G14.5 增补例外条款:dev 早期直接 `git add -A` 一包提交,不为粒度打断节奏、
  不反问用户提交范围;同时保留「如实披露混合内容」的硬要求,并给出可操作判据
  「为了让提交能编译而不得不先测依赖锥 ⇒ 本来就是一个单元」。
  原文一字未删,例外写在原文之下;头部版本 V1.3 → V1.5。
- bugs_and_errors.md 新增 E09:症状(一句「提交一下」被做成依赖锥测量 +
  hunk 挑拣工具 + 5 轮索引导出编译 + 最后反问用户范围)、根因(把 G14
  「git 是后台事务」读成了「提交粒度值得花成本」)、修法、以及实测出的依赖锥
  (ASR / LLM 调用层 / 编排 Agent / 联网搜索 是一个编译单元)。

E09 记的是我自己的工作方式错误,不是环境坑。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 22:22:34 +08:00

424 lines
20 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# bugs and errors
> **用途**:记录开发过程中**真实发生过**的错误与坑。
> 每条必须能回答四件事:**症状 → 根因 → 怎么修 → 怎么早点发现**。
>
> 不写流水账,不写「今天很顺利」。这份文件的价值只在于:下一个踩同一个坑的人,
> 能在五分钟内认出自己遇到的是同一件事,而不是花两小时重新推导一遍。
>
> 关联 `TOP_CODING_RULES.md`(G02 错误不得静默、G04 完成判定靠事实)。
## 索引
| # | 日期 | 区域 | 一句话 |
|---|---|---|---|
| E01 | 2026-09-26 | 本地 ASR 依赖 | `torchaudio>=2.9` 删了 pyannote 3.x 要的三个 API,装完 `import` 才炸 |
| E02 | 2026-09-26 | 模型下载 | HuggingFace 不通 + pyannote repo gated,hf-mirror 403,只有魔搭能下 |
| E03 | 2026-09-26 | 模型下载 | `jonatasgrosman/…-chinese-zh-cn` 没有 `model.safetensors`,只有 `.bin` |
| E04 | 2026-09-26 | 依赖安装 | whisperx 的依赖树(lightning/optuna/opentelemetry)解析十几分钟不收敛 |
| E05 | 2026-09-26 | 进度测量 | `/proc/PID/io` 的 `read_bytes` **不算 socket 读**,据此误判「下载卡死」 |
| E06 | 2026-09-26 | 本地 ASR 显存 | 8G 卡被 llama-server 占了 5G,large-v3 的 float16 一加载就 CUDA OOM |
| E07 | 2026-09-26 | 本地 ASR 加载 | pyannote 的 `from_pretrained` **只认文件不认目录**,给目录被当成 HF repo id |
| E08 | 2026-09-26 | 本地 ASR 加载 | torch≥2.6 把 `weights_only` 默认翻成 True,pyannote 旧权重直接拒载 |
| E09 | 2026-09-26 | 工作方式 | 一句「提交一下」被我做成依赖锥测量 + hunk 挑拣工具,最后反问用户提交范围 |
---
## E01 `torchaudio>=2.9` 删了 pyannote 3.x 要的 API
**日期**:2026-09-26 **区域**:`~/asr-poc` 本地 ASR 环境 **影响**:整个本地 ASR 跑不起来
### 症状
装完一切正常(`Successfully installed …` 无任何警告),第一次导入就硬失败:
```
$ ./venv/bin/python -c "import pyannote.audio"
File ".../pyannote/audio/__init__.py", line 29, in <module>
from .core.inference import Inference
File ".../pyannote/audio/core/inference.py", line 35, in <module>
from pyannote.audio.core.io import AudioFile
File ".../pyannote/audio/core/io.py", line 60, in <module>
) -> torchaudio.AudioMetaData:
AttributeError: module 'torchaudio' has no attribute 'AudioMetaData'
```
当时的版本:`torch 2.14.0` / `torchaudio 2.11.0` / `pyannote.audio 3.4.0`。
### 根因
`pyannote.audio` 3.x 有三处依赖 torchaudio 的 **backend-dispatch I/O API**:
| 位置 | 用的是 |
|---|---|
| `pyannote/audio/core/io.py:60` | `torchaudio.AudioMetaData` 作返回注解 |
| `pyannote/audio/core/io.py:81` | `torchaudio.list_audio_backends()` |
| `pyannote/audio/core/io.py:85` | `torchaudio.info(...)` |
而 **torchaudio 从 2.9 起把这套 API 整体移除了**(I/O 交给 `torchcodec`),只留下 `load`。
本机实测 torchaudio 2.11.0:
```python
>>> [n for n in ('load','info','list_audio_backends','AudioMetaData') if hasattr(torchaudio,n)]
['load']
```
**修复后拿到了 torchaudio 官方的一手佐证** —— 降到 2.8.0 之后 import pyannote.audio,
它自己把这条弃用警告打了出来,等于承认了根因:
```
UserWarning: torchaudio._backend.list_audio_backends has been deprecated. This deprecation
is part of a large refactoring effort to transition TorchAudio into a maintenance phase.
The decoding and encoding capabilities of PyTorch for both audio and video are being
consolidated into TorchCodec. ... It will be removed from the 2.9 release.
```
「It will be removed from the 2.9 release」—— 正是我们装的 2.11 里发生的事。
两个放大伤害的细节:
1. `io.py` 没有 `from __future__ import annotations`,所以第 60 行的**注解在 import 时求值** ——
不是用到才炸,是 `import pyannote.audio` 这一行就炸。
2. 这决定了 **任何 pyannote.audio 3.x 都没救**,不是某个小版本的问题。
要么降 torchaudio,要么整个换到 pyannote 4。
### 为什么会踩
- 最初是裸装:`pip install torch torchaudio faster-whisper "pyannote.audio>=3.1,<4" …`
—— **torch/torchaudio 没钉版本**,拿到当时最新的 2.14.0 / 2.11.0。
- pip 全程没有任何警告,`Successfully installed` 一长串看着完全正常。
- 讽刺的是:正因为**绕开 whisperx 手工装**(见 E04)才丢掉了版本约束 ——
而 whisperx 自己钉的是 `torch~=2.8.0`,等于生态早就知道该用 2.8。
### 修法
钉到 pyannote 3.4 那一代的组合:
```bash
pip install --index-url https://mirrors.aliyun.com/pypi/simple \
"torch==2.8.0" "torchaudio==2.8.0"
```
**没走的那条路**:升到 `pyannote.audio 4.0.7`(它已不再依赖 torchaudio)。
`pip install --dry-run` 的结果:
```
Would install … opentelemetry-api/-sdk/-proto/-exporter-*(10 个)
pyannoteai-sdk-0.4.0 torchcodec-0.16.0 safetensors-0.8.0
pyannote-core-6.0.1 pyannote-database-6.1.1 pyannote-metrics-4.1
```
否决理由:多出十几个包,其中 **`pyannoteai-sdk` 是厂商云 SDK、opentelemetry 是遥测上报** ——
与「100% 本地化部署」的原则相抵;且 4.x 吃不吃 `version: 3.1.0` 的 config **没验证过**,
正好是当初选 pyannote 3.x 时就想避开的那个未知数。
### 怎么早点发现
**装完立刻 import 一遍,别等跑数据。**
```bash
./venv/bin/python -c "import torch, faster_whisper, pyannote.audio; print(torch.__version__, pyannote.audio.__version__)"
```
一秒的事,能省掉 4.2GB venv + 4.1GB pip 缓存 + 约 2.5GB 重下的往返。
已固化成 `~/asr-poc/selfcheck.sh`,装完/重建后跑一次。
**通用教训**:GPU 生态里「装成功」和「能用」是两件事 ——
`pip install` 只证明包落盘了,不证明 ABI/API 对得上。
凡是带 C 扩展的包(torch / torchaudio / ctranslate2 / torchcodec),装完必须真 import 一次。
---
## E02 HuggingFace 不通 + gated,只有魔搭能下
**日期**:2026-09-26 **区域**:模型下载
**症状**:`huggingface.co` 上 `pyannote/segmentation-3.0` 等 repo 是 **gated**(需登录并接受条款),
本机连 `huggingface.co` 都连不上(curl 返回 `000`);退而用 `hf-mirror.com` 代理,**一律 403**。
**根因**:gated 是账号态的授权,代理站点没有你的 HF 账号,自然过不了。
**修法**:改用 **`modelscope.cn`(魔搭)** —— 它把这些 gated repo 整个镜像到了**官方命名空间**
(路径就是 `pyannote/segmentation-3.0`),且在魔搭上不 gated。
同一文件同一时刻的实测:
| 站点 | 结果 |
|---|---|
| `huggingface.co` | `000`(不通) |
| `hf-mirror.com` | `403` |
| `www.modelscope.cn` | `206`(分片续传正常) |
**注意**:**gated 不等于许可变更**(许可仍是 MIT / CC-BY-4.0),
但镜像确实绕过了「在 HF 上点接受条款」那一步,交付前值得再确认一次(关联 G19.10 / P06.11)。
**怎么早点发现**:探测顺序应该是「官方 → 官方镜像 → 国内镜像」,而不是「官方失败就放弃」。
且要用**具体文件**去探,不是探首页 —— 首页 200 不代表文件可下(hf-mirror 就是首页通、文件 403)。
---
## E03 这个中文对齐 repo 没有 `model.safetensors`
**日期**:2026-09-26 **区域**:模型下载
**症状**:按惯例去取 `model.safetensors` → 404。
**根因**:`jonatasgrosman/wav2vec2-large-xlsr-53-chinese-zh-cn`
**只发布了 `pytorch_model.bin`**,没有 safetensors 版本。
**修法**:下 `pytorch_model.bin`。已写进 `download_models.sh` 的注释与下载清单,
免得下一个人照 safetensors 的惯例又踩一次。
**怎么早点发现**:下之前先列 repo 文件清单
(魔搭 `…/repo/files?Revision=master`),别按别的 repo 的文件名去猜。
---
## E04 whisperx 的依赖树解析不收敛
**日期**:2026-09-26 **区域**:依赖安装
**症状**:`pip install whisperx` 跑了 **16 分钟以上**仍在 `Resolving dependencies`,
没有失败、没有输出进度,看起来像卡死。
**根因**:whisperx 3.8.6 依赖 `pyannote-audio>=4.0.0` 与 `torch~=2.8.0`,
连带拖进 `lightning` / `optuna` / `opentelemetry` / `aiohttp` 一大棵树,回溯空间极大。
**修法**:放弃 whisperx,只装实际用到的那几个,并**逐条钉版本**:
```
torch==2.8.0 torchaudio==2.8.0 faster-whisper pyannote.audio>=3.1,<4
fastapi uvicorn python-multipart
```
说话人分离用 pyannote 直接做,不需要 whisperx 那层封装。
**怎么早点发现**:`pip install` 长时间无输出就是危险信号。此时应看
`/tmp/pip-unpack-*/` 目录是否在长(见 E05),而不是凭感觉判断「卡死了」。
---
## E05 `/proc/PID/io` 的 `read_bytes` 不算 socket 读
**日期**:2026-09-26 **区域**:进度测量(**我自己犯的错**)
**症状**:pip 下载中,我读 `/proc/<pid>/io` 的 `read_bytes` 得到 0 KB/s,
据此向用户报告「下载卡死了」,并切了一次源。
**根因**:`read_bytes` 统计的是**块设备 I/O**,即真正落到磁盘的字节。
pip 是边下边写临时文件,套接字收包不计入该字段 —— 这个数字在下载期间本来就接近 0,
它**不是**一个「下载进度」指标。
**修法**:量 pip 的下载进度要看**临时目录体积的增长**:
```bash
du -sb /tmp/pip-unpack-*/ # 隔几秒看两次,差值才是真实速度
```
**后果**:因这一次误判,我多发了一次无谓的切换(源其实没问题)。
结论是「源慢」而不是「源死」,两者的处置完全不同。
**怎么早点发现**:用指标前先确认这个指标**定义的是什么**。
一个恒为 0 的读数,先怀疑指标选错了,再怀疑被测对象。
---
## E06 8G 显存被 llama-server 占掉 5G,large-v3 装不下
**日期**:2026-09-26 **区域**:本地 ASR(`~/asr-poc`)
**症状**:`selfcheck.sh` 全绿、模型文件也都在位,一跑就炸:
```
File "/home/eaiadmin/asr-poc/asr_core.py", line 71, in _fw_model
_FW_CACHE["m"] = WhisperModel(str(FW_DIR), device="cuda", compute_type="float16", ...)
File ".../faster_whisper/transcribe.py", line 689, in __init__
self.model = ctranslate2.models.Whisper(...)
RuntimeError: CUDA failed with error out of memory
```
**根因**:不是代码问题,是卡上真的没地方了。`nvidia-smi` 实测:
```
memory.total 8192 MiB, memory.used 5106 MiB, memory.free 2681 MiB
3418, /app/llama-server, 4778 MiB
3449, /app/llama-server, 232 MiB
```
那 5 GiB 是仓库 README 里 8080/8081 那两个**常驻 llama.cpp 服务**占的 ——
属于用户的服务,不是我们这次起的。而 large-v3 的 float16 权重本身就要 3 GB。
**修法**:不动别人的进程,改自己的取用方式(默认 `int8_float16`,实测 2.5 GiB 空闲下装得下,
质量损失可接受;`ASR_COMPUTE_TYPE=float16` 可在显存宽裕时调回):
```python
_COMPUTE_LADDER = [os.environ.get("ASR_COMPUTE_TYPE", "int8_float16"), "int8"]
```
连带的第二个坑:**whisper 和 pyannote 不能同时在卡上**。
只剩两三百 MB 余量,两个都驻留必炸 —— 而这两步本来就是先后关系。
所以转写完立刻 `_free_fw()`(`gc.collect()` + `torch.cuda.empty_cache()`)再上 pyannote。
实测释放后空闲回到 2.50 GiB,分离正常。
**怎么早点发现**:**报「CUDA out of memory」时第一件事是 `nvidia-smi` 看谁占着**,
而不是去调自己的 batch size。本机只有 8G 且常年被别的服务占着,
「模型本身装不下」这种情况会反复出现,值得一开始就把 `mem_get_info()` 打进日志
(现在加载前会打一行 `当前空闲显存 x.xx GiB`)。
---
## E07 pyannote 的 `from_pretrained` 只认文件,不认目录
**日期**:2026-09-26 **区域**:本地 ASR 加载
**症状**:把 HF repo id 换成本地绝对路径(一个**目录**)之后,加载报的错看着像路径写错了,
其实完全不是那个意思:
```
HFValidationError: Repo id must be in the form 'repo_name' or 'namespace/repo_name':
'/home/.../models/pyannote/segmentation-3.0'. Use `repo_type` argument if needed.
```
**根因**:pyannote 判「这是本地路径还是 HF repo id」**只看它是不是一个文件**:
- `core/model.py:588` —— `if os.path.isfile(checkpoint): path_for_pl = checkpoint`
- `core/pipeline.py:78` —— `if os.path.isfile(checkpoint_path): config_yml = checkpoint_path`
目录两条都不成立,于是落到 else 分支去当 repo id 校验,报出上面那句
「repo id 格式不对」——**它压根没打算读目录**。
**修法**:给到**文件**。管线给 `config.yaml`,子模型给 `pytorch_model.bin`:
```python
Pipeline.from_pretrained(str(DIA_DIR / "config.yaml")) # 管线:config 文件
Model.from_pretrained(checkpoint=str(SEG_DIR / "pytorch_model.bin")) # 子模型:权重文件
```
config.yaml 里那两项也得跟着变成 dict 形式(`getter.py:81` 走
`Model.from_pretrained(**dict)` 那一支):
```yaml
segmentation:
checkpoint: <dir>/pytorch_model.bin
```
**而且不能给 `hparams_file`** —— 权重里自带 pytorch-lightning 的 hparams,
repo 里那份 `config.yaml` 是模型结构配置、没有 `task:` 段,塞进去只会换来
`ConfigAttributeError: Missing key setup / full_key: task.setup`。
**怎么早点发现**:**路径参数报「格式不对」,先去看那个函数的判定条件是什么**,
别顺着报错字面去改路径写法(我第一反应是路径要加引号/要相对路径,全错)。
另外:`Pipeline.from_pretrained` 有**两个**同名参数路径,管线和模型各判各的文件类型,
改了一处不代表另一处也通了 —— 这次就是改完管线还在模型那处炸。
---
## E08 torch≥2.6 的 `weights_only` 默认值翻了个面
**日期**:2026-09-26 **区域**:本地 ASR 加载
**症状**:路径问题解决后,加载 pyannote 权重时换了一个完全不同的错:
```
_pickle.UnpicklingError: Weights only load failed. ...
(1) In PyTorch 2.6, we changed the default value of the `weights_only` argument
in `torch.load` from `False` to `True`. ...
WeightsUnpickler error: Unsupported global: GLOBAL torch.torch_version.TorchVersion
was not an allowed global by default.
```
**根因**:torch 2.6 起 `torch.load` 的 `weights_only` 默认由 `False` 翻成 `True`,
pickle 里没在白名单上的全局符号一律拒载。pyannote 3.4 的 `.bin` 里存着 4 个数据类,
逐个不在默认白名单里 —— 而且它是**一次只报一个**,放行一个再报下一个。
**修法**:不要图省事用 `weights_only=False`(那等于把反序列化变成任意代码执行),
而是把这 4 个**数据类**逐个放行:
```python
torch.serialization.add_safe_globals([
torch.torch_version.TorchVersion,
pyannote.audio.core.task.Specifications,
pyannote.audio.core.task.Problem,
pyannote.audio.core.task.Resolution,
])
```
放行的全是数据类、没有可调用对象;权重是我们自己下的、过了 SHA256SUMS,
不存在「不可信来源」的问题。
**怎么早点发现**:这 4 个是**实测枚举出来的**(捕获异常 → 正则抠出 `GLOBAL x.y.Z` →
import 回来 → 再放行 → 重试,直到加载成功),不是猜的。下次遇到同类拒载,
可以用同一招把清单跑出来,别一个个手试。
**遗留疑问**:这 4 个是**这一次加载路径**上遇到的;换模型/换 pyannote 版本可能还要加。
所以 `selfcheck.sh` 里那条「模型文件在位」之外,**首次加载必须真跑一次 diarize** ——
只 import 不加载是查不出来的(import 一路都是好的)。
---
## E09 一句「提交一下」,我做成了一次依赖锥测量
**日期**:2026-09-26 **区域**:工作方式(**我自己犯的错**)
**症状**:用户说「提交一下」(三个字)。我的做法是:
1. 通读 `git status`,107 个条目,逐条判断哪些属于本次 ASR 工作、哪些属于并行工作流;
2. 为了把「本次改动」单独拎出来,写了个 hunk 级挑选工具 `/tmp/pickhunks.py`
(`git diff` 切 hunk → 按序号挑选 → 输出可 `git apply --cached` 的补丁);
3. 把索引导出到临时目录,跑 `go build ./...` 自证「这一包能编译」;
4. 编译失败 → 补 stage 几个文件 → 再导出 → 再编译 …… **循环了 5 轮**;
5. 第 5 轮撞到 `package eai_agentplatform/backend/internal/search is not in std`,
发现锥已经伸到 playwright 和 `go.mod` 了;
6. 于是把索引清空,**弹了个选择框问用户「这次提交装哪些文件」**。
用户驳回,并说:「不做任何区分了,直接一包提交,这种事情以后不要干扰工作节奏,
现在只是在 dev 的早期」。
**根因**:我把 G14 的「dev 阶段 git 是后台事务,不是讨论焦点」读成了
「**提交粒度值得花成本**」——恰恰读反了。G14 的原意是 git 不该占戏份,
而我花了整个回合的注意力在 git 上,最后还把决定权推回给用户。
顺带暴露一个事实判断错误:我以为「ASR 改动」是一个可分离的集合。**它不是。**
**这次实测出来的依赖锥**(留着有用,不是猜测,是逐轮编译失败逼出来的):
```
internal/ai/llm.go 的 ctx 签名变更
→ 12 个调用点必须同时改
→ chat_message.go 的 ctx 改动与「编排重写 callChatModel」**处在同一个 hunk 里**(无法干净拆分)
→ internal/ai/agent.go、web_search_tool.go、general_assistant/orchestrate.go、persistence.go
→ internal/search/(playwright)→ go.mod / go.sum
```
结论:**ASR / LLM 调用层 / 编排 Agent / 联网搜索 在编译上是同一个单元**
(只有网盘是可分离的)。任何「按工作流拆分」的方案都会产出编不过的中间态。
**修法**:`git add -A` 一包提交,提交正文里**逐条列出混合了哪些并行线**。
已写进 `TOP_CODING_RULES.md` G14.5 的例外条款(V1.5)。
**怎么早点发现**:一条比「阶段判断」更好操作的判据 ——
> **如果为了让这次提交能编译,你得先测量一遍依赖锥,那这些改动本来就是一个单元,不要拆。**
实操上的三个信号,出现任一个就该直接 `git add -A` 收工:
- 开始写脚本/工具来**辅助这次提交**(为一次提交造工具,成本已经超过收益);
- 索引导出后编译失败,且**补文件补到第 3 个**还没收敛;
- 发现自己准备**问用户「这次提交装哪些文件」** —— 用户要的是提交完成,不是参与分类。
**边界(别过度矫正)**:这不是「以后永远一包提交」。进入**交付 / 需要回滚定位 / 多人协作**
任一场景,就恢复按事拆分;而且**粒度可以放宽,如实披露不能放宽** ——
混合提交必须在正文里写明混了什么。
---
## 待沉淀(还没写进规则的)
- [ ] **装完必须导入自检**:带 C 扩展的包(torch / torchaudio / ctranslate2)装完立刻 import 一次(见 E01)。
是否升格为 `TOP_CODING_RULES.md` 的一条(放在 G19 依赖那节,或新开一条「依赖装完必须自检」)待定。
- [ ] **E08 的通用形状**:`torch.load(weights_only=True)` 是 2.6 之后的默认行为,
凡是加载 2.6 之前产出的 `.bin` / `.ckpt` 都会撞上,且与具体项目无关 ——
候选升格为 P06 的一条(「旧权重在新 torch 上要先放行 unpickle 白名单」)。
- [ ] **E06 的通用形状**:GPU 是共享资源,跑之前先 `nvidia-smi` 看**别人**占了多少,
再决定自己的精度档位 —— 候选升格为 P06 的一条。