《AI时代的程序员修养》前面讲了测试。测试回答的是:在已知输入下,系统是不是按预期工作。
但线上系统更麻烦。真实用户的输入不一定在测试里出现,第三方接口不一定稳定,队列可能积压,数据库可能慢,某个新版本可能只在一小部分请求上出错。这个时候,代码写得再漂亮也没用,你必须能看见系统正在发生什么。
第 10 篇讲可观测性。日志、指标、Trace、审计记录和业务埋点,不是上线以后补几行print。它们是系统设计的一部分,也是 AI 生成代码必须满足的工程边界。
AI 写代码时最容易漏掉“眼睛”
让 AI 写一个接口,它通常会把主流程写出来:
async def create_report(request: ReportRequest) -> ReportResponse: report = await save_report_request(request) await enqueue_report_job(report.id) return ReportResponse(report_id=report.id, status="queued")这段代码看起来可以跑,但如果线上用户说“我点了生成报告,一直没有结果”,你马上会遇到一串问题:
- 请求有没有进来?
- 数据库有没有写成功?
- 队列有没有投递成功?
- worker 有没有消费?
- 第三方接口有没有超时?
- 失败后有没有重试?
- 重试是不是重复扣费或重复写结果?
- 这个用户是不是命中了某个特殊参数?
- 只有这个用户慢,还是所有用户都慢?
如果系统没有可观测性,这些问题只能靠猜。
AI 生成代码最常见的问题,是把“功能能跑”当成“系统可交付”。真正可交付的服务,必须在出问题时留下足够的证据。
日志、指标、Trace 各看一层
很多人把可观测性理解成日志。日志当然重要,但只靠日志不够。
一个服务至少需要三类视角:
日志 Logs 发生了什么事 指标 Metrics 整体状态变成什么样 链路 Trace 一次请求经过了哪些步骤日志适合回答“这一次发生了什么”。例如某个report_id创建成功、某个第三方接口返回 429、某次重试失败。
指标适合回答“系统整体怎么样”。例如每分钟请求数、错误率、P95 延迟、队列积压、数据库连接池使用率。
Trace 适合回答“一次请求慢在哪里”。例如 API 花了 80ms,数据库写入 20ms,队列投递 10ms,worker 等待 4 分钟,第三方调用 15 秒。
这三者缺一块,排障都会变成半盲。
日志不是字符串,是事件
AI 很容易写出这样的日志:
logger.info(f"create report success: {report.id}")这比没有日志好,但还不够。它只是给人看的字符串,机器很难稳定分析。
生产系统更应该写结构化日志:
logger.info( "report_request_created", extra={ "event": "report_request_created", "report_id": report.id, "user_id": request.user_id, "tool_id": request.tool_id, "request_id": context.request_id, "trace_id": context.trace_id, "status": "queued", }, )关键区别在于字段。
字段让日志可以被查询、聚合和关联。你可以查某个report_id的完整过程,可以看某个tool_id最近失败率,可以把 API 日志和 worker 日志按trace_id串起来。
结构化日志里最重要的字段通常有这些:
timestamp 事件时间 level 日志级别 event 稳定事件名 service 服务名 env 环境 request_id 单次入口请求 ID trace_id 分布式链路 ID span_id 当前步骤 ID user_id 用户 ID,注意脱敏和合规 resource_id 业务对象 ID,例如 report_id / order_id status 成功、失败、跳过、重试 error_code 稳定错误码 duration_ms 当前步骤耗时这里有一个很实际的判断标准:日志不是给当时写代码的人看的,而是给三个月后值班的人看的。
日志级别要有纪律
AI 生成代码经常乱用日志级别。正常分支打error,大循环里打info,异常又被吞掉,只留一句“failed”。
日志级别应该有相对固定的含义:
DEBUG 开发和临时诊断,生产默认不打开或采样 INFO 正常生命周期事件,例如请求创建、任务完成、状态变更 WARN 可恢复异常,例如重试、降级、限流、第三方临时失败 ERROR 当前操作失败,需要被统计和告警 FATAL 进程无法继续工作,通常很少用这里最容易犯错的是INFO。如果一个循环要处理几十万个对象,每个对象都打一条INFO,日志系统会被拖慢,数据库也可能被写垮。被跳过的正常分支,通常应该是计数器或采样日志,而不是逐条刷屏。
给 AI 的要求要写清楚:
不要在高频循环里逐条写 INFO 日志。 对正常 skip 分支使用 metrics 计数。 DEBUG 日志允许采样,队列满时可以丢弃摘要。 ERROR 日志必须包含 error_code、trace_id、resource_id 和异常类型。这不是吹毛求疵。日志本身也会成为系统负载。
指标看趋势,不看故事
日志告诉你某次请求发生了什么。指标告诉你系统是不是正在变坏。
服务最常用的是 RED 指标:
Rate 请求速率 Errors 错误率 Duration 延迟如果是资源层,还会看 USE 指标:
Utilization 使用率,例如 CPU、连接池、磁盘 Saturation 饱和度,例如队列积压、线程等待 Errors 错误数假设有一个报告生成服务,最少应该有这些指标:
http_requests_total{route, method, status} http_request_duration_seconds_bucket{route, method} report_requests_total{tool_id, status} report_jobs_enqueued_total{queue} report_jobs_processed_total{queue, status} report_job_duration_seconds_bucket{queue} report_queue_depth{queue} third_party_calls_total{provider, status} third_party_call_duration_seconds_bucket{provider} db_pool_in_use{database}注意,指标标签不能乱加。
route、status、provider、tool_id这种低到中等基数的字段适合做标签。user_id、request_id、report_id通常不适合做指标标签,因为基数太高,会把时序数据库打爆。
这也是 AI 生成代码需要被检查的地方。它可能会觉得“字段越多越好”,但指标系统最怕无限基数。
直方图比平均值诚实
延迟指标不要只看平均值。
平均 200ms 的接口,可能是大多数请求 50ms,少数请求 10 秒。用户感受到的是尾部延迟,不是平均值。
工程里更常看 P50、P95、P99:
P50 一半请求低于这个耗时 P95 95% 请求低于这个耗时 P99 99% 请求低于这个耗时如果报告接口 P50 是 120ms,P95 是 2s,P99 是 15s,说明少数请求很慢。这个时候应该顺着 trace 查慢请求,而不是盯着平均值说“看起来还行”。
让 AI 做指标时,要明确:
为外部接口、数据库查询、队列任务和整体请求增加 duration histogram。 不要只记录平均耗时。 告警以错误率、P95/P99、队列积压和饱和度为主。Trace 是一次请求的时间线
Trace 的核心是把一次请求拆成多个 span。
一个报告生成链路可能长这样:
trace_id=abc API /reports ├─ validate_request 3ms ├─ insert report_request 18ms ├─ enqueue report_job 9ms └─ return queued 1ms worker report_job ├─ load report_request 12ms ├─ call provider A 2400ms ├─ normalize result 35ms ├─ save report_result 22ms └─ publish notification 8ms如果没有 Trace,你只能在 API 日志、队列日志、worker 日志之间来回搜。Trace 把这些步骤放到一条时间线上。
这里最关键的是上下文传播。入口请求生成trace_id,API 写数据库、发消息、调用下游时,都要把这个 ID 传下去。队列任务被 worker 消费时,worker 要继续使用同一个 trace,而不是重新生成一个孤立 trace。
HTTP 里通常通过 header 传:
traceparent: 00-<trace_id>-<span_id>-01队列里可以放到 message metadata:
{ "job_id": "job_123", "report_id": "rpt_456", "trace_id": "abc", "parent_span_id": "def" }如果让 AI 写异步任务,这一点必须写进需求。否则 API 和 worker 会各打一套日志,出了问题只能靠业务 ID 硬拼。
审计记录不是日志的替代品
还有一类数据经常被混在日志里:审计记录。
日志面向工程排障,审计记录面向业务事实。两者不能互相替代。
比如用户修改权限,日志可以记录:
event=permission_update_saved trace_id=...审计记录应该记录更稳定的业务事实:
{ "actor_id": "user_1", "action": "permission.grant", "target_type": "project", "target_id": "project_9", "before": {"role": "viewer"}, "after": {"role": "editor"}, "reason": "owner_action", "created_at": "2026-07-03T08:00:00Z" }审计记录要可追溯、可展示、可长期保存。日志可以按保留期清理,审计记录通常不能随便丢。
AI 生成后台管理、权限、扣费、状态变更代码时,必须问一句:这个动作需不需要审计?
业务埋点回答“用户怎么用”
日志和 Trace 主要帮助工程师看系统。业务埋点帮助产品和运营看用户行为。
同一个报告生成服务,可以埋这些事件:
report_create_clicked report_create_submitted report_create_succeeded report_create_failed report_result_viewed report_result_exported每个事件都要有稳定 schema:
{ "event": "report_create_submitted", "user_id": "user_1", "session_id": "sess_1", "tool_id": "tool_x", "source": "search_result", "input_size_bucket": "small", "created_at": "2026-07-03T08:00:00Z" }埋点最怕两件事。
一是事件名随手起。今天叫click_report,明天叫report_click,后天叫submit_report_request,最后没人敢用。
二是把所有字段都塞进去。敏感信息、长文本、原始参数、用户输入,如果未经处理就进入埋点系统,后面会非常麻烦。
让 AI 写埋点时,应该先给事件字典,而不是让它自由发挥。
可观测性也有成本
可观测性不是越多越好。
日志太多会增加存储成本,指标标签太多会撑爆时序库,Trace 全量采样会很贵,业务埋点太细会让分析变得混乱。
实际系统里常用这些策略:
日志:ERROR 全量,INFO 控制数量,DEBUG 默认关闭或采样。 指标:核心路径全量聚合,避免高基数字段。 Trace:低流量服务可全量,高流量服务按比例采样,错误请求全量保留。 审计:关键业务动作全量保存,字段稳定。 埋点:围绕漏斗和关键路径设计,不随手埋。如果某个服务每天只有几千次请求,简单日志加少量指标可能足够。如果每天有几千万次请求,不控制采样和标签,观测系统会比业务系统先倒。
这是 AI 不会天然替你判断的地方。它可以生成接入代码,但成本边界要由人指定。
一个接口的观测验收清单
如果让 AI 写一个生产接口,我会把可观测性要求放进验收清单:
1. 每个入口请求生成或继承 request_id 和 trace_id。 2. 成功、失败、重试、跳过都有稳定 event 名。 3. ERROR 日志包含 error_code、异常类型、trace_id、业务对象 ID。 4. 高频循环不逐条写 INFO。 5. 请求数、错误数、延迟直方图有 metrics。 6. 外部调用、数据库查询、队列投递有独立 span。 7. 异步任务继承上游 trace context。 8. 权限、扣费、状态变更写审计记录。 9. 用户关键行为写业务埋点,事件名和字段来自事件字典。 10. 指标标签没有 user_id、request_id 这类高基数字段。这份清单比“加日志”有效得多。
给 AI 的提示词可以这样写
可观测性要在实现前写进 prompt:
请实现报告生成接口和异步 worker。 除了业务逻辑,还必须满足以下可观测性要求: - 使用结构化日志,不要拼接不可查询的长字符串。 - 每个入口请求都有 request_id 和 trace_id。 - API、数据库写入、队列投递、worker 消费、第三方调用分别创建 trace span。 - 队列消息必须携带 trace_id 和 parent_span_id。 - 暴露 RED 指标:请求数、错误数、延迟直方图。 - 暴露队列指标:入队数、消费数、失败数、队列积压、任务耗时。 - ERROR 日志必须包含 error_code、trace_id、report_id、异常类型。 - 高频循环禁止逐条 INFO;正常 skip 用 counter,DEBUG 可采样。 - 用户发起生成、查看结果、导出结果需要写业务埋点。 - 创建、取消、扣费、权限变化需要写审计记录。 实现完成后,请列出可以用哪些查询定位: 1. 某个 report_id 卡在哪里。 2. 最近 15 分钟错误率是否升高。 3. 某个 provider 是否变慢。 4. 队列是否积压。 5. 某个用户的关键操作路径。这段提示词的目的不是让 AI 多写几行样板代码,而是让它知道:系统上线以后要能被理解。
真正的工程质量,是出事时不慌
平时看一个系统,大家容易看功能、界面、性能。
出事时,真正救命的是证据链。
请求从哪里来,经过哪些模块,在哪一步变慢,哪个依赖失败,影响了多少用户,是否重复执行,是否产生业务损失,是否需要补偿。
这些问题靠日志、指标、Trace、审计记录和埋点一起回答。
AI 会让代码生成更快,也会让“看起来能跑”的服务更多。程序员的责任不是阻止 AI 写代码,而是给它设下工程边界:能测试,能观测,能排障,能交接。
系统迟早会出事。区别只在于,出事时你是打开控制台开始定位,还是坐在黑盒前面猜。