一、实现目标
第一篇文章就有交代,想要打造一个AIGC,既然都是AI了,那么当然希望人工干预的成分越低越好,所以初步想法就是
1)我输入一些关键字;
2)ChatGLM可以自主识别出我意图;
3)ChatGLM可以自主判断出为了满足我的意图,它需要执行哪些动作;
但是ChatGLM只是一个预训练的大语言模型,在执行动作上这个层面上来讲,可行性不大,那么就只有基于ChatGLM做业务层的开发,将单一业务封装成一个一个的工具,通过Prompt预先告诉ChatGLM这些工具的作用,然后让它去判断要使用哪些工具,以及这些工具调用的顺序情况
langchain在这一方面为我们做了很好的封装,但是经过测试,并不完全适合于ChatGLM,网上针对这一块到处的说法都千篇一律,也搞得好像很深奥的样子,其实就是提示词工程
二、Agent概述
2.1 基本概念
Agent主要分为两种:
执行代理:在每一步骤需要执行前,使用所有先前执行的输出来决定下一个执行
计划和执行代理:预先决定完整的操作顺序,然后执行所有操作而不更新计划
Agent里面涉及的其他类包括:
工具:代理可以采取的操作。说直接一点就是具体干活的
这些概念不清楚其实也没关系,主要是明白自己想做什么,明白代理和工具的关系。用户把关键信息告诉Agent,Agent决定使用哪些工具,可能有的朋友会疑惑,不是说ChatGLM决定使用哪些工具的吗?
解惑:Agent里面有一个对象llm,最终决定使用哪些工具是Agent里面的llm(ChatGLM)决定的
2.2 执行流程及重点类&方法&属性

接下来,在清楚流程之后,再重源码上去梳理整个调用关系以及涉及到的类和方法,代码具体的梳理跟踪过程就不一一展示了,主要说一下在整个流程中比较关键的类和方法以及他们的作用。重点,重点,重点
1) AgentExecutor: 这个类就是在实际调用工具方法的类,其中很关键的方法是_call,_take_next_step两个方法,AgentExecutor集成于Chain,_call方法相当于是执行入口,而在_call方法内部调用了_take_next_step方法。_take_next_step方法是核心,这个方法其实就是在循环调用GLM挑选出的工具集合,并返回每个工具执行的结果(记住,这个方法在langchain里面是在最后return的是“每个工具执行的结果集合”)
observation就是工具执行的结果
2)BaseMultiActionAgent:这个类就是一个Agent类,llm就是被包装到这个类里面,其中的关键方法是plan,这个方法就是在挑选工具并做排列工具调用的顺序(在langchain中这是个抽象方法,很明显是拿来被重写的)
3)AgentAction,AgentFinish:这两个类是数据类,并没有方法,主要作用是让AgentExecutor类下的_call方法判断工具是否已经调用完,是否需要返回最终结果,如果是AgentFinish这直接返回最终结果
4)BaseTool:工具类,其中的关键方法是_run,工具的具体执行动作都写在这个方法下,在这个类里面还有一个很重要的属性:return_direct:bool,为True则代表执行完这个方法后直接返回执行结果(后面会利用这个属性做重写)
说明:以上重点类,方法,以及整个执行过程,均为我个人项目里面所涉及到的内容,并不代表langchain的正确使用方式
2.3 源码调用顺序
我们先看客户端是怎样调用代理的
# 1、实例化llm 看不懂这一句的情去看我前几篇文章
llm_helper = ChatGLM_Helper.instance()
# 2、定义工具集合XHS_Note_Tool,Travel_Tools_Base_XC分别是两个不同的工具
tools = [
XHS_Note_Tool(llm=llm_helper.llm),
Travel_Tools_Base_XC(llm=llm_helper.llm)]
# 3、实例化Agent类,API_Sequence_Agent是我自己重写的一个类,后面会详细说明
agent = API_Sequence_Agent(tools=tools, llm=llm_helper.llm)
# 4、使用agent和tools实例化出一个AgentExecutor
agent_exec = Sequence_AgentExecutor.from_agent_and_tools(agent=agent,
tools=tools,
verbose=True,
max_iterations=1)
# 5、调用AgentExecutor的run方法直接执行代理,AgentExecutor继承于Chain
# 5、其实是在调用Chain的run方法,而Chain的run方法里面直接返回self(),所以是在调用Chain的__call__
# 5、上篇文章有讲,Chain的__call__其实是在调用子类的_call,也就是AgentExecutor的_call
agent_generate = agent_exec.run(prompt)
从run方法开始现在顺着入口F12跟踪源码查看整个调用链的顺序结构(具体每一步就不做详细介绍了)

在_take_next_step方法中调用plan,让Agent去判断调用哪些工具,以及工具调用的顺序,然后在按顺序调用工具的_run方法获得执行数据
三、langchain框架下打造适配ChatGLM的Agent
通过上面的执行流程和代码调用流程的了解,我开始打造自己的Agent,langchain本身的源码应该是基于ChatGPT进行打造的Agent,不太适用于ChatGLM,原因就不多说了,所以我需要动手改造的地方如下:
1)定义自己的提示词模板(让LLM判断使用哪些工具的模板)langchain本身是采用边执行边思考的策略,这种策略确实不合适,所以改造
SEQUENCE_EXECUTE_API_TOOLS_PROMPT_TEMPLATE: str = """
已知工具信息:{intents},你需要根据已知工具信息,自主思考为了满足用户意图,必须从已知工具中按从
前到后的逻辑顺序调用哪些工具(某些工具的输出可以作为另一些工具的输入),
并且你的输出格式必须按照:
工具调用链:[你认为要调用的工具的名称。如果需要调用多个工具,请用按从前到后的调用顺序输出工具名
称,并用逗号分割]
不能随意更改输出格式
例如:
用户输入:创作一篇敦煌的游记,并发布到【XXX】平台
工具调用链:[XXX笔记查询工具,XXX旅行游记生成工具]
用户输入:'{query}'
"""
2)重写Plan,按我自己的提示词进行计划,按我规定的格式进行输出,然后根据输出得到对应工具信息
import re
from typing import List, Tuple, Any, Union
from langchain.schema import AgentAction, AgentFinish
from langchain.agents import BaseMultiActionAgent
from langchain import LLMChain, PromptTemplate
from langchain.base_language import BaseLanguageModel
from agents.BaseTools.base_tools import functional_Tool
from agents.Config.global_config import SEQUENCE_EXECUTE_API_TOOLS_PROMPT_TEMPLATE
# 创建Controller来识别用户意图以及实现用户意图需要调用的工具集合
class API_Sequence_Agent(BaseMultiActionAgent):
tools: List[functional_Tool]
llm: BaseLanguageModel
intent_template: str = SEQUENCE_EXECUTE_API_TOOLS_PROMPT_TEMPLATE
prompt = PromptTemplate.from_template(intent_template)
llm_chain: LLMChain = None
def get_llm_chain(self):
if not self.llm_chain:
self.llm_chain = LLMChain(llm=self.llm, prompt=self.prompt)
def output_parser(self, text: str):
if not text:
raise ValueError("未能获取到需要解析的文本信息")
# 使用正则表达式匹配方括号中的内容
matches = re.findall(r'[(.*?)]', text)
# 将匹配到的内容构造成一个数组
result_array = []
if matches:
result_array = [match.strip() for match in matches[0].split(',')]
return result_array
# 根据提示(prompt)选择工具
def choose_tools(self, query) -> List[str]:
self.get_llm_chain()
tool_names = [{tool.name: tool.description} for tool in self.tools]
resp = self.llm_chain.predict(intents=tool_names, query=query)
select_tools = self.output_parser(resp)
return select_tools
@property
def input_keys(self):
return ["input"]
# 通过 AgentAction 调用选择的工具,工具的输入是 "input"
def plan(self, intermediate_steps: List[Tuple[AgentAction, str]],
**kwargs: Any) -> Union[List[AgentAction], AgentFinish]:
# 单工具调用
tools = self.choose_tools(kwargs["input"])
for tool in self.tools:
if tool.name == tools[-1]:
tool.return_direct = True
result: List[Union[AgentAction, AgentFinish]] = []
for tool in tools:
result.append(
AgentAction(tool=tool, tool_input=kwargs["input"], log=""))
return result
async def aplan(self, intermediate_steps: List[Tuple[AgentAction, str]],
**kwargs: Any) -> Union[List[AgentAction], AgentFinish]:
raise NotImplementedError("IntentAgent does not support async")
注意plan方法里有一句:
for tool in self.tools:
if tool.name == tools[-1]:
tool.return_direct = True
这是很重要的一个细节点,最后一个工具的return_direct 设置为True,在_take_next_step方法调用工具时才知道哪一个工具执行完之后直接返回结果,langchain原本策略是让chatgpt去判断是否可以返回最终结果,但是ChatGLM....,我的策略就是假设ChatGLM在判断工具( tools = self.choose_tools(kwargs["input"])),并整理工具顺序时就已经完全正确,包含了所有工具信息,排序也完全正确
3)重写_take_next_step,这是关键中的关键
from typing import Dict, List, Optional, Tuple, Union
from langchain.agents import AgentExecutor
from langchain.callbacks.manager import CallbackManagerForChainRun
from langchain.schema.agent import AgentAction, AgentFinish
from langchain.tools.base import BaseTool
class Sequence_AgentExecutor(AgentExecutor):
@property
def input_keys(self) -> List[str]:
"""Return the input keys.
:meta private:
"""
return self.agent.input_keys
@property
def output_keys(self) -> List[str]:
"""Return the singular output key.
:meta private:
"""
if self.return_intermediate_steps:
return self.agent.return_values + ["intermediate_steps"]
else:
return self.agent.return_values
def _take_next_step(
self,
name_to_tool_map: Dict[str, BaseTool],
color_mapping: Dict[str, str],
inputs: Dict[str, str],
intermediate_steps: List[Tuple[AgentAction, str]],
run_manager: Optional[CallbackManagerForChainRun] = None,
) -> Union[AgentFinish, List[Tuple[AgentAction, str]]]:
intermediate_steps = self._prepare_intermediate_steps(
intermediate_steps)
output = self.agent.plan(
intermediate_steps,
callbacks=run_manager.get_child() if run_manager else None,
**inputs,
)
if isinstance(output, AgentFinish):
return output
actions: List[AgentAction]
if isinstance(output, AgentAction):
actions = [output]
else:
actions = output
result = []
for agent_action in actions:
if run_manager:
run_manager.on_agent_action(agent_action, color="green")
if agent_action.tool in name_to_tool_map:
tool = name_to_tool_map[agent_action.tool]
return_direct = tool.return_direct
color = color_mapping[agent_action.tool]
tool_run_kwargs = self.agent.tool_run_logging_kwargs()
if return_direct:
tool_run_kwargs["llm_prefix"] = ""
if result is not None and len(result) > 0:
a, o = result[-1]
observation = tool.run(
o,
verbose=self.verbose,
color=color,
callbacks=run_manager.get_child()
if run_manager else None,
**tool_run_kwargs,
)
else:
observation = tool.run(
agent_action.tool_input,
verbose=self.verbose,
color=color,
callbacks=run_manager.get_child()
if run_manager else None,
**tool_run_kwargs,
)
if tool.return_direct is False:
result.append((agent_action, observation))
else:
return AgentFinish({"output": observation},
log=observation)
return result
Sequence_AgentExecutor集成于AgentExecutor,看_take_next_step方法的最后
if tool.return_direct is False:
result.append((agent_action, observation))
else:
return AgentFinish({"output": observation},
log=observation)
如果return_direct 为True就直接返回AgentFinish,AgentExecutor类中的_call在接收到AgentFinish类型的结果后就会认为工具链调用完毕,直接返回结果到客户端。所以在我的策略里面,工具链的最后一个工具的return_direct 属性一定为True,否则就只能等待程序超时或报错
4)封装工具
from typing import Optional
from langchain.tools import BaseTool
from langchain.callbacks.manager import (
AsyncCallbackManagerForToolRun,
CallbackManagerForToolRun)
# 重写抽象方法,"call_func"方法执行 tool
class functional_Tool(BaseTool):
name: str = ""
description: str = ""
def _call_func(self, query, **kwargs):
raise NotImplementedError("subclass needs to overwrite this method")
def _run(self,
query: str,
run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
return self._call_func(query)
async def _arun(
self,
query: str,
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
) -> str:
raise NotImplementedError("APITool does not support async")
定义一个基础工具类,所有的工具都集成于这个类
接着实现一个自定义工具类
from datetime import datetime
from langchain.schema.language_model import BaseLanguageModel
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from agents.BaseTools.base_tools import functional_Tool
from agents.Config.global_config import TOOLS_NOTE_TOOL_PROMPT_TEMPLATE
from utils.pipeline import Pipeline_Item, Pipeline_Process_Task
from utils.genrate.note_generate_utils_xhs import Note_Generate_Utils_XHS
from utils.genrate.note_generate_utils_xc import Note_Generate_Utils_XC
from utils.genrate.note_generate_utils import Content_Type
class XHS_Note_Tool(functional_Tool):
llm: BaseLanguageModel
name = "XXX旅行游记生成工具"
description = "自动生成相关游记文章,并发布到XXX平台"
qa_template = TOOLS_NOTE_TOOL_PROMPT_TEMPLATE
prompt = PromptTemplate(input_variables=["intents", "query"],
template=qa_template)
llm_chain: LLMChain = None
categories = ["XXX", "XXX", "XXX", "XXX", "XXX", "XXX", "XXX", "XXX", "XXX", "XXX"]
def parser_output(self, output: str):
if not output:
raise ValueError("未能获取到正确的输入信息")
# 按换行符分割字符串
lines = output.split('
')
# 提取意图类别和关键字
intent_category = lines[0].split(':')[1].strip()
keywords = lines[1].split(':')[1].strip()
if "、" in keywords:
keywords = keywords.split('、')[0].strip()
if "," in keywords:
keywords = keywords.split(',')[0].strip()
return intent_category, keywords
def _call_func(self, query) -> str:
if not query:
raise ValueError("未能获取到正确的用户信息")
self.get_llm_chain()
..........
# 这个方法里面就是需要自己实现的工具执行代码,至于需不需要llm就根据自己的情况来定
# name和description是必须要的LLM模型就是根据 name和description在判断是否要调用这个工具
# 可以对照1)中的提示词模板揣摩一下name和description的作用
def get_llm_chain(self):
if not self.llm_chain:
self.llm_chain = LLMChain(llm=self.llm, prompt=self.prompt)
到此为止,自己的工具基本上已经完成,目前在ChatGLM2-6b上跑得很通畅,工具判断和调用几乎都没问题