1. Python 静默失败到底卡在哪
Python 运行出错却没有 Traceback,是很多人做脚本、定时任务、后台服务时最头疼的一类问题。正常情况下,Python 解释器遇到未捕获异常会打印完整堆栈,告诉你哪个文件哪一行炸了。但现实里你经常看到的是:进程退出码非 0,日志里干干净净,或者干脆进程还在跑但结果就是不对。这种「静默失败」不是 Python 的 bug,而是异常在到达解释器默认处理器之前,被某一层吞掉了。
我把它归成四类典型场景。第一类是try/except把异常捕获了但没打印,比如except Exception: pass,或者只写了except却只做返回值处理。第二类是日志被重定向,sys.stderr被替换、被关闭,或者 logging 没配 handler,异常信息写进了没人看的文件。第三类是子进程,subprocess默认不继承父进程的输出流,子进程崩了父进程只拿到一个返回码。第四类是守护进程或nohup后台运行,标准输出被丢弃,异常无处可去。
这篇要解决的就是:怎么让这些被吞掉的错误重新「现形」,并且把调用链路的日志集中起来看。我会给你可直接复制的 logging 配置、faulthandler启用片段、子进程输出捕获写法,以及验证命令。同时说明怎么用 TaoToken 的统一 Key 和 API 通道,把模型调用相关的日志集中查看,快速还原错误现场。适合正在写 Python 脚本、做定时任务、跑后台服务,被静默失败折磨过的同学。
2. 用 TaoToken 统一 Key 打通日志链路
排查静默失败,光靠本地 print 往往不够,尤其是脚本里调用了大模型 API 的时候。错误可能发生在请求阶段、响应解析阶段,或者模型返回了非预期结构。如果每个脚本各用各的 Key、各写各的日志,出问题时你根本不知道是哪次调用、哪个参数触发的。
TaoToken 在这里的作用是提供一个统一的 API 通道和 Key 管理入口。你可以把多个脚本、多个服务的模型调用都收敛到同一个 Key 上,调用日志集中在一个地方看。这样当某个 Python 脚本静默失败时,你可以先去 TaoToken 的调用记录里确认:请求到底发出去了没有、返回了什么状态、耗时多少。这一步能快速区分「本地代码异常」和「远端调用异常」,省掉大量猜测。
具体入口我列一下,方便你按需取用。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api ,注意 API 地址不加 UTM 参数。需要生成和管理 Key 就去 API Keys 页面:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。想先验证模型通不通,用模型对话页:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。接入细节看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果你长期写代码、跑 Agent,Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
注意:统一 Key 的价值在于「可追溯」。脚本再多,只要都走同一个通道,排查时就有统一的日志入口,而不是挨个翻本地文件。
3. 可复制的 logging 与 faulthandler 配置
先说最基础的:让异常一定能被看到。下面这段 logging 配置可以直接放进你的脚本入口,它会同时输出到控制台和文件,并且带上时间、级别、模块名和行号。
import logging import sys def setup_logging(logfile="app.log", level=logging.DEBUG): logger = logging.getLogger() logger.setLevel(level) fmt = logging.Formatter( "%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) console = logging.StreamHandler(sys.stderr) console.setFormatter(fmt) logger.addHandler(console) file_handler = logging.FileHandler(logfile, encoding="utf-8") file_handler.setFormatter(fmt) logger.addHandler(file_handler) return logger if __name__ == "__main__": log = setup_logging() log.info("script started")关键点是StreamHandler(sys.stderr)显式绑定到 stderr,避免有人把 stdout 重定向后异常信息跟着消失。文件 handler 用encoding="utf-8",防止中文日志写乱码。
第二段是faulthandler,它能捕获那些连 Python 异常机制都拦不住的情况,比如段错误、死锁、卡死。启用方式很简单,在入口最前面加几行:
import faulthandler import sys faulthandler.enable(file=sys.stderr) # 如果怀疑卡死,可以设置超时后 dump 堆栈 faulthandler.dump_traceback_later(30, exit=True)dump_traceback_later(30, exit=True)的意思是:如果 30 秒后程序还没结束,就把所有线程的堆栈打印出来然后退出。这对排查「进程还在但没反应」的场景特别有用。实测下来,很多死锁问题就是靠这个定位的。
第三段是异常捕获的正确写法。不要用except: pass,至少要把堆栈打出来:
import traceback import logging log = logging.getLogger(__name__) try: result = risky_operation() except Exception: log.error("risky_operation failed\n%s", traceback.format_exc()) raisetraceback.format_exc()返回完整堆栈字符串,配合log.error写进日志。最后的raise决定要不要继续往上抛,如果你希望上层也能感知,就保留它。
4. 子进程与守护进程的输出捕获
子进程是静默失败的重灾区。subprocess.run默认不捕获输出,子进程的 stderr 会直接进父进程的终端;但如果你用了capture_output=True又没检查,输出就被吞进变量里了。正确写法是这样:
import subprocess import logging log = logging.getLogger(__name__) proc = subprocess.run( ["python", "worker.py"], capture_output=True, text=True, timeout=60, ) if proc.returncode != 0: log.error("worker.py exited with %s", proc.returncode) log.error("stdout:\n%s", proc.stdout) log.error("stderr:\n%s", proc.stderr)重点是returncode != 0时把 stdout 和 stderr 都打出来。很多人只打 stderr,但有些脚本把错误信息写到了 stdout,漏掉就白排查了。
守护进程场景,比如用nohup或 systemd 跑的后台服务,标准输出可能被丢弃。这时候要么在代码里把日志写文件,要么在启动命令里重定向:
nohup python daemon.py > /var/log/daemon.out 2>&1 &2>&1把 stderr 合并到 stdout,再一起重定向到文件。如果你用 systemd,可以在 unit 文件里配StandardOutput=append:/var/log/daemon.log和StandardError=append:/var/log/daemon.log,这样两个流都进同一个文件。
还有一个容易忽略的点:sys.stdout和sys.stderr被替换或关闭。有些库会做这种事,导致后续所有输出消失。可以在入口处检查一下:
import sys print("stdout ok:", sys.stdout is not None, file=sys.stderr) print("stderr ok:", sys.stderr is not None, file=sys.stderr)如果发现被替换了,就手动恢复成sys.__stdout__和sys.__stderr__。
5. 验证请求与成功结果
配置写完,得验证它真的生效。第一步,写一个故意报错的脚本:
import logging import traceback logging.basicConfig( level=logging.DEBUG, format="%(asctime)s | %(levelname)s | %(message)s", ) log = logging.getLogger("verify") def boom(): return 1 / 0 try: boom() except Exception: log.error("caught exception\n%s", traceback.format_exc())运行python verify.py,你应该在终端看到类似这样的输出:
2025-01-01 10:00:00 | ERROR | caught exception Traceback (most recent call last): File "verify.py", line 12, in <module> boom() File "verify.py", line 9, in boom return 1 / 0 ZeroDivisionError: division by zero看到完整堆栈,说明 logging 和 traceback 链路通了。第二步验证 faulthandler,写一个死循环脚本,加上dump_traceback_later(5, exit=True),运行后等 5 秒,应该看到所有线程堆栈被打印出来然后进程退出。
第三步验证子进程捕获,写一个worker.py故意抛异常,用上面的subprocess.run调用,确认父进程日志里能看到子进程的 stderr。
第四步,如果你脚本里调用了模型 API,去 TaoToken 的调用记录里核对。用同一个 Key 发起一次请求,确认记录里能看到这次调用、返回状态和耗时。这样本地日志和远端调用日志就能对上,排查时不会两头断线。
6. 本篇常见错排查
错误一:日志文件写了但没内容。最常见原因是 handler 没加或者 level 设太高。检查logger.setLevel和 handler 的 level,两者都要放开。另外确认文件路径有写权限,容器里经常因为挂载问题写不进去。
错误二:traceback.format_exc()返回NoneType: None。这说明你不在 except 块里调用它。format_exc依赖当前异常上下文,必须在except内部使用,或者用traceback.format_exception(*sys.exc_info())显式传入。
错误三:子进程输出是空的。检查是不是用了capture_output=True但没读proc.stdout。另外text=True没加的话拿到的是 bytes,打印出来是b'...',容易看漏。
错误四:faulthandler 没反应。dump_traceback_later是定时器,如果主线程被 C 扩展阻塞,可能不触发。可以配合faulthandler.enable()先捕获致命信号,再看是否需要在关键位置手动faulthandler.dump_traceback()。
错误五:TaoToken 调用记录里找不到请求。先确认脚本用的 Base URL 是https://taotoken.net/api,Key 是从 API Keys 页面生成的且没过期。如果请求根本没发出去,问题在本地网络或代码逻辑;如果发出去了但报错,记录里会有状态码,按状态码排查参数或额度。
错误六:日志时间对不上。容器时区默认 UTC,本地是东八区,差 8 小时。在 logging Formatter 里加converter=time.localtime,或者启动容器时设TZ=Asia/Shanghai。
排查顺序建议:先确认异常有没有被捕获,再看日志有没有写出去,然后看子进程输出有没有拿到,最后核对远端调用记录。一层层往下,基本不会漏。
如果你在接入或排障过程中卡住,直接去 API Keys 页面重新生成一个 Key 试一次,或者翻接入文档对照参数:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 和 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。想先确认模型本身通不通,用模型对话页发一条消息最快:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。长期写代码、跑 Agent 的话,Coding Plan 能把调用和日志管理一起收拢:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。