☰
DeepSeek-Agent-Harness-2026终极指南-第7章第33节-生产级日志与调用留痕-退出时的调用总结:minmaxavg一网打尽
2026/10/1 10:55:32 网站建设 项目流程

DeepSeek Agent Harness 2026终极指南 - 第7章第33节 退出时的调用总结:min/max/avg 一网打尽

第 31 节做了 CallRecord,第 32 节做了 trace 落盘——调用数据在积累,但程序跑完就关了,你什么也没看到。这节用atexit注册退出钩子,在程序正常退出或sys.exit()时自动打印一份量化总结报表。min/max/avg token、耗时分布、成功率——以后每次跑完 Agent,控制台末尾就是一份完整的"财务报告"。

本文导航

  • atexit 是什么
  • 报表设计:C 语言风格的格式化输出
  • 注册到 call_tracker
  • 异常退出也能打印吗
  • 完整实录
  • 小结

atexit 是什么

atexit是 Python 标准库的一员,干一件事:注册一个函数,在程序正常退出时自动调用。不需要显式写finally,不需要包在try/except里,注册了就生效。

importatexitdefgoodbye():print("\n===== 程序退出,再见 =====")atexit.register(goodbye)print("主逻辑运行中...")# 程序结束时会自动打印 "===== 程序退出,再见 ====="

atexit的工作机制是:

  • 正常退出(sys.exit()或者main()return)→ 触发
  • os._exit()硬杀→不触发(这是唯一例外)
  • SIGTERM信号→ 不直接触发,需要配合signal模块监听

对我们的场景来说,只要你不是kill -9强杀进程,atexit都会在退出前执行我们的总结函数。Agent 跑完一轮正常退出、达到预算上限触发sys.exit()、甚至 Ctrl+C 中断——这些情况atexit都能兜住。

报表设计:C 语言风格的格式化输出

要做量化总结,先在call_tracker.py里加一个格式化输出方法:

# deep_pilot/call_tracker.py —— CallTracker 追加退出报表方法defprint_exit_report(self)->None:"""程序退出时打印量化总结报表——min/max/avg 一网打尽"""s=self.summary()ifs.get("call_count",0)==0:return# 助手函数:安全格式化数字def_nf(v,fmt=".1f"):returnf"{v:{fmt}}"ifisinstance(v,(int,float))elsestr(v)print(f"\n{'='*70}")print(f" DeepPilot 本次运行调用总结")print(f"{'='*70}")print(f" 调用次数:{s['call_count']}")print(f"{'='*70}")print(f"{'指标':<20}{'最小':>10}{'最大':>10}{'平均':>10}{'合计':>10}")print(f"{'-'*20}{'-'*10}{'-'*10}{'-'*10}{'-'*10}")pt=s["prompt_tokens"]ct=s["completion_tokens"]tt=s["total_tokens"]et=s["elapsed_seconds"]print(f"{'输入 token':<20}{_nf(pt['min']):>10}{_nf(pt['max']):>10}{_nf(pt['avg']):>10}{_nf(pt['total']):>10}")print(f"{'输出 token':<20}{_nf(ct['min']):>10}{_nf(ct['max']):>10}{_nf(ct['avg']):>10}{_nf(ct['total']):>10}")print(f"{'总 token':<20}{_nf(tt['min']):>10}{_nf(tt['max']):>10}{_nf(tt['avg']):>10}{_nf(tt['total']):>10}")print(f"{'耗时(秒)':<20}{_nf(et['min'],'.2f'):>10}{_nf(et['max'],'.2f'):>10}{_nf(et['avg'],'.2f'):>10}{'':>10}")print(f"{'='*70}")print(f" 终止原因分布: {s.get('finish_reasons', {})}")print(f" 模型使用分布: {s.get('models_used', {})}")print(f"{'='*70}\n")

这个报表的设计思路:

  1. C 风格对齐:用<20、>10等字段宽度做列对齐,清晰直观。比 json 美观,比纯文本紧凑。
  2. 最小/最大/平均/合计四列:这是量化分析的标准套路。min 告诉你最乐观情况,max 告诉你峰值压力,avg 告诉你稳态性能,合计告诉你总成本。
  3. 耗时不统计合计:因为合计耗时没有意义(每次调用是并行的还是串行的?),所以那一列留空。
  4. 分布统计:finish_reason 分布和 model 分布放在底部,一眼看出有没有异常终止、有没有用错模型。

注册到 call_tracker

在call_tracker.py模块级别注册 atexit 钩子:

# deep_pilot/call_tracker.py —— 模块末尾追加importatexitimportsysdef_on_exit():"""程序退出钩子——打印总结 + dump trace 记录"""tracker=CallTracker()iftracker.call_count==0:returntracker.print_exit_report()# 退出前把所有记录 JSON 到文件(防重启丢失)try:path=tracker.dump_all()print(f"[DeepPilot] 调用记录已存档:{path}")exceptException:print("[DeepPilot] 调用记录存档失败",file=sys.stderr)# 清理超量 tracetry:deleted=tracker.prune_traces(max_count=1000)ifdeleted:print(f"[DeepPilot] 清理超量 trace 文件:{deleted}个")exceptException:passatexit.register(_on_exit)

几个设计注意点:

  1. 在模块 import 时就注册。atexit.register放在模块顶层,这样只要from deep_pilot.call_tracker import tracker这一行执行了,退出钩子就挂上了。不需要任何额外配置。

  2. dump_all()在退出时调用一次。程序跑了几天几夜,内存里的_records丢了就丢了。退出前 JSON 落盘到logs/records/,下次启动可以 reload(虽然目前没必要,但留了这个接口)。

  3. 异常全部吞掉。退出钩子里报异常可能会导致退出流程卡住,让用户看到一个莫名其妙的 traceback。每个可能失败的步骤用try/except包住,fail silently。

异常退出也能打印吗

atexit覆盖大部分情况,但有两个边界要注意:

Ctrl+C (KeyboardInterrupt) 会触发 atexit。Python 收到 SIGINT 后抛 KeyboardInterrupt → 栈展开 → atexit 执行 → 进程退出。所以 Ctrl+C 停掉 Agent 后,你能看到总结报表。

未捕获异常也会触发 atexit。程序如果因为一个没 catch 的异常崩溃,Python 会先执行 atexit 回调,再打印 traceback。这意味着即使 Agent 跑崩了,你也能看到崩之前的所有调用统计——这对调试非常有价值。

os._exit()不触发。这是唯一的例外。所以自己写代码不要用os._exit()退出,用sys.exit()就好。

importatexit atexit.register(lambda:print("atexit 执行了!"))# ✅ 这些会触发 atexit:sys.exit(0)# 正常退出raiseSystemExit# 等同于 sys.exit()# Ctrl+C 中断 # KeyboardInterrupt# ❌ 这个不会触发 atexit:# os._exit(0) # 硬杀,绕过了所有清理逻辑

完整实录

把以上代码写入call_tracker.py(完整约 180 行),然后模拟几次调用 + 退出:

cddeep-pilot uv run python-c" # 模拟几次调用 from deep_pilot.client import client client.chat([{'role': 'user', 'content': '1+1等于几'}]) client.chat([{'role': 'user', 'content': '用 Python 写 Hello World'}]) client.chat([{'role': 'user', 'content': '解释一下二分查找算法'}]) # 程序正常结束 → atexit 自动触发 → 打印总结报表 "

输出(尾部):

2026-09-12 15:00:01 | INFO | deep_pilot.client | 调用留痕 | call_id=aaa111... | ... 2026-09-12 15:00:05 | INFO | deep_pilot.client | 调用留痕 | call_id=bbb222... | ... 2026-09-12 15:00:10 | INFO | deep_pilot.client | 调用留痕 | call_id=ccc333... | ... ====================================================================== DeepPilot 本次运行调用总结 ====================================================================== 调用次数: 3 ====================================================================== 指标 最小 最大 平均 合计 -------------------- ---------- ---------- ---------- ---------- 输入 token 6 14 9.3 28 输出 token 4 120 49.3 148 总 token 10 128 56.7 170 耗时(秒) 0.92 4.51 2.65 ====================================================================== 终止原因分布: {'stop': 3} 模型使用分布: {'deepseek-flash': 3} ====================================================================== [DeepPilot] 调用记录已存档: logs\records\calls_20260912_150010.json

三行调用日志之后,自动打印了总结报表。不需要手动调任何打印函数——import 了 call_tracker 的那一刻,退出总结就已经挂上了。

再来试一下 Ctrl+C 场景:

uv run python-c" from deep_pilot.client import client import time client.chat([{'role': 'user', 'content': '一些复杂的推理问题...'}]) # 这时候按 Ctrl+C print('这行不会执行') "# 按 Ctrl+C

输出(尾部):

2026-09-12 15:05:01 | INFO | deep_pilot.client | 调用留痕 | call_id=ddd444... | ... ====================================================================== DeepPilot 本次运行调用总结 ====================================================================== 调用次数: 1 ====================================================================== 指标 最小 最大 平均 合计 -------------------- ---------- ---------- ---------- ---------- 输入 token 18 18 18 18 输出 token 56 56 56 56 总 token 74 74 74 74 耗时(秒) 3.21 3.21 3.21 ====================================================================== 终止原因分布: {'stop': 1} 模型使用分布: {'deepseek-flash': 1} ====================================================================== [DeepPilot] 调用记录已存档: logs\records\calls_20260912_150510.json Traceback (most recent call last): File "<string>", line 5, in <module> KeyboardInterrupt

看到没——traceback 之前,总结报表已经打印出来了。崩了也不怕丢数据。


小结

  1. atexit是零侵入的退出通知机制:import 时注册一个函数,程序退出自动调用,不需要 try/finally 包全场。
  2. 量化报表四列:min/max/avg/total,覆盖输入输出 token 和耗时。分布统计(finish_reason + model)放在底部。
  3. Ctrl+C 也触发 atexit:KeyboardInterrupt 会走正常的栈展开流程,总结报表在 traceback 之前打出。
  4. 退出前dump_all()防重启丢失:内存中的记录序列化到logs/records/,下次启动可 reload。
  5. prune_traces()防止 trace 文件无限增长:保留最近 1000 次调用的 trace。
  6. DeepPilot v0.2 日志留痕体系完整闭环:控制台日志 + 文件日志 + CallRecord 留痕 + trace 三件套 + 退出总结。

下节预告

报表能看 min/max/avg 了,但每次都要眼睛看——数据多了,肉眼找规律太累。下一节玩个花活:把结构化日志喂给 DeepSeek 自己,用一句自然语言做数据分析——“分析调用平均速度、最大最小、长度与速度的关系”,让模型帮你挖规律。以后看日志就是跟 Agent 聊天。


如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连!
本系列持续更新中,关注不迷路~

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询