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 | 列出當前行附近的源代碼 / 整個函數 |
w | where(堆棧跟蹤) |
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-client 的 debugpy。
常見陷阱
-
pytest-xdist 下的 pdb 靜默無效。 你不會看到提示符,測試只會掛起。始終使用
-p no:xdist或-n 0。 -
CI / 非 TTY 上下文中的
breakpoint()會掛起進程。 本地使用是安全的;切勿提交包含它的代碼。添加 pre-commit grep 作為安全措施。 -
PYTHONBREAKPOINT=0會禁用所有breakpoint()調用。如果你的斷點未命中,請檢查環境變量:echo $PYTHONBREAKPOINT -
僅當你同時調用
wait_for_client()時,debugpy.listen才會阻塞。 如果沒有它,執行將繼續,你的第一個斷點可能在客戶端附加之前就已觸發。 -
在 hardened 內核上附加到 PID 會失敗。
ptrace_scope=1(Ubuntu 默認值)僅允許對子進程進行同用戶 ptrace。變通方法:echo 0 > /proc/sys/kernel/yama/ptrace_scope(需要 root 權限)或從一開始就在debugpy下啟動。 -
線程。
pdb僅調試當前線程。對於多線程代碼,使用debugpy(感知線程的 DAP)或為每個線程設置threading.settrace()。 -
asyncio。
pdb可以在協程中工作,但在 pdb 內部使用await需要 Python 3.13+,或在較舊版本中使用interact模式下的await。對於 3.11/3.12,使用asyncio.run_coroutine_threadsafe技巧或通過asyncio.ensure_future使用基於!stmt的 await。 -
scripts/run_tests.sh會剝離憑據並設置HOME=<tmpdir>。 如果你的 bug 依賴於用戶配置或真實的 API 密鑰,它在包裝器下無法復現。首先使用原始pytest進行調試以復現問題,然後在包裝器下再次確認。 -
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