前回でOpenSearchによるデータベースを作成することができましたので、ここからこれを利用してRAGを作成していきます。
RAGへの適用
RAGAPI用のプログラム
RAGのプログラムをまとめて記載します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 |
# rag.py # Python # coding: utf-8 import uvicorn import datetime import pytz # import json from fastapi import FastAPI, HTTPException, Request from fastapi.responses import StreamingResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from langchain_community.vectorstores import OpenSearchVectorSearch from langchain.chains import RetrievalQA from langchain.memory.buffer_window import ConversationBufferWindowMemory from langchain.callbacks.base import BaseCallbackHandler # Streamingのためのコールバック from langchain_huggingface.embeddings import HuggingFaceEmbeddings from langchain.prompts import PromptTemplate import asyncio import re import logging from logging.handlers import TimedRotatingFileHandler # ロガーの設定 logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) # フォーマットの設定 fmt = "%(asctime)s %(levelname)s %(name)s :%(message)s" formatter = logging.Formatter(fmt) # TimedRotatingFileHandlerの設定(毎日ログを回転) # ファイル名には日付が追加される log_handler = TimedRotatingFileHandler( filename="./log/logfile", # ベースとなるファイル名 when="midnight", # ログを回転させるタイミング(この場合は毎日) interval=1, # 日ごとに回転 encoding="utf-8", ) # フォーマットをハンドラに設定 log_handler.setFormatter(formatter) # ロガーにハンドラを追加 logger.addHandler(log_handler) from langchain_community.llms.llamacpp import LlamaCpp # モデルのパス model_path = "./llm/model/gemma-2-2b-jpn-it-Q4_K_M.gguf" # モデルの設定 logger.info(f"モデルの作成 モデル名: {model_path}") # 参考: https://api.python.langchain.com/en/latest/llms/langchain_community.llms.llamacpp.LlamaCpp.html llm_llama_cpp = LlamaCpp( model_path=model_path, n_ctx=0, # コンテキストの長さ(=0の場合はモデルから?) n_gpu_layers=-1, # gpuに処理させるlayerの数 stop=["Question:", "Answer:", "質問:", "回答:"], # 停止文字列 ) print("APIサーバーの設定開始") logger.info("APIサーバーの設定開始") # APIサーバーの設定 app = FastAPI( title="LangChain Server", version="1.0", description="A simple api server using Langchain's Runnable interfaces", ) print("embeddingsの設定開始") logger.info("embeddingsの設定開始") # huggingfaceを使用する場合 # embeddingsにはfileinsert.pyで使用したモデルを使う embeddings_model_name = "intfloat/multilingual-e5-large" embeddings_model_path = f"D:/codes/python/rag-opensearch/embeddings/models/{embeddings_model_name}" # embeddings = HuggingFaceEmbeddings(model_name=embeddings_model_path) # CPUを使用する embeddings = HuggingFaceEmbeddings(model_name=embeddings_model_path, model_kwargs={"device": "cuda:0"}) # GPUを使用する # END huggingfaceを使用する場合 logger.info("OpenSearchVectorSearchインスタンス作成") print("OpenSearchVectorSearchインスタンス作成") # OpenSearchクライアントの設定 # インデックスragの設定 opensearch_client = OpenSearchVectorSearch( opensearch_url="http://localhost:9200", embedding_function=embeddings, http_auth=("admin", '"tG7$9!5A"'), # 認証情報を適切に設定 use_ssl=True, http_compress=True, verify_certs=False, index_name="rag", timeout=30000, ) # ストリーミングコールバックを定義 class MyStreamingCallback(BaseCallbackHandler): def __init__(self): self.queue = [] self.is_end = False print("__init__") # チェーンが開始されたときに呼び出される def on_chain_start(self, serialized, inputs, **kwargs): print("on_chain_start") # llmが開始されたときに呼び出される(on_chain_startの後に呼び出される) def on_llm_start(self, serialized, prompts, **kwargs): print("on_llm_start") # queueを初期化 self.queue = [] # llmが終了したときに呼び出される def on_llm_end(self, error, **kwargs): self.is_end = True print("on_llm_end", self.queue) # 最初と最後のスペースや改行を削除する self.queue = self.rm_start_end_space(self.queue) # トークンが来たらqueueに追加 async def on_llm_new_token(self, token, **kwargs): self.queue.append(token) # queueを返す def get_stream(self): return self.queue # 最後のスペースや改行を削除する def rm_start_end_space(self, tokens): # リストの最後の要素が空白, スペース, 改行の連続の場合、削除して再帰 if tokens[-1] == "" or tokens[-1] == " " or re.search("^\n+$", tokens[-1]): # print("tokens[-1]", tokens[-1]) tokens.pop(-1) # tokensの長さが1になったら終了 if len(tokens) == 1: return ["すみません、わかりませんでした。"] self.rm_start_end_space(tokens) # リストの最初の要素が空白, スペース, 改行の連続の場合、削除して再帰 if tokens[0] == "" or tokens[0] == " " or re.search("^\n+$", tokens[0]): # print("tokens[-1]", tokens[-1]) tokens.pop(0) # tokensの長さが1になったら終了 if len(tokens) == 1: return ["すみません、わかりませんでした。"] self.rm_start_end_space(tokens) return tokens ###################################################################### RetrievalQAの設定 # プロンプトテンプレートの定義(日本語で回答するようにする) question_prompt_template = """ここからの会話は全て日本語で行われる。 あなたはAIです。 メタ的なコメントをしないようにしてください。 ###や**や```で囲まれたコメントは避けてください。 #会話履歴: {chat_history} #コンテキスト情報: {{context}} 質問: {{question}} 回答:""" # プロンプトの設定 QUESTION_PROMPT = PromptTemplate( template=question_prompt_template, # プロンプトテンプレートをセット input_variables=["context", "question"], # プロンプトに挿入する変数 ) # プロンプトテンプレートの設定 question_prompt_template = question_prompt_template.format(chat_history="チャット履歴が入ります") # プロンプトテンプレートを返す関数 def return_question_prompt_template(): return """ここからの会話は全て日本語で行われる。 あなたはAIです。 メタ的なコメントをしないようにしてください。 ###や**や```で囲まれたコメントは避けてください。 #会話履歴: {chat_history} #コンテキスト情報: {{context}} 質問: {{question}} 回答:""" # テンプレートに挿入する変数を指定してプロンプトを設定 def set_rag_prompt(question_prompt_template): return PromptTemplate(template=question_prompt_template, input_variables=["chat_history", "context", "question"]) # メモリー(会話履歴)の設定 memory = ConversationBufferWindowMemory( memory_key="chat_history", # メモリーのキー名 input_key="query", # 入力のキー名 output_key="result", # 出力ののキー名 k=5, # 保持する会話の履歴数 return_messages=True, # チャット履歴をlistで取得する場合はTrue human_prefix="私", ) # メモリーの初期化 def get_init_memory(): return ConversationBufferWindowMemory( memory_key="chat_history", # メモリーのキー名 input_key="query", # 入力のキー名 output_key="result", # 出力ののキー名 k=5, # 保持する会話の履歴数 human_prefix="私", ) # クライアントのメモリーを記録する配列 client_memories = [] ###################################################################### # apiのメイン関数 async def opensearch_llm_qa(question): # qinfoのRetrieverの設定 retriever = opensearch_client.as_retriever( search_kwargs={ "text_field": "content", # 検索するフィールド "k": 7, # 類似度の高いものをいくつ取得するか "score_threshold": 1, # 類似度の閾値 }, ) # コールバックインスタンスを作成 callback = MyStreamingCallback() # クライアントのメモリーを削除する # 作成時から時間が経過したらメモリーを削除する global client_memories new_client_memories = [] for client_memory in client_memories: # 10分以上経過したら削除 if (datetime.datetime.now(pytz.timezone("Asia/Tokyo")) - client_memory["created_at"]).seconds <= 600: new_client_memories.append(client_memory) else: print("メモリーを削除します", client_memory["user_id"]) logger.info(f"10分以上経過したので、メモリーを削除します{client_memory['user_id']}") client_memories = new_client_memories # クライアントのメモリーを取得する memory = None print("client_memories", client_memories, question.user_id) # user_idが一致するメモリーを取得 client_memories_search = [item for item in client_memories if item["user_id"] == question.user_id] # user_idがなければ初期化 if client_memories_search == []: memory = get_init_memory() print("メモリーが見つからなかったので初期化します") client_memories.append( dict(user_id=question.user_id, memory=memory, created_at=datetime.datetime.now(pytz.timezone("Asia/Tokyo"))) ) # user_idがあれば取得 else: print("メモリーが見つかりました") memory = client_memories_search[0]["memory"] print("client_memories", client_memories) # プロンプトテンプレートを取得 question_prompt_template = return_question_prompt_template() # プロンプトに会話履歴をセット question_prompt_template = question_prompt_template.format( chat_history=memory.load_memory_variables({})["chat_history"] ) # プロンプトをセット QUESTION_PROMPT = set_rag_prompt(question_prompt_template) # RAGの設定 qa = RetrievalQA.from_chain_type( llm=llm_llama_cpp, # LLMの設定 chain_type="stuff", # チェインタイプ(参考:https://note.com/mega_gorilla/n/n6f46fc1985ca) retriever=retriever, # opensearchのretrieverをセット return_source_documents=True, # 参照ファイルを返す memory=memory, # メモリーをセット chain_type_kwargs={ "prompt": QUESTION_PROMPT, "verbose": True, }, # プロンプトをセット ) logger.info(f"RetrievalQA実行 ユーザーID: {question.user_id} 質問: {question.user}") # RAGを実行 result = qa.invoke({"query": question.user}, {"callbacks": [callback]}) # print("result", result["result"]) logger.info(f"RetrievalQA結果 ユーザーID: {question.user_id} 回答: {result['result']}") # メモリーをグローバル変数に保存 client_memories_search = [i for i, item in enumerate(client_memories) if item["user_id"] == question.user_id] # user_idがなければ追加 if client_memories_search == []: client_memories.append( dict(user_id=question.user_id, memory=memory, created_at=datetime.datetime.now(pytz.timezone("Asia/Tokyo"))) ) # user_idがあれば更新 else: index = client_memories_search[0] client_memories[index]["memory"] = memory # 参照を取得する references = [doc.metadata for doc in result["source_documents"]] # print("references", references) response_references = [] for reference in references: print("reference", reference["path"], reference["score"]) # 全て参考にする response_references.append(dict(path=reference["path"], score=reference["score"])) # 結果をストリーミング出力する(そのように見せているだけ) for chunk in callback.get_stream(): yield chunk await asyncio.sleep(0.02) # metadataにあれば出力 if response_references: yield "\n\n" yield "参照ファイル: " for i, chunk in enumerate(response_references): yield "\n" yield f'{i+1}. スコア: {chunk["score"]:.5f} パス: {chunk["path"]} ' await asyncio.sleep(0.02) class ChatRequest(BaseModel): user_id: int = -1 user: str system: str class Config: orm_mode = True class ChatResponse(BaseModel): response: str # アクセスを許可するオリジン(URLのようなもの)を設定 origins = [ "http://127.0.0.1:8000", ] app.add_middleware( CORSMiddleware, allow_origins=origins, # 認証情報のアクセスを許可 allow_credentials=True, # 全てのリクエストメソッドを許可 allow_methods=["GET", "POST"], # アクセス可能なレスポンスヘッダーを設定 allow_headers=["*"], ) # ChatAPI # コマンドプロンプトで呼び出す例 # curl -X POST "http://localhost:8100/chat" -H "Content-Type: application/json" -d @test.json @app.post("/chat", response_model=ChatResponse) async def chat(chat_request: ChatRequest, request: Request): client_host = request.client.host logger.info(f"Connect ChatAPI HostIP: {client_host}") try: # APIはstreaming出力 return StreamingResponse(content=opensearch_llm_qa(chat_request), media_type="text/event-stream") except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # HealthCheckAPI # コマンドプロンプトで呼び出す例 # curl -X GET "http://localhost:8100/health" @app.get("/health") def health_check(request: Request): client_host = request.client.host logger.info(f"Connect HealthCheckAPI HostIP: {client_host}") return {"status": "ok"} def main(): logger.info("apiサーバー起動") # localhostの8100でサーバーを起動 uvicorn.run("rag:app", host="localhost", port=8100, reload=True) if __name__ == "__main__": main() |
足りないライブラリは適宜インストールしてください。
今回llmの呼び出しにllamacppを使用しますが、pip install llama-cpp-python
は結構時間がかかります。
今回紹介するRAGですが、私なりに社内RAGとして使うのに汎用性があるように、
少し工夫していて、そのせいでコードが複雑になっております。変な部分は修正して使っていただけると幸いです。
opensearch-pyのコードの変更
また、 rag.py
以外にライブラリ内のコードを変更するポイントが一つあります。
以下で記載していますが、AIからの返答に検索のスコアを表示する関係で、opensearch-pyの中身を少し変更します。これをしないとスコアを返答してくれません。
変更するファイルは、
"D:\codes\python\rag-opensearch\.venv\Lib\site-packages\langchain_community\vectorstores\opensearch_vector_search.py"
で、変更部分は750行あたりの similarity_search
関数です。vscodeを使っている場合は rag.py
のOpenSearchのクライアント設定の部分の OpenSearchVectorSearch
を選択し、F12でジャンプすると対象のファイルに移動できます。
最後のreturnの部分を以下のように変更します。
1 2 3 4 5 6 7 8 9 10 |
# opensearch_vector_search.py # 変更前 # return [doc[0] for doc in docs_with_scores] # 変更後(resultにscoreを含める) result = [] for doc, score in docs_with_scores: doc.metadata['score'] = score result.append(doc) return result |
工夫したところ
社内RAGとして使うのに汎用性があるように工夫した点をまとめておきます。
- チャット形式にする
ただ1ラリーで返答するだけでは面白くないので、チャット形式で過去の会話も参照するようにしています。ConversationBufferWindowMemory
の設定で5会話文までメモリーに保存するようになっています。
小さいLLMモデルを使用しており、コンテキスト長に制限(8192)があるため、とりあえず5会話文にしています。OpenSearchで文章を400文字でチャンクに分けて、追加しているのもそれが理由です。 - ユーザーごとに分けて会話するようにする
APIとして利用する場合は、いろんなユーザーが使用することが想定されるため、APIサーバに保存するメモリーはユーザーごとに分ける必要があります。
そのため、APIリクエストbodyにuser_id
を入れるようにして、user_id
で参照するメモリーを探すようにしています。メモリーが増えるのを防ぐために、時間制限で一定時間でメモリーを削除するようにしています。 - 参考文献を表示する
RAGの回答で、AIが参考にした参考文献を表示することで、あとで人間がその資料を見に行きやすくしました。同時にスコアも表示することで、どのくらい類似度があるのかも明確にするようにしました。 - レスポンスChatGPTみたいにする
これは自己満足ですが、ChatGPTを使用していると、AIからの返答は文字がちょっとずつ表示されると思います。これはLLMが1トークンずつ予測して出力しているせいだと思いますが、せっかくなので、それを実現したいと思いました。ライブラリの機能として、AIからのすべて回答を待つ前に、少しずつ表示する方法があるみたいなのですが、私のコードではうまくいきませんでした。そこで、なくなくAIからの回答をすべて取得してから、1トークンずつAPIからレスポンスを返すようにしています。参考として次の記事でReactでチャットアプリ作ったときの表示方法についても記載しておきます。
プログラムの全体の流れは基本的には以下のようになると思います。
- RAG用APIの起動
- LLMモデルの読み込み
- OpenSearchの接続
- OpenSearchで検索
- 検索結果を利用してRAGを実行
RAGの実行と検証
RAGの実行
すべて準備できたら、いよいよRAGを実行します。
まずOpenSearchとRAG用のAPIを起動します。
前回の通りOpenSearchは opensearch-windows-install.bat
から起動できます。
RAG用APIはコマンドプロンプトなどから、仮想環境に入り、
py rag.py
で起動できます。
RAG用のAPIが立ち上がるのは少し時間がかかりますが、立ち上がると、
INFO: Application startup complete.
と表示されます。
検索の実行は以下のようにjsonを参照することもできますし、そのまま記述することもできます。
1 2 3 4 5 |
# コマンドプロンプト # json参照 curl -X POST "http://localhost:8100/chat" -H "Content-Type: application/json" -d @test.json # 直接記述 curl -X POST "http://localhost:8100/chat" -H "Content-Type: application/json" -d "{\"user_id\":1,\"user\":\"こんにちは私の名前はあしぺんです。あなたの名前は?\", \"system\": \"Please transrate your response in Japanese. And must answer only Japanese response.\"}" |
直接記述を見ればわかりますが、jsonの中身は以下のようになっています。
1 2 3 4 5 6 |
# test.json { "user_id": 1, "user":"こんにちは。私の名前はあしぺんです。あなたの名前は?", "system": "Please transrate your response in Japanese. And must answer only Japanese response." } |
user_id
はユーザーID、user
は質問内容、system
は質問内容以外に会話に含めるものです。
今回 system
には「日本語でおk」という内容を入れています。今使っているモデルが日本語特化のモデルなので、必要ないかもしれませんが、念の為入れています。
以上でRAGが完成しました。
回答例
では、試しに回答例を見てみましょう。
通常の会話
まず、RAGに関係ない普通の質問をしてみます。
- 質問:熱中症対策を5つ教えて
- 回答:1. 十分な水分補給をする
2. 日陰で休息を取る
3. 熱中症予防グッズ(帽子、サングラスなど)を活用する
4. 暑さを感じたら早めに病院に行く
5. こまめに体温を計測し、高熱の場合は適切な治療を受ける
ちゃんと日本語でそれっぽいことを教えてくれます。ローカル環境でここまでできるのは感動しますよね。
会話を覚えているか
次に、過去の会話内容を参照して、チャットができるか確認します。
- 質問:こんにちは私の名前はあしぺんです。あなたの名前は?
- 回答:私の名前は、山田です
- 質問:今日はいい天気です。そちらは?
- 回答:はい、今日はいい天気ですね。
- 質問;私の名前を答えてください。
- 回答:あしぺ
参照ファイル以下は、参考にしたファイルを示しています。
まあ、私の名前に一文字足りない気がしますが、ちゃんと私のことをおぼえているようです!このときのプロンプトはどうなっているか見てみます。
上図のように、プロンプトに以前の会話の内容が含まれているため、私の名前を答えられたわけですね。
コンテキスト情報にはOpenSearchで検索した結果が入っています。最後の質問「私の名前を答えてください」に対する検索結果だと思いますが、回答では無視されています。つまり、たぶんファイルを参考にしつつ、ファイルの内容は関係ないと判断して、私と会話していることになります。すごいですね!
このあたりの調整は opensearch_client.as_retriever
のscore_threshold
値を調整することで、必要ない情報まで取得するかどうか変更できるかもしれません。
しかし、あまり高くしすぎると、RAGで必要になる情報まで取得しなくなるので、難しいところです。うまくいかないようであれば、OpenSearchに保存されているデータ自体に問題があるかもしれません。
この会話に、どのくらいのレスポンス時間がかかるか動画を載せておきます。
1レスポンスあたり十数秒くらいでしょうか、GPUを使ってこれなので、正直遅いと言われたら遅いですが、モデルも比較的軽量のものなので、速くするにはスペックを上げるしかないですかね。
RAGの有無による違い
次に、OpenSearchを使ったRAGについて、RAGを使用しない場合と、使用した場合でどのくらい違うのか見てみます。
太宰治の走れメロスについての質問です。
- 質問:メロスが激怒した理由は?
- RAGなしの回答:メロスが激怒した理由は、アフロディーテに、自分の父親であるゼウスを貶める言葉をかけているからです。
- RAGありの回答:メロスが激怒した理由は、暴君ディオニスが「人を信ずる事が出来ぬ」と言ったこと。
RAGありのほうが、作品の内容を引用し、それっぽい回答をしていると思います。
アフロディーテはメロス島という島で出土した女神像らしいです。RAGがない場合は、作品の走れメロスについてはおそらく知らない?ので、一般的な知識から回答を作成したみたいです。
RAGありについて、追加で質問しました。
- 追加質問:暴君ディオニスが人を信じられない理由は?
- RAGありの回答:暴君ディオニスは、メロスが「人の心を疑うのは、最も恥ずべき悪徳だ」と言ったことを理由に人への不信感を抱いている。
作品中の言葉は使っているけどなんとちがう?
RAGありについて、別の回答例を見てみます。宮沢賢治の銀河鉄道の夜についての質問です。
- 質問:カムパネルラは誰を助けようとしましたか?
- 回答:カムパネルラはザネリを助けようとしました。
- 追加質問:その後ザネリとカムパネルラはどうなりましたか?
- 回答:ザネリは舟の上から鳥うりのあかりを水の流れる方へ押して、舟がゆれたことで水へ落ちてしまった。
時系列が間違っているように思います。やはり、400文字ごとにチャンクを分けたのが影響して、文章ごとの順番が理解できていないのでしょうか。
一応チャンク分けの際にオーバーラップはさせているので、多少の時系列はわかると思いますが、あまりうまく行っていないかもしれません。
というわけで、今回作成したRAGの回答例は以上です。
まとめ
0円でRAGを実現する方法を紹介しました。私のPCのスペックの制限で、今回使ったgemma2-2b程度でやっとでしたが、
オープンソースには、Metaのllama3.1を初めとしてもっと精度が良いものがたくさんあります。ローカルで動かす限界があると思いますが、そういうものを使えば精度は上がるのかもしれません。
また、RAGにつかうデータベースですが、今回使うOpenSearchだけでなく、類似度の計算ができるものであれば、他にも色々あると思います。
私はOpenSearch以外にもFaissを試しましたが、なぜか重かったし、OpenSearchはそれ単体でも検索エンジンとして十分優れていると思ったので、今回はOpenSearchを使いました。
RAGの開発には、LangChainというライブラリを使っており、ここに書いてあるものであれば色々使えると思いますので、試してみるのもいいかもしれません。
LLMモデルを使うにあたって、今回はllama_cppを使用しましたが、他にもOllamaも試しました。しかし、これも重かったので、今回はllama_cppにしました(使い方間違ってたかもしれません)。
Ollamaはすごく使いやすく、プログラムを書かなくても簡単にローカルでLLMを体験できるので、試してみると面白いと思います。
あと、やはり検索データそのものに問題がある場合は、精度が落ちると思います。今回はもともと、青空文庫で整形されてあったデータだったため、
そのまま使用してもそれなりの検索結果だったと思います。しかし、これでも文章の初めの記号の説明や文末の作品と関係のない文章はノイズになっている可能性はあります(下図のような)。
本来であればこういった情報を整理したうえで除外し、データベースに追加する必要があると思います。
社内のデータであればデータの整理はより難しく思います。
テキスト形式であればまだ整形しやすいですが、エクセル形式のファイルなどは整形するのにかなりの工夫が必要であるように思います。
いくらモデルの精度が良くても検索データがしょぼければ、生成される情報もとんちんかんになる可能性があります。
他にも注意点はあるかもしれませんが、今回はこのあたりにしたいと思います。
別の記事でこのRAGAPIを使ったチャットアプリの作成について少し触れたいと思います。
コメント