LangChain初探 - 郑瀚Andrew
2023-7-6 18:6:0 Author: www.cnblogs.com(查看原文) 阅读量:30 收藏

LangChain是一个使用LLMs构建应用程序的工具箱,包含

  • Models(LLM 调用)
    • 支持多种模型接口,比如 OpenAI、Hugging Face、AzureOpenAI ...
    • Fake LLM,用于测试
    • 缓存的支持,比如 in-mem(内存)、SQLite、Redis、SQL
    • 用量记录
    • 支持流模式(就是一个字一个字的返回,类似打字效果)
  • Prompts(Prompt管理):支持各种自定义模板
  • Indexes(对索引的支持)
    • 文档分割器
    • 向量化
    • 对接向量存储与搜索,比如 Chroma、Pinecone、Qdrand
  • Memory
  • Chains
    • LLMChain
    • 各种工具Chain
    • LangChainHub
  • Agents:使用 LLMs 来确定采取哪些行动以及以何种顺序采取行动。操作可以是使用工具并观察其输出,也可以是返回给用户。如果使用得当,代理可以非常强大。
  • Callbacks
  • 等核心模块

本质上讲,LangChain 是一个用于开发由语言模型驱动的应用程序的框架。他主要拥有 2 个能力:

  1. 可以将 LLM 模型与外部数据源进行连接
  2. 允许与 LLM 模型进行交互

参考链接:

https://zhuanlan.zhihu.com/p/628433395
https://serpapi.com/dashboard 
https://github.com/liaokongVFX/LangChain-Chinese-Getting-Started-Guide

因为数据相关性搜索其实是向量运算。所以,不管我们是使用 openai api embedding 功能还是直接通过向量数据库直接查询,都需要将我们的加载进来的数据 Document 进行向量化,才能进行向量运算搜索。转换成向量也很简单,只需要我们把数据存储到对应的向量数据库中即可完成向量的转换。

官方也提供了很多的向量数据库供我们使用。

我们可以把 Chain 理解为任务。一个 Chain 就是一个任务,当然也可以像链条一样,一个一个的执行多个链。 

from langchain.llms import OpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.chains import SimpleSequentialChain

# location 链
llm = OpenAI(temperature=1)
template = """Your job is to come up with a classic dish from the area that the users suggests.
% USER LOCATION
{user_location}

YOUR RESPONSE:
"""
prompt_template = PromptTemplate(input_variables=["user_location"], template=template)
location_chain = LLMChain(llm=llm, prompt=prompt_template)

# meal 链
template = """Given a meal, give a short and simple recipe on how to make that dish at home.
% MEAL
{user_meal}

YOUR RESPONSE:
"""
prompt_template = PromptTemplate(input_variables=["user_meal"], template=template)
meal_chain = LLMChain(llm=llm, prompt=prompt_template)

# 通过 SimpleSequentialChain 串联起来,第一个答案会被替换第二个中的user_meal,然后再进行询问
overall_chain = SimpleSequentialChain(chains=[location_chain, meal_chain], verbose=True)
review = overall_chain.run("Rome")

用于衡量文本的相关性。这个也是 OpenAI API 能实现构建自己知识库的关键所在。

他相比 fine-tuning 最大的优势就是,不用进行训练,并且可以实时添加新的内容,而不用加一次新的内容就训练一次,并且各方面成本要比 fine-tuning 低很多。

0x1:Agents技术基本概念

Agent作为Langchain框架中驱动决策制定的实体。它可以访问一组工具,并可以根据用户的输入决定调用哪个工具。正确地使用agent,可以让它变得非常强大。

Agents 有以下几个核心概念:

  • Tool:执行特定的功能。可以是谷歌搜索,数据库查找,Python REPL,其他暴露API借口的任意工具。工具的接口目前是一个函数,期望以字符串作为输入,以字符串作为输出。
  • LLM:驱动 Agents 的语言模型。
  • Agent:要使用的代理,这应该是一个引用支持代理类的字符串,本质就是一系列prompt program。

简单来说,用户向LangChain输入任意的内容,同时将一套工具集合(也可以自定义工具)托管给LLM,让LLM自己决定使用工具中的某一个(如果存在的话)。

input answer query -> thought for actions -> action call -> observation result -> judge if anymore thought -> loop until finnish answers

从开发者的角度来看,Agents技术带来了两个主要的好处:

  • (1)为工具类软件提供全新的智能交互体验:对于许多工具类软件而言,新手引导是不可或缺的。然而,实际情况是新手引导并未能有效降低用户使用工具的门槛。若能基于Agent构建一个自然语言控制的工具软件,用户将会非常容易上手,真正实现“人人都能上手”的目标。
  • (2)搭建智能化工作流,实现真正的面向NLP的数据驱动智能编程:尽管AutoGPT近期非常火,但我认为更有效的方法是构建一套智能化的工作流。即通过人工预先定义一套流程,然后借助不同的Agent去执行,最终达成特定目标。与AutoGPT相比,这种智能化工作流方法更具可控性和可靠性。

0x2:一个简单的Agents例子

下面用一个简单的例子说明使用过程,

首先,这里自定义了两个简单的工具

from langchain.tools import BaseTool


# 天气查询工具 ,无论查询什么都返回Sunny
class WeatherTool(BaseTool):
    name = "Weather"
    description = "useful for When you want to know about the weather"

    def _run(self, query: str) -> str:
        return "Sunny^_^"

    async def _arun(self, query: str) -> str:
        """Use the tool asynchronously."""
        raise NotImplementedError("BingSearchRun does not support async")


# 计算工具,暂且写死返回3
class CustomCalculatorTool(BaseTool):
    name = "Calculator"
    description = "useful for when you need to answer questions about math."

    def _run(self, query: str) -> str:
        return "3"

    async def _arun(self, query: str) -> str:
        raise NotImplementedError("BingSearchRun does not support async")

接下来是针对于工具的简单调用。

# -*- coding: utf-8 -*-
from langchain.tools import BaseTool
from langchain.agents import initialize_agent
from langchain.llms import OpenAI


# 天气查询工具 ,无论查询什么都返回Sunny
class WeatherTool(BaseTool):
    name = "Weather"
    description = "useful for When you want to know about the weather"

    def _run(self, query: str) -> str:
        return "Sunny^_^"

    async def _arun(self, query: str) -> str:
        """Use the tool asynchronously."""
        raise NotImplementedError("BingSearchRun does not support async")


# 计算工具,暂且写死返回3
class CustomCalculatorTool(BaseTool):
    name = "Calculator"
    description = "useful for when you need to answer questions about math."

    def _run(self, query: str) -> str:
        return "3"

    async def _arun(self, query: str) -> str:
        raise NotImplementedError("BingSearchRun does not support async")


if __name__ == '__main__':
    llm = OpenAI(
        temperature=0,
        openai_api_key="sk-xxxx"
    )
    tools = [WeatherTool(), CustomCalculatorTool()]
    agent = initialize_agent(tools, llm, agent="zero-shot-react-description", verbose=True)
    agent.run("Query the weather of this week, And How old will I be in ten years? This year I am 28")

完整的响应过程:

0x3:基本工作原理

我们来看一下上述例子的工作原理。

首先看输入的问题

Query the weather of this week, And How old will I be in ten years? This year I am 28

查询本周天气,以及十年后我多少岁,今年我28。

主要是调用AgentExecutor_call方法,代码如下:

def _call(self, inputs: Dict[str, str]) -> Dict[str, Any]:
    """Run text through and get agent response."""
    # Construct a mapping of tool name to tool for easy lookup
    name_to_tool_map = {tool.name: tool for tool in self.tools}
    # We construct a mapping from each tool to a color, used for logging.
    color_mapping = get_color_mapping(
        [tool.name for tool in self.tools], excluded_colors=["green"]
    )
    intermediate_steps: List[Tuple[AgentAction, str]] = []
    # Let's start tracking the number of iterations and time elapsed
    iterations = 0
    time_elapsed = 0.0
    start_time = time.time()
    # We now enter the agent loop (until it returns something).
    while self._should_continue(iterations, time_elapsed):
        next_step_output = self._take_next_step(
            name_to_tool_map, color_mapping, inputs, intermediate_steps
        )
        if isinstance(next_step_output, AgentFinish):
            return self._return(next_step_output, intermediate_steps)

        intermediate_steps.extend(next_step_output)
        if len(next_step_output) == 1:
            next_step_action = next_step_output[0]
            # See if tool should return directly
            tool_return = self._get_tool_return(next_step_action)
            if tool_return is not None:
                return self._return(tool_return, intermediate_steps)
        iterations += 1
        time_elapsed = time.time() - start_time
    output = self.agent.return_stopped_response(
        self.early_stopping_method, intermediate_steps, **inputs
    )
    return self._return(output, intermediate_steps)

主要是while循环体中的逻辑,主体逻辑如下:

  • 调用_take_next_step方法
  • 判断返回的结果是否可以结束
  • 如果可结束就直接返回结果,否则继续步骤1-2

_take_next_step方法

在”thought-action-observation循环“中采取单一步骤。重写此方法以控制Agent如何做出选择和行动。

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]],
    ) -> Union[AgentFinish, List[Tuple[AgentAction, str]]]:
        """Take a single step in the thought-action-observation loop.

        Override this to take control of how the agent makes and acts on choices.
        """
        # Call the LLM to see what to do.
        output = self.agent.plan(intermediate_steps, **inputs)
        # If the tool chosen is the finishing tool, then we end and return.
        if isinstance(output, AgentFinish):
            return output
        actions: List[AgentAction]
        if isinstance(output, AgentAction):
            actions = [output]
        else:
            actions = output
        result = []
        for agent_action in actions:
            self.callback_manager.on_agent_action(
                agent_action, verbose=self.verbose, color="green"
            )
            # Otherwise we lookup the tool
            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"] = ""
                # We then call the tool on the tool input to get an observation
                observation = tool.run(
                    agent_action.tool_input,
                    verbose=self.verbose,
                    color=color,
                    **tool_run_kwargs,
                )
            else:
                tool_run_kwargs = self.agent.tool_run_logging_kwargs()
                observation = InvalidTool().run(
                    agent_action.tool,
                    verbose=self.verbose,
                    color=None,
                    **tool_run_kwargs,
                )
            result.append((agent_action, observation))
        return result
  • 调用LLM决定下一步需要做什么
  • 如果返回结果是AgentFinish就直接返回
  • 如果返回结果是AgentAction就根据action调用配置的tool
  • 然后调用LLM返回的AgentAction和调用tool返回的结果(observation)一起加入到结果中

那LLM是怎么判断返回的结果是AgentFinish还是AgentAction呢? 

继续跟进plan方法

def plan(
        self, intermediate_steps: List[Tuple[AgentAction, str]], **kwargs: Any
    ) -> Union[AgentAction, AgentFinish]:
        """Given input, decided what to do.

        Args:
            intermediate_steps: Steps the LLM has taken to date,
                along with observations
            **kwargs: User inputs.

        Returns:
            Action specifying what tool to use.
        """
        full_inputs = self.get_full_inputs(intermediate_steps, **kwargs)
        full_output = self.llm_chain.predict(**full_inputs)
        return self.output_parser.parse(full_output)
  • (1)构建输入参数
  • (2)调用LLM(openai)获取输出结果
  • (3)解析结果,在这里就是根据返回结果判断是AgentFinish还是AgentAction

我们逐个分析上述3个步骤:

构建输入参数

def get_full_inputs(
        self, intermediate_steps: List[Tuple[AgentAction, str]], **kwargs: Any
    ) -> Dict[str, Any]:
        """Create the full inputs for the LLMChain from intermediate steps."""
        thoughts = self._construct_scratchpad(intermediate_steps)
        new_inputs = {"agent_scratchpad": thoughts, "stop": self._stop}
        full_inputs = {**kwargs, **new_inputs}
        return full_inputs

其中使用的prompt template如下:

Answer the following questions as best you can. You have access to the following tools:

Weather: useful for When you want to know about the weather
Calculator: Auseful for when you need to answer questions about math.

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [Weather, Calculator]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}

通过这个模板向openai规定了一系列的规范,包括目前现有哪些工具集,你需要思考回答什么问题,你需要用到哪些工具,你对工具需要输入什么内容,等等。

如果仅仅是这样,openAI会完全补完你的回答,中间无法插入任何内容。因此LangChain使用OpenAI的stop参数,截断了AI当前对话。"stop": ["\\nObservation: ", "\\n\\tObservation: "]

做了以上设定以后,OpenAI仅仅会给到Action和 Action Input两个内容就被stop早停了。在OpenAI返回具体的工具调用指令后,LangChain才能够执行具体的调用并获取返回结果,然后拼接在Observation后面。

换句话说,thought-of-chains的前提是LLM能否分步骤、分段地思考,并在每步的思考间隙停下来,等待具体的外部工具调用返回结果后,再根据返回的结果,继续后续的思考和推理。

继续回到代码分析上来,构建输入参数其实主要就是构建agent_scratchpad参数,具体的步骤如下:

def _construct_scratchpad(
        self, intermediate_steps: List[Tuple[AgentAction, str]]
    ) -> Union[str, List[BaseMessage]]:
        """Construct the scratchpad that lets the agent continue its thought process."""
        thoughts = ""
        for action, observation in intermediate_steps:
            thoughts += action.log
            thoughts += f"\n{self.observation_prefix}{observation}\n{self.llm_prefix}"
        return thoughts
  • action.log:调用LLM返回的action结果
  • observation_prefix:一般就是:"Observation: "
  • observation:调用tools返回的结果
  • llm_prefix:一般就是:"Thought:"

比如:

  • action.log: I need to use two different tools to answer this question
  • observation: Sunny^_^

最终拼接的结果如下:

I need to use two different tools to answer this question
Action: Weather
Action Input: This week
Observation: Sunny^_^
Thought: 

调用LLM

本例中和openai交互,原则上和其他LLM交互也是可以的。

第一次prompt如下:

"Answer the following questions as best you can. You have access to the following tools:

Weather: useful for When you want to know about the weather
Calculator: Auseful for when you need to answer questions about math.

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [Weather, Calculator]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: Query the weather of this week, And How old will I be in ten years? This year I am 28
Thought:"

返回的结果如下:

I need to find out the weather and calculate my age in ten years.
Action: Weather
Action Input: This week
Observation: The weather this week is expected to

这里从Tools中找到name=Weather的工具,然后再将This Week传入方法。具体业务处理看详细情况。这里仅返回Sunny^_^。

由于当前找到了Action和Action Input。 代表OpenAI认定当前任务链并没有结束。因此像请求体后拼接结果:Observation: Sunny 并且让他再次思考Thought:

第二次生成的prompt如下:

"Answer the following questions as best you can. You have access to the following tools:

Weather: useful for When you want to know about the weather
Calculator: Auseful for when you need to answer questions about math.

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [Weather, Calculator]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: Query the weather of this week, And How old will I be in ten years? This year I am 28
Thought: I need to find out the weather and calculate my age in ten years.
Action: Weather
Action Input: This week
Observation: The weather this week is expected to Sunny^_^
Thought:
"

返回结果如下:

I need to calculate my age in ten years
Action: Calculator
Action Input: 28 + 10

由于计算器工具只会返回3,结果会拼接出一个错误的结果,构造成了一个新的prompt请求体。

第三次prompt如下:

"Answer the following questions as best you can. You have access to the following tools:

Weather: useful for When you want to know about the weather
Calculator: Auseful for when you need to answer questions about math.

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [Weather, Calculator]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: Query the weather of this week, And How old will I be in ten years? This year I am 28
Thought: I need to find out the weather and calculate my age in ten years.
Action: Weather
Action Input: This week
Observation: The weather this week is expected to Sunny^_^
Thought: I need to calculate my age in ten years
Action: Calculator
Action Input: 28 + 10
Observation: 3.
Thought: "

返回结果如下:

I need to clarify the observation for my age calculation
Action: Calculator
Action Input: 28 + 10 = 38
Observation: The result of 28 + 10 is 38
Thought: I now know the final answer
Final Answer: The weather this week is expected to be sunny and I will be 38 years old in ten years. 

此时已经得到完成的Thought-of-Chains结果。

OpenAi在完全拿到结果以后会返回I now know the final answer。并且根据完整上下文。把多个结果进行归纳总结。

同时可以看到。ai严格的按照设定返回想要的内容,并且还以外的把28+10=3这个数学错误给改正了

因为看到Finnal Answer了,”thought-action-observation循环“结束。

解释结果

接下来分析一下LangChain是怎么根据LLM返回的结果,判定”thought-action-observation循环“结束的。

class MRKLOutputParser(AgentOutputParser):
    def get_format_instructions(self) -> str:
        return FORMAT_INSTRUCTIONS

    def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
        if FINAL_ANSWER_ACTION in text:
            return AgentFinish(
                {"output": text.split(FINAL_ANSWER_ACTION)[-1].strip()}, text
            )
        # \s matches against tab/newline/whitespace
        regex = r"Action\s*\d*\s*:(.*?)\nAction\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
        match = re.search(regex, text, re.DOTALL)
        if not match:
            raise OutputParserException(f"Could not parse LLM output: `{text}`")
        action = match.group(1).strip()
        action_input = match.group(2)
        return AgentAction(action, action_input.strip(" ").strip('"'), text)

简单的字符串匹配,区分是AgentFinish还是AgentAction
比如第一次返回的结果:

I need to find out the weather and calculate my age in ten years.
Action: Weather
Action Input: This week
Observation: The weather this week is expected to

解释后生成AgentAction

action:'Weather'
action_input:'This week'

第二次返回结果:

I need to calculate my age in ten years
Action: Calculator
Action Input: 28 + 10

同上。

第三次返回的结果:

I need to clarify the observation for my age calculation
Action: Calculator
Action Input: 28 + 10 = 38
Observation: The result of 28 + 10 is 38
Thought: I now know the final answer
Final Answer: The weather this week is expected to be sunny and I will be 38 years old in ten years. 

解释后生成AgentFinish

log: 上面的原文
return_values.output:"The weather this week is expected to be sunny and I will be 38 years old in ten years."

通过以上的分析,我们发现,Agents技术,或者说Thought-of-Chains技术,其本质还是涉及到Prompt的设计,通过设计的Prompt可以让LLM一步步分析和拆解任务,然后调用预制的tool来完成任务。

如果设计比较精美的prompt就可以让LLM自动完成一些比较复杂的任务,这也是AutoGPT和BabyAGI等技术的核心思想。

0x3:对超长文本进行总结

假如我们想要用 openai api 对一个段文本进行总结,我们通常的做法就是直接发给 api 让他总结。但是如果文本超过了 api 最大的 token 限制就会报错。

这时,我们一般会进行对文章进行分段,比如通过 tiktoken 计算并分割,然后将各段发送给 api 进行总结,最后将各段的总结再进行一个全部的总结。

我们可以用 LangChain实现这个功能,他很好的帮我们处理了这个过程,使得我们编写代码变的非常简单。

from langchain.document_loaders import UnstructuredFileLoader
from langchain.chains.summarize import load_summarize_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain import OpenAI

# 导入文本
loader = UnstructuredFileLoader("/content/sample_data/data/lg_test.txt")
# 将文本转成 Document 对象
document = loader.load()
print(f'documents:{len(document)}')

# 初始化文本分割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 500,
    chunk_overlap = 0
)

# 切分文本
split_documents = text_splitter.split_documents(document)
print(f'documents:{len(split_documents)}')

# 加载 llm 模型
llm = OpenAI(model_name="text-davinci-003", max_tokens=1500)

# 创建总结链
chain = load_summarize_chain(llm, chain_type="refine", verbose=True)

# 执行总结链,(为了快速演示,只总结前5段)
chain.run(split_documents[:5])

0x4:构建本地知识库问答机器人

在这个例子中,我们从我们本地读取多个文档构建知识库,并且使用 Openai API 在知识库中进行搜索并给出答案。

我们可以很方便的做一个可以介绍公司业务的机器人,或是介绍一个产品的机器人。

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain import OpenAI
from langchain.document_loaders import DirectoryLoader
from langchain.chains import RetrievalQA

# 加载文件夹中的所有txt类型的文件
loader = DirectoryLoader('/content/sample_data/data/', glob='**/*.txt')
# 将数据转成 document 对象,每个文件会作为一个 document
documents = loader.load()

# 初始化加载器
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)
# 切割加载的 document
split_docs = text_splitter.split_documents(documents)

# 初始化 openai 的 embeddings 对象
embeddings = OpenAIEmbeddings()
# 将 document 通过 openai 的 embeddings 对象计算 embedding 向量信息并临时存入 Chroma 向量数据库,用于后续匹配查询
docsearch = Chroma.from_documents(split_docs, embeddings)

# 创建问答对象
qa = RetrievalQA.from_chain_type(llm=OpenAI(), chain_type="stuff", retriever=docsearch.as_retriever(), return_source_documents=True)
# 进行问答
result = qa({"query": "科大讯飞今年第一季度收入是多少?"})
print(result)

参考链接:

https://juejin.cn/post/7225056158980292667 
https://aitechtogether.com/python/80452.html 
https://juejin.cn/post/7217759646881742903
https://cloud.tencent.com/developer/article/2286923

文章来源: https://www.cnblogs.com/LittleHann/p/17532918.html
如有侵权请联系:admin#unsafe.sh