Agent的中文名叫做代理或者智能体,和一般的对话模式相比,进入Agent模式的大模型,具有更多的自我意识和独立思考能力,同时它在回答问题时除了可以利用训练时内化到无穷参数中的知识,还可以利用不同的工具,甚至是将多种工具组合起来以解决特定的问题。
Agent的实现其实不难,其主要技术思路是通过提示工程在常规的思考问题之上再构建一层反思,引导大模型思考和拆解输入问题,并自主判断每个子问题需要完成的动作,也就是要调用的工具以及该工具的输入,在获取到每个动作的返回结果后,对结果进行分析和反思,在全流程结束后对结果进行整合。
现在已经有很多模型和框架支持Agent的实现,例如Langchain和ChatGPT的经典组合,最近发布的一些国产开源模型,如Qwen和ChatGLM3,也添加了对于Agent的支持。下面我就以ChatGLM3给出的langchain调用代码为例,测试一下ChatGPT和ChatGLM3的Agent能力,顺便读懂Langchain的整套逻辑,看它能不能完成一些较为复杂的任务。
工具定义
首先下载上面给出的github项目并安装环境,然后想想要测试什么问题。我打算问一个这样的问题:,理想情况下,Agent加持的大模型会分析出需要调用获取字符串长度和计算三次幂这两个工具,然后连续调用它们得到正确的结果。
所以第一步就是要把这两个工具定义好,在这个项目里,只要在Tool文件夹下定义好和这两个文件,这个工具就算可以使用了,我们对这两个工具的定义如下,注意这些工具的输入和输出都得是字符串。(yaml文件是给ChatGLM3用的,所以只是测ChatGPT的话不用定义也可以)
模型定义
已知ChatGPT的api_key和base_url,回答时用到的llm定义如下:
如果你写成下面这种:
就会喜提报错:
如果你地址里少写了v1,写成下面这种:
就会喜提另一种看不懂的报错:
工具导入
在main函数里面写:
然后看一下的实现,其实逻辑很简单,就是用传进来的tool和llm来初始化一个agent,然后执行prompt:
Agent类型
在上面的代码中,初始化agent时需要指定这个参数,不同AgentType对于结果的影响很大,下面汇总一下不同的AgentType。
首先介绍一些常见的关键词:
- ReAct:由单词“Reason”和“Act”组合而成,前者对应于推理,即大模型的通用文本逻辑判断能力,或者说是对问题进行思考和拆解的能力;后者对应于行动,即具备专业知识的特定领域精确回答能力,或者说是调用外部工具的能力。ReAct顾名思义就是把思考和行动相结合,通过二者的依次迭代执行完成任务。
- Zero-shot:零样本,或者说是无记忆。在运行时,只考虑与当前代理的一次交互,不保留对话历史。
- Conversational:引入了对话历史,因为有记忆了,所以需要初始化代理时引入memory 参数。其一个缺点是可能无法执行复杂的Tool调用任务。
- Chat:常规情况下以OpenAI方式初始化LLM,此类Agent可以以ChatOpenAI方式初始化模型。前者是更通用的接口,用于与不同类型的语言模型进行交互,可以与各种LLM模型集成。ChatOpenAI接口是对其的高级封装,更专注于对话式交互。
Agent测试
对基于REACT的几种Agent做了测试,结果如下,可以看出在我们的这个设定下,ZERO_SHOT_REACT_DEscriptION是
- CONVERSATIONAL_REACT_DEscriptION
看到CONVERSATIONAL,就知道要引入memory了,代码修改如下,注意一定要把return_messages设为TRUE,不然会报这种奇怪的错误:
以自问自答形式思考,解析结果失败
- CHAT_CONVERSATIONAL_REACT_DEscriptION
以JSON形式思考,但只能调用一次工具,回答得不对
- ZERO_SHOT_REACT_DEscriptION
以思维链形式思考,非常完美的回答
- CHAT_ZERO_SHOT_REACT_DEscriptION
以JSON形式思考,但整理成了无法识别的嵌套格式,报错了
- STRUCTURED_CHAT_ZERO_SHOT_REACT_DEscriptION
以JSON形式思考,很艰难地得到了正确答案
提示词分析
上面我们可以看到,不同Agent的输出格式相差很大,这主要是由于Agent内置的提示词不同导致的。在使用初始化agent变量之后,可以查看生成的提示词,比较它们的区别。
- 以ZERO_SHOT_REACT_DEscriptION为例,对其生成的变量agent打印可以看到一个非常完整的思维链提示
-
而STRUCTURED_CHAT_ZERO_SHOT_REACT_DEscriptION的情况比较不一样,拆分成了系统和用户提示词,可以分别打印和来查看:
-
用户提示
Langchain中最核心的chain,其实就是llm+template+input。使用agent时的template又等于Template+tool,其中Template就是不同agent自带的模版定义,tool是用户传入的工具列表,input是当前输入的问题。从这个角度看,agent是一种特殊的chain,其整体执行过程如下图所示。
解析算法分析
不管是思维链文本形式还是json形式,大模型上面的输出都是很符合人类的推理思维的,那么程序内部又是怎么将文本转化成程序的实际动作,或者说是如何完成Action的呢。这就要看一下agent中用到的的实现了。
- 以ZERO_SHOT_REACT_DEscriptION为例,执行下面代码查看函数的实现
代码主要通过正则匹配解析大模型输出,判断是否包含FINAL_ANSWER_ACTION和Action/Input,然后封装返回不同的对象。主要逻辑分支包括是否匹配、答案在前后、包含答案但没有匹配等情况。因此,大模型只有输出的回答非常符合规范才行,某些开源离线模型由于智商过低,回答得乱七八糟,很容易出现无法正确解析的情况。
如果格式正确,那么会返回两种类型,要么对话结束返回,要么继续动作执行。
- STRUCTURED_CHAT_ZERO_SHOT_REACT_DEscriptION就比较复杂了,直接输出的结果是这样,可以定义或者用进行解析,这里应该是调了预定义的。
执行下面的命令,打印出的实现看看
可以看出其本质上就是在走这样一个流程,尝试用解析器解析,如果解析失败出现格式问题,使用retry_chain尝试重整格式并再次解析,最多重复max_retries次。
下面我们再看看内部的逻辑,其实跟之前差不多,还是正则表达式,不同的是通过JSON来提取action的具体参数。
自定义Agent
其实Agent最重要的就是两个部分,一部分是要定义好Template模版规范模型的输出格式,一部分是要定义好output_parser去解析输出内容。前者可以通过继承类来实现,后者可以通过继承类来自定义,这里就不细说了。
基于LLM的Tool
Tool的定义不仅仅可以基于算法,也可以引入一个LLM,比如说写一个小说创作的Tool。
模型定义
看了看里定义的类,其实最重要的就两块,一个是模型加载
另一块就是模型推理了
注意在上面的代码中,传入的prompt即agent生成的模版内容,其中对工具及其传入参数的描述为字符串形式,形如。但是ChatGLM可能训练时没有格式与之对齐,不能直接将其作为输入,因此需要使用做一步转换,将字符串转换为json。
转换时首先用正则表达式提取出工具名,然后从本地读取对应的yaml文件为json,其中包含了对函数以及函数输入参数的详细描述,形如。最后按如下格式返回history和query。
在执行推理步骤后,整个对话的history如下图所示:
测试效果
支持对话
虽然现在功能算是测试通过了,但有一个小问题,那就是这个模型是不支持对话的,只能直接调用工具,在一些场景下,可能需要常规对话和工具调用并行不悖,这时候可以对其推理函数做一些小小的修改,加入对当前是否为agent模式的判断。