跳到主要內容

Python Debugpy

調試 Python:pdb REPL + debugpy 遠程 (DAP)。

技能元數據

來源捆綁(默認安裝)
路徑skills/software-development/python-debugpy
版本1.0.0
作者Hermes Agent
許可證MIT
平臺linux, macos
標籤debugging, python, pdb, debugpy, breakpoints, dap, post-mortem
相關技能systematic-debugging, node-inspect-debugger

參考:完整 SKILL.md

信息

以下是 Hermes 在觸發此技能時加載的完整技能定義。這是技能激活時代理所看到的指令。

Python 調試器 (pdb + debugpy)

概述

三種工具,根據情況選擇:

工具適用場景
breakpoint() + pdb本地、交互式、最簡單。在源代碼中添加 breakpoint(),正常運行,在該行獲得 REPL。
python -m pdb在無需編輯源代碼的情況下,通過 pdb 啟動現有腳本。適用於快速探查。
debugpy遠程 / 無頭模式 / “附加到已運行的進程”。使用 DAP 通信,可從終端進行腳本化控制,適用於長期運行的進程(網關、守護進程、PTY 子進程)。

breakpoint() 開始。 這是成本最低且有效的方案。

何時使用

  • 測試失敗,且回溯信息未揭示值錯誤的原因
  • 需要單步執行函數並觀察集合的變化
  • 長期運行的進程(hermes gateway, tui_gateway)行為異常且無法重啟
  • 事後分析(Post-mortem):生產類代碼中拋出異常,希望檢查崩潰點的局部變量
  • 子進程 / 子項(Python _SlashWorker, PTY bridge worker)是實際的 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() # <-- drops into pdb here
return result + y

正常運行代碼。你將停留在 breakpoint() 行,並完全訪問局部變量。

提交前別忘了移除 breakpoint() 使用 git diff 或預提交 grep:

rg -n 'breakpoint\(\)' --type py

方案 2:在 pdb 下啟動腳本(無需編輯源代碼)

python -m pdb path/to/script.py arg1 arg2
# Lands at first line of script
(Pdb) b path/to/script.py:42
(Pdb) c

方案 3:調試 pytest 測試

hermes 測試運行器和 pytest 都支持此功能:

# Drop to pdb on failure (or on any raised exception):
scripts/run_tests.sh tests/path/to/test_file.py::test_name --pdb

# Drop to pdb at the START of the test:
scripts/run_tests.sh tests/path/to/test_file.py::test_name --trace

# Show locals in tracebacks without 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
# or
source .venv/bin/activate
python -m pytest tests/foo_test.py::test_bar --pdb

這會繞過 hermetic-env 保證——對於調試來說沒問題,但在推送之前請在包裝器下重新運行以確認。

方案 4:對任何異常進行事後分析

import pdb, sys
try:
run_the_thing()
except Exception:
pdb.post_mortem(sys.exc_info()[2])

或者包裹整個腳本:

python -m pdb -c continue script.py
# When it crashes, pdb catches it and you're in the frame of the exception

或者在 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() # optional: pause immediately once attached

啟動進程;它將在 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 injects itself into the process. Then attach a client as below.

某些內核/安全配置會阻止基於 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"})
# ... loop reading events and sending continue/stepIn/etc.

這適用於一次性自動化任務,但作為交互式用戶體驗則非常痛苦。

選項 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) # blocks until connection

然後從終端執行:

nc 127.0.0.1 4444
# You get a (Pdb) prompt exactly as if debugging locally.

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 near the top of 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) # in the RPC handler you want to trap

從 TUI 觸發匹配的斜槓命令,然後在另一個終端中執行 nc 127.0.0.1 4444

_SlashWorker 子進程

模式相同 — 在工作進程的 exec 路徑中使用帶有 set_trace()remote-pdb。工作進程在斜槓命令之間是持久存在的,因此第一次觸發會阻塞直到你連接;後續的斜槓命令將正常通過,除非你重新武裝斷點。

網關(gateway/run.py

長期運行。在處理程序中使用 remote-pdb,或者如果你反正要重啟網關,可以使用帶有 --wait-for-clientdebugpy

常見陷阱

  1. pytest-xdist 下的 pdb 靜默無效。 你不會看到提示符,測試只會掛起。始終使用 -p no:xdist-n 0

  2. CI / 非 TTY 上下文中的 breakpoint() 會掛起進程。 本地使用是安全的;切勿提交包含它的代碼。添加 pre-commit grep 作為安全措施。

  3. PYTHONBREAKPOINT=0 會禁用所有 breakpoint() 調用。如果你的斷點未命中,請檢查環境變量:

    echo $PYTHONBREAKPOINT
  4. 僅當你同時調用 wait_for_client() 時,debugpy.listen 才會阻塞。 如果沒有它,執行將繼續,你的第一個斷點可能在客戶端附加之前就已觸發。

  5. 在 hardened 內核上附加到 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 可以在協程中工作,但在 pdb 內部使用 await 需要 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 / 多進程。 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

一次性食譜

“為什麼這個字典缺少一個鍵?”

# add above the KeyError site
breakpoint()
# then in pdb:
(Pdb) pp d
(Pdb) pp list(d.keys())
(Pdb) w # how did we get here

“此測試在孤立運行時通過,但在套件中失敗。”

scripts/run_tests.sh tests/the_test.py --pdb -p no:xdist
# But if it only fails WITH other tests:
source .venv/bin/activate
python -m pytest tests/ -x --pdb -p no:xdist
# Now it pdb-traps at the exact failing test after state accumulated.

“我的異步處理程序死鎖。”

# Add at handler entry
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
# On crash, pdb lands at the frame of the exception with full locals