0. 想法来源
所谓有得必有失。一个便宜的GPT API必定有其便宜的理由。
我现在使用的某个API,便宜是真的便宜,但……显然功能上有所缺失。例如,很多本该支持工具的模型不支持工具调用。
当然,也有部分模型生来就不支持这个。
然而……Astrbot调用函数工具和MCP的方式刚好是把Function Tool直接塞给模型。
我又希望我的Bot可以通过函数工具调用一些奇奇怪怪的东西……
既然先天不支持,那后天摇个轮椅总行了吧。
1. 背景
Astrbot正常的大致运作流程:
触发OnLLMRequestEvent,判断模型是否启用了工具和图像支持。没启用则清空相应内容。
初始化Agent。
执行Agent的step方法。
判断Agent的状态是否为DONE或者ERROR,运行次数是否超过限制。
是则停止循环。
不是则进行返回值处理后返回上一步。
其中Agent的step流程为:
转换运行状态为RUNNING。
发送LLM请求。
判断请求是否成功。不成功则转换状态为ERROR。
判断工具调用是否为空。为空转换状态至DONE,触发on_agent_done,触发OnLLMResponseEvent
如果信息链不为空,或LLM响应文本不为空,返回LLM响应。
如果工具调用不为空,调用工具,将工具结果添加回请求。
2. 问题与解决方案
Astrbot提供了一套很不错的插件接口。至少注册和调试还算方便。
也有一些接口刚好命中我们的需求。
先考虑一下我们最理想的流程应该是什么样子:
向API发送LLM请求前,截获请求并把工具列表和说明塞到API支持的地方。
显然,稳妥保险的方式是塞到Prompt里。LLM响应时,判断是否有调用工具。如果有的话,调用工具获取结果。
把结果塞回给LLM,重复上述流程
显然我们可以在OnLLMRequestEvent时将工具信息塞入Prompt中,在OnLLMResponseEvent时判断有无工具调用,把信息链和响应文本清空防止Agent提前返回,使其调用工具。
但是问题出现了:OnLLMResponseEvent触发时,Agent状态已经转为DONE。哪怕此时我们把工具调用塞入了LLM Response,工具被调用后,结果也塞回了Request。但是Astrbot认为Agent状态已经是DONE了,不会再去调用step,请求永远发不出去。
我们需要劫持Agent的状态。但是显然没有这样的接口。
另外,运行过程中大概率不是只有一个Agent。我们得找到正确的Agent。
OnLLMResponseEvent中我们也无法获取到Agent。我们得想办法访问它。
于是,我劫持了Agent的_transition_state方法。这个方法负责状态转化。在这个方法里我可以通过self来获取Agent对象。
OnLLMResponseEvent中可以获得LLM Response。可以用它来生成ID。每个Agent在一次step中只会创建一个Response。我们可以用Response关联到Agent。
我们创建一个dict来储存ID和Agent的对应关系。这样我们就能在自己的函数中获得任意一个Agent。我们在_transition_state中将Agent储存到dict里。
现在,OnLLMResponseEvent触发时,我们可以通过Response找到这个Agent,修改它的状态和响应,然后使它继续执行,完成它该干的事情。
3. 后记
说实话,我还是希望api能原生实现我想要的功能。但……没人能在开始时想得那么周到。
每个目标都有许多种实现的途径。加钱,加力,加智慧,加机遇。
