Bootstrap

Langchain实战:基于Chain实现Prompt的高级应用

一. Langchain介绍

LangChain 是一个用于开发由语言模型驱动的应用程序的框架。它使得应用程序能够:

  • 具有上下文感知能力:将语言模型连接到上下文来源(提示指令,少量的示例,需要回应的内容等)。
  • 具有推理能力:依赖语言模型进行推理(根据提供的上下文如何回答,采取什么行动等)。

LangChain 包的主要价值主张是:

  • 组件:用于处理语言模型的可组合工具和集成。无论你是否使用 LangChain 框架的其余部分,组件都是模块化的,易于使用
  • 现成的链:用于完成高级任务的组件的内置组合

现成的链使得开始变得容易。组件使得定制现有链和构建新链变得容易。

LangChain的安装与入门请参考:快速入门指南

二. 项目背景

假设有一串长文本,我们希望利用大模型提取出文本中与指定类型的商品品牌、型号等相关的信息,并通过JSON格式将商品信息输出,如下所示:

文本内容:"我今天买了一台Huawei Mate 60, 请你帮我送到华中科技大学南大门, 手机是蓝色的, 512G"
输出:
```json
{
	"品牌": "Huawei",
	"品类": "手机",
	"属性": {
		"型号": "Mate 60",
		"颜色": "蓝色",
		"存储容量": "512G"
	},
	"商品名称": "Huawei Mate 60 蓝色 512G"
}

三. 代码实现

3.1 导入必要的库并调用GPT大模型

# 导入Langchain相关的库
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.prompts import (
    ChatPromptTemplate,
    PromptTemplate,
    SystemMessagePromptTemplate,
    AIMessagePromptTemplate,
    HumanMessagePromptTemplate
)
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
from langchain.chat_models import ChatOpenAI
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage
)

OPENAI_API_KEYOPENAI_API_BASE 是两个与 OpenAI API 交互时常用的环境变量,它们各自有不同的用途:

  • OPENAI_API_KEY
    用途:这是一个用于身份验证的密钥,允许你访问 OpenAI 的服务。当你通过 API 发送请求时,需要在请求头中包含这个 API 密钥,以便 OpenAI 能够验证请求者的身份。
    格式:通常是一个由数字和字母组成的字符串,长度固定。

  • OPENAI_API_BASE
    用途:这个环境变量用于指定 OpenAI API 的基础 URL。它决定了你的请求将被发送到哪个服务器。
    默认值:通常情况下,你不需要更改它,因为默认值已经指向了 OpenAI 的生产环境服务器。
    如何使用:如果你需要将请求发送到不同的服务器(如沙盒环境、自定义端点或其他地区特定的服务器),你可以设置这个环境变量。

import os
os.environ["OPENAI_API_KEY"] = "..."
os.environ["OPENAI_API_BASE"] = "..."
# 导入ChatModel
chat = ChatOpenAI(temperature=0)

3.2 输出解析器(指定输出格式)

在Langchain中封装了结构化输出的功能,通过Promt的输出解析器,可以直接将LLM的输出结果转化为指定格式:结构化输出解析器 structured

比如前面提到,我们希望输出为JSON格式,那么:

#定义输出格式
response_schemas = [
    ResponseSchema(name="品牌", description="商品的品牌"),
    ResponseSchema(name="品类", description="商品的品类"),
    ResponseSchema(name="属性", description="商品除品牌、品类外能够提炼的其他属性"),
    ResponseSchema(name="商品名称", description="根据提取的信息输出商品名称"),
]
 
# 初始化解析器
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)

format_instructions = output_parser.get_format_instructions()

format_instructions将作为Prompt的一部分输入大模型中

3.3 定义Prompt模板

LangChain 提供了不同类型的 MessagePromptTemplate。最常用的是 AIMessagePromptTemplateSystemMessagePromptTemplateHumanMessagePromptTemplate,分别用于创建 AI 消息、系统消息和人工消息:与聊天相关的提示模板

# 创建SystemMessagePromptTemplate
SystemPrompt = PromptTemplate(
    template="你是一个 {industry} 行业的专家,你对行业内各个品牌的名称和型号了如指掌。",
    input_variables=["industry"]
)
SystemMessagePrompt = SystemMessagePromptTemplate(prompt=SystemPrompt)
# 创建HumanMessagePromptTemplate
HumanPrompt = PromptTemplate(
    template="""
                用户问题:给你一段输入文本,请从里面提炼与{goods}相关的以下信息:
                品牌:商品的品牌
                品类:商品的品类
                属性:商品除品牌、品类外能够提炼的其他属性,以json形式给出
                商品名称:根据提取的信息输出商品描述
                
                <context>
                {ocr_result}
                </context>
                根据<context>里的信息回答用户问题
                输出格式: {format_instructions}

                让我们一步一步分析,给出你分析的过程,并注意以下要点:
                1.只提取和{goods}相关的信息,如果无法提炼返回空json.只输出一个可能性最大的商品信息,输出的json只包含一种商品;
                2:参考{industry}行业内的常见品牌,并将文本中识别错误的品牌信息,根据字体之间的相似性与已有品牌进行对应;
                3.一般来说商品的品牌会在商品描述的前面,并且他们距离不会太远,如果提取到多个品牌信息,则考虑品牌和商品描述之间的距离;
                4.你需要判断提取到的品牌是否属于{industry}行业,若提取到的品牌明显不属于{industry}行业,则忽略该品牌信息;
                5.保证输出json的合法性,输出你分析的过程.
            """,
    input_variables=["goods","ocr_result","format_instructions","industry"]
)
HumanMessagePrompt = HumanMessagePromptTemplate(prompt=HumanPrompt)

# 组合多个Prompt
chat_template = ChatPromptTemplate.from_messages([SystemMessagePrompt,HumanMessagePrompt])

在上面的Prompt中,我们需要外部导入四个参数,分别是:

  • goods:商品类别,比如手机,电脑等。
  • ocr_result:希望大模型提取信息的文本。
  • format_instructions:3.2中定义的输出解析器。
  • industry:该商品所处的行业,比如3C,家用电器等。

关于Prompt如何设计,请参考:Prompt之美:如何设计提示词让大模型变“聪明”

3.4 构造LLMChain并推理

链允许我们将多个组件组合在一起,创建一个单一的、一致的应用程序。例如,我们可以创建一个链,该链接接受用户输入,使用 PromptTemplate 对其进行格式化,然后将格式化后的响应传递给 LLM。我们可以通过将多个链组合在一起,或者通过将链与其他组件组合在一起,来构建更复杂的链:快速开始: 使用LLMChain

chain=LLMChain(llm=chat, prompt=chat_template)

接下来就可以推理了:

ocr = '我今天买了一台Huawei Mate 60, 请你帮我送到华中科技大学南大门, 手机是蓝色的, 512G'
res = chain.run(industry="电子产品",ocr_result=ocr,goods="手机",format_instructions=format_instructions)
print(res)
#输出:
1. 首先从文本中提取可能与手机相关的信息:
   - 品牌:Huawei
   - 商品名称:Mate 60
   - 属性:蓝色、512G

2. 根据电子产品行业内的常见品牌,确认Huawei属于电子产品行业的品牌,无需修正。

3. 根据文本中的描述,品牌信息在商品描述前面,且距离不远,因此可以确定Huawei是商品的品牌。

4. 综合以上信息,得出提取的手机相关信息如下:

```json
{
	"品牌": "Huawei",
	"品类": "手机",
	"属性": "蓝色, 512G",
	"商品名称": "Huawei Mate 60"
}

3.5 解析推理结果

在得到输出结果后,我们希望解析输出的字符串中的JSON信息,从而提取我们想要的品牌、品类相关信息:

output = output_parser.parse(res)
print(output)
#输出
{'品牌': 'Huawei', '品类': '手机', '属性': '蓝色, 512G', '商品名称': 'Huawei Mate 60'}

解析出来的信息为字典格式,然后就可以从中提取我们想要的信息。

3.6 异步调用

上文展示了如何推理一条文本信息,当我们有大量的文本信息时,采用串行执行的方式将会非常耗时,因此考虑能不能采用并行执行的方式提高推理速度。Langchain支持通过利用asyncio库为代理提供异步支持:
如何使用异步API进行代理
Langchain(五)进阶之异步调用

import time,sys
import asyncio #异步调用

#测试
async def async_function():
    print("Hello, async!")
    sys.stdout.flush()  # 刷新标准输出流

await async_function()

假设我们有如下的五条文本信息:

ocr_results = [
'我今天买了一台Huawei Mate 60, 请你帮我送到华中科技大学南大门, 手机是蓝色的, 512G',
'这款红米Note 13 Pro手机现在在京东有优惠活动,原价1399元的商品,通过领取满1000元减100元、满99元减30元的优惠券后,实付价格低至1262.01元。如果购买京东PLUS会员,还可以享受立减6.99元的优惠。这款手机采用了6.67英寸超细四窄边直屏,搭载了Pro+同款金刚骨骼架构和第二代1.5K高光护眼屏。',
'2023 年 10 月 31 日,苹果发布了全新 M3 系列芯片(M3、M3 Pro、M3 Max),首次采用 3nm 工艺,同时发布了搭载 M3 系列芯片的全新 MacBook Pro 14/16 英寸,以及 24 英寸的 iMac。',
'据外媒 FujiFrmors 报道,富士 X-T50 相机有望于 5 月 16 日发布,这款相机将引入机身五轴防抖(IBIS)功能,同时还将搭载 X-T5 同款 X-Trans V CMOS 传感器,内置 1 个 SD 卡插槽。',
'联想旗下新款ThinkBook 16+笔记本电脑现已上架,其中集成显卡版本售价为7699元,配备RTX 4060独立显卡的版本售价为9999元。'
]

首先不采用异步调用串联推理:

result = []
s = time.perf_counter()
for ocr in ocr_results:
    res = chain1.run(industry="电子产品",ocr_result=ocr,goods="电子产品",format_instructions=format_instructions)
    output = output_parser.parse(res)
    result.append(output['商品名称'])
print(result)
elapsed = time.perf_counter() - s
print("\033[1m" + f"Serial executed in {elapsed:0.2f} seconds." + "\033[0m")

输出:

['Huawei Mate 60', 
'红米Note 13 Pro手机', 
'苹果 MacBook Pro 16 英寸(搭载 M3 系列芯片)', 
'富士 X-T50 相机', 
'联想ThinkBook 16+ 笔记本电脑']
Serial executed in 18.15 seconds.

采用异步调用并联推理:

async def async_generate(ocr_result):
    res = await chain.arun(industry='电子产品',ocr_result=ocr_result,goods='电子产品',format_instructions=format_instructions)
    output = output_parser.parse(res)
    return output['商品名称']

async def generate_concurrently(data):
    tasks = [async_generate(ocr) for ocr in data]
    return await asyncio.gather(*tasks) ####异步调用

s = time.perf_counter()
result=await generate_concurrently(ocr_results)
print(result)
elapsed = time.perf_counter() - s
print("\033[1m" + f"Concurrent executed in {elapsed:0.2f} seconds." + "\033[0m")

输出:

['Huawei Mate 60', 
'红米Note 13 Pro手机', 
'Apple MacBook Pro with M3 Series Chip, 14/16-inch', 
'富士 X-T50 相机', 
'ThinkBook 16+笔记本电脑']
Concurrent executed in 7.21 seconds.

可以看到二者输出的商品描述几乎一致,但是推理时间从18.15s减少到了7.21s,速度快了一倍还要多。

四. 参考文献

LangChain 中文网,500页码中文文档教程
LangChain 中文文档 v0.1.7

;