浅谈Agent系统设计
随笔
现在越来越多的系统集成了Agent,例如美团,大众点评。对于程序员常用的系统来说,有根因分析,自动修复,代码review等等系统。这些系统本质上就是用Agent来完成了某些相对复杂的功能需求,但是因为Agent以及LLM自身的特性,使得我们需要围绕着相应的模块做更多系统上的改进。这篇blog会介绍我的一些经验。其实这些经验是成熟的软件工程实践中经常使用的。
分析
我们可以将讨论的场景抽象为系统A将上下文准备好(无论是文件系统,docker还是prompt),然后将上下文发送给一个特定的Agent,这里可以假设是Codex。然后我们会等到Agent结束,并收集Agent的输出,例如从标准输出流或者文件中。这个系统显然是一个生产者消费者构成的系统。通常来说,我们可能会有很多要完成的任务,因此同一时间会有很多Agent在后台持续的运行。这个简化的场景我觉得基本上涵盖了几乎99%的使用Agent的情况。
异步消息队列
我们需要一个媒介来不断的消费已有的任务,其实就是将任务交付给Agent。这里我们定义一个Worker。Worker的职责是从生产者那里获取任务,然后将任务交给Agent,Agent的工作结束后,我们再唤醒这个Worker,做一些后续的处理,并继续下一轮循环。假设我们的系统最多能够同时运行X个Agent,那么我们就会有X个worker。将所有任务存储到消息队列中是一个比较好的实践。因为Agent有时候会崩溃导致任务失败,此时会比较方便我们做重试等。而且我们也可以将生产者和消费任务的Worker/Agent分离开。消息队列也为我们提供了很多原子操作,非常的省心。
一个坏的实践是自己通过数据库来记录目前生产的任务和任务分配的情况,执行的时间等。这个是GPT5.6 SOL给我提供的方案,如果你真的部署这个方案,你会被并发控制,超时,重试等问题折磨的非常痛苦。当然,本质上可能没有太多区别,不过用现成的轮子会为你提供非常多的便利。
SDK or CLI
很难找到一个推荐使用CLI而不是SDK的场景。因为SDK提供了更灵活和丰富的选项,包括MCP,工具调用,权限控制,工作目录等等。即使CLI也可以提供,但是我们调用CLI的方式通常是通过一个cmd.call或者subprocess来做的,里面会有大量字符串的拼接,维护成本高。另一方面,SDK有一个CLI不一定能实现的特性,那就是可以收集每一步操作的详细内容,这给我们带来了极大的可观测性上的改善,毕竟在长程任务中,信用分配,每一步操作对最终结果的贡献情况,模型的偏好等都只能通过完整的轨迹来进行判断。
任务管理
使用Agent的时候你会频繁的遇到超时等情况,尤其是在模型请求高峰期的时候。当然,这其实也取决于很多因素,例如你的思考等级设置的太高。当一次任务没有正常完成的时候,朴素的想法是对任务进行重试。常见的重试方式有指数避让等。但是仔细想想,我们并不想超时后还从头开始,从我自己的经验上来看,某个任务在执行过程中超时的话,重试继续超时的概率很高。
其实每个任务都是一次对话,它是有session id的,因此借助session id,我们可以实现更灵活的重试。比如接着上次失败的地方继续做(这里可能要额外分配新的最大执行时长),也可以让模型上下文压缩一下后再接着做(在一个新的对话中)。总之,在重试的时候尽量复用之前任务的中间结果,减少Token的消耗。
其他关于任务状态的管理,由简单到复杂有多种方案。最简单的就是通过消息队列控制开始/结束/重试等。复杂一点的话,可能需要记录任务的中间状态,让Agent自己更新或者由Harness更新都可以。前者实现简单,但是不稳定。后者实现会复杂一些,因为需要监听或者定时检查轨迹。如果要支持更复杂的,具有连续+离散的状态管理的话,机制会更复杂一些,但是其实我们很难评估当前距离任务结束还有百分之多少的工作量。
可观测性
可观测性指的是我们能从多少正交的维度来观测Agent模块,以此来判断当前的行为是否正常,以及如何进行Debug。我认为这里存在两种可观测性。一种是稳定性/鲁棒性指标,主要用于观测崩溃,重试,超时,卡死,输出错误,未正常结束等情况,常见的观测指标包括了崩溃次数,重试次数,超时次数,输出错误的次数,Token消耗,轨迹长度,执行时间等等。我所说的都是单位时间内的。另一种是有效性指标,主要用于观测一段时间内Agent完成任务的有效性如何,例如是否成功,效果是否令人满意等。有效性指标就比较难设置了,因为有些任务我们找不到客观指标,只能用主观指标,你一使用主观指标,就会涉及到llm as a judge,那你就要受到幻觉,输出不稳定等问题的影响。目前我认为这里依赖人工较正过的LLM评分更能在生产环境中使用,客观指标虽然可以复现,但是往往是从一些间接角度来设置的。
输出不稳定
LLM输出不稳定是一个老生常谈的问题,比如JSON格式错误,Key错误等。我尝试过JSON,正则表达式,Yaml格式等,还有一些类似JSON Repair的工具。目前LLM指令跟随的能力已经很强了,所以JSON+JSON Repair在95%的场景下应该都够用。但是如果你想要更好的确定性,我的实践是构造一个submit工具,这个工具接收输出的scheme,以及模型的输出,然后校验输出是否有不符合scheme的地方,并像编译器一样把错误返回给Agent。这样,Agent就会自己修改和校验结果。
其实Agent的输出更适合给Agent读,因为Agent对输入的宽容程度很高,我们也不需要使用JSON,而是使用YAML这样更简单的格式也可以。YAML的格式和Python很像,没有那么多符号,通过缩进来控制层次关系。