前段时间确实有点忙,好久没有发文了。不过最近有好多AI技术方面的想法要跟大家分享:-)
今天我们主要聊一聊在AI Agent开发中非常重要的一个特性:human-in-the-loop。
我们在以前的文章中曾经讨论过在AI Agent开发中确定性和自主性的关系问题。自主性带来智能的行为和新的可能性,但软件的交付需要为客户提供确定性。这两者可以说是一对矛盾。
于是,在Agent的执行过程中引入人工确认,就成了消除不确定性的一种思路。想象一个做自动化运维的Agent,它在决定往生产环境部署一个服务之前,很可能需要获得管理员的核准才能继续运行。再想象一个做客户关系维护的Agent,它自动阅读了客户邮件,然后撰写了一封回复邮件,这个时候它在真正发送这封邮件之前,可能也需要先经过人工审核才能发送。而且,审核人员如果觉得邮件内容有欠妥的地方,可能还会给出具体的修改建议。Agent就可以根据人类建议对邮件内容进行修改,然后再继续执行。
AI的产品形式早已不再局限于chat式的一问一答。很多Agent可以长时间运行,比如,在连续运行几分钟甚至几小时后向人类交付结果。在企业环境下,你可以把这种新型的AI Agent想象成一个数字员工。它在业务水平上可能只有实习生的水平,但明显的优势就是不怕累,可以不眠不休地干着重复性的工作。
当然,它在行为方式上也跟实习生类似。想象公司里来了一名实习生,领导让你带着他干活。于是你交给了他一项任务,他就跑到旁边默默地去干了。在干的过程中,他碰到了一个棘手的问题,卡住了。他就会来找你请教,这个情况应该怎么处理。这个时候,你只需要给他一些指导,他就又重新投入到工作中去了。你要做的,只是提供必要的指导,而不用亲自动手去完成所有的事情,负担果然减轻了不少啊。如果这名实习生是一个虚拟的AI Agent呢?那么,它主动来找你提供指导,就是一种human-in-the-loop。作为一名虚拟的“实习生”,这个AI Agent在运行过程中如果碰到棘手的问题,或者待决断的问题,它就会停下来,然后通过各种渠道(比如IM、邮件)来找你。等到你有空了,回复它一下,并提供必要的处理指令,它就继续干活儿去了。
从抽象层面来看,在这样的一个处理过程中,人类被AI Agent牵涉其中,成为了Agent为了完成其自主操作的其中一环。这也是为什么这个机制称为human-in-the-loop。人类在提供核准或者指导意见的时候,我们一方面可以看做是,人类为AI Agent的运行提供了更具体、更准确的上下文;另一方面也可以看做是,AI把人当做了“工具”,它在必要的时候(通常是比较难处理的时候),把人类当做一个工具来调用了(而且这个工具相当智能和权威)。
现在,我们来思考一下,如何在技术层面来实现这种human-in-the-loop的机制。有哪些关键的技术因素需要考虑?
根据上一节的描述,AI Agent需要在执行过程中“停下来”,然后在跟人类完成交互后,再继续运行。有人可能会说,很多编程语言都有await的机制,是不是用await就能实现“停下来再继续运行”的效果?
这当然不是问题的全部。我们需要到真正的生产环境中去考虑这个问题。在生产环境中,有一些复杂的系统架构方面的因素需要考虑。其中有两个因素,对于如何实现human-in-the-loop有关键的影响:
我们先来讨论第一个因素——“分布式”架构的影响。一般来说,生产环境都不止一台服务器,很可能是一个包含多个机器节点的Agent集群。如下图,左边是server端,右边是client端。
从上图我们发现,一个human-in-the-loop交互是由server端主动发起的。这跟传统的互联网应用开发不太一样。
假设一个AI Agent运行在节点A上。它在执行过程中发生了某种特殊事件,于是发起了一个human-in-the-loop的交互。也就是说,它通过某种通道向client端发送request,请求人类的介入。假设用户收到了来自Agent的这个请求,并给出了自己的反馈 (feedback) 。接下来,client端需要把这个feedback发送回server端。由于server端有多个服务器节点,一般来说,来自client端的网络请求会被随机分配到某个服务器节点上。这样就会导致,来自client的feedback信息,未必会落在当初发起human-in-the-loop请求的节点A上;同时,节点A由于收不到feedback而没法把human-in-the-loop继续下去。
之所以会出现上面的问题,除了分布式集群带来的影响之外,还有一个原因是,用户和AI Agent之间的通信通道没有能够做到会话保持 (session sticky) 。根据场景和运行环境的不同,用户和AI Agent之间的通道性质可能呈现很大的差异。下面是几种典型的情况:
针对以上两种情况,还有一些技术实现上需要注意的地方。
首先对于第一种情况,我们在AI应用开发中经常使用的SSE技术 (Server-Sent Events) ,它属于HTTP协议,也具备一定的「server push」的能力,但仍然支持不了让AI Agent主动发起请求。原因在于,SSE依赖client端先建立起同server端的连接之后,server端才能向这个连接进行push。换句话说,SSE本质上其实还是由client主动发起交互的,用于实现一些流式的效果,但server端不能随时发起一个交互(至少在一个常规的实现中是这样的)。
还有一个需要注意的地方是,Agent集群外面可能存在一个网关,所有client都通过这个网关与后面的server建立连接。这时候,要实现server主动发起交互并且做到会话保持,首先要求client和网关之间是某种长连接,其次还要求网关具备会话保持的能力,有能力将server主动发起的request和来自client的feedback保持在一个会话内。这样,整个human-in-the-loop的流程才能由节点A来全部完成。
不管怎么说,在以上这两种情况下,用户和AI Agent之间的通信通道对于开发者来说,还是可控的。但还有第三种情况,这个通道是由第三方提供的,它的性质是开发人员控制不了的。如下图:
考虑到以上这些因素,服务端对于human-in-the-loop的实现,就需要不同的技术方案。
我们把前面几种情况分成两类:
对于(1),我们可以利用长连接和会话保持的优势,让server端的一个节点完成human-in-the-loop的整个交互过程。这样server端的实现就会简单很多。以Python语言为例,human-in-the-loop的机制可以这样实现:
当然,这种实现方式对于基础设施存在比较高的要求,维护长连接和保持会话,通常不是那么容易的事。而且,系统本身维持长连接也是有成本的。
对于(2),client端和server端无法保持会话,来自用户的feedback可能落在任意server节点上。这时只有一种办法:对Agent的整个运行状态进行序列化、持久化、反序列化。整个技术处理流程较复杂,如下:
有人可能会问,什么是序列化和反序列化呢?简单来说,序列化是把一个内存对象转成一串bytes或string的过程;而反序列化是从一串bytes或string中恢复一个内存对象的过程。
不得不说的是,把一个复杂对象进行序列化和反序列化,不是一件容易的事。为什么这么说呢?难度来源于对象之间的关系:
假设仅仅是对于某个数据对象进行序列化和反序列化,情况可能尚在可控范围内。数据对象通常只包含数据字段,数据对象之间的引用关系一般也呈现单向的引用关系。但让问题更复杂的是,Agent对象不仅仅是一个数据对象,它更是一个包含运行行为的运行时对象。运行时对象之间,可能存在错综复杂的引用关系。
总之,我们必须谨慎地选择,把哪些信息放到序列化的数据之中,哪些不放。通常来说,应该只序列化那些必要的、动态的数据,而其他信息可以尽量保留在代码中。以后有机会了我们再仔细展开这个话题。
今天我们探讨了human-in-the-loop这种机制,它出现的技术背景、两种不同的实现思路,以及中间的成本和难点。下一篇,我尝试通过代码来展示这两种实现方案。敬请期待。
(正文完)
其它精选文章: