译者 | 朱先忠
审校 | 重楼
简介
想象一下,你想买点东西。于是,你访问某个电子商务网站并使用搜索选项查找所需内容。也许你有很多东西要买,所以这个过程不是很有效。现在,请考虑一下这样一个场景:打开一个应用程序,用简单的英语描述一下你想要的东西,然后按下回车键。你不必担心搜索和价格比较,因为应用程序会自动为你处理了。很酷,对吧?这正是我们将在本文中要构建的目标程序。
下面,先让我们先看一些例子。
用户同时请求多个产品
用户咨询他/她能做出的最划算的购买
接下来,让我们为这个应用程序增强一些功能。我们将使用Meta公司开发的具有函数调用功能的Llama 3开源模型。不过,本文示例程序也可以使用此模型的3.1版本来实现。根据Meta公司的公告(https://ai.meta.com/blog/meta-llama-3-1/),3.1版本模型可以更有效地使用工具和函数。
【注意】这些模型是支持多语言的,具有128K的更长的上下文长度和最先进的工具使用能力和整体更强的推理能力。
我将在本文示例开发中使用Groq云平台,特别是他们在本文中的大数据模型。此应用程序的初始工作流程包括一个嵌入模型、一个检索器,还有两个主要工具,用于处理用户购买兴趣和与成本相关的问题。总之,我们需要类似于下图所述的组件内容。
示例应用程序架构图
现在,我们必须要在开发中选择使用一个LLM组件框架。为此,我选择了我一直最喜欢的生产级开源AI平台框架Haystack(https://haystack.deepset.ai/)。
准备好了我们需要的内容后,接下来就让我们开始投入关键的开发工作吧!
加载和索引数据
由于我们本示例程序中使用了一个RAG管道,我们首先构建一个文档索引服务,将使用Haystack提供的内存向量数据库。请注意,我们矢量数据库中的每个文档都包含如下字段:
- 内容(Content)——我们用来执行相似性搜索的内容
- Id——唯一标识符
- 价格(Price)——产品价格
- URL——产品URL
当调用我们的RAG管道时,Content字段用于向量搜索。所有其他字段都作为元数据包含在矢量数据库中。注意,保存这些元数据是至关重要的,因为它们在用户的前端演示中至关重要。
接下来,让我们看看如何实现这一点。
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加载产品数据
df = pd.read_csv("product_sample.csv")
#初始化内存中的文档存储区
document_store = InMemoryDocumentStore()
#将产品数据转换为Haystack文档对象
documents = [
Document(
content=item.product_name,
meta={
"id": item.uniq_id,
"price": item.selling_price,
"url": item.product_url
}
) for item in df.itertuples()
]
#创建一个用于编制文档索引的管道
indexing_pipeline = Pipeline()
#使用句子转换器模型向管道中添加一个文档嵌入器
indexing_pipeline.add_component(
instance=SentenceTransformersDocumentEmbedder(model="sentence-transformers/all-MiniLM-L6-v2"), name="doc_embedder"
)
# 向管道中添加文档写入器,以便在文档存储区中存储文档
indexing_pipeline.add_component(instance=DocumentWriter(document_store=document_store), name="doc_writer")
#将嵌入器的输出连接到写入器的输入
indexing_pipeline.connect("doc_embedder.documents", "doc_writer.documents")
#运行索引管道来处理和存储文档
indexing_pipeline.run({"doc_embedder": {"documents": documents}})
太好了,我们已经完成了AI代理应用程序的第一步。现在,是时候构建产品标识符函数工具了。为了更好地理解产品标识符的主要任务,让我们考虑下面的示例。
用户查询内容如下:
英语原文: I want to buy a camping boot, a charcoal and google pixel 9 back cover.
中文意思:我想买一双露营靴、一块木炭和谷歌pixel 9手机外壳。
现在,先让我们了解一下产品标识符函数的理想化工作流程。
产品标识函数工作流程
首先,我们需要创建一个工具来分析用户查询并识别用户感兴趣的产品。我们可以使用下面的代码片段构建这样一个工具。
构建用户查询分析器
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")
好了,现在我们已经完成了第一个函数的一半,现在是时候通过添加RAG管道来完成该函数了。
产品标识功能工作流程
创建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")
执行上面的代码之后,我们就完成了RAG和查询分析管道的构建。现在是时候把它转换成一个工具了。为此,我们可以使用常规函数声明,如下所示。为AI代理创建工具就像创建Python函数一样。如果你有类似于下面这样的问题:
代理如何调用这个函数?
解决方案很简单:利用特定于模型的工具模式。当然,我们将在稍后的步骤中加入该模式。现在,是时候创建一个同时使用查询分析器和RAG管道的包装器函数了。
还是先让我们来明确一下这个函数的目标。
目标1:识别用户感兴趣的所有产品,并将其作为列表返回。
目标2:对于每个已识别的产品,从数据库中检索最多五个产品及其元数据。
实现产品标识符函数
def product_identifier_func(query: str):
"""
根据给定的查询来标识产品,并检索每个已标识的产品的相关详细信息。
参数:
query (str): 用于标识产品的查询字符串。
返回值:
dict: 一个字典,其中键是产品名称,值是每个产品的详细信息。如果没有找到产品,则返回“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
产品标识函数工作流程
至此,我们完成了代理的第一个工具的构建。现在,先让我们来看看它是否按预期工作。
query = "I want crossbow and woodstock puzzle"
#执行函数
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'}}
成功了!然而,值得注意的是这里返回的输出模式。下面给出输出的总体模式架构。
{
"product_key": {
"name": "string",
"price": "float",
"url": "string"
}
}
这正是我们建议RAG管道中生成的模式。下一步,让我们构建一个名为find_budget_friend_option的可选工具函数。
def find_budget_friendly_option(selected_product_details):
"""
为每一类产品找到最经济友好的选择。
参数:
selected_product_details (dict): 一个字典,其中的键是产品类别和值是列表的产品细节。每个产品的细节都应该是一个包含一个“price”键的字典。
返回结果:
dict: 一个字典,其中键是产品类别,值是每个类别的最经济友好的产品详细信息。
"""
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
让我们关注这个应用程序最关键的方面,也就是,让AI代理能够根据需要使用这些功能。正如我们之前所讨论的,这可以通过特定于模型的工具模式来实现。因此,我们需要定位特定于所选模型的工具模式。幸运的是,Groq模型库(https://huggingface.co/Groq/Llama-3-Groq-70B-Tool-Use)中提到了这一点。我们仅需要把它调整一下,以适应我们的使用场景即可。
最终确定聊天模板
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|>
'''
现在,只剩下几步了。在做任何事情之前,还是先让我们来测试一下我们的代理。
##测试代理
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)
## 响应结果
{'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}})]}
至此,我们已经完成了大约90%的工作。
工作接近尾声。
在上述响应结果中,你可能已经注意到XML标签<tool_call>包含了工具调用。因此,我们需要开发一种机制来提取tool_call对象。
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
}
完成此步骤后,当代理调用工具时,我们可以直接访问代理的响应。现在唯一悬而未决的是获取工具调用对象并相应地执行函数。让我们也把这一部分完成。
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:
# 解析函数调用信息
function_name = function_call["name"]
function_args = function_call["arguments"]
#找到相应的函数并用给定的参数调用它
function_to_call = available_functions[function_name]
function_response = function_to_call(**function_args)
# 使用`ChatMessage.from_function`在消息列表中附加函数响应
messages.append(ChatMessage.from_function(content=json.dumps(function_response), name=function_name))
response = chat_generator.run(messages=messages)
现在,是时候将前面的每个组件连接在一起,从而构建一个完整的聊天应用程序了。为此,我选择使用强大的开源的深度学习模型可视化工具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:
#解析函数调用信息
function_name = function_call["name"]
function_args = function_call["arguments"]
#找到相应的函数并用给定的参数调用它
function_to_call = available_functions[function_name]
function_response = function_to_call(**function_args)
# 使用`ChatMessage.from_function`在消息列表中附加函数响应
messages.append(ChatMessage.from_function(content=json.dumps(function_response), name=function_name))
response = chat_generator.run(messages=messages)
# 定期对话
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()
就是这么简单!至此,我们已经成功构建了一个基于Llama 3模型的人工智能代理程序,它本身具有函数调用功能。你可以从GitHub仓库(https://github.com/Ransaka/ai-agents-with-llama3)访问其完整的源代码。
此外,你可以通过Kaggle链接(https://www.kaggle.com/datasets/promptcloud/amazon-product-dataset-2020)访问本文中使用的数据集。
结论
归纳来看,在构建基于人工智能代理的系统程序时,重要的是要考虑完成任务所需的时间以及每个任务所使用的API调用(令牌)的数量。这方面开发面临的一个主要挑战是减少系统中的幻觉,这也是当前一个十分活跃的研究领域。因此,构建LLM和代理系统没有固定的规则。开发团队有必要耐心和战略性地规划工作,以确保人工智能代理LLM正常运行。
最后,除非另有说明,本文中所有图片均由作者本人提供。
参考资料
- 《Llama 3.1简介:我们的迄今为止最强大的模型》,我们的最新模型为所有人带来了开放式智能,扩展了上下文长度,增加了对八种语言的支持,等等。链接地址:https://ai.meta.com/blog/meta-llama-3-1/?source=post_page-----7e74f79d1ccc--------------------------------
- 《Groq/Llama-3-Groq-70B-Tool-Use · Hugging Face》,我们正在通过开源和开放科学推进人工智能并使其民主化。链接地址:https://huggingface.co/Groq/Llama-3-Groq-70B-Tool-Use?source=post_page-----7e74f79d1ccc--------------------------------
- https://docs.together.ai/docs/llama-3-function-calling
译者介绍
朱先忠,51CTO社区编辑,51CTO专家博客、讲师,潍坊一所高校计算机教师,自由编程界老兵一枚。
原文标题:Using Llama 3 for Building AI Agents,作者:Ransaka Ravihara