从一句报错,回到一条执行路径
已有 Agent Loop 笔记记录过一个缩进问题:程序提示达到最大步数,实际却只跑了一轮。增加 Step 日志后,才看见第二轮没有发生。
9 月 17 日的进度进一步涉及 run_id、工具名、错误类型和耗时。这里用配套离线练习演示这些字段如何协作,日志不包含历史系统的私人会话。
不同标识回答不同问题
| 字段 | 本练习中的含义 |
|---|---|
| run_id | 一次运行,主程序每次启动重新生成 |
| tool_call_id | 一次工具调用;该调用重试时保持不变 |
| step | 所处的决策步骤,由调用者传入 |
| attempt | 当前调用的第几次执行尝试 |
| tool_name | 注册表中的工具名;未知名称统一记为 unknown |
| success / error_type | 这次尝试的结果与错误类别 |
| duration_ms | 这次尝试的耗时,不包含后续退避等待 |
完整 Agent 中还可能有 conversation_id,用来定位会话。这个离线练习没有聊天历史,所以不造一个数据库会话来填充它。
一条日志怎样读
下面是字段示意,不是历史项目运行证据;实际运行会生成不同 ID 和耗时。
{
"event": "tool_attempt",
"level": "INFO",
"run_id": "run-demo",
"tool_call_id": "call-demo",
"step": 1,
"tool_name": "multiply",
"attempt": 2,
"success": false,
"error_type": "connection",
"duration_ms": 1.2
}它表达的是这个调用的第二次尝试遇到连接问题,不能直接判断整个运行最终失败。还要看同一调用后面是否成功、是否达到次数上限,以及上层怎样处理结果。
输入校验失败时,程序没有执行工具,日志的 attempt 为 0;外部取消时 error_type 为 cancelled,并继续传播取消。这些约定让阅读者知道字段的含义。
配置与业务分别负责什么
示例为了便于下载,将函数放在一个文件中,但职责分开:make_logger 配置输出与格式,EventFormatter 生成白名单 JSON,execute 在执行边界发出事件,close_logger 释放处理器。
继续拆成工程目录时,可以把格式与输出配置放到 logging_config.py,把模型放到 contracts.py,把 execute 放到 executor.py。这里只展示建议的拆分方向,不声称原项目已经完成该目录结构。
Python Logging Cookbook介绍如何给日志补充上下文。此练习显式传入运行标识,暂不引入全局状态;未来有并发请求时,不能让一个可变全局 run_id 被不同任务覆盖。
日志保留多少,记录什么
默认只输出到控制台。要演示文件输出,可以在主程序中把 logger 的创建改为 make_logger(Path("exercise.log"))。配套测试会在临时目录验证轮转,测试结束后清理该目录。
文件处理器使用 RotatingFileHandler,默认 maxBytes=1000000、backupCount=2。它按大小轮转并保留有限备份;单条记录可能超过设定大小,因此不要把 maxBytes 当作精确磁盘配额。机制见 Python 日志处理器文档。
示例只输出允许的字段,不记录原始参数、提示词、模型回答、异常消息或密钥。测试会检查一些敏感示例字符串未出现在日志中。这个白名单设计仍有边界:调用者不能把私人内容塞进 run_id 或 tool_call_id;真实项目还需要访问控制、保留时间和脱敏规则。
本练习的轮转配置面向单进程演示,不覆盖多个进程同时写同一文件的协调问题。
日志与 Trace 还隔着哪些工作
几条 JSON 日志能帮助定位调用,但并未自动形成完整的持久化 Trace。还需要决定哪些事件进入数据库、怎样关联父子步骤、如何查询、谁能查看以及什么时候清理。
本轮只验证离线日志字段、失败记录和文件轮转。数据库持久化、线上采样、服务级指标和生产告警继续作为后续学习内容。回到最新学习记录,可以区分本轮配套练习与个人学习进度。
来源
依据既有 Agent Loop 排错记录与《Agent 学到哪里了---9.17》,结合上方 Python 官方文档整理。可运行程序与测试见工具契约专题。资料核对日期:2026-09-21。
试着做一个小练习
运行连接失败后恢复的测试,用相同 tool_call_id 串起三次尝试;不要把输入原文加入日志。