在本系列的上篇中,我们拿介绍了如何在亚马逊云科技(Amazon Bedrock)上集成Anthropic的Model Context Protocol(MCP),并使用Python构建MCP客户端和服务器的技术方案。文章涵盖了MCP的概念、优势、架构设计及实现流程,重点展示了如何通过MCP搭建一个智能代理系统环境的配置步骤,实现博客文章摘要生成和链接检查功能。在本系列下篇文章中我们会分享具体业务逻辑实现的源代码和步骤。欢迎大家关注小李哥,不要错过未来更多的国际前沿AI技术方案和动态
定义客户端
为了简化,我们将客户端的所有业务逻辑封装在一个类MCPClient中。
# client.py
class MCPClient:
MODEL_ID = "anthropic.claude-3-sonnet-20240229-v1:0"
def __init__(self):
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.bedrock = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')
async def connect_to_server(self, server_script_path: str):
if not server_script_path.endswith(('.py', '.js')):
raise ValueError("Server script must be a .py or .js file")
command = "python" if server_script_path.endswith('.py') else "node"
server_params = StdioServerParameters(command=command, args=[server_script_path], env=None)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()
response = await self.session.list_tools()
print("\nConnected to server with tools:", [tool.name for tool in response.tools])
async def cleanup(self):
await self.exit_stack.aclose()
-
self.session
是我们正在建立的MCP会话对象。在本代码示例中,我们将使用标准输入/输出(stdio)与MCP交互,我们使用的工具和服务器托管在同一台机器上。 -
self.bedrock
创建了一个亚马逊云科技Bedrock客户端,它提供与Amazon Bedrock运行时API交互功能,使大家可以调用converse
API与基础模型进行通信。 -
self.exit_stack = AsyncExitStack()
创建了一个上下文管理器,帮助管理多个异步资源(如网络连接和文件句柄),并在程序退出时按相反顺序自动释放它们,类似于一堆嵌套的async with
语句,但更加灵活和程序化。我们将在cleanup
方法中使用self.exit_stack
以确保资源释放。 connect_to_server
方法用于建立与通过Python或Node.js脚本创建MCP服务器的双向通信通道,使用标准输入/输出(stdio)进行消息传递,并初始化一个会话,使客户端能够发现并调用服务器脚本提供的工具。
处理查询
接下来,我们深入代码逻辑的核心代码部分。
# client.py
def _make_bedrock_request(self, messages: List[Dict], tools: List[Dict]) -> Dict:
return self.bedrock.converse(
modelId=self.MODEL_ID,
messages=messages,
inferenceConfig={"maxTokens": 1000, "temperature": 0},
toolConfig={"tools": tools}
)
async def process_query(self, query: str) -> str:
# (1)
messages = [Message.user(query).__dict__]
# (2)
response = await self.session.list_tools()
# (3)
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
} for tool in response.tools]
bedrock_tools = Message.to_bedrock_format(available_tools)
# (4)
response = self._make_bedrock_request(messages, bedrock_tools)
# (6)
return await self._process_response( # (5)
response, messages, bedrock_tools
)
_make_bedrock_request
方法是一个私有辅助方法,它向Amazon Bedrock的Converse
API发送请求,传递对话历史(messages
)、可用工具,以及模型配置参数(如token
限制和temperature等
),从基础模型中获取下一轮对话的响应。我们将在多个方法中使用这个方法。process_query
方法负责处理和编排整个查询流程:- 从用户的问题中创建一条消息
- 获取连接服务器上可用的工具
- 格式化这些工具,使其符合Bedrock的API格式
- 使用用户问题和可用工具向Bedrock发送请求
- 处理响应,可能需要多轮对话(如果涉及到外部工具的调用)
- 返回最终的响应
这个方法是处理用户查询的入口,用户管理用户、工具和基础模型之间的对话流程。接下来我们将深入了解对话是如何进行的。
循环对话
以下代码实现了一个对话循环,处理来自用户发送的和Bedrock返回的各种请求。让我们详细介绍它的工作原理:
# client.py
async def _process_response(self, response: Dict, messages: List[Dict], bedrock_tools: List[Dict]) -> str:
# (1)
final_text = []
MAX_TURNS=10
turn_count = 0
while True:
# (2)
if response['stopReason'] == 'tool_use':
final_text.append("received toolUse request")
for item in response['output']['message']['content']:
if 'text' in item:
final_text.append(f"[Thinking: {item['text']}]")
messages.append(Message.assistant(item['text']).__dict__)
elif 'toolUse' in item:
# (3)
tool_info = item['toolUse']
result = await self._handle_tool_call(tool_info, messages)
final_text.extend(result)
response = self._make_bedrock_request(messages, bedrock_tools)
# (4)
elif response['stopReason'] == 'max_tokens':
final_text.append("[Max tokens reached, ending conversation.]")
break
elif response['stopReason'] == 'stop_sequence':
final_text.append("[Stop sequence reached, ending conversation.]")
break
elif response['stopReason'] == 'content_filtered':
final_text.append("[Content filtered, ending conversation.]")
break
elif response['stopReason'] == 'end_turn':
final_text.append(response['output']['message']['content'][0]['text'])
break
turn_count += 1
if turn_count >= MAX_TURNS:
final_text.append("\n[Max turns reached, ending conversation.]")
break
# (5)
return "\n\n".join(final_text)
_process_response
方法初始化了一个最大10轮的对话(MAX_TURNS
),并用final_text
追踪大语言模型的响应内容。- 当模型请求使用工具时,它会处理CoT思考步骤(文本)和工具执行步骤(
toolUse
)。 - 在调用工具时,该方法也会调用工具处理器,并使用工具返回的结果向Bedrock发送新请求。请记住,我们的工具是托管在本地MCP服务器上的。
- 此外该方法会处理各种终止条件(如最大token限制、内容过滤、停止序列、对话结束等),在合适的时候追加消息并终止循环。
- 最后它会将多轮荻花累积的文本用换行符连接起来,并返回完整的对话历史。
处理工具请求
# client.py
async def _handle_tool_call(self, tool_info: Dict, messages: List[Dict]) -> List[str]:
# (1)
tool_name = tool_info['name']
tool_args = tool_info['input']
tool_use_id = tool_info['toolUseId']
# (2)
result = await self.session.call_tool(tool_name, tool_args)
# (3)
messages.append(Message.tool_request(tool_use_id, tool_name, tool_args).__dict__)
messages.append(Message.tool_result(tool_use_id, result.content).__dict__)
# (4)
return [f"[Calling tool {tool_name} with args {tool_args}]"]
_handle_tool_call
方法用于大模型要执行工具请求,它从提供的信息中提取工具名称、参数和ID。- 它通过Session的会话接口调用工具,并等待其返回结果。
- 该方法会将工具请求和结果都记录到对话历史中。这样可以让Bedrock知道,我们的对话过程中还涉及了一个外部工具(即运行在本地机器上的工具)。
- 最终它会返回一个格式化的消息,指明调用了哪个工具,使用了哪些参数。
这个方法本质上是模型的调用工具请求(来自Bedrock)和实际工具执行系统(运行在本机)之间的桥梁。
与MCPserver对话聊天测试
chat_loop
方法创建了一个简单的交互式命令行界面,它会不断接受用户输入,通过MCP服务器系统处理用户查询,并显示大模型最终的响应,直到用户输入quit
或发生错误时停止。
# client.py
async def chat_loop(self):
print("\nMCP Client Started!\nType your queries or 'quit' to exit.")
while True:
try:
query = input("\nQuery: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query)
print("\n" + response)
except Exception as e:
print(f"\nError: {str(e)}")
主函数代码部分
# client.py
async def main():
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)
client = MCPClient()
try:
await client.connect_to_server(sys.argv[1])
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
asyncio.run(main())
在主函数部分我们验证了命令行参数,并初始化一个MCP客户端,用到连接到指定的服务器。然后,我们在异步上下文中运行聊天循环,并通过Python的asyncio
来管理异步执行流程,确保资源得到正确释放。
实验结果展示
这里有一个演示视频,展示了我们构建的客户端如何与天气工具交互,这是Anthropic发布的官方教程的一部分。
我们将分别测试瑞安两个工具:
- 天气警报工具:帮助我们获取美国某个州的天气警报
示例输入:"get me a summary of the weather alerts in California"
- 天气预报工具:帮助我们获取美国某个城市的天气预报
示例输入:"get me a summary of the weather forecast for Buffalo, NY"
运行对话主文件
在命令行中使用uv
运行client.py
,不要忘记添加工具存放的路径。
uv run client.py ../weather/weather.py
用MCP自动化博客文章审核
现在我们已经搭建好了MCP客户端和服务器,可以开始解决真实的业务场景的需求了。在本示例中我们将构建自定义外部API调用工具,为大语言模型提供网页浏览能力。
更新MCP服务器非常简单,只需向服务器文件添加新函数,或者创建一个新的服务器文件即可。在本示例中,我们将在现有服务器文件中添加几个非常强大的功能,并展示Claude如何智能地将它们组合起来执行任务。在编写代码之前,请确保切换到MCP服务器文件夹,并安装额外的依赖项。
cd ../weather
uv add requests markdownify
访问网页
我们为大语言模型提供了一个定义工具的HTTP客户端,以访问网页并提取其中的Markdown内容。
# wheather.py (I know, I know...)
import re
import requests
from markdownify import markdownify
from requests.exceptions import RequestException
@mcp.tool()
def visit_webpage(url: str) -> str:
"""Visits a webpage at the given URL and returns its content as a markdown string.
Args:
url: The URL of the webpage to visit.
Returns:
The content of the webpage converted to Markdown, or an error message if the request fails.
"""
try:
# Send a GET request to the URL
response = requests.get(url, timeout=30)
response.raise_for_status() # Raise an exception for bad status codes
# Convert the HTML content to Markdown
markdown_content = markdownify(response.text).strip()
# Remove multiple line breaks
markdown_content = re.sub(r"\n{3,}", "\n\n", markdown_content)
return markdown_content
except RequestException as e:
return f"Error fetching the webpage: {str(e)}"
except Exception as e:
return f"An unexpected error occurred: {str(e)}"
visit_webpage
函数定义的就是API调用工具,它使用HTTP GET请求从指定URL获取内容,并将获取的HTML内容转换为更清晰的Markdown格式,去除多余的换行符,并处理边缘情况。该函数还包含全面的错误处理,以应对网络问题和意外错误,并在出现问题时返回适当的错误消息。
链接有效性验证
validate_links
函数用于接收一个URL列表,并检查每个URL是否是有效、可访问的网页。它会对每个URL执行HTTP GET请求,并根据请求是否成功且返回的内容是否为空来判断链接是否有效。该函数返回一个“URL-有效性”键值
对列表,其中每个键值对为URL及其有效性(布尔值),同时函数中包含对网络错误和一般异常的处理逻辑。
# weather.py
@mcp.tool()
def validate_links(urls: list[str]) -> list[str, bool]:
"""Validates that the links are valid webpages.
Args:
urls: The URLs of the webpages to visit.
Returns:
A list of the url and boolean of whether or not the link is valid.
"""
output = []
for url in urls:
try:
# Send a GET request to the URL
response = requests.get(url, timeout=30)
response.raise_for_status() # Raise an exception for bad status codes
print('validateResponse',response)
# Check if the response content is not empty
if response.text.strip():
output.append([url, True])
else:
output.append([url, False])
except RequestException as e:
output.append([url, False])
print(f"Error fetching the webpage: {str(e)}")
except Exception as e:
output.append([url, False])
print(f"An unexpected error occurred: {str(e)}")
return output
进一步增强工具能力
最令我印象深刻的是即使没有明显提前安排,Claude依然能够规划并组合这些工具以实现复杂的任务。在本示例中我们的MCP系统首先下载了网页内容,然后提取所有链接,验证所有链接的有效性,并最终返回一个总结给用户。
总结
这篇技术文档详细介绍了如何使用MCP客户端和服务器处理自然语言查询、调用本地工具、进行多轮对话以及添加更多大模型Agent外部API调用工具,用于博客文章有效性审核。大家可以根据自身需求,扩展MCP服务器的功能,让基础模型在更多场景中发挥更大作用!