LangChain Expression Language（LCEL）はじめの一歩 RAG編 on Azure OpenAI

## はじめに
みなさんは、**LangChain Expression Language(以下、LCEL)** をご存じでしょうか？ 先日、Azure OpenAI Service上で、LCELによるコード（Python）を動かしたときのことを次の記事に書かせて頂きました。


https://qiita.com/hiromitsu_iwasaki/items/7d471ba05b0ecc582489



今回、LangChainドキュメントに書かれている**LCELによる基本的なRAG**のコード（Python）2つをAzure OpenAI Service上で動かしました。


LCELでは、チェーンを **"| "** で表し、次のように表現します。とてもシンプルです。
``` Python
chain = setup_and_retrieval | prompt | model | output_parser
``` 

## LCELとは
LangChain Expression Language（LCEL）は、LLMアプリケーション開発を簡素化するためのLangChainフレームワークの一部です。簡単なチェーンから少し複雑なアプリケーションのプロトタイピングまで、開発者はよりシンプルにチェーンを組むことができます。

## 環境
Windows10
Python v3.11.4

APIキー等の環境変数は、試したコードと同じフォルダに".env"ファイルを作り、その中に記述しています。

``` Python
AZURE_OPENAI_TYPE = "azure"
AZURE_OPENAI_KEY = "YOUR AZURE OPENAI KEY" 
AZURE_OPENAI_VERSION = "2023-05-15"
azure_endpoint = "YOUR AZURE ENDPOINT URL"
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME = "YOUR AZURE EMBEDDING DEPLOYMENT NAME"
AZURE_OPENAI_DEPLOYMENT_NAME = "YOUR AZURE DEPLOYMENT NAME"
```
※ AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAMEは、エンベディング用モデルのデプロイ名
※ AZURE_OPENAI_DEPLOYMENT_NAMは、チャット用モデルのデプロイ名

## LCELを使ったRAG検索例　その1

``` Python
# LCELを使ったRAG検索例　その1
# main1.py

import os

from dotenv import load_dotenv
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai import AzureOpenAIEmbeddings
from langchain_openai import AzureChatOpenAI

# 環境変数読み込み
load_dotenv("./.env")

# ベクターストアの設定
vectorstore = DocArrayInMemorySearch.from_texts(
    ["私の会社は、東京都の赤坂にあります。", "私の会社の最寄り駅は、溜池山王駅です。"],
    embedding=AzureOpenAIEmbeddings(
        openai_api_type="azure",
        openai_api_version=os.getenv("AZURE_OPENAI_VERSION"),
        azure_endpoint=os.getenv("azure_endpoint"),
        openai_api_key=os.getenv("AZURE_OPENAI_KEY"),
        model="text-embedding-ada-002",
        azure_deployment=os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"),
        embedding_ctx_length=8191,
        chunk_size=1000,
        max_retries=2,
    ),
)

# 検索機能の設定
retriever = vectorstore.as_retriever()

template = """次の文脈（context）のみに基づいて質問（question）に答えてください。:
{context}

質問: {question}
"""

# プロンプトテンプレートの設定
prompt = ChatPromptTemplate.from_template(template)

# モデルの設定
model = AzureChatOpenAI(
    api_version="2023-05-15",
    azure_endpoint=os.getenv("azure_endpoint"),
    api_key=os.getenv("AZURE_OPENAI_KEY"),
    azure_deployment=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"),
    temperature=0,
)

# 出力パーサの設定
output_parser = StrOutputParser()

# 情報の取得と質問の処理
setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)

# LCELによるチェーンの作成と結果の取得
chain = setup_and_retrieval | prompt | model | output_parser

result = chain.invoke("私の会社はどこにありますか？")
print(result)

``` 
上記フローは次の通りです。

①環境変数の読み込み
➁ベクターストアの設定
　DocArrayInMemorySearch.from_textsを使って、指定のテキストからベクターストアをメモリ上に作成しています。
③検索機能の設定
　vectorstore.as_retriever()で、ベクターストアから情報を取得するための検索機能（retriever）を作成しています。
④プロンプトテンプレートの設定
　モデルが質問に答えるために使用するテンプレートを設定しています。
　このテンプレートには、質問と回答のための文脈の両方が含まれます。
⑤モデルの設定
　Azure OpenAIのチャットモデルを設定。このモデルは、質問に対する回答を生成します。
⑥出力パーサの設定
　モデルからの出力を解析し、読みやすい形式にしてくれます。
⑦情報の取得と質問の処理
　RunnableParallelを使って、回答情報の取得と質問の処理を同時に行います。質問はそのまま渡され、回答情報は検索機能を通じて取得されます。
⑧LCELによるチェーンの作成と結果の取得
　ステップを連結し（ "|" でつなげ）、質問に対する回答を生成するプロセス。

最後に、chain.invoke( "私の会社はどこにありますか？" )として質問をモデルに投げ、生成された回答をprint関数で表示しています。以上のプロセスを通じて、コードは与えられた質問に対して最も適切な回答を生成しようとします。

``` powershell
# モデルからの回答
あなたの会社は東京都の赤坂にあります。
``` 



## LCELを使ったRAG検索例　その2
以下の例は、基本的には「RAG検索例 その1」にほぼ同じですが、次の点が異なっています。

・ベクターストアの設定にDocArrayInMemorySearch.from_textsではなくFAISS（Facebook、現META社が開発した近似最近傍探索ライブラリ）を利用。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537852/b0aefd04-0ddf-382c-0ac2-e4db91273206.png)


・RunnableParallelモジュールを使わず、chain時に直接、{"context": retriever, "question": RunnablePassthrough()}として記述。chainを縦につなげて記述。
（横につなげて書いても同じ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537852/af400d52-6a13-1439-5f1a-7dcbf957fb2f.png)


``` Python
# LCELを使ったRAG検索例　その2
# main2.py

import os

from dotenv import load_dotenv
from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import AzureOpenAIEmbeddings
from langchain_openai import AzureChatOpenAI

# 環境変数読み込み
load_dotenv("./.env")

# ベクターストアの設定
vectorstore = FAISS.from_texts(
    ["私の会社は、東京都の赤坂にあります。", "私の会社の最寄り駅は、溜池山王駅です。"],
    embedding=AzureOpenAIEmbeddings(
        openai_api_type="azure",
        openai_api_version=os.getenv("AZURE_OPENAI_VERSION"),
        azure_endpoint=os.getenv("azure_endpoint"),
        openai_api_key=os.getenv("AZURE_OPENAI_KEY"),
        model="text-embedding-ada-002",
        azure_deployment=os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"),
        embedding_ctx_length=8191,
        chunk_size=1000,
        max_retries=2,
    ),
)

# 検索機能の設定
retriever = vectorstore.as_retriever()

template = """次の文脈（context）のみに基づいて質問（question）に答えてください。:
{context}

質問: {question}
"""

# プロンプトテンプレートの設定
prompt = ChatPromptTemplate.from_template(template)

# モデルの設定
model = AzureChatOpenAI(
    api_version="2023-05-15",
    azure_endpoint=os.getenv("azure_endpoint"),
    api_key=os.getenv("AZURE_OPENAI_KEY"),
    azure_deployment=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"),
    temperature=0,
)

# 出力パーサの設定
output_parser = StrOutputParser()

# LCELによるチェーンの作成と結果の取得
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | output_parser
)

result = chain.invoke("私の会社の最寄り駅は何駅ですか？")
print(result)

``` 

``` powershell
# モデルからの回答
最寄り駅は溜池山王駅です。
``` 


## requirements.txt
requirements.txtの内容は次の通りです。

``` python
aiohttp==3.9.1
aiosignal==1.3.1
altair==5.2.0
anyio==4.2.0
asgiref==3.7.2
astroid==3.0.3
attrs==23.1.0
backoff==2.2.1
bandit==1.7.7
bcrypt==4.1.2
beautifulsoup4==4.12.3
black==23.11.0
blinker==1.7.0
build==1.0.3
cachetools==5.3.2
certifi==2024.2.2
charset-normalizer==3.3.2
chroma-hnswlib==0.7.3
chromadb==0.4.22
click==8.1.7
colorama==0.4.6
coloredlogs==15.0.1
dataclasses-json==0.5.14
Deprecated==1.2.14
dill==0.3.8
distro==1.9.0
docarray==0.40.0
faiss-cpu==1.7.4
fastapi==0.109.2
filelock==3.13.1
flake8==7.0.0
flatbuffers==23.5.26
frozenlist==1.4.0
fsspec==2024.2.0
gitdb==4.0.11
GitPython==3.1.40
google-auth==2.28.0
googleapis-common-protos==1.62.0
greenlet==3.0.1
grpcio==1.60.1
h11==0.14.0
httpcore==1.0.3
httptools==0.6.1
httpx==0.26.0
huggingface-hub==0.20.3
humanfriendly==10.0
idna==3.6
importlib-metadata==6.11.0
importlib-resources==6.1.1
isort==5.13.2
Jinja2==3.1.2
joblib==1.3.2
jsonpatch==1.33
jsonpointer==2.4
jsonschema==4.20.0
jsonschema-specifications==2023.11.2
kubernetes==29.0.0
langchain==0.1.7
langchain-community==0.0.20
langchain-core==0.1.23
langchain-openai==0.0.6
langchainplus-sdk==0.0.20
langsmith==0.0.87
markdown-it-py==3.0.0
MarkupSafe==2.1.3
marshmallow==3.20.1
mccabe==0.7.0
mdurl==0.1.2
mmh3==4.1.0
monotonic==1.6
mpmath==1.3.0
multidict==6.0.4
mypy==1.8.0
mypy-extensions==1.0.0
nltk==3.8.1
numexpr==2.8.7
numpy==1.26.0
oauthlib==3.2.2
onnxruntime==1.17.0
openai==1.12.0
openapi-schema-pydantic==1.2.4
opentelemetry-api==1.22.0
opentelemetry-exporter-otlp-proto-common==1.22.0
opentelemetry-exporter-otlp-proto-grpc==1.22.0
opentelemetry-instrumentation==0.43b0
opentelemetry-instrumentation-asgi==0.43b0
opentelemetry-instrumentation-fastapi==0.43b0
opentelemetry-proto==1.22.0
opentelemetry-sdk==1.22.0
opentelemetry-semantic-conventions==0.43b0
opentelemetry-util-http==0.43b0
orjson==3.9.14
overrides==7.7.0
packaging==23.2
pandas==2.1.1
pathspec==0.12.1
pbr==6.0.0
Pillow==10.1.0
platformdirs==4.1.0
posthog==3.4.1
protobuf==4.25.1
pulsar-client==3.4.0
pyarrow==14.0.1
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycodestyle==2.11.1
pydantic==1.10.9
pydeck==0.8.1b0
pyflakes==3.2.0
Pygments==2.17.2
pylint==3.0.3
Pyment==0.3.3
PyPika==0.48.9
pyproject_hooks==1.0.0
pyreadline3==3.4.1
python-dateutil==2.8.2
python-dotenv==1.0.0
pytz==2023.3.post1
PyYAML==6.0.1
referencing==0.31.1
regex==2023.10.3
requests==2.31.0
requests-oauthlib==1.3.1
rich==13.7.0
rpds-py==0.13.2
rsa==4.9
six==1.16.0
smmap==5.0.1
sniffio==1.3.0
soupsieve==2.5
SQLAlchemy==2.0.23
starlette==0.36.3
stevedore==5.1.0
streamlit==1.31.1
sympy==1.12
tenacity==8.2.3
tiktoken==0.5.2
tokenizers==0.15.2
toml==0.10.2
tomlkit==0.12.3
toolz==0.12.0
tornado==6.4
tqdm==4.66.1
typer==0.9.0
types-requests==2.31.0.20240125
typing-inspect==0.9.0
typing_extensions==4.8.0
tzdata==2023.3
tzlocal==5.2
urllib3==2.1.0
uvicorn==0.27.1
validators==0.22.0
watchdog==3.0.0
watchfiles==0.21.0
websocket-client==1.7.0
websockets==12.0
wrapt==1.16.0
yarl==1.9.3
zipp==3.17.0
```

## さいごに
**LangChain Expression Language（LCEL）** は、LangChainフレームワークにおいて、よりスマートにchainを書ける新機能となっています。今後、LangChainを使って開発を行う場合、一部は、このLCELによるアプローチが使われていくものと思います。**RAG**も世の中では、重要な役割を担ってきています。**Azure OpenAI Service**と組み合わせることでセキュアなRAGが構築できますので、私もLangChainを使うときのためにLCELの表現に慣れていきたいと思います。

:::note warn
この記事に載せたコードは、以下のlangChainドキュメントを元にしています。langChainドキュメントのコードは基本的にはOpenAI社のモデルを想定して書かれています。Azure OpenAI Service上で動かすために環境変数の設定等をAzure用変更しています。本来のコードの書き方とは異なる部分があるかもしれませんので、langChainドキュメントも参考にして頂ければと思います。
:::

https://python.langchain.com/docs/expression_language/get_started

※↑このページの"RAG Search Example"


https://python.langchain.com/docs/expression_language/cookbook/retrieval