smolagents 新增视觉语言模型支持
We now support VLMs in smolagents!
Hugging Face 为 smolagents 加上视觉支持,可直接在智能体流水线中使用视觉语言模型。图像既可以在 run 方法开始时以列表传入,由 TaskStep 的 task_images 属性保存,也可以通过 step_callbacks 在每一步把新截图写入 ActionStep 的 observation_images。
官方给出视觉能力接入智能体管线的两种方式,并附可运行的浏览器智能体示例,便于判断多模态 Agent 的落地路径。
你这假冒为善的人,先去掉自己眼中的梁木,然后才能看得清楚,去掉你弟兄眼中的刺。马太福音 7:3-5
TL;DR
我们已经为 smolagents 添加了视觉支持,这使得可以在代理管道中原生使用视觉语言模型。
目录
概述
在代理领域,许多能力都隐藏在视觉障碍之后。一个常见的例子是网页浏览:网页包含丰富的视觉内容,仅通过提取文本无法完全恢复,比如物体的相对位置、通过颜色传达的信息、特定图标……在这种情况下,视觉是代理的真正超能力。因此,我们刚刚为我们的 smolagents 添加了这一能力!
先睹为快:一个完全自主导航网络的代理浏览器!
以下是它的一个示例:
我们如何赋予 smolagents 视觉能力
🤔 我们如何向代理传递图像?传递图像可以通过两种方式:
- 你可以让图像在代理启动时直接可用。这在 Document AI 中很常见。
- 有时,需要动态添加图像。一个很好的例子是网页浏览器刚刚执行了一个操作,需要查看其对视口的影响。
1. 在代理启动时传递一次图像
对于希望一次性传递图像的情况,我们添加了在 run 方法中向代理传递图像列表的功能:agent.run("Describe these images:", images=[image_1, image_2])。
这些图像输入随后与您想要完成的任务提示一起存储在 TaskStep 的 task_images 属性中。
运行代理时,它们将被传递给模型。这在基于包含视觉元素的长 PDF 采取行动等情况下非常方便。
2. 在每一步传递图像 ⇒ 使用回调
如何动态地将图像添加到代理的内存中?
要弄清楚这一点,我们首先需要了解我们的代理是如何工作的。
smolagents 中的所有代理都基于单一的 MultiStepAgent 类,这是 ReAct 框架的抽象。在基本层面上,该类按照以下步骤循环执行操作,其中现有的变量和知识按如下方式合并到代理日志中:
- 初始化:系统提示存储在
SystemPromptStep中,用户查询记录到TaskStep中。 - ReAct Loop (While):
- 使用
agent.write_inner_memory_from_logs()将代理日志写入一个 LLM 可读的 聊天消息列表。 - 将这些消息发送到
Model对象以获取其补全结果。解析补全结果以获取操作(对于ToolCallingAgent是 JSON 数据块,对于CodeAgent是代码片段)。 - 执行操作并将结果记录到内存中(一个
ActionStep)。 - 在每一步结束时,运行
agent.step_callbacks中定义的所有回调函数。 ⇒ 这就是我们添加图像支持的地方:创建一个将图像记录到内存中的回调!
- 使用
下图详细说明了这个过程:
如你所见,对于动态检索图像的使用场景(例如网页浏览器代理),我们支持将图像添加到模型的 ActionStep 中,即属性 step_log.observation_images。
这可以通过回调来完成,回调将在每一步结束时运行。
让我们演示如何制作这样的回调,并用它来构建一个网页浏览器代理。👇👇
如何创建一个具有视觉功能的网页浏览代理
我们将使用 helium。它基于 selenium 提供浏览器自动化功能:这将让我们的智能体更容易操作网页。
pip install "smolagents[all]" helium selenium python-dotenv
智能体本身可以直接使用 helium,因此不需要专门的工具:它可以直接使用 helium 执行操作,例如 click("top 10") 来点击页面上可见的名为 "top 10" 的按钮。
我们仍然需要制作一些工具来帮助智能体浏览网页:一个返回上一页的工具,以及另一个关闭弹窗的工具,因为对于 helium 来说这些相当难以捕捉,因为它们的关闭按钮上没有任何文本。
from io import BytesIO
from time import sleep
import helium
from dotenv import load_dotenv
from PIL import Image
from selenium import webdriver
from selenium.common.exceptions import ElementNotInteractableException, TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from smolagents import CodeAgent, LiteLLMModel, OpenAIServerModel, TransformersModel, tool
from smolagents.agents import ActionStep
load_dotenv()
import os
@tool
def search_item_ctrl_f(text: str, nth_result: int = 1) -> str:
"""
Searches for text on the current page via Ctrl + F and jumps to the nth occurrence.
Args:
text: The text to search for
nth_result: Which occurrence to jump to (default: 1)
"""
elements = driver.find_elements(By.XPATH, f"//*[contains(text(), '{text}')]")
if nth_result > len(elements):
raise Exception(f"Match n°{nth_result} not found (only {len(elements)} matches found)")
result = f"Found {len(elements)} matches for '{text}'."
elem = elements[nth_result - 1]
driver.execute_script("arguments[0].scrollIntoView(true);", elem)
result += f"Focused on element {nth_result} of {len(elements)}"
return result
@tool
def go_back() -> None:
"""Goes back to previous page."""
driver.back()
@tool
def close_popups() -> str:
"""
Closes any visible modal or pop-up on the page. Use this to dismiss pop-up windows! This does not work on cookie consent banners.
"""
# Common selectors for modal close buttons and overlay elements
modal_selectors = [
"button[class*='close']",
"[class*='modal']",
"[class*='modal'] button",
"[class*='CloseButton']",
"[aria-label*='close']",
".modal-close",
".close-modal",
".modal .close",
".modal-backdrop",
".modal-overlay",
"[class*='overlay']"
]
wait = WebDriverWait(driver, timeout=0.5)
for selector in modal_selectors:
try:
elements = wait.until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, selector))
)
for element in elements:
if element.is_displayed():
try:
# Try clicking with JavaScript as it's more reliable
driver.execute_script("arguments[0].click();", element)
except ElementNotInteractableException:
# If JavaScript click fails, try regular click
element.click()
except TimeoutException:
continue
except Exception as e:
print(f"Error handling selector {selector}: {str(e)}")
continue
return "Modals closed"
目前,智能体没有视觉输入。
所以让我们演示如何通过回调在它的步骤日志中动态地向它提供图像。
我们制作一个回调 save_screenshot,它将在每个步骤结束时运行。
def save_screenshot(step_log: ActionStep, agent: CodeAgent) -> None:
sleep(1.0) # Let JavaScript animations happen before taking the screenshot
driver = helium.get_driver()
current_step = step_log.step_number
if driver is not None:
for step_logs in agent.logs: # Remove previous screenshots from logs for lean processing
if isinstance(step_log, ActionStep) and step_log.step_number <= current_step - 2:
step_logs.observations_images = None
png_bytes = driver.get_screenshot_as_png()
image = Image.open(BytesIO(png_bytes))
print(f"Captured a browser screenshot: {image.size} pixels")
step_log.observations_images = [image.copy()] # Create a copy to ensure it persists, important!
# Update observations with current URL
url_info = f"Current url: {driver.current_url}"
step_log.observations = url_info if step_logs.observations is None else step_log.observations + "\n" + url_info
return
这里最重要的一行是当我们在 observations images 中添加图像时:step_log.observations_images = [image.copy()]。
这个回调同时接受 step_log 和 agent 本身作为参数。将 agent 作为输入允许执行比仅仅修改最后几条日志更深层的操作。
让我们来创建一个模型。我们已经在所有模型中添加了对图像的支持。
只是一点说明:当将 TransformersModel 与 VLM 一起使用时,为了使其正常工作,你需要在初始化时传入
flatten_messages_as_text 作为 False,就像这样:
model = TransformersModel(model_id="HuggingFaceTB/SmolVLM-Instruct", device_map="auto", flatten_messages_as_text=False)
对于这个演示,让我们通过 Fireworks API 使用更大的 Qwen2VL:
model = OpenAIServerModel(
api_key=os.getenv("FIREWORKS_API_KEY"),
api_base="https://api.fireworks.ai/inference/v1",
model_id="accounts/fireworks/models/qwen2-vl-72b-instruct",
)
现在让我们继续定义我们的智能体。我们将 verbosity_level 设置为最高,以显示 LLM 的完整输出消息来查看其思考过程,并将 max_steps 增加到 20,以便给智能体更多步骤来探索网页。
我们还为它提供了上面定义的回调 save_screenshot。
agent = CodeAgent(
tools=[go_back, close_popups, search_item_ctrl_f],
model=model,
additional_authorized_imports=["helium"],
step_callbacks = [save_screenshot],
max_steps=20,
verbosity_level=2
)
最后,我们为智能体提供一些关于使用 helium 的指导。
helium_instructions = """
You can use helium to access websites. Don't bother about the helium driver, it's already managed.
First you need to import everything from helium, then you can do other actions!
Code:
```py
from helium import *
go_to('github.com/trending')
```<end_code>
You can directly click clickable elements by inputting the text that appears on them.
Code:
```py
click("Top products")
```<end_code>
If it's a link:
Code:
```py
click(Link("Top products"))
```<end_code>
If you try to interact with an element and it's not found, you'll get a LookupError.
In general stop your action after each button click to see what happens on your screenshot.
Never try to login in a page.
To scroll up or down, use scroll_down or scroll_up with as an argument the number of pixels to scroll from.
Code:
```py
scroll_down(num_pixels=1200) # This will scroll one viewport down
```<end_code>
When you have pop-ups with a cross icon to close, don't try to click the close icon by finding its element or targeting an 'X' element (this most often fails).
Just use your built-in tool `close_popups` to close them:
Code:
```py
close_popups()
```<end_code>
You can use .exists() to check for the existence of an element. For example:
Code:
```py
if Text('Accept cookies?').exists():
click('I accept')
```<end_code>
Proceed in several steps rather than trying to solve the task in one shot.
And at the end, only when you have your answer, return your final answer.
Code:
```py
final_answer("YOUR_ANSWER_HERE")
```<end_code>
If pages seem stuck on loading, you might have to wait, for instance `import time` and run `time.sleep(5.0)`. But don't overuse this!
To list elements on page, DO NOT try code-based element searches like 'contributors = find_all(S("ol > li"))': just look at the latest screenshot you have and read it visually, or use your tool search_item_ctrl_f.
Of course, you can act on buttons like a user would do when navigating.
After each code blob you write, you will be automatically provided with an updated screenshot of the browser and the current browser url.
But beware that the screenshot will only be taken at the end of the whole action, it won't see intermediate states.
Don't kill the browser.
"""
运行智能体
现在一切都准备好了:让我们运行我们的智能体!
github_request = """
I'm trying to find how hard I have to work to get a repo in github.com/trending.
Can you navigate to the profile for the top author of the top trending repo, and give me their total number of commits over the last year?
"""
agent.run(github_request + helium_instructions)
但请注意,这个任务真的很难:取决于你使用的 VLM,这可能并不总是有效。像 Qwen2VL-72B 或 GPT-4o 这样强大的 VLM 成功率更高。
后续步骤
这将让你一窥支持视觉的 CodeAgent 的能力,但还有更多事情要做!
- 你可以在这里开始使用智能体网络浏览器。
- 在我们的公告博客文章中阅读更多关于 smolagents 的内容。
- 阅读 smolagents 文档。
我们期待看到你将用视觉语言模型和 smolagents 构建出什么!
来源:Hugging Face Blog · hf-mirror.com