在 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 — 调试信息
DEBUG 是 Debug(调试)。
主要用于开发阶段观察内部状态,例如:
logger.debug(f"Retrieved {len(chunks)} chunks")
适合记录:
中间变量
检索结果数量
路由选择结果
内部执行步骤
模型参数
生产环境通常不会大量输出。
INFO — 正常运行信息
INFO 是 Information(信息)。
表示系统正在正常执行重要动作:
logger.info("Agent execution started")
例如:
应用启动
用户登录
任务开始
Agent 调用完成
知识库加载完成
请求完成
WARNING — 警告
WARNING 表示:
系统还能继续运行,但是出现了值得关注的问题。
例如:
logger.warning("Knowledge base returned no result")
或者业务异常:
logger.warning(f"NotFoundError: {exc}")
miniagent 的全局异常处理目前就把 NotFoundError、AlreadyExistsError 等可预期业务异常记录为 WARNING,而不是直接当作系统崩溃处理。
这是非常重要的日志分级思想:
业务失败,不等于系统故障。
ERROR — 系统错误
ERROR 表示:
某个功能已经无法正常完成。
例如:
logger.error("Database initialization failed")
适合:
数据库连接失败
LLM 调用失败
文件读取失败
关键服务不可用
CRITICAL — 严重故障
CRITICAL 是 Critical(严重、关键)。
用于:
系统无法启动
核心数据库损坏
关键配置缺失
核心基础设施不可用
这类日志通常意味着:
系统可能已经无法继续提供服务。
三、不要“什么都打日志”
日志体系里还有一条很重要的原则:
不是执行过的每一步,都值得成为日志。
例如:
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 当前日志架构的整体关系:
客户端请求"] --> 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 输出的就不再是一堆散乱的调试信息。
而是一套:
可搜索、可关联、可排错、可审计、可分析的工程日志。
这才是日志体系架构真正应该解决的问题。
开源代码
🪐祝您好运🪐