AI时代的程序员修养:系统出事时,你要看得见
《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_idreport.id, statusqueued)这段代码看起来可以跑但如果线上用户说“我点了生成报告一直没有结果”你马上会遇到一串问题请求有没有进来数据库有没有写成功队列有没有投递成功worker 有没有消费第三方接口有没有超时失败后有没有重试重试是不是重复扣费或重复写结果这个用户是不是命中了某个特殊参数只有这个用户慢还是所有用户都慢如果系统没有可观测性这些问题只能靠猜。AI 生成代码最常见的问题是把“功能能跑”当成“系统可交付”。真正可交付的服务必须在出问题时留下足够的证据。日志、指标、Trace 各看一层很多人把可观测性理解成日志。日志当然重要但只靠日志不够。一个服务至少需要三类视角日志 Logs 发生了什么事 指标 Metrics 整体状态变成什么样 链路 Trace 一次请求经过了哪些步骤日志适合回答“这一次发生了什么”。例如某个report_id创建成功、某个第三方接口返回 429、某次重试失败。指标适合回答“系统整体怎么样”。例如每分钟请求数、错误率、P95 延迟、队列积压、数据库连接池使用率。Trace 适合回答“一次请求慢在哪里”。例如 API 花了 80ms数据库写入 20ms队列投递 10msworker 等待 4 分钟第三方调用 15 秒。这三者缺一块排障都会变成半盲。日志不是字符串是事件AI 很容易写出这样的日志logger.info(fcreate 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、P99P50 一半请求低于这个耗时 P95 95% 请求低于这个耗时 P99 99% 请求低于这个耗时如果报告接口 P50 是 120msP95 是 2sP99 是 15s说明少数请求很慢。这个时候应该顺着 trace 查慢请求而不是盯着平均值说“看起来还行”。让 AI 做指标时要明确为外部接口、数据库查询、队列任务和整体请求增加 duration histogram。 不要只记录平均耗时。 告警以错误率、P95/P99、队列积压和饱和度为主。Trace 是一次请求的时间线Trace 的核心是把一次请求拆成多个 span。一个报告生成链路可能长这样trace_idabc 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_idAPI 写数据库、发消息、调用下游时都要把这个 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 硬拼。审计记录不是日志的替代品还有一类数据经常被混在日志里审计记录。日志面向工程排障审计记录面向业务事实。两者不能互相替代。比如用户修改权限日志可以记录eventpermission_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 用 counterDEBUG 可采样。 - 用户发起生成、查看结果、导出结果需要写业务埋点。 - 创建、取消、扣费、权限变化需要写审计记录。 实现完成后请列出可以用哪些查询定位 1. 某个 report_id 卡在哪里。 2. 最近 15 分钟错误率是否升高。 3. 某个 provider 是否变慢。 4. 队列是否积压。 5. 某个用户的关键操作路径。这段提示词的目的不是让 AI 多写几行样板代码而是让它知道系统上线以后要能被理解。真正的工程质量是出事时不慌平时看一个系统大家容易看功能、界面、性能。出事时真正救命的是证据链。请求从哪里来经过哪些模块在哪一步变慢哪个依赖失败影响了多少用户是否重复执行是否产生业务损失是否需要补偿。这些问题靠日志、指标、Trace、审计记录和埋点一起回答。AI 会让代码生成更快也会让“看起来能跑”的服务更多。程序员的责任不是阻止 AI 写代码而是给它设下工程边界能测试能观测能排障能交接。系统迟早会出事。区别只在于出事时你是打开控制台开始定位还是坐在黑盒前面猜。