生成AIで無駄な事務処理を削減したい【Gemini + LangChain】

---

## 目次
1. [意外と無駄な事務作業ってまだある](#意外と無駄な事務作業ってまだある)
2. [実装してみた](#実装してみた)
    - [2-1. 環境構築](#環境構築)
    - [2-2. データ読み込みと準備](#データ読み込みと準備)
    - [2-3. ベクトル検索のセットアップ](#ベクトル検索のセットアップ)
    - [2-4. 質問応答チェーンの構築](#質問応答チェーンの構築)
    - [2-5. 実行](#実行)
3. [実行例](#実行例)
4. [終わりに](#終わりに)

---

## 意外と無駄な事務作業ってまだある
こんにちは。SIerで働いている新米エンジニアです。
みなさんはこの作業無駄だな…と感じる事務作業はあったりしませんか？
私は、入社以降、「え、この作業まだ人手でやってるの？」と感じる瞬間が多々ありました。。
特に、私がやっていた業務では「ある文書の中から値をコピーし、システムに転記する作業」を全て手動で実施する、ということがあり、どうにか自動化出来ないかな…と考えていました。
そこで、生成AIの勉強も兼ねて、Gemini + LangChainを利用して**文書中から必要な情報を質問形式で取得できる仕組み**を構築してみました。
今回は、検証用に適当に作成した以下の請求書から、合計金額を取得するRAGをpythonのlangchainで実装してみました。

![Qiita用.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3822223/459f6327-4d7d-afea-9bb6-6d43452d4214.png)
(検証用なので、かなり雑です。。)


---
## 実装してみた

以下が作成したコードになります。

### 環境構築
まず必要なライブラリをインストールします。

```bash
pip install -U langchain-openai
pip install langchain PyMuPDF openai faiss-cpu
pip install -U langchain-openai langchain-community
```

### データ読み込みと準備
API KEYはOpenAIとGemini用をそれぞれのプラットフォームで事前に作成します。
私はGoogle colabで動かしていたので、API KEYはcolabのシークレットキーとして設定して読み込んでいましたが、皆様のお手元の環境に合わせて動かしていただけたらと思います。
```python
import os
from google.colab import userdata
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# gemini APIキーの設定
google_api_key = userdata.get("GOOGLE_API_KEY")
os.environ["GOOGLE_API_KEY"] = google_api_key  # 環境変数に設定
# OpenAI APIキーの設定
openai_api_key = userdata.get("API_KEY")
os.environ["OPENAI_API_KEY"] = openai_api_key  # 環境変数に設定

# データ保存ディレクトリ
data_dir = "/content/data"

# データフォルダ内のドキュメントを読み込む
def load_documents_from_folder(folder_path):
    documents = []
    for file_name in os.listdir(folder_path):
        if file_name.endswith(".pdf"):
            loader = PyPDFLoader(os.path.join(folder_path, file_name))
            documents.extend(loader.load())
    return documents

documents = load_documents_from_folder("/content/data")

# ドキュメントをチャンクに分割
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
docs = text_splitter.split_documents(documents)
```

### ベクトル検索のセットアップ
RAGの検索方法は[ベクトル検索](https://cloud.google.com/vertex-ai/docs/vector-search/overview?hl=ja)を用いました。
ドキュメントの埋め込みにはOpen AIの埋め込み用モデルを用いています。
```python
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS

embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(docs, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
```

### 質問応答チェーンの構築
今回は請求書から各項目をユーザーの質問に答える形で抜き出して回答するシステムなので、プロンプトはかなりシンプルでも機能しました。
実際、より複雑なドキュメントになってくると、few shotなど[プロンプトエンジニアリング](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/prompt-design-strategies?hl=ja)を考えて工夫する必要があるかもしれません。。

```python
from langchain.chat_models import ChatGoogleGenerativeAI
from langchain.prompts import PromptTemplate
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory

# プロンプトテンプレートの定義
prompt_template = """
あなたは請求書から必要な情報を抜き出して教えてくれるRPAツールのバックエンド処理を担当するツールです。
以下の参考文献とこれまでの会話履歴をもとに、ユーザーの質問に対して正確かつ簡潔に答えてください。
回答の際には必ず参考文献からの情報を引用してください。

# 会話履歴
{chat_history}

# ユーザーの質問
{question}

# 参考文献
{context}

# 回答
"""

PROMPT = PromptTemplate(
    input_variables=["chat_history", "context", "question"],
    template=prompt_template
)

# 会話履歴を保持するメモリを作成
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

# LLMとチェーンの設定
llm = ChatGoogleGenerativeAI(model="gemini-pro")
qa_chain = ConversationalRetrievalChain.from_llm(
    llm=llm, retriever=retriever, memory=memory, combine_docs_chain_kwargs={"prompt": PROMPT}
)
```

### 実行
ユーザーの質問に基づいて回答を生成するようにします。
```python
print("質問を入力してください（終了するには 'exit' と入力）")
while True:
    query = input("ユーザー: ")
    if query.lower() == 'exit':
        print("プログラムを終了します。")
        break
    result = qa_chain({"question": query})
    print("アシスタント:", result["answer"])
```
## 実行例

次のように質問すると、値を返してくれました！
```
質問:
1月の請求金額は？
```
```
回答:
23,184円
```

## 終わりに
今回は、請求書から転記したい値を取り出す部分のみ作成しました。
この後は、取り出した値をシステムにペーストするだけ…というところですが、そこの方が厄介そうなのでまだまだ先は長そうです…
(また、回答の安定性がどうか、などまだまだ課題はありそうです…)
実際に実装してみて、自動化も意外と難しいんだな、と感じました。。
また何か進展があったらブログを書きたいと思います！