
1. 为什么“能跑”的AI员工系统往往死在“说不清”上我见过太多团队把AI员工系统搭起来之后第一周跑得挺欢第二周开始出现“它昨天明明能做对今天怎么又不行了”的诡异现象。你去问开发开发说模型没换你去问运维运维说服务没重启你去翻日志发现日志里只有一行task completed连它调了哪个工具、传了什么参数、中间哪一步开始跑偏都看不出来。这种系统不是不能跑而是跑得不明不白。“给AI员工加一层可观测性”这件事本质上不是加几个print或者接一个日志采集器就完事。AI员工和传统后端服务最大的区别在于传统服务的执行路径是代码写死的出问题你能顺着调用栈往回找而AI员工的执行路径是模型在运行时动态生成的同一个输入今天走A工具链明天可能走B工具链后天可能直接跳过工具自己编一个答案。你如果没有一层专门为“动态决策过程”设计的可观测层排查问题基本等于算命。这篇内容适合三类人看第一类是把AI员工/Agent系统推进到生产环境、已经开始被“不可复现问题”折磨的工程师第二类是正在设计Agent执行网关、想提前把可观测性做进架构的架构师第三类是做AI应用运维、需要向业务方解释“为什么这次回答和上次不一样”的技术负责人。我会从日志采集的坑讲起一路讲到可重放这个真正能救命的能力中间穿插我自己踩过的具体问题和可落地的配置方案。先把核心结论摆出来可观测性不等于日志多可重放不等于录屏。AI员工的可观测性要解决的是三个层次的问题——看得见日志与追踪、看得懂结构化上下文、回得去可重放执行。大部分团队只做到第一层所以永远在“猜”。2. 日志采集这一层AI员工系统和普通服务完全不是一回事2.1 普通日志采集方案直接搬过来会漏掉什么很多团队的第一反应是日志嘛上 Filebeat 采集送到 ElasticsearchKibana 一看就完事了。这套方案对普通Web服务没问题但放到AI员工系统上会立刻暴露三个缺口。第一个缺口是日志作用域混乱。普通服务一个请求一个traceId从头串到尾。AI员工系统里一次用户请求可能触发多次模型调用、多次工具调用、多次子Agent派发每次调用都有自己的输入输出。如果你只用一个大traceId串起来日志面板上就是几千行混在一起根本分不清哪段属于哪次模型推理。我早期就吃过这个亏一个用户问题触发了7次工具调用日志里7次调用的参数和结果交错打印排查时花了两个小时才理清顺序。第二个缺口是非文本载荷无法直接采集。AI员工的中间状态往往包含结构化数据——工具返回的JSON、模型输出的function call参数、向量检索的候选列表。这些内容如果直接str()一下塞进日志行采集没问题但后续想按字段检索、想还原当时上下文就全废了。Filebeat 采集的是文本行它不理解你的JSON结构。第三个缺口是采集时机与执行时机错位。AI员工的执行是流式的模型一边生成一边可能就触发了工具调用。如果你的日志是在整个任务结束后才批量写入那么任务中途崩溃时你丢失的恰恰是最关键的“崩溃前最后几步”。我现在的做法是每个执行步骤结束立即落盘哪怕牺牲一点写入性能也要保证崩溃时能拿到最后一条完整记录。2.2 用结构化事件替代文本日志行我的建议是AI员工系统的日志层不要输出“日志行”要输出结构化事件。每一条事件是一个JSON对象至少包含这些字段{ event_id: evt_20250101_001, trace_id: trace_abc123, span_id: span_004, parent_span_id: span_001, event_type: tool_call, timestamp: 1735689600123, agent_id: assistant_main, step_index: 4, payload: { tool_name: search_knowledge_base, arguments: {query: 退款政策, top_k: 5}, result_summary: 返回3条候选, result_raw_ref: blob://trace_abc123/span_004/result.json }, duration_ms: 342, status: success }这里有几个设计决策值得展开说。span_id和parent_span_id是为了还原调用树AI员工的子任务派发天然是一棵树没有这两个字段你无法重建执行拓扑。step_index是AI员工特有的它标记这是整个任务的第几步因为模型决策是有顺序的顺序错了整个复现就错了。result_raw_ref指向原始结果存储而不是把大块结果直接塞进事件里——这是为了避免日志体积爆炸同时保证原始数据可追溯。提示payload里不要放超过2KB的内容大结果一律走对象存储或独立文件事件里只留引用。我见过一个团队把完整的向量检索结果几万条写进日志结果ES集群三天就撑爆了。2.3 Filebeat采集结构化事件的配置要点如果你用 Filebeat 采集这些JSON事件有几个配置必须改否则默认配置会给你埋坑。默认的json解析在遇到格式异常时会直接丢弃整条消息而AI员工系统的日志恰恰可能因为模型输出包含特殊字符而格式异常。我的配置是这样的filebeat.inputs: - type: filestream paths: - /var/log/ai-agent/events/*.jsonl parsers: - ndjson: target: overwrite_keys: true add_error_key: true expand_keys: true fields: service: ai-agent-gateway fields_under_root: true processors: - decode_json_fields: fields: [message] target: overwrite_keys: true add_error_key: true - drop_fields: fields: [agent, ecs, host, input] ignore_missing: true关键点是add_error_key: true它保证解析失败的事件不会被静默丢弃而是带上错误标记继续往下走这样你在Kibana里能筛出error.keyword存在的记录专门处理。另外expand_keys: true能把嵌套JSON展开成扁平字段方便后续按payload.tool_name这种路径检索。还有一个容易被忽略的点日志文件轮转策略。AI员工系统的日志增长速度远超普通服务一次复杂任务可能产生几百条事件。如果按默认的10MB轮转高峰期几分钟就切一个文件Filebeat的采集延迟会明显上升。我一般设成50MB轮转、保留7天同时用close_inactive: 5m让Filebeat及时释放不活跃文件句柄。3. 从“看得见”到“看得懂”执行网关里的上下文注入3.1 为什么日志里必须带决策上下文光有结构化事件还不够。我遇到过最头疼的一类问题是日志显示模型调用了search_knowledge_base参数也对结果也返回了但最终回答就是错的。你去查日志每一步都“success”那问题出在哪出在模型为什么决定调这个工具这个决策上下文没有记录。AI员工的执行网关也就是包在模型外面那层调度逻辑应该在每次模型调用前后把决策相关的上下文注入到事件里。具体来说至少记录这几样东西当前可用的工具列表模型是从哪些选项里选的、系统提示词的哈希值提示词变了行为就变了、历史对话的摘要或轮数、以及模型返回的原始function call意图在参数解析之前。我现在的做法是在执行网关里维护一个ExecutionContext对象每次模型调用时把它序列化成一个decision_context字段附在事件上。这个字段不需要很大但必须包含“模型做选择时看到了什么”。这样当出现“它为什么选错工具”的问题时你能直接对比两次执行的decision_context差异而不是靠猜。3.2 用OpenTelemetry把AI执行链路串成树结构化事件解决了单点记录但AI员工的执行是一棵树你需要一个能表达树形关系的追踪方案。OpenTelemetry 的 Span 模型天然适合这个场景而且它和日志可以关联——每个Span可以挂载对应的日志事件ID。我的做法是在执行网关里为每个执行单元创建Span一次用户请求是根Span每次模型调用是一个子Span每次工具调用是模型Span下的子Span子Agent派发是独立的子Span树。Span上除了标准属性我额外加了几个AI特有的属性属性名类型说明ai.model.namestring模型标识ai.model.temperaturedouble采样温度影响可复现性ai.tool.namestring工具名称ai.tool.call_idstring工具调用唯一ID用于关联结果ai.step.indexint执行步序ai.decision.context_hashstring决策上下文哈希用于快速比对这里ai.decision.context_hash是我自己加的非常有用。它是对decision_context做一次哈希如果两次执行的这个哈希相同但结果不同那问题大概率出在模型本身的不确定性上如果哈希不同那问题出在上下文变化上。这一个字段就能帮你快速定位问题方向。3.3 日志与追踪的关联别让两套系统各说各话很多团队上了OpenTelemetry做追踪又上了ELK做日志结果两套系统各说各话——追踪里看到一个慢Span想去日志里找对应记录发现对不上。根因是两边没有共享标识。我的做法很简单每个Span的span_id和trace_id必须写进对应的日志事件里反过来日志事件的event_id也作为Span的一个属性挂上去。这样你在追踪系统里点开一个Span能直接跳到对应的日志事件在日志里看到一条异常能直接跳到对应的Span。这个双向关联不需要什么高级工具就是两个字段的事但没做的话排查效率差十倍。注意如果你的日志采集链路和追踪采集链路是分开的要确保两边的trace_id生成规则一致。我见过一个团队日志用UUID、追踪用W3C traceparent格式结果两边永远对不上白白浪费了两套系统。4. 可重放把“那次执行”完整地搬回来4.1 可重放到底重放的是什么“可重放”这个词容易被误解。它不是让你把用户请求再发一遍看结果一样不一样——那叫重试不叫重放。真正的可重放是给定一次历史执行的完整记录你能在隔离环境里精确复现当时的每一步决策和工具调用包括模型看到的输入、模型给出的输出、工具返回的结果。为什么需要这个因为AI员工系统的问题往往不可复现。用户说“昨天它回答错了”你今天用同样的问题问它它回答对了。你没法调试一个不复现的问题。可重放让你把“昨天那次”冻结下来反复回放直到找到问题点。可重放需要三样东西完整的输入快照、确定性的执行环境、可替换的模型层。输入快照就是前面说的结构化事件加上原始载荷确定性执行环境意味着工具调用要能被mock不能真的去查数据库可替换的模型层意味着你要能用一个“回放模型”替代真实模型这个回放模型不真正推理而是按记录返回当时的结果。4.2 记录什么才能保证可重放不是所有日志都能支撑可重放。要支撑可重放你的记录必须满足“足够重建执行状态”这个标准。具体来说每次模型调用必须记录完整的messages数组包括system prompt、历史对话、工具定义、模型参数temperature、top_p等、模型返回的原始响应包括function call的原始文本。每次工具调用必须记录工具名称、完整参数、完整返回值、调用耗时。这里有个坑很多团队记录的是解析后的结果而不是原始响应。比如模型返回了一个function call你解析成{tool: search, args: {...}}存下来但原始响应里可能还有模型的其他输出比如一段解释文字这些在解析时被丢掉了。回放时你只有解析后的结果无法还原模型当时到底输出了什么。我的做法是原始响应和解析结果都存原始响应存到对象存储解析结果存到事件里。另一个坑是工具返回值的截断。为了控制日志体积很多团队会把大返回值截断只留前N个字符。这在排查时够用但回放时不够——工具返回的完整数据可能影响模型后续决策。我的做法是返回值完整存对象存储事件里存摘要加引用回放时按引用加载完整数据。4.3 回放执行器的实现思路回放执行器本质上是一个“假的执行网关”它对外暴露和真实网关一样的接口但内部不真正调用模型和工具而是从记录里读取。实现上有两种模式逐步回放和全量回放。逐步回放是每次只回放一步你可以在每一步之后检查状态、修改输入、观察变化。这对调试特别有用——你可以把第3步的工具返回值改掉看模型后续决策会不会变。全量回放是一次性跑完整个执行链用于验证“在记录不变的情况下执行结果是否一致”。我实现逐步回放时用了一个简单的状态机回放器维护一个step_cursor每次调用next_step()时从记录里取出下一步的事件如果是模型调用就返回记录的模型响应如果是工具调用就返回记录的工具结果。执行网关在回放模式下不感知差异它以为自己还在正常执行。这样回放器和真实执行走的是同一套网关代码避免了“回放逻辑和真实逻辑不一致”这个经典问题。class ReplayGateway: def __init__(self, trace_record): self.record trace_record self.cursor 0 def call_model(self, messages, **kwargs): event self.record[self.cursor] assert event[event_type] model_call self.cursor 1 return event[payload][raw_response] def call_tool(self, tool_name, arguments): event self.record[self.cursor] assert event[event_type] tool_call self.cursor 1 return event[payload][result_raw]这段代码的关键是assert——如果回放时执行路径和记录不一致比如模型这次决定调另一个工具断言会立刻失败告诉你“执行路径偏离了记录”。这本身就是一种有价值的信号说明系统行为已经不可复现了。5. 落地时最容易翻车的几个细节5.1 日志量爆炸与采样策略AI员工系统的日志量是普通服务的几十倍。一次复杂任务可能产生几百条事件每条事件几百字节到几KB不等。如果不做控制一天跑几千次任务就能产生几十GB日志。我的策略是分级采样所有执行的元数据事件类型、时间、状态、耗时全量保留但原始载荷模型原始响应、工具完整返回值按比例采样采样率根据任务重要性动态调整。具体做法是给每个任务打一个importance标签用户主动发起的、涉及敏感操作的、之前失败过的任务标记为高重要性原始载荷全量保留批量后台任务、测试流量标记为低重要性原始载荷只保留1%。这样既保证了关键问题可追溯又控制了存储成本。提示采样决策要在任务开始时做不能中途改。我见过一个团队中途根据任务耗时决定是否保留载荷结果一个慢任务的前半段载荷被丢了回放时缺数据。5.2 敏感信息的脱敏时机AI员工的输入输出里经常包含用户隐私、内部数据。日志里直接存原文是合规风险。但脱敏做早了会影响回放——你把用户手机号脱敏成***回放时模型看到的就是脱敏后的输入行为和真实执行不一致。我的做法是双写日志事件里存脱敏后的版本用于日常检索和展示同时把原始版本加密存到独立的审计存储里只有回放时才有权限读取。脱敏规则在采集层做加密存储在写入层做两者互不干扰。这样日常看日志是安全的回放时又能拿到真实数据。5.3 时间戳的坑别用本地时间这个坑很小但很致命。AI员工系统可能跨多台机器执行如果日志时间戳用本地时间跨机器关联时就会出现顺序错乱。我统一用UTC毫秒时间戳并且在事件里额外记录一个单调递增的sequence_number用于同一台机器内的严格排序。跨机器排序靠trace_id加span_id的父子关系不靠时间戳。还有一个细节模型调用的耗时记录要区分“网关侧耗时”和“模型侧耗时”。网关侧耗时包括序列化、网络传输、重试等模型侧耗时是模型真正推理的时间。这两个混在一起你没法判断慢是因为模型慢还是因为网关慢。我在事件里分别记录gateway_duration_ms和model_duration_ms排查性能问题时一目了然。6. 一个真实问题的排查链路从日志到回放6.1 问题现象与第一轮日志排查之前有个线上问题用户反馈AI员工在处理“修改订单地址”请求时偶尔会错误地触发“取消订单”操作。频率不高大概几十次里有一次。第一轮排查我直接查日志筛选event_typetool_call且tool_name包含order的记录发现错误执行的那次模型确实调用了cancel_order而不是update_address。但日志只能告诉我“它调错了”不能告诉我“为什么调错”。我对比了正确执行和错误执行的decision_context发现两者几乎一样——可用工具列表相同、系统提示词哈希相同、历史对话轮数相同。唯一的差异是ai.model.temperature那次是0.7而正确执行时是0.2。这是一个线索但不是根因因为0.7也不应该导致这么离谱的错误。6.2 用回放定位到具体偏差步骤我启动了逐步回放把那次错误执行完整回放了一遍。回放到第2步模型调用时我注意到模型的原始响应里function call的name字段是cancel_order但arguments里传的却是地址修改的参数。也就是说模型“想”修改地址但把工具名写成了取消订单。继续回放我发现第1步的工具调用返回了一个包含订单状态的结果这个结果里“取消”这个词出现了三次因为订单状态描述里提到了“可取消”。模型在第2步决策时很可能被这个高频词影响了把“取消”和当前操作关联了起来。这是一个典型的上下文污染问题——工具返回值里的无关词汇影响了模型决策。6.3 修复方案与验证定位到根因后修复方案有两个方向一是改工具返回值的格式把状态描述里的“可取消”改成更中性的表述二是在系统提示词里加一条约束明确“修改地址时禁止调用取消订单工具”。我两个都做了然后用回放验证——把修复后的提示词和工具返回值注入回放器重新跑那次执行模型这次正确调用了update_address。这个案例的价值在于如果没有可重放我可能永远找不到“上下文污染”这个根因只能靠加约束硬堵。而有了回放我能精确看到模型在哪一步、因为什么输入而跑偏。这就是可观测性从“看得见”进化到“回得去”的实际价值。7. 关于这套方案我踩过之后想说的几句可观测性这层东西做的时候觉得是负担出问题的时候才知道是救命的。我最大的体会是不要等系统复杂了再补可观测性要在第一个Agent跑通的时候就把它做进去。因为可观测性的设计会影响执行网关的接口设计后补的话往往要重构执行链路。另一个体会是可重放不是万能药它只能重放你记录下来的东西。如果你记录的时候漏了某个关键上下文回放时照样抓瞎。所以记录什么、怎么记录这个决策比回放器本身更重要。我的经验是每次遇到一个“查不出来”的问题就回头看看是哪个字段没记把它补上。这样可观测性层会随着系统一起进化。最后说一个实操小技巧在开发环境里我会把回放器接成一个HTTP接口输入trace_id就返回回放结果。这样测试同学发现异常时不用找开发自己就能回放看是哪一步出了问题。这个接口上线后我们团队排查AI员工问题的平均时间从半天降到了半小时以内。工具不复杂关键是让能复现问题的人直接拿到复现能力。