Python Debugpy
调试 Python: pdb REPL + debugpy 远程 (DAP)。
技能元数据
| 来源 | 内置(默认安装) |
| 路径 | skills/software-development/python-debugpy |
| 版本 | 1.0.0 |
| 作者 | Hermes 智能体 |
| 许可证 | MIT |
| 平台 | linux, macos |
| 标签 | 调试, python, pdb, debugpy, 断点, dap, 事后分析 |
| 相关技能 | 系统性调试, node-inspect-debugger, hermes-tui-commands-调试 |
以下是 Hermes 在触发此技能时加载的完整技能定义。这是智能体在技能激活时看到的指令。
Python 调试器 (pdb + debugpy)
概述
三种工具,根据情况选择:
| 工具 | 使用时机 |
|---|---|
breakpoint() + pdb | 本地、交互式、最简单。在源代码中添加 breakpoint(),正常运行,即可在该行获得一个 REPL。 |
| python -m pdb | 无需修改源代码,即可在 pdb 下启动现有脚本。适用于快速探查。 |
| debugpy | 远程/无头/"附加到已运行的进程"。使用 DAP 协议,可从终端脚本化操作,适用于长期运行的进程(网关、守护进程、PTY 子进程)。 |
从 breakpoint() 开始。 它是最省事的可行方案。
何时使用
- 测试失败,但错误追踪信息无法揭示某个值为何出错
- 你需要单步执行一个函数并观察集合的变化
- 一个长期运行的进程(hermes 网关、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 钩子进行检查:
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 网关、tui_gateway、守护进程,一个已经行为异常且无法干净重启的进程。
设置
source /home/bb/hermes-agent/.venv/bin/activate
pip install debugpy
模式 A:修改源代码——进程在启动时等待调试器
在入口点附近(或在你想调试的函数内部)添加:
import debugpy
debugpy.listen(("127.0.0.1", 5678))
print("debugpy 监听 5678 端口,等待客户端连接...", 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 <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 客户端脚本:
# /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)}\r\n\r\n".encode() + body)
def recv():
header = b""
while b"\r\n\r\n" not in header:
header += s.recv(1)
length = int(header.decode().split("Content-Length:")[1].split("\r\n")[0].strip())
body = b""
while len(body) < length:
body += s.recv(length - len(body))
return json.loads(body)
send({"type": "request", "command": "initialize", "arguments": {"adapterID": "python"}})
print(recv())
send({"type": "request", "command": "attach", "arguments": {}})
print(recv())
send({"type": "request", "command": "setBreakpoints",
"arguments": {"source": {"path": sys.argv[1]},
"breakpoints": [{"line": int(sys.argv[2])}]}})
print(recv())
send({"type": "request", "command": "configurationDone"})
# ... 循环读取事件并发送 continue/stepIn 等命令。
这对于一次性自动化操作可行,但作为交互式用户体验则很痛苦。
选项 2:从 VS Code / Cursor / Zed 附加 —— 如果用户打开了这些编辑器,可以添加一个 launch.json:
{
"name": "附加到 Hermes",
"type": "debugpy",
"request": "attach",
"connect": { "host": "127.0.0.1", "port": 5678 },
"justMyCode": false,
"pathMappings": [
{ "localRoot": "${workspaceFolder}", "remoteRoot": "/home/bb/hermes-agent" }
]
}
选项 3:放弃 DAP,使用 remote-pdb —— 通常这才是终端智能体真正想要的:
pip install remote-pdb
在你的代码中:
from remote_pdb import set_trace
set_trace(host="127.0.0.1", port=4444) # 阻塞直到连接
然后在终端:
nc 127.0.0.1 4444
# 你会得到一个 (Pdb) 提示符,就像在本地调试一样。
当 debugpy 的 DAP 协议过于复杂时,remote-pdb 是最简洁的智能体友好选择。仅在你确实需要 IDE 集成时才使用 debugpy。
调试 Hermes 特定进程
测试
参见方案 3。始终添加 -p no:xdist 或在没有 xdist 的情况下运行单个测试。
run_agent.py / CLI —— 一次性运行
最简单:在可疑行附近添加 breakpoint(),然后正常运行 hermes。控制权会在暂停点返回到你的终端。
tui_gateway 子进程(由 hermes --tui 生成)
网关作为 Node TUI 的子进程运行。选项:
A. 修改网关源代码:
# tui_gateway/server.py 在 serve() 函数顶部附近
import debugpy
debugpy.listen(("127.0.0.1", 5678))
debugpy.wait_for_client()
启动 hermes --tui。TUI 会看起来卡住(其后端正在等待)。附加客户端;当你执行 continue 时,执行恢复。
B. 在特定处理程序使用 remote-pdb:
from remote_pdb import set_trace
set_trace(host="127.0.0.1", port=4444) # 在你想要捕获的 RPC 处理程序中
从 TUI 触发匹配的斜杠命令,然后在另一个终端 nc 127.0.0.1 4444。
_SlashWorker 子进程
相同的模式——在 worker 的 exec 路径中使用 remote-pdb 和 set_trace()。该 worker 在斜杠命令之间是持久的,因此第一次触发会阻塞直到你连接;后续的斜杠命令会正常通过,除非你重新设置断点。
网关 (gateway/run.py)
长期运行。在处理程序中使用 remote-pdb,或者如果你无论如何都要重启网关,可以使用带有 --wait-for-client 的 debugpy。
常见陷阱
-
在 pytest-xdist 下,pdb 会静默失效。 你看不到提示,测试会直接挂起。请务必使用
-p no:xdist或-n 0。 -
在 CI 或非 TTY 环境中,
breakpoint()会挂起进程。 本地使用是安全的,但永远不要提交它。添加一个提交前检查作为安全网。 -
PYTHONBREAKPOINT=0会禁用所有breakpoint()调用。 如果你的断点没有触发,请检查环境变量:echo $PYTHONBREAKPOINT -
debugpy.listen只有在你同时调用了wait_for_client()时才会阻塞。 没有它,程序会继续执行,你的第一个断点可能在客户端连接之前就触发了。 -
在加固的内核上附加到 PID 会失败。
ptrace_scope=1(Ubuntu 默认)只允许对子进程进行同用户 ptrace。解决方法:echo 0 > /proc/sys/kernel/yama/ptrace_scope(需要 root)或从开始就使用debugpy启动。 -
多线程。
pdb只能调试当前线程。对于多线程代码,请使用debugpy(支持线程的 DAP)或为每个线程设置threading.settrace()。 -
asyncio。
pdb在协程中可以工作,但在旧版本(3.13 之前)的 pdb 内部使用await需要 Python 3.13+ 或在interact模式下使用await。对于 3.11/3.12,可以使用asyncio.run_coroutine_threadsafe技巧或通过asyncio.ensure_future使用!stmt-based 的 await。 -
scripts/run_tests.sh会剥离凭据并设置HOME=<tmpdir>。 如果你的 bug 依赖于用户配置或真实的 API 密钥,那么在包装脚本下它将无法重现。先使用原始的pytest进行调试以复现问题,然后再在包装脚本下重新确认。 -
分叉 / 多进程。 pdb 不会跟踪分叉。每个子进程都需要自己的
breakpoint()或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()rg -n 'breakpoint\(\)|set_trace\(|debugpy\.listen' --type py
一次性配方
“为什么这个字典缺少一个键?”
# 在 KeyError 发生的位置上方添加
breakpoint()
# 然后在 pdb 中:
(Pdb) pp d
(Pdb) pp list(d.keys())
(Pdb) w # 查看我们是如何到达这里的
“这个测试单独运行时通过,但在整个套件中失败。”
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 陷阱。
“我的异步处理器死锁了。”
# 在处理器入口添加
import remote_pdb; remote_pdb.set_trace(host="127.0.0.1", port=4444)
触发处理器。nc 127.0.0.1 4444,然后输入 w 查看挂起的帧,!import asyncio; asyncio.all_tasks() 查看还有哪些待处理的任务。
“对 Ink 子进程 / 子进程中的崩溃进行事后分析。”
PYTHONFAULTHANDLER=1 python -m pdb -c continue path/to/entrypoint.py
# 崩溃时,pdb 会停在异常的帧,并带有完整的局部变量