在 AI 辅助编程越来越普遍之后,一个很容易被忽略的问题正在出现:

AI 很会写日志,但不一定会写“有用的日志”。

让 AI 实现一个功能,它很可能顺手生成:

logger.info("start")
logger.info("processing...")
logger.info("data loaded")
logger.error("failed")

单独看似乎没有问题,但项目越来越大之后,你会发现日志开始变成这样:

start
processing...
loading data...
done
request failed
retry...
success

真正出了故障,却很难回答几个最基本的问题:

  • 哪个请求出的错?
  • 哪个模块出的错?
  • 哪一步出的错?

问题并不是“日志太少”,而往往是:

日志很多,但没有体系。

因此,和异常处理、依赖注入、接口规范一样,日志也应该成为项目架构的一部分,而不是让开发者或者 AI 自由发挥。


📌 技术名片

Logging Architecture(日志体系架构)

指通过统一的日志组件、格式、等级、上下文字段和链路标识,对整个系统的运行记录进行规范化管理。

它解决的并不仅仅是:

“怎么把一行文字打印出来?”

而是:

系统发生问题以后,我们能不能快速还原当时发生了什么。

一个比较完整的日志体系通常包含:

统一日志入口
统一日志格式
日志等级规范
请求上下文
链路标识 request_id
文件 / JSON / 日志平台
检索、排错、审计、性能分析

其中 request_id 可以理解为:

一次请求的身份证号码。

只要整个调用过程都带着这个号码,就可以把散落在不同模块里的日志重新串起来。


一、为什么不能让 AI 自由打印日志?

假设我们让 AI 实现三个模块。

AI 可能分别写成:

logger.info("user created")
logger.info("Create agent success")
logger.info(f"knowledge base {kb_id} loaded")

甚至还有:

print("start")

从单个文件来看都能工作。

但是整个项目组合起来,就会出现几个典型问题。

1. 格式不统一

有的日志写:

create user success

有的写:

User created successfully

有的甚至:

ok!!!

机器几乎无法进行稳定分析。

2. 日志等级混乱

例如 AI 很容易写:

logger.error("User not found")

但“用户不存在”很多情况下只是正常业务结果,不应该记为系统错误。

相反:

logger.info(f"Database connection failed: {e}")

数据库连接失败却被记成普通信息。

最终就会变成:

ERROR ERROR ERROR ERROR ERROR

真正的重要错误反而被淹没。

3. 同一个请求的日志无法关联

一次聊天请求可能经过:

HTTP API
ChatService
AgentRunner
Tool
Knowledge Base
LLM

每一层都有日志。

如果没有统一链路编号,看到的只是几十条互不相关的信息。


二、日志规则

1. 统一日志入口

日志体系最重要的第一步并不是设计格式,而是:

整个项目只有一种标准日志使用方式。

以 miniagent 为例,项目把核心日志配置集中在:

backend/app/core/logger_config.py

并提供统一方法:

def get_logger(name: str = None):
    if name:
        return logger.bind(name=name)
    return logger

业务模块统一使用:

from app.core.logger_config import get_logger

logger = get_logger(__name__)

而不是有的地方:

import logging

有的地方:

from loguru import logger

还有地方:

print(...)

miniagent 当前使用 Loguru 作为日志库,并通过统一配置管理控制台、文件、错误日志和调试日志。

这样做有一个非常重要的价值:

AI 不需要每次重新设计日志体系,只需要遵守项目已经定义好的入口。


2. 统一日志格式

一个好的日志格式至少应该回答:

什么时候?
什么等级?
哪个请求?
哪个模块?
哪个函数?
发生了什么?

miniagent 当前控制台日志格式大致为:

时间 | 等级 | request_id | 模块:函数:行号 | 消息

真实配置类似:

format=(
    "{time:YYYY-MM-DD HH:mm:ss.SSS} | "
    "{level: <8} | "
    "{extra[request_id]} | "
    "{extra[name]}:{function}:{line} | "
    "{message}"
)

最终看到的日志就会类似:

2026-08-12 18:21:31.426 | INFO     |
2b91c7... |
app.services.chat:send_message:126 |
Agent execution started

这条日志已经包含了几个核心维度:

时间
日志等级
request_id
模块
函数
代码行
事件

于是排查问题不再依赖“猜”。


3. 明确日志等级

常见日志等级包括:

DEBUG — 调试信息

DEBUGDebug(调试)

主要用于开发阶段观察内部状态,例如:

logger.debug(f"Retrieved {len(chunks)} chunks")

适合记录:

中间变量
检索结果数量
路由选择结果
内部执行步骤
模型参数

生产环境通常不会大量输出。

INFO — 正常运行信息

INFOInformation(信息)

表示系统正在正常执行重要动作:

logger.info("Agent execution started")

例如:

应用启动
用户登录
任务开始
Agent 调用完成
知识库加载完成
请求完成

WARNING — 警告

WARNING 表示:

系统还能继续运行,但是出现了值得关注的问题。

例如:

logger.warning("Knowledge base returned no result")

或者业务异常:

logger.warning(f"NotFoundError: {exc}")

miniagent 的全局异常处理目前就把 NotFoundErrorAlreadyExistsError 等可预期业务异常记录为 WARNING,而不是直接当作系统崩溃处理。

这是非常重要的日志分级思想:

业务失败,不等于系统故障。

ERROR — 系统错误

ERROR 表示:

某个功能已经无法正常完成。

例如:

logger.error("Database initialization failed")

适合:

数据库连接失败
LLM 调用失败
文件读取失败
关键服务不可用

CRITICAL — 严重故障

CRITICALCritical(严重、关键)

用于:

系统无法启动
核心数据库损坏
关键配置缺失
核心基础设施不可用

这类日志通常意味着:

系统可能已经无法继续提供服务。


三、不要“什么都打日志”

日志体系里还有一条很重要的原则:

不是执行过的每一步,都值得成为日志。

例如:

logger.info("enter function")
logger.info("get user")
logger.info("check user")
logger.info("start processing")
logger.info("processing...")
logger.info("return result")

这种日志最大的作用通常只是:

制造噪声。

更好的方法是记录“事件”。

例如:

logger.info(
    f"Agent execution started: agent_id={agent_id}"
)

以及:

logger.info(
    f"Agent execution completed: agent_id={agent_id}, "
    f"duration={duration:.3f}s"
)

日志应该重点记录:

状态变化
关键决策
外部调用
异常
性能指标
安全事件
业务审计事件

而不是:

我运行到第 17 行了。

四、真正关键的一步:加入链路标识

随着系统变复杂,一个 HTTP 请求可能经历几十个函数。

例如 miniagent:

POST /chat
Chat API
ChatService
AgentRunner
     ├── LLM
     ├── Knowledge Base
     └── Web Search

如果每个模块单独打印日志,很难知道哪些日志属于同一个请求。

解决方法就是:

request_id

即:

Request Identifier(请求标识符)

miniagent 在 HTTP 中间件中为每个请求生成:

request_id = str(uuid4())

其中 UUID 是:

Universally Unique Identifier(通用唯一标识符)

然后写入:

request.state.request_id = request_id

假设这次请求得到:

request_id = 742fd2b1...

接下来所有日志都带上:

742fd2b1...

于是:

742fd2b1 | HTTP request received
742fd2b1 | Agent started
742fd2b1 | KB retrieval started
742fd2b1 | KB returned 6 chunks
742fd2b1 | LLM started
742fd2b1 | Agent completed
742fd2b1 | HTTP 200

瞬间就形成了一条完整调用链。


五、miniagent 日志体系:从一次请求到完整可追踪链路

下面就是 miniagent 当前日志架构的整体关系:

flowchart TB A["HTTP Request
客户端请求"] --> B["FastAPI Middleware
请求日志中间件"] B --> C["Generate request_id
生成唯一请求标识"] C --> D["Logging Context
日志上下文"] C --> E["Audit Context
审计上下文"] D --> F["Application Code
业务代码"] F --> F1["API / Service"] F --> F2["AgentRunner"] F --> F3["Knowledge Base / RAG"] F --> F4["LLM / Tool"] F1 --> G["get_logger(__name__)"] F2 --> G F3 --> G F4 --> G H["Third-party Libraries
第三方组件"] --> H1["Uvicorn"] H --> H2["SQLAlchemy"] H --> H3["ChromaDB"] H1 --> I["Python logging"] H2 --> I H3 --> I I --> J["InterceptHandler
标准日志桥接"] G --> K["Loguru
统一日志中心"] J --> K D -. "request_id 自动注入" .-> K K --> L["Console
控制台"] K --> M["miniagent_YYYY-MM-DD.log
INFO 及以上"] K --> N["error.log
ERROR 及以上"] K --> O["debug.log
DEBUG / 开发环境"] K --> P["Structured JSON Log
JSON 结构化日志"] E --> Q["Audit Log DB
数据库审计记录"] C -. "同一个 request_id" .-> Q B --> R["HTTP Response"] R --> S["X-Request-ID
返回链路标识"] R --> T["X-Process-Time
返回请求耗时"]

这张图里最重要的其实是三条主线。

第一条是:

HTTP Request
Middleware
request_id
Logging Context
业务代码
Loguru
Console / File / JSON

第二条是:

Uvicorn / SQLAlchemy / ChromaDB
      Python logging
     InterceptHandler
          Loguru

第三条是:

              request_id
             /          \
            ↓            ↓
      应用运行日志      Audit Context
                     Audit Log DB

也就是说,miniagent 并不是简单地“统一打印日志”,而是把:

运行日志
第三方日志
请求链路
审计记录
性能耗时

放进同一套关联体系里。


1. 如何自动传播 request_id

如果每个函数都这样传:

service.run(request_id=request_id)

当然可以。

但随着调用层级增加,代码会变得非常难看。

miniagent 使用 Loguru 的上下文化机制:

with logger.contextualize(request_id=request_id):
    response = await call_next(request)

在这个请求作用域里面执行的日志,会自动获得对应的 request_id

于是业务代码仍然可以简单写:

logger.info("Agent started")

最终输出时却自动变成:

742fd2b1 | Agent started

这就是:

Context Logging(上下文日志)

它的核心思想是:

业务代码
不负责反复传 request_id


基础设施层
自动注入上下文

这是一种非常值得让 AI 遵守的架构边界。


2. 把 request_id 返回给前端

链路标识不仅服务器自己使用。

miniagent 还会:

response.headers["X-Request-ID"] = request_id

HTTP Header 即:

Hypertext Transfer Protocol Header(超文本传输协议头)

于是如果前端用户报告:

聊天接口报错了。

开发人员不必再问:

大概几点?
哪个用户?
发了什么?

前端只需要提供:

X-Request-ID: 742fd2b1...

服务端直接搜索:

742fd2b1

就可以还原整个请求过程。

这就是日志从:

“打印文字”

升级成:

故障追踪系统。


3. 链路日志还可以和审计日志打通

miniagent 更进一步。

审计上下文中也保存:

@dataclass
class AuditRequestContext:
    request_id: str
    method: str
    path: str
    ip_address: Optional[str] = None
    user_id: Optional[int] = None
    username: Optional[str] = None

并且 HTTP 请求创建审计上下文时,会直接复用同一个:

request_id=request_id

于是:

应用日志
     │ request_id
742fd2b1

数据库审计记录
     │ request_id
742fd2b1

两套系统就连接起来了。

你不仅能知道:

程序出了什么问题

还可以知道:

是谁
什么时候
通过哪个接口
执行了什么操作
最终结果如何

这对于后台管理系统尤其重要。


4. 第三方库日志也必须收编

实际项目还有一个常见问题。

自己的代码使用 Loguru:

logger.info(...)

但是很多第三方库使用 Python 标准日志模块:

logging.getLogger(...)

例如:

Uvicorn
SQLAlchemy
ChromaDB

如果不统一处理,就会看到:

2026-08-12 | INFO | miniagent ...
INFO: uvicorn request ...
sqlalchemy.engine INFO ...

日志格式再次碎片化。

miniagent 为此实现了:

class InterceptHandler(logging.Handler):

把 Python 标准 logging 日志统一转发到 Loguru。

然后:

logging.basicConfig(
    handlers=[InterceptHandler()],
    level=0,
    force=True
)

并专门接管:

uvicorn
uvicorn.error
uvicorn.access

因此最终形成:

业务代码 ──────┐
FastAPI ──────┤
Uvicorn ──────┤
              ├──→ Loguru
SQLAlchemy ───┤
ChromaDB ─────┘

所有日志使用同一种格式、同一种文件策略和同一种链路机制。


5. 不同日志应该进入不同文件

把所有日志全部塞进:

app.log

虽然简单,但并不适合长期运行。

miniagent 当前按用途进行了拆分。

普通日志

miniagent_2026-08-12.log

记录:

INFO
WARNING
ERROR
CRITICAL

每天轮转一次,并保留 30 天。

错误日志

error.log

只记录:

ERROR
CRITICAL

文件达到 10 MB 时轮转,并保存最近的历史文件。

这样排查生产故障时可以直接:

error.log

而不用从几十万条正常请求里寻找异常。

调试日志

开发模式下:

debug.log

记录更详细的:

DEBUG

信息。

这避免了生产环境长期产生大量无意义调试日志。


6. 结构化日志:为机器准备日志

传统日志是给人看的:

2026-08-12 18:20:31 | INFO | 742fd2b1 | Agent completed

但如果未来需要接入日志分析平台,更适合使用:

{
  "time": "2026-08-12T18:20:31",
  "level": "INFO",
  "request_id": "742fd2b1",
  "module": "agent_runner",
  "event": "agent_completed",
  "duration_ms": 842
}

这种方式叫:

Structured Logging(结构化日志)

最常见格式是 JSON

JSON 全称:

JavaScript Object Notation(JavaScript 对象表示法)

它最大的好处不是“看起来高级”,而是方便机器进行:搜索过滤统计

miniagent 已经支持通过配置:

JSON_LOG_ENABLED=true

生成结构化日志文件,并使用 Loguru 的:

serialize=True

输出 JSON 日志。

之后就可以更方便地接入:

ELK
Loki
Grafana

其中:

ELK 是:

Elasticsearch + Logstash + Kibana

分别负责日志存储搜索、采集处理和可视化。

Grafana 则常用于:

监控指标和日志可视化。

对于 miniagent 当前阶段来说,本地文件已经足够;当系统部署规模扩大,再接集中式日志平台即可。

这就是:

先把架构边界设计好,而不是一开始就堆复杂基础设施。


六、给 AI 的日志规则应该怎么写?

如果我们已经建立日志体系,就应该进一步把规则告诉 AI。

例如在项目规则里加入:

## 日志记录规则

1. 运行时日志记录请勿使用 `print()`。
2. 应用程序日志请使用 `get_logger(__name__)`。
3. 业务模块内部请勿创建自定义日志配置。
4. 内部诊断信息请使用 `DEBUG`。
5. 重要业务事件请使用 `INFO`。
6. 可恢复或预期异常情况请使用 `WARNING`。
7. 操作失败请使用 `ERROR`。
8. 需要堆栈跟踪时请使用 `logger.exception()`。
9. 请勿记录密码、令牌、API 密钥或其他敏感数据。
10. 业务服务内部请勿手动生成 `request_id`。
11. 请求范围的日志会自动继承中间件的 `request_id`。
12. 优先使用有意义的事件,而不是过程消息,例如:“start”、“processing”或“done”。

翻译成架构要求就是:

禁止 print
统一 Logger
统一等级
统一格式
自动携带 request_id
禁止敏感信息
只记录有价值事件

以后 AI 写新功能时,就不应该再自由发挥日志体系。


七、 日志使用的注意事项

1. 日志也要定义“禁止事项”

日志规范不只是规定:

应该记录什么。

还必须明确:

绝对不能记录什么。

例如:

logger.info(f"password={password}")

绝对不应该出现。

同样包括:

密码
JWT
API Key
Access Token
Refresh Token
数据库密码
完整 Cookie
身份证号等敏感信息

其中 JWT 是:

JSON Web Token(JSON Web 令牌)

API 是:

Application Programming Interface(应用程序编程接口)

日志文件往往保存时间很长,而且可能被:

开发人员
运维人员
日志服务器
监控系统

读取。

因此:

日志不是临时调试窗口,而是持久化数据。


2. AI 最容易犯的日志错误

以后让 AI 写代码时,可以重点检查下面几种情况。

❌ 错误一:滥用 INFO

logger.info("enter method")
logger.info("checking param")
logger.info("query database")
logger.info("return result")

应该减少过程噪声。

❌ 错误二:异常只打印字符串

except Exception as e:
    logger.error(str(e))

这样经常会丢失:

Stack Trace(调用堆栈)

更适合:

except Exception:
    logger.exception("Agent execution failed")

❌ 错误三:重复记录异常

例如:

logger.error(f"Failed: {exc}")
logger.exception(exc)

如果没有特殊目的,通常会造成重复日志。

❌ 错误四:业务异常全部 ERROR

例如:

logger.error("User not found")

很多情况下更合理的是:

logger.warning("User not found")

❌ 错误五:业务层自己生成 request_id

request_id = uuid4()

这样会破坏整条调用链。

应该复用入口层已经创建的请求上下文。


八、 日志体系真正约束的是 AI 的“自由度”

AI 写代码最大的问题往往并不是不会实现功能。恰恰相反:

它太容易实现功能。

如果项目没有规则,AI 每写一个模块都可能顺手发明:

新的日志方式
新的异常方式
新的返回结构
新的工具类
新的命名习惯

代码单独看都能跑,项目整体却会越来越乱。

日志体系的价值就在这里:

没有日志架构:

AI
自由决定格式
自由决定等级
自由决定字段
自由决定是否 print
日志逐渐失控

而建立规则之后:

AI
get_logger()
Logging Rules
统一 Level
统一 request_id
统一输出

AI 不需要再“设计日志”。

只需要:

在已有边界内写业务代码。


写在最后

一个成熟的软件项目,不应该依靠开发者“记得怎么打日志”。

更不应该期待 AI 每次都能自动猜出:

哪些日志重要
应该是什么等级
应该带哪些上下文
应该保存到哪里

这些事情应该在架构层提前确定。

日志体系的目标也不是:

让系统打印更多内容。

而是:

用尽可能少、但足够关键的日志,还原系统真正发生过的事情。

对于 AI 编程而言,这种约束尤其重要。

因为真正值得规范的,不是:

logger.info() 应该怎么写

而是:

什么时候允许 AI 写日志、写什么日志,以及这些日志如何进入整个系统的可观测链路。

当日志格式、等级、上下文和链路编号都被固定下来以后,AI 输出的就不再是一堆散乱的调试信息。

而是一套:

可搜索、可关联、可排错、可审计、可分析的工程日志。

这才是日志体系架构真正应该解决的问题。

开源代码


🪐祝您好运🪐