摘要 #
下图展示了与聊天机器人的示例交互。
现在我们要将工具的使用提升到另一个层次!我们将为 Claude 提供一套可供其选择的工具。我们的目标是为一家名为 TechNova 的虚构电子公司构建一个简单的客户支持聊天机器人。它将可以访问简单的工具,包括:
get_user
通过电子邮件、用户名或电话号码查找用户详细信息get_order_by_id
直接通过订单 ID 查找订单get_customer_orders
查找属于客户的所有订单cancel_order
取消给定特定订单 ID 的订单
这个聊天机器人功能非常有限,但它展示了使用多种工具的一些关键部分。
这是与聊天机器人的另一种交互,每当 Claude 使用工具时,我们都会明确打印出一条消息,以便更容易理解幕后发生的事情:
下图显示了潜在单个聊天消息的信息流:
重要提示:这个聊天机器人非常简单,只要有下单用户的用户名或电子邮件,任何人都可以取消订单。此实现仅用于教育目的,不应未经修改就集成到实际代码库中!
构建虚拟数据库 #
在开始使用 Claude 之前,我们首先要定义一个非常简单的“FakeDatabase”类,该类将保存一些虚假客户和订单,并提供与数据交互的方法。在现实世界中,聊天机器人可能会连接到一个或多个实际数据库。
class FakeDatabase:
def __init__(self):
self.customers = [
{"id": "1213210", "name": "John Doe", "email": "john@gmail.com", "phone": "123-456-7890", "username": "johndoe"},
{"id": "2837622", "name": "Priya Patel", "email": "priya@candy.com", "phone": "987-654-3210", "username": "priya123"},
{"id": "3924156", "name": "Liam Nguyen", "email": "lnguyen@yahoo.com", "phone": "555-123-4567", "username": "liamn"},
{"id": "4782901", "name": "Aaliyah Davis", "email": "aaliyahd@hotmail.com", "phone": "111-222-3333", "username": "adavis"},
{"id": "5190753", "name": "Hiroshi Nakamura", "email": "hiroshi@gmail.com", "phone": "444-555-6666", "username": "hiroshin"},
{"id": "6824095", "name": "Fatima Ahmed", "email": "fatimaa@outlook.com", "phone": "777-888-9999", "username": "fatimaahmed"},
{"id": "7135680", "name": "Alejandro Rodriguez", "email": "arodriguez@protonmail.com", "phone": "222-333-4444", "username": "alexr"},
{"id": "8259147", "name": "Megan Anderson", "email": "megana@gmail.com", "phone": "666-777-8888", "username": "manderson"},
{"id": "9603481", "name": "Kwame Osei", "email": "kwameo@yahoo.com", "phone": "999-000-1111", "username": "kwameo"},
{"id": "1057426", "name": "Mei Lin", "email": "meilin@gmail.com", "phone": "333-444-5555", "username": "mlin"}
]
self.orders = [
{"id": "24601", "customer_id": "1213210", "product": "Wireless Headphones", "quantity": 1, "price": 79.99, "status": "Shipped"},
{"id": "13579", "customer_id": "1213210", "product": "Smartphone Case", "quantity": 2, "price": 19.99, "status": "Processing"},
{"id": "97531", "customer_id": "2837622", "product": "Bluetooth Speaker", "quantity": 1, "price": "49.99", "status": "Shipped"},
{"id": "86420", "customer_id": "3924156", "product": "Fitness Tracker", "quantity": 1, "price": 129.99, "status": "Delivered"},
{"id": "54321", "customer_id": "4782901", "product": "Laptop Sleeve", "quantity": 3, "price": 24.99, "status": "Shipped"},
{"id": "19283", "customer_id": "5190753", "product": "Wireless Mouse", "quantity": 1, "price": 34.99, "status": "Processing"},
{"id": "74651", "customer_id": "6824095", "product": "Gaming Keyboard", "quantity": 1, "price": 89.99, "status": "Delivered"},
{"id": "30298", "customer_id": "7135680", "product": "Portable Charger", "quantity": 2, "price": 29.99, "status": "Shipped"},
{"id": "47652", "customer_id": "8259147", "product": "Smartwatch", "quantity": 1, "price": 199.99, "status": "Processing"},
{"id": "61984", "customer_id": "9603481", "product": "Noise-Cancelling Headphones", "quantity": 1, "price": 149.99, "status": "Shipped"},
{"id": "58243", "customer_id": "1057426", "product": "Wireless Earbuds", "quantity": 2, "price": 99.99, "status": "Delivered"},
{"id": "90357", "customer_id": "1213210", "product": "Smartphone Case", "quantity": 1, "price": 19.99, "status": "Shipped"},
{"id": "28164", "customer_id": "2837622", "product": "Wireless Headphones", "quantity": 2, "price": 79.99, "status": "Processing"}
]
def get_user(self, key, value):
if key in {"email", "phone", "username"}:
for customer in self.customers:
if customer[key] == value:
return customer
return f"Couldn't find a user with {key} of {value}"
else:
raise ValueError(f"Invalid key: {key}")
return None
def get_order_by_id(self, order_id):
for order in self.orders:
if order["id"] == order_id:
return order
return None
def get_customer_orders(self, customer_id):
return [order for order in self.orders if order["customer_id"] == customer_id]
def cancel_order(self, order_id):
order = self.get_order_by_id(order_id)
if order:
if order["status"] == "Processing":
order["status"] = "Cancelled"
return "Cancelled the order"
else:
return "Order has already shipped. Can't cancel it."
return "Can't find that order!"
我们将创建一个“FakeDatabase”的实例:db = FakeDatabase()
让我们确保我们的 get_user
方法按预期工作。它期望我们传入以下之一的 key
:
- “email”
- “username”
- “phone”
它还期望一个相应的 value
,它将使用该 value
执行基本搜索,希望返回匹配的用户。
db.get_user("email", "john@gmail.com")
{'id': '1213210',
'name': 'John Doe',
'email': 'john@gmail.com',
'phone': '123-456-7890',
'username': 'johndoe'}
db.get_user("username", "adavis")
{'id': '4782901',
'name': 'Aaliyah Davis',
'email': 'aaliyahd@hotmail.com',
'phone': '111-222-3333',
'username': 'adavis'}
db.get_user("phone", "666-777-8888")
{'id': '8259147',
'name': 'Megan Anderson',
'email': 'megana@gmail.com',
'phone': '666-777-8888',
'username': 'manderson'}
我们还可以通过调用“get_customer_orders”并传入客户 ID 来获取属于特定客户的所有订单列表:
db.get_customer_orders("1213210")
[{'id': '24601',
'customer_id': '1213210',
'product': 'Wireless Headphones',
'quantity': 1,
'price': 79.99,
'status': 'Shipped'},
{'id': '13579',
'customer_id': '1213210',
'product': 'Smartphone Case',
'quantity': 2,
'price': 19.99,
'status': 'Processing'},
{'id': '90357',
'customer_id': '1213210',
'product': 'Smartphone Case',
'quantity': 1,
'price': 19.99,
'status': 'Shipped'}]
db.get_customer_orders("9603481")
[{'id': '61984',
'customer_id': '9603481',
'product': 'Noise-Cancelling Headphones',
'quantity': 1,
'price': 149.99,
'status': 'Shipped'}]
如果我们有订单 ID,我们还可以直接查找单个订单:
db.get_order_by_id('24601')
{'id': '24601',
'customer_id': '1213210',
'product': 'Wireless Headphones',
'quantity': 1,
'price': 79.99,
'status': 'Shipped'}
最后,我们可以使用“cancel_order”方法取消订单。订单只有在“处理中”时才可以取消。我们不能取消已经发货的订单!该方法要求我们传入要取消的订单的 order_id。
# 让我们查找一个处于处理状态的订单:
db.get_order_by_id("47652")
{'id': '47652',
'customer_id': '8259147',
'product': 'Smartwatch',
'quantity': 1,
'price': 199.99,
'status': 'Processing'}
#Now let's cancel it!
db.cancel_order("47652")
'Cancelled the order'
# It's status should now be "Cancelled"
db.get_order_by_id("47652")
{'id': '47652',
'customer_id': '8259147',
'product': 'Smartwatch',
'quantity': 1,
'price': 199.99,
'status': 'Cancelled'}
编写客服工具函数 #
现在我们已经测试了(非常)简单的假数据库功能,让我们编写定义工具结构的 JSON 模式。让我们看一个与我们的“get_order_by_id”方法相对应的非常简单的工具:
tool1 = {
"name": "get_order_by_id",
"description": "Retrieves the details of a specific order based on the order ID. Returns the order ID, product name, quantity, price, and order status.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": ["order_id"]
}
}
与往常一样,我们包括名称、描述和输入概述。在本例中,只有一个输入 order_id
,并且它是必需的。
现在让我们看一个与 get_user
方法相对应的更复杂的工具。回想一下,此方法有两个参数:
key
参数,它是以下字符串之一:
*“email”
*“username”
*“phone”value
参数,它是我们将用于搜索的术语(实际的电子邮件、电话号码或用户名)
以下是相应的工具定义:
tool2 = {
"name": "get_user",
"description": "Looks up a user by email, phone, or username.",
"input_schema": {
"type": "object",
"properties": {
"key": {
"type": "string",
"enum": ["email", "phone", "username"],
"description": "The attribute to search for a user by (email, phone, or username)."
},
"value": {
"type": "string",
"description": "The value to match for the specified attribute."
}
},
"required": ["key", "value"]
}
}
请特别注意我们在架构中使用“enum”定义“key”的可能有效选项集的方式。
我们还有两个工具要编写,但为了节省时间,我们只为您提供可供我们使用的完整工具列表。(欢迎您先尝试自己定义它们作为练习!)
tools = [
{
"name": "get_user",
"description": "Looks up a user by email, phone, or username.",
"input_schema": {
"type": "object",
"properties": {
"key": {
"type": "string",
"enum": ["email", "phone", "username"],
"description": "The attribute to search for a user by (email, phone, or username)."
},
"value": {
"type": "string",
"description": "The value to match for the specified attribute."
}
},
"required": ["key", "value"]
}
},
{
"name": "get_order_by_id",
"description": "Retrieves the details of a specific order based on the order ID. Returns the order ID, product name, quantity, price, and order status.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": ["order_id"]
}
},
{
"name": "get_customer_orders",
"description": "Retrieves the list of orders belonging to a user based on a user's customer id.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "The customer_id belonging to the user"
}
},
"required": ["customer_id"]
}
},
{
"name": "cancel_order",
"description": "Cancels an order based on a provided order_id. Only orders that are 'processing' can be cancelled",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order_id pertaining to a particular order"
}
},
"required": ["order_id"]
}
}
]
Claude调用工具函数 #
接下来,我们需要做几件事:
- 告诉 Claude 我们的工具,并将用户的聊天消息发送给 Claude。
- 处理我们从 Claude 那里得到的响应:
- 如果 Claude 不想使用工具:
- 将 Claude 的输出打印给用户。
- 询问用户他们的下一条消息。
- 如果 Claude 确实想使用工具
- 验证 Claude 是否调用了其中一个适当的工具。
- 执行底层函数,如
get_user
或cancel_order
。 - 将运行工具的结果发送给 Claude。
最终,目标是编写一个命令行脚本,创建一个循环运行的交互式聊天机器人。但为了简单起见,我们将从线性编写基本代码开始。最后我们将创建一个交互式聊天机器人脚本。
我们从一个可以将 Claude 的工具使用响应转换为我们的 db
上的实际方法调用的函数开始:
def process_tool_call(tool_name, tool_input):
if tool_name == "get_user":
return db.get_user(tool_input["key"], tool_input["value"])
elif tool_name == "get_order_by_id":
return db.get_order_by_id(tool_input["order_id"])
elif tool_name == "get_customer_orders":
return db.get_customer_orders(tool_input["customer_id"])
elif tool_name == "cancel_order":
return db.cancel_order(tool_input["order_id"])
让我们从一个简单的演示开始,该演示展示了 Claude 在看到多个工具列表时可以决定使用哪个工具。我们会问 Claude“你能查一下我的订单吗?我的电子邮件地址是 john@gmail.com”,看看会发生什么!
import anthropic
import json
client = anthropic.Client()
MODEL_NAME = "claude-3-sonnet-20240229"
messages = [{"role": "user", "content": "Can you look up my orders? My email is john@gmail.com"}]
#Send a request to Claude
response = client.messages.create(
model=MODEL_NAME,
max_tokens=4096,
tools=tools,
messages=messages
)
print(response)
ToolsBetaMessage(id='msg_0112Ab7iKF2gWeAcAd2XWaBb', content=[TextBlock(text='Sure, let me look up your orders based on your email. To get the user details by email:', type='text'), ToolUseBlock(id='toolu_01CNTeufcesL1sUP2jnwT8TA', input={'key': 'email', 'value': 'john@gmail.com'}, name='get_user', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=574, output_tokens=96))
Claude 想要使用 get_user
工具。
现在我们将编写基本逻辑来更新我们的消息列表,调用适当的工具,并使用工具结果再次更新我们的消息列表。
# Update messages to include Claude's response
messages.append(
{"role": "assistant", "content": response.content}
)
#If Claude stops because it wants to use a tool:
if response.stop_reason == "tool_use":
tool_use = response.content[-1] #Naive approach assumes only 1 tool is called at a time
tool_name = tool_use.name
tool_input = tool_use.input
print("Claude wants to use the {tool_name} tool")
print(f"Tool Input:")
print(json.dumps(tool_input, indent=2))
#Actually run the underlying tool functionality on our db
tool_result = process_tool_call(tool_name, tool_input)
print(f"\nTool Result:")
print(json.dumps(tool_result, indent=2))
#Add our tool_result message:
messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": str(tool_result),
}
],
},
)
else:
#If Claude does NOT want to use a tool, just print out the text reponse
print("\nTechNova Support:" + f"{response.content[0].text}" )
Claude wants to use the {tool_name} tool
Tool Input:
{
"key": "email",
"value": "john@gmail.com"
}
Tool Result:
{
"id": "1213210",
"name": "John Doe",
"email": "john@gmail.com",
"phone": "123-456-7890",
"username": "johndoe"
}
我们现在的消息列表如下所示:
messages
[{'role': 'user',
'content': 'Can you look up my orders? My email is john@gmail.com'},
{'role': 'assistant',
'content': [TextBlock(text='Sure, let me look up your orders based on your email. To get the user details by email:', type='text'),
ToolUseBlock(id='toolu_01CNTeufcesL1sUP2jnwT8TA', input={'key': 'email', 'value': 'john@gmail.com'}, name='get_user', type='tool_use')]},
{'role': 'user',
'content': [{'type': 'tool_result',
'tool_use_id': 'toolu_01CNTeufcesL1sUP2jnwT8TA',
'content': "{'id': '1213210', 'name': 'John Doe', 'email': 'john@gmail.com', 'phone': '123-456-7890', 'username': 'johndoe'}"}]}]
现在我们将使用更新的消息列表向 Claude 发送第二个请求:
response2 = client.messages.create(
model=MODEL_NAME,
max_tokens=4096,
tools=tools,
messages=messages
)
response2
ToolsBetaMessage(id='msg_011Fru6wiViEExPXg7ANQkfH', content=[TextBlock(text='Now that I have your customer ID 1213210, I can retrieve your order history:', type='text'), ToolUseBlock(id='toolu_011eHUmUgCZXwr7swzL1LE6y', input={'customer_id': '1213210'}, name='get_customer_orders', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=733, output_tokens=80))
现在 Claude 已经获得了 get_user
工具的结果,包括用户的 ID,它想要调用 get_customer_orders
来查找与该特定客户相对应的订单。它正在为这项工作挑选合适的工具。注意:这里有很多潜在的问题和 Claude 可能出错的地方,但这只是一个开始!
编写交互式脚本 #
通过交互式命令行脚本与此聊天机器人交互要容易得多。
以下是将上述所有代码组合成一个函数,该函数启动基于循环的聊天会话(您可能希望从命令行将其作为自己的脚本运行):
import anthropic
import json
client = anthropic.Client()
MODEL_NAME = "claude-3-sonnet-20240229"
# MODEL_NAME = "claude-3-opus-20240229"
class FakeDatabase:
def __init__(self):
self.customers = [
{"id": "1213210", "name": "John Doe", "email": "john@gmail.com", "phone": "123-456-7890", "username": "johndoe"},
{"id": "2837622", "name": "Priya Patel", "email": "priya@candy.com", "phone": "987-654-3210", "username": "priya123"},
{"id": "3924156", "name": "Liam Nguyen", "email": "lnguyen@yahoo.com", "phone": "555-123-4567", "username": "liamn"},
{"id": "4782901", "name": "Aaliyah Davis", "email": "aaliyahd@hotmail.com", "phone": "111-222-3333", "username": "adavis"},
{"id": "5190753", "name": "Hiroshi Nakamura", "email": "hiroshi@gmail.com", "phone": "444-555-6666", "username": "hiroshin"},
{"id": "6824095", "name": "Fatima Ahmed", "email": "fatimaa@outlook.com", "phone": "777-888-9999", "username": "fatimaahmed"},
{"id": "7135680", "name": "Alejandro Rodriguez", "email": "arodriguez@protonmail.com", "phone": "222-333-4444", "username": "alexr"},
{"id": "8259147", "name": "Megan Anderson", "email": "megana@gmail.com", "phone": "666-777-8888", "username": "manderson"},
{"id": "9603481", "name": "Kwame Osei", "email": "kwameo@yahoo.com", "phone": "999-000-1111", "username": "kwameo"},
{"id": "1057426", "name": "Mei Lin", "email": "meilin@gmail.com", "phone": "333-444-5555", "username": "mlin"}
]
self.orders = [
{"id": "24601", "customer_id": "1213210", "product": "Wireless Headphones", "quantity": 1, "price": 79.99, "status": "Shipped"},
{"id": "13579", "customer_id": "1213210", "product": "Smartphone Case", "quantity": 2, "price": 19.99, "status": "Processing"},
{"id": "97531", "customer_id": "2837622", "product": "Bluetooth Speaker", "quantity": 1, "price": "49.99", "status": "Shipped"},
{"id": "86420", "customer_id": "3924156", "product": "Fitness Tracker", "quantity": 1, "price": 129.99, "status": "Delivered"},
{"id": "54321", "customer_id": "4782901", "product": "Laptop Sleeve", "quantity": 3, "price": 24.99, "status": "Shipped"},
{"id": "19283", "customer_id": "5190753", "product": "Wireless Mouse", "quantity": 1, "price": 34.99, "status": "Processing"},
{"id": "74651", "customer_id": "6824095", "product": "Gaming Keyboard", "quantity": 1, "price": 89.99, "status": "Delivered"},
{"id": "30298", "customer_id": "7135680", "product": "Portable Charger", "quantity": 2, "price": 29.99, "status": "Shipped"},
{"id": "47652", "customer_id": "8259147", "product": "Smartwatch", "quantity": 1, "price": 199.99, "status": "Processing"},
{"id": "61984", "customer_id": "9603481", "product": "Noise-Cancelling Headphones", "quantity": 1, "price": 149.99, "status": "Shipped"},
{"id": "58243", "customer_id": "1057426", "product": "Wireless Earbuds", "quantity": 2, "price": 99.99, "status": "Delivered"},
{"id": "90357", "customer_id": "1213210", "product": "Smartphone Case", "quantity": 1, "price": 19.99, "status": "Shipped"},
{"id": "28164", "customer_id": "2837622", "product": "Wireless Headphones", "quantity": 2, "price": 79.99, "status": "Processing"}
]
def get_user(self, key, value):
if key in {"email", "phone", "username"}:
for customer in self.customers:
if customer[key] == value:
return customer
return f"Couldn't find a user with {key} of {value}"
else:
raise ValueError(f"Invalid key: {key}")
return None
def get_order_by_id(self, order_id):
for order in self.orders:
if order["id"] == order_id:
return order
return None
def get_customer_orders(self, customer_id):
return [order for order in self.orders if order["customer_id"] == customer_id]
def cancel_order(self, order_id):
order = self.get_order_by_id(order_id)
if order:
if order["status"] == "Processing":
order["status"] = "Cancelled"
return "Cancelled the order"
else:
return "Order has already shipped. Can't cancel it."
return "Can't find that order!"
tools = [
{
"name": "get_user",
"description": "Looks up a user by email, phone, or username.",
"input_schema": {
"type": "object",
"properties": {
"key": {
"type": "string",
"enum": ["email", "phone", "username"],
"description": "The attribute to search for a user by (email, phone, or username)."
},
"value": {
"type": "string",
"description": "The value to match for the specified attribute."
}
},
"required": ["key", "value"]
}
},
{
"name": "get_order_by_id",
"description": "Retrieves the details of a specific order based on the order ID. Returns the order ID, product name, quantity, price, and order status.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": ["order_id"]
}
},
{
"name": "get_customer_orders",
"description": "Retrieves the list of orders belonging to a user based on a user's customer id.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "The customer_id belonging to the user"
}
},
"required": ["customer_id"]
}
},
{
"name": "cancel_order",
"description": "Cancels an order based on a provided order_id. Only orders that are 'processing' can be cancelled",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order_id pertaining to a particular order"
}
},
"required": ["order_id"]
}
}
]
db = FakeDatabase()
def process_tool_call(tool_name, tool_input):
if tool_name == "get_user":
return db.get_user(tool_input["key"], tool_input["value"])
elif tool_name == "get_order_by_id":
return db.get_order_by_id(tool_input["order_id"])
elif tool_name == "get_customer_orders":
return db.get_customer_orders(tool_input["customer_id"])
elif tool_name == "cancel_order":
return db.cancel_order(tool_input["order_id"])
def simple_chat():
user_message = input("\nUser: ")
messages = [{"role": "user", "content": user_message}]
while True:
#If the last message is from the assistant, get another input from the user
if messages[-1].get("role") == "assistant":
user_message = input("\nUser: ")
messages.append({"role": "user", "content": user_message})
#Send a request to Claude
response = client.messages.create(
model=MODEL_NAME,
max_tokens=4096,
tools=tools,
messages=messages
)
# Update messages to include Claude's response
messages.append(
{"role": "assistant", "content": response.content}
)
#If Claude stops because it wants to use a tool:
if response.stop_reason == "tool_use":
tool_use = response.content[-1] #Naive approach assumes only 1 tool is called at a time
tool_name = tool_use.name
tool_input = tool_use.input
print(f"======Claude wants to use the {tool_name} tool======")
#Actually run the underlying tool functionality on our db
tool_result = process_tool_call(tool_name, tool_input)
#Add our tool_result message:
messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": str(tool_result),
}
],
},
)
else:
#If Claude does NOT want to use a tool, just print out the text reponse
print("\nTechNova Support: " + f"{response.content[0].text}" )
# Start the chat!!
# simple_chat()
以下是一个对话示例:
如您所见,聊天机器人在需要时会调用正确的工具,但实际的聊天响应并不理想。我们可能不希望面向客户的聊天机器人告诉用户它将要调用的具体工具!
系统提示增强 #
我们可以通过为聊天机器人提供可靠的系统提示来改善聊天体验。我们应该做的第一件事就是给 Claude 一些背景信息,并告诉它它是 TechNova 的客户服务助理。
以下是我们系统提示的开头:
system_prompt = """
You are a customer support chat bot for an online retailer called TechNova.
Your job is to help users look up their account, orders, and cancel orders.
Be helpful and brief in your responses.
"""
向 Claude 发出请求时,不要忘记将此提示传递到“系统”参数中!
以下是添加上述系统提示详细信息后的示例对话:
请注意,助手知道它是 TechNova 的支持机器人,并且不再告诉用户有关其工具的信息。可能还值得明确告诉 Claude 根本不要引用工具。
注意:=======Claude 想要使用 cancel_order_tool=======
行是我们添加到脚本中的日志,而不是 Claude 的实际输出!
如果您经常使用此脚本,您可能会注意到其他问题。以下对话说明了其中一个非常明显且非常棘手的问题:
上面的截图显示了整个对话。用户请求帮助取消订单,并表示他们不知道订单 ID。Claude 应该跟进并询问客户的电子邮件、电话号码或用户名,以查找客户,然后找到匹配的订单。相反,Claude 只是决定调用 get_user
工具,而实际上并不知道有关用户的任何信息。Claude 编造了一个电子邮件地址并试图使用它,但当然没有找到匹配的客户。
为了防止这种情况,我们将更新系统提示以包含类似以下内容的语言:
system_prompt = """
You are a customer support chat bot for an online retailer called TechNova.
Your job is to help users look up their account, orders, and cancel orders.
Be helpful and brief in your responses.
You have access to a set of tools, but only use them when needed.
If you do not have enough information to use a tool correctly, ask a user follow up questions to get the required inputs.
Do not call any of the tools unless you have the required data from a user.
"""
以下是使用更新的系统提示与助手对话的屏幕截图:
变好很多!
优化输出格式 #
在使用 Opus 和工具时,模型通常会在 `<thinking>` 或 `<reflection>` 标签中输出其想法,然后再实际响应用户。您可以在下面的屏幕截图中看到一个示例:
这种思考过程往往会带来更好的结果,但显然会带来糟糕的用户体验。我们不想向用户暴露这种思维逻辑!这里最简单的解决方法是明确要求模型在特定的一组 XML 标记中输出面向用户的响应。我们首先更新系统提示:
system_prompt = """
You are a customer support chat bot for an online retailer called TechNova.
Your job is to help users look up their account, orders, and cancel orders.
Be helpful and brief in your responses.
You have access to a set of tools, but only use them when needed.
If you do not have enough information to use a tool correctly, ask a user follow up questions to get the required inputs.
Do not call any of the tools unless you have the required data from a user.
In each conversational turn, you will begin by thinking about your response.
Once you're done, you will write a user-facing response.
It's important to place all user-facing conversational responses in <reply></reply> XML tags to make them easy to parse.
"""
查看对话输出示例:
Claude 现在使用 <reply>
标签来响应实际面向用户的响应。我们现在需要做的就是提取这些标签之间的内容,并确保这是我们实际打印给用户的响应的唯一部分。这是一个更新的 start_chat
函数,它正是这样做的!
import re
def extract_reply(text):
pattern = r'<reply>(.*?)</reply>'
match = re.search(pattern, text, re.DOTALL)
if match:
return match.group(1)
else:
return None
def simple_chat():
system_prompt = """
You are a customer support chat bot for an online retailer called TechNova.
Your job is to help users look up their account, orders, and cancel orders.
Be helpful and brief in your responses.
You have access to a set of tools, but only use them when needed.
If you do not have enough information to use a tool correctly, ask a user follow up questions to get the required inputs.
Do not call any of the tools unless you have the required data from a user.
In each conversational turn, you will begin by thinking about your response.
Once you're done, you will write a user-facing response.
It's important to place all user-facing conversational responses in <reply></reply> XML tags to make them easy to parse.
"""
user_message = input("\nUser: ")
messages = [{"role": "user", "content": user_message}]
while True:
#If the last message is from the assistant, get another input from the user
if messages[-1].get("role") == "assistant":
user_message = input("\nUser: ")
messages.append({"role": "user", "content": user_message})
#Send a request to Claude
response = client.messages.create(
model=MODEL_NAME,
system=system_prompt,
max_tokens=4096,
tools=tools,
messages=messages
)
# Update messages to include Claude's response
messages.append(
{"role": "assistant", "content": response.content}
)
#If Claude stops because it wants to use a tool:
if response.stop_reason == "tool_use":
tool_use = response.content[-1] #Naive approach assumes only 1 tool is called at a time
tool_name = tool_use.name
tool_input = tool_use.input
print(f"======Claude wants to use the {tool_name} tool======")
#Actually run the underlying tool functionality on our db
tool_result = process_tool_call(tool_name, tool_input)
#Add our tool_result message:
messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": str(tool_result),
}
],
},
)
else:
#If Claude does NOT want to use a tool, just print out the text reponse
model_reply = extract_reply(response.content[0].text)
print("\nTechNova Support: " + f"{model_reply}" )
# Start the chat!!
# simple_chat()
以下是显示上述更改影响的屏幕截图:
最终版本 #
这是较长对话的屏幕截图,其中的脚本版本对输出进行了着色。
结束语 #
这是一个教育演示,用于说明工具使用工作流程。此脚本和提示尚未准备好投入生产。助手会回复“您可以在订单确认电子邮件中找到订单 ID”之类的内容,但实际上并不知道这是否属实。在现实世界中,我们需要向 Claude 提供大量有关我们特定公司的背景知识(至少是这样的)。
我们还需要严格测试聊天机器人,并确保它在所有情况下都能正常运行。此外,让任何人都能如此轻松地取消订单可能是一个坏主意!如果您可以访问他们的电子邮件、用户名或电话号码,取消许多人的订单将非常容易。在现实世界中,我们可能需要某种形式的身份验证。长话短说:这是一个演示,而不是完整的聊天机器人实现!
练习 #
- 为我们的助手(以及底层的
FakeDatabase
类)添加功能,允许用户更新他们的电子邮件和电话号码。 - 定义一个名为
get_user_info
的新工具,该工具结合了get_user
和get_customer_orders
的功能。此工具应将用户的电子邮件、电话或用户名作为输入,并一次性返回用户的信息及其订单历史记录。 - 添加错误处理和输入验证!
奖励 #
- 更新聊天机器人以使用实际数据库而不是我们的
FakeDatabase
类。