アダコテック技術ブログ

アダコテックのエンジニアが発信する技術ブログです

製造業の現場に生成AIを取り入れる — RAGを使った過去履歴活用の試み

はじめに

こんにちは、アダコテックの伊藤です。 私たちは、カメラやセンサーで取得したデータの解析を中心に、製造業向けの画像検査ソリューションを提供しています。 近年、生成AIの技術革新が進み、製造現場においてもその活用が注目されるようになってきました。 こうした流れを受け、当社でもLLMの研究開発に取り組み、製造業への新たな価値提供を目指しています。 中でも、製造現場の障害対応は属人化しやすく、過去の対応履歴が活かされず無駄な時間がかかることが多いのが現状です。過去事例から最適な対応を提案できれば、復旧スピードの向上が期待できます。 この記事では、その解決策として検証したRAG(Retrieval-Augmented Generation)のプロトタイプと、その実装・評価・考察についてご紹介します。


考えられる3つの技術的アプローチ

生成AIを利用して課題解決に取り組む際、主に以下の3つの方法が考えられます。

1. プロンプトエンジニアリング

もっとも手軽に始められるのがこの方法です。 汎用の大規模言語モデル(LLM)に対して、工夫したプロンプト(指示文)を与えることで、より的確な応答を引き出す技術です。 例えば、過去のトラブル履歴を簡単に文章化し、それをプロンプトに含めて「これと似たケースの対応を教えてください」と依頼するとか、「結論を最初に記述し、その理由をその後に説明して」のように出力のフォーマットを指示する、といった使い方です。 ただし、製造業の障害対応のように専門的かつ現場固有の知識が必要な領域では、汎用的なモデルに対してプロンプトだけで対応するのは限界があります。

2. パラメータ効率型ファインチューニング(PEFT)

LoRA(Low-Rank Adaptation)に代表される、パラメータ効率型のファインチューニングです。 モデルの全パラメータではなく、一部だけを追加的に学習させることで、フルチューニングに比べて低コストでドメイン適応が可能です。 ただし、低コストといってもそれなりに高性能な学習環境の整備が必要であり、今回のような試験的な検証としてはややハードルが高いというデメリットもあります。

3. RAG(Retrieval-Augmented Generation)

LLMは「書き方」は得意でも、業界固有の知識や細かい履歴までは持っていません。そこで、必要な情報を外部のデータベースから検索し、その結果をコンテキストに加えた上で、LLMに回答させるという流れです。 検索に使うデータを自社ドメインにする事で、ドメイン特化した高品質な応答が期待できます。 計算コストも低く扱いやすい点も長所となっており、本文ではこの方法をベースにした取り組みを紹介しています。


実験と評価方法

製造業と分野が異なるものですが、Kaggleの下記データセットを使って評価を行いました。

www.kaggle.com

このデータセットはIT Supportにおいて発行されるチケット情報のデータセットとなっており顧客からの問い合わせ情報として、

  • 問い合わせタイトル(subject)
  • 問い合わせ本文(body)
  • 問い合わせ言語
  • ソフトウェアバージョン

という情報があり、この情報を受けて

  • リクエスト、障害報告といった問い合わせ区分(type)
  • カスターマーサポートやプリセールスといった対応部署(queue)
  • 問い合わせの重要度(priority)

といった情報を付与し、問い合わせに対する回答文書(answer)を作成します。 問い合わせ内容、言語、バージョンから、対応区分情報と問い合わせに対する回答を予測することでサポート業務を効率化するという想定です。 検証にあたり、出力の良し悪しを定量的に測るために以下の評価指標を設定しました。

  • type, queue, priorityに対する識別精度をMacro F1-scoreで評価
  • answerの品質として、正解と出力に対する比較をROUGE-LとBERTScoreで評価。(ROUGE-Lにより文全体の流れや構造を評価し、BERTScoreで意味的な類似性を評価します。詳しくは下記リンクを参照ください)

zenn.dev

この条件で、RAGの有無による性能変化を検証してみました。 なお、検証は28587データから50データをテストに利用して、残りをデータベースの構築に活用しています。


実装の概要

構成

本システムは、LangChain を使って実装しました。 LangChainは、外部データの検索・前処理・プロンプト生成・LLM呼び出しといった一連の処理をつなげて構築できるライブラリです。RAGのように「検索して生成する」仕組みを簡単に組めるのが特徴です。 今回の実装では、以下の要にしました。

  • 埋め込みモデル:OpenAI の text-embedding-ada-002

    • テキストをベクトルに変換するモデルです。
  • ベクトルDB:FAISS

    • ベクトル化した過去事例を保存・検索する高速なデータベースです。
  • LLM:GPT-4

    • 類似事例と問い合わせ内容をもとに、最適な回答を生成します。

処理の流れ

  1. 過去履歴のベクトル化

    train.csvから過去のチケットをFAISSに登録。

  2. 問い合わせの選択

    test.csvの中から1件選び、問い合わせ文として利用。

  3. 類似事例の検索

    FAISSで問い合わせに近い3件の過去履歴を取得。

  4. プロンプトの構築

    類似事例をコンテキストにし、LLMへの指示文を組み立て。

  5. LLM呼び出し

    GPT-4にプロンプトを投げ、type, queue, priority, answerを生成。

コード

過去履歴のベクトル化

CSVファイル1行を1チャンクとして、ベクトル化しています。

import os

import pandas as pd
from dotenv import load_dotenv
from langchain.docstore.document import Document
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

def main():
    # 環境変数読み込み
    load_dotenv()
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key:
        raise ValueError("OPENAI_API_KEY が .env に設定されていません")

    # 埋め込みインスタンス
    embedding = OpenAIEmbeddings(openai_api_key=api_key)

    # train.csvを読み込み
    train_df = pd.read_csv("./Customer_IT_Support/train.csv")

    docs = []
    for _, row in train_df.iterrows():
        metadata = {
            "type": row['type'],
            "queue": row['queue'],
            "priority": row['priority'],
            "answer": row['answer']
        }
        content = (
            f"subject: {row['subject']}\n"
            f"body: {row['body']}\n"
            f"language: {row['language']}\n"
            f"version: {row['version']}"
        )
        docs.append(Document(page_content=content, metadata=metadata))

    # ベクトルDBを構築
    db = FAISS.from_documents(docs, embedding)

    # 保存
    db.save_local("faiss_index")
    print("✅ ベクトルDBを faiss_index に保存しました。")

if __name__ == "__main__":
    main()

テストデータの評価

RAGを利用する際のプロンプトでは、入力に対して意味合いの近い過去事例を3件並べて問い合わせしています。

また、RAGの有無はuse_ragのTrue/Falseで制御してプロンプト文章を変更する形にしています。

import os
import re

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from bert_score import score as bert_score
from dotenv import load_dotenv
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from rouge_score import rouge_scorer
from sklearn.metrics import classification_report, confusion_matrix, f1_score

load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")

embedding = OpenAIEmbeddings(openai_api_key=api_key)
llm = ChatOpenAI(model="gpt-4", temperature=0, openai_api_key=api_key)

db = FAISS.load_local("faiss_index", embedding, allow_dangerous_deserialization=True)

test_df = pd.read_csv("./Customer_IT_Support/test.csv")

# 保存用データ
results = []

scorer = rouge_scorer.RougeScorer(['rougeL'], use_stemmer=True)

use_rag = True

def build_prompt(similar_docs, query_row):
    context = "\n\n".join(
        f"subject: {doc.page_content}\ntype: {doc.metadata['type']}\nqueue: {doc.metadata['queue']}\npriority: {doc.metadata['priority']}\nanswer: {doc.metadata['answer']}"
        for doc in similar_docs
    )

    if use_rag:
        context_block = f"""
    --- 過去の事例 ---
    {context}
    """
    else:
        context_block = """
    --- 過去の事例 ---
    (今回のケースでは過去事例は提供されていません。)
    """

    prompt = f"""
    過去の事例を考慮して、以下の条件に従い、新しい問い合わせに対して最適な type, queue, priority, answer を提案してください。

    - type は以下のいずれかから選んでください:
    Change / Incident / Problem / Request

    - queue は以下のいずれかから選んでください:
    Billing and Payments /
    Customer Service /
    General Inquiry /
    Human Resources /
    IT Support /
    Product Support /
    Returns and Exchanges /
    Sales and Pre-Sales /
    Service Outages and Maintenance /
    Technical Support

    - priority は以下のいずれかから選んでください:
    high / medium / low

    - answer は、body に書かれた内容に対する具体的で適切な回答案を生成してください。

    {context_block}

    --- 新しい問い合わせ ---
    subject: {query_row['subject']}
    body: {query_row['body']}
    language: {query_row['language']}
    version: {query_row['version']}

    出力フォーマットは以下の通りです:
    type: …
    queue: …
    priority: …
    answer: …
    """
    return prompt

def parse_output(text):
    # 正規表現で抽出
    type_ = queue = priority = answer = ""
    for line in text.splitlines():
        if line.lower().startswith("type:"):
            type_ = line.split(":", 1)[1].strip()
        elif line.lower().startswith("queue:"):
            queue = line.split(":", 1)[1].strip()
        elif line.lower().startswith("priority:"):
            priority = line.split(":", 1)[1].strip()
        elif line.lower().startswith("answer:"):
            answer = line.split(":", 1)[1].strip()
    return type_, queue, priority, answer

for idx, row in test_df.iterrows():
    print(f"Processing row {idx+1}/{len(test_df)}...")
    query_text = f"subject: {row['subject']}\nbody: {row['body']}\nlanguage: {row['language']}\nversion: {row['version']}"
    similar_docs = db.similarity_search(query_text, k=3)
    prompt = build_prompt(similar_docs, row)
    pred = llm.invoke(prompt)
    type_, queue, priority, answer = parse_output(pred.content)

    rougeL = scorer.score(row['answer'], answer)['rougeL'].fmeasure
    results.append({
        'true_type': row['type'], 'pred_type': type_,
        'true_queue': row['queue'], 'pred_queue': queue,
        'true_priority': row['priority'], 'pred_priority': priority,
        'true_answer': row['answer'], 'pred_answer': answer,
        'rougeL': rougeL
    })

df_results = pd.DataFrame(results)

# BERTScore 計算
P, R, F1 = bert_score(df_results['pred_answer'].tolist(), df_results['true_answer'].tolist(), lang='en')
df_results['BERTScore'] = F1.numpy()

df_results.to_csv("evaluation_results.csv", index=False)
print("📄 評価結果を evaluation_results.csv に保存しました")

# 分類タスク評価
for col in ['type', 'queue', 'priority']:
    y_true = df_results[f'true_{col}']
    y_pred = df_results[f'pred_{col}']

    labels = sorted(list(set(y_true) | set(y_pred)))
    cm = confusion_matrix(y_true, y_pred, labels=labels, normalize='true')
    present_labels = sorted(set(y_true))
    macro_f1 = f1_score(y_true, y_pred, labels=present_labels, average='macro', zero_division=0)
    print(f"\n{col.upper()} Macro F1: {macro_f1:.4f}")
    print(classification_report(y_true, y_pred))

    plt.figure(figsize=(8,6))
    sns.heatmap(cm, annot=True, fmt='.2f', cmap='Blues', xticklabels=labels, yticklabels=labels)
    plt.title(f"{col.upper()} Confusion Matrix (Macro F1={macro_f1:.4f})")
    plt.ylabel("True")
    plt.xlabel("Predicted")
    plt.tight_layout()
    plt.savefig(f"confusion_matrix_{col}.png")
    plt.close()
    print(f"🖼 Confusion matrix saved: confusion_matrix_{col}.png")

print("✅ 全ての評価が完了しました")

参考までにpoetryで環境を構築して実行するコードを共有します。

github.com


結果と所感

プロンプトのみの場合と、RAGを利用した場合のMacro F1-scoreの比較結果は以下の通りです。

この結果から、RAGを導入することで性能が大きく向上していることが確認できました。

プロンプトのみ RAGの利用
priority 0.379 0.711
queue 0.103 0.664
type 0.346 0.812

また、テストデータに対するROUGE-LとBERTScoreの分布は下図の通りです。

この分布を見ると、RAGの有無にかかわらずBERTScoreは高い傾向(=文章の意味が近い)が見られましたが、プロンプトのみの場合はROUGE-Lが低い傾向(=言い回しが異なる)が顕著でした。

例えば、データセット上の正解が

原文
<name> has written an acknowledgment email regarding the suspected data breach in the hospital's system. 
Understanding the urgency of the situation, they assure that it is being taken seriously. 
To better assist you, they would like to gather more information about the nature of the breach and the systems affected. 
Could you please provide details on the breach and the steps that have been taken so far? 
They would also like to schedule a call to discuss the next steps. They are available for a call at your earliest convenience.

AI翻訳
<name>は、病院のシステムにおけるデータ漏洩の疑いに関する確認メールを送信しました。
状況の緊急性を理解し、真剣に対応していることを保証しています。  
より適切な支援を提供するため、漏洩の性質と影響を受けたシステムに関する詳細な情報を収集したいと考えています。  
漏洩の詳細と、これまでに行われた対応措置についてご提供いただけますでしょうか?  
また、今後の対応について議論するための電話会議をスケジュールしたいと考えています。ご都合の良い時間帯に電話会議にご参加いただけます。

に対して、プロンプトのみの結果は、rougeL=0.284、BERTScore=0.876と文章の意味は近いですが、言い回しが違う以下のような出力になります。

原文
We understand the urgency of the situation and we're sorry to hear about the data breach in your hospital's system. 
Our IT Support team is available and ready to assist you.
We will start by conducting a thorough investigation to understand the extent of the breach and identify the vulnerabilities. 
We will then work on strengthening your security protocols and systems to prevent further breaches. 
We will keep you updated on our progress and next steps. Please ensure all system users are aware of the situation and are on high alert for any suspicious activities. 
We appreciate your patience and cooperation in this matter.

AI翻訳
状況の緊急性を理解しており、貴院のシステムにおけるデータ漏洩の件についてお聞きし、お詫び申し上げます。  
当社のITサポートチームは、ご支援の準備が整っております。
まず、漏洩の程度を把握し、脆弱性を特定するために徹底的な調査を行います。
その後、さらなる漏洩を防ぐため、セキュリティプロトコルとシステムの強化に取り組みます。
進捗状況と今後の対応については、随時お知らせいたします。すべてのシステムユーザーにこの状況を周知し、不審な行動に警戒するようご指示ください。
ご迷惑をおかけしますが、ご理解とご協力のほどよろしくお願いいたします。

一方、RAGを活用した結果は、rougeL=0.819、BERTScore=0.9655と文章の意味も言い回しも正解と近く、以下のような表現になりました。

原文
<name> writing an acknowledgment email regarding the suspected data breach in the hospital's system. 
Understanding the urgency of the situation, we assure you that we take it seriously. 
To better assist you, we would like to gather more information about the nature of the breach and the systems affected. 
Could you please provide us with the details of the breach and the steps you have taken so far? 
Additionally, we would like to schedule a call to discuss the next steps. 
We are available at your earliest convenience for this call.

AI翻訳
<名前> 病院のシステムにおけるデータ漏洩の疑いに関する確認メールを送信しています。
状況の緊急性を理解し、真剣に対応していることをお約束します。  
より適切な支援を提供するため、漏洩の性質と影響を受けたシステムに関する詳細な情報を収集したいと考えております。  
漏洩の詳細と、これまでに行われた対応措置についてご提供いただけますでしょうか?  
さらに、今後の対応について議論するための電話会議をスケジュールしたいと考えております。  
この電話会議は、ご都合の良い日時でご対応いただけますようお願い申し上げます。

この結果を比較すると、全体的な意味合いは一致しているもののプロンプトのみの結果はお詫びしているのに対して、正解文書やRAGを活用した場合は真剣に対応しますというような言い回しになっています。

これらの結果から、自社ドメインに特化したデータが十分に整備されていれば、より適切な回答が得られる可能性が高まることが示唆されます。


おわりに

今回の実験を通じて、RAGが現場の履歴活用に有効である可能性が見えました。 特に、過去の豊富なデータさえ用意できれば、専門的なドメイン知識の「移植」を大規模な学習なしに実現できるのが強みです。

今後は、実際の製造業の履歴データに置き換え、ベクトルDBの更新や評価指標の精緻化を進めながら、より良いソリューションを目指していきたいと考えています。

ここまでお読みいただき、ありがとうございました。


一緒に挑戦してくれる仲間を募集しています

アダコテックでは、技術や事業について気軽に質問することができるカジュアル面談の場を設けております。 今回の記事で少しでも製造業界向けの事業に関心を持った方はぜひお気軽にご応募ください。

herp.careers