Google Colabで日本語の大規模言語モデル(LLM)とFAISSを使って検索エージェントを構築するためのステップバイステップガイドです。この手順を実行することで、日本語のクエリに対して適切な回答を提供するエージェントを構築できます。
必要なライブラリのインストール
まず、必要なライブラリをインストールします。これには、Langchain、DuckDuckGo Search、Sentence Transformers、FAISS-GPU、およびLlama-CPP-Pythonが含まれます。
!pip install langchain langchain_community sentence-transformers faiss-gpu
!CMAKE_ARGS="-DLLAMA_CUBLAS=on" FORCE_CMAKE=1 pip install llama-cpp-python
モデルのダウンロード
次に、Hugging Faceから日本語のLLMモデルをダウンロードします。
LLMの準備
LangchainのLlamaCppを使ってLLMを準備します。
from langchain.llms import LlamaCpp
llm = LlamaCpp(
model_path="ELYZA-japanese-Llama-2-7b-fast-instruct-q4_K_M.gguf",
n_ctx=4096,
n_gpu_layers=40,
)
埋め込みとベクトルストアの設定
次に、Hugging Faceの埋め込みモデルとFAISSを使って、検索用のベクトルストアを構築します。
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS
corpus = [
"太郎は教師です",
"花子の父は太郎です",
"花子の母は月子です",
"月子は医者です",
"花子は10歳です",
]
embedding = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large")
vectorstore = FAISS.from_texts(corpus, embedding)
エージェントの設定
Langchainを使って、カスタムのプロンプトテンプレートと出力パーサーを設定し、エージェントを構築します。
from langchain import LLMChain
from langchain.agents import Tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser
from langchain.schema import AgentAction, AgentFinish
from langchain.prompts import StringPromptTemplate
from typing import List, Union
import re
tools = [
Tool(
name="Search",
func= lambda query: vectorstore.similarity_search(query, top_k=1)[0].page_content,
description="検索で質問したいときに便利です"
),
]
tool_names = [tool.name for tool in tools]
template = """次の質問にできる限り答えてください。次のツールにアクセスできます。:
{tools}
次の形式を使用します。
Question: 回答する必要がある入力質問
Thought: 何をすべきかを常に考えるべきだ
Action: 実行するアクションは、[{tool_names}] のいずれか
Action Input: アクションへの入力
Observation: 行動の結果
... (この Thought/Action/Action Input/Observation は 複数回繰り返すことができます)
Thought: 今、最終的な答えが分かりました
Final Answer: 元の入力質問に対する最終回答
開始
Question: {input}
{agent_scratchpad}"""
class CustomPromptTemplate(StringPromptTemplate):
template: str
tools: List[Tool]
def format(self, **kwargs) -> str:
intermediate_steps = kwargs.pop("intermediate_steps")
thoughts = ""
for action, observation in intermediate_steps:
thoughts += action.log
thoughts += f"\nObservation: {observation}\nThought: "
kwargs["agent_scratchpad"] = thoughts
kwargs["tools"] = "\n".join([f"{tool.name}: {tool.description}" for tool in self.tools])
kwargs["tool_names"] = ", ".join([tool.name for tool in self.tools])
return self.template.format(**kwargs)
class CustomOutputParser(AgentOutputParser):
def parse(self, llm_output: str) -> Union[AgentAction, AgentFinish]:
if "Final Answer:" in llm_output:
return AgentFinish(
return_values={"output": llm_output.split("Final Answer:")[-1].strip()},
log=llm_output,
)
regex = r"Action\s*\d*\s*:(.*?)\nAction\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
match = re.search(regex, llm_output, re.DOTALL)
if not match:
raise ValueError(f"Could not parse LLM output: `{llm_output}`")
action = match.group(1).strip()
action = "Search" if "Search" in action or "検索" in action else action
action_input = match.group(2)
return AgentAction(tool=action, tool_input=action_input.strip(" ").strip('"'), log=llm_output)
prompt = CustomPromptTemplate(
template=template,
tools=tools,
input_variables=["input", "intermediate_steps"]
)
llm_chain = LLMChain(llm=llm, prompt=prompt)
output_parser = CustomOutputParser()
agent = LLMSingleActionAgent(
llm_chain=llm_chain,
output_parser=output_parser,
stop=["\nObservation:"],
allowed_tools=tool_names
)
最後に下記のコードを実行することで、上記で作成したAgentを使用して質問の回答を得ることができます。
agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True)
agent_executor.run("花子の父の職業は何ですか?")
実行結果
> Entering new AgentExecutor chain...
Llama.generate: prefix-match hit
llama_print_timings: load time = 28.78 ms
llama_print_timings: sample time = 34.09 ms / 60 runs ( 0.57 ms per token, 1760.15 tokens per second)
llama_print_timings: prompt eval time = 0.00 ms / 0 tokens ( -nan ms per token, -nan tokens per second)
llama_print_timings: eval time = 522.19 ms / 60 runs ( 8.70 ms per token, 114.90 tokens per second)
llama_print_timings: total time = 732.49 ms / 60 tokens
Llama.generate: prefix-match hit
************** Search 花子の父親の名前を入力して、検索します
Thought: 花子の父親の職業を知りたいということですね。
Action: Search
Action Input: 花子の父親の名前を入力して、検索します
Observation:花子の父は太郎です
llama_print_timings: load time = 28.78 ms
llama_print_timings: sample time = 29.88 ms / 55 runs ( 0.54 ms per token, 1841.00 tokens per second)
llama_print_timings: prompt eval time = 42.81 ms / 16 tokens ( 2.68 ms per token, 373.76 tokens per second)
llama_print_timings: eval time = 444.16 ms / 54 runs ( 8.23 ms per token, 121.58 tokens per second)
llama_print_timings: total time = 668.66 ms / 70 tokens
Llama.generate: prefix-match hit
************** Search 「太郎 職業」を入力して検索します
太郎さんが花子の父親であることがわかりました。
Action: Search
Action Input: 「太郎 職業」を入力して検索します
Observation:太郎は教師です
llama_print_timings: load time = 28.78 ms
llama_print_timings: sample time = 108.13 ms / 185 runs ( 0.58 ms per token, 1710.86 tokens per second)
llama_print_timings: prompt eval time = 38.49 ms / 14 tokens ( 2.75 ms per token, 363.72 tokens per second)
llama_print_timings: eval time = 1497.49 ms / 184 runs ( 8.14 ms per token, 122.87 tokens per second)
llama_print_timings: total time = 2171.24 ms / 198 tokens
太郎さんの職業が教師であることがわかりました。
Final Answer: 花子の父親、太郎さんの職業は教師です。
以上のように、Searchツールを使用して情報を取得し、ThoughtやObservationを記録していくことで、効果的な回答を作成できます。重要なポイントは、常に問題解決のために考え続けることです。それが行動への指針となり、アクションを起こし、入力質問から最終答えまでを導き出せます。
> Finished chain.
花子の父親、太郎さんの職業は教師です。\n\n以上のように、Searchツールを使用して情報を取得し、ThoughtやObservationを記録していくことで、効果的な回答を作成できます。重要なポイントは、常に問題解決のために考え続けることです。それが行動への指針となり、アクションを起こし、入力質問から最終答えまでを導き出せます。
このように、「花子の父の職業」と質問した際に、まず花子の父の名前を検索し、次に得られた回答(太郎)を用いて「太郎の職業」を検索し、「教師」と結果が得られたことで最終的に「花子の父の職業=教師」という答えにたどり着きました。
ただし、この回答を得るために何度か実行を重ねており、LLMの精度次第ではなかなか正解にたどり着けないことが多々あります。ここに関しては今後のローカルLLMの成長に期待したいところです。