用 LangChain 构建基于资料库的问答机器人(一):基础用法

大家好,我是学生大使 Jambo。在上一个系列中,我们介绍了关于 Azure OpenAI API 的使用。如果你有跟着教程使用过,那么你应该能感觉到仅仅是调用 API 是非常简单的,繁琐的是如何将 API 与你的应用结合起来。接下来,我将会介绍一个名为 LangChain 的库,它可以帮助你更方便地将 Azure OpenAI 结合到你的应用中。

我也会将这个做成一个系列,最终目标是实现一个可以根据资料库回答问题的聊天机器人。

为什么要用 LangChain

许多开发者希望将像 GPT 这样的大语言模型整合到他们的应用中。而这些应用不仅仅是简单地将用户的输入传递给 GPT,然后将 GPT 的输出返回给用户。

这些应用可能需要根据特定的资料源来回答问题,因此需要考虑如何存储和查找资料。或者需要整理用户的输入,保存以前的消息记录并提取重点。如果你希望模型按照特定的格式输出文本,那么你需要在 prompt(提示)中详细描述格式,甚至需要提供示例。这些 prompt 通常是应用程序后台进行管理,用户往往不会注意到它们的存在。对于一些复杂的应用程序,一个问题可能需要多个执行动作。例如声称可以自动完成指定项目的 AutoGPT,实际上是根据目标和作者编写的 prompt 生成所需的动作并以JSON格式输出,然后程序再执行相应的动作。

LangChain 基本上已经将这些你可能会使用到的功能打包好了,只需要规划程式逻辑并调用函数即可。此外,LangChain 的这些功能与具体使用的模型API无关,不必为不同的语言模型编写不同的代码,只需更换 API 即可。

基本用法

在使用 LangChain 之前,建议先了解 Azure OpenAI API 的调用,否则即使是使用 LangChain,参数和用法也可能不容易理解。具体可以参考我之前的系列教程:用 Python 调用 Azure OpenAi API

LangChain 将由文字续写(补全)的语言模型称为 llm ,拥有聊天界面(输入为聊天记录)的语言模型称为聊天模型。接下来我们也会用 Azure OpenAI API 来进行示例。

安装

因为 LangChain 在调用 OpenAI 的 API 时,实际上会使用 OpenAI 提供的 SDK,因此我们还需要一并安装 openai

pip install langchain
pip install openai

生成文本

实例化模型对象

在使用 API 之前,我们需要先设置环境变量。如果你使用的是 OpenAI 原生的接口,就只需要设置 api_key;如果是 Azure OpenAI API 则还需要设置 api_versionapi_base ,具体的值与使用 openai 库调用 Azure API 一样,可以参考我之前的教程:用 Python 调用 Azure OpenAi API

import os
os.environ["OPENAI_API_KEY"] = ""
os.environ["OPENAI_API_VERSION"] = ""
os.environ["OPENAI_API_BASE"] = ""

当然,这些值也可以在 terminal 中使用 export (在 Linux 下)命令设置,或者在 .env 文件中设置,然后用 python-dotenv 库导入进环境变量。

LangChain 的大语言模型(llm)的类都封装在 llms 中,我们需要从中导入 AzureOpenAI 类,并设置相关的参数。其中指定模型的参数名是 deployment_name,剩下的参数就是 OpenAI API 的参数了。事实上,上面在环境变量中设置的 API 信息也可以在这里作为参数传入,但考虑到便利性和安全性,仍建议在环境变量中设置 API 信息。

要注意的是,prompt 和 stop 参数并不是在这里传入的(stop 可以但是会报警告),而是在下面生成文本时传入。

from langchain.llms import AzureOpenAI
llm = AzureOpenAI(
    deployment_name="text-davinci-003",
    temperature=0.9,
    max_tokens=265,
)

另外,如果你使用的是原生 OpenAI API ,那么导入的类应该是 OpenAI ,并且指定模型的参数名是 model_name,例如:

from langchain.llms import AzureOpenAI
llm = AzureOpenAI(model_name="text-davinci-003")

序列化 LLM 配置

假如你需要对多个场景有不同的 llm 配置,那么将配置写在代码中就会不那么简单灵活。在这种情况下,将 llm 配置保存在文件中显然会更方便。

from langchain.llms import OpenAI
from langchain.llms.loading import load_llm

LangChain 支持将 llm 配置以 json 或 yaml 的格式读取或保存。假设我现在有一个 llm.json 文件,内容如下:

{
    "model_name": "text-davinci-003",
    "temperature": 0.7,
    "max_tokens": 256,
    "top_p": 1.0,
    "frequency_penalty": 0.0,
    "presence_penalty": 0.0,
    "n": 1,
    "best_of": 1,
    "request_timeout": null,
    "_type": "openai"
}

那么我们可以使用 load_llm 函数将其转换成 llm 对象,具体使用的是什么语言模型是使用 _type 参数定义。

llm = load_llm("llm.json")
# llm = load_llm("llm.yaml")

当然你也可以从 llm 对象导出成配置文件。

llm.save("llm.json")
llm.save("llm.yaml")

从文本生成文本

接下来我们就要使用上面实例化的模型对象来生成文本。LangChain 的 llm 类有三种方法从 String 生成文本:predict() 方法、generate()方法、像函数一样直接调用对象(__call__)。

看上去途径很多,但其实都只是 generate() 一种而已。具体来说,perdict() 简单检查后调用了 __call__ ,而 __call__ 简单检查后调用了 generate()generate() 方法与其他两种途径最大的区别在于 prompt 的传入和返回的内容:generate() 传入的是包含 prompt 的 list 并返回一个 LLMResult 对象,而其他两种方法传入的是 prompt 本身的 string ,返回生成文本的 string。意思是 generate() 可以一次对多个 prompt 独立生成对应的文本。

prompt = "1 + 1 = "
stop = ["\n"]
# 下面三种生成方法是等价的
res1 = llm(prompt, stop=stop)
res2 = llm.predict(prompt, stop=stop)
res3 = llm.generate([prompt], stop=stop).generations[0][0].text

如果只是想单纯的从文字续写(生成)文字的话,推荐用 predict() 方法,因为这种最方便也最直观。

聊天模型

实例化模型对象

与上面的生成模型一样,我们需要先设置环境变量

import os
os.environ["OPENAI_API_KEY"] = ""
os.environ["OPENAI_API_VERSION"] = ""
os.environ["OPENAI_API_BASE"] = ""

LangChain 的聊天模型包装在 langchain.chat_models 下,我们这里一样使用 Azure OpenAI 进行演示,导入的是 AzureChatOpenAI 类。

如果你有读过我之前直接调用 API 的教程,那么应该清楚我们对聊天模型输入的 prompt 不再是文字,而是消息记录,消息记录中是用户和模型轮流对话的内容,这些消息被 LangChain 包装为 AIMessageHumanMessageSystemMessage,分别对应原先 API 中的 assistantusersystem

from langchain.chat_models import AzureChatOpenAI
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage
)

chat = AzureChatOpenAI(deployment_name="gpt-35-turbo", temperature=0)

我们先构建一个初始的消息记录,当然 SystemMessage 并不是必须的。聊天模型用直接调用对象的方式生成消息,而他返回的会是一个 AIMessage 对象。

messages = [
    SystemMessage(content="你是一名翻译员,将中文翻译成英文"),
    HumanMessage(content="你好世界")
]
chat(messages)
AIMessage(content='Hello world.', additional_kwargs={}, example=False)

和之前一样,聊天模型的 generate() 方法也支持对多个聊天记录生成消息,不过这里的返回值是一个 LLMResult 对象。

chat.generate([messages, messages])

LLMResult

上面说 llm 的 generate() 方法返回的是一个 LLMResult 对象,它由三个部分组成:generations 储存生成的内容和对应的信息、llm_output 储存 token 使用量和使用的模型、run 储存了唯一的 run_id,这是为了方便在生成过程中调用回调函数。通常我只需要关注 generationsllm_output

为了展示 LLMResult 的结果,这里我们重新创建一个 llm 对象,并设置参数 n=2 ,代表模型对于每个 prompt 会生成两次结果,这个值默认是 n=1

llm = AzureOpenAI(deployment_name="text-davinci-003", temperature=0, n=2)
llm_result = llm.generate([f"{i}**2 =" for i in range(1, 11)], stop="\n")
print(len(llm_result.generations))
# -> 10
print(len(llm_result.generations[0]))
# -> 2

因为 LLMResult 是继承自 Pydantic 的 BaseModel ,因此可以用 json() 将其格式化为 JSON :

print(llm_result.json())
{
    "generations": [
        [
            {
                "text": " 1",
                "generation_info": {
                    "finish_reason": "stop",
                    "logprobs": null
                }
            },
            {
                "text": " 1",
                "generation_info": {
                    "finish_reason": "stop",
                    "logprobs": null
                }
            }
        ],
        ...
    ],
    "llm_output": {
        "token_usage": {
            "prompt_tokens": 40,
            "total_tokens": 58,
            "completion_tokens": 18
        },
        "model_name": "text-davinci-003"
    },
    "run": {
        "run_id": "cf7fefb2-2e44-474d-918f-b8695a514646"
    }
}

可以看到 generations 是一个二维数组,第一维的每个元素代表对应的 prompt 生成的结果,第二维的每个元素代表这个 prompt 的一次生成结果,因为我们设置了 n=2 ,所以每个 prompt 会生成两次结结果。

对于生成(补全)模型,生成的结果会是一个字典,具体生成的内容在 text 字段。而对于聊天模型,它会把结果包装成 ChatGeneration 对象,其中 text 字段是生成的文字,message 字段则是文字对应的 AIMessage 对象,例如:

LLMResult(generations=[
        [
            ChatGeneration(
                text='Hello world.', 
                generation_info=None, 
                message=AIMessage(
                    content='Hello world.', 
                    additional_kwargs={}, 
                    example=False)
            )
        ]
    ], 
    llm_output={
        'token_usage': {
            'completion_tokens': 6, 'prompt_tokens': 80, 'total_tokens': 86
        }, 
    'model_name': 'gpt-3.5-turbo'
    }, 
    run=RunInfo(run_id=UUID('fffa5a38-c738-4eef-bdc4-0071511d1422')))

Prompt 模板

很多时候我们并不会把用户的输入直接丢给模型,可能会需要在前后文进行补充信息,而这个补充的信息就是“模板”。下面是一个简单的例子,这个 prompt 包含一个输入变量 product,:

template = """
我希望你担任顾问,帮忙为公司想名字。
这个公司生产{product},有什么好名字?
"""

我们可以用 PromptTemplate 将这个带有输入变量的 prompt 包装成一个模板。

from langchain import PromptTemplate

prompt_template = PromptTemplate(
    input_variables=["product"],
    template=template,
)
prompt_template.format(product="运动衬衫")
# -> 我希望你担任顾问,帮忙为公司想名字。
# -> 这个公司生成运动衬衫,有什么好名字?

当然,如果 prompt 中没有输入变量,也可以将其用 PromptTemplate 包装,只是 input_variables 参数输入的是空列表。

如果你不想手动指定 input_variables ,也可以使用 from_template() 方法自动推导。

prompt_template = PromptTemplate.from_template(template)
prompt_template.input_variables
# -> ['product']

你还可以将模板保存到本地文件中,目前 LangChain 只支持 json 和 yaml 格式,它可以通过文件后缀自动判断文件格式。

prompt_template.save("awesome_prompt.json") # 保存为 json

也可以从文件中读取

from langchain.prompts import load_prompt
prompt_template = load_prompt("prompt.json")

Chain

Chain 是 LangChain 里非常重要的概念(毕竟都放在名字里了),它与管道(Pipeline)类似,就是将多个操作组装成一个函数(流水线),从而使得代码更加简洁方便。

比如我们执行一个完整的任务周期,需要先生成 prompt ,将 prompt 给 llm 生成文字,然后可能还要对生成的文字进行其他处理。更进一步,我们或许还要记录任务每个阶段的日志,或者更新其他数据。这些操作如果都写出来,那么代码会非常冗长,而且不容易复用,但是如果使用 Chain ,你就可以将这些工作都打包起来,并且代码逻辑也更清晰。你还可以将多个 Chain 组合成一个更复杂的 Chain 。

我们首先创建一个 llm 对象和一个 prompt 模板。

from langchain.llms import AzureOpenAI
from langchain import PromptTemplate

chat = AzureChatOpenAI(deployment_name="gpt-35-turbo", temperature=0)
prompt = PromptTemplate(
    input_variables=["input"],
    template="""
    将给定的字符串进行大小写转换。
    例如:
    输入: ABCdef
    输出: abcDEF
    
    输入: AbcDeF
    输出: aBCdEf
    
    输入: {input}
    输出: 
    """,
)

接下来我们可以通过 LLMChain 将 llm 和 prompt 组合成一个 Chain 。这个 Chain 可以接受用户输入,然后将输入填入 prompt 中,最后将 prompt 交给 llm 生成结果。另外如果你用的是聊天模型,那么使用的 Chain 是 ConversationChain

from langchain.chains import LLMChain

chain = LLMChain(llm=chat, prompt=prompt)
print(chain.run("HeLLo"))
# -> hEllO

如果 prompt 中有多个输入变量,可以使用字典一次将它们传入。

print(chain.run({"input": "HeLLo"}))
# -> hEllO

Debug 模式

上面都是些简单的例子,只牵扯到少量的输入变量,但在实际使用中可能会有大量的输入变量,并且 llm 的输出还是不固定的,这就使得我们很难从最重的结果反推问题所在。为了解决这个问题,LangChain 提供了 verbose 模式,它会将每个阶段的输入输出都打印出来,这样就可以很方便地找到问题所在。

chain_verbose = LLMChain(llm=llm, prompt=prompt, verbose=True)
print(chain_verbose.run({"input": "HeLLo"}))
> Entering new  chain...
Prompt after formatting:

    将给定的字符串进行大小写转换。
    例如:
    输入: ABCdef
    输出: abcDEF
    
    输入: AbcDeF
    输出: aBCdEf
    
    输入: HeLLo
    输出: 
    

> Finished chain.
hEllO

组合 Chain

一个 Chain 对象只能完成一个很简单的任务,但我们可以像搭积木一样将多个简单的动作组合在一起,就可以完成更复杂的任务。其中最简单的是顺序链 SequentialChain,它将多个 Chain 串联起来,前一个 Chain 的输出将作为后一个 Chain 的输入。不过要注意的是,因为这只是个最简单的链,所以它不会对输入进行任何处理,也不会对输出进行任何处理,所以你需要保证每个 Chain 的输入输出都是兼容的,并且它要求每个 Chain 的 prompt 都只有一个输入变量。

下面,我们先计算输入数字的平方,然后将平方数转换成罗马数字。

from langchain.chains import SimpleSequentialChain

chat = AzureChatOpenAI(deployment_name="gpt-35-turbo", temperature=0)
prompt1 = PromptTemplate(
    input_variables=["base"], template="{base}的平方是: "
)
chain1 = LLMChain(llm=chat, prompt=prompt1)

prompt2 = PromptTemplate(input_variables=["input"], template="将{input}写成罗马数字是:")
chain2 = LLMChain(llm=chat, prompt=prompt2)

overall_chain = SimpleSequentialChain(chains=[chain1, chain2], verbose=True)
overall_chain.run(3)
> Entering new  chain...


9
IX

> Finished chain.
'IX'

LangChain 已经预先准备了许多不同 Chain 的组合,具体可以参考官方文档,这里就先不展开了。

你可能感兴趣的:(用,LangChain,构建基于资料库的问答机器人,langchain,机器人,人工智能)