资讯动态

多Agent系统生产审计:架构设计、执行语义与状态管理全解

发布时间:2026/10/17 5:31:01 来源:尧图企业网站定制
上周帮一个团队排查多Agent系统的生产事故前前后后折腾到凌晨三点。系统本身是典型的编排式架构——一个Planner负责拆任务三个Worker并行干活最后还有个Reviewer收口。表面上看分工清晰可一旦出了问题谁先在哪个环节做了哪个决定、基于什么上下文、改了什么数据竟然完全说不清楚。日志明明都在链路也能串个七七八八可真要回答“这个结论怎么来的”“那笔数据被谁改成这样的”直接卡壳。这不是个例。我接触过好几个把多Agent系统推上生产的团队几乎都遇到过同一堵墙系统能跑、效果也还行但没法审计。而“没法审计”这四个字在严肃业务场景里就是定时炸弹——金融合规过不去、客户投诉说不清、内部复盘全靠猜。所以这篇就专门聊聊多Agent系统进入生产后为什么审计这么难我会从架构设计、执行语义、状态管理几个层面拆开讲再附上我自己落地过的审计方案骨架和排查经验。适合正在做多Agent应用落地、或者已经在生产上被“锅甩不动”问题折磨的开发者、架构师和SRE们。1. 多Agent系统的生产审计到底卡在哪了1.1 从“单Agent”到“多Agent”审计对象发生了质变单Agent系统的审计说白了就三件事记输入、记输出、记录中间调了哪些工具。因为只有一个执行主体链路是线性的从头到尾一条线拉下来哪怕步骤多点也顶多是“日志长”的问题不存在“断”的问题。多Agent完全不是这个逻辑。系统里同时跑着好几个执行主体它们之间存在委托、协作、竞争、互相校验的关系。你的审计对象不再是“一条线”而是一张动态生成的协作网络。每一个Agent都可能发起子任务、调用外部工具、修改共享状态甚至和其他Agent并行交互。我习惯用一个类比单Agent就像一个人做外包项目甲方盯他一个就够了多Agent等于甲方找了一家装修公司公司里设计师、工长、水电工、监理各干各的最后你问“这面墙为什么是斜的”没人能一句话给你解释清楚。审计的复杂度从“记录”变成了“还原一段多人协作的完整过程”这是一切困难的源头。1.2 协作模式越灵活审计盲区越多多Agent系统的协作方式五花八门主流的有编排式、协商式、流水线式、黑board式几种。问题在于越灵活的协作模式越难被审计。编排式中心节点统一调度审计相对友好因为所有决策都经过一个大脑链路和决策点都有据可查。这也是为什么生产系统里编排式还是主流。协商式Agent之间互相谈判、讨价还价谁能说服谁谁就上。这个过程充满随机性可能谈判了五轮才定下来也可能两轮就崩了每一轮的内容都在变审计要记录“谈判过程”而不是“执行结果”。黑板式大家往一块共享黑板上写东西、读东西没有明确的调用关系。这模式下连“谁先看到什么、谁后来改了它”都很难精确还原跟几个人围着一块白板讨论方案事后让你复盘每个人说的每句话一样难。这不是说这些模式不能用而是说选型之前就要想清楚你想要协作的灵活性就得接受审计上的妥协。生产环境下我一般不推荐纯协商式或纯黑板式作为核心链路除非你有非常强的审计兜底方案。1.3 非确定性让“复现”成为奢侈品传统软件系统的审计有一个隐含前提同样的输入必然产生同样的输出。有问题时把当时的输入重放一遍就能复现、定位、修复。多Agent系统把这个前提打碎了。LLM本身就有温度参数带来的随机性再加上Agent决策过程中的开放式推理、上下文窗口的变化、外部工具返回的时序差异同一个请求跑十次十次的过程和结果都可能不一样。这意味着什么意味着你拿到一份审计日志发现某个Agent做了一个异常决策想把当时的输入原样丢回去复现一下——对不起它这次可能就不会再犯同样的错了。你没法通过“重放”来确认bug是代码逻辑引起的还是模型随机性导致的还是当时的上下文巧合造成的。这是我踩过最深的坑之一。后来我才意识到多Agent系统的审计目标不该是“精确复现”而是**“完整还原决策环境”**——把当时Agent看到的所有上下文、调用的参数、模型的输出都存下来让事后复盘的人能站在和Agent相同的信息条件下理解它为什么做出这个选择。这个思路的转变直接决定了一套审计方案到底能不能用。2. 审计难难在四个具体层面2.1 执行轨迹断裂你看到的不是完整链路多Agent系统里最常见也是最头疼的问题就是执行轨迹断裂。A调用BB调用了CC内部又起了两个并行任务结果其中一个任务的数据没传回来整个链路在日志层面就断了。断的原因很多Message Queue里消息丢失、Agent内部异常导致没有返回、子任务超时被父节点放弃但子任务还在继续跑、有时候纯粹是日志组件自己承受不住高并发把trace丢了。链路一断审计人员就抓瞎了——你只能看到“A发出了调用然后就没有然后了”中间发生了什么全是黑洞。我在生产环境见过一个典型事故一个Worker Agent在处理用户退款请求时先从数据库读取了订单状态然后调用支付网关退款最后再更新订单状态。第二步调用卡了很久触发了超时外层的Planner认为这次任务失败重新分配了另一个Worker去处理同一个订单。结果第一个Worker其实在超时前已经退款成功了只是响应没回来第二个Worker看不到更新后的状态又退了一次。从日志上看两次执行都有记录但没有任何一条链路是完整的如果不是实在没法跟用户交代、拉了三方数据对账这个双退款问题根本定位不了。2.2 上下文漂移每个Agent理解的“当前状态”各不相同多Agent系统里每个Agent都有自己独立的上下文窗口。A和B同时处理一个任务时A看到的用户资料是10:03分的版本B看到的是10:05分的版本等它们汇合的时候到底该以谁看到的为准这个“上下文不一致”的问题我用一个词概括叫上下文漂移。它不像轨迹断裂那样直观但危害更大因为它直接导致审计结果“看起来没毛病实际上对不上”。举例说明一个风控Agent和一个营销Agent同时读取用户画像。风控Agent看到的是最新的、含风险标记的数据于是拒绝了一笔交易营销Agent看到的还是昨日的、干净的画像所以给这个用户推送了大额优惠券。事后审计只看单个Agent的决策过程每个都是合理的但你横向对比就会发现两个Agent基于的信息根本不在一个版本上。这还算好的至少各自的行为能说通更麻烦的是当Agent之间需要协作时A把自己看到的上下文传给BB拿到的已经是过时的信息但B不知道还会基于这个信息做决策。审计层面单个Agent的日志再完整也没用你必须有办法对齐所有Agent在同一时间点的上下文版本。做不到这一点审计结果就是“各说各话”。2.3 状态变更不透明悄悄改了谁的数据如果说轨迹和上下文是审计的“过程”那状态变更就是审计的“结果”。多Agent系统在生产的核心价值就是能自动完成一系列真实世界的操作——改数据库、调外部API、发消息、创建订单。而这些操作一旦出错影响是实打实的。状态变更审计难难在两点第一变更入口分散。不同Agent可能通过不同方式去改数据有的直接连数据库有的通过内部API有的调用外部服务。每个入口都有自己的日志格式要统一收集和关联非常费劲。第二变更意图不透明。一个Agent执行了“更新订单状态为已发货”这个操作审计时要回答的不只是“谁改的”还有“为什么改”——是用户要求的是另一个Agent通知的还是自己根据规则判断的这些意图往往埋在Agent的推理过程里不会直接出现在数据库日志或API调用日志中。我强烈建议在生产环境给所有Agent的数据写操作加一道统一的“审计网关”而不是直接让Agent连数据库。用Andon板、插线板那种概念理解也行——所有电器的电都从这过哪出了问题第一时间就能知道是哪一路电出的问题。2.4 权限边界模糊账号混用导致无法定责权限审计在多Agent系统里是一个容易被忽略但很致命的问题。很多团队初期为了省事所有Agent共用一个服务账号谁操作的在数据库层面和日志层面都只能看到一个账号。这意味着什么假设系统里有三个Agent都在处理订单某天一个脏数据写进了订单表排查下来只能确定“服务账号A干的”但到底是三个Agent里的哪一个完全无从谈起。如果涉及内部问责或者外部合规检查这直接就卡死了。我现在做多Agent系统设计有一条硬性要求每个Agent必须有独立身份。哪怕底层用的是同一个服务账号在业务层也必须注入Agent级别的标识数据库连接参数、API调用授权、日志字段里都要有agent_id。宁可运维上多花点功夫也不能让审计“查不到人”。3. 可观测性不等于审计先把这个误区掰开3.1 日志、链路追踪、审计追踪是三种东西很多团队觉得我上了日志系统、链路追踪、metrics监控不就等于有审计能力了吗这个误区的代价通常要到合规检查或重大事故复盘时才付出来。日志Logging是记录发生了什么事属于事实记录链路追踪Tracing是记录请求的调用路径属于过程还原而审计追踪Audit Trail则是回答谁、在什么时间、基于什么信息、做了什么事情、结果如何、为什么这么做。它们在记录内容、使用方式上差异很大维度日志链路追踪审计追踪核心问题系统发生了什么请求经过了哪些环节谁在什么背景下做了什么决定典型字段时间、级别、消息trace_id、span_id、耗时actor、decision_context、reason主要使用者开发、运维开发、SRE合规、安全、业务复盘保存周期短则几天短则一周通常半年起步是否要求完整允许有缺失允许采样必须完整且不可篡改日志丢了还能忍受链路丢了可以重新采样但审计追踪一旦有缺失整个链条就不可信了。这就是为什么拿日志系统当审计系统用迟早要出事的根本原因。3.2 为什么很多团队“指标很漂亮审计仍是空白”这几年LLM应用爆火以后各大可观测性平台都出了LLM相关的监控方案什么token消耗、首token延迟、模型调用次数一个比一个漂亮。但这些指标能回答“系统健康吗”回答不了“这个决定为什么这么做”。我自己用过的经验是观测平台把Agent调用LLM的次数精确到个位耗时精确到毫秒但当我需要知道某个Agent当时被注入了什么system prompt、带了哪些检索上下文、为什么选了这个工具而不是那个工具时这些平台给不了。根本原因是审计关注的是“决策逻辑域”而不是“系统运行域”。系统运行域的指标再丰富也无法自动推导出决策域的上下文。就像监控摄像头再多也只能记录“谁进了哪个房间”记录不了“他为什么进这个房间以及他有没有在前台登记过”。只有针对决策过程专门设计的审计机制才能覆盖这个盲区。3.3 审计事件模型的最小集合基于上面的分析我给多Agent系统的审计事件模型定了六个必备字段缺一个都不行action做了什么比如“调用工具”“修改数据库”“发送消息”。actor谁做的必须是Agent粒度不能是系统粒度。context_hash决策那一刻的上下文指纹。decision_input触发该动作的关键输入比如用户指令、上游Agent消息。result动作的结果成功、失败、部分成功以及返回值。reasonAgent自己给出的决策理由或依据。每条审计记录本质上是这六个字段的组合。围绕这个最小模型去设计系统可比追着一堆日志文档自己拼线索省心太多。4. 面向审计的多Agent系统设计建议4.1 全局唯一Trace ID从入口一路透传到底如果说审计体系建设有哪一件事必须最先做我的答案一定是全局唯一的Trace ID透传。具体做法是在请求入口生成一个trace_id通过HTTP Header、消息队列消息头、函数调用参数一路透传下去所有的Agent执行节点、LLM调用、工具调用、数据库操作都记录这个id。这样无论链路怎么拆怎么并审计时都能按trace_id把所有事件拉出来。这块最大的坑在Agent内部。如果Agent框架本身有自己的trace机制和你业务代码里的trace机制不打通就会出现“上游有trace_id下游也有trace_id但对不上”的情况。我见过最离谱的一个请求跑下来有四个不同的“trace”串都串不起来。建议在框架层把trace_id注入到Agent的上下文对象里或者在Agent框架做代码钩子由它统一往子任务里传trace_id这样业务代码就不需要关心了。每次进入新Agent或新任务时先检查trace_id是否还在不在就报警宁肯多花一点性能也要保证贯穿。4.2 决策快照把每次LLM调用的“输入-输出-参数”都存下来这一条是多Agent系统审计和传统分布式系统审计最不一样的地方。在传统系统里函数调用的输入输出可能不需要专门存档靠日志就能重建但在多Agent系统里LLM调用是黑盒决策的核心环节必须做完整的“决策快照”。一个决策快照至少包括模型名称和版本号温度参数、max_tokens等采样参数system prompt全文注入的上下文内容按token数限制做截断并记录截断位置用户消息全文模型输出全文响应时间、token消耗当时的上下文窗口占用率存这些不只是为了合规更实际的用途是劣化分析——比如线上某个Agent突然开始输出重复内容你就得知道是不是上下文窗口被塞满了客户投诉答复质量下降你得看是不是某个版本的prompt改出了问题。存储成本的问题大家都会担心但实测下来只要做裁剪——不需要逐token存存大版本、摘要、关键片段——成本完全可控。4.3 状态变更审计统一拦截写操作我前面提到了“审计网关”的思路这里展开讲一下如何落地。多Agent系统里的状态变更五花八门但本质上都可以归为三类数据库写操作、外部API调用、消息发送。我的做法是对这三类操作分别做审计wrapper所有Agent对外的写操作必须经过wrapper才能执行。数据库写操作以ORM层做统一拦截记录表名、主键、变更前后值、SQL语句。外部API调用通过HTTP Client层做统一拦截记录URL、请求体摘要、响应状态、耗时。消息发送通过消息发送组件统一拦截记录队列名、消息内容摘要、投递结果。这三层wrapper的日志都加上trace_id和agent_id审计时就能把“Agent决策”和“系统状态变更”完美串起来。谁决策的、改了什么、什么时候改的一目了然。这里有一个重要提醒审计网关永远不要传Agent的“原话”而是记录它的“结构化意图”。收到Agent请求时先由网关解析出操作类型和目标对象入库的也是解析后的结构化数据这样即使Agent某次输出是非结构化的审计记录依然可以保持一致。4.4 身份透传每个Agent都该有独立身份证身份透传的根本目的是让审计的“actor”字段永远有值且准确。具体到落地我总结为三点Agent注册表系统启动时所有Agent在注册表中登记拿到唯一的agent_id和对应权限。调用上下文注入Agent发起的任何外部调用不管是HTTP、数据库还是消息队列都要带上自己的agent_id。授权模型收敛不要让Agent直接以特权账号操作而是用“最小权限映射”——每个agent_id映射一组最小权限权限变更留痕。这套身份体系一开始只是为了让审计能查“人”后来发现它的好处远不止审计——出了问题能精确定位到具体Agent做限流、降级、定向灰度就都有了抓手。4.5 审计存储与保留策略审计数据是越攒越多的如果不提前规划存储成本和查询性能很快会成为新瓶颈。我的实践经验是分两层存热存储保留近30天的原始审计事件用ClickHouse或ES这类列式存储支持快速检索和关联分析。冷存储超过30天的数据压缩后转对象存储按天打包保留至少180天或者按合规要求更久。冷数据不提供在线查询需要时恢复特定天数的索引。这里要特别强调审计日志的不可篡改性。生产环境下让Agent自己写审计日志等于没有审计——权限足够大的Agent可以先篡改日志再干坏事。对高风险操作建议用独立的审计日志服务加校验链。技术上也不用搞什么分布式账本写一条校验哈希、定期做哈希链比对就够了。5. 落地实操一个最小可用的审计追踪方案5.1 审计事件表设计这一套落地最核心的就是一张大表设计如下同时考虑几类不同的查询需求我习惯直接建三张关联表audit_event审计主事件表字段名类型说明event_idvarchar(64)审计事件唯一IDtrace_idvarchar(64)全局链路IDagent_idvarchar(64)执行Agent IDactionvarchar(64)动作类型llm_call、tool_call、db_update、msg_senddecision_inputtext触发决策的关键输入摘要resultvarchar(16)success / failure / partialreasontextAgent给出的决策理由context_hashvarchar(64)上下文指纹哈希created_atdatetime事件产生时间llm_call_snapshotLLM调用快照表字段名类型说明event_idvarchar(64)关联audit_eventmodel_namevarchar(64)模型名称model_versionvarchar(64)模型版本temperaturefloat采样温度max_tokensint最大输出token数system_prompttextSystem prompt全文user_messagetext用户消息全文context_snapshottext注入的上下文摘要outputtext模型输出token_usageinttoken消耗created_atdatetime记录时间state_change状态变更明细表字段名类型说明event_idvarchar(64)关联audit_eventtarget_typevarchar(32)变更对象类型DB/API/MQtarget_namevarchar(128)目标库表/接口/队列名operationvarchar(32)INSERT/UPDATE/DELETE/POSTbefore_valuetext变更前值after_valuetext变更后值source_ipvarchar(45)来源IPcreated_atdatetime变更时间三张表用event_id关联查询时先按trace_id筛出主事件再分别关联快照和状态变更。5.2 关键代码路径的埋点思路有了表结构接下来就是在代码里埋点。以Python为例我在Agent的主流程里封装了一个审计上下文对象所有Action都通过它来记录。class AuditContext: def __init__(self, trace_id, agent_id): self.trace_id trace_id self.agent_id agent_id self.events [] def record(self, action, decision_input, result, reason, context_hash): event { trace_id: self.trace_id, agent_id: self.agent_id, action: action, decision_input: decision_input, result: result, reason: reason, context_hash: context_hash, created_at: datetime.utcnow().isoformat() } self.events.append(event) # 异步写入审计队列 audit_queue.send(event)Agent内部统一用这个上下文记录事件不做别的事。关键点在于record方法必须是异步非阻塞的否则会因为审计本身的IO开销拖慢整个Agent的执行。再来看一个LLM调用快照的埋点示例def llm_call_with_audit(audit_ctx, model_cfg, system_prompt, user_message, context): # 生成上下文指纹 context_hash hashlib.sha256( json.dumps(context, ensure_asciiFalse).encode() ).hexdigest() # 记录决策输入 audit_ctx.record( actionllm_call, decision_input{user_message: user_message[:500]}, resultpending, reasonagent initiated llm call, context_hashcontext_hash ) response model_cfg.invoke(system_prompt, user_message, context) # 记录快照 snapshot { event_id: audit_ctx.last_event_id, model_name: model_cfg.name, model_version: model_cfg.version, temperature: model_cfg.temperature, max_tokens: model_cfg.max_tokens, system_prompt: system_prompt, user_message: user_message, context_snapshot: json.dumps(context)[:2000], # 预算控制 output: response.text, token_usage: response.usage.total_tokens } audit_queue.send_snapshot(snapshot) audit_ctx.record( actionllm_call, decision_input{user_message: user_message[:500]}, resultsuccess, reasonllm response received, context_hashcontext_hash ) return response.text这里有几个实操细节值得展开上下文指纹我用整个上下文的哈希值作为context_hash作用是标记决策环境的唯一性。如果两个事件指纹相同可以断定它们基于完全相同的上下文这比反复比对具体内容高效得多。摘要截断decision_input里我只存前500个字符context_snapshot截断到2000个字符。别贪全全部存下来成本高、查询慢真查起来也几乎不会全文读。摘要的目的是快速筛选需要全文时再回源对象存储捞。异步队列审计事件发送我用的队列在内存里用缓冲区、工人线程批量写库结构上等价于一个本地缓冲加批量刷新。一开始图省事直接同步写数据库结果Agent的响应时间飙升了30%改成异步批量写后就基本没有影响了。5.3 与现有系统的对接与告警审计数据收集起来不能烂在库里得对接现有告警链路否则又是“数据有但没人看”的死局。我按以下优先级做告警对接效果很直接高风险状态变更告警比如某个Agent直接执行了删除数据库行的操作立即触发告警。多Agent系统里删除操作永远应该是需要人工确认的高危动作。审计事件缺失告警如果trace_id已经创建但在3分钟内没有产生任何审计事件说明链路可能断了或者Agent挂了。决策重复告警同一个trace_id下某个Agent重复执行了同一动作超过N次大概率是agent陷入了死循环。对接告警系统时可以保留原始日志用于深查告警本身用来控制爆炸半径。用更通俗的话说审计的第一价值不是事后复盘而是出事时最快速度止损。告警能把“事后查半年日志”压缩成“当时就知道哪里不对”。6. 常见问题与排查技巧实录6.1 常见问题速查表我把实际生产中经常碰到的问题整理了一下基本涵盖了多Agent系统审计的典型症状问题现象根本原因排查切入点日志里看到Agent调用了工具但结果没记录工具调用返回异常Agent未将结果写回审计检查工具调用后的异常处理分支trace_id在下游Agent中消失Agent框架内部新建了上下文没有做trace透传看Agent框架的上下文初始化代码同一个动作重复记录但时间间隔很短Agent内部循环重试每次迭代都写审计对比trace_id下同一action的连续时间差审计表数据也有但查询特别慢没有按trace_id/时间建索引或者索引失效EXPLAIN查询计划重建索引模型输出内容在审计里是空的输出流式返回但快照记录在流结束前执行等流式生成完成后统一记录两个Agent处理同一订单时数据不一致并行读取了同一份源数据没有锁或版本控制看context_hash是否一致不一致说明上下文漂移6.2 排查定位的具体过程拿一个真实案例来演示排查流程。某个在线客服系统用户反馈“我明明取消了订单但账单还是被扣款了”。第一步按用户会话ID找到入口trace_id从audit_event表拉出所有事件。看到两个trace_id——原来是Planner超时后重试开了个新trace。这里就暴露了第一个问题重试时没有沿用原trace_id。第二步对比两个trace的事件。第一个trace里Agent查了订单状态是“active”然后调用了扣款接口第二个trace里Agent也查了订单状态但查到的时候第一个Agent还没更新状态所以也调用了扣款接口。这就解释了为什么扣了两次。第三步看state_change表。第一个trace的Agent确实执行了“置为已取消”的update操作但时间是在“扣款接口”调用之后——原因找到了Agent是先扣款后改状态改状态这个动作发生在扣款之后。整个定位过程不到20分钟。换作没有审计体系之前光是从三个系统的日志里拼出这两条链路就得花至少半天。6.3 几个容易踩的坑下面这些坑是我一遍遍趟出来的希望后来的人少交点学费。坑一采样审计数据。有些团队为了省成本按百分比采样存审计。这在可观测性场景没问题但在审计场景直接判死刑——你想查的那条数据恰好没被采到的概率不低。审计数据的红线是全量采集不能采样。坑二只在关键节点审计。只记录Agent的输入输出不记录中间的工具调用和决策理由等于只看到了“做了什么”看不到“为什么做”。有些案例里Agent自己都忘了几轮之前做过的决策你不把中间过程存下来事后根本没法对齐。坑三审计和主业务流程同生命周期。主流程回滚了审计日志也跟着回滚等于没审。审计日志应该独立于业务事务即使业务失败审计记录必须保留而且要记录的是“尝试过什么”而不是“成功了什么”。坑四小看时间同步问题。多Agent系统跨容器跨机器部署各节点时间不一致审计事件排序全是乱的。生产环境一定要配置NTP同步记录时间统一用UTC展示时再转本地时区。我遇到过因为时间差五分钟导致事件前后顺序颠倒、差点冤枉了一个Agent的案例。坑五上下文快照只存指针不存内容。为了省存储一些方案只记录“上下文引用了哪个对象、哪条数据”不记录当时的实际内容。这下可好源数据一变你审计时看到的上下文和Agent当时看到的完全两样等于废了。上下文快照必须是独立于源数据的物理副本哪怕只存摘要也行不能只存指针。结尾不另写总结就在最后一个实操要点的地方收住再补一小段个人体会收尾。我个人在做过多套多Agent系统的落地后最深的感觉是审计这件事做得早还是做得晚差别很大。刚开始总觉得“跑起来再说日志先打上”但等到出了事故再来补审计不仅要改一堆Agent框架代码还得面对已经丢掉的决策上下文后悔都来不及。先花两周把审计事件模型定了、埋点铺好、身份透传做了后面整个生产周期的排障、复盘、合规环节都会轻松非常多。还有一个小技巧分享给大家给每个Agent起一个好认的名字比如payment-worker-v2、risk-check-main别看这不起眼真到对账查日志时可比一串UUID舒服太多了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑