大家好,我是张大鹏,10 年全栈开发经验。做了这么多年开发,我越来越确信一个道理:日志系统不是"能跑就行"的基础设施,而是后期排障效率的基石。最近在重构如意Agent的日志系统,踩了不少坑,也做了一些取舍和对比,本文记录完整的方案设计和开发规划。
一、一个 bug 让我重新审视日志系统
事情是这样的。上周有学生反馈,如意Agent在运行一段时间后"卡住了",没有任何响应。按常规操作,我去翻日志------然后头大了。
日志文件 app.log 已经膨胀到 1.2GB,打开它需要等 10 秒。更崩溃的是,里面混着 [Info]、[WARN]、[ERROR]、[Debug] 等各种手写标签,格式参差不齐,没有时间戳、没有行号、没有模块名。我想查"最后一次 LLM 调用返回了什么",但日志里只有:
[WARN] stream timeout
什么请求超时了?超时发生在第几步?当时的 Token 数是多少?上下文是什么?通通没有。
更糟糕的是,我发现 llmcore.py 里有一段"好心"的代码:
python
def safeprint(*argv):
try: _oldprint(*argv)
except OSError: pass
print = safeprint
把 stdout 关闭时的 OSError 吞掉了------这意味着一旦出问题,连错误信息都可能被静默吞噬。这就像一个火灾报警器,本该在着火时响,结果被人手动关了静音。
排查了整整一个下午,最后发现是某个第三方 API 超时导致的连锁反应。如果日志系统稍微完善一点,这个问题 10 分钟就能定位。
所以我决定:对日志系统进行一次彻底的重构。
二、现状分析:三个硬伤
先梳理一下当前日志系统的问题:
| 问题 | 表现 | 根源 |
|---|---|---|
| 日志框架缺失 | 全项目上百处 print(),无统一 Logger |
从项目初期就"先跑起来再说" |
| 无结构化 | 标签格式随心所欲,无法过滤查询 | 没有设计数据模型 |
| 无轮转机制 | app.log 无限增长,可到 GB 级 |
纯文件 append 无管理 |
| 错误上下文丢失 | 异常时只有 "ERROR: xxx",无调用栈 | 没有异常捕获机制 |
| LLM 调用无链路 | 不知道每次用户操作对应哪些 LLM 调用 | 没有 trace_id 贯穿 |
| 错误被静默吞噬 | safeprint 吞掉 OSError |
猴子补丁副作用 |
本质上,这是一个从项目第一天就欠下的"技术债"------初期为了快速验证想法,用 print() 是最直接的方式。但当项目成长到需要维护和排障时,这套"原始日志"就成了瓶颈。
三、三种方案,三种取舍
针对上述问题,我设计了三个递进的方案,从轻量到重量,覆盖不同的投入产出比。
3.1 方案 A:轻量级统一重构(2-3 天)
思路 :用 Python 内置 logging 模块替换所有 print()。
python
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s:%(lineno)d %(message)s",
handlers=[
logging.handlers.RotatingFileHandler("logs/app.log", maxBytes=10_000_000, backupCount=5),
logging.StreamHandler(),
]
)
| 优点 | 缺点 |
|---|---|
| 零依赖、改动最小 | 非结构化,查询靠 grep |
| 标准库,无学习成本 | 跨组件追踪需手写 request_id |
3.2 方案 B:loguru 分层日志(3-5 天)
思路:引入 loguru 库,建立三层日志体系。
python
from loguru import logger
logger.add("logs/trace/{time:YYYY-MM-DD}_trace.log", level="DEBUG", rotation="1 day", retention=7)
logger.add("logs/app.log", level="INFO", rotation="10 MB", retention=10)
logger.add("logs/error/{time:YYYY-MM-DD}_error.log", level="ERROR", rotation="1 day", retention=30)
| 优点 | 缺点 |
|---|---|
| 配置极简,轮转内置 | 仍是非结构化文本 |
| 自动 traceback 捕获 | 无法按字段查询 |
| 彩色控制台,开发友好 | LLM 调用仍无链路关联 |
3.3 方案 C:结构化可观测性栈 ⭐(10-14 天)
思路:JSON 结构化日志 + SQLite 索引 + 调用链路追踪 + GUI 日志查看器。
| 优点 | 缺点 |
|---|---|
| 结构化可查询,告别 grep | 实现工作量最大 |
| 完整调用链路追踪,排障利器 | 代码量较大 |
| 每个错误带上下文快照 | 需要测试覆盖保障质量 |
| GUI 内嵌查看器,降低反馈门槛 |
我的选择:方案 C
我的决策逻辑很简单------如意Agent是一个 AI Agent 框架,它的核心运行逻辑涉及大量的 LLM 调用、工具调用、多轮推理。每次排查问题时,最大的痛点就是"无法复现上一次的操作链路"。 方案 C 的 trace_id 链路追踪恰好解决这个问题。
而且方案 C 并不是"全有或全无"------它的架构是分层的,前期可以先实现 JSON 结构化 + SQLite 存储 + tracer 链路,GUI 查看器可以放在第二期。
四、方案详解:7 个模块的架构设计
整个日志系统分为 7 个模块,模块之间的依赖关系如下:
core(数据模型)
├── config(配置 + 初始化)
│ ├── handlers(输出端)
│ └── storage(SQLite 存储)
├── tracer(链路追踪)── 依赖 core + storage
├── query(查询)── 依赖 storage
├── errors(异常快照)── 依赖 core
└── widgets(GUI 查看器)── 依赖 query
4.1 core --- 数据模型
所有日志的"通用语言":
python
@dataclass
class LogEntry:
timestamp: str # ISO 8601 时间戳
level: str # DEBUG / INFO / WARNING / ERROR / CRITICAL
logger: str # 模块名(app / llm / tool / turn)
message: str # 日志正文
module: str # 源文件名
line: int # 行号
function: str # 函数名
trace_id: str | None # 调用链路 ID
session_id: str | None # 会话 ID
extra: dict # 扩展字段
为什么 trace_id 是核心字段?因为这是连接"用户操作 → LLM 调用 → 工具执行 → 错误"的主键。
4.2 config --- 启动配置
整个日志系统的唯一入口:
python
@dataclass
class LogConfig:
log_dir: str = "logs"
level: str = "INFO"
json_format: bool = True
rotation_size: str = "10 MB"
rotation_count: int = 10
db_path: str = "logs/logs.db"
console: bool = True
def setup_logging(config: LogConfig | None = None) -> None:
"""初始化整个日志系统。应用启动时调用一次。"""
4.3 handlers --- 输出端
| Handler | 输出目标 | 格式 | 轮转 |
|---|---|---|---|
JsonFileHandler |
文件 | JSON | 按日轮转,保留 30 天 |
RotatingFileHandler |
文件 | JSON/文本 | 按大小轮转 |
ConsoleHandler |
stderr | 彩色文本 | 无 |
StorageHandler |
SQLite | LogEntry | 无(随 db 管理) |
4.4 storage --- SQLite 存储
结构化日志的核心优势------可查询:
sql
CREATE TABLE logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
level TEXT NOT NULL,
logger TEXT NOT NULL,
message TEXT NOT NULL,
module TEXT, line INTEGER, function TEXT,
trace_id TEXT, session_id TEXT,
extra TEXT, -- JSON
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_logs_level ON logs(level);
CREATE INDEX idx_logs_trace_id ON logs(trace_id);
CREATE INDEX idx_logs_timestamp ON logs(timestamp);
4.5 tracer --- 调用链路追踪
这是整个方案的核心亮点。每次用户操作,都会生成一棵"调用树":
Trace: sess_abc123
├── Turn 1: "帮我查天气"
│ ├── LLM Call: 分析意图 (300ms, 150 tokens)
│ ├── Tool Call: get_weather("北京") (200ms)
│ └── LLM Call: 生成回答 (500ms, 200 tokens)
└── Turn 2: "那明天呢"
└── ...
关键接口:
python
class TraceContext:
"""贯穿整个调用链路的上下文对象。"""
def start_span(self, span_type: str, input: Any = None) -> str:
"""开启一个新的调用跨度,返回 span_id。"""
def end_span(self, span_id: str, output: Any = None, error: str | None = None) -> None:
"""结束一个调用跨度,记录耗时和结果。"""
class LLMTracer:
"""LLM 调用追踪器,记录每次请求/响应的耗时和 Token 数。"""
def trace(self, session_id, messages, response, duration_ms, token_usage):
"""记录一次 LLM 调用的完整信息。"""
class ToolTracer:
"""工具执行追踪器,记录工具调用的输入输出。"""
4.6 query --- 查询 DSL
面向开发者的查询接口:
python
# 最近 1 小时的所有 ERROR
errors = LogQuery().level("ERROR").since("1h").all()
# 按 trace_id 追踪完整链路
trace = LogQuery().trace("trace_abc").with_spans()
# 按关键词搜索
results = LogQuery().keyword("timeout").since("24h").limit(50).all()
4.7 errors --- 异常快照
当异常发生时,不只记录错误消息,还自动保存完整上下文:
json
{
"timestamp": "2026-05-02T14:30:01.123",
"type": "TimeoutError",
"message": "LLM response timeout",
"traceback": "File 'llmcore.py', line 740...",
"locals": {"model": "claude-sonnet-4", "prompt_tokens": 4521},
"memory_mb": 245.3
}
4.8 widgets --- GUI 日志查看器(第二期)
在如意Agent的 ChatPanel 中嵌入第五个标签页,提供实时日志流和过滤功能。这一步可以放在第二期实现,不影响核心日志能力。
五、集成点:6 处埋点
日志系统需要注入到现有代码的关键路径中:
| 文件 | 注入点 | 追踪内容 |
|---|---|---|
agent_loop.py |
tool_before_callback / tool_after_callback |
工具执行前后 + 耗时 |
agent_loop.py |
turn_end_callback |
每轮推理结果 |
agent_loop.py |
agent_runner_loop |
LLM 调用包裹 |
agentmain.py |
put_task / run / abort |
任务生命周期 |
llmcore.py |
ToolClient.chat() |
LLM 请求/响应 + Token |
main.py |
ChatPanel 发送 / 接收 |
用户操作日志 |
六、TDD 开发规划:10 个 Task
这个项目目前零测试,所以我决定用 TDD 来保证质量------日志系统自己不靠谱,还怎么靠它排障?
Task 1:测试基础设施
bash
# pyproject.toml 添加
pip install pytest pytest-qt
配置 conftest.py,提供共享 fixtures:临时目录、mock 配置、测试用的 SQLite 实例。
Task 2:core 数据模型
写测试 → 验证失败 → 实现 → 验证通过
python
def test_log_entry_json_roundtrip():
entry = LogEntry(
timestamp="2026-05-02T12:00:00",
level="INFO", logger="test", message="hello",
module="test.py", line=42, function="foo",
trace_id="trace_1", session_id="sess_1",
extra={"key": "value"},
)
data = entry.to_dict()
restored = LogEntry.from_dict(data)
assert restored.timestamp == entry.timestamp
assert restored.extra["key"] == "value"
assert restored.trace_id == "trace_1"
Task 3:config 启动配置
测试各种配置路径:默认配置、自定义目录、无效路径降级行为。
Task 4:handlers 输出端
测试 JSON 文件写入、按日轮转、控制台彩色输出。
Task 5:storage SQLite 存储
测试建表、写入日志、按级别/时间/模块查询、按 trace_id 检索。
Task 6:tracer 链路追踪
测试嵌套 span 的父子关系、耗时计算、错误传播、trace_id 自动传递。
Task 7:query 查询 DSL
测试多条件组合查询、时间范围、分组统计。
Task 8:errors 异常快照
测试异常自动捕获、上下文保存、JSON 序列化、嵌套异常处理。
Task 9:widgets GUI 查看器
测试 LogViewer 组件渲染、过滤交互(用 pytest-qt 模拟点击)。
Task 10:集成联调
将 tracer 埋入 6 个注入点,替换 main.py 的 stdout 重定向,端到端验证。
七、为什么我坚持用 TDD
很多开发者觉得"日志系统写个测试差不多就行了"。但我吃过亏------
日志系统是一个横切关注点 ,它不直接产生业务价值,但一旦出问题,就会影响所有模块的排障能力。更可怕的是:错误捕获模块自身的 bug 是最难发现的------你都出错了,怎么知道错误捕获也坏了?
TDD 的三重保障:
- 行为锁定 --- 改 SQLite schema、改 JSON 格式,先跑测试
- 错误捕获的可靠性 --- ErrorSnapshot 本身不能抛异常,测试保证这一点
- 回归防护 --- 10 个人接手项目后,没人敢乱改有测试覆盖的模块
八、项目文件结构
logging/ # 新增:日志系统核心包
├── __init__.py # 统一导出
├── core.py # LogLevel / LogEntry 数据模型
├── config.py # 配置 + 初始化入口
├── handlers.py # 文件/控制台输出端
├── storage.py # SQLite 存储
├── tracer.py # 调用链路追踪
├── query.py # 查询 DSL
├── errors.py # 异常快照
└── widgets.py # GUI 日志查看器
tests/ # 新增:测试目录
├── conftest.py # 共享 fixtures
├── test_core.py
├── test_config.py
├── test_handlers.py
├── test_storage.py
├── test_tracer.py
├── test_query.py
├── test_errors.py
└── test_widgets.py
总结
| 维度 | 内容 |
|---|---|
| 核心思路 | JSON 结构化日志 + SQLite 索引 + TraceID 链路追踪 + GUI 查看器 |
| 核心模块 | 7 个(core / config / handlers / storage / tracer / query / errors) |
| 集成点 | 6 处埋点(agent_loop / agentmain / llmcore / main) |
| 开发方式 | TDD,10 个 Task,先写测试后实现 |
| 开发周期 | 10-14 天 |
| 二期规划 | GUI 日志查看器组件(非核心,可延后) |
参考资料:
- Python logging 官方文档
- loguru 文档
- SQLite 官方文档
- pytest 文档
- 如意Agent 项目源码(基于 MIT 协议二开)
作者 :张大鹏
日期:2026-05-02