🤨

streamlitで自社の規定RAGを作成しよう!LangGraph/自己修正RAG

2024/12/07に公開

本記事は以下記事を実践的に使用できるようにしたものです。コードの基本的構造は変えていません。
https://zenn.dev/tsuzukia/articles/0724729c2b733e
https://zenn.dev/tsuzukia/articles/e4db5889a9a02c
上記記事で紹介したコードではLangGraphのチュートリアルコードそのままに特定ドキュメントのRAG、ネット検索というものでしたが、今回は社内規則のQ&Aボットを想定して、社内規則を読み込み、それに関する質問に答えられるRAGというものを実装します!

ライブラリのバージョン

langchainは移り変わりの激しいライブラリで、アップデートするとコードが動かなかったり、挙動が変わったりというのは日常茶飯事です。執筆時点では以下のバージョンで行なっています。

langchain==0.3.9
langchain-community==0.3.9
langchain-openai=0.2.11
langchain-experimental==0.3.3
langgraph==0.2.56

langchain以外だと以下ライブラリが必要です。
こちらは特にバージョンは指定しなくて大丈夫です。(多分)

pip install streamlit pypdfium2

また、RAGの挙動や使用状況の確認のためにLangsmith等監視ツールの導入は必須です。
https://zenn.dev/pharmax/articles/61edc477e4de17

今回使うドキュメント

今回は厚生労働省のモデル就業規則を使用させて頂きます!
https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/koyou_roudou/roudoukijun/zigyonushi/model/index.html
https://www.mhlw.go.jp/content/001018385.pdf
モデル就業規則ということで例文みたいな感じですがデモには十分です。

ベクトルストアの作成は以下で実行できます。

# 必要なライブラリをインポート
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from glob import glob
from typing import Any
from langchain_openai.embeddings import OpenAIEmbeddings
from langchain_experimental.text_splitter import SemanticChunker
from langchain_community.document_loaders import PyPDFium2Loader

# OpenAIの埋め込みモデルを初期化(text-embedding-3-largeを使用)
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# テキスト分割器の設定
# 意味的なチャンク分割を行い、日本語の句読点で分割
# breakpoint_threshold_type: 分割ポイントの決定方法
# sentence_split_regex: 日本語の句読点でのテキスト分割用の正規表現
text_splitter = SemanticChunker(
    embeddings,
    breakpoint_threshold_type="standard_deviation",
    sentence_split_regex=r"(?<=[、。【])\s+",
    buffer_size=1
)

# PDFファイルのURLを指定
file = "https://www.mhlw.go.jp/content/001018385.pdf"

# PDFローダーを使用してファイルを読み込み
loader = PyPDFium2Loader(file)
document = loader.load()

# 読み込んだドキュメントをチャンクに分割
docs = text_splitter.split_documents(document)

# 分割されたテキストを確認するための処理
texts = ""
print(len(docs))  # 分割されたチャンクの数を表示
for doc in docs:
    # 各チャンクの内容を区切り線付きで連結
    texts += doc.page_content+"\n\n///////////////////////////////"
print(texts)

# FAISSベクトルストアの作成
# 分割されたドキュメントを検索可能な形式に変換
db = FAISS.from_documents(docs, embeddings)

# ベクトルストアをローカルに保存
db.save_local("employment_rules")

ベクトルストアはemployment_rulesという名前で保存しました。
今回はちょっと珍しいSemantic splitっていうのを使用します。埋め込みモデルを使って分割チャンクを決めます。面白いアイディアですよね。詳細は以下記事にあります。
https://github.com/FullStackRetrieval-com/RetrievalTutorials/blob/main/tutorials/LevelsOfTextSplitting/5_Levels_Of_Text_Splitting.ipynb

ワークフロー

以下がワークフローになります。

自己修正RAGの詳細は以下記事でも解説していますのでぜひチェックしてみてください
https://zenn.dev/tsuzukia/articles/0724729c2b733e
自己修正RAGですので以下機能を有しています。

  1. 質問分析
  2. 取得したドキュメントの分析
  3. 回答のハルシネーションチェック
  4. 回答の有用性チェック
  5. クエリの調整

特に重要だと考えているのが1の質問分析です。
ここはプロンプトの調整とファインチューニングによる継続的な改善が不可欠です。
この点については、後ほど詳細を説明します。

Streamlitのコード

長いですが以下になります。前回記事のものと基本的構造は変えておらず、変更点はRAGを使用することになったということです。
あまりに長いのでデフォルト非表示にしておきます。

コード
import streamlit as st
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_community.vectorstores import FAISS
from langgraph.graph import StateGraph
from typing import List, Literal, TypedDict
from langgraph.graph import END, StateGraph, START
import asyncio
import os
from langgraph.constants import Send

class RouteQuery(BaseModel):
    """ユーザーの質問をデータソースにルーティングするか判断します。"""

    choice: Literal["vectorstore", "no_tool_use"] = Field(
        ...,
        description="ユーザーの質問に対してデータソースにルーティングするかしないか",
    )
    reason: str = Field(
        ...,
        description="判断理由の説明",
    )
    answer: str = Field(
        ...,
        description="追加の回答情報(必要に応じて)",
    )
    query: str = Field(
        ...,
        description="ベクトルストアにルーティングする場合は、質問をベクトル検索に適したクエリを作成してください。",
    )

class GradeDocuments(BaseModel):
    """取得された文書の関連性チェックのためのバイナリスコア。"""
    binary_score: str = Field(
        description="文書が質問に関連しているかどうか、「yes」または「no」"
    )

class GradeHallucinations(BaseModel):
    """生成された回答における幻覚の有無を示すバイナリスコア。"""
    binary_score: str = Field(
        description="回答が事実に基づいているかどうか、「yes」または「no」"
    )

class GradeAnswer(BaseModel):
    """回答が質問に対処しているかどうかを評価するバイナリスコア。"""
    binary_score: str = Field(
        description="回答が質問に対処しているかどうか、「yes」または「no」"
    )

class GraphState(TypedDict):
    """
    グラフの状態を表します。
    属性:
        question: 質問
        query: 検索キーワード
        generation: LLM生成
        documents: 文書のリスト
    """
    question: str
    query: str
    generation: str
    documents: List[str]

def update_status_and_messages(message: str, state: str = "running", expanded: bool = True, additional_info: str = ""):
    """ステータス、プレースホルダー、ステータスメッセージを一括更新する関数
    Args:
        message (str): 表示するメッセージ
        state (str, optional): ステータスの状態. Defaults to "running".
        expanded (bool, optional): ステータスを展開するかどうか. Defaults to True.
        additional_info (str, optional): プレースホルダーに表示する追加情報. Defaults to "".
    """
    st.session_state.status.update(label=f"{message}", state=state, expanded=expanded)
    if additional_info:
        st.session_state.placeholder.markdown(additional_info)
    st.session_state.status_messages += message + "\n\n"
    if additional_info:
        st.session_state.status_messages += additional_info + "\n\n"

async def route_question(state):
    """
    質問を分析し、ベクトルストアを使用するか判定する関数
    vectorstore: 社内規定に関する質問
    no_tool_use: その他の質問
    """
    update_status_and_messages(
        "**---ROUTE QUESTION---**",
        expanded=False,
    )
    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    structured_llm_router = llm.with_structured_output(RouteQuery)
    system = """ユーザーからの質問を分析し、ベクトルストアを使用するか判定してください。回答は以下の形式で提供してください:

1. choice: "vectorstore" または "no_tool_use" を選択
2. reason: 選択した理由を簡潔に説明
3. answer: 追加情報(必要な場合のみ)
4. query: ベクトルストアにルーティングする場合は、質問をベクトル検索に適した質問に変換してください。

ベクトルストア(vectorstore)には社内規定が含まれています。以下の場合は "vectorstore" を選択してください:
- 特定の条文の内容を引用・参照する質問。ただし、複数の条文の比較や解釈、外部情報との照合が必要な場合は、"no_tool_use" を選択する。
- 特定の用語の定義に関する質問
- 社内規定やに直接関連する質問

以下の場合は "no_tool_use" を選択してください:
- 広範囲に渡る調査や分析が必要な質問
- 複数のデータソースを参照・比較する必要がある質問
- 主観的な判断や創造性を必要とする質問
- 専門的な知識や経験が必要な質問
- 法律や倫理に関わる複雑な質問
- ベクトルストアの内容と明らかに無関係な質問

"no_tool_use"を選択した場合answerは以下の回答を提供してください。専門用語は使わずに回答すること。:
- 通常の会話: ユーザーが日常的な会話や雑談をしている場合は、それに合わせてフレンドリーに会話を続けてください。
- 曖昧な質問へのアドバイス: ユーザーの質問が曖昧で、何を求めているのか明確でない場合は、より具体的な質問をするように分かりやすくアドバイスしてください。
- 専門的な質問への対応: 専門知識が必要な質問に対しては、専門家や担当者への相談を促すなど、適切な対応を提案してください。
- 対応できない質問への対応: 倫理的な問題や、あなたの能力を超える質問に対しては、対応できない旨を分かりやすく伝えて下さい。

以下は例です:
question: 初年度に与えられる有給休暇は何日か教えてください。
choice: "vectorstore"
reason: "この質問では、社内規定に記載されている具体的な手順について尋ねています。"
answer: ""
query: "有給休暇 日数"

question: 以前のバージョンの社内規程からの変更点は何ですか?
choice: "no_tool_use"
reason: "この質問は、ベクトルストアの範囲外となる広範な調査が必要になる可能性があります。"
answer: "このアプリの範囲外となる広範な調査が必要になる可能性があります。直接担当者へ確認することをお勧めします。"
query: ""

question: 新車のおすすめは何ですか?
choice: "no_tool_use"
reason: "ベクトルストアの内容と明らかに無関係な質問です。"
answer: "このアプリの範囲外となる質問と推測されます。直接担当者へ確認することをお勧めします。"
query: ""
"""

    route_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            ("human", "{question}"),
        ]
    )

    question_router = route_prompt | structured_llm_router

    question = state["question"]
    source = question_router.invoke({"question": "question:" + question})
    if source.choice  == "no_tool_use":
        update_status_and_messages(
            "NOT ROUTE QUESTION TO RAG",
            expanded=False,
        )
        return Send("no_tool_use",{"question": question, "generation": source.answer})
    elif source.choice  == "vectorstore":
        update_status_and_messages(
            "ROUTE QUESTION TO RAG",
            additional_info=f"Query: {source.query}"
        )
        return Send("retrieve",{"question": question,"query": source.query})

async def no_tool_use(state):
    return state

async def retrieve(state):
    """
    FAISSベクトルストアから関連文書を検索する関数
    """
    embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
    update_status_and_messages(
        "**---RETRIEVE---**",
        additional_info=f"RETRIEVING…\n\nKEY WORD:{state['query']}"
    )
    file="employment_rules"
    db=FAISS.load_local(file, embeddings,allow_dangerous_deserialization=True)

    retriever = db.as_retriever(search_kwargs={'k': 6})
    query = state["query"]
    documents = retriever.invoke(query)
    update_status_and_messages(
        "**RETRIEVE SUCCESS!!**",
        state="complete",
    )
    state["documents"] = documents
    return state

async def grade_documents(state):
    """
    検索された文書が質問に関連しているかを評価する関数
    2回目の試行までは文書の関連性をチェック
    """
    st.session_state.number_trial += 1
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

    structured_llm_grader = llm.with_structured_output(GradeDocuments)

    system = """あなたは、ユーザーの質問に対して取得されたドキュメントの関連性を評価する採点者です。
ドキュメントにユーザーの質問に関連するキーワードや意味が含まれている場合、それを関連性があると評価してください。
目的は明らかに誤った取得を排除することです。厳密なテストである必要はありません。
ドキュメントが質問に関連しているかどうかを示すために、バイナリスコア「yes」または「no」を与えてください。"""
    grade_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            ("human", "Retrieved document: \n\n {document} \n\n User question: {question}"),
        ]
    )

    retrieval_grader = grade_prompt | structured_llm_grader
    update_status_and_messages(
        "**---CHECK DOCUMENT RELEVANCE TO QUESTION---**",
        expanded=False
    )       
    question = state["question"]
    documents = state["documents"]
    filtered_docs = []
    i = 0
    for d in documents:
        if st.session_state.number_trial <= 2:
            file_name = d.metadata["source"]
            file_name = os.path.basename(file_name.replace("\\","/"))
            page = d.metadata["page"]
            content = d.page_content
            i += 1
            score = retrieval_grader.invoke(
                {"question": question, "document": d.page_content}
            )
            grade = score.binary_score
            if grade == "yes":
                update_status_and_messages(
                    "**GRADE: DOCUMENT RELEVANT**",
                    state="complete",
                    additional_info=f"DOC {i}/{len(documents)} {file_name} page.{page} : **RELEVANT**\n\n{content}",
                )
                filtered_docs.append(d)
            else:
                update_status_and_messages(
                    "**GRADE: DOCUMENT NOT RELEVANT**",
                    state="error",
                    additional_info=f"DOC {i}/{len(documents)} {file_name} page.{page} : **NOT RELEVANT**\n\n{content}"
                )
        else:
            filtered_docs.append(d)

    if not st.session_state.number_trial <= 2:
        update_status_and_messages(
            "**NO NEED TO CHECK. QUERY TRANSFORMATION HAS BEEN COMPLETED**",
            state="complete",
            expanded=False
        )
    state["documents"] = filtered_docs
    return state

async def generate(state):
    """
    検索された文書を基に回答を生成する関数
    """
    update_status_and_messages(
        "**---GENERATE---**",
        expanded=False
    )
    prompt = ChatPromptTemplate.from_messages(
            [
                ("system", """あなたはとある会社の社内規定アシスタントです。以下の取得された文書を使用して社員からの質問に答えてください。
文書は質問に対し、ベクトル検索して取得されたものです。必ずしも最適な文書があるとは限りませんが、その結果を基に回答してください。
答えがわからない場合や文書が不十分な場合は、嘘の回答を作ったりせずに担当部署へ直接問い合わせるよう伝えてください。
回答に文書を参照した場合には、回答の最後には参照した文書名、ページ、文書の改定日を示してください。"""),
                ("human", """Question: {question} 
Context: {context}"""),
            ]
        )
        
    llm = ChatOpenAI(model_name="gpt-4o", temperature=0)

    rag_chain = prompt | llm | StrOutputParser()
    question = state["question"]
    documents = state["documents"]
    generation = rag_chain.invoke({"context": documents, "question": question})
    state["generation"] = generation
    return state

async def transform_query(state):
    """
    検索クエリを最適化して再検索を行う関数
    より良い検索結果を得るためにクエリを書き換える
    """
    update_status_and_messages(
        "**---TRANSFORM QUERY---**",
        expanded=False
    )
    st.session_state.placeholder.empty()
    llm = ChatOpenAI(model="gpt-4o", temperature=0)

    system = """あなたは、検索キーワードをベクトルストア検索に最適化されたより良いバージョンに変換するキーワードリライターです。
この質問はベクトル化された社内規定に対してベクトル検索するために使用されます。
一回目のキーワードは、良いドキュメントを取得出来なかったので再挑戦です。
質問を見て、質問者の意図/意味について推論してより良いベクトル検索の為のキーワードを作成してください。
キーワードを1個のみ文字列として出力してください。
"""
    re_write_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            (
                "human",
                "最初のキーワード: \n{query}\n\nユーザーからの質問:\n{question}\n\n改善された質問をキーワードを作成してください。",
            ),
        ]
    )

    question_rewriter = re_write_prompt | llm | StrOutputParser()
    question = state["question"]
    query = state["query"]
    better_question = question_rewriter.invoke({"query": query ,"question": question})
    update_status_and_messages(
        "**---TRANSFORM QUERY COMPLETE---**",
        state="complete",
        additional_info = f"**Better question: {better_question}**",
    )
    state["query"] = better_question
    return state

async def decide_to_generate(state):
    """
    文書が見つからない場合はクエリを変換し、
    文書が見つかった場合は回答生成に進む判断を行う関数
    """
    filtered_documents = state["documents"]
    if not filtered_documents:
        update_status_and_messages(
            "**DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY**",
            state="error",
            expanded=False
        )
        return "transform_query"                                     
    else:
        update_status_and_messages(
            "**DECISION: GENERATE**",
            expanded=False
        )
        return "generate"

async def grade_generation_v_documents_and_question(state):
    """
    生成された回答の品質を評価する関数
    - 文書に基づいているか(幻覚がないか)
    - 質問に適切に答えているか
    をチェック
    """
    st.session_state.number_trial += 1  
    update_status_and_messages(
        "**---CHECK HALLUCINATIONS---**",
        expanded=False
    )
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

    structured_llm_grader = llm.with_structured_output(GradeHallucinations)

    system = """あなたは、LLMの生成が取得された事実のセットに基づいているか/サポートされているかを評価する採点者です。
バイナリスコア「yes」または「no」を与えてください。「yes」は、回答が事実のセットに基づいている/サポートされていることを意味します。"""
    hallucination_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            ("human", "Set of facts: \n\n {documents} \n\n LLM generation: {generation}"),
        ]
    )
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

    structured_llm_grader = llm.with_structured_output(GradeAnswer)

    system = """あなたは、回答が質問に対処しているか/解決しているかを評価する採点者です。
バイナリスコア「yes」または「no」を与えてください。「yes」は、回答が質問を解決していることを意味します。"""
    answer_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            ("human", "User question: \n\n {question} \n\n LLM generation: {generation}"),
        ]
    )

    answer_grader = answer_prompt | structured_llm_grader
    hallucination_grader = hallucination_prompt | structured_llm_grader
    question = state["question"]
    documents = state["documents"]
    generation = state["generation"]
    score = hallucination_grader.invoke(
        {"documents": documents, "generation": generation}
    )
    grade = score.binary_score
    if st.session_state.number_trial <= 3:
        if grade == "yes":
            update_status_and_messages(
                "**DECISION: ANSWER IS BASED ON A SET OF FACTS**",
                state="complete",
            )
            update_status_and_messages(
                "**---GRADE GENERATION vs QUESTION---**",
            )
            score = answer_grader.invoke({"question": question, "generation": generation})
            grade = score.binary_score
            if grade == "yes":
                update_status_and_messages(
                    "**DECISION: GENERATION ADDRESSES QUESTION**",
                        additional_info=f"**USEFULL!!**\n\nquestion : {question}\n\ngeneration : {generation}",
                        state="complete",
                    )
                return "useful"
            else:
                st.session_state.number_trial -= 1
                update_status_and_messages(
                    "**DECISION: GENERATION DOES NOT ADDRESS QUESTION**",
                        additional_info=f"**NOT USEFULL!!**\n\nquestion:{question}\n\ngeneration:{generation}",
                        state="error",
                    )
                return "not useful"
        else:
            update_status_and_messages(
                "**DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY**",
                additional_info=f"**NOT GROUNDED**\n\nquestion:{question}\n\ngeneration:{generation}",
                state="error",
            )
            return "not supported"
    else:
        update_status_and_messages(
            "**TRIAL LIMIT EXCEEDED. NO NEED TO CHECK**",
            expanded=False,
            state="complete",
        )
        return "useful"

async def run_workflow(inputs):
    """
    全体のワークフローを実行する関数
    状態管理とUIの更新を行う
    """
    st.session_state.number_trial = 0
    with st.status(label="**GO!!**", expanded=True,state="running") as st.session_state.status:
        st.session_state.placeholder = st.empty()
        value = await st.session_state.workflow.ainvoke(inputs)

    st.session_state.placeholder.empty()
    st.session_state.answer = st.empty()
    st.session_state.status.update(label="**FINISH!!**", state="complete", expanded=False)
    st.session_state.answer.markdown(value["generation"])
    with st.popover("ログ"):
        st.markdown(st.session_state.status_messages)

# メインのStreamlitアプリケーション部分
if 'status_messages' not in st.session_state:
    st.session_state.status_messages = ""

# ワークフローの初期化(初回のみ実行)
if not hasattr(st.session_state, "workflow"):
    # StateGraphの設定
    workflow = StateGraph(GraphState)
    # ノードの追加
    workflow.add_node("no_tool_use", no_tool_use)
    workflow.add_node("retrieve", retrieve)
    workflow.add_node("grade_documents", grade_documents)
    workflow.add_node("generate", generate)
    workflow.add_node("transform_query", transform_query)

    # エッジの追加(フローの制御)
    workflow.add_conditional_edges(
        START,
        route_question,
        {
            "retrieve": "retrieve",
            "no_tool_use": "no_tool_use",
        },
    )
    workflow.add_edge("retrieve", "grade_documents")
    workflow.add_conditional_edges(
        "grade_documents",
        decide_to_generate,
        {
            "transform_query": "transform_query",
            "generate": "generate",
        },
    )
    workflow.add_edge("transform_query", "retrieve")
    workflow.add_edge("no_tool_use", END)
    workflow.add_conditional_edges(
        "generate",
        grade_generation_v_documents_and_question,
        {
            "not supported": "transform_query",
            "useful": END,
            "not useful": "transform_query",
        },
    )
    app = workflow.compile()
    app = app.with_config(recursion_limit=20,run_name="Agent",tags=["Agent","FB"])
    app.name = "Agent"
    st.session_state.workflow = app

st.title("自己修正RAG")
st.write("社内規定に関する**一問一答形式**のチャットボットです。")

if prompt := st.chat_input("質問を入力してください"):
    st.session_state.status_messages = ""
    with st.chat_message("user", avatar="😊"):
        st.markdown(prompt)

    inputs = {"question": prompt}
    asyncio.run(run_workflow(inputs))

懸念するポイント

RAGの公開(特にAIリテラシーが高くないユーザーによる利用)に関して、以下の2点のような質問を懸念しています。

1.極端に短かったり、曖昧すぎる質問
2.RAGでは対応不可能な質問・無理難題な質問

1については、例えばAIに対してGoogle検索のような使い方をされるケース、2については、RAGがピンポイントな質問には対応できても、全体の要約のような質問には対応できないケースが考えられます。開発者や詳しい人であればこれらの限界は理解していますが、初めて利用するユーザーには分かりにくいでしょう。結果として、質問しても全く役に立たないという状況は避けたいと考えています。そのため、対応できない質問に対しては、はっきりと「できません」と回答する、Noと言えるチャットボットを目指しています。

質問ルーターのプロンプト

重要なのは、最初に質問分析を行い、前述の懸念事項に該当する質問を除外することです。
加えて、次のノードで必要となる変数を適切に出力させる必要があります。
以下は、質問分析ルーターのプロンプトです。

出力形式の指定

出力形式は以下の4つ出すようにしています。

choice: "vectorstore" または "no_tool_use" のいずれかを選択します。
reason: 選択した理由を簡潔に説明します(デバッグ用)。
answer: "no_tool_use" を選択した場合、この回答をもって終了します。必要に応じユーザーにアドバイスをします。
query: ベクトル検索を行う際のキーワードです。ユーザーの質問をそのまま検索に使うのではなく、AIに適切なキーワードを生成させます。

ユーザーからの質問を分析し、ベクトルストアを使用するか判定してください。回答は以下の形式で提供してください:

1. choice: "vectorstore" または "no_tool_use" を選択
2. reason: 選択した理由を簡潔に説明
3. answer: 追加情報(必要な場合のみ)
4. query: ベクトルストアにルーティングする場合は、質問をベクトル検索に適した質問に変換してください。

ルーティングの詳細条件

ここで質問を除外させる条件を詳細に決めます。

ベクトルストア(vectorstore)には社内規定が含まれています。以下の場合は "vectorstore" を選択してください:
- 特定の条文の内容を引用・参照する質問。ただし、複数の条文の比較や解釈、外部情報との照合が必要な場合は、"no_tool_use" を選択する。
- 特定の用語の定義に関する質問
- 社内規定やに直接関連する質問

以下の場合は "no_tool_use" を選択してください:
- 広範囲に渡る調査や分析が必要な質問
- 複数のデータソースを参照・比較する必要がある質問
- 主観的な判断や創造性を必要とする質問
- 専門的な知識や経験が必要な質問
- 法律や倫理に関わる複雑な質問
- ベクトルストアの内容と明らかに無関係な質問

ベクトル検索をしない場合の回答の指定

"no_tool_use"を選択した場合answerは以下の回答を提供してください。専門用語は使わずに回答すること。:
- 通常の会話: ユーザーが日常的な会話や雑談をしている場合は、それに合わせてフレンドリーに会話を続けてください。
- 曖昧な質問へのアドバイス: ユーザーの質問が曖昧で、何を求めているのか明確でない場合は、より具体的な質問をするように分かりやすくアドバイスしてください。
- 専門的な質問への対応: 専門知識が必要な質問に対しては、専門家や担当者への相談を促すなど、適切な対応を提案してください。
- 対応できない質問への対応: 倫理的な問題や、あなたの能力を超える質問に対しては、対応できない旨を分かりやすく伝えて下さい。

few-shot prompting

ここは実際にあった質問を基に調整されるのがいいかと思います。

以下は例です:
question: 初年度に与えられる有給休暇は何日か教えてください。
choice: "vectorstore"
reason: "この質問では、社内規定に記載されている具体的な手順について尋ねています。"
answer: ""
query: "有給休暇 日数"

question: 以前のバージョンの社内規程からの変更点は何ですか?
choice: "no_tool_use"
reason: "この質問は、ベクトルストアの範囲外となる広範な調査が必要になる可能性があります。"
answer: "このアプリの範囲外となる広範な調査が必要になる可能性があります。直接担当者へ確認することをお勧めします。"
query: ""

question: 新車のおすすめは何ですか?
choice: "no_tool_use"
reason: "ベクトルストアの内容と明らかに無関係な質問です。"
answer: "このアプリの範囲外となる質問と推測されます。直接担当者へ確認することをお勧めします。"
query: ""

プロンプトは大幅に変える余地があります!
ここはプロンプトエンジニアリングの見せ所ですので、ユースケースに合わせて、また使用状況をモニタリングして追い込んでいきましょう!

まとめたプロンプト

上記をまとめたプロンプトは以下になります。

プロンプト
    system = """ユーザーからの質問を分析し、ベクトルストアを使用するか判定してください。回答は以下の形式で提供してください:

1. choice: "vectorstore" または "no_tool_use" を選択
2. reason: 選択した理由を簡潔に説明
3. answer: 追加情報(必要な場合のみ)
4. query: ベクトルストアにルーティングする場合は、質問をベクトル検索に適した質問に変換してください。

ベクトルストア(vectorstore)には社内規定が含まれています。以下の場合は "vectorstore" を選択してください:
- 特定の条文の内容を引用・参照する質問。ただし、複数の条文の比較や解釈、外部情報との照合が必要な場合は、"no_tool_use" を選択する。
- 特定の用語の定義に関する質問
- 社内規定やに直接関連する質問

以下の場合は "no_tool_use" を選択してください:
- 広範囲に渡る調査や分析が必要な質問
- 複数のデータソースを参照・比較する必要がある質問
- 主観的な判断や創造性を必要とする質問
- 専門的な知識や経験が必要な質問
- 法律や倫理に関わる複雑な質問
- ベクトルストアの内容と明らかに無関係な質問

"no_tool_use"を選択した場合answerは以下の回答を提供してください。専門用語は使わずに回答すること。:
- 通常の会話: ユーザーが日常的な会話や雑談をしている場合は、それに合わせてフレンドリーに会話を続けてください。
- 曖昧な質問へのアドバイス: ユーザーの質問が曖昧で、何を求めているのか明確でない場合は、より具体的な質問をするように分かりやすくアドバイスしてください。
- 専門的な質問への対応: 専門知識が必要な質問に対しては、専門家や担当者への相談を促すなど、適切な対応を提案してください。
- 対応できない質問への対応: 倫理的な問題や、あなたの能力を超える質問に対しては、対応できない旨を分かりやすく伝えて下さい。

以下は例です:
question: 初年度に与えられる有給休暇は何日か教えてください。
choice: "vectorstore"
reason: "この質問では、社内規定に記載されている具体的な手順について尋ねています。"
answer: ""
query: "有給休暇 日数"

question: 以前のバージョンの社内規程からの変更点は何ですか?
choice: "no_tool_use"
reason: "この質問は、ベクトルストアの範囲外となる広範な調査が必要になる可能性があります。"
answer: "このアプリの範囲外となる広範な調査が必要になる可能性があります。直接担当者へ確認することをお勧めします。"
query: ""

question: 新車のおすすめは何ですか?
choice: "no_tool_use"
reason: "ベクトルストアの内容と明らかに無関係な質問です。"
answer: "このアプリの範囲外となる質問と推測されます。直接担当者へ確認することをお勧めします。"
query: ""

ファインチューニング

これで実運用していってデータが揃って、プロンプトの調整もある程度出来たら、ファインチューニングすることをお勧めします!
openaiのファインチューニングはすごい簡単で以下記事が参考になります。

https://zenn.dev/pharmax/articles/40c0e385a8956b

記事でも触れられていますが、ある程度プロンプトエンジニアリングで追い込んで最後の仕上げとしてファインチューニングするという流れがいいです。

RAGシステムをブーストさせろ!PDCAサイクルで最強のAIチャットボットへ進化させよう!

RAGシステムの改善は、ユーザーの生の声を聞き、データを徹底的に分析することから始まります!
集めたデータは宝の山。回答の質やルーティング精度をガンガン評価していきます。分析には、書類作成者の知見を借りるのもアリ!最強の布陣で挑みましょう。

分析結果を基に、プロンプトを磨き上げ、ベクトルストアを最適化し、必要ならファインチューニングで最終兵器を投入!
改善後は、再びデータを収集・分析して効果測定。PDCAサイクルを高速回転させ、次なる進化へとつなげます!

このサイクルを回し続けることで、RAGシステムはどんどん賢くなり、ユーザーの期待を超える、頼れるAIチャットボットへと成長していきます。

さあ、あなたもこのサイクルに参加して、RAGシステムを一緒に最強に育て上げましょう!
そして、この挑戦の軌跡を、ぜひあなたのエンジニアリングブログで発信してください。
あなたの経験は、他のエンジニアたちの道しるべとなり、AI技術の発展に貢献することでしょう。
私たちと一緒に、未来のAIチャットボットを創造しましょう!
Let's RAG and Roll!

Discussion