譯者 | 朱先忠
審校 | 重樓
簡(jiǎn)介
想象一下,你想買(mǎi)點(diǎn)東西。于是,你訪(fǎng)問(wèn)某個(gè)電子商務(wù)網(wǎng)站并使用搜索選項(xiàng)查找所需內(nèi)容。也許你有很多東西要買(mǎi),所以這個(gè)過(guò)程不是很有效?,F(xiàn)在,請(qǐng)考慮一下這樣一個(gè)場(chǎng)景:打開(kāi)一個(gè)應(yīng)用程序,用簡(jiǎn)單的英語(yǔ)描述一下你想要的東西,然后按下回車(chē)鍵。你不必?fù)?dān)心搜索和價(jià)格比較,因?yàn)閼?yīng)用程序會(huì)自動(dòng)為你處理了。很酷,對(duì)吧?這正是我們將在本文中要構(gòu)建的目標(biāo)程序。
下面,先讓我們先看一些例子。
用戶(hù)同時(shí)請(qǐng)求多個(gè)產(chǎn)品
用戶(hù)咨詢(xún)他/她能做出的最劃算的購(gòu)買(mǎi)
接下來(lái),讓我們?yōu)檫@個(gè)應(yīng)用程序增強(qiáng)一些功能。我們將使用Meta公司開(kāi)發(fā)的具有函數(shù)調(diào)用功能的Llama 3開(kāi)源模型。不過(guò),本文示例程序也可以使用此模型的3.1版本來(lái)實(shí)現(xiàn)。根據(jù)Meta公司的公告(https://ai.meta.com/blog/meta-llama-3-1/),3.1版本模型可以更有效地使用工具和函數(shù)。
【注意】這些模型是支持多語(yǔ)言的,具有128K的更長(zhǎng)的上下文長(zhǎng)度和最先進(jìn)的工具使用能力和整體更強(qiáng)的推理能力。
我將在本文示例開(kāi)發(fā)中使用Groq云平臺(tái),特別是他們?cè)诒疚闹械拇髷?shù)據(jù)模型。此應(yīng)用程序的初始工作流程包括一個(gè)嵌入模型、一個(gè)檢索器,還有兩個(gè)主要工具,用于處理用戶(hù)購(gòu)買(mǎi)興趣和與成本相關(guān)的問(wèn)題。總之,我們需要類(lèi)似于下圖所述的組件內(nèi)容。
示例應(yīng)用程序架構(gòu)圖
現(xiàn)在,我們必須要在開(kāi)發(fā)中選擇使用一個(gè)LLM組件框架。為此,我選擇了我一直最喜歡的生產(chǎn)級(jí)開(kāi)源AI平臺(tái)框架Haystack(https://haystack.deepset.ai/)。
準(zhǔn)備好了我們需要的內(nèi)容后,接下來(lái)就讓我們開(kāi)始投入關(guān)鍵的開(kāi)發(fā)工作吧!
加載和索引數(shù)據(jù)
由于我們本示例程序中使用了一個(gè)RAG管道,我們首先構(gòu)建一個(gè)文檔索引服務(wù),將使用Haystack提供的內(nèi)存向量數(shù)據(jù)庫(kù)。請(qǐng)注意,我們矢量數(shù)據(jù)庫(kù)中的每個(gè)文檔都包含如下字段:
- 內(nèi)容(Content)——我們用來(lái)執(zhí)行相似性搜索的內(nèi)容
- Id——唯一標(biāo)識(shí)符
- 價(jià)格(Price)——產(chǎn)品價(jià)格
- URL——產(chǎn)品URL
當(dāng)調(diào)用我們的RAG管道時(shí),Content字段用于向量搜索。所有其他字段都作為元數(shù)據(jù)包含在矢量數(shù)據(jù)庫(kù)中。注意,保存這些元數(shù)據(jù)是至關(guān)重要的,因?yàn)樗鼈冊(cè)谟脩?hù)的前端演示中至關(guān)重要。
接下來(lái),讓我們看看如何實(shí)現(xiàn)這一點(diǎn)。
from haystack import Pipeline, Document
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.components.writers import DocumentWriter
from haystack.components.embedders import SentenceTransformersDocumentEmbedder
from haystack.components.generators import OpenAIGenerator
from haystack.utils import Secret
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.components.builders import PromptBuilder
from haystack.components.embedders import SentenceTransformersTextEmbedder
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
from haystack.dataclasses import ChatMessage
import pandas as pd
#從CSV加載產(chǎn)品數(shù)據(jù)
df = pd.read_csv("product_sample.csv")
#初始化內(nèi)存中的文檔存儲(chǔ)區(qū)
document_store = InMemoryDocumentStore()
#將產(chǎn)品數(shù)據(jù)轉(zhuǎn)換為Haystack文檔對(duì)象
documents = [
Document(
content=item.product_name,
meta={
"id": item.uniq_id,
"price": item.selling_price,
"url": item.product_url
}
) for item in df.itertuples()
]
#創(chuàng)建一個(gè)用于編制文檔索引的管道
indexing_pipeline = Pipeline()
#使用句子轉(zhuǎn)換器模型向管道中添加一個(gè)文檔嵌入器
indexing_pipeline.add_component(
instance=SentenceTransformersDocumentEmbedder(model="sentence-transformers/all-MiniLM-L6-v2"), name="doc_embedder"
)
# 向管道中添加文檔寫(xiě)入器,以便在文檔存儲(chǔ)區(qū)中存儲(chǔ)文檔
indexing_pipeline.add_component(instance=DocumentWriter(document_store=document_store), name="doc_writer")
#將嵌入器的輸出連接到寫(xiě)入器的輸入
indexing_pipeline.connect("doc_embedder.documents", "doc_writer.documents")
#運(yùn)行索引管道來(lái)處理和存儲(chǔ)文檔
indexing_pipeline.run({"doc_embedder": {"documents": documents}})
太好了,我們已經(jīng)完成了AI代理應(yīng)用程序的第一步?,F(xiàn)在,是時(shí)候構(gòu)建產(chǎn)品標(biāo)識(shí)符函數(shù)工具了。為了更好地理解產(chǎn)品標(biāo)識(shí)符的主要任務(wù),讓我們考慮下面的示例。
用戶(hù)查詢(xún)內(nèi)容如下:
英語(yǔ)原文: I want to buy a camping boot, a charcoal and google pixel 9 back cover.
中文意思:我想買(mǎi)一雙露營(yíng)靴、一塊木炭和谷歌pixel 9手機(jī)外殼。
現(xiàn)在,先讓我們了解一下產(chǎn)品標(biāo)識(shí)符函數(shù)的理想化工作流程。
產(chǎn)品標(biāo)識(shí)函數(shù)工作流程
首先,我們需要?jiǎng)?chuàng)建一個(gè)工具來(lái)分析用戶(hù)查詢(xún)并識(shí)別用戶(hù)感興趣的產(chǎn)品。我們可以使用下面的代碼片段構(gòu)建這樣一個(gè)工具。
構(gòu)建用戶(hù)查詢(xún)分析器
template = """
Understand the user query and list of products the user is interested in and return product names as list.
You should always return a Python list. Do not return any explanation.
Examples:
Question: I am interested in camping boots, charcoal and disposable rain jacket.
Answer: ["camping_boots","charcoal","disposable_rain_jacket"]
Question: Need a laptop, wireless mouse, and noise-cancelling headphones for work.
Answer: ["laptop","wireless_mouse","noise_cancelling_headphones"]
Question: {{ question }}
Answer:
"""
product_identifier = Pipeline()
product_identifier.add_component("prompt_builder", PromptBuilder(template=template))
product_identifier.add_component("llm", generator())
product_identifier.connect("prompt_builder", "llm")
好了,現(xiàn)在我們已經(jīng)完成了第一個(gè)函數(shù)的一半,現(xiàn)在是時(shí)候通過(guò)添加RAG管道來(lái)完成該函數(shù)了。
產(chǎn)品標(biāo)識(shí)功能工作流程
創(chuàng)建RAG管道
template = """
Return product name, price, and url as a python dictionary.
You should always return a Python dictionary with keys price, name and url for single product.
You should always return a Python list of dictionaries with keys price, name and url for multiple products.
Do not return any explanation.
Legitimate Response Schema:
{"price": "float", "name": "string", "url": "string"}
Legitimate Response Schema for multiple products:
[{"price": "float", "name": "string", "url": "string"},{"price": "float", "name": "string", "url": "string"}]
Context:
{% for document in documents %}
product_price: {{ document.meta['price'] }}
product_url: {{ document.meta['url'] }}
product_id: {{ document.meta['id'] }}
product_name: {{ document.content }}
{% endfor %}
Question: {{ question }}
Answer:
"""
rag_pipe = Pipeline()
rag_pipe.add_component("embedder", SentenceTransformersTextEmbedder(model="sentence-transformers/all-MiniLM-L6-v2"))
rag_pipe.add_component("retriever", InMemoryEmbeddingRetriever(document_store=document_store, top_k=5))
rag_pipe.add_component("prompt_builder", PromptBuilder(template=template))
rag_pipe.add_component("llm", generator())
rag_pipe.connect("embedder.embedding", "retriever.query_embedding")
rag_pipe.connect("retriever", "prompt_builder.documents")
rag_pipe.connect("prompt_builder", "llm")
執(zhí)行上面的代碼之后,我們就完成了RAG和查詢(xún)分析管道的構(gòu)建?,F(xiàn)在是時(shí)候把它轉(zhuǎn)換成一個(gè)工具了。為此,我們可以使用常規(guī)函數(shù)聲明,如下所示。為AI代理創(chuàng)建工具就像創(chuàng)建Python函數(shù)一樣。如果你有類(lèi)似于下面這樣的問(wèn)題:
代理如何調(diào)用這個(gè)函數(shù)?
解決方案很簡(jiǎn)單:利用特定于模型的工具模式。當(dāng)然,我們將在稍后的步驟中加入該模式?,F(xiàn)在,是時(shí)候創(chuàng)建一個(gè)同時(shí)使用查詢(xún)分析器和RAG管道的包裝器函數(shù)了。
還是先讓我們來(lái)明確一下這個(gè)函數(shù)的目標(biāo)。
目標(biāo)1:識(shí)別用戶(hù)感興趣的所有產(chǎn)品,并將其作為列表返回。
目標(biāo)2:對(duì)于每個(gè)已識(shí)別的產(chǎn)品,從數(shù)據(jù)庫(kù)中檢索最多五個(gè)產(chǎn)品及其元數(shù)據(jù)。
實(shí)現(xiàn)產(chǎn)品標(biāo)識(shí)符函數(shù)
def product_identifier_func(query: str):
"""
根據(jù)給定的查詢(xún)來(lái)標(biāo)識(shí)產(chǎn)品,并檢索每個(gè)已標(biāo)識(shí)的產(chǎn)品的相關(guān)詳細(xì)信息。
參數(shù):
query (str): 用于標(biāo)識(shí)產(chǎn)品的查詢(xún)字符串。
返回值:
dict: 一個(gè)字典,其中鍵是產(chǎn)品名稱(chēng),值是每個(gè)產(chǎn)品的詳細(xì)信息。如果沒(méi)有找到產(chǎn)品,則返回“No product found”。
"""
product_understanding = product_identifier.run({"prompt_builder": {"question": query}})
try:
product_list = literal_eval(product_understanding["llm"]["replies"][0])
except:
return "No product found"
results = {}
for product in product_list:
response = rag_pipe.run({"embedder": {"text": product}, "prompt_builder": {"question": product}})
try:
results[product] = literal_eval(response["llm"]["replies"][0])
except:
results[product] = {}
return results
產(chǎn)品標(biāo)識(shí)函數(shù)工作流程
至此,我們完成了代理的第一個(gè)工具的構(gòu)建?,F(xiàn)在,先讓我們來(lái)看看它是否按預(yù)期工作。
query = "I want crossbow and woodstock puzzle"
#執(zhí)行函數(shù)
product_identifier_func(query)
# {'crossbow': {'name': 'DB Longboards CoreFlex Crossbow 41" Bamboo Fiberglass '
# 'Longboard Complete',
# 'price': 237.68,
# 'url': 'https://www.amazon.com/DB-Longboards-CoreFlex-Fiberglass-Longboard/dp/B07KMVJJK7'},
# 'woodstock_puzzle': {'name': 'Woodstock- Collage 500 pc Puzzle',
# 'price': 17.49,
# 'url': 'https://www.amazon.com/Woodstock-Collage-500-pc-Puzzle/dp/B07MX21WWX'}}
成功了!然而,值得注意的是這里返回的輸出模式。下面給出輸出的總體模式架構(gòu)。
{
"product_key": {
"name": "string",
"price": "float",
"url": "string"
}
}
這正是我們建議RAG管道中生成的模式。下一步,讓我們構(gòu)建一個(gè)名為find_budget_friend_option的可選工具函數(shù)。
def find_budget_friendly_option(selected_product_details):
"""
為每一類(lèi)產(chǎn)品找到最經(jīng)濟(jì)友好的選擇。
參數(shù):
selected_product_details (dict): 一個(gè)字典,其中的鍵是產(chǎn)品類(lèi)別和值是列表的產(chǎn)品細(xì)節(jié)。每個(gè)產(chǎn)品的細(xì)節(jié)都應(yīng)該是一個(gè)包含一個(gè)“price”鍵的字典。
返回結(jié)果:
dict: 一個(gè)字典,其中鍵是產(chǎn)品類(lèi)別,值是每個(gè)類(lèi)別的最經(jīng)濟(jì)友好的產(chǎn)品詳細(xì)信息。
"""
budget_friendly_options = {}
for category, items in selected_product_details.items():
if isinstance(items, list):
lowest_price_item = min(items, key=lambda x: x['price'])
else:
lowest_price_item = items
budget_friendly_options[category] = lowest_price_item
return budget_friendly_options
讓我們關(guān)注這個(gè)應(yīng)用程序最關(guān)鍵的方面,也就是,讓AI代理能夠根據(jù)需要使用這些功能。正如我們之前所討論的,這可以通過(guò)特定于模型的工具模式來(lái)實(shí)現(xiàn)。因此,我們需要定位特定于所選模型的工具模式。幸運(yùn)的是,Groq模型庫(kù)(https://huggingface.co/Groq/Llama-3-Groq-70B-Tool-Use)中提到了這一點(diǎn)。我們僅需要把它調(diào)整一下,以適應(yīng)我們的使用場(chǎng)景即可。
最終確定聊天模板
chat_template = '''<|start_header_id|>system<|end_header_id|>
You are a function calling AI model. You are provided with function signatures within <tools></tools> XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. For each function call return a json object with function name and arguments within <tool_call></tool_call> XML tags as follows:
<tool_call>
{"name": <function-name>,"arguments": <args-dict>}
</tool_call>
Here are the available tools:
<tools>
{
"name": "product_identifier_func",
"description": "To understand user interested products and its details",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to use in the search. Infer this from the user's message. It should be a question or a statement"
}
},
"required": ["query"]
}
},
{
"name": "find_budget_friendly_option",
"description": "Get the most cost-friendly option. If selected_product_details has morethan one key this should return most cost-friendly options",
"parameters": {
"type": "object",
"properties": {
"selected_product_details": {
"type": "dict",
"description": "Input data is a dictionary where each key is a category name, and its value is either a single dictionary with 'price', 'name', and 'url' keys or a list of such dictionaries; example: {'category1': [{'price': 10.5, 'name': 'item1', 'url': 'http://example.com/item1'}, {'price': 8.99, 'name': 'item2', 'url': 'http://example.com/item2'}], 'category2': {'price': 15.0, 'name': 'item3', 'url': 'http://example.com/item3'}}"
}
},
"required": ["selected_product_details"]
}
}
</tools><|eot_id|><|start_header_id|>user<|end_header_id|>
I need to buy a crossbow<|eot_id|><|start_header_id|>assistant<|end_header_id|>
<tool_call>
{"id":"call_deok","name":"product_identifier_func","arguments":{"query":"I need to buy a crossbow"}}
</tool_call><|eot_id|><|start_header_id|>tool<|end_header_id|>
<tool_response>
{"id":"call_deok","result":{'crossbow': {'price': 237.68,'name': 'crossbow','url': 'https://www.amazon.com/crossbow/dp/B07KMVJJK7'}}}
</tool_response><|eot_id|><|start_header_id|>assistant<|end_header_id|>
'''
現(xiàn)在,只剩下幾步了。在做任何事情之前,還是先讓我們來(lái)測(cè)試一下我們的代理。
##測(cè)試代理
messages = [
ChatMessage.from_system(
chat_template
),
ChatMessage.from_user("I need to buy a crossbow for my child and Pokémon for myself."),
]
chat_generator = get_chat_generator()
response = chat_generator.run(messages=messages)
pprint(response)
## 響應(yīng)結(jié)果
{'replies': [ChatMessage(content='<tool_call>\n'
'{"id": 0, "name": "product_identifier_func", '
'"arguments": {"query": "I need to buy a '
'crossbow for my child"}}\n'
'</tool_call>\n'
'<tool_call>\n'
'{"id": 1, "name": "product_identifier_func", '
'"arguments": {"query": "I need to buy a '
'Pokemon for myself"}}\n'
'</tool_call>',
role=<ChatRole.ASSISTANT: 'assistant'>,
name=None,
meta={'finish_reason': 'stop',
'index': 0,
'model': 'llama3-groq-70b-8192-tool-use-preview',
'usage': {'completion_time': 0.217823967,
'completion_tokens': 70,
'prompt_time': 0.041348261,
'prompt_tokens': 561,
'total_time': 0.259172228,
'total_tokens': 631}})]}
至此,我們已經(jīng)完成了大約90%的工作。
工作接近尾聲。
在上述響應(yīng)結(jié)果中,你可能已經(jīng)注意到XML標(biāo)簽<tool_call>包含了工具調(diào)用。因此,我們需要開(kāi)發(fā)一種機(jī)制來(lái)提取tool_call對(duì)象。
def extract_tool_calls(tool_calls_str):
json_objects = re.findall(r'<tool_call>(.*?)</tool_call>', tool_calls_str, re.DOTALL)
result_list = [json.loads(obj) for obj in json_objects]
return result_list
available_functions = {
"product_identifier_func": product_identifier_func,
"find_budget_friendly_option": find_budget_friendly_option
}
完成此步驟后,當(dāng)代理調(diào)用工具時(shí),我們可以直接訪(fǎng)問(wèn)代理的響應(yīng)?,F(xiàn)在唯一懸而未決的是獲取工具調(diào)用對(duì)象并相應(yīng)地執(zhí)行函數(shù)。讓我們也把這一部分完成。
messages.append(ChatMessage.from_user(message))
response = chat_generator.run(messages=messages)
if response and "<tool_call>" in response["replies"][0].content:
function_calls = extract_tool_calls(response["replies"][0].content)
for function_call in function_calls:
# 解析函數(shù)調(diào)用信息
function_name = function_call["name"]
function_args = function_call["arguments"]
#找到相應(yīng)的函數(shù)并用給定的參數(shù)調(diào)用它
function_to_call = available_functions[function_name]
function_response = function_to_call(**function_args)
# 使用`ChatMessage.from_function`在消息列表中附加函數(shù)響應(yīng)
messages.append(ChatMessage.from_function(content=json.dumps(function_response), name=function_name))
response = chat_generator.run(messages=messages)
現(xiàn)在,是時(shí)候?qū)⑶懊娴拿總€(gè)組件連接在一起,從而構(gòu)建一個(gè)完整的聊天應(yīng)用程序了。為此,我選擇使用強(qiáng)大的開(kāi)源的深度學(xué)習(xí)模型可視化工具Gradio。
import gradio as gr
messages = [ChatMessage.from_system(chat_template)]
chat_generator = get_chat_generator()
def chatbot_with_fc(message, messages):
messages.append(ChatMessage.from_user(message))
response = chat_generator.run(messages=messages)
while True:
if response and "<tool_call>" in response["replies"][0].content:
function_calls = extract_tool_calls(response["replies"][0].content)
for function_call in function_calls:
#解析函數(shù)調(diào)用信息
function_name = function_call["name"]
function_args = function_call["arguments"]
#找到相應(yīng)的函數(shù)并用給定的參數(shù)調(diào)用它
function_to_call = available_functions[function_name]
function_response = function_to_call(**function_args)
# 使用`ChatMessage.from_function`在消息列表中附加函數(shù)響應(yīng)
messages.append(ChatMessage.from_function(content=json.dumps(function_response), name=function_name))
response = chat_generator.run(messages=messages)
# 定期對(duì)話(huà)
else:
messages.append(response["replies"][0])
break
return response["replies"][0].content
def chatbot_interface(user_input, state):
response_content = chatbot_with_fc(user_input, state)
return response_content, state
with gr.Blocks() as demo:
gr.Markdown("# AI Purchase Assistant")
gr.Markdown("Ask me about products you want to buy!")
state = gr.State(value=messages)
with gr.Row():
user_input = gr.Textbox(label="Your message:")
response_output = gr.Markdown(label="Response:")
user_input.submit(chatbot_interface, [user_input, state], [response_output, state])
gr.Button("Send").click(chatbot_interface, [user_input, state], [response_output, state])
demo.launch()
就是這么簡(jiǎn)單!至此,我們已經(jīng)成功構(gòu)建了一個(gè)基于Llama 3模型的人工智能代理程序,它本身具有函數(shù)調(diào)用功能。你可以從GitHub倉(cāng)庫(kù)(https://github.com/Ransaka/ai-agents-with-llama3)訪(fǎng)問(wèn)其完整的源代碼。
此外,你可以通過(guò)Kaggle鏈接(https://www.kaggle.com/datasets/promptcloud/amazon-product-dataset-2020)訪(fǎng)問(wèn)本文中使用的數(shù)據(jù)集。
結(jié)論
歸納來(lái)看,在構(gòu)建基于人工智能代理的系統(tǒng)程序時(shí),重要的是要考慮完成任務(wù)所需的時(shí)間以及每個(gè)任務(wù)所使用的API調(diào)用(令牌)的數(shù)量。這方面開(kāi)發(fā)面臨的一個(gè)主要挑戰(zhàn)是減少系統(tǒng)中的幻覺(jué),這也是當(dāng)前一個(gè)十分活躍的研究領(lǐng)域。因此,構(gòu)建LLM和代理系統(tǒng)沒(méi)有固定的規(guī)則。開(kāi)發(fā)團(tuán)隊(duì)有必要耐心和戰(zhàn)略性地規(guī)劃工作,以確保人工智能代理LLM正常運(yùn)行。
最后,除非另有說(shuō)明,本文中所有圖片均由作者本人提供。
參考資料
- 《Llama 3.1簡(jiǎn)介:我們的迄今為止最強(qiáng)大的模型》,我們的最新模型為所有人帶來(lái)了開(kāi)放式智能,擴(kuò)展了上下文長(zhǎng)度,增加了對(duì)八種語(yǔ)言的支持,等等。鏈接地址:https://ai.meta.com/blog/meta-llama-3-1/?source=post_page-----7e74f79d1ccc--------------------------------
- 《Groq/Llama-3-Groq-70B-Tool-Use · Hugging Face》,我們正在通過(guò)開(kāi)源和開(kāi)放科學(xué)推進(jìn)人工智能并使其民主化。鏈接地址:https://huggingface.co/Groq/Llama-3-Groq-70B-Tool-Use?source=post_page-----7e74f79d1ccc--------------------------------
- https://docs.together.ai/docs/llama-3-function-calling
譯者介紹
朱先忠,51CTO社區(qū)編輯,51CTO專(zhuān)家博客、講師,濰坊一所高校計(jì)算機(jī)教師,自由編程界老兵一枚。
原文標(biāo)題:Using Llama 3 for Building AI Agents,作者:Ransaka Ravihara