iT邦幫忙

2024 iThome 鐵人賽

DAY 25
0
生成式 AI

T 大使 AI 之旅系列 第 25

【Day 25】RAG + Memory = 超級 Agent?Agent 的進化之路

  • 分享至 

  • xImage
  •  

前情提要

上一篇文章了解 Agent 的用途,也實作了簡單的 Agent。那基於這個簡單的例子,要來把生成式 AI 的另外兩個技術:RAG、Memory 功能與 Agent 做結合,透過簡單的實作來將 LangChain 中比較常被用到的技術結合在一起。
https://ithelp.ithome.com.tw/upload/images/20240830/20168336TNOT2QhtRr.png

Agent 工具

實作之前先來介紹今天要使用的兩個 Agent 工具:

Retriever Tool

要使用 RAG 的話,那就必須使用到這個工具,只要針對這個工具命名與描述使用時機即可。

Code

# 匯入套件
from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_qdrant import QdrantVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from qdrant_client import QdrantClient
from qdrant_client.http import models
from qdrant_client.http.models import Distance
from langchain.tools.retriever import create_retriever_tool

# 選擇模型
llm = ChatOpenAI(model="gpt-4o", temperature=0)
embeddings = OpenAIEmbeddings()
client = QdrantClient(url="http://localhost:6333")
collections_name = "ithome-Agent"

# 若 collection 存在則刪除 (現在沒有recreate函數)
if client.collection_exists(collection_name=collections_name):
	client.delete_collection(collection_name=collections_name)
else:
	pass

# 建立一個新的 collections
client.create_collection(
	collection_name=collections_name,
	vectors_config=
		models.VectorParams(
			size=1536,
			distance=Distance.COSINE,
	),
)

# langchain 連線 Qdrant 已存在的 collections
qdrant_vector_store = QdrantVectorStore.from_existing_collection(
	url="http://localhost:6333",
	collection_name=collections_name,
	embedding=embeddings,
)

# 切割內容
loader = WebBaseLoader("https://www.nba.com/news/zaccharie-risacher-hawks-2024-nba-draft")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
split_docs = splitter.split_documents(docs)

# 將資料匯入 Qdrant
qdrant_vector_store.add_documents(documents=split_docs)
retriever = qdrant_vector_store.as_retriever(search_kwargs={"k": 3})

# 建立 tools
retriever_tool = create_retriever_tool(
	retriever,
	"nba_draft_search",
	"Use this tool when searching for information about 2024 NBA draft"
)

程式碼結果探討 🧐:

  • 前面的步驟都跟之前 RAG 的部分一樣,像是切段落、資料匯入 Qdrant、建立檢索等等。那最後透過利用 create_retriever_tool 包裝成一個 Tool,那因為這個網頁是在講 NBA 2024 的選秀,所以名字給他取 nba_draft_search,描述的部分就是輸入有關 2024 NBA 選秀的部分就會去進行檢索。

Tavily Search

這是另一個很特別的工具,他是專門開發給 Agent 使用的 Tool。這個工具的目標是消弭 AI 系統與來自網絡的實時、事實信息之間的差距,但當然最即時的資訊可能還是有差,但是我必須說真的是很強很強的工具了!

Tavily 跟 Upstash-Redis 一樣有提供免費額度給使用者使用,每個月是 1000 次的 requests,對我來說其實挺夠用的,在測試上也很方便。

Code

from langchain_community.tools.tavily_search import TavilySearchResults

search = TavilySearchResults()

程式碼結果探討 🧐:

  • 大家沒有看錯,使用這個工具很方便,LangChain 都包裝好了,直接取用即可。

實戰🔥

Memory 儲存於變數

# 匯入套件
from dotenv import load_dotenv
load_dotenv()
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.tools.retriever import create_retriever_tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import create_openai_functions_agent, AgentExecutor
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import HumanMessage, AIMessage
from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_qdrant import QdrantVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from qdrant_client import QdrantClient
from qdrant_client.http import models
from qdrant_client.http.models import Distance
from langchain.tools.retriever import create_retriever_tool

# 選擇模型
llm = ChatOpenAI(model="gpt-4o", temperature=0)
embeddings = OpenAIEmbeddings()
client = QdrantClient(url="http://localhost:6333")
collections_name = "ithome-Agent"

# 若 collection 存在則刪除 (現在沒有recreate函數)
if client.collection_exists(collection_name=collections_name):
	client.delete_collection(collection_name=collections_name)
else:
	pass

# 建立一個新的 collections
client.create_collection(
	collection_name=collections_name,
	vectors_config=
		models.VectorParams(
			size=1536,
			distance=Distance.COSINE,
	),
)

# langchain 連線 Qdrant 已存在的 collections
qdrant_vector_store = QdrantVectorStore.from_existing_collection(
	url="http://localhost:6333",
	collection_name=collections_name,
	embedding=embeddings,
)

# 讀取網頁並切割內容
loader = WebBaseLoader("https://www.nba.com/news/zaccharie-risacher-hawks-2024-nba-draft")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
split_docs = splitter.split_documents(docs)

# 將資料匯入 Qdrant
qdrant_vector_store.add_documents(documents=split_docs)
retriever = qdrant_vector_store.as_retriever(search_kwargs={"k": 3})


# 建立 retriever tools
retriever_tool = create_retriever_tool(
	retriever,
	"nba_draft_search",
	"Use this tool when searching for information about 2024 NBA draft"
)

# 建立 tavily tools
search = TavilySearchResults()
tools = [search, retriever_tool]

# 設定 prompt
	prompt = ChatPromptTemplate.from_messages([
	("system", "You are the friendly assistant called Lulu"),
	MessagesPlaceholder(variable_name="chat_history"),
	("human", "{input}"),
	MessagesPlaceholder(variable_name="agent_scratchpad")
])

# 建立 Agent
agent = create_openai_functions_agent(
	llm=llm,
	prompt=prompt,
	tools=tools,
)

# 設定 Agent 的 Chain
agent_executor = AgentExecutor(
	agent=agent,
	tools=tools,
	verbose=True
)

# 建立函數來執行對話
def proccess_chat(agent_executor, user_input, chat_history):
	response = agentExecutor.invoke({
		"input": user_input,
		"chat_history": chat_history
	})
	return response['output']

# 開始與 AI 對話
if __name__ == "__main__":
	chat_history = []
	while True:
		user_input = input("You: ")
		if user_input.lower() == "exit":
			break
		response = proccess_chat(agentExecutor, user_input, chat_history)
		chat_history.append(HumanMessage(content=user_input))
		chat_history.append(AIMessage(content=response))
		print("Assistant : ", response)

RAG 成果

可以如我所設定的詢問關於 2024 NBA 選秀的內容,模型有去檢索資料庫,並回傳正確的內容。那如果資料庫內容很豐富的話可以多建立幾個 Tools,讓 AI 自己去判別什麼問題要去檢索那個資料庫。那因為 tavily 這個工具太強了,RAG 先 demo 到這邊~
https://ithelp.ithome.com.tw/upload/images/20240830/20168336SMZcUVzp4D.png

Tavily AI 成果

https://ithelp.ithome.com.tw/upload/images/20240830/20168336Rl09Qm6TC4.png
https://ithelp.ithome.com.tw/upload/images/20240830/20168336XwnApv8yy1.png
https://ithelp.ithome.com.tw/upload/images/20240830/20168336N7WuLP7eRf.png
總共有三個部分:

  1. 第一個是我詢問 NBA 2024 的總冠軍是誰,他有透過 Tavily AI 搜尋回答正確。
  2. 第二個部分 Memory 的功能就派上用場了,接著我再詢問東區系列賽 MVP,他就知道我在問的是 2024 賽季,然後我接著問西區,他沒有使用 Tools,反而是很快的就回答出來了,我在想這也是因為 Memory 的關係。
  3. 第三個部分就是我詢問例行賽的 MVP,那 AI 一樣就知道是 2024 賽季。結果發生了 Tavily 沒有查詢到正確答案的情況,然後他就拿 2023 賽季的 MVP 來頂替。我就重新問他一次,那也是因為 Memory 讓我可以用這樣的方式與 AI 互動,那他重新搜尋之後就回傳了正確的結果。

Memory 儲存於 Upstash-Redis

# 匯入套件
from dotenv import load_dotenv
import os
load_dotenv()
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.tools.retriever import create_retriever_tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import create_openai_functions_agent, AgentExecutor
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_qdrant import QdrantVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from qdrant_client import QdrantClient
from qdrant_client.http import models
from qdrant_client.http.models import Distance
from langchain.tools.retriever import create_retriever_tool
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories.upstash_redis import UpstashRedisChatMessageHistory

# 選擇模型
llm = ChatOpenAI(model="gpt-4o", temperature=0)
embeddings = OpenAIEmbeddings()
client = QdrantClient(url="http://localhost:6333")
collections_name = "ithome-Agent"

# 若 collection 存在則刪除 (現在沒有recreate函數)
if client.collection_exists(collection_name=collections_name):
	client.delete_collection(collection_name=collections_name)
else:
	pass

# 建立一個新的 collections
client.create_collection(
	collection_name=collections_name,
	vectors_config=
		models.VectorParams(
			size=1536,
			distance=Distance.COSINE,
	),
)

# langchain 連線 Qdrant 已存在的 collections
qdrant_vector_store = QdrantVectorStore.from_existing_collection(
	url="http://localhost:6333",
	collection_name=collections_name,
	embedding=embeddings,
)

# 切割內容
loader = WebBaseLoader("https://www.nba.com/news/zaccharie-risacher-hawks-2024-nba-draft")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
split_docs = splitter.split_documents(docs)

# 將資料匯入 Qdrant
qdrant_vector_store.add_documents(documents=split_docs)
retriever = qdrant_vector_store.as_retriever(search_kwargs={"k": 3})

# 建立 tools
retriever_tool = create_retriever_tool(
	retriever,
	"nba_draft_search",
	"Use this tool when searching for information about 2024 NBA draft"
)
search = TavilySearchResults()
tools = [search, retriever_tool]

# 設定 prompt
prompt = ChatPromptTemplate.from_messages([
	("system", "You are the friendly assistant called Lulu"),
	MessagesPlaceholder(variable_name="chat_history"),
	("human", "{input}"),
	MessagesPlaceholder(variable_name="agent_scratchpad")
	]
)

# 建立 agent
agent = create_openai_functions_agent(
	llm=llm,
	prompt=prompt,
	tools=tools,
)

# 設定 Agent 的 Chain
agentExecutor = AgentExecutor(
	agent=agent,
	tools=tools,
	verbose=True
)

# 設定儲存對話紀錄至 Upstash-Redis
def get_session_history(session_id: str) -> BaseChatMessageHistory:
	return UpstashRedisChatMessageHistory(
		url=os.environ['UPSTASH_REDIS_REST_URL'],
		token=os.environ['UPSTASH_REDIS_REST_TOKEN'],
		session_id=session_id
	)

# 將對話紀錄與 Agent Chain 在一起
agent_with_memory = RunnableWithMessageHistory(
	agentExecutor,
	get_session_history,
	input_messages_key="input",
	history_messages_key="chat_history",
)
  
# 設定對話函數
def proccess_chat(agent_with_memory, user_input, session_id):
	response = agent_with_memory.invoke({'input': user_input}, {'configurable': {'session_id': session_id}})
	return response['output']

# 開始與 AI 對話
if __name__ == "__main__":
	while True:
		user_input = input("You: ")
		if user_input.lower() == "exit":
			break
		response = proccess_chat(agent_with_memory, user_input, "5")
		print("Assistant : ", response)

https://ithelp.ithome.com.tw/upload/images/20240830/20168336bnYRaTzaC5.png
https://ithelp.ithome.com.tw/upload/images/20240830/20168336GRBZXsFkVo.png
程式碼結果探討 🧐:

  • 其實大部分的部分都跟前面一樣,然後之前在實作 Memory 是輸入一般的 Chain,現在改成 RunnableWithMessageHistory 只需要輸入 RunnableAgent 的 Chain 即可,就可以成功儲存對話紀錄,也可以根據任務使用對應的 Tools。
  • 詢問剛結束的奧運結果也可以回答的很正確精準,怕大家看的太辛苦,藍色部分是 Tavily 搜尋的結果,可以略過不看,我實作這樣比較好理解有無呼叫到 Tools。

結論

今天實作了用 RAG 來當我們的 Tools,也使用了應該是目前最強最方便的工具 - Tavily,可以實現即時資訊的回覆。那也使用了 Memory 的功能,不管是簡單的儲存在環境變數還是 Upstash-Redis,整體感覺其實跟網頁上使用 ChatGPT 非常接近了,真的是很厲害的工具。

題外話🤣

這週真的是超爆炸,三個專案趕 8 月底結案,然後還有一個 AI 競賽要交件,下禮拜還要考雲端證照,真的是分身乏術,導致鐵人賽每天都寫不完🥲

下一篇文章:程式小白的福音:用 FlowiseAI 探索生成式 AI


上一篇
【Day 24】Agent 入門指南:從零開始構建智能代理
下一篇
【Day 26】程式小白的福音:用 FlowiseAI 探索生成式 AI
系列文
T 大使 AI 之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言