概述
三种工具,按需选择:| 工具 | 使用场景 |
|---|---|
breakpoint() + pdb | 本地、交互式、最简单。在源码中添加 breakpoint(),正常运行,在该行进入 REPL。 |
python -m pdb | 无需修改源码,在 pdb 下启动现有脚本。适合快速探索。 |
debugpy | 远程/无头/"附加到已运行进程"。使用 DAP 协议,可从终端脚本化,适用于长期运行的进程(gateway、daemon、PTY 子进程)。 |
使用场景
- 测试失败,回溯信息无法揭示值为何错误
- 需要逐函数步进并观察集合的变化
- 长期运行的进程(hermes gateway、tui_gateway)异常且无法重启
- 事后调试:生产环境代码抛出异常,想在崩溃点检查局部变量
- 子进程/子进程(Python _SlashWorker、PTY 桥接工作进程)是真正的 bug 所在
print() / logging.debug 能在一分钟内解决的问题,或 pytest -vv --tb=long --showlocals 已经揭示的问题。
pdb 快速参考
在任意 pdb 提示符((Pdb))内:
| 命令 | 作用 |
|---|---|
h / h cmd | 帮助 |
n | 下一行(步过) |
s | 步入 |
r | 从当前函数返回 |
c | 继续 |
unt N | 继续到第 N 行 |
j N | 跳转到第 N 行(仅限同一函数内) |
l / ll | 列出当前行附近的源码 / 完整函数 |
w | 位置(堆栈跟踪) |
u / d | 在堆栈中上移/下移 |
a | 打印当前函数的参数 |
p expr / pp expr | 打印/美化打印表达式 |
display expr | 每次停止时自动打印表达式 |
b file:line | 设置断点 |
b func | 在函数入口处断点 |
b file:line, cond | 条件断点 |
cl N | 清除断点 N |
tbreak file:line | 一次性断点 |
!stmt | 执行任意 Python 语句(包括赋值) |
interact | 在当前作用域进入完整 Python REPL(Ctrl+D 退出) |
q | 退出 |
interact 命令最强大——你可以导入任何东西、检查复杂对象,甚至调用改变状态的方法。局部变量默认只读;在 (Pdb) 提示符使用 !x = 42 来修改。
操作步骤 1:本地断点
最简单。编辑文件:
def compute(x, y):
result = some_helper(x)
breakpoint() # <-- 在此进入 pdb
return result + y
正常运行代码。你会在 breakpoint() 行停下,拥有对局部变量的完全访问。
提交前别忘了移除 breakpoint()。使用 git diff 或 pre-commit grep:
rg -n 'breakpoint()' --type py
操作步骤 2:在 pdb 下启动脚本(无需修改源码)
python -m pdb path/to/script.py arg1 arg2
# 在脚本第一行停下
(Pdb) b path/to/script.py:42
(Pdb) c
操作步骤 3:调试 pytest 测试
hermes 测试运行器和 pytest 都支持:
# 失败时进入 pdb(或任何抛出的异常):
scripts/run_tests.sh tests/path/to/test_file.py::test_name --pdb
# 在测试开始时进入 pdb:
scripts/run_tests.sh tests/path/to/test_file.py::test_name --trace
# 在回溯中显示局部变量(无需 pdb):
scripts/run_tests.sh tests/path/to/test_file.py --showlocals --tb=long
注意:scripts/run_tests.sh 默认使用 xdist(-n 4),pdb 在 xdist 下不工作。添加 -p no:xdist 或用 -n 0 运行单个测试:
scripts/run_tests.sh tests/foo_test.py::test_bar --pdb -p no:xdist
# 或
source .venv/bin/activate
python -m pytest tests/foo_test.py::test_bar --pdb
这绕过了隔离环境保证——调试时没问题,但推送前要在包装器下重新确认。
操作步骤 4:任意异常的事后调试
import pdb, sys
try:
run_the_thing()
except Exception:
pdb.post_mortem(sys.exc_info()[2])
或包装整个脚本:
python -m pdb -c continue script.py
# 崩溃时,pdb 捕获它,你进入异常帧
或在 repl/jupyter 中设置全局钩子:
import sys
def excepthook(etype, value, tb):
import pdb; pdb.post_mortem(tb)
sys.excepthook = excepthook
操作步骤 5:使用 debugpy 远程调试(附加到运行中的进程)
适用于长期运行的进程:Hermes gateway、tui_gateway、daemon、已经异常且无法干净重启的进程。安装配置
source /home/bb/hermes-agent/.venv/bin/activate
pip install debugpy
模式 A:修改源码——进程在启动时等待调试器
在入口点顶部(或你想调试的函数内)添加:
import debugpy
debugpy.listen(("127.0.0.1", 5678))
print("debugpy listening on 5678, waiting for client...", flush=True)
debugpy.wait_for_client()
debugpy.breakpoint() # 可选:附加后立即暂停
启动进程;它会阻塞在 wait_for_client()。
模式 B:不修改源码——用 -m debugpy 启动
python -m debugpy --listen 127.0.0.1:5678 --wait-for-client your_script.py arg1
模块入口的等效写法:
python -m debugpy --listen 127.0.0.1:5678 --wait-for-client -m your.module
模式 C:附加到已运行的进程
需要 PID 且目标环境中已安装 debugpy:
python -m debugpy --listen 127.0.0.1:5678 --pid
# debugpy 将自己注入进程。然后如下附加客户端。
某些内核/安全配置会阻止基于 ptrace 的注入(/proc/sys/kernel/yama/ptrace_scope)。修复:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
从终端连接客户端
最简单的终端端 DAP 客户端是 VS Code CLI 或小脚本。在 Hermes 内有两个实用选项: 选项 1:debugpy 自己的 CLI REPL——不是官方功能,但有一个小型 DAP 客户端脚本:bash rg -n 'breakpoint()|set_trace(|debugpy.listen' --type py# /tmp/dap_client.py import socket, json, itertools, time, sys HOST, PORT = "127.0.0.1", 5678 s = socket.create_connection((HOST, PORT)) seq = itertools.count(1) def send(msg): msg["seq"] = next(seq) body = json.dumps(msg).encode() s.sendall(f"Content-Length: {len(body)}rnrn".encode() + body) def recv(): header = b"" while b"rnrn" not in header: header += s.recv(1) length = int(header.decode().split("Content-Length:")[1].split("rn")[0].strip()) body = b"" while len(body) /proc/sys/kernel/yama/ptrace_scope(需要 root)或从一开始就在 debugpy 下启动。threading.settrace()
- 线程。pdb 只调试当前线程。对于多线程代码,使用 debugpy(线程感知的 DAP)或为每个线程设置
。awaitasyncio。pdb 在协程中工作,但 pdb 内的 需要 Python 3.13+ 或旧版本的 interact 模式。对于 3.11/3.12,使用asyncio.run_coroutine_threadsafe技巧或通过asyncio.ensure_future的!stmt基 await。scripts/run_tests.sh剥离凭证并设置HOME=。如果你的 bug 依赖用户配置或真实 API 密钥,在包装器下不会重现。先用原始 pytest 调试复现,然后在包装器下重新确认。breakpoint()Fork/多进程。pdb 不跟随 fork。每个子进程需要自己的 或set_trace()。对于 Hermes 子代理,一次调试一个进程。pip install debugpy验证清单
- [ ]
后,确认:python -c "import debugpy; print(debugpy.__version__)"ss -tlnp | grep 5678[ ] 对于远程调试,确认端口确实在监听: PYTHONBREAKPOINT=0[ ] 第一个断点确实命中(如果没有,可能是 、在 xdist 下,或附加前执行已完成)where[ ] /w显示预期的调用栈breakpoint()[ ] 调试后清理:提交的代码中没有遗留的 /set_trace()
一键配方
"为什么这个字典缺少一个键?"
python
# 在 KeyError 位置上方添加
breakpoint()
# 然后在 pdb 中:
(Pdb) pp d
(Pdb) pp list(d.keys())
(Pdb) w # 我们怎么到这的
"这个测试单独通过但在套件中失败。"
bash
scripts/run_tests.sh tests/the_test.py --pdb -p no:xdist
# 但如果只在有其他测试时失败:
source .venv/bin/activate
python -m pytest tests/ -x --pdb -p no:xdist
# 现在 pdb 在状态累积后的确切失败测试处捕获
"我的异步处理器死锁。"
python
# 在处理器入口添加
import remote_pdb; remote_pdb.set_trace(host="127.0.0.1", port=4444)
bash PYTHONFAULTHANDLER=1 python -m pdb -c continue path/to/entrypoint.py # 崩溃时,pdb 进入异常帧,拥有完整局部变量 ```触发处理器。nc 127.0.0.1 4444,然后w查看暂停的帧,!import asyncio; asyncio.all_tasks()` 查看还有什么待处理。"Ink 子进程/子进程崩溃的事后调试。"
安装指南
复制下方命令,在终端运行即可安装:
# 安装到当前项目
npx skills add python-debugpy
# 全局安装 — 所有项目可用
npx skills add python-debugpy -g
使用指南
安装完成后,在对话框中直接使用此技能。