教程:https://www.bilibili.com/video/BV1yjz5BLEoY

代码:https://github.com/shangxiang0907/HeiMa-AI-LLM-RAG-Agent-Dev

云开发平台:https://hzh.sealos.run

Agent智能体-07、[Agent项目]日志和路径工具开发

介绍:完成utils部分代码

在这里插入图片描述

【路径工具】

→提示词:@zhisaotong_agent/PROJECT_OVERVIEW.md 帮我实现文档utils中的path_tool.py代码,需要实现def get_project_root() -> strdef get_abs_path(relative_path: str) -> str两个函数,并在if __name__ == __main__ 中编写相应测试代码,代码需要满足强约束 / 安全性更高的场景

←AI回复

我先看下当前项目目录和是否已有 path_tool.py 文件,然后在其中实现两个函数,并加上简单的自测代码,确保能正确返回项目根路径和任意相对路径的绝对路径。

[3 tools called]

已实现内容说明

  • get_project_root():返回当前项目根目录(约定为 zhisaotong_agent/)的绝对路径,基于当前文件所在位置 zhisaotong_agent/utils/path_tool.py 向上一级目录推断。

  • get_abs_path(relative_path: str)

    • 若传入的是绝对路径,返回其规范化后的绝对路径;
    • 若传入的是相对路径,则以项目根目录为基准拼接,并返回规范化后的绝对路径。
  • if __name__ == "__main__": 中添加了简单自测代码:

    • 打印项目根目录;
    • 测试多种相对路径(如 PROJECT_OVERVIEW.mddatautils/path_tool.py./data/../PROJECT_OVERVIEW.md)转换为绝对路径;
    • 使用当前文件的绝对路径测试“绝对路径输入时应原样返回”的逻辑。

你可以在项目根目录下执行:

python -m zhisaotong_agent.utils.path_tool

查看测试输出,确认路径是否符合预期。

代码:path_tool.py

from pathlib import Path


def get_project_root() -> str:
    """
    获取项目根目录的绝对路径。

    约定:本文件位于 `zhisaotong_agent/utils/path_tool.py`,
    项目根目录为其上一级目录 `zhisaotong_agent/`。
    """
    return str(Path(__file__).resolve().parent.parent)


def get_abs_path(relative_path: str) -> str:
    """
    将“项目内相对路径”转换为基于项目根目录的绝对路径。

    要求:
    - 只接受相对路径;
    - 如果传入的是绝对路径,则抛出 ValueError,避免误用掩盖问题。
    """
    p = Path(relative_path)
    if p.is_absolute():
        raise ValueError(f"get_abs_path 仅接受相对路径,实际收到绝对路径: {relative_path!r}")

    root = Path(get_project_root())
    return str((root / p).resolve())


if __name__ == "__main__":
    # 简单自测代码
    print("项目根目录:", get_project_root())

    # 1. 测试相对路径转换
    rel_examples = [
        "PROJECT_OVERVIEW.md",          # 根目录下已存在的文件(根据当前项目结构)
        "data",                         # 根目录下已有的 data 目录
        "utils/path_tool.py",           # 本文件自身
        "./data/../PROJECT_OVERVIEW.md" # 含有 . 和 .. 的相对路径
    ]
    for rel in rel_examples:
        # 这里的 !r 表示使用 repr() 的形式显示变量值(带引号、便于调试区分空字符串等)
        print(f"relative: {rel!r} -> abs: {get_abs_path(rel)!r}")

    # 2. 测试绝对路径误用场景(应抛出异常)
    current_file_abs = str(Path(__file__).resolve())
    try:
        print("absolute input ->", get_abs_path(current_file_abs))
    except ValueError as e:
        print("absolute input error:", e)


执行

python3 zhisaotong_agent/utils/path_tool.py

结果

项目根目录: /home/devbox/project/zhisaotong_agent
relative: 'PROJECT_OVERVIEW.md' -> abs: '/home/devbox/project/zhisaotong_agent/PROJECT_OVERVIEW.md'
relative: 'data' -> abs: '/home/devbox/project/zhisaotong_agent/data'
relative: 'utils/path_tool.py' -> abs: '/home/devbox/project/zhisaotong_agent/utils/path_tool.py'
relative: './data/../PROJECT_OVERVIEW.md' -> abs: '/home/devbox/project/zhisaotong_agent/PROJECT_OVERVIEW.md'
absolute input error: get_abs_path 仅接受相对路径,实际收到绝对路径: '/home/devbox/project/zhisaotong_agent/utils/path_tool.py'

【日志工具】

→提示词:有必要实现这个日志工具吗,是否符合最佳实践?

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

←AI回复

总结先说结论

  • 有没有“必要”:不绝对必须,但这样封装一个统一的 get_logger 模块在中大型项目里是很常见、也算是合理的实践
  • 是否最佳实践:整体思路是对的(统一格式、控制台+文件 handler、目录管理),但还可以按官方推荐方式再优化一下。

这个模块在做什么 & 什么时候值得保留

  • 统一日志根目录LOG_ROOT = get_abs_path("logs") + os.makedirs(..., exist_ok=True)
  • 统一格式DEFAULT_LOG_FORMAT = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s')
  • 统一入口函数:通过 get_logger(name, console_level, file_level, log_file) 控制:
    • 控制台日志级别
    • 文件日志级别
    • 日志输出文件路径(默认按日期生成)

适用场景

  • 项目中很多模块都要打日志,希望格式、路径、级别行为统一
  • 不想在每个文件里都重复配置 handler;
    → 这种情况下保留这个模块是有价值的

当前实现里值得注意/可改进的点

  • 避免重复添加 handler:你已经写了:

    if logger.handlers:
        return logger
    

    这是对的,可以避免多次调用 get_logger 时重复添加 StreamHandler/FileHandler

  • 建议按模块名获取 logger

    每个业务模块里建议这样用:

    logger = get_logger(__name__)
    

    而不是都叫 "agent",这样方便按模块过滤和定位问题。

  • 文件日志建议用轮转 handler

    如果是长期跑的服务,推荐改成:

    from logging.handlers import TimedRotatingFileHandler
    
    file_handler = TimedRotatingFileHandler(
        log_file,
        when="D",      # 每天一个文件
        interval=1,
        backupCount=7, # 保留 7 天
        encoding="utf-8",
    )
    

    这样比单纯的 FileHandler 更符合生产实践。

  • 集中配置 vs. 自写工厂函数

    更“官方”的一种做法是使用 logging.config.dictConfig 做集中配置;
    你现在这种写一个 get_logger 工厂函数,也是社区里非常常见的用法,不算反模式。


推荐可以演进成的一个小模板(示意)

# logger_handler.py
import logging
import os
from datetime import datetime
from logging.handlers import TimedRotatingFileHandler
from utils.path_tool import get_abs_path

LOG_ROOT = get_abs_path("logs")
os.makedirs(LOG_ROOT, exist_ok=True)

DEFAULT_LOG_FORMAT = logging.Formatter(
    "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s"
)

def get_logger(
    name: str = __name__,
    console_level: int = logging.INFO,
    file_level: int = logging.DEBUG,
    log_file: str | None = None,
) -> logging.Logger:
    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)

    if logger.handlers:
        return logger

    console_handler = logging.StreamHandler()
    console_handler.setLevel(console_level)
    console_handler.setFormatter(DEFAULT_LOG_FORMAT)
    logger.addHandler(console_handler)

    if not log_file:
        log_file = os.path.join(
            LOG_ROOT, f"{name}_{datetime.now().strftime('%Y%m%d')}.log"
        )

    file_handler = TimedRotatingFileHandler(
        log_file, when="D", interval=1, backupCount=7, encoding="utf-8"
    )
    file_handler.setLevel(file_level)
    file_handler.setFormatter(DEFAULT_LOG_FORMAT)
    logger.addHandler(file_handler)

    return logger

结论再强调一遍

  • 保留这个模块是有意义的,尤其是你希望项目日志行为统一时。
  • 不算“反模式”,整体方向 OK,只是可以:
    • __name__ 作为 logger 名称;
    • 用轮转 handler;
    • 明确在项目里约定:所有模块只通过这个 get_logger 获取 logger。

如果你愿意,我可以帮你把当前项目里的日志调用方式梳理一下,给出一份适配你项目结构的最终版本。

→提示词:@zhisaotong_agent/PROJECT_OVERVIEW.md 根据你以上建议,帮我实现日志工具

←AI回复

我先在项目里创建/补全 utils/logger_handler.py,实现一个统一的 get_logger,支持控制台+按天轮转文件日志,并结合你现有的 path_tool.get_abs_path。然后我会简单检查一下是否有明显的语法/导入问题,并给你说明如何在各模块里使用。

[3 tools called]

日志工具已实现

根据 PROJECT_OVERVIEW.md 和之前的建议,我已经在项目里实现了统一的日志工具 utils/logger_handler.py,核心特性如下:

  • 统一日志目录

    • 使用 get_abs_path("logs"),在项目根目录下创建 logs/ 目录(不存在则自动创建)。
  • 统一格式

    • 全局格式为:"%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s"
  • 统一获取入口:get_logger

    • 推荐在各模块中这样用:
      from utils.logger_handler import get_logger
      
      logger = get_logger(__name__)
      
    • 默认行为:
      • 控制台:INFO 级别;
      • 日志文件:DEBUG 级别;
      • 日志文件名:logs/<模块名>_YYYYMMDD.log(按天区分)。
  • 文件日志按时间轮转(符合生产最佳实践)

    • 使用 TimedRotatingFileHandler,默认配置:
      • when="D":按天轮转;
      • interval=1:每天一个新文件;
      • backupCount=7:保留最近 7 天日志;
      • encoding="utf-8"
    • 若多次调用 get_logger,会检测 logger.handlers不会重复添加 handler
  • 可定制参数

    • get_logger(name=__name__, console_level=logging.INFO, file_level=logging.DEBUG, log_file=None, when="D", interval=1, backup_count=7)
    • 若你传入 log_file 且是相对路径,会自动通过 get_abs_path 转成基于项目根目录的绝对路径;也支持直接传绝对路径。

如果你愿意,我可以顺手在例如 app.py 或未来的 rag_service.py 里给你加一两个使用示例(包括一些典型的 info / warning / error 调用),方便后续直接照着用。

代码:logger_handler.py

"""
项目统一日志工具。

设计目标:
- 所有模块通过 `get_logger(__name__)` 获取 logger,保证格式与输出行为一致;
- 默认输出到控制台 + 按天轮转的日志文件(保留最近若干天);
- 日志文件统一存放在项目根目录下的 `logs/` 目录。
"""

from __future__ import annotations

import logging
import os
from datetime import datetime
from logging.handlers import TimedRotatingFileHandler
from typing import Optional

from .path_tool import get_abs_path

# 日志根目录:项目根目录下的 logs 目录
LOG_ROOT = get_abs_path("logs")
os.makedirs(LOG_ROOT, exist_ok=True)

# 默认日志格式:时间 - logger 名称 - 级别 - 文件名:行号 - 消息
DEFAULT_LOG_FORMATTER = logging.Formatter(
    # 时间 - logger 名称 - 级别 - 文件名:行号 - 消息
    # 2026-03-04 13:31:41,240 - zhisaotong_agent.utils.logger_handler - DEBUG - test.py:3 - 这是一条 DEBUG 日志(默认只写入文件)。
    "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s" 
)


def _build_log_file_path(name: str) -> str:
    """
    根据 logger 名称和当前日期生成日志文件路径。

    例如:logs/agent_20260304.log
    """
    safe_name = name.replace(":", "_").replace("/", "_")
    filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d')}.log"
    return os.path.join(LOG_ROOT, filename)


def get_logger(
    name: str = __name__,
    console_level: int = logging.INFO,
    file_level: int = logging.DEBUG,
    log_file: Optional[str] = None,
    when: str = "D",
    interval: int = 1,       # 多久轮转一次日志文件(配合 when 使用),默认 1 天
    backup_count: int = 7,   # 最多保留多少个历史日志文件,超过会自动删除旧文件
) -> logging.Logger:
    """
    获取一个已经按项目标准配置好的 logger。

    参数说明:
    - name: logger 名称。实际项目中推荐在各业务模块里显式写 `get_logger(__name__)`,
      这样每个模块都会拿到带有自己模块名(如 `zhisaotong_agent.rag.rag_service`)的 logger,
      便于在日志中按模块区分来源;如果不传则使用本模块自己的名称。
    - console_level: 控制台输出日志级别(默认 INFO),控制“在终端里能看到多详细的日志”。
    - file_level: 文件日志级别(默认 DEBUG);
    - log_file: 指定日志文件的绝对路径;如果为 None,则自动根据 name+日期生成;
    - when / interval / backup_count: TimedRotatingFileHandler 的轮转策略:
        - when: "S", "M", "H", "D", "W0"-"W6", "midnight" 等;
        - interval: 间隔单位数量;
        - backup_count: 保留的历史文件数量。

    说明:
    - 为避免重复添加 handler,如果 logger 已经配置过 handler,则直接返回。
    """
    logger = logging.getLogger(name)
    # 顶层 logger 级别设置为 DEBUG,具体输出由各 handler 控制
    logger.setLevel(logging.DEBUG)

    # 如果已经配置过 handler,直接返回,避免重复添加
    if logger.handlers:
        return logger

    # 1. 控制台 Handler
    console_handler = logging.StreamHandler()
    console_handler.setLevel(console_level)
    console_handler.setFormatter(DEFAULT_LOG_FORMATTER)
    logger.addHandler(console_handler)

    # 2. 文件 Handler(按时间轮转)
    if log_file is None:
        log_file = _build_log_file_path(name)

    # 如果传入了相对路径,自动转为基于项目根目录的绝对路径
    if not os.path.isabs(log_file):
        log_file = get_abs_path(log_file)

    file_handler = TimedRotatingFileHandler(
        filename=log_file,
        when=when,
        interval=interval,
        backupCount=backup_count,
        encoding="utf-8",
    )
    file_handler.setLevel(file_level)
    file_handler.setFormatter(DEFAULT_LOG_FORMATTER)
    logger.addHandler(file_handler)

    return logger


__all__ = ["get_logger", "LOG_ROOT"]  # 明确模块对外暴露的公共接口,配合 `from utils.logger_handler import *` 使用


if __name__ == "__main__":
    """
    简单自测代码:
    - 在控制台打印不同级别日志;
    - 在 `logs/` 目录下生成对应的日志文件。
    运行方式(二选一):
        # 在项目根目录下
        python -m zhisaotong_agent.utils.logger_handler

        # 或在包根目录 zhisaotong_agent/ 下
        python -m utils.logger_handler
    """
    # test_logger = get_logger("logger_handler_demo")
    test_logger = get_logger()
    test_logger.debug("这是一条 DEBUG 日志(默认只写入文件)。")
    test_logger.info("这是一条 INFO 日志。")
    test_logger.warning("这是一条 WARNING 日志。")
    test_logger.error("这是一条 ERROR 日志。")
    test_logger.critical("这是一条 CRITICAL 日志。")


运行

cd /home/devbox/project
python -m zhisaotong_agent.utils.logger_handler

结果

2026-03-04 18:18:45,614 - __main__ - INFO - logger_handler.py:124 - 这是一条 INFO 日志。
2026-03-04 18:18:45,614 - __main__ - WARNING - logger_handler.py:125 - 这是一条 WARNING 日志。
2026-03-04 18:18:45,614 - __main__ - ERROR - logger_handler.py:126 - 这是一条 ERROR 日志。
2026-03-04 18:18:45,614 - __main__ - CRITICAL - logger_handler.py:127 - 这是一条 CRITICAL 日志。

注意因为是直接执行,所以name显示__main__,如果是被其他模块作为包导入,并且以__name__作为参数值传入,才会显示日志打印者的模块路径

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐