1. 从单Agent到多Agent:不是炫技,是需求倒逼
先把话说在前面:如果你的项目只有一个LLM调用,确实没必要上多智能体。很多人在刚开始接触Agent的时候,会把“多智能体”当成一个加分项,仿佛Agent数量一多,系统就显得高级。但在实际开发里,多Agent是代价最高的一种架构,它引入的并发、上下文、通信、状态一致性问题,能把一个本来3天能上线的功能拖成3周。所以,想搞清楚TS/JS生态里从单Agent演进到多Agent的路径,得先弄明白:到底什么场景下,单Agent撑不住了。
我在实际项目里最常遇到的情况是“角色混乱”。用一个Agent去处理全链路时,你需要在System Prompt里同时塞入“你是客服”“你会写SQL”“你需要调用工单系统”“你还得做情绪安抚”,Prompt越来越长,模型在意图识别上越来越飘,经常出现答非所问。另一个问题是工具集膨胀:一个Agent挂着20个工具,模型在每一步做工具选择时,判断成本急剧上升,而且容易选错。第三个问题是状态爆炸:单Agent在做多步骤任务时,上下文里堆满了中间过程,一旦某个分支出错,你很难定位是哪一步的决策污染了最终结果。
这时候多Agent的优势就体现出来了:每个Agent只负责一个职责域,Prompt精简,工具集小,上下文独立,失败边界清晰。说白了,它就是软件工程里的“单一职责原则”在LLM应用层的投射。从单Agent到多Agent,不是架构上的自嗨,而是当你遇到上面三类问题时的自然解法。
在TS/JS生态里做多Agent,和Python生态思路类似,但有个天然优势:JS的事件循环天生适合处理Agent之间的异步消息流。Node环境下,你用Promise、EventEmitter或者简单的消息队列就能搭出一套能跑的协作机制。而且TypeScript的类型系统对多Agent的状态管理非常友好,设计得好,编译期就能挡住一大半通信字段错误。
当然,多Agent不是银弹。如果任务足够简单,比如“根据用户输入查个天气”,单Agent足够。我的经验是:一个Agent能清晰表述职责边界,且工具数少于5个,就不要拆。拆分的信号不是代码量,而是“你发现单Agent的Prompt已经超过3000字,或者它经常在一个上下文里做两种以上毫不相干的事”。
2. 自己动手:用TS写一个最小多Agent协作骨架
讲再多理论,不如直接看代码。这一节我会带你从零写一个基于Node.js + TypeScript的最小多Agent系统。它不依赖任何重量级框架,只是把协作的核心机制——消息路由、状态隔离、结果汇总——用最朴素的方式实现一遍。这里面有很多设计决策,我会逐个拆解。
2.1 先定义Agent接口:约束比实现重要
多Agent系统的第一步不是写Agent,而是定义Agent之间的契约。我用TypeScript,所以会先声明一个基础接口。这个接口是整个系统的“宪法”,所有Agent都必须遵守它。
type AgentId = string; interface AgentContext { id: AgentId; role: string; taskQueue: Task[]; workspace: Map<string, unknown>; } interface AgentInput { from: AgentId; type: string; payload: unknown; } interface AgentOutput { to: AgentId | 'ROUTER'; type: string; payload: unknown; } interface Agent { id: AgentId; role: string; handle(input: AgentInput, context: AgentContext): Promise<AgentOutput[]>; }这里的核心是AgentOutput[],允许一个Agent在收到一条消息后产生零到多条输出。零条意味着它认为不需要继续处理;多条意味着它可以同时派发多个子任务。这为后续的平行任务做好了铺垫。
关于workspace:这是每个Agent的私有工作区,不直接共享。多Agent系统里最大的坑是“隐式共享状态”——两个Agent直接读一个全局对象,看起来方便,但写起来噩梦。用Map隔离,需要共享的数据显式通过消息传递,这是我能给的最朴素也最有效的建议。
2.2 实现一个主管-员工模式:总经理不写代码
有了接口,我们来实现一个经典模式:Supervisor(主管)和Worker(员工)。主管负责接收用户请求,拆解任务,分发给不同Worker,然后汇总结果。Worker只处理自己的细粒度任务。我用一个“客服工单分类”的例子来演示:主管把用户的投诉内容分发给分类Agent和情绪分析Agent,再把结果合并。
class ClassificationAgent implements Agent { id = 'classifier'; role = '工单分类员'; async handle(input: AgentInput) { const content = input.payload as { text: string }; // 实际开发中这里会调用LLM,这里简化为关键词规则 const labels = []; if (content.text.includes('退款')) labels.push('退款'); if (content.text.includes('卡顿')) labels.push('性能'); return [{ to: 'ROUTER', type: 'classification.done', payload: { labels } }]; } } class SentimentAgent implements Agent { id = 'sentiment'; role = '情绪分析员'; async handle(input: AgentInput) { const content = input.payload as { text: string }; const sentiment = content.text.includes('愤怒') ? 'negative' : 'neutral'; return [{ to: 'ROUTER', type: 'sentiment.done', payload: { sentiment } }]; } } class SupervisorAgent implements Agent { id = 'supervisor'; role = '客服主管'; async handle(input: AgentInput, context: AgentContext) { if (input.type === 'user.request') { const text = input.payload.text; // 派发两个子任务 return [ { to: 'classifier', type: 'classify', payload: { text } }, { to: 'sentiment', type: 'analyze', payload: { text } } ]; } if (input.type === 'classification.done' || input.type === 'sentiment.done') { const responses = context.workspace.get('responses') ?? []; responses.push(input); context.workspace.set('responses', responses); // 当两个子任务都完成时,汇总结果 if (responses.length === 2) { return [{ to: 'ROUTER', type: 'final.result', payload: { summary: `分类为${responses[0].payload.labels},情绪为${responses[1].payload.sentiment}` } }]; } } return []; } }看到这里你可能已经发现问题:主管Agent居然偷偷改了自己的workspace,而且依赖“两条消息都收到”这个隐式条件。这正是我前面说的“状态隔离”在实际落地时的难点——为了演示方便,我用工作区做了本地聚合,但真正的生产系统需要更完备的消息关联机制,比如用一个requestId把一次会话的所有消息串起来。这个我们后面聊OpenClaw时再展开。
2.3 手工搭建的局限:为什么你需要编排层
上面这套东西确实能跑,但如果你继续往下加Agent,很快就会撞墙。第一,路由逻辑写死在每个Agent里。每个Agent都知道“我该把结果发给谁”,甚至知道全局有哪些Agent ID。这违反了低耦合原则,一旦你新增一个Agent,所有可能和它通信的Agent代码都要改。
第二,没有重试和超时机制。任何一个LLM调用都可能失败,失败后谁来负责重试?是发起方还是接收方?手工代码里你会写大量try/catch,最后整个系统全是防御代码,主流程都看不清。
第三,没有上下文窗口的概念。真实场景中,主管Agent需要记住整个会话的来龙去脉,Worker Agent只需要知道当前这一个任务。如果全量传递,Token成本爆炸。手工代码里你很容易变成“取message传message”,根本没人管理记忆。
第四,没有可观测性。当你想知道“这个用户工单为什么被分类错了”,你只能去翻日志。但Agent之间的消息流是异步的,谁先到谁后到,中间有没有消息丢了,没有任何一个界面能完整回放。
这些就是编排层(Orchestration Layer)要解决的问题。所谓编排层,可以理解成一个“路由器 + 调度器 + 状态管理器 + 日志记录器”的组合体。它不关心你的Agent业务逻辑,只关心Agent之间怎么协作、消息怎么流转、状态怎么回收、异常怎么处理。这个思路不是我发明的,是所有成熟的多Agent系统沉淀出来的共性需求。
3. OpenClaw到底帮你做了什么:编排核心机制拆解
说到这儿,终于要进入标题里的主角:OpenClaw。我这里说的OpenClaw,指的是一个基于TypeScript实现的多智能体编排框架,它的核心不是提供各种花哨的Agent能力,而是把前面我列的四个痛点——通信路由、状态管理、容错重试、可观测性——以统一的方式解决了。我最早接触它,是基于一个社区项目,后来在多个生产环境里改造使用,踩了一圈坑之后,才敢拿出来写这篇总结。
如果你手头没有OpenClaw的环境,也没关系,这一节你把它当做一个“优秀编排层该有的样子”去看,后面遇到任何同类框架,思路都是一模一样的。
3.1 消息路由:不再硬编码“发给谁”
OpenClaw最核心的概念是一个全局的消息总线。Agent之间不直接引用对方的ID,而是通过声明自己的“能力标签”来让路由层匹配。比如一个Agent在注册时声明capabilities: ['text_classification'],另一个Agent想要做分类时,只需要向总线投递一个intent: 'text_classification'的消息,路由层会把消息自动分发给符合条件的目标Agent。
这种方式带来的直接好处是:新增Agent不需要修改任何既有代码。你只需要注册一个新能力,路由表自动多一跳。我在一个采用这种设计的项目里,从3个Agent扩展到12个Agent,只动了新增文件和注册配置,原有Agent一行没改。
它的实现原理并不复杂,本质是一个基于能力列表的订阅-发布模型。每个Agent在注册时,把自己的能力写入一个共享Registry,Router通过消息头的intent字段做匹配。匹配策略可以很简单——精确匹配能力名,也可以做加权——比如一个Agent声明了classification,另一个声明了text_classification.quick,Router根据规则选择一个最优的。
// OpenClaw风格的消息定义 interface ClawMessage { id: string; intent: string; payload: unknown; from?: AgentId; retryCount?: number; timestamp: number; }实际注册Agent时,你只需要写:
claw.register(new ClassificationAgent(), { capabilities: ['text_classification'], maxConcurrency: 3 });然后投递消息:
await claw.post({ intent: 'text_classification', payload: { text: '我要退款' } });后面的路由、分发、重试,都跟你的业务代码无关。
3.2 工作流与上下文窗口:让每个Agent只看该看的东西
多Agent协作里,最容易被忽视的是“上下文隔离”。很多初学者会把整段对话历史传给每个Agent,导致一个简单的翻译任务,翻译Agent却要读完长达5000字的工作日志。OpenClaw解决这个问题的方式,是提供一套显式的上下文传递API。
它的核心概念叫TaskContext,代表一次具体的任务执行单元。当主管Agent创建一个子任务时,会自动生成一个子上下文,只携带任务相关的数据。父上下文最终只保留汇总后的结论,中间过程全部丢弃。这和人类工作很像:项目经理只会告诉你说“把这份合同的条款提取出来”,而不会把整个项目从立项到现在的全部邮件都发给你。
// 创建子任务并显式传递数据 const childTask = await claw.createTask({ intent: 'contract_extraction', input: { fileId: '12345' }, ttl: 60000 // 任务超时 }); const result = await childTask.wait();这套机制还附带了一个很重要的能力:状态回收。每个子任务有ttl,超时后自动清理,不会残留内存里的孤儿状态。我见过很多手工实现的多Agent系统,跑着跑着内存上涨,就是因为子任务完成后没人回收工作区。
3.3 容错与恢复:失败不是异常,是常态
LLM调用天然不可靠,所以编排层必须把“失败”当成一级公民来处理。OpenClaw提供了三层容错:单次重试、级联回退、人工兜底。
单次重试很好理解,一条消息发送后如果目标Agent没有在指定时间内返回,路由层可以自动重新投递。这里有一个关键点:必须保证消息投递的幂等性。否则重试时,Agent会把同一个任务执行两遍。解决方案是在消息里带上唯一的taskId,Agent在处理前先检查自己是否已经处理过该ID。
级联回退是指:当主Agent连续失败,Framework会自动寻找备选Agent。这有点像服务发现里的熔断降级。我在生产里就用过这个能力:在线翻译Agent挂了,自动切换到本地词典Agent,至少能给出一个不完全正确但能用的结果,而不是让整个流程崩溃。
人工兜底最实用。编排层允许你给某个步骤配置一个“人工处理回调”,当机器处理超过N次失败后,把这个任务打包成一个待审单,推给人工工作台。说实话,做AI应用,完全没有人工兜底就像不买保险开快车,迟早出事。
4. 实战:用OpenClaw搭一个市场调研多Agent系统
前面讲了这么多机制,接下来是硬碰硬的实战。我用一个“竞品市场调研”的例子,带你完整走一遍从需求拆解到系统跑通的全过程。这个例子我选了TS/JS社区最常见的场景,不涉及任何敏感行业,适合直接抄作业。
4.1 需求拆解:一个调研任务怎么分给多个Agent
假设我们需要完成一个任务:调研某类产品(比如“智能猫砂盆”)的用户口碑,输出一份结构化报告。如果让一个Agent做,它要同时完成搜索、信息抽取、情感分析、观点聚合、报告撰写,直接崩溃。拆分后,我们定义四个Agent:
- 采集Agent:负责从公开平台拉取相关讨论文本。这里我们不讨论爬虫,假设它通过API获取数据。
- 信息抽取Agent:从原始文本中提取产品名、价格、功能点、问题点。
- 情感分析Agent:对每条用户评价做正面/负面/中性判断。
- 撰写Agent:综合以上输出,生成一份Markdown格式的调研报告。
这四个Agent之间没有复杂的依赖链,属于典型的并行+聚合模式。主管Agent先并行派发采集任务,等采集完成后,再并行派发抽取和情感分析,最后把结果汇总给撰写Agent。用OpenClaw实现,这个编排逻辑可以写在路由规则或一个简单的流程定义里。
import { Claw, task } from 'openclaw'; const claw = new Claw(); // 注册四个Agent claw.register('collector', collectorAgent, { capabilities: ['web.search'] }); claw.register('extractor', extractorAgent, { capabilities: ['info.extract'] }); claw.register('sentimenter', sentimenterAgent, { capabilities: ['sentiment.analyze'] }); claw.register('writer', writerAgent, { capabilities: ['report.write'] }); // 定义工作流 const workflow = task('market_research') .step('collect', { intent: 'web.search', params: { query: '智能猫砂盆 用户评价' } }) .parallel( task('extract', { intent: 'info.extract' }), task('sentiment', { intent: 'sentiment.analyze' }) ) .step('report', { intent: 'report.write' }) .build(); await claw.run(workflow, { input: { product: '智能猫砂盆' } });这只是伪代码级别的示例,但骨架已经很清晰。你不需要关心每个Agent内部怎么调用LLM,只需要在注册时告诉调度器“谁能干什么”。这样业务逻辑和调度逻辑完全分离。
4.2 Agent内部实现:一个负责抽取的Agent长什么样
我们以信息抽取Agent为例,展示在OpenClaw里写一个Agent的典型姿势。这里我假设用的是OpenAI接口,但换成其他模型也一样,重点是结构。
import { ClawAgent } from 'openclaw'; class ExtractorAgent extends ClawAgent { capabilities = ['info.extract']; async execute(message: ClawMessage) { const { rawText } = message.payload as { rawText: string }; // 用结构化输出的方式调用LLM const response = await llm.chat({ model: 'gpt-4o', messages: [ { role: 'system', content: '你是一个信息抽取器。只输出JSON,包含字段:product_name, features, issues, price_range。' }, { role: 'user', content: rawText } ], response_format: { type: 'json_object' } }); const extracted = JSON.parse(response.choices[0].message.content); return { type: 'info.extracted', payload: extracted }; } }要注意几点:第一,每个Agent只依赖message.payload,不读取全局状态,这样便于测试——你可以在单元测试里直接构造一个message,不启动整个编排层,单纯验证抽取逻辑。第二,返回时带上type,这相当于告诉路由层后续该谁处理。第三,LLM调用最好封装成通用client,不要在每个Agent里重复写配置。
4.3 结果聚合与报告生成
最后一个撰写Agent需要接收前面所有Agent的输出。这里最需要注意的是顺序。并行Agent完成顺序是不确定的,撰写Agent必须等到所有必要结果都到达后才会被触发。OpenClaw的编排层会根据task的依赖关系自动等待,但如果你是自己写代码,就要用Promise.all之类的机制来保证。
class WriterAgent extends ClawAgent { capabilities = ['report.write']; async execute(message: ClawMessage) { // 这里data中已有extract和sentiment的结果 const { extractResults, sentimentResults } = message.payload; const report = [ '# 市场调研报告', '', '## 功能点分布', extractResults.map(p => `- ${p.product_name}: ${p.features.join(',')}`).join('\n'), '', '## 用户情感统计', `正面:${sentimentResults.filter(s => s.sentiment === 'positive').length}`, `负面:${sentimentResults.filter(s => s.sentiment === 'negative').length}` ].join('\n'); return { type: 'report.generated', payload: { content: report } }; } }看到没有,撰写Agent根本不关心数据是哪来的,只要消息负载里带了extractResults和sentimentResults,它就能干活。这就是编排层带来的模块化收益:你可以只替换采集数据源,或者只用新的情感分析模型,其他Agent完全不受影响。
5. 多Agent项目里的常见坑和排查心得
再好的框架也架不住踩坑。这一节我不讲空话,全部来自我在多个项目里真实的翻车记录。你如果正在做类似的事情,对照着避雷。
5.1 循环调用与死锁:你的消息在鬼打墙
多Agent系统最常见的故障不是代码崩溃,而是Agent之间互相发消息,形成一个环。A发消息给B,B处理完又发消息给A,A再发给B……直到超时。这种问题在人工路由时代非常隐蔽,因为单个Agent看自己的逻辑都是正确的。一个排查技巧是:给每一条消息带上traceId,然后在消息总线层面加一个“每个traceId最多转发的Agent个数”上限,比如10次。超过就报警,这能快速发现问题。
死锁则更多出现在同步等待场景。比如主管Agent用await childTask.wait()等待子任务,但子任务的触发条件是主管发另一条消息,这就自己等自己了。解决办法是:要么用异步回调,要么把超时时间设短一点,让死锁自动暴露而不是永久挂着。
我在开发里遇到最诡异的一次是:Agent A处理完任务后,想给自己发一条“继续下一批”的消息,结果因为能力匹配太宽泛,消息被路由给了Agent B。排查了半天,最后发现是能力标签里重复声明了。所以给Agent声明能力时,一定要精确,不要图方便用一个很宽泛的标签。
5.2 上下文污染:你以为是负责任的传递,其实是甩锅
开发时最常见的问题是:主管为了让子Agent“干活更精准”,把自己上下文里的历史对话、系统提示、甚至其他子Agent的中间结果一股脑塞进去。结果子Agent的上下文越来越长,推理越来越慢,而且容易被无关信息误导。
我的经验是:每次传递前,显式挑出本次任务需要的字段。如果你不能确定需要哪些字段,就多传一个“摘要字段”,让子Agent先看摘要,按需请求更多信息。这比全量传递强一个数量级。举个例子,抽取Agent需要的是原文,但原文可能有5000字,其中有效信息只有200字。与其让它读全量,不如先行截断或做一个“重要片段抽取”。
另一个上下文污染源头是循环内的变量引用。TypeScript的闭包非常容易让Agent在并发执行时拿到同一个引用对象,导致数据串了。解决方法是使用不可变数据(Object.freeze或structuredClone)在消息边界处复制数据。我踩过一次之后,定了个规矩:任何跨Agent传递的payload必须是可JSON序列化的普通对象,一旦涉及函数、引用类型,就拒绝。
5.3 并发与顺序的博弈:什么时候并行,什么时候串行
多Agent系统并不是越并行越好。我见过有人同时发了10个并行子任务,结果所有子任务都在竞态地调用同一个LLM接口,触发限流,最终耗时反而比串行还长。这提醒我们:并行度需要全局控制。OpenClaw这类框架通常会提供统一的并发池,你注册时设maxConcurrency,调度器会自动排队。
但是什么时候该并行,什么时候必须串行?我总结了两条经验:
- 如果子任务之间没有任何数据依赖,且整体处理量可控(比如10个以内),才考虑并行。
- 如果子任务生产的结果会被下游合并,且合并逻辑要求顺序(比如按时间排序),那就不要让每个子任务自己返回时间戳,而是由调度器在完成后统一加
completionOrder字段,避免排序不一致。
还有一点,不要用数组下标作为消息关联的依据。两个并行任务返回顺序可能不一致,如果你在主管Agent里用result[0]去对应第一项,很可能对应错。正确做法是每个任务放入一个唯一subTaskId,返回时带上这个ID,再按ID聚合。
5.4 成本失控:Token比你想的烧得快
多Agent系统的成本不是“每个Agent一次调用的成本之和”,而是“所有Agent的调用次数 × 上下文长度”的乘积。尤其链式调用,一个任务解决不了会传导到下一个Agent,错误也会放大。我见过一个调研任务,因为采集Agent返回的数据格式不规范,信息抽取Agent反复重试了8次,最后一算,光这一个任务的Token成本就是预期的5倍。
控制成本的做法,第一是给每个Agent设置单次调用超时和最大重试次数;第二是尽量使用结构化输出,减少无效的对话式往返;第三是引入“缓存层”,对同样的输入、同样的意图,直接命中结果,不重新调用LLM。我通常在编排层外面包一层Redis,把intent + payload的hash作为缓存key,能省下不少冤枉钱。
另外,观察你的实际Token消耗,最好在部署时对每个Agent的消耗单独打点。哪来的成本高,一目了然。不要只看总费用,那样你只能傻眼。
6. 可观测性:没有追踪,你就是在盲开
多Agent系统比传统API服务更需要可观测性。传统服务一条请求最多经过三四个服务,链路追踪已经很关键;多Agent一次任务可能有几十条消息,分发给十几个Agent,没有追踪的话,问题复现都无从谈起。
6.1 消息追踪要从设计阶段就做
用OpenClaw这类框架时,最简单的办法是利用它内置的追踪钩子。通常一个消息从投递到完成,会有以下节点:投递、路由匹配、Agent受理、Agent完成、后续路由。每个节点都应该打点。我一般是记录以下几个字段:
traceId:一次业务请求的全局唯一IDmessageId:单条消息的IDagentId:当前处理的Agentintent:消息意图duration:这一步耗时inputPayloadSize和outputPayloadSize:用来估算上下文负载status:成功、失败、超时
这些数据汇总以后,你用任何日志平台都能重建一次任务的完整流程图。我在本地开发时,最常用的是把消息流转打成一个树形结构,每个父任务下面挂几个子任务,一眼就能看出是不是有任务被重复派发。
claw.on('message.processed', (event) => { logger.log({ traceId: event.traceId, messageId: event.messageId, agentId: event.agentId, intent: event.intent, duration: event.duration, status: event.status }); });6.2 单元和集成测试:Agent可以单个测,协作必须整体验
Agent的逻辑本身是纯函数式的,所以单元测试非常容易。你直接构造一个输入消息,调用agent.execute,断言输出。难的是协作链路的测试。我的做法是写一组“集成测试脚本”,模拟一次完整的用户请求,然后断言最终输出里的核心字段。
这里有一个反直觉的经验:不要做严格的顺序断言。如果两个子任务是并发执行的,你断言“消息A一定比消息B先处理”,测试就会因为调度器的并发随机性而偶发失败。正确做法是断言“两者都完成了且结果正确”,而不是“谁先谁后”。
可观测性还要和告警联动。我推荐的告警指标是:单次任务成功率和平均耗时是最核心的;其次是每个Agent的重试次数占比;然后是上下文平均长度如果持续上涨,说明可能有状态泄漏。不要等用户投诉了你才发现问题,好的告警能让你在用户感知之前就搞定。
6.3 回调地狱与长任务:异步才是归宿
多Agent任务往往不是一次HTTP请求能完成的,尤其是涉及实时数据采集和推理的场景,一次任务可能跑几十秒甚至几分钟。如果前端一直同步等待,体验极差,也会导致超时。合理的方式是任务异步化:提交任务后,立刻返回一个taskId,前端轮询或通过WebSocket接收进度,后端编排层持续执行。
在OpenClaw里实现异步任务也很简单,claw.run()可以返回一个任务句柄,你不需要await它完成。你可以在任意Agent的完成回调里,通过WebSocket推送进度。比如调研任务,采集Agent完成后推送“正在提取信息”,撰写Agent完成后推送“报告生成中”。用户体验会好很多,而且能避免网关超时。
异步化带来的新问题是任务状态管理。你需要把taskId映射到当前运行阶段,并且允许用户手动取消任务。取消实现的核心原则是“取消时,向所有已派发的子任务投递一个中断消息”。不要试图“杀死”正在运行的Agent,而是通过标记让它在下一个检查点主动退出。
7. 从框架到生产:最后的几个建议
写到现在,基础的东西都覆盖了。最后我再补几个实战层面的建议,也算是我个人从“跑通Demo”到“上线稳跑”这阶段里最有价值的一些沉淀。
第一个建议:不要一上来就上一套“标准多Agent框架”。先把前面第二节的手工版本写通,理解清楚消息流转、状态隔离、汇总的逻辑。只有当你真的觉得搭路由、做重试、做追踪这些事开始重复繁琐时,再引入OpenClaw完整的编排能力。直接上手框架,你很可能对底层机制一无所知,出了问题无法排查。
第二个建议:Agent之间的接口设计,要比对外的API更严格。对外的API你可能还有文档和联调,Agent之间的消息没有任何人类审核,全靠代码定义。所以一定要用TypeScript的强类型约束payload结构,拒绝滥用any。每一条消息的类型都要有一个对应的Interface,并且加一个version字段,将来消息格式升级时可以兼容处理。
第三个建议:灰度发布时,先替换一个Agent,不要一次性替换全链路。比如你的调研系统跑得好好的,想换一个更强的情感分析模型,那就只换SentimentAgent,其他Agent不动。对比新旧Agent在同一批数据上的输出差异,确认没有回退后再扩大范围。这个思路和微服务架构里的“金丝雀发布”完全一致。
第四个建议也是我认为最重要的:给Agent赋予“不做某事”的能力。就像人一样,一个Agent要清楚自己的能力和职责边界。当收到的消息不在自己的能力范围内时,它能明确回复I cannot handle this,并携带一条建议路由给哪个Agent,而不是强行返回一个错误或者乱猜。这能极大降低跨Agent的错误传导。我在OpenClaw里见过有人在消息负载里加了一个abilityCheck字段,让Agent在行动前先自检,用了之后系统故障率下降不少。
现在我回过头去看那些从单Agent一路演进过来的项目,最有感触的一点是:多Agent系统的复杂度,主要不在每个Agent内部,而是在Agent之间的连接。连接一旦清晰、可控、可观测,整个系统就会变得异常稳定。反之,即便每个Agent内部的Prompt写得再精细,也可能因为一条消息路由错位而全线崩溃。
如果你正打算在自己项目里做TS/JS多Agent开发,把前面几节的手工版本先搭一遍,再决定要不要引入OpenClaw。磨刀不误砍柴工,Agent的每一个设计决策,都会在将来的维护里反复出现。希望这篇基于实际体验的总结,能帮你少走几步弯路。