LangChain的函数,工具和代理(二):LangChain的表达式语言(LCEL)

LangChain Expression Language (LCEL) 是 LangChain 工具包的重要补充,旨在提高文本处理任务的效率和灵活性。LCEL 允许用户采用声明式方法来组合链,便于进行流处理、批处理和异步任务。其模块化架构还允许轻松定制和修改链组件。LCEL 的优势之一是它使用户更容易个性化链的不同部分。链的声明性和模块化特性允许轻松地交换组件。此外,现在的提示更加明显,可以轻松地修改以适应特定的用例。在 LangChain 中,提示只是默认值,但是可以为生产应用程序进行更改。

一,简单链(Simple Chain)

要实现一个链(chain),首先让我们做一些初始化的工作,如设置opai的api_key,这里我们需要说明一下,在我们项目的文件夹里会存放一个 .env的配置文件,我们将api_key放置在该文件中,我们在程序中会使用dotenv包来读取api_key,这样可以避免将api_key直接暴露在程序中:

#pip install -U python-dotenvimport os
import openaifrom dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file
openai.api_key = os.environ['OPENAI_API_KEY']

接下来我们需要导入langchain相关的包,并且实现一个简单chain,然后我们查看一下chain的内容:

from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.schema.output_parser import StrOutputParser#通过prompt模板创建一个prompt
prompt = ChatPromptTemplate.from_template("请为我写一首关于 {topic}的诗。"
)
#定义opai的语言模型,默认使用gpt-3.5-turbo模型
model = ChatOpenAI()
output_parser = StrOutputParser()#创建一个简单链
chain = prompt | model | output_parserchain

这里我们看到chain中包含了3部分信息,其中first部分是关于prompt模板的信息,middel部分是关于语言模型的信息,这里我们使用的是openai的gpt-3.5-turbo模型,last部分是关于输出解析器的信息。简单的链接就包含这些内容,下面我们使用invoke方法来调用这个chain:

response = chain.invoke({"topic": "大海"})
print(response)

二、更复杂的链(More complex chain)

接下来我们将用户的问题和向量数据库检索结果结合起来,使用RunnableMap来组合一个更复杂的chain,不过我们首先需要定义一个基于内存的简单向量数据库,并在该向量数据库中存放两个简单的文档:

from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import DocArrayInMemorySearch
from langchain.schema.runnable import RunnableMap#创建向量数据库
vectorstore = DocArrayInMemorySearch.from_texts(["人是由恐龙进化而来","熊猫喜欢吃天鹅肉"],embedding=OpenAIEmbeddings()
)
retriever = vectorstore.as_retriever()#创建检索器
retriever = vectorstore.as_retriever()

在上面的向量数据库中我们加入了两段文本:“人是由恐龙进化而来” 和 “熊猫喜欢吃天鹅肉”,加入这两句违反常识的文本是为了后面确认llm只从向量库中获取相关答案。

下面我们让检索器来检索相关文档:

retriever.get_relevant_documents("人从哪里来?")

 检索器会返回和问题相关的问题,并且会根据文档的与问题的相似度对相关文档进行排序。

retriever.get_relevant_documents("熊猫喜欢吃什么?")

 下面我们来创建一个比之前的simple chain更复杂的chain, 在这个chain中我们会加入RunnableMap,它用来提供和用户问题相关的文档集,最后我们查看一下chain中的内容:

template = """Answer the question based only on the following context:
{context}Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)inputs = RunnableMap({"context": lambda x: retriever.get_relevant_documents(x["question"]),"question": lambda x: x["question"]
})chain = inputs | prompt | model | output_parserchain

 这里我们看到chain中的first部分为RunnableMap的相关信息,在middle中包含了两部分信息(prompt模板和llm模型),last部分仍然为输出解析器,下面我们还是使用invoke方法来调用该chain:

chain.invoke({"question": "人从哪里来?"})

chain.invoke({"question": "熊猫喜欢吃什么?"})

前面我们在prompt中设置了两个变量:“context”和 "question", 由于我们创建这个chain的时候将RunnableMap放置在prompt 前面,所以RunnableMap需要为prompt中的“context”和 "question"这两个变量提供数据,我们来看看RunnableMap所提供的数据:

inputs.invoke({"question": "人从哪里来?"})

 

这里我们看到 RunnableMap为context变量提供了和问题相关的两篇文档,为question变量提供了用户的问题,最终LLM会从相关问题中总结出最优的答案来返回给用户。

三、绑定(Bind)

在之前的OpenAI的函数调用这篇博客中我们讨论了openai的函数调用功能,openai的llm会根据用户提供的外部函数功能的描述信息,来判断在回答用户的问题时是否需要调用该外部函数,如何需要调用外部函数,则返回调用函数所需的参数(该参数从用户问题中提炼出来的)。下面我们用langchain来实现openai的函数调用功能,不过首先我们需要创建一个函数描述信息,这里我们仍然使用上一篇博客中的关于城市天气的查询函数get_current_weather:

functions = [{"name": "get_current_weather","description": "Get the current weather in a given location","parameters": {"type": "object","properties": {"location": {"type": "string","description": "The city and state, e.g. San Francisco, CA",},"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},},"required": ["location"],},}
]

 接下来我们使用.bind方法将外部函数描述信息绑定到llm上:

#根据模板创建prompt
prompt = ChatPromptTemplate.from_messages([("human", "{input}")]
)
#创建openai模型并绑定函数描述信息
model = ChatOpenAI(temperature=0).bind(functions=functions)runnable = prompt | modelrunnable

这里我们看到runnable的first部分为prompt模板,middel部分为空,last部分包含了openai的模型信息以及函数描述信息kwargs。下面我们来实现通过提交用户问题并返回函数参数的功能:

response = runnable.invoke({"input": "上海的天气怎么样?"})
response

 这里我们看到了返回信息中包含了function_call和arguments,我们可以从arguments中提取所需的参数:

arguments = response.additional_kwargs['function_call']['arguments']
print(arguments)

 下面我们将在functions变量中同时定义两个函数的描述信息,然后让llm来判断当用户提问时应该调用哪个函数,这两个函数中一个是之前的查询天气的函数:get_current_weather,另一个是商品信息查询函数:product_search。查询天气的函数get_current_weather接受的参数为location即所在城市的名称,商品信息查询函数product_search接受的参数为product_name即商品名称。

functions = [{"name": "get_current_weather","description": "Get the current weather in a given location","parameters": {"type": "object","properties": {"location": {"type": "string","description": "The city and state, e.g. San Francisco, CA",},"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},},"required": ["location"],},},{"name": "product_search","description": "Search for product information","parameters": {"type": "object","properties": {"product_name": {"type": "string","description": "The product to search for"},},"required": ["product_name"]}}]

下面我们来向llm询问有关手机的问题:

model = model.bind(functions=functions)runnable = prompt | modelresponse = runnable.invoke({"input": "请问iphone14 pro 现在卖什么价格?"})
response

从上面的返回信息中我们可以看到当我们询问iphone14 pro 的价格时,llm知道此时需要调用外部函数了,并且返回了调用外部函数的名称为product_search及参数product_name为“iphone14 pro”,这样我们的应用程序就可以直接用product_name变量去调用外部的product_search函数了。同样我们也可以询问天气情况:

response = runnable.invoke({"input": "上海的天气怎么样?"})
arguments = response.additional_kwargs['function_call']['arguments']
print(arguments)

四、后备措施(Fallbacks)

由于早期的openai的模型如"text-davinci-001",这些模型不支持格式化的输出结果即它们都是以字符串的形式输出结果,这会给我们的应用程序解析LLM的输出结果带来很大的麻烦,下面我们使用openai早期模型"text-davinci-001"来回答用户的问题,我们希望llm能以json格式输出结果:

#导入langchain早期版本openai的llm包
from langchain.llms import OpenAI
from langchain.schema.output_parser import StrOutputParser
import json#创建输出解析器
output_parser = StrOutputParser()#创建llm
simple_model = OpenAI(temperature=0, max_tokens=1000, model="text-davinci-001"
)
#创建一个简单chain
simple_chain = simple_model |output_parser | json.loads

这里我们定义了一个早期版本的openai的llm:text-davinci-001 和一个simple_chain, 我们希望simple_chainn能以json格式输出结果,所以我们在创建simple_chain时在最后面组合了json.loads以便让simple_chain按json格式输出最终的结果,接下来我们先测试一下这个早期版本的simple_model,我们让simple_model写三首诗,并以josn格式输出,每首诗必须包含:标题,作者和诗的第一句:

#让llm写三首诗,并以josn格式输出,每首诗必须包含:标题,作者和诗的第一句。
challenge = "write three poems in a json blob, where each poem is a json blob of a title, author, and first line"response = simple_model.invoke(challenge)
response

 这里我们看到早期版本的openai的llm无法输出指定格式的内容,它只能输出字符串内容,虽然这里的每首诗都是用"[ ]"隔开,但是它们仍然都在一个大的字符串内,这对于我们解析输出结果非常的不方便,下面我们使用simple_chain 来测试输出结果:

simple_chain.invoke(challenge)

这里我们看到即便我们在创建 simple_chain的时候加了格式化输出项(json.loads),但它仍然无法实现格式化的输出,其中主要的原因还是由于早期版本的openai模型不支持格式的输出,所以即便我们在使用langchain创建chain时加上了格式化输出项(json.loads)也无济于事。下面我们使用较新的gpt-3.5-turbo模型:

model = ChatOpenAI(temperature=0)
chain = model | output_parser | json.loadschain.invoke(challenge)

这里我们看到当我们使用了gpt-3.5-turbo模型(ChatOpenAI默认使用gpt-3.5-turbo模型)时输出结果完全符合我们的要求即全部以json格式输出。如果你的项目用的时早期的“text-davinci-001”模型,在不大幅度修改源代码的情况下是否有办法让早期模型也能做到格式化输出呢?下面我们使用fallbacks的方式来让openai的早期模型也能做到格式化的输出:

final_chain = simple_chain.with_fallbacks([chain])
final_chain.invoke(challenge)

这里我们使用了fallbacks方法将早期模型的chain"嫁接"到新模型的chain上,这样就让早期模型的chain也同样可以做到格式化的输出了。

五、接口(Interface)

接口定义了自定义链,并启用了标准化调用。公开的标准接口包括:

- stream:将输出内容已流式返回
- invoke:在输入上调用chain
- batch:在输入列表上调用chain

这些也有相应的异步方法:
- astream:异步将输出内容已流式返回
- ainvoke:在输入上异步调用chain
- abatch:在输入列表上异步调用chain。

 下面我们来测试以invoke,batch,stream的方式来调用chain,看看会得到怎样的结果:

prompt = ChatPromptTemplate.from_template("请给我讲一个关于 {topic}的笑话"
)
model = ChatOpenAI()
output_parser = StrOutputParser()chain = prompt | model | output_parserchain.invoke({"topic": "老鼠"})

 上面我们使用了常规的invoke调用,并且顺利得到了想要的结果,下面使用batch方法来调用chain即给chain提供一个输入列表,在列表中可以有多个问题,也就是说我们向让llm同时回答多个问题:

chain.batch([{"topic": "猫"}, {"topic": "狗"}])

这里我们看到当我们使用batch调用时,llm可以同时回答了多个问题 ,下面我们使用stream调用即让llm以流的形式返回结果:

for t in chain.stream({"topic": "bears"}):print(t)

 这里我们看到当我们使用stream调用时结果将会逐字输出,这可以应用在某些应用场合中,如在web页面中和chatbot交流时,在页面上逐字输出chatbot的回复内容这样会有一个比较好的用户体验,接下来我们使用ainvoke来实现异步调用:

response = await chain.ainvoke({"topic": "青蛙"})
response

 六、总结

今天我们介绍了LangChain的表达式语言(LCEL)的基础知识,其中包括了简单链(Simple Chain),复杂链(More complex chain),绑定(Bind),后备措施(Fallbacks),接口(Interface)的应用,其中包括对invoke,batch,stream等方法的使用以及相应的异步方法的介绍,希望今天的内容对大家有所帮助。

七、参考资料

DLAI - Learning Platform Beta 

LCEL: A Guide to LangChain Expression Language - Comet

Introduction | 🦜️🔗 Langchain

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/187582.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

知识图谱最简单的demo实现——基于pyvis

1、前言 我们在上篇文章中介绍了知识图谱的简单实现,最后使用neo4j进行了展示,对于有些情况我们可能并不想为了查看知识图的结果再去安装一个软件去实现,那么我们能不能直接将三元组画出来呢/ 接下来我们就介绍一个可视化的工具pyvis&#…

STM32/GD32_分散加载

Q:如何将一个变量、某个源文件的函数在编译阶段就存储在用户指定的区域? KEIL环境:.map后缀文件、.sct后缀文件 IAR环境:.map后缀文件、.icf后缀文件 【map文件】 对固件里面的变量、函数、常量等元素的存储空间进行分配的说明…

ffmpeg开发 环境配置

ffmpeg开发简图 1 下载ffmpeg开发包 https://ffmpeg.org/download.html 包含三个版本:Static、Shared以及Dev Static --- 包含3个应用程序:ffmpeg.exe , ffplay.exe , ffprobe.exe,体积都很大,相关的DLL已经被编译到exe里面去…

VS2022使用Vim按键

VS2022使用Vim按键 在插件管理里面搜索VsVim 点击安装,重启VS 工具->选项->VsVim 配置按键由谁处理,建议Ctrl C之类常用的使用VS处理,其它使用Vim处理

golang WaitGroup的使用与底层实现

使用的go版本为 go1.21.2 首先我们写一个简单的WaitGroup的使用代码 package mainimport ("fmt""sync" )func main() {var wg sync.WaitGroupwg.Add(1)go func() {defer wg.Done()fmt.Println("xiaochuan")}()wg.Wait() }WaitGroup的基本使用场…

力扣202题 快乐数 双指针算法

快乐数 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」 定义为: 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果这个过程 结果为 1&#…

在ubuntu虚拟机上安装不同版本的交叉编译工具链

在之前的章节中,学习了如何安装了4.8.3的交叉编译工具链: 交叉编译 和 软硬链接 的初识(面试重点)-CSDN博客 但是,在之后学习内核编译时,由于我的树莓派内核版本较高,为6.1,所以在…

【android开发-01】android中toast的用法介绍

1,android中toast的作用 在Android开发中,Toast是一种用于向用户显示简短消息的轻量级对话框。它通常用于向用户提供一些即时的反馈信息,例如操作结果、提示或警告。 Toast的主要作用如下: 提供反馈:Toast可以在用户…

chrome vue devTools安装

安装好后如下图所示: 一:下载vue devTools 下载链接https://download.csdn.net/download/weixin_44659458/13192207?spm1001.2101.3001.6661.1&utm_mediumdistribute.pc_relevant_t0.none-task-download-2%7Edefault%7ECTRLIST%7EPaid-1-13192207…

知乎禁止转载的回答怎么复制做笔记?

问题 对于“禁止转载”的回答,右键复制是不行的,ctrl-c也不行,粘贴之后都是当前回答的标题。稍微看了代码,应该是对copy事件进行了处理。不过这样真的有用吗,真是防君子不防小人,只是给收集资料增加了许多…

sso单点登录

一:业务需求 客户要求在门户网站上实现一次登录能访问所以信任的系统 二: 处理方式 实现sso单点登录需要前后端配合处理 1. 通过网页授权登录获取当前用户的openid,userid 2.设置单点登录过滤器并进行参数配置 3.另外写一个登录接口&…

Git分支批量清理利器:自定义命令行插件实战

说在前面 不知道大家平时工作的时候会不会需要经常新建git分支来开发新需求呢?在我这边工作的时候,需求都是以issue的形式来进行开发,每个issue新建一个关联的分支来进行开发,这样可以通过issue看到一个需求完整的开发记录&#x…

菜鸟学习日记(Python)——基本数据类型

Python 中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。 在 Python 中,变量就是变量,它没有类型,我们所说的"类型"是变量所指的内存中对象的类型。 等号()用来…

深入学习redis-基于Jedis通过客户端操作Redis

目录 redis客户端(JAVA) 配置 引入依赖 建立连接 常用命令实现 get/set exists/del keys expire和ttl type 字符串(String) mget和mset getrange和setrange append incr和decr 列表(list) …

运行启动vue项目报报错node: --openssl-legacy-provider is not allowed in NODE_OPTIONS解决

报错的问题就是package.json中的Scripts下的dev 解决方法就是要不升级你的应用代码,支持 新版本的node.js 要不就是删除SET NODE_OPTIONS--openssl-legacy-provider &&代码,如下代码即可正常运行起来

windows判断端口是否在使用 bat脚本

脚本 REM 查询端口是否占用 netstat -ano|findstr 3306 >nul &&echo y >1.log ||echo n >1.log REM 读取文本内容赋值给变量 set /P resu<1.log if %resu% y (echo port in use ) else (echo port not in use ) mysql服务不运行的时候检测效果 mysql服…

鸿蒙原生应用/元服务开发-开发者如何进行真机测试

前提条件&#xff1a;已经完成鸿蒙原生应用/元服务开发&#xff0c;已经能相对熟练使用DevEco Studio,开发者自己有鸿蒙4.0及以上的真机设备。 真机测试具体流程如下 1.手机打开开发者模式 2.在项目中&#xff0c;左上角 文件(F)->项目结构 进行账号连接 3.运行

Flash学习

FLASH介绍 FLASH是常用的&#xff0c;用于存储数据的半导体器件&#xff0c;它具有容量大&#xff0c;可重复擦写&#xff0c;按“扇区/块”擦除、掉电后数据可继续保存的特性。 常见的FLASH有NOR FLASH和NAND FLASH。 NOR和NAND是两种数字门电路&#xff0c;可以简单地认为F…

【负载均衡 SLB介绍及其算法详解】(一万两千字)

目录 一、负载均衡 SLB 定义 二、负载均衡SLB的作用 三、负载均衡器&#xff08;Load Balancer&#xff09; 【1】工作原理 【2】主要功能 【3】关键概念 四、工作负载&#xff08;Workload&#xff09; 五、负载均衡算法 【1】轮询&#xff08;Round Robin&#xff0…

python 中文件相对路径 和绝对路径

什么是绝对路径 绝对路径&#xff1a;就是从盘符(c盘、d盘)开始一直到文件所在的具体位置。 例如&#xff1a;xxx.txt 文件的绝对路径为&#xff1a; “C:\Users\xiaoyuzhou\Desktop\file\xxx.txt”相对路径 “相对路径”就是针对“当前文件夹”这一参考对象&#xff0c;来描述…