{/* 此页面由 website/scripts/generate-skill-docs.py 从技能的 SKILL.md 自动生成。请编辑源文件 SKILL.md,而非此页面。 */}

Python Debugpy

调试 Python:pdb REPL + debugpy 远程调试(DAP)。

技能元数据

来源内置(默认已安装)
路径skills/software-development/python-debugpy
版本1.0.0
作者Hermes Agent
许可证MIT
平台linux, macos
标签debuggingpythonpdbdebugpybreakpointsdappost-mortem
相关技能systematic-debuggingnode-inspect-debuggerdebugging-hermes-tui-commands

参考:完整 SKILL.md

:::info 以下是当此技能被触发时 Hermes 加载的完整技能定义。这是技能激活时代理所看到的指令。 :::

Python 调试器(pdb + debugpy)

概述

三种工具,根据不同场景选择:

工具适用场景
breakpoint() + pdb本地、交互式、最简单。在源码中添加 breakpoint(),正常运行,在对应行获得 REPL。
python -m pdb在 pdb 下启动现有脚本,无需修改源码。适用于快速探查。
debugpy远程/无头/”附加到已运行进程”。使用 DAP 协议,可从终端脚本化,适用于长期运行的进程(gateway、守护进程、PTY 子进程)。

breakpoint() 开始。 这是成本最低的有效方法。

何时使用

  • 测试失败且回溯无法揭示值为何错误
  • 需要单步执行函数并观察集合的变化
  • 长期运行的进程(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列出当前行周围的源码/完整函数
wwhere(栈回溯)
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 或预提交 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
 
# 在测试 START 处进入 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、守护进程、或已出现异常且无法干净重启的进程。

设置

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 <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 等

这对于一次性自动化很好,但作为交互式 UX 则较麻烦。

选项 2:从 VS Code / Cursor / Zed 附加——如果用户打开了一个编辑器,他们可以添加 launch.json

{
  "name": "Attach to 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 启动)

Gateway 作为 Node TUI 的子进程运行。选项:

A. 源码编辑 Gateway:

# 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 子进程

相同模式——在工作器的 exec 路径中使用 remote-pdb 配合 set_trace()。工作器在斜杠命令间保持持久,因此首次触发会阻塞直到你连接;后续斜杠命令正常通过,除非你重新启用。

Gateway(gateway/run.py

长期运行。在处理器中使用 remote-pdb,或如果你无论如何都要重启 gateway,使用带 --wait-for-clientdebugpy

常见陷阱

  1. pdb 在 pytest-xdist 下静默无操作。 你不会看到提示符,测试只是挂起。始终使用 -p no:xdist-n 0

  2. CI/非 TTY 环境中的 breakpoint() 会挂起进程。 在本地是安全的;切勿提交。添加预提交 grep 作为安全网。

  3. PYTHONBREAKPOINT=0 禁用所有 breakpoint() 调用。如果断点未命中,检查环境变量:

    echo $PYTHONBREAKPOINT
  4. debugpy.listen 仅在你同时调用 wait_for_client() 时才会阻塞。 没有它,执行会继续,你的第一个断点可能在客户端附加之前就已触发。

  5. 在强化内核上附加到 PID 会失败。 ptrace_scope=1(Ubuntu 默认)仅允许同用户 ptrace 子进程。解决方法:echo 0 > /proc/sys/kernel/yama/ptrace_scope(需要 root)或从一开始就在 debugpy 下启动。

  6. 线程。 pdb 仅调试当前线程。对于多线程代码,使用 debugpy(线程感知的 DAP)或为每个线程设置 threading.settrace()

  7. asyncio。 pdb 可以在协程中工作,但 await 在 pdb 内部需要 Python 3.13+ 或旧版本使用 interact 模式下的 await。对于 3.11/3.12,使用 asyncio.run_coroutine_threadsafe 技巧或通过 asyncio.ensure_future 进行基于 !stmt 的 await。

  8. scripts/run_tests.sh 会剥离凭据并设置 HOME=<tmpdir> 如果你的 bug 依赖于用户配置或真实的 API 密钥,它在包装器下无法复现。首先使用原始 pytest 调试以复现,然后在包装器下重新确认。

  9. Forking / multiprocessing。 pdb 不会跟随 fork。每个子进程需要自己的 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 会停在异常帧处,带有完整的局部变量