ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI员工系统可观测性实战:从日志记录到可重放引擎的完整落地

AI员工系统可观测性实战:从日志记录到可重放引擎的完整落地 1. 为什么“能跑”的AI员工系统最后都卡在“说不清”上做AI员工系统的团队几乎都会经历同一个阶段Demo跑通那一刻特别兴奋觉得这事成了。再过两个月系统接入了十几个业务场景每天处理几千次任务问题就开始冒出来——某个任务为什么失败了当时模型看到了什么上下文调用了哪个工具返回了什么是模型判断错了还是工具超时了还是上游数据本身就是脏的没人说得清。这就是AI员工系统和传统后端服务最本质的区别。传统服务的执行路径是代码写死的出问题看堆栈就能定位。AI员工不一样它的执行路径是模型在运行时“现编”的同一个输入今天走A路径明天可能走B路径。你没法靠读代码来复现问题只能靠记录它当时到底干了什么。所以“给AI员工加一层可观测性”这件事核心不是装个日志框架那么简单。它要解决的是一个更根本的问题让一次AI执行过程变成可回放、可审计、可归因的完整记录。日志只是原料可重放才是目的。我前后在三个不同规模的AI员工项目里搭过这套东西踩过的坑从“日志写了一堆但没人看”到“想重放却发现关键上下文没记”最后沉淀下来的方案核心思路就一句话把AI员工的每一次执行当成一笔需要留痕的交易来对待。下面把这套东西完整拆开讲。1.1 先搞清楚AI员工的“可观测性”和普通服务差在哪普通服务的可观测性三板斧是日志、指标、链路追踪。这套东西放到AI员工身上前两个还能用第三个直接失效。因为链路追踪的前提是调用链是确定的而AI员工的调用链是模型动态生成的你事先不知道它会调几个工具、按什么顺序调。我试过直接套用OpenTelemetry那套Span模型结果发现一个任务里模型可能先查数据库、再调搜索、发现不对又回头查数据库、最后调了个翻译工具。这些Span之间的父子关系是运行时才确定的而且同一个工具可能被调用多次每次的入参还不一样。硬套标准链路模型最后画出来的图自己都看不懂。所以AI员工的可观测性我把它拆成四个必须记录的东西输入快照用户原始输入、系统提示词、注入的上下文、可用的工具列表。这是重放的起点缺一个都复现不了。决策轨迹模型每一步的思考如果有、选择了哪个工具、传了什么参数、为什么这么选。这是最核心的部分也是普通日志最容易漏掉的部分。执行结果每个工具调用的返回、耗时、成功失败状态、错误信息。注意这里要记原始返回不是加工后的。状态变更这次执行对系统状态产生了什么影响比如写了哪条数据库记录、发了哪个消息、改了哪个文件。这四样东西凑齐一次执行才算“可重放”。少任何一样重放的时候都会卡住。1.2 为什么“记日志”和“可重放”之间隔着一道鸿沟很多团队的第一版方案就是加日志用Python的logging或者structlog在每个关键节点打一行。跑起来看着挺热闹日志文件一天几百兆。但真出问题的时候翻日志的人会发现几个致命问题。第一日志是扁平的没有结构。一行文本里混着时间戳、任务ID、模型输出、工具返回想按任务ID过滤得靠grep想按工具名统计得写正则。第二日志是“事后补”的开发的时候没想到要记某个字段出问题的时候才发现那个字段恰恰是关键。第三日志和实际执行是脱节的日志说调了搜索工具但搜索工具当时收到的完整参数是什么日志里只记了个摘要。我踩过最典型的一个坑有个任务一直失败日志显示“工具调用超时”。但重放的时候发现超时那个工具的入参里有一个字段是空的而模型明明在思考里说了要传这个字段。问题出在参数序列化环节模型输出的JSON被解析的时候丢了一个字段。这个bug靠看日志根本发现不了因为日志只记了“调用超时”没记“实际传了什么参数”。所以从“记日志”到“可重放”中间要补的是结构化和完整性这两课。结构化是指每条记录都是可查询的JSON有统一的schema。完整性是指记录的内容要足够支撑一次完整的重放而不是只够看个大概。2. 执行网关把可观测性做进AI员工的骨架里这套东西落地的时候我强烈建议不要做成“外挂”。什么意思就是不要在每个业务代码里手动调log函数那样迟早会漏。正确的做法是做一个执行网关让AI员工的所有动作都必须经过这个网关网关负责记录一切。这个思路借鉴的是API网关的做法。API网关挡在服务前面所有请求都得过它它顺便就把日志、鉴权、限流做了。AI员工的执行网关也是同理模型要调工具必须通过网关模型要输出结果必须通过网关。网关在转发的同时把该记的都记了。2.1 执行网关的核心职责拆解网关要做的事我整理成下面这张表每一项都对应可重放需要的一个要素职责记录内容对应可重放要素请求拦截任务ID、时间戳、用户输入、会话上下文输入快照模型调用代理提示词全文、模型原始输出、token消耗决策轨迹工具调用代理工具名、完整入参、原始返回、耗时、状态执行结果状态变更钩子写操作的目标、变更前后值状态变更链路关联父子任务ID、重试次数、上游任务ID重放顺序这张表里最容易被忽略的是最后一行“链路关联”。AI员工经常会有子任务比如主任务拆出一个子任务去处理子任务又调了工具。如果不在网关层把父子关系记下来重放的时候就是一堆散落的记录拼不起来。网关的实现方式我试过两种。一种是SDK方式给业务代码提供一个装饰器或者中间件业务代码调用工具的时候走SDK。另一种是代理方式业务代码正常调用但工具的endpoint指向网关网关再转发到真实工具。两种各有优劣SDK方式侵入性强但性能好代理方式无侵入但多一跳网络开销。我现在的选择是混合模型调用和关键工具走代理内部高频工具走SDK。2.2 日志schema设计别让记录变成垃圾场网关记下来的东西如果没有统一的schema过两个月就是垃圾场。我现在的schema设计原则是每条记录都是一个自包含的事件事件之间靠trace_id和span_id关联。一个工具调用事件的schema大概长这样{ event_type: tool_call, trace_id: task_20250101_abc123, span_id: span_004, parent_span_id: span_002, timestamp: 2025-01-01T10:23:45.123Z, tool_name: search_knowledge_base, tool_version: v2.1, input: { query: 退货政策, top_k: 5, filters: {category: after_sale} }, output: { status: success, results_count: 3, raw_response: ... }, duration_ms: 234, retry_count: 0, model_decision_context: { step: 3, reasoning: 用户问退货需要查知识库, alternative_tools: [search_web, ask_human] } }这里有几个设计细节值得说。model_decision_context这个字段是我后来加的一开始没记结果重放的时候发现模型为什么选这个工具完全靠猜。加上之后重放时能清楚看到模型当时的决策依据。raw_response要记原始返回不要记加工后的因为加工逻辑本身可能有问题重放的时候要用原始数据重新跑一遍加工逻辑。注意日志schema一旦定下来改动要非常谨慎。我吃过一次亏中途给某个字段改了名结果新旧日志混在一起查询的时候要写兼容逻辑特别恶心。建议schema里预留一个schema_version字段方便后续演进。2.3 采样策略不是所有执行都值得全量记录全量记录听起来很美好但成本扛不住。一个中等规模的AI员工系统每天几万次执行每次执行平均调5个工具每个工具调用记2KB日志一天就是几百MB。一个月下来存储成本很可观。我的采样策略是分层的全量记录元数据trace_id、任务类型、耗时、成功失败、token消耗。这些数据量小全量记用于统计和告警。全量记录失败执行失败的执行必须完整记录这是排查问题的核心素材。采样记录成功执行成功执行按比例采样比如10%用于分析正常路径和性能优化。重点任务全量记录标记为“关键任务”的执行比如涉及资金、涉及用户敏感操作的全量记录。这个策略的关键是采样决策要在网关层做而且采样标记要写进日志里。这样查询的时候能明确知道哪些是采样数据哪些是全量数据不会误判。3. 可重放引擎让历史执行“活”过来记录做完了重放才是真正体现价值的地方。可重放不是简单地把日志读出来展示而是要让历史执行能够被重新驱动一遍观察在不同条件下的行为差异。3.1 重放的三种模式我实践下来重放分三种模式用途完全不同模式一只读回放。把一次执行的完整记录按时间顺序展示出来像看录像一样。这种模式用于人工排查最常用。实现上就是把该trace_id下的所有事件按timestamp排序渲染成时间线。模式二确定性重放。用记录下来的输入和工具返回重新跑一遍模型的决策逻辑看模型是否会做出同样的选择。这种模式用于验证模型行为的一致性。注意这里工具返回要用记录下来的原始返回不能重新调工具否则就不是确定性重放了。模式三反事实重放。修改某个条件比如换一个模型、改一个工具返回、调整提示词然后重新跑观察结果差异。这种模式用于优化和实验价值最高但实现也最复杂。三种模式的实现难度递增我建议先从模式一做起跑通了再往模式二、模式三演进。很多团队一上来就想做模式三结果基础记录都没做好重放出来的结果不可信。3.2 重放引擎的关键实现细节重放引擎的核心是拦截器替换。正常执行时工具调用走真实工具重放时工具调用走一个“回放拦截器”拦截器从记录里找到对应的工具返回直接返回不真实调用。这里有个坑怎么保证重放时找到的返回就是当时那次调用的返回如果同一个trace里同一个工具被调了多次怎么区分我的做法是在记录里给每次工具调用分配一个唯一的call_id重放时按call_id精确匹配。call_id的生成规则是trace_id span_id 调用序号保证唯一。另一个坑是时间。重放的时候如果代码里有依赖当前时间的逻辑比如“判断是否超过24小时”重放结果会和当时不一致。解决办法是在重放上下文里注入一个“虚拟时钟”所有时间相关的调用都从虚拟时钟取虚拟时钟的初始值设为记录里的时间戳。class ReplayContext: def __init__(self, trace_record): self.virtual_clock trace_record.start_time self.tool_responses { call.call_id: call.output for call in trace_record.tool_calls } def now(self): return self.virtual_clock def call_tool(self, tool_name, params, call_id): if call_id in self.tool_responses: return self.tool_responses[call_id] raise ReplayMissError(fcall_id {call_id} not found)这段代码是重放引擎的骨架。ReplayMissError这个异常很重要它意味着记录不完整重放无法继续。出现这个异常的时候要能明确告诉用户缺了哪次调用而不是静默失败。3.3 重放结果的对比与归因重放跑完之后要和原始执行做对比。对比的维度包括最终输出是否一致、工具调用序列是否一致、每步决策是否一致、耗时是否一致。不一致的地方就是归因的入口。比如重放时模型选了不同的工具那就要看两次的提示词是否一致、上下文是否一致、模型版本是否一致。我做过一个统计重放不一致的原因里排前三的是模型版本变了、提示词模板变了、工具返回的格式变了。这三个都是“静默变更”不重放根本发现不了。归因这块我建议做一个差异高亮视图把原始执行和重放执行并排展示不一致的地方标红。人工排查的时候一眼就能看到哪里不一样。4. 落地实操从零搭一套可观测性体系前面讲的是设计思路这一节讲具体怎么落地。我按搭建顺序来讲每一步都给出可操作的方案。4.1 第一步定义事件模型和存储选型事件模型就是前面说的schema先定下来。存储选型我试过三种Elasticsearch查询灵活适合做全文检索和聚合分析。缺点是写入成本高schema管理麻烦。ClickHouse写入性能极好适合海量日志聚合查询快。缺点是不支持全文检索复杂查询要写SQL。对象存储Parquet成本最低适合归档和批量分析。缺点是查询延迟高不适合实时排查。我现在的方案是ClickHouse做主存储对象存储做归档。实时排查走ClickHouse历史分析走对象存储。如果团队规模小直接用PostgreSQL也行JSONB字段足够灵活量不大的时候完全够用。建表的时候几个关键字段要建索引trace_id、timestamp、event_type、tool_name、status。这几个是查询最频繁的维度。4.2 第二步网关接入与埋点网关接入的方式我推荐用中间件模式。以Python为例如果AI员工是用FastAPI或者Flask写的可以写一个中间件拦截所有请求自动记录。app.middleware(http) async def observability_middleware(request, call_next): trace_id request.headers.get(X-Trace-Id) or generate_trace_id() start_time time.time() with observability_context(trace_id): response await call_next(request) record_event({ event_type: http_request, trace_id: trace_id, path: request.url.path, method: request.method, status_code: response.status_code, duration_ms: (time.time() - start_time) * 1000 }) return response工具调用的埋点建议封装一个统一的工具调用函数所有工具调用都走这个函数。这样埋点只写一次不会漏。async def call_tool_with_tracing(tool_name, params, trace_id, span_id): call_id f{trace_id}_{span_id}_{next_seq()} start time.time() try: result await real_tool_call(tool_name, params) status success except Exception as e: result {error: str(e)} status error record_event({ event_type: tool_call, trace_id: trace_id, span_id: span_id, call_id: call_id, tool_name: tool_name, input: params, output: result, status: status, duration_ms: (time.time() - start) * 1000 }) return result提示埋点函数里不要做复杂的加工逻辑尽量原样记录。加工逻辑放在查询侧做这样原始数据永远可追溯。4.3 第三步重放引擎的实现重放引擎我建议做成一个独立的服务不要和主服务混在一起。主服务负责记录重放服务负责读取记录并驱动重放。重放服务的核心接口就两个start_replay(trace_id, mode)和get_replay_result(replay_id)。start_replay创建一个重放任务异步执行返回replay_id。get_replay_result查询重放进度和结果。重放执行的时候要在一个隔离的环境里跑避免影响生产。隔离的方式可以是独立的进程、独立的容器或者至少是独立的数据库连接。我试过在同一个进程里跑重放结果重放时的工具调用不小心打到了生产数据库差点出事。后来改成独立容器彻底隔离。4.4 第四步可视化与告警可视化这块我建议做三个视图任务列表视图按时间倒序展示所有任务支持按状态、任务类型、耗时过滤。这是日常排查的入口。任务详情视图展示单个任务的完整执行时间线每个工具调用可展开看详情。这是排查具体问题的核心视图。重放对比视图并排展示原始执行和重放执行差异高亮。这是归因分析的核心视图。告警这块几个关键指标要监控任务失败率、工具调用超时率、平均执行耗时、token消耗异常。告警阈值根据业务情况定我一般设失败率超过5%告警超时率超过10%告警。5. 踩坑记录与常见问题排查这套东西搭下来踩的坑不少挑几个典型的讲。5.1 日志写入把主流程拖慢第一版方案里日志是同步写入的每次工具调用都要等日志写完才返回。结果工具调用本身只要50ms日志写入要200ms整体性能直接腰斩。解决办法是异步写入。日志先写内存队列后台线程批量刷到存储。这样主流程几乎不受影响。但要注意异步写入有丢日志的风险进程崩溃时队列里的日志会丢。我的做法是关键事件同步写非关键事件异步写。关键事件就是失败、超时、状态变更这些。5.2 重放时找不到对应的工具返回这个问题的根源是call_id生成规则不稳定。一开始我用的是trace_id 工具名 时间戳结果同一个工具在同一毫秒被调了两次call_id撞了。后来改成trace_id span_id 自增序号才稳定下来。还有一个原因是记录不完整。有些工具调用是在异常处理分支里发生的埋点没覆盖到。解决办法是把埋点放在最外层用try-finally保证一定记录。5.3 日志量太大查询慢日志量上来之后查询变得很慢。一个trace_id的查询要扫几百万行。解决办法是分区。按天分区查询的时候先定位到分区再查具体记录。另外把不常用的字段从主表拆出去放到扩展表主表只留查询最频繁的字段。5.4 常见问题速查表问题现象可能原因排查方法解决方案重放结果和原始不一致模型版本变了对比两次的模型版本号固定模型版本或记录版本差异重放卡住不动缺少工具返回记录检查ReplayMissError补全埋点确保所有调用都记录日志查询超时数据量太大或索引缺失看查询计划加分区、加索引、冷热分离日志写入影响性能同步写入看主流程耗时改异步写入关键事件同步重放时工具被真实调用隔离没做好看工具调用日志重放环境独立部署工具调用走拦截器日志字段缺失埋点遗漏对比schema和实际记录补埋点加schema校验5.5 几个独家避坑技巧第一个技巧在网关层加一个“记录完整性校验”。每次任务结束的时候校验一下这次任务的关键事件是否都记录了比如模型调用、工具调用、状态变更。如果缺了打一个告警。这样能及早发现埋点遗漏。第二个技巧重放引擎要支持“部分重放”。有时候只想重放某一步不想从头跑。支持从任意span开始重放能大大提升排查效率。第三个技巧日志里记一个“环境指纹”。包括代码版本、模型版本、提示词版本、工具版本。重放不一致的时候先对比环境指纹能快速定位是不是环境变了。第四个技巧给重放结果打标签。比如“一致”、“不一致-模型差异”、“不一致-工具差异”、“不一致-数据差异”。积累一段时间后能看出哪类差异最常见针对性优化。6. 从可观测性到可运营这套体系的延伸价值搭这套体系的初衷是排查问题但用起来之后发现它的价值远不止于此。最直接的延伸是成本归因。每个任务的token消耗、工具调用次数、耗时都记下来了可以按业务线、按用户、按任务类型做成本分摊。哪个业务线最烧钱一目了然。第二个延伸是质量评估。有了完整的执行记录可以离线跑评估。比如把历史执行拿出来用新模型重跑一遍对比效果。这比在线A/B测试更灵活因为可以回溯历史数据。第三个延伸是训练数据生产。AI员工的执行记录本身就是高质量的轨迹数据。筛选出成功的执行可以作为微调数据。筛选出失败的执行可以作为负样本。这套记录体系天然就是数据飞轮的原料。第四个延伸是合规审计。AI员工做的每一个决策、调用的每一个工具、产生的每一个结果都有完整记录。出了纠纷能拿出证据链。这在金融、医疗这些强监管行业是刚需。我在实际项目里的体会是可观测性这件事越早做越好。等到系统复杂了再补成本高十倍不止。而且这东西有个特点投入是线性的收益是指数的。刚开始只有排查问题一个用途用着用着成本、质量、数据、合规全都靠它。最后分享一个小技巧如果你现在系统里已经有日志了但很乱不要推倒重来。先加一个“日志规范化”层把现有日志转成统一schema再逐步替换。这样迁移成本最低业务也不会中断。
返回列表