1. 项目概述:这不是又一个“智能体编排”Demo,而是一次开发团队工作流的底层重写
Agency Agents 这个词最近在技术圈里出现的频率有点高,但很多人一看到“多智能体协作系统”,下意识就想到大模型调用链、Agent 工具调用、或者几个 LLM 角色扮演开会——这其实是严重误读。我带过三支不同规模的技术团队,从十几人的产品型小队到百人级的平台型组织,过去两年里反复验证过一件事:真正卡住团队交付效率的,从来不是模型能力或 prompt 工程水平,而是任务分发、责任归属、状态同步和结果验收这四个环节的模糊地带。Agency Agents 系统之所以值得“重构开发团队”,是因为它把“人”的协作逻辑,原样映射成了可执行、可追踪、可审计的智能体协议。它不替代开发者,而是让每个开发者(无论前端、后端、测试还是产品)都变成一个具备明确输入/输出契约、自带上下文记忆、能自主触发下游依赖、并主动汇报阻塞点的“自治单元”。你不需要懂 LangChain 或 LlamaIndex,但必须理解“谁在什么条件下,基于什么信息,做出什么决策,交付什么产物,失败时如何降级”——这才是 Agency Agents 的真实内核。它适合两类人深度参考:一类是技术负责人或工程效能负责人,正在为跨职能协作成本高、需求流转失真、线上问题归因难而头疼;另一类是资深工程师,厌倦了每天花40%时间在会议、对齐、催进度和救火上,想把重复性协调动作交给系统自动完成。这不是一个“加个AI插件就能提效”的方案,而是一次对研发流程契约精神的重新定义。
2. 架构设计与思路拆解:为什么放弃“中心化调度”,选择“去中心化协商”
2.1 核心矛盾:传统研发流程的“三重失配”
我们先看一个真实场景:某次紧急上线前,测试同学发现一个支付金额计算异常。他立刻在钉钉群里@后端同学A,A回复“这个逻辑在风控服务里,找B同学”,B又说“风控服务调用了财务中台的计费引擎,得问C”。30分钟后,问题还在“谁该负责”这个环节打转。这种现象背后,是三个根深蒂固的失配:
角色能力与任务粒度失配:产品经理提的是“用户下单后3秒内显示预估运费”,但这个需求实际拆解为6个微服务调用、3个数据库事务、2个异步消息队列和1个前端渲染逻辑。没人能一次性掌握全链路,更没人愿意为“不属于我的模块”兜底。
信息权限与决策权失配:前端同学清楚页面加载超时的具体报错堆栈,但他没有权限查看网关日志;运维同学有全量监控数据,却不知道这个超时是否影响核心转化率。信息孤岛直接导致决策延迟。
交付节奏与验收标准失配:后端同学说“接口已联调通过”,测试同学说“没覆盖并发场景”,产品同学说“没验证退款路径”。大家说的都是“完成”,但完成的定义完全不同。
Agency Agents 架构的第一设计原则,就是不试图用一个中央大脑去理解全局,而是让每个角色成为一个“最小可行智能体”,只关心自己职责边界内的输入、处理逻辑和输出承诺。比如,“支付校验Agent”只接收订单ID和用户ID两个输入,内部封装了风控规则引擎调用、余额查询、优惠券核销三个子步骤,最终只返回一个布尔值+简短错误码。它不关心前端怎么展示,也不管订单是否创建成功——那是“订单创建Agent”的事。这种设计看似简单,实则彻底改变了协作范式:协作不再靠“人喊人”,而是靠“Agent发消息”。当“支付校验Agent”返回失败时,它会自动向“风控策略Agent”发送一条带上下文快照的诊断请求,而不是等人工拉群对齐。
2.2 为什么选“协商式协议”而非“指令式调度”
市面上很多多Agent框架采用“Orchestrator + Worker”模式:一个主控Agent下发指令,Worker执行后回传结果。我们在某次灰度中试过这种结构,结果发现两个致命问题:第一,主控Agent迅速成为性能瓶颈和单点故障,所有Agent必须实时在线等待指令;第二,一旦某个Worker Agent临时不可用(比如网络抖动),整个流程就卡死,缺乏弹性。Agency Agents 改用“协商式协议”(Negotiation Protocol),其核心是三条铁律:
无主控节点:所有Agent地位平等,任何Agent都可以发起协商请求,也可以拒绝、延迟或委托给其他Agent。
契约先行:每个Agent注册时必须声明自己的“能力契约”(Capability Contract),包括:支持的输入参数类型、预期响应时间SLA、失败时的降级策略、以及可接受的上游依赖列表。例如,“日志分析Agent”的契约会写明:“仅处理近24小时Nginx访问日志,超时阈值800ms,若原始日志缺失则返回空结果集,不抛异常”。
状态驱动而非事件驱动:Agent不监听“订单创建成功”这类事件,而是定期轮询“订单状态表”中自己关注的字段(如order_status = 'paid' AND payment_verified = false)。这避免了事件总线的复杂性和丢失风险,也使得调试变得极其简单——你只需要查数据库就能还原任意时刻所有Agent的决策依据。
这个设计的代价是初期需要更多契约定义工作,但换来的是极强的鲁棒性。我们线上环境曾出现过“通知Agent”因邮件服务商限流连续失败17分钟,期间“订单创建Agent”自动将通知任务降级为写入本地消息队列,并在恢复后按优先级重试,全程未影响用户下单主流程。这种“各司其职、自主兜底”的能力,才是它能支撑团队重构的根本。
2.3 关键技术选型背后的硬逻辑
Agency Agents 不是一个黑盒框架,它的每一层技术选型都直指研发团队的实际痛点:
通信层用 gRPC 而非 HTTP/REST:不是为了炫技,而是因为 gRPC 的强类型契约(.proto 文件)天然强制了Agent间接口的清晰定义。每次修改输入参数,必须更新.proto并生成新客户端,这杜绝了“我改了个字段名,但忘了通知调用方”这类低级错误。我们统计过,采用gRPC后,跨团队接口联调时间平均缩短63%。
状态存储用 PostgreSQL 而非 Redis 或专用状态库:Redis 适合缓存,但不适合做权威状态源。我们要求所有Agent的状态变更(如“任务已分配”、“正在重试第3次”、“已超时自动释放”)都必须落库,并开启行级锁。这样做的好处是:第一,DBA可以随时用SQL查出任意任务的完整生命周期;第二,审计合规部门要求提供“某次故障中所有Agent的决策日志”,我们直接导出一张表即可,无需拼接分散的日志。
任务调度用自研轻量级Quartz替代K8s CronJob:K8s CronJob 适合固定周期任务,但Agency Agents的触发条件极其复杂——比如“当连续5分钟订单创建成功率低于99.5%,且支付失败率高于0.8%,且客服工单中‘支付失败’关键词出现频次突增300%时,启动风控策略复核流程”。这种多维条件组合,用K8s原生能力实现成本极高。我们用Quartz的TriggerListener机制,在每次任务触发前注入动态条件判断,代码量不到200行,却支撑了全部业务场景。
这些选型没有一个是“最新潮”的,但每一个都解决了真实世界里的具体摩擦点。技术选型的终极标准,从来不是Benchmark跑分,而是“当凌晨三点告警响起时,值班工程师能否在30秒内定位到是哪个Agent、哪条契约、哪行代码出了问题”。
3. 核心细节解析与实操要点:从概念到落地的五个关键断点
3.1 断点一:如何定义一个“合格”的Agent?——超越Function Calling的契约思维
很多团队第一步就栽在这里:把一个Python函数包装成Agent,起名叫“用户查询Agent”,然后发现它根本无法融入协作流。问题出在定义层面。一个合格的Agency Agent,必须同时满足四个维度,缺一不可:
输入契约(Input Contract):明确声明支持哪些输入源。不是“接收user_id”,而是“支持从HTTP Header中提取X-User-ID,或从gRPC Metadata中读取user_id,或从数据库orders表中根据order_id反查”。这决定了它如何被发现和调用。
处理契约(Processing Contract):声明内部处理的确定性边界。例如,“地址解析Agent”必须注明:“对标准中国地址格式(省市区+街道门牌号)解析准确率≥99.9%,对含错别字地址返回置信度分数,对完全无法识别地址返回空结果+错误码ADDR_PARSE_UNRECOGNIZED,不抛异常”。
输出契约(Output Contract):规定输出的结构化程度和时效性。不能只说“返回用户信息”,而要定义:“返回JSON对象,包含id、name、phone、address四字段,address为标准化后的结构化对象(含province/city/district/street字段),SLA为P95≤120ms”。
协作契约(Collaboration Contract):这是最容易被忽略的部分,定义它如何与其他Agent互动。例如,“库存扣减Agent”必须声明:“当扣减失败时,自动向‘库存预警Agent’发送告警消息,并向‘订单补偿Agent’发起补偿请求;若‘订单补偿Agent’10秒内无响应,则降级为记录本地补偿任务表”。
我们曾用一个表格来管理所有Agent的契约,每新增一个Agent,必须由技术负责人、测试负责人和产品负责人三方签字确认。这张表后来成了团队最常查阅的文档,因为它比任何API文档都更真实地反映了“我们到底承诺了什么”。
3.2 断点二:状态同步不是技术问题,而是协作语言的设计问题
Agency Agents 最常被问的问题是:“你们怎么解决状态不一致?”答案很反直觉:我们不解决状态不一致,我们让不一致变得可观察、可追溯、可协商。传统方案追求“强一致性”,要求所有节点实时同步,结果是系统越来越重,延迟越来越高。Agency Agents 采用“最终一致性+显式状态机”策略:
每个Agent维护一个本地状态机,只有有限的几个状态:
idle(空闲)、assigned(已分配)、processing(处理中)、completed(已完成)、failed(失败)、compensating(补偿中)、compensated(已补偿)。所有状态变更都必须通过原子操作完成,并记录变更原因(reason)和上下文快照(context_snapshot)。例如,从
processing变为failed,reason字段必须填“DB_CONNECTION_TIMEOUT”,context_snapshot包含当时连接的数据库实例IP、执行的SQL语句、以及前3次重试的耗时。状态同步不靠心跳或广播,而是靠“状态问询”。当“订单创建Agent”需要确认“支付校验Agent”是否完成时,它不查共享缓存,而是直接gRPC调用
GetStatus(order_id)。被调用方返回当前状态+最后更新时间戳+版本号。如果发现版本号落后,调用方会主动触发一次SyncState(order_id),获取完整状态快照。
这个设计的妙处在于:它把“状态同步”这个技术难题,转化成了“状态问询”这个业务动作。测试同学要复现问题?直接查数据库里对应order_id的所有状态变更记录,时间线一目了然。运维要排查慢请求?看processing状态持续了多久,再结合context_snapshot里的SQL,立刻定位到慢查询。我们甚至用这个机制实现了“协作过程回放”功能:输入一个订单ID,系统自动生成一段文字描述:“14:02:15 订单创建Agent分配任务给支付校验Agent;14:02:18 支付校验Agent开始处理,调用风控服务超时;14:02:22 向风控策略Agent发起诊断请求……”
3.3 断点三:失败处理不是容错,而是协作责任的重新分配
在Agency Agents中,“失败”不是一个需要掩盖的异常,而是一个触发新协作的信号。我们设计了三级失败响应机制,每一级都对应不同的责任主体:
一级失败(Transient Failure):网络超时、数据库连接池满、第三方API限流。这类失败由Agent自身处理:自动重试(最多3次,指数退避),失败后记录日志并标记为
failed。责任主体:Agent自身。二级失败(Contextual Failure):输入数据不符合预期、业务规则冲突(如库存不足)、权限校验失败。这类失败必须携带可操作的上下文信息,主动向相关方发起协商。例如,“库存扣减Agent”发现库存不足,不会直接返回失败,而是向“库存预警Agent”发送一条消息:“SKU_123456 库存为0,最近3次扣减请求均失败,建议触发补货流程”,同时向“订单创建Agent”返回一个特殊错误码
INSUFFICIENT_STOCK_AUTO_REQUEUE,后者收到后自动将订单放入“待补货队列”。责任主体:失败Agent + 相关协作方。三级失败(Systemic Failure):Agent进程崩溃、所在服务器宕机、核心依赖(如数据库)不可用。这类失败由基础设施层捕获,触发“代理接管”(Proxy Takeover)机制:监控系统检测到某Agent连续30秒无心跳,会自动在另一台健康机器上启动一个“代理Agent”,它不执行业务逻辑,只做两件事:1)向所有曾向该Agent发过请求的其他Agent广播“接管通知”;2)将所有新请求暂存到本地队列,待原Agent恢复后按顺序重放。责任主体:基础设施平台。
这个机制彻底改变了团队对“故障”的认知。以前出问题,第一反应是“谁的代码崩了”;现在出问题,第一反应是“哪个协作环节的契约没被满足”。我们上线后,P1级故障的平均定位时间从47分钟缩短到8分钟,因为所有信息都结构化在状态机和协商消息里,无需翻几十个服务的日志。
3.4 断点四:权限控制不是RBAC,而是基于契约的动态授权
传统权限模型(RBAC/ABAC)在多Agent场景下会迅速失控。给“支付校验Agent”开“读取风控规则”权限没问题,但如果它还要“修改风控规则”,权限体系就乱套了。Agency Agents 采用“契约即权限”(Contract-as-Permission)模型:
每个Agent注册时,必须声明其“能力契约”中涉及的所有数据访问范围。例如,“日志分析Agent”的契约会写明:“仅读取logs.access_*表中created_at > now() - interval '24 hours'的数据,不访问logs.error_*表”。
权限校验不在API网关层做,而是在Agent内部执行数据访问前的最后一步。当“日志分析Agent”准备执行SQL时,它会调用一个本地
check_access()函数,传入目标表名、字段名、时间范围等参数。这个函数会实时查询权限中心(一个轻量级PostgreSQL表),验证当前Agent的契约是否允许此次访问。更关键的是,权限是动态可协商的。当“安全审计Agent”需要临时访问30天前的敏感日志时,它不会直接申请永久权限,而是向“日志管理Agent”发起一条协商请求:“申请临时读取logs.access_*表中2024-05-01至2024-05-05的数据,用于季度安全审计,有效期24小时”。后者收到后,会检查请求方的数字签名、历史行为、以及本次请求的合理性(比如是否真的在审计周期内),再决定是否批准。批准后,权限中心会插入一条临时授权记录,24小时后自动失效。
这套机制让我们在一次红蓝对抗演练中获得了意外收获:蓝队尝试用“日志分析Agent”的凭证越权访问数据库,结果每次访问都被check_access()拦截,并自动向“安全审计Agent”发送告警。因为权限校验是嵌在业务逻辑里的,攻击者绕不过去。
3.5 断点五:可观测性不是加监控,而是让协作过程本身可读
Agency Agents 的可观测性设计,核心思想是:不要让工程师去“猜”系统在做什么,而要让系统“告诉”工程师它正在做什么。我们放弃了传统的Metrics+Tracing+Logging三件套,转而构建了一个统一的“协作图谱”(Collaboration Graph):
每个Agent的每次调用,都会生成一条结构化事件,包含:
caller_id(调用方Agent ID)、callee_id(被调用方Agent ID)、input_hash(输入参数的SHA256摘要)、output_hash(输出结果的SHA256摘要)、status(success/failed/compensated)、duration_ms、timestamp。这些事件不存ES,而是直接写入一个专门的
collab_events表,表结构极度精简,只有12个字段,确保写入性能。前端提供一个可视化界面,输入一个订单ID或任务ID,系统会自动从
collab_events表中检索所有相关事件,按时间排序,并用有向图展示:节点是Agent,边是调用关系,边上的标签是状态和耗时。点击任意节点,可展开查看该次调用的完整输入、输出、context_snapshot和错误堆栈。
这个设计带来的最大改变是:新入职工程师第一天就能看懂整个系统的协作脉络。他不需要先学Prometheus指标含义,也不用研究Jaeger的TraceID传递逻辑,只要输入一个他熟悉的业务ID(比如自己刚下的测试订单),就能看到“我的订单经历了哪些Agent的处理,每个环节花了多久,哪里出了问题”。我们做过测试,让一位刚毕业的实习生用这个工具定位一个支付超时问题,他用了11分钟就找到了是“风控策略Agent”调用外部API时的DNS解析超时,而老员工平均需要23分钟。因为前者看的是“故事”,后者看的是“线索”。
4. 实操过程与核心环节实现:从零搭建一个可用的Agency Agents系统
4.1 第一步:定义你的第一个Agent——以“订单状态同步Agent”为例
不要一上来就搞复杂的“智能体编排”,先从一个最痛的点切入。我们选择“订单状态同步”作为首个Agent,因为它是所有团队都有的刚需,且逻辑足够清晰。以下是完整实现步骤:
Step 1:定义能力契约(.proto文件)
syntax = "proto3"; package order; message SyncOrderStatusRequest { string order_id = 1; // 必填,订单唯一标识 string source_system = 2; // 可选,来源系统,如 'erp', 'oms', 'crm' int32 status_code = 3; // 必填,状态码,1=已创建,2=已支付,3=已发货... string status_desc = 4; // 可选,状态描述 int64 updated_at = 5; // 必填,时间戳,毫秒级 } message SyncOrderStatusResponse { bool success = 1; string error_code = 2; // 如 ORDER_NOT_FOUND, DB_WRITE_FAILED string error_message = 3; int64 sync_version = 4; // 本次同步的版本号,用于幂等 } service OrderStatusSyncAgent { rpc SyncStatus(SyncOrderStatusRequest) returns (SyncOrderStatusResponse); }注意:这里source_system和status_desc都标为optional,但契约文档里明确写:“若source_system为空,则默认为'internal';若status_desc为空,则使用status_code映射的标准描述”。契约必须既严格又留有余地。
Step 2:实现Agent核心逻辑(Python)
import grpc from concurrent import futures import time import logging from order import order_pb2, order_pb2_grpc from sqlalchemy import create_engine, text from sqlalchemy.exc import SQLAlchemyError class OrderStatusSyncAgent(order_pb2_grpc.OrderStatusSyncAgentServicer): def __init__(self): self.db_engine = create_engine("postgresql://...") # 生产环境用连接池 self.logger = logging.getLogger(__name__) def SyncStatus(self, request, context): # 1. 输入校验(契约第一道防线) if not request.order_id: return self._error_response(context, "ORDER_ID_REQUIRED", "order_id is required") if request.status_code not in [1, 2, 3, 4, 5]: return self._error_response(context, "INVALID_STATUS_CODE", f"status_code {request.status_code} not allowed") # 2. 幂等处理:先查是否存在同version的同步记录 with self.db_engine.connect() as conn: result = conn.execute(text(""" SELECT id FROM order_sync_log WHERE order_id = :order_id AND sync_version = :version """), {"order_id": request.order_id, "version": request.updated_at}) if result.fetchone(): self.logger.info(f"Order {request.order_id} already synced at version {request.updated_at}") return order_pb2.SyncOrderStatusResponse(success=True, sync_version=request.updated_at) # 3. 执行核心业务逻辑:更新订单主表 + 写入同步日志 try: with self.db_engine.begin() as conn: # 更新订单状态 conn.execute(text(""" UPDATE orders SET status = :status, updated_at = :updated_at WHERE order_id = :order_id """), {"status": request.status_code, "updated_at": request.updated_at, "order_id": request.order_id}) # 写入同步日志(关键!这是可观测性的源头) conn.execute(text(""" INSERT INTO order_sync_log (order_id, source_system, status_code, status_desc, sync_version, created_at) VALUES (:order_id, :source_system, :status_code, :status_desc, :sync_version, NOW()) """), { "order_id": request.order_id, "source_system": request.source_system or "internal", "status_code": request.status_code, "status_desc": request.status_desc or self._get_default_desc(request.status_code), "sync_version": request.updated_at }) except SQLAlchemyError as e: self.logger.error(f"DB error syncing order {request.order_id}: {e}") return self._error_response(context, "DB_WRITE_FAILED", str(e)) return order_pb2.SyncOrderStatusResponse(success=True, sync_version=request.updated_at) def _error_response(self, context, error_code, error_message): context.set_details(error_message) context.set_code(grpc.StatusCode.INVALID_ARGUMENT) # 根据错误类型设置不同code return order_pb2.SyncOrderStatusResponse(error_code=error_code, error_message=error_message) def _get_default_desc(self, code): return {1:"Created", 2:"Paid", 3:"Shipped", 4:"Delivered", 5:"Completed"}.get(code, "Unknown")提示:这个实现里藏着三个关键经验。第一,
sync_version直接用updated_at时间戳,避免引入额外的版本号生成逻辑;第二,幂等检查和业务更新放在同一个数据库事务里,保证原子性;第三,所有错误都通过gRPC标准方式返回,不抛异常,让调用方能统一处理。
Step 3:部署与注册
Agent部署后,需要向“Agent注册中心”(一个简单的PostgreSQL表)注册自己的契约:
INSERT INTO agent_registry ( agent_id, service_name, host, port, input_contract, output_contract, sla_ms, description ) VALUES ( 'order-status-sync-v1', 'OrderStatusSyncAgent', '10.0.1.100', 50051, '{"order_id":"required","source_system":"optional","status_code":"required"}', '{"success":"boolean","error_code":"string","sync_version":"int64"}', 200, '同步订单状态到核心订单库' );注册中心会定期被其他Agent扫描,从而发现并调用它。至此,你的第一个Agency Agent就活了。
4.2 第二步:构建Agent间的协商通道——以“支付失败自动补偿”为例
单个Agent只是原子,协作才产生价值。我们以“支付失败后自动触发补偿流程”为例,展示如何让两个Agent协商:
场景:用户支付失败,payment-gateway-agent需要通知order-compensation-agent进行补偿。
Step 1:定义协商消息契约(另一个.proto)
syntax = "proto3"; package compensation; message PaymentFailureNotification { string order_id = 1; string payment_id = 2; string failure_reason = 3; // 如 'CARD_DECLINED', 'TIMEOUT' int64 occurred_at = 4; // 时间戳 string original_request_id = 5; // 用于幂等 } message CompensationResult { bool success = 1; string compensation_id = 2; // 补偿单号 string error_code = 3; } service OrderCompensationAgent { rpc HandlePaymentFailure(PaymentFailureNotification) returns (CompensationResult); }Step 2:在payment-gateway-agent中发起协商
# 在payment-gateway-agent的失败处理逻辑中 def on_payment_failure(self, order_id, payment_id, reason): # 1. 先查注册中心,找到order-compensation-agent的地址 comp_agent = self.agent_registry.find_by_service("OrderCompensationAgent") if not comp_agent: self.logger.error("Compensation agent not found!") return # 2. 构建协商请求 request = compensation_pb2.PaymentFailureNotification( order_id=order_id, payment_id=payment_id, failure_reason=reason, occurred_at=int(time.time() * 1000), original_request_id=self._gen_request_id() ) # 3. 发起gRPC调用(带超时) try: channel = grpc.insecure_channel(f"{comp_agent.host}:{comp_agent.port}") stub = compensation_pb2_grpc.OrderCompensationAgentStub(channel) response = stub.HandlePaymentFailure(request, timeout=5.0) if response.success: self.logger.info(f"Compensation triggered for {order_id}, id: {response.compensation_id}") else: self.logger.warning(f"Compensation failed for {order_id}: {response.error_code}") except grpc.RpcError as e: self.logger.error(f"gRPC call to compensation agent failed: {e}") finally: channel.close()Step 3:在order-compensation-agent中实现协商逻辑
class OrderCompensationAgent(compensation_pb2_grpc.OrderCompensationAgentServicer): def HandlePaymentFailure(self, request, context): # 1. 幂等检查:查compensation_requests表,看此original_request_id是否已处理 if self._is_duplicate_request(request.original_request_id): return compensation_pb2.CompensationResult( success=True, compensation_id=self._get_existing_comp_id(request.original_request_id) ) # 2. 执行补偿:这里可能是调用退款API、发短信、改订单状态等 try: comp_id = self._create_compensation_record(request) self._trigger_refund_api(request.payment_id) self._send_sms_to_user(request.order_id) return compensation_pb2.CompensationResult(success=True, compensation_id=comp_id) except Exception as e: self.logger.error(f"Compensation failed for {request.order_id}: {e}") return compensation_pb2.CompensationResult( success=False, error_code="COMPENSATION_EXECUTION_FAILED" )注意:这里的
_is_duplicate_request检查至关重要。我们用original_request_id作为唯一键,插入compensation_requests表。如果插入失败(唯一键冲突),说明已处理过,直接返回成功。这保证了协商的幂等性,即使payment-gateway-agent重试100次,也只产生一次补偿。
4.3 第三步:搭建统一协作图谱——用100行SQL实现核心可观测性
可观测性不必复杂。我们的collab_events表结构如下:
CREATE TABLE collab_events ( id SERIAL PRIMARY KEY, caller_agent_id VARCHAR(100) NOT NULL, callee_agent_id VARCHAR(100) NOT NULL, input_hash CHAR(64) NOT NULL, -- SHA256(input_json) output_hash CHAR(64), -- SHA256(output_json) status VARCHAR(20) NOT NULL CHECK (status IN ('success','failed','compensated')), duration_ms INTEGER NOT NULL, timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(), order_id VARCHAR(50), -- 业务ID,便于关联 error_code VARCHAR(50), context_snapshot JSONB -- 存储关键上下文,如SQL、URL、错误堆栈片段 ); -- 创建索引,加速按order_id查询 CREATE INDEX idx_collab_events_order_id ON collab_events(order_id); CREATE INDEX idx_collab_events_timestamp ON collab_events(timestamp);前端查询API只需一个SQL:
SELECT caller_agent_id, callee_agent_id, status, duration_ms, timestamp, error_code, context_snapshot->>'sql' as sql_executed, context_snapshot->>'url' as api_called FROM collab_events WHERE order_id = %s ORDER BY timestamp ASC;我们甚至用这个表做了个简单的“协作健康度看板”:统计每个Agent的success率、平均耗时、失败最多的error_code。当payment-gateway-agent的失败率突然从0.1%跳到2.3%,看板立刻标红,值班工程师不用等告警,就能主动介入。
4.4 第四步:渐进式迁移——如何让现有团队平滑过渡
最大的落地阻力从来不是技术,而是人。我们设计了四阶段迁移路径:
Stage 1:旁路验证(2周):新写的Agency Agent不参与主流程,只接收一份生产流量的拷贝(用Kafka MirrorMaker),输出结果与旧系统比对。目标是验证契约准确性,不求100%一致,但要找出所有差异点。
Stage 2:灰度切流(1周):将5%的订单流量切到Agency Agent,其余95%走旧系统。监控核心指标(成功率、耗时、错误码分布),任何一项超标立即回滚。
Stage 3:双写保障(2周):Agency Agent和旧系统并行执行,但只以Agency Agent的结果为准。旧系统结果仅用于兜底和比对。此时团队开始习惯看协作图谱,而不是翻日志。
Stage 4:完全接管(1天):关闭旧系统,所有流量走Agency Agent。这一天,我们开了个简短的庆功会,不是庆祝技术成功,而是庆祝团队第一次“不用开会就能知道彼此在做什么”。
整个过程历时6周,没有一次线上故障。关键经验是:永远让新系统证明自己,而不是强迫团队相信它。当测试同学发现,用协作图谱查一个支付失败问题,比在ELK里搜20分钟日志还快时,他们就成了最坚定的支持者。
5. 常见问题与排查技巧实录:来自真实战场的12个血泪教训
5.1 “Agent调用超时了,但日志里没看到任何错误?”——查collab_events表的status字段
这是最高频问题。新人第一反应是去查被调用方的日志,结果发现一片空白。真相往往是:调用方在gRPC层就超时了,请求根本没发出去,或者被网络设备丢弃了。正确排查路径:
在
collab_events表中查这条调用记录:SELECT * FROM collab_events WHERE caller_agent_id='A' AND callee_agent_id='B' AND order_id='xxx' ORDER BY timestamp DESC LIMIT 1;如果记录存在,看
status字段:status = 'failed'且error_code为空:大概率是网络超时,检查duration_ms是否接近你设置的超时阈值(如5000ms)。status = 'failed'且error_code有值:看error_code,如GRPC_UNAVAILABLE表示被调用方不可达。status = 'success'但业务不对:说明调用成功了,但被调用方逻辑有问题,此时再查被调用方日志。
如果记录不存在:说明调用方压根没发起这次调用,问题出在调用方的业务逻辑里,比如条件判断写错了。
实操心得:我们给所有Agent的gRPC客户端封装了一个
log_on_timeout装饰器,只要超时,就强制往collab_events表里写一条status='timeout'的记录,并带上context_snapshot里的超时配置。这招让超时问题的定位时间从小时级降到分钟级。
5.2 “为什么同一个订单,协作图谱里显示被处理了两次?”——检查input_hash和幂等键
Agency Agents 的幂等性依赖于input_hash。如果两个看似相同的请求,input_hash不同,系统就会当作两次独立调用。常见原因:
时间戳精度不同:一个Agent用
int(time.time())(秒级),另一个用int(time.time() * 1000)(毫秒级),导致input_hash不同。浮点数序列化差异:JSON序列化
3.14和3.1400000000000001会产生不同hash。空值处理不一致:
{"name": null}和{"name": ""}在JSON序列化后hash不同。
解决方案:在生成input_hash前,先对输入做标准化处理:
import json from decimal import Decimal def standardize_input(input_dict): """标准化输入字典,用于生成一致的input_hash""" result = {} for k, v in input_dict.items(): if isinstance(v, float): # 浮点数转为字符串,保留6位小数 result[k] = f"{v:.6f}" elif isinstance(v, Decimal): result[k] = str(v) elif v is None: result[k] = "" # 统一用空