【LangChain】RAGとchain周りを理解する②
LangChain公式ドキュメントのRAGの説明が雑なので、丁寧に勉強しました
https://python.langchain.com/docs/expression_language/cookbook/retrieval
chain
会話として成立させるにはchainにpromptとmodelを渡す必要がありますが、実行だけならpromptだけでもできます。
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template("私のメッセージは「{my_message}」です")
chain = prompt
templateにmy_messageというパラメーターをつけているので、chain.invoke()の引数にはmy_messageに文言を渡す必要があります
chain.invoke({"my_message":"こんにちは"})
#->ChatPromptValue(messages=[HumanMessage(content='私のメッセージは「こんにちは」です')])
my_messageをつけずに直接文言を入れるとエラーとなります
# エラーとなる
chain.invoke("こんにちは")
#-> Expected mapping type as input to ChatPromptTemplate. Received <class 'str'>.
これを回避するにはRunnablePassthrough()を使います
from langchain_core.runnables import RunnablePassthrough
chain = {
"my_message":RunnablePassthrough(),
} | prompt
chain.invoke("こんにちは")
promptの前に{"my_message":RunnablePassthrough()}を入れたことにより、invokeで渡された引数がmy_messageのキーをつけられてpromptに渡されます。
複数パラメーターを渡す
このように複数パラメーターを渡すこともできます。
template="""
私のメッセージは「{my_message}」、
あなたのメッセージは「{your_message}」
"""
prompt = ChatPromptTemplate.from_template(template)
chain = prompt
chain.invoke({"my_message":"こんにちは。今日は寒いですね","your_message":"そうですね"})
itemgetterを使ってパラメーターの値を取得することができます。
(itemgetterはLangChainのライブラリではなく、python標準ライブラリです)
以下の例では、my_messageをhuman_messageに、your_messageをai_messageに置き換えています
from operator import itemgetter
template="""
人間のメッセージは「{human_message}」、
AIのメッセージは「{ai_message}」
"""
prompt = ChatPromptTemplate.from_template(template)
chain = {
"human_message":itemgetter("my_message"),
"ai_message":itemgetter("your_message")
} | prompt
chain.invoke({
"my_message":"こんにちは。今日は寒いですね",
"your_message":"そうですね"
})
パラメーターを加工する
my_messageのうち、あいさつの部分だけを切り出してpromptに渡します。
# あいさつ部分だけを切り取る関数
def only_greeting(message):
return message.split('。')[0]
only_greeting('こんにちは。今日は寒いですね')
#-> こんにちは
chainの中で、itemgetter(my_message)で「こんにちは。今日は寒いですね」を取り出し → only_greetingであいさつ部分だけを取り出しという処理をしたいです。
chainでは|で区切ることによって、後工程に渡せるので、以下のように書くことができます
chain = {
"human_message":itemgetter("my_message") | only_greeting,
"ai_message":itemgetter("your_message")
} | prompt
ただし、上記の書き方はエラーとなります。
忘れがちですが、chainの中にはRunnnableしか入れることができません。
なので関数であるonly_greetingを入れるにはRunnableLambdaでRunnableに変換してやる必要があります
chain = {
"human_message":itemgetter("my_message") | RunnableLambda(only_greeting) ,
"ai_message":itemgetter("your_message")
} | prompt
lambdaで編集して渡す
chainのRunnableにlambdaを設定することにより、パラメーターを加工してpromptに渡すことができます
prompt = ChatPromptTemplate.from_template("会話履歴:{chat_history}")
chain = {
"chat_history": lambda x: "人間:" + x["my_message"] +"、" + "AI:"+ x["your_message"]
} | prompt
chain.invoke({"my_message": "こんにちは。今日は寒いですね", "your_message": "そうですね"})
#-> ChatPromptValue(messages=[HumanMessage(content='会話履歴:人間:こんにちは。今日は寒いですね、AI:そうですね')])
ただ、上記の方法ではmy_messageとyour_messageをがっちゃんこしてchat_historyにしてしまったので、もとのmy_messageとyour_messageはpromptに渡せなくなってしまってます。
RunnablePassthrough().assign()を使うことで、my_messageとyour_messageを保持したまま、新たにchat_historyを作成し、後工程に渡すことができます。
(繰り返しになりますが、chainに入れられる要素はRunnableなので、ただのassignではなくRunnableのassignでなければなりません)
template="""
私のメッセージは「{my_message}」、
あなたのメッセージは「{your_message}」
会話履歴:{chat_history}」
"""
prompt = ChatPromptTemplate.from_template(template)
chain = (
RunnablePassthrough().assign(chat_history = lambda x: "人間:" + x["my_message"] +"、" + "AI:"+ x["your_message"] )
| prompt
)
chain.invoke({"my_message": "こんにちは。今日は寒いですね", "your_message": "そうですね"})
#-> ChatPromptValue(messages=[HumanMessage(content='\n私のメッセージは「こんにちは。今日は寒いですね」、\nあなたのメッセージは「そうですね」\n\n会話履歴:人間:こんにちは。今日は寒いですね、AI:そうですね」\n')])
ここまで紐解けば、公式ドキュメントの例が何やっているのか見えてきました。
_inputs = RunnableParallel(
standalone_question=RunnablePassthrough.assign(
chat_history=lambda x: get_buffer_string(x["chat_history"])
)
| CONDENSE_QUESTION_PROMPT
| ChatOpenAI(temperature=0)
| StrOutputParser(),
)
_context = {
"context": itemgetter("standalone_question") | retriever | _combine_documents,
"question": lambda x: x["standalone_question"],
}
conversational_qa_chain = _inputs | _context | ANSWER_PROMPT | ChatOpenAI()
次はメモリに会話履歴を保存するところをやっていきます。
Discussion