LangChain と Pinecone で共通仕様書を参照した業務日報アドバイス

# はじめに
弊社のコアビジネスである土木事業では、各自治体が発行する『共通仕様書』と呼ばれる、工事に共通して適用される注意事項を記載したドキュメントに基づいて工事を行う必要があります。
土木業務に従事する社員が毎日提出する業務日報をもとに、この仕様書に関連する報告内容があればアドバイスしてくれる仕組みを検証してみました。

参照する共通仕様書は以下からダウンロードします。

https://www.pref.yamagata.jp/180030/kensei/nyuusatsujouhou/nyuusatsujouhou/2nd_chotatsu/nyuusatsujouhou/kn/d-siyousyo-h26.html

# 採用技術
はやりの RAG を採用してみます。具体的にはOpenAI API + LangChain + Pinecone です。
ベクトル DB については Chroma と迷いましたが、単純にローカルリソースを使いたくなく、クラウドサービスで使いやすいと感じた Pinecone としました。
ライブラリとしては llama-index を最初使いましたが、好みの問題で LangChain に変更しました。

# LangChain の準備
以下を参考にインストールします。

https://python.langchain.com/v0.2/docs/integrations/vectorstores/pinecone/

``` bash
pip install --upgrade langchain-pinecone langchain-openai langchain langchain-community
```

# Pinecone の準備
GCP 使ってますので以下の内容をもとにマーケットプレイスから追加します。

https://docs.pinecone.io/guides/organizations/manage-billing/setting-up-billing-through-gcp-marketplace

検索して
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351434/a24beea2-5d7e-579a-75d4-bdce48ca6a83.png)

登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351434/ae374467-4503-4d06-ded3-42040b90b79d.png)

プロジェクトを選択して Google アカウントでログインするとこんなメールが飛んできます。最後のリンクをクリックして Pinecone 管理コンソールにログインすると勝手に組織が出来ています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351434/f9fe29ca-75fe-fdb6-718c-43945442d8ff.png)

プラントしては Starter (無料) だとリクエストに制限があるという情報があったため Standard プラン (有料) に変更しました。

次にインデックスを作成します。
Dimention は OpenAI の Embedding (text-embedding-3-small を使用) に合わせて 1536 次元とします。

``` py
from pinecone import Pinecone, PodSpec

pc = Pinecone(api_key='YOUR_API_KEY')

pc.create_index(
    name = 'doboku',
    dimension = 1536,
    metric = 'cosine',
    spec = PodSpec(
        environment = 'asia-northeast1-gcp',
        pod_type = 's1.x1',
        pods = 1
    )
)
```

# Pinecone への登録
ダウンロードした共通仕様書の中から、業務に関連する以下の PDF を対象とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351434/38e7b914-0c87-3945-381c-1c8d283df89b.png)

作成した Pinecone の index にベクトルを登録します。
チャンクサイズ、オーバーラップ、セパレーターは思い付きでとりあえず試してみたという感じです。
特定の情報にフォーカスした方がいいだろうと思い、長すぎるのは違うだろうという勘です。
``` py
import os
import glob

from langchain_community.document_loaders import PyPDFLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter
from langchain_pinecone import PineconeVectorStore

os.environ['PINECONE_API_KEY'] = 'YOUR_API_KEY'
os.environ['OPENAI_API_KEY'] = 'YOUR_API_KEY'


def main():
    index_name = 'doboku'
    embeddings = OpenAIEmbeddings(model='text-embedding-3-small')
    
    text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=5, separator='。')
    
    # ディレクトリを指定
    directory = '/PATH/TO/doboku'
    pdf_files = glob.glob(os.path.join(directory, '*.pdf'))
    
    all_pages = []
    
    for pdf_file in pdf_files:
        loader = PyPDFLoader(pdf_file)
        pages = loader.load_and_split(text_splitter)
        all_pages.extend(pages)
    
    vectorstore = PineconeVectorStore.from_documents(all_pages, embedding=embeddings, index_name=index_name)


if __name__ == '__main__':
    main()
```
結果はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351434/5dd3b08c-3fd7-93a4-11b9-09fe8fcb08a0.png)

規格値が記載されている PDF なんかは表が多くまともなベクトルにはなっていないようです。
PDF Parser 丸投げが良くないのは想定内ですが今後の課題です。

# 日報の内容を使ってクエリーを投げてみる
実際の日報の記載を使用して結果を見てみます。

``` py
import os

from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_pinecone import PineconeVectorStore
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

os.environ['PINECONE_API_KEY'] = 'YOUR_API_KEY'
os.environ['OPENAI_API_KEY'] = 'YOUR_API_KEY'

llm = ChatOpenAI(model='gpt-4o', temperature=0.5)
output_parser = StrOutputParser()
embeddings = OpenAIEmbeddings(model='text-embedding-3-small')
index_name = 'doboku'


# プロンプトテンプレート
message = """
        あなたは建設会社の土木部門の責任者です。
        提供されたコンテキストを使用して、部員の日報にアドバイスや指摘があれば提示してください。
        冒頭で、適度に部員への気遣いや労いの言葉を添えてください。文末に気遣いや労いの言葉は不要です。
        "部員への労いとアドバイス"、"お疲れ様です"、"アドバイスをさせていただきます"という前置きも不要です。
        関連度合いが高いと思われる文書を検索し、部員に提供する情報を整理してください。
        関連情報があった場合は部員の日報の該当部分を ">"で引用し、関連情報の記載内容、記載があった文書、そのページ番号を「『記載内容』 - 【文書名】 (ページ番号：)」の形式で示してください。
        "該当部分" という前置きは不要です。【文書名】はファイル名のみで構いません。
        ページ番号は-1してください。
        関連度合いが低い内容については過度に部員には伝えないでください。例えば顧客とのやり取りに関する情報は重要な指摘事項がない限り、部員には伝えないでください。
    {comment}
    Context:
    {context}
    """

vectorstore = PineconeVectorStore.from_existing_index(index_name=index_name, embedding=embeddings)

retriever = vectorstore.as_retriever(
    search_type = 'similarity',
    search_kwargs = {'k': 5}
)

prompt = ChatPromptTemplate.from_messages([('human', message)])

rag_chain = {'context': retriever, 'comment': RunnablePassthrough()} | prompt | llm

query = '雨のためジョイント補修は中止。パトロール日誌で上がってきた桝清掃に変更しました。'

response = rag_chain.invoke(query)
print(response.content)
```
結果は以下のようになりました。
``` 
いつもお疲れ様です。雨の影響でジョイント補修が中止となったこと、そしてパトロール日誌に基づいて桝清掃に変更したことについて、適切な対応をされていると思います。以下に関連する情報を提供しますので、参考にしてください。

> 雨のためジョイント補修は中止

「劣化するので、雨天の時の作業は中止しなければならない。これ以外の場合は、設計図書によるものとする」 - 【03_土木工事共通編】 (ページ番号：19)

また、雨天時に作業を中止することの重要性については、以下の記載も参考になります。

「雨が降り出した場合、敷均し作業を中止し、すでに敷均した箇所の混合物をすみやかに締固めて仕上げを完了させなければならない」 - 【03_土木工事共通編】 (ページ番号：67)

このように、雨天時の作業中止は適切な判断であり、今後も安全と品質を第一に考えて作業を進めてください。
``` 
一見、それっぽい回答のように見えますが、仕様書の該当ページを実際に確認してみると、特定の工種に関する記載事項であり、日報を書いた社員が実際に行った業務とは関係なく、単に雨に反応しただけのようです。

# まとめ
課題は沢山あるようです。
日報には工種と作業内容の選択肢がありますので選択項目を強く反応させるとか、仕様書の見出し等をベクトルのメタデータに入れ込むとか。
今回は何も工夫せずにやっただけですのでしょうがありませんが他にもいろいろ考えて試してみようと思います。