在上一课中,我们介绍了工具使用工作流程。现在是时候开始实施一个简单的工具使用示例了。总结一下,工具使用过程最多有 4 个步骤
为 Claude 提供工具和用户提示:(API 请求) #
- 定义您希望 Claude 可以访问的工具集,包括其名称、描述和输入模式。
- 提供可能需要使用其中一个或多个工具来回答的用户提示。
Claude 使用工具:(API 响应) #
- Claude 评估用户提示并决定是否有任何可用工具可以帮助用户的查询或任务。如果是,它还会决定使用哪个工具以及使用哪些输入。
- Claude 输出格式正确的工具使用请求。
- API 响应将具有 tool_use 的 stop_reason,表明 Claude 想要使用外部工具。
提取工具输入、运行代码并返回结果:(API 请求) #
- 在客户端,您应该从 Claude 的工具使用请求中提取工具名称和输入。
- 在客户端运行实际的工具代码。
- 通过使用包含 tool_result 内容块的新用户消息继续对话,将结果返回给 Claude。
Claude 使用工具结果来制定响应:(API 响应) #
- 收到工具结果后,Claude 将使用该信息来制定对原始用户提示的最终响应。
我们将从一个简单的演示开始,只需要与 Claude“交谈”一次(别担心,我们很快就会得到更多令人兴奋的例子!)。这意味着我们暂时不会费心执行第 4 步。我们将要求 Claude 回答一个问题,Claude 将请求使用工具来回答它,然后我们将提取工具输入、运行代码并返回结果值。
当今的大型语言模型在数学运算方面举步维艰,以下代码就是明证。
我们要求 Claude “将 1984135 乘以 9343116”:
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv()
client = Anthropic()
# A relatively simple math problem
response = client.messages.create(
model="claude-3-haiku-20240307",
messages=[{"role": "user", "content":"Multiply 1984135 by 9343116. Only respond with the result"}],
max_tokens=400
)
print(response.content[0].text)
18555375560
通过多次运行上述代码,我们可能会得到不同的答案,但这是 Claude 给出的一个答案:
18593367726060
实际正确答案是:
18538003464660
Claude 的答案略有偏差,为 55364261400!
加入工具调用 #
Claude 不擅长做复杂的数学运算,所以让我们通过提供计算器工具来增强 Claude 的能力。
下面是解释该过程的简单图表:
第一步是定义实际的计算器函数并确保它独立于 Claude 工作。我们将编写一个非常简单的函数,它需要三个参数:
- 像“加”或“乘”这样的操作
- 两个操作数
这是一个基本的实现:
def calculator(operation, operand1, operand2):
if operation == "add":
return operand1 + operand2
elif operation == "subtract":
return operand1 - operand2
elif operation == "multiply":
return operand1 * operand2
elif operation == "divide":
if operand2 == 0:
raise ValueError("Cannot divide by zero.")
return operand1 / operand2
else:
raise ValueError(f"Unsupported operation: {operation}")
请注意,这个简单的函数实用性非常有限,因为它只能处理像 234 + 213 或 3 * 9 这样的简单表达式。这里的重点是通过一个非常简单的教育示例来完成使用工具的过程。
让我们测试一下我们的函数并确保它能正常工作。
calculator("add", 10, 3)
13
calculator("divide", 200, 25)
8.0
下一步是定义我们的工具并告诉 Claude。定义工具时,我们遵循非常具体的格式。每个工具定义包括:
- name:工具的名称。必须匹配正则表达式 ^[a-zA-Z0-9_-]{1,64}$。
- description:工具功能、使用时机和行为方式的详细纯文本描述。
- input_schema:定义工具预期参数的 JSON Schema 对象。
不熟悉 JSON Schema?在此处了解更多信息。
这是一个假设工具的简单示例:
{
"name": "send_email",
"description": "Sends an email to the specified recipient with the given subject and body.",
"input_schema": {
"type": "object",
"properties": {
"to": {
"type": "string",
"description": "The email address of the recipient"
},
"subject": {
"type": "string",
"description": "The subject line of the email"
},
"body": {
"type": "string",
"description": "The content of the email message"
}
},
"required": ["to", "subject", "body"]
}
}
此工具名为 send_email,需要以下输入:
- to 是字符串,是必需的
- subject 是字符串,是必需的
- body 是字符串,是必需的
以下是另一个名为 search_product 的工具的定义:
{
"name": "search_product",
"description": "Search for a product by name or keyword and return its current price and availability.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The product name or search keyword, e.g. 'iPhone 13 Pro' or 'wireless headphones'"
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "home", "toys", "sports"],
"description": "The product category to narrow down the search results"
},
"max_price": {
"type": "number",
"description": "The maximum price of the product, used to filter the search results"
}
},
"required": ["query"]
}
}
此工具有 3 个输入:
- 必需的查询字符串,表示产品名称或搜索关键字
- 可选的类别字符串,必须是预定义值之一,以缩小搜索范围。请注意定义中的“枚举”。
- 可选的 max_price 数字,用于过滤低于特定价格点的结果
我们的计算器工具定义 #
让我们为之前编写的计算器函数定义相应的工具。我们知道计算器函数有 3 个必需参数:
- 操作 – 只能是“加”、“减”、“乘”或“除”
- 操作数 1 应该是一个数字
- 操作数 2 也应该是一个数字
以下是工具定义:
calculator_tool = {
"name": "calculator",
"description": "A simple calculator that performs basic arithmetic operations.",
"input_schema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The arithmetic operation to perform."
},
"operand1": {
"type": "number",
"description": "The first operand."
},
"operand2": {
"type": "number",
"description": "The second operand."
}
},
"required": ["operation", "operand1", "operand2"]
}
}
练习 #
让我们以以下函数为例练习编写格式正确的工具定义:
def inventory_lookup(product_name, max_results):
return "this function doesn't do anything"
#You do not need to touch this or do anything with it!
# 这个假设的 inventory_lookup 函数应该这样调用:
inventory_lookup("AA batteries", 4)
inventory_lookup("birthday candle", 10)
# 您的任务是编写相应的、格式正确的工具定义。假设您的工具定义中需要这两个参数。
为 Claude 提供我们的工具 #
现在回到我们之前的计算器函数。此时,Claude 对计算器工具一无所知!它只是一个小小的 Python 字典。在向 Claude 发出请求时,我们可以传递一个工具列表来“告诉”Claude。现在让我们尝试一下:
response = client.messages.create(
model="claude-3-haiku-20240307",
messages=[{"role": "user", "content": "Multiply 1984135 by 9343116. Only respond with the result"}],
max_tokens=300,
# Tell Claude about our tool
tools=[calculator_tool]
)
接下来我们来看看Claude给我们的回应:
response
ToolsBetaMessage(id='msg_01UfKwdmEsgTh99wfpgW4NJ7', content=[ToolUseBlock(id='toolu_015wQ7Wipo589yT9B3YTwjF1', input={'operand1': 1984135, 'operand2': 9343116, 'operation': 'multiply'}, name='calculator', type='tool_use')], model='claude-3-haiku-20240307', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=420, output_tokens=93))
您可能会注意到,我们的响应看起来与通常的情况有些不同!具体来说,我们现在收到的不是简单的 Message,而是 ToolsMessage。
此外,我们可以检查 response.stop_reason 并看到 Claude 停止了,因为它决定是时候使用工具了
response.stop_reason:’tool_use’
response.content 包含一个包含 ToolUseBlock 的列表,该列表本身包含有关工具名称和输入的信息:
response.content:
[ToolUseBlock(id=’toolu_015wQ7Wipo589yT9B3YTwjF1′, input={‘operand1’: 1984135, ‘operand2’: 9343116, ‘operation’: ‘multiply’}, name=’calculator’, type=’tool_use’)]
tool_name = response.content[0].name
tool_inputs = response.content[0].input
print("The Tool Name Claude Wants To Call:", tool_name)
print("The Inputs Claude Wants To Call It With:", tool_inputs)
The Tool Name Claude Wants To Call: calculator
The Inputs Claude Wants To Call It With: {'operand1': 1984135, 'operand2': 9343116, 'operation': 'multiply'
下一步就是简单地获取 Claude 提供给我们的工具名称和输入,并使用它们来实际调用我们之前编写的计算器函数。然后我们就会得到最终答案!
operation = tool_inputs["operation"]
operand1 = tool_inputs["operand1"]
operand2 = tool_inputs["operand2"]
result = calculator(operation, operand1, operand2)
print("RESULT IS", result)
RESULT IS 18538003464660
我们得到了正确答案 18538003464660!!!我们不必依赖 Claude 来获得正确的数学答案,只需向 Claude 提出一个问题,并让其访问一个工具,它可以在必要时决定使用哪个工具。
重要提示 #
如果我们问 Claude 一些不需要使用工具的问题,在这种情况下是一些与数学或计算无关的问题,我们可能希望它能像往常一样做出反应。Claude 通常会这样做,但有时 Claude 非常渴望使用它的工具!
这里有一个例子,有时 Claude 会尝试使用计算器,尽管使用它没有意义。让我们看看当我们问 Claude“祖母绿是什么颜色的?”时会发生什么。
response = client.messages.create(
model="claude-3-haiku-20240307",
messages=[{"role": "user", "content":"What color are emeralds?"}],
max_tokens=400,
tools=[calculator_tool]
)
response:
ToolsBetaMessage(id='msg_01Dj82HdyrxGJpi8XVtqEYvs', content=[ToolUseBlock(id='toolu_01Xo7x3dV1FVoBSGntHNAX4Q', input={'operand1': 0, 'operand2': 0, 'operation': 'add'}, name='calculator', type='tool_use')], model='claude-3-haiku-20240307', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=409, output_tokens=89))
克劳德想让我们调用计算器工具?一个非常简单的解决方法是调整我们的提示或添加一个系统提示,内容如下:您可以使用工具,但只能在必要时使用它们。如果不需要工具,请正常响应
response = client.messages.create(
model="claude-3-haiku-20240307",
system="You have access to tools, but only use them when necessary. If a tool is not required, respond as normal",
messages=[{"role": "user", "content":"What color are emeralds?"}],
max_tokens=400,
tools=[calculator_tool]
)
response
ToolsBetaMessage(id='msg_01YRRfnUUhP1u5ojr9iWZGGu', content=[TextBlock(text='Emeralds are green in color.', type='text')], model='claude-3-haiku-20240307', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=Usage(input_tokens=434, output_tokens=12))
现在,Claude 会用适当的内容做出回应,不会在工具使用不合理的情况下强行将其强行塞入。这是我们得到的新回应:
“祖母绿是绿色的。”
我们还可以看到 stop_reason 现在是 end_turn 而不是 tool_use。
response.stop_reason:’end_turn’
综述 #
def calculator(operation, operand1, operand2):
if operation == "add":
return operand1 + operand2
elif operation == "subtract":
return operand1 - operand2
elif operation == "multiply":
return operand1 * operand2
elif operation == "divide":
if operand2 == 0:
raise ValueError("Cannot divide by zero.")
return operand1 / operand2
else:
raise ValueError(f"Unsupported operation: {operation}")
calculator_tool = {
"name": "calculator",
"description": "A simple calculator that performs basic arithmetic operations.",
"input_schema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The arithmetic operation to perform.",
},
"operand1": {"type": "number", "description": "The first operand."},
"operand2": {"type": "number", "description": "The second operand."},
},
"required": ["operation", "operand1", "operand2"],
},
}
def prompt_claude(prompt):
messages = [{"role": "user", "content": prompt}]
response = client.messages.create(
model="claude-3-haiku-20240307",
system="You have access to tools, but only use them when necessary. If a tool is not required, respond as normal",
messages=messages,
max_tokens=500,
tools=[calculator_tool],
)
if response.stop_reason == "tool_use":
tool_use = response.content[-1]
tool_name = tool_use.name
tool_input = tool_use.input
if tool_name == "calculator":
print("Claude wants to use the calculator tool")
operation = tool_input["operation"]
operand1 = tool_input["operand1"]
operand2 = tool_input["operand2"]
try:
result = calculator(operation, operand1, operand2)
print("Calculation result is:", result)
except ValueError as e:
print(f"Error: {str(e)}")
elif response.stop_reason == "end_turn":
print("Claude didn't want to use a tool")
print("Claude responded with:")
print(response.content[0].text)
prompt_claude("I had 23 chickens but 2 flew away. How many are left?")
Claude want to use the calculator tool
Calculation result is: 21
prompt_claude("What is 201 times 2")
Claude want to use the calculator tool
Calculation result is: 402
prompt_claude("Write me a haiku about the ocean")
Claude didn't want to use a tool
Claude responded with:
Here is a haiku about the ocean:
Vast blue expanse shines,
Waves crash upon sandy shores,
Ocean's soothing song.
练习 #
您的任务是使用 Claude 帮助构建研究助手。用户可以输入他们想要研究的主题,并将维基百科文章链接列表保存到 markdown 文件中以供以后阅读。我们可以尝试直接要求 Claude 生成文章 URL 列表,但 Claude 对 URL 不可靠,可能会产生幻觉。此外,在 Claude 的培训截止日期之后,合法文章可能已移至新 URL。相反,我们将使用连接到真实维基百科 API 的工具来实现这一点!
我们将为 Claude 提供一个工具的访问权限,该工具接受 Claude 生成但可能产生幻觉的可能的维基百科文章标题列表。我们可以使用此工具搜索维基百科以找到实际的维基百科文章标题和 URL,以确保最终列表包含所有实际存在的文章。然后,我们将这些文章 URL 保存到 markdown 文件中以供以后阅读。
我们为您提供了两个功能来提供帮助:
import wikipedia
def generate_wikipedia_reading_list(research_topic, article_titles):
wikipedia_articles = []
for t in article_titles:
results = wikipedia.search(t)
try:
page = wikipedia.page(results[0])
title = page.title
url = page.url
wikipedia_articles.append({"title": title, "url": url})
except:
continue
add_to_research_reading_file(wikipedia_articles, research_topic)
def add_to_research_reading_file(articles, topic):
with open("output/research_reading.md", "a", encoding="utf-8") as file:
file.write(f"## {topic} \n")
for article in articles:
title = article["title"]
url = article["url"]
file.write(f"* [{title}]({url}) \n")
file.write(f"\n\n")
第一个函数 generate_wikipedia_reading_list 需要传递一个研究主题,如“夏威夷的历史”或“世界各地的海盗”,以及我们将让 Claude 生成的潜在维基百科文章名称列表。该函数使用 wikipedia 包搜索相应的真实维基百科页面,并构建包含文章标题和 URL 的词典列表。
然后它调用 add_to_research_reading_file,传入维基百科文章数据列表和整体研究主题。此函数只是将每个维基百科文章的 markdown 链接添加到名为 output/research_reading.md 的文件中。文件名现在是硬编码的,函数假定它存在。它存在于此 repo 中,但如果在其他地方工作,您需要自己创建它。
我们的想法是让 Claude “调用”generate_wikipedia_reading_list,其中包含可能是也可能不是真实的潜在文章标题列表。 Claude 可能会传递以下文章标题输入列表,其中一些是真正的维基百科文章,而有些则不是:
[“海盗”、“著名海盗船”、“海盗黄金时代”、“海盗名单”、“海盗和鹦鹉”、“21 世纪的海盗行为”]
generate_wikipedia_reading_list 函数会遍历每个文章标题,并收集实际存在的任何维基百科文章的真实文章标题和相应的 URL。然后,它调用 add_to_research_reading_file 将该内容写入 markdown 文件以供以后参考。
最终目标 #
您的工作是实现一个名为 get_research_help 的函数,该函数接受一个研究主题和所需的文章数量。该函数应使用 Claude 实际生成可能的维基百科文章列表,并从上面调用 generate_wikipedia_reading_list 函数。以下是一些示例函数调用:
get_research_help(“Pirates Across The World”, 7)
get_research_help(“History of Hawaii”, 3)
get_research_help(“are animal conscious?”, 3)
经过这 3 个函数调用后,我们的输出 research_reading.md 文件如下所示(请在 output/research_reading.md 中自行查看):
要实现这一点,您需要执行以下操作:
- 为 generate_wikipedia_reading_list 函数编写工具定义
- 实现 get_research_help 函数
- 向 Claude 编写提示,告诉它您需要帮助收集有关特定主题的研究以及您希望它生成多少篇文章标题
- 告诉 Claude 它可以访问的工具
- 向 Claude 发送您的请求
- 检查 Claude 是否调用了该工具。如果调用了,您需要将其生成的文章标题和主题传递给我们提供给您的 generate_wikipedia_reading_list 函数。该函数将收集实际的维基百科文章链接,然后调用 add_to_research_reading_file 将链接写入 output/research_reading.md
- 打开 output/research_reading.md 查看它是否有效!
入门代码
# Here's your starter code!
import wikipedia
def generate_wikipedia_reading_list(research_topic, article_titles):
wikipedia_articles = []
for t in article_titles:
results = wikipedia.search(t)
try:
page = wikipedia.page(results[0])
title = page.title
url = page.url
wikipedia_articles.append({"title": title, "url": url})
except:
continue
add_to_research_reading_file(wikipedia_articles, research_topic)
def add_to_research_reading_file(articles, topic):
with open("output/research_reading.md", "a", encoding="utf-8") as file:
file.write(f"## {topic} \n")
for article in articles:
title = article["title"]
url = article["url"]
file.write(f"* [{title}]({url}) \n")
file.write(f"\n\n")
def get_research_help(topic, num_articles=3):
#Implement this function!
pass