質問するログイン新規登録

Q&A

解決済

1回答

507閲覧

RAG型チャットボットで誤回答を訂正した内容を蓄積し賢くする仕組みは可能か?(Gemini API・画像入力あり)

yukki-1227

総合スコア53

Flask

FlaskはPython用のマイクロフレームワークであり、Werkzeug・Jinja 2・good intentionsをベースにしています。

AI(人工知能)

AI(人工知能)とは、言語の理解や推論、問題解決などの知的行動を人間に代わってコンピューターに行わせる技術のことです。

機械学習

機械学習は、データからパターンを自動的に発見し、そこから知能的な判断を下すためのコンピューターアルゴリズムを指します。人工知能における課題のひとつです。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

1クリップ

投稿2026/04/08 06:33

編集2026/04/09 02:40

0

1

実現したいこと

完全無料かつクレジットカード登録なしで、業務用チャットボットを構築したいと考えています。

対象は外部案件で、業務マニュアルやQ&Aが存在します。
これらをもとに、人手ではなくAIがチャット形式で回答できる仕組みを作りたいです。

現在はGeminiの無料APIを使用しており、
マニュアルやQ&AはSQLiteなどのデータベースに登録し、
検索(キーワード検索+簡易的な意味検索)を行ったうえでAIに渡す構成にしています。

そのため、
・誤回答を訂正した内容を蓄積し、次回以降の回答精度を上げる仕組みはあるのか
・完全無料でそれに近い構成を実現する方法があるのか
を知りたいです。

発生している問題・分からないこと

この案件では伝票画像などの画像情報が非常に重要で、
回答は「質問文」ではなく「画像の内容」を基準に判断する必要があります。
ただし、OCRでは読み取り精度に限界があり、正確にテキスト化できないケースも多いため画像フォルダにマニュアル画像を入れてAPIに渡しています。

さらに問題として、
質問者の前提が誤っているケース(例:対象外と書かれていないのに対象外だと思い込んでいる)が多く、
AIには単なる回答ではなく「正誤判定」もさせたいと考えています。

現状では、
・質問+画像を送信してもマニュアルと異なる回答が返ってくる
・一般知識に引っ張られた誤回答が発生する
という課題があります。

また、Gemini APIはリクエスト単位で完結するため、
誤回答に対して「それは違う」と訂正しても、その内容が蓄積されず次回に活かされません。現状、手動で調べて回答する方が精度が高く現段階のこのアプリは全く使い物になりません。

プロンプトの問題でしょうか?
マニュアルは20数ページ、Q&Aの数は100個程度あります。

よろしくお願いいたします。またより良い方法があれば教えてください。

該当のソースコード

search.py

1import sqlite3 2 3 4# ===== マニュアル(全件取得)===== 5def search_knowledge(query): 6 7 conn = sqlite3.connect("db.sqlite") 8 cur = conn.cursor() 9 10 cur.execute(""" 11 SELECT content, answer, page 12 FROM knowledge 13 WHERE type = 'manual' 14 """) 15 16 rows = cur.fetchall() 17 conn.close() 18 19 manuals = [] 20 for row in rows: 21 manuals.append({ 22 "content": row[0], 23 "answer": row[1], 24 "page": row[2], 25 }) 26 27 return manuals 28 29 30# ===== 事例(LIKE検索で取得)===== 31def search_cases(query, limit=5): 32 33 conn = sqlite3.connect("db.sqlite") 34 cur = conn.cursor() 35 36 cur.execute(""" 37 SELECT question, answer, keywords, image_path 38 FROM knowledge 39 WHERE type = 'case' 40 AND ( 41 question LIKE ? 42 OR keywords LIKE ? 43 ) 44 LIMIT ? 45 """, ('%' + query + '%', '%' + query + '%', limit)) 46 47 rows = cur.fetchall() 48 conn.close() 49 50 cases = [] 51 for row in rows: 52 cases.append({ 53 "question": row[0], 54 "answer": row[1], 55 "keywords": row[2], 56 "image": row[3], 57 }) 58 59 return cases

app.py

1from flask import Flask, request, render_template 2import os 3import requests 4import base64 5from dotenv import load_dotenv 6from search import search_knowledge 7 8load_dotenv() 9 10app = Flask(__name__) 11 12GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") 13 14# ===== Gemini呼び出し ===== 15def ask_gemini(prompt, image_bytes=None): 16 17 url = f"https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent?key={GEMINI_API_KEY}" 18 19 parts = [{"text": prompt}] 20 21 # 画像がある場合 22 if image_bytes: 23 encoded = base64.b64encode(image_bytes).decode() 24 parts.append({ 25 "inline_data": { 26 "mime_type": "image/png", 27 "data": encoded 28 } 29 }) 30 31 payload = { 32 "contents": [ 33 { 34 "parts": parts 35 } 36 ] 37 } 38 39 response = requests.post(url, json=payload) 40 41 if response.status_code != 200: 42 return f"Geminiエラー: {response.text}" 43 44 data = response.json() 45 46 try: 47 return data["candidates"][0]["content"]["parts"][0]["text"] 48 except: 49 return "回答生成に失敗しました" 50 51 52# ===== プロンプト作成(RAGの核)===== 53def build_prompt(manuals, question): 54 55 manual_text = "" 56 57 for m in manuals: 58 manual_text += f""" 59【ルール】 60ページ: {m['page']} 61内容: {m['content']} 62処理: {m['answer']} 63""" 64 65 prompt = f""" 66あなたはこの案件専用の処理判定AIです。 67基本的には以下のルールに従って判断してください。 68 69==================== 70【ルール一覧】 71{manual_text} 72==================== 73 74【質問】 75{question} 76 77【重要ルール】 78・マニュアルのルールを最優先。世間のルールと相違している場合はマニュアルのルールを優先。 79・質問者の前提が正しいか必ず検証する 80・マニュアルと矛盾している場合は「誤り」と明確に指摘する 81・画像に書かれている内容も必ず考慮する。 82・判断できる場合は必ず答える 83・どうしても不明な場合のみ「該当情報なし」とする 84 85 86【出力形式(必須)】 87① 結論: 88② 理由: 89③ 根拠ルール(ページ or 事例): 90 91※③は必ずページ番号または事例内容を明記すること 92※該当なしの場合は「該当ルールなし」と書く 93""" 94 95 return prompt 96 97 98# ===== メイン処理 ===== 99@app.route("/ask", methods=["POST"]) 100def ask(): 101 102 try: 103 question = request.form.get("question") 104 file = request.files.get("image") 105 106 if not question: 107 return render_template("index.html", answer="質問を入力してください", results=[]) 108 109 image_bytes = file.read() if file else None 110 111 # ===== ① ハイブリッド検索 ===== 112 results = search_knowledge(question) 113 114 # ===== ② プロンプト生成 ===== 115 prompt = build_prompt(results, question) 116 117 # ===== ③ AI回答 ===== 118 answer = ask_gemini(prompt, image_bytes) 119 120 # ===== ④ 画面表示 ===== 121 return render_template( 122 "index.html", 123 answer=answer, 124 results=results 125 ) 126 127 except Exception as e: 128 return render_template("index.html", answer=f"エラー: {str(e)}", results=[]) 129 130 131# ===== トップページ ===== 132@app.route("/") 133def home(): 134 return render_template("index.html") 135 136 137if __name__ == "__main__": 138 # ローカルPC上にWebサーバーを立てる 139 app.run(debug=True)

pdf_import.py

1import sqlite3 2 3conn = sqlite3.connect("db.sqlite") 4cur = conn.cursor() 5 6# ===== カラム追加(なければ)===== 7try: 8 cur.execute("ALTER TABLE knowledge ADD COLUMN type TEXT") 9except: 10 pass 11 12try: 13 cur.execute("ALTER TABLE knowledge ADD COLUMN question TEXT") 14except: 15 pass 16 17 18# ===== マニュアル =====外部案件のためマニュアルや事例の内容はダミーにしてあります 19manual_data = [ 20 { 21 "content": "赤線や青線がない場合の処理", 22 "keywords": "赤線,青線, 23 "answer": "赤線や青線がなければそのままSaveする。", 24 "image_path": "3.png", 25 "page": 3, 26 "section":"(1) 処理手順" 27 } 28] 29 30 31# ===== 事例 ===== 32case_data = [ 33 { 34 "question": "警告マークが出ている場合は対象外ですか?", 35 "answer": "対象外ボタンを押してください", 36 "image_path": "receipt_sekiryō.png", 37 "keywords": "警告","対象外" 38 } 39] 40 41 42# ===== マニュアル登録 ===== 43for m in manual_data: 44 cur.execute(""" 45 INSERT INTO knowledge 46 (type, question, content, keywords, answer, image_path, source, page, section) 47 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 48 """, ( 49 "manual", 50 None, 51 m.get("content"), 52 m.get("keywords"), 53 m.get("answer"), 54 m.get("image_path") or m.get("image"), # ←ここ重要 55 m.get("source", "manual"), 56 m.get("page"), 57 m.get("section") 58 )) 59 60 61# ===== 事例登録 ===== 62for c in case_data: 63 cur.execute(""" 64 INSERT INTO knowledge 65 (type, question, content, keywords, answer, image_path, source, page, section) 66 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 67 """, ( 68 "case", 69 c.get("question"), 70 None, 71 c.get("keywords"), 72 c.get("answer"), 73 c.get("image_path"), 74 "case", 75 None, 76 None 77 )) 78 79 80conn.commit() 81conn.close() 82 83print("登録完了")

index.html

1//文字数制限のため省略 2

migrate_to_chroma.py

1import sqlite3 2import chromadb 3import requests 4import os 5from dotenv import load_dotenv 6 7load_dotenv() 8GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") 9 10def get_embedding(text: str): 11 url = f"https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key={GEMINI_API_KEY}" 12 13 payload = { 14 "model": "models/text-embedding-004", 15 "content": {"parts": [{"text": text}]} 16 } 17 18 res = requests.post(url, json=payload) 19 res.raise_for_status() 20 21 return res.json()["embedding"]["values"] 22 23 24def migrate(): 25 26 conn = sqlite3.connect("db.sqlite") 27 cur = conn.cursor() 28 29 cur.execute(""" 30 SELECT id, type, question, content, answer, page, image_path 31 FROM knowledge 32 """) 33 34 rows = cur.fetchall() 35 conn.close() 36 37 chroma = chromadb.PersistentClient(path="./chroma_db") 38 39 collection = chroma.get_or_create_collection( 40 name="knowledge", 41 metadata={"hnsw:space": "cosine"} 42 ) 43 44 for row in rows: 45 id_, type_, question, content, answer, page, image = row 46 47 # ★ embedding対象(ここ重要) 48 text = f"{question or ''} {content or ''} {answer or ''}".strip() 49 50 if not text: 51 continue 52 53 print("Embedding:", text[:50]) 54 55 embedding = get_embedding(text) 56 57 collection.add( 58 ids=[str(id_)], 59 embeddings=[embedding], 60 documents=[text], 61 metadatas=[{ 62 "type": type_ or "", 63 "question": question or "", 64 "content": content or "", 65 "answer": answer or "", 66 "page": str(page) if page else "", 67 "image": image or "" 68 }] 69 ) 70 71 print("✅ 移行完了") 72 73 74if __name__ == "__main__": 75 migrate() 76 77 78 79

試したこと・調べたこと

  • teratailやGoogle等で検索した
  • ソースコードを自分なりに変更した
  • 知人に聞いた
  • その他
上記の詳細・結果

LIKE検索のみだと、質問者が的外れなことを言っている場合、データベースから検索されないので意味検索も含めた。マニュアルの大量にある画像全てをリクエストごとにAIに送ると重くなり精度も落ちるため質問文や質問画像から近い場合のみ該当するデータベース情報を引き渡す方が良いと知った。

補足

特になし

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

hiroki-o

2026/04/08 10:07

そもそも、無料APIに投げていい業務データなのですか? 学習されてしまうし、他の人の回答に出るかもしれません。
yukki-1227

2026/04/08 11:19

質問ありがとうございます。 はい、そこは確認済みで許可が出ています
yukki-1227

2026/04/08 11:25

無料APIにこだわりはありませんので、何らかの学習モデルを使用するなどより良い方法があればご教示頂けると幸いです。
guest

回答1

0

ベストアンサー

シンプルなRAGで正しい回答を返さない原因は、大きく次の2つになるかと思います。

LLMに正しい情報を渡すことができていない

LLMに正しい情報を渡せないとLLMは正しい回答ができません。この場合は、手前の検索部分を改善する必要があります。また、余計な情報を渡してしまうことも精度低下につながるので、なるべく正しい情報のみに絞る必要があります。

正しい情報を渡せているがLLMが正しく解釈しない

正しい情報を渡せている場合は、プロンプトの調整や、より高性能なモデルへの切り替えなどが手段になるかと思います。

なお、RAGの精度改善の方法はいろいろ研究されています。「Advanced RAG 」でググってもらうと、いろいろな解説がみつかるかと思いますので参考にしていただけるとよいかと思います。

あと、

Gemini APIはリクエスト単位で完結するため、誤回答に対して「それは違う」と訂正しても、その内容が蓄積されず次回に活かされません。

こちらについては、会話履歴を渡せば「それは違う」と訂正したあとの会話も続けられます。過去のやりとりを毎回送り直すイメージです。こちらが参考になりますでしょうか。
https://ai.google.dev/api?hl=ja#multi-turn-conversations

参考になりましたら幸いです。

投稿2026/04/08 15:11

編集2026/04/08 15:18
segavvy

総合スコア1047

yukki-1227

2026/04/09 02:34

ご回答ありがとうございます!助かります。 履歴を送れるということは知りませんでした。改善出来るように試行錯誤してみます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.25%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問