如意Agent日志系统重构:从 print() 大海捞针到结构化可观测性栈

大家好,我是张大鹏,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 的三重保障:

  1. 行为锁定 --- 改 SQLite schema、改 JSON 格式,先跑测试
  2. 错误捕获的可靠性 --- ErrorSnapshot 本身不能抛异常,测试保证这一点
  3. 回归防护 --- 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 日志查看器组件(非核心,可延后)

参考资料


作者 :张大鹏
日期:2026-05-02

相关推荐
jieyucx1 小时前
Go 零基础数据结构:顺序表(像「排抽屉」一样学增删改查)
java·数据结构·golang
曦夜日长1 小时前
C++ STL容器string(一):string的变量细节、默认函数的认识以及常用接口的使用
java·开发语言·c++
北山有鸟1 小时前
IS_ERR 判断出错后,再用 PTR_ERR 把它强制转换回 int 型的错误码作为函数的返回值。
java·开发语言
We་ct1 小时前
LeetCode 97. 交错字符串:动态规划详解
前端·算法·leetcode·typescript·动态规划
phltxy1 小时前
深度解析:Spring Cloud Gateway 从入门到实战
java·开发语言
HAPPY酷2 小时前
从Public到Private:UE5 C++类创建路径差异全解析
java·c++·ue5
Chengbei112 小时前
轻量化 Web 安全日志分析神器 星川智盾日志威胁检测、地理溯源、MITRE ATT&CK 映射,支持 Windows/macOS/Linux
前端·人工智能·安全·web安全·macos·系统安全·安全架构
风流 少年2 小时前
Python Web框架:FastAPI
前端·python·fastapi
许彰午2 小时前
CacheSQL(一):手写数据库的工程化重生
java·数据库·缓存