1. 为什么“能跑通Demo”和“能上生产”之间隔着一道鸿沟
如果你最近半年动手搭过Agent,大概率经历过这样一个过程:看了一篇讲ReAct循环的文章,觉得思路很清晰,于是花一个下午写了个循环——调模型、解析工具调用、执行工具、把结果塞回上下文、再调模型,跑通了,挺开心。然后你试着让它处理一个稍微复杂点的任务,比如“帮我查一下最近三天的行业新闻,整理成摘要,再根据摘要写一份周报草稿”,结果发现它要么陷入死循环,要么在第7步的时候把第2步的工具返回结果给忘了,要么两个子任务同时操作同一份文件导致内容互相覆盖。
这不是你的代码写得不好,而是单Agent循环和Agent Runtime之间,本质上不是一个东西。
单Agent循环解决的是“一次推理加一次工具调用”的问题,它的核心是一个while循环加上消息列表的管理。而Agent Runtime要解决的是“多个Agent、多个任务、多种工具、多种状态在同一个系统里协调运转”的问题。前者像是一个人自己干活,后者像是一个团队在协作——你需要考虑谁负责什么、信息怎么传递、冲突怎么解决、失败了怎么恢复、资源怎么分配。
我见过不少团队在Agent编排上踩的坑,归根结底都是把“单Agent循环”当成了“Agent Runtime”来用。一开始觉得够用,等到任务复杂度上来之后,发现整个系统像一团乱麻,改一处崩三处。所以这篇文章想做的事情是:把Agent编排这件事从底层原理到工程落地完整地拆一遍,重点讲清楚构建一套真正好用的Agent Runtime需要解决哪些核心问题,以及每个问题在实际工程中通常怎么解。
这篇文章适合两类人看:一类是已经写过简单Agent、想往生产级系统演进的开发者;另一类是在做技术选型、想搞清楚Agent编排到底涉及哪些层面的架构决策的技术负责人。我会尽量少讲空泛的概念,多讲实际工程中会遇到的问题和常见的解法。
2. Agent Runtime到底在运行时管理什么
2.1 从“消息列表”到“运行时状态机”的认知转变
大部分人第一次写Agent的时候,脑子里想的是一个消息列表:system prompt放最前面,然后user message,然后assistant message带tool_calls,然后tool message带结果,如此循环。这个模型在单轮任务里没问题,但一旦任务变复杂,你就会发现消息列表根本不够用。
为什么?因为消息列表只能表达“对话历史”,它表达不了“当前任务执行到哪一步了”“哪些子任务已经完成”“哪些工具正在被占用”“上一个Agent的输出有没有通过校验”。这些东西不属于对话历史,它们属于运行时状态。
我习惯把Agent Runtime的状态分成四层来理解:
- 对话层:消息历史,就是传统意义上的context window里的内容。这一层负责让模型知道“之前发生了什么”。
- 任务层:当前有哪些任务、每个任务的状态(待执行/执行中/已完成/失败)、任务之间的依赖关系。这一层负责让系统知道“现在该做什么”。
- 资源层:哪些工具正在被调用、哪些文件正在被读写、哪些外部API有速率限制。这一层负责让系统知道“什么能做、什么不能做”。
- 执行层:当前有哪些Agent实例在运行、每个实例的上下文是什么、它们之间怎么通信。这一层负责让系统知道“谁在做”。
这四层状态需要被统一管理,而且它们之间是有联动关系的。比如一个任务完成了,对话层要记录结果,任务层要更新状态,资源层要释放占用的工具,执行层可能要触发下一个Agent启动。如果你把这四层混在一个消息列表里,很快就会失控。
2.2 编排的核心不是“调度”而是“状态一致性”
很多人一提到编排就想到调度——哪个Agent先跑、哪个后跑、怎么分配任务。调度当然重要,但实际工程中更棘手的问题是状态一致性。
举个具体的场景:你有一个Agent负责从数据库查数据,另一个Agent负责根据数据生成报告。查数据的Agent把结果写到了一个共享的存储里,然后通知生成报告的Agent去读。如果通知发出去了,但写入还没完成(比如异步写入有延迟),生成报告的Agent读到的就是空数据或者旧数据。这就是状态不一致。
再比如:两个Agent同时操作同一份文件,一个在写、一个在读,读到的可能是写了一半的内容。这也是状态不一致。
解决状态一致性的常见手段有几种。最简单的是串行化——同一时间只允许一个Agent操作共享资源,其他Agent排队等待。这种方式实现简单,但吞吐量低。稍微复杂一点的是版本控制——每次写入都带一个版本号,读取的时候检查版本号是否匹配,不匹配就重试。这种方式吞吐量高,但需要额外的版本管理逻辑。还有一种方式是事件驱动——不直接操作共享资源,而是发事件,由专门的消费者按顺序处理事件。这种方式解耦最彻底,但系统复杂度也最高。
选哪种方式取决于你的任务特征。如果任务量不大、对延迟不敏感,串行化就够了。如果任务量大、需要并发,版本控制或事件驱动更合适。我个人的经验是:先从串行化开始,等到确实遇到性能瓶颈再升级。过早引入复杂的并发控制,往往会让系统变得难以调试。
2.3 一个容易被忽略的问题:Agent的“身份”管理
在多Agent系统里,每个Agent需要知道自己是谁、能做什么、不能做什么。这听起来像废话,但实际工程中很容易被忽略。
我见过一个系统,所有Agent共用同一个system prompt,只是通过不同的user message来区分任务。结果就是Agent经常“越权”——本该只做检索的Agent试图去写文件,本该只做格式化的Agent试图去调外部API。原因很简单:模型不知道自己当前扮演的是什么角色,它只看到了一个通用的system prompt和一条具体的任务指令。
正确的做法是给每个Agent一个明确的身份定义,包括:角色名称、职责范围、可用工具列表、输出格式要求、以及明确的“不做什么”的约束。这个身份定义应该在Agent实例创建时就确定下来,而不是每次调用时临时拼凑。
更进一步,身份定义还应该和权限系统绑定。比如“检索Agent”只能调用搜索类工具,“写入Agent”只能调用文件操作类工具。这样即使模型产生了幻觉、试图调用不该调用的工具,运行时层面也能拦截住。
3. 多Agent协作的几种拓扑结构及其适用边界
3.1 流水线式:最简单但也最容易僵化
流水线式是最直观的多Agent协作方式:Agent A做完交给Agent B,B做完交给C,像工厂流水线一样。这种结构的优点是逻辑清晰、容易调试、每个Agent的职责非常明确。缺点是灵活性差——如果任务需要回退或者跳步,流水线就很难处理。
流水线式适合什么样的场景?适合步骤固定、顺序明确、每步的输出格式可预期的任务。比如“文档处理流水线”:一个Agent负责解析文档结构,一个负责提取关键信息,一个负责生成摘要,一个负责格式化输出。这种场景下流水线式非常高效。
但如果你试图用流水线式处理“开放式研究任务”——比如“调研某个技术方向的现状并给出建议”——就会很痛苦。因为研究过程中经常需要回退:查到某个信息后发现之前的假设不成立,需要重新检索;或者某个子问题需要深入挖掘,需要临时插入额外的步骤。流水线式对这种动态调整的支持很差。
3.2 层级式:主管Agent加执行Agent的经典模式
层级式是目前工程上用得最多的多Agent结构。它的核心思路是:有一个主管Agent(或者叫编排Agent、规划Agent)负责拆解任务、分配任务、汇总结果;下面有若干执行Agent负责具体执行。
这种结构的优点是职责分离清晰:主管Agent专注于“做什么、谁来做、什么时候做”,执行Agent专注于“怎么做”。主管Agent不需要知道每个工具的具体调用细节,执行Agent不需要知道全局的任务规划。
但层级式也有它的坑。最大的坑是主管Agent容易成为瓶颈。如果所有决策都要经过主管Agent,那么主管Agent的上下文会迅速膨胀,推理延迟会成为整个系统的瓶颈。而且主管Agent一旦出错(比如拆解任务不合理、分配错了执行Agent),整个任务就会失败。
我在实际项目中总结了几条缓解这个问题的经验:
- 限制主管Agent的管理幅度:一个主管Agent下面不要挂太多执行Agent,通常3到5个比较合适。超过这个数量,主管Agent的决策质量会明显下降。
- 给主管Agent减负:不是所有决策都需要主管Agent来做。执行Agent可以在自己的职责范围内自主决策,只有跨职责的协调才需要上报主管Agent。
- 允许嵌套:如果任务确实复杂,可以有多层主管Agent。顶层主管负责大方向,中层主管负责子任务拆解,底层执行Agent负责具体操作。但层数不要超过三层,否则调试会非常困难。
3.3 黑板模式:共享状态驱动的协作
黑板模式(Blackboard Pattern)是一种比较特殊的协作方式。它的核心思路是:所有Agent共享一个“黑板”(可以理解为一个共享的状态存储),每个Agent都可以往黑板上写信息,也可以从黑板上读信息。Agent之间不直接通信,而是通过黑板间接协作。
这种模式适合没有明确执行顺序、需要多角度分析同一份数据的场景。比如“多维度内容审核”:一个Agent负责检查事实准确性,一个负责检查语言风格,一个负责检查合规性,它们都读同一份内容,各自把检查结果写到黑板上,最后由一个汇总Agent读取所有结果做最终判断。
黑板模式的优点是解耦彻底:每个Agent只需要关心自己读什么、写什么,不需要知道其他Agent的存在。缺点是控制流不明确:没有明确的“谁先谁后”,容易出现某个Agent一直在等另一个Agent的输出、而另一个Agent也在等它的死锁情况。
用黑板模式的时候,我通常会加一个协调器来管理触发条件。比如“当黑板上出现了事实检查结果和风格检查结果时,触发合规检查Agent”。这样既保留了黑板模式的解耦优势,又避免了死锁。
3.4 市场式:竞价与协商机制
市场式是一种更前沿的协作模式,核心思路是:把任务当作“商品”,Agent当作“竞标者”,通过竞价机制来决定任务分配给谁。出价低的Agent获得任务执行权,但同时也承担相应的责任。
这种模式在理论上很优雅,能实现资源的动态分配和负载均衡。但在实际工程中,我很少见到纯粹的市场式实现。原因很简单:竞价机制本身的开销可能比任务执行本身还大。而且如何定义“出价”也是一个难题——是按token消耗算?按延迟算?按成功率算?不同的定义会导致完全不同的分配结果。
不过市场式的某些思想是可以借鉴的。比如在分配任务时,不是简单地轮询或者随机分配,而是根据每个Agent当前的状态(负载、历史成功率、专长领域)来做一个加权选择。这种“轻量级市场机制”在工程上更实用。
4. 工具调用层:Agent Runtime里最容易出问题的环节
4.1 工具描述的质量直接决定调用准确率
很多人把工具调用的问题归结为“模型能力不够”,但实际上,大部分工具调用失败的原因是工具描述写得不好。
我做过一个对比实验:同一组工具,用两种不同的描述方式,让同一个模型去调用。第一种描述是“查询数据库”,第二种描述是“根据用户ID查询用户的基本信息,包括姓名、邮箱、注册时间,输入参数为user_id(字符串格式),返回JSON格式的用户信息”。结果第二种描述的工具调用准确率比第一种高了将近40%。
好的工具描述应该包含这几个要素:
- 功能说明:这个工具是做什么的,一句话说清楚。
- 输入参数:每个参数的名字、类型、是否必填、含义、格式要求。
- 输出格式:返回什么类型的数据,包含哪些字段。
- 使用场景:什么情况下应该用这个工具,什么情况下不应该用。
- 边界条件:参数有什么限制,比如ID必须是数字、时间必须是ISO格式等。
还有一个容易被忽略的点:工具之间的区分度。如果你有两个工具功能很相似,模型很容易搞混。这时候要么合并这两个工具,要么在描述里明确写出它们的区别和使用场景。
4.2 工具调用的错误处理与重试策略
工具调用失败是常态,不是异常。网络超时、API限流、参数格式错误、权限不足——这些都会导致工具调用失败。关键是怎么处理这些失败。
最粗暴的做法是直接报错、终止任务。这显然不可取。稍微好一点的做法是重试,但重试也有讲究:
- 参数错误不要重试:如果是因为参数格式不对导致的失败,重试多少次都是一样的结果。这时候应该把错误信息返回给模型,让模型修正参数后重新调用。
- 网络超时和限流可以重试:但要加退避策略,比如第一次等1秒,第二次等2秒,第三次等4秒。不要密集重试,否则可能触发更严格的限流。
- 权限错误不要重试:这属于配置问题,重试解决不了。应该记录日志并通知人工处理。
还有一个进阶技巧:给模型提供“降级方案”。比如主搜索工具失败了,告诉模型“可以尝试用备用搜索工具”。这样模型在遇到失败时不会卡死,而是会尝试替代方案。
4.3 工具调用的并发控制与资源隔离
当多个Agent同时运行时,工具调用的并发控制就变得很重要。我见过一个系统,多个Agent同时调用同一个外部API,结果触发了API的速率限制,所有Agent都被封了一段时间。
解决这个问题有几种思路:
- 全局速率限制器:在Runtime层面维护一个令牌桶,所有工具调用都要先获取令牌。这样可以保证整体调用速率不超过限制。
- 工具级别的队列:每个工具维护一个调用队列,同一时间只允许N个并发调用。超出的请求排队等待。
- Agent级别的配额:给每个Agent分配一定的调用配额,用完就等待下一轮。这样可以防止某个Agent独占资源。
我通常会把全局速率限制和工具级别队列结合起来用。全局速率限制保证不触发外部API的限制,工具级别队列保证单个工具不会被过度使用。
5. 上下文管理:Agent Runtime的“内存管理”
5.1 上下文窗口是稀缺资源,需要像管理内存一样管理它
Agent的上下文窗口是有限的,但任务执行过程中产生的信息是无限的。这就产生了一个矛盾:你需要把足够的信息放进上下文让模型做出正确决策,但上下文又不能太满,否则会挤占推理空间、增加延迟和成本。
我习惯把上下文管理类比成操作系统的内存管理。上下文窗口就是物理内存,消息就是内存页,你需要决定哪些页留在内存里、哪些页换出到磁盘、哪些页直接丢弃。
常见的上下文管理策略有几种:
- 滑动窗口:只保留最近N条消息,更早的消息丢弃。这种方式实现简单,但会丢失早期的重要信息。
- 摘要压缩:把早期的消息用模型压缩成一段摘要,保留摘要、丢弃原始消息。这种方式能保留关键信息,但摘要本身可能丢失细节。
- 分层存储:把消息分成“热数据”(最近几条)、“温数据”(较早但可能相关的)、“冷数据”(很久之前的)。热数据始终在上下文里,温数据按需加载,冷数据只在特定条件下加载。
- 结构化提取:不保留原始消息,而是把关键信息提取成结构化数据(比如任务状态、已完成的步骤、关键发现),只把结构化数据放进上下文。
实际工程中,我通常会把摘要压缩和结构化提取结合起来用。对话历史用摘要压缩,任务状态用结构化提取。这样既保留了对话的连贯性,又保证了任务状态的准确性。
5.2 不同Agent之间的上下文隔离与共享
在多Agent系统里,上下文管理还有一个额外的维度:哪些上下文应该被隔离,哪些应该被共享。
隔离的好处是防止信息污染。比如一个负责代码生成的Agent和一个负责代码审查的Agent,如果共享上下文,审查Agent可能会被生成Agent的推理过程影响,导致审查不够客观。
共享的好处是减少信息传递的开销。如果每个Agent都要重新获取一遍上下文,不仅浪费token,还可能导致信息不一致。
我的经验法则是:执行上下文隔离,任务上下文共享。具体来说,每个Agent有自己的对话历史和推理过程(执行上下文),这些不共享;但任务的状态、已完成步骤的输出、共享资源的状态(任务上下文)是共享的。这样既保证了Agent的独立性,又保证了任务的一致性。
5.3 上下文压缩的时机与策略选择
上下文压缩不是越早越好,也不是越晚越好。压缩得太早,可能把后面还需要的信息压掉了;压缩得太晚,上下文已经快满了,压缩效果不好。
我通常会在上下文使用率达到70%左右的时候触发压缩。这个阈值可以根据实际情况调整:如果任务对历史信息依赖度高,可以调到80%;如果任务比较独立、历史信息不太重要,可以调到60%。
压缩策略的选择也有讲究。对于对话历史,我通常用“保留最近N条 + 摘要更早的”策略。对于工具调用结果,我通常只保留最近几次的完整结果,更早的只保留关键字段。对于推理过程,我通常只保留结论,丢弃中间步骤。
还有一个技巧:压缩时保留“锚点”。所谓锚点,就是那些后续可能会被引用的关键信息,比如文件路径、变量名、任务ID等。这些信息即使在其他内容被压缩掉之后,也应该保留在上下文里。
6. 从原型到生产:Agent Runtime的工程化落地要点
6.1 可观测性:没有日志和追踪就没有调试
Agent系统的调试难度比传统系统高一个数量级,因为它的行为是不确定的——同样的输入,两次运行可能走完全不同的路径。如果没有完善的可观测性,出了问题你根本不知道从哪里查起。
我在实际项目中要求Agent Runtime至少记录以下几类信息:
- 每次模型调用的完整输入输出:包括system prompt、user message、模型返回的完整内容、token消耗、延迟。
- 每次工具调用的完整信息:包括工具名、参数、返回值、耗时、成功/失败状态。
- 状态变更事件:任务状态变更、Agent启动/停止、上下文压缩触发等。
- 决策链路:主管Agent为什么选择这个执行Agent、为什么拆解成这几个子任务。
这些信息不仅要记录,还要能关联起来。比如通过一个trace_id把一次任务执行中的所有模型调用、工具调用、状态变更串联起来,这样排查问题时可以完整回放整个执行过程。
6.2 失败恢复:Agent系统必须能“断点续跑”
Agent任务执行时间可能很长,从几分钟到几小时不等。如果执行到一半系统崩了,重新从头跑一遍代价太大。所以Agent Runtime需要支持断点续跑——从上次中断的地方继续执行,而不是从头开始。
实现断点续跑的关键是状态持久化。每次状态变更后,把状态写入持久化存储(数据库、文件、Redis等)。系统重启后,从持久化存储中恢复状态,继续执行。
这里有一个设计决策:持久化的粒度。粒度太粗(比如只在任务级别持久化),恢复后需要重跑很多步骤;粒度太细(比如每次消息都持久化),写入开销太大。我通常会在步骤级别做持久化——每个子任务完成后持久化一次状态。这样恢复时最多重跑一个子任务,开销可以接受。
还有一个相关的问题:幂等性。如果恢复后重跑某个步骤,这个步骤的操作必须是幂等的——重复执行不会产生副作用。比如“写入文件”这个操作,如果重跑时文件已经存在,应该覆盖而不是追加。这需要在工具层面做保证。
6.3 成本控制:Token消耗的监控与优化
Agent系统的token消耗通常比单次模型调用高一个数量级,因为每次工具调用都要把完整上下文重新发给模型。如果不做成本控制,账单会很难看。
成本控制的手段有几种:
- 上下文压缩:前面讲过了,减少每次调用的token量。
- 模型分级:不是所有决策都需要用最强的模型。简单的决策(比如格式化输出、参数校验)可以用小模型,复杂的决策(比如任务拆解、结果汇总)用大模型。
- 缓存:对于重复的查询(比如同样的搜索请求),可以缓存结果,避免重复调用。
- 提前终止:如果任务已经不可能成功(比如关键工具不可用、必要参数缺失),尽早终止,不要继续消耗token。
我通常会在Runtime层面加一个token预算机制:给每个任务分配一个token预算,执行过程中实时监控消耗,接近预算时触发告警或降级策略。
6.4 安全边界:Agent能做什么、不能做什么
Agent系统的一个核心风险是:模型可能会做出你意想不到的操作。比如删除了不该删除的文件、调用了不该调用的API、把敏感信息发送到了外部。
所以Agent Runtime需要有一套安全边界机制:
- 工具白名单:每个Agent只能调用明确授权的工具,其他工具一律拒绝。
- 参数校验:对工具调用的参数做校验,比如文件路径必须在指定目录下、API调用的目标地址必须在白名单内。
- 操作审计:所有敏感操作都要记录审计日志,包括谁在什么时候做了什么。
- 人工确认:对于高风险操作(比如删除文件、发送邮件),可以要求人工确认后再执行。
这些机制看起来简单,但在实际工程中非常重要。我见过不止一个系统因为缺少安全边界而出了事故。
7. 我在Agent编排上踩过的几个坑
7.1 过度设计:一开始就上多Agent,结果调试成本爆炸
我刚开始做Agent编排的时候,觉得多Agent是银弹——一个Agent做不好,那就拆成多个Agent。结果拆完之后发现,Agent之间的通信开销、状态同步开销、调试开销加起来,比单Agent还高。
后来我总结了一条经验:能用单Agent解决的,不要用多Agent。单Agent的调试成本是线性的,多Agent的调试成本是指数级的。只有当单Agent确实解决不了(比如上下文窗口不够、职责确实需要分离)的时候,才考虑多Agent。
7.2 忽略工具描述:模型调用失败,第一反应是换模型
前面提过,工具调用失败的大部分原因是工具描述写得不好。但我一开始没有意识到这一点,遇到调用失败就换模型——从A模型换到B模型,从B模型换到C模型。换了一圈发现,问题依然存在。
后来我才明白:模型的能力是有上限的,但工具描述的质量是可以无限提升的。与其花时间换模型,不如花时间把工具描述写清楚。
7.3 不做状态持久化:系统一崩,全部重来
早期做的Agent系统没有状态持久化,任务执行到一半如果进程挂了,就得从头开始。对于短任务还好,对于长任务(比如需要跑几十分钟的研究任务),重来的代价太大了。
后来我加上了状态持久化,虽然增加了一些开发量,但系统的可靠性提升了一个档次。现在即使系统崩溃,重启后也能从上次中断的地方继续跑。
7.4 上下文管理太粗暴:要么全留,要么全丢
一开始我的上下文管理策略很简单:要么全留(直到上下文满了报错),要么全丢(只保留最近几条消息)。这两种策略都有问题:全留会导致上下文溢出,全丢会丢失关键信息。
后来我改成了分层管理:热数据保留、温数据摘要、冷数据按需加载。虽然实现复杂了一些,但效果好了很多。
8. 一套可落地的Agent Runtime最小架构
8.1 核心模块划分
如果你现在要从零构建一套Agent Runtime,我建议至少包含以下几个模块:
- Agent管理器:负责Agent的创建、销毁、状态管理。
- 任务调度器:负责任务的拆解、分配、状态跟踪。
- 工具注册中心:负责工具的注册、描述管理、调用路由。
- 上下文管理器:负责上下文的存储、压缩、加载。
- 状态存储:负责运行时状态的持久化。
- 可观测性模块:负责日志、追踪、指标收集。
- 安全网关:负责工具调用的权限校验和参数校验。
这些模块不需要一开始就全部实现,可以按优先级逐步添加。我的建议是:先实现Agent管理器和工具注册中心,让单Agent能跑起来;然后加状态存储和可观测性,让系统可调试;最后加任务调度器和上下文管理器,支持多Agent协作。
8.2 技术选型建议
技术选型没有标准答案,取决于你的团队技术栈和任务特征。但有几个原则可以参考:
- 状态存储:如果任务量不大,用SQLite或文件就够了;如果任务量大、需要并发,用PostgreSQL或Redis。
- 工具调用:如果工具都是内部的,直接函数调用就行;如果工具是外部的,需要加HTTP客户端和重试逻辑。
- 可观测性:如果团队已经有日志系统,直接接入;如果没有,从简单的文件日志开始。
- 模型调用:建议抽象一层模型接口,方便切换不同的模型提供商。
8.3 从最小可用到生产级的演进路径
我的建议是分三个阶段演进:
第一阶段:单Agent + 基础工具调用。目标是跑通“模型推理-工具调用-结果返回”的循环。这个阶段不需要考虑多Agent、不需要考虑复杂的上下文管理。
第二阶段:加状态持久化和可观测性。目标是让系统可调试、可恢复。这个阶段需要加状态存储、日志、追踪。
第三阶段:加多Agent协作和高级上下文管理。目标是支持复杂任务。这个阶段需要加任务调度器、上下文管理器、安全网关。
每个阶段之间不需要严格串行,可以根据实际需求调整。但核心原则是:不要跳过第二阶段直接到第三阶段。没有可观测性和状态持久化的多Agent系统,调试起来会让你怀疑人生。
9. 关于Agent编排,几个我经常被问到的问题
9.1 到底用框架还是自己写
这是被问得最多的问题。我的回答通常是:如果你只是想快速验证一个想法,用框架;如果你想上生产,自己写。
框架的好处是上手快,很多基础功能(工具调用、上下文管理、多Agent协作)都帮你封装好了。但框架的缺点是:当你想做一些框架不支持的事情时,会非常痛苦。而且框架的抽象层会增加调试难度——出了问题你不仅要查自己的代码,还要查框架的代码。
自己写的好处是可控性高,每一行代码你都知道在做什么。缺点是开发量大,很多基础功能要自己实现。
我的建议是:用框架做原型,用自研做生产。先用框架快速验证想法,跑通之后再逐步替换成自研的实现。
9.2 多Agent真的比单Agent好吗
不一定。多Agent的优势在于职责分离和并行执行,但代价是通信开销和调试复杂度。如果你的任务不需要职责分离、不需要并行执行,单Agent可能是更好的选择。
我通常会用这个标准来判断:如果单Agent的system prompt超过2000字,或者单Agent需要调用超过10个工具,就考虑拆成多Agent。因为这时候单Agent的上下文压力已经很大了,拆成多Agent可以减轻每个Agent的负担。
9.3 怎么评估一个Agent Runtime的好坏
我通常看几个指标:
- 任务成功率:给定一组任务,系统能成功完成的比例。
- 平均执行时间:完成任务平均需要多长时间。
- 平均token消耗:完成任务平均消耗多少token。
- 恢复能力:系统崩溃后,能否从断点恢复。
- 可调试性:出了问题,能否快速定位原因。
这几个指标里,我认为可调试性是最重要的。因为Agent系统的行为是不确定的,没有好的可调试性,你根本不知道系统为什么成功、为什么失败,也就无法持续优化。
10. 最后分享几个实操中的小技巧
第一个技巧:给Agent加一个“思考日志”。让Agent在每次决策前,先把思考过程写到一个单独的日志里(不放进上下文)。这样排查问题时,你可以看到Agent的完整决策链路,而不是只看到最终的输出。
第二个技巧:工具调用加超时。任何工具调用都要设超时,不要让Agent无限等待。超时后返回一个明确的错误信息,让模型决定是重试还是换方案。
第三个技巧:上下文压缩时保留“最近一次成功的工具调用结果”。这个结果通常是后续步骤的基础,压缩掉之后模型可能会重复调用同一个工具。
第四个技巧:给每个Agent设一个“最大步数”。防止Agent陷入死循环,执行到最大步数后强制终止,返回当前已完成的部分结果。
第五个技巧:定期回放历史任务的执行日志。从日志中找出常见的失败模式,然后针对性地优化工具描述、调整上下文管理策略、或者修改Agent的职责划分。这个习惯帮我发现了很多隐藏的问题。