终极Flutter开发资源指南:10个免费工具加速跨平台应用构建
CrewAI + Browserbase 打造多智能体酒店预订系统:hotel-booking-crew 本地化实战解析
本篇技术指南围绕开源仓库 ai-engineering-hub 中的 hotel-booking-crew 实战项目展开,讲解如何用 CrewAI 编排多智能体、以 Browserbase 无头浏览器作为 Agent 的网页工具、以本地推理(DeepSeek-R1 via Ollama)或云端 API 作为 LLM 后端,构建一个能够在真实预订网站上自动检索酒店、对比房型与报价的 Streamlit 应用。读完本文,你将掌握这套"多智能体 + 浏览器工具 + 流式 UI"的完整落地链路,并理解仓库中各源码文件(app.py、app_openai.py、browserbase.py、kayak.py)各自的职责与调用关系。
项目概览:一个本地优先的多智能体酒店搜索 Crew
hotel-booking-crew/README.md 将本项目的核心定位概括为:构建一个 100% 本地运行的多智能体酒店预订 Crew,目标是自动为用户找到性价比最高、最合适的酒店。它包含两条关键技术选型:
- Browserbase:提供可远程访问的浏览器实例,项目基于它封装了一个"无头浏览器"工具,让 Agent 能真实打开并读取 Kayak 等酒店聚合站点的搜索结果;
- CrewAI:负责多智能体的角色设定、任务编排与协作执行。
README 同时说明其模型接入意图:主程序 app.py 面向本地运行的 DeepSeek-R1(通过 Ollama 拉起),app_openai.py 则依赖 OpenAI API。这种"一个界面、两种模型后端"的设计,既保证了想完全离线运行时可行,也保留了使用云端模型快速验证的入口。
从当前仓库快照看,hotel-booking-crew 目录共包含以下文件,它们共同构成完整的可运行应用:
| 文件 | 职责 |
|---|---|
| hotel-booking-crew/README.md | 项目说明、依赖同步与环境变量指南 |
| hotel-booking-crew/pyproject.toml | 依赖声明与版本约束(uv 项目元数据) |
| hotel-booking-crew/app.py | Streamlit 主界面 + CrewAI 编排(显式指定 LLM 模型) |
| hotel-booking-crew/app_openai.py | 同构的备用入口,走 CrewAI 默认 OpenAI 模型 |
| hotel-booking-crew/browserbase.py | 基于 Playwright 封装的 Browserbase 浏览器加载工具 |
| hotel-booking-crew/kayak.py | 生成 Kayak 酒店搜索 URL 的工具 |
| hotel-booking-crew/uv.lock | 锁定依赖版本,保证可复现安装 |
多智能体编排流程:两个 Agent 与两个 Task
从 app.py 与 app_openai.py 的源码可以看到,应用定义了两个 CrewAI Agent 与两个 Task:
- Hotels Agent(角色 "Hotels"):目标是"搜索酒店",绑定了
kayak_hotels与browserbase两个工具,并设置allow_delegation=False,不允许向其他 Agent 转交工作。它承担所有需要访问真实网页的检索动作。 - Summarize Agent(角色 "Summarize"):目标是"汇总酒店信息与设施",不带任何工具,只负责将检索结果整理成结构清晰、便于用户阅读的文本。它也关闭了委派。
两个任务均挂在 Hotels Agent 上:
- search_task:根据用户输入的标准(如地点、日期、入住人数)搜索酒店,
description中通过模板变量注入动态信息:"Search hotels according to criteria {request}. Current year: {current_year}"; - search_booking_providers_task:加载酒店详情,找出可用的预订渠道及其价格,即执行比价。
这里值得留意的是 expected_output 字段。源码为每个任务都写死了"输出范例":
- search_task 期望输出形如"纽约 9 月 21-22 日 Top 5 酒店",每条包含 Rating、Price、Location、Amenities 与 Booking 链接;
- search_booking_providers_task 期望输出包含 Room Types、Price Range、Special Offers 及多个 Booking Options(Kayak / Hotels.com / 直订价)。
在 CrewAI 中,expected_output 会作为约束条件进入 LLM 的提示词,它本质上是格式规范器——用结构化范例引导模型输出固定字段,避免自由发挥导致结果无法解析。这也是把多 Agent 输出变得稳定可用的关键技巧。
两个任务按顺序流程(Crew 默认 Process)在 Agent 上依次执行:先由 Hotels Agent 用浏览器搜出候选酒店清单,再加载各家酒店详情与报价,最后交给 Summarize Agent 汇总为最终答复。Crew 实例的组装与执行在 app.py:
crew = Crew(
agents=[hotels_agent, summarize_agent],
tasks=[search_task, search_booking_providers_task],
max_rpm=1,
verbose=True,
planning=True,
llm=load_llm(),
)
result = crew.kickoff(
inputs={
"request": request,
"current_year": datetime.date.today().year,
}
)
其中 planning=True 会启用 CrewAI 的计划阶段,让 LLM 先规划任务执行顺序;max_rpm=1 将请求速率限制在每分钟 1 次,降低对模型的调用压力。两个文件在此存在参数差异:app.py 为 max_rpm=1,而 app_openai.py 为 max_rpm=100,且后者未在 Agent/Crew 层显式注入 LLM,改用 CrewAI 默认的 OpenAI 配置。
环境准备与依赖安装
README 给出的起步命令非常简洁——使用 uv 同步项目依赖:
uv sync
仓库根目录的 pyproject.toml 记录了完整依赖与约束:
[project]
name = "hotel-booking-crew"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"browserbase>=1.2.0",
"crewai-tools>=0.38.1",
"html2text>=2024.2.26",
"playwright>=1.51.0",
"python-dotenv>=1.1.0",
"streamlit>=1.44.1",
]
各依赖在本项目中的实际用途如下:
| 依赖 | 版本下限 | 用途 |
|---|---|---|
| streamlit | 1.44.1 | 构建搜索表单、展示结果的 Web UI |
| crewai-tools | 0.38.1 | 提供 @tool 装饰器,将函数包装为 Agent 可调用的工具 |
| playwright | 1.51.0 | 无头浏览器核心,负责连接 Browserbase 的远端 Chromium |
| browserbase | 1.2.0 | Browserbase SDK(作为会话相关的配套依赖声明) |
| html2text | 2024.2.26 | 将浏览器抓取的 HTML 转为纯文本,方便 LLM 阅读 |
| python-dotenv | 1.1.0 | 从 .env 加载环境变量 |
需要说明:项目要求 Python >= 3.12,这是运行前的重要前提。uv.lock 已锁定全部传递依赖的具体版本,uv sync 后可获得一致的运行环境。
环境变量配置:BROWSERBASE_API_KEY 与可选的 OPENAI_API_KEY
README 明确列出了两类需要配置的环境变量:
BROWSERBASE_API_KEY=... # 必填,browserbase.py 连接浏览器依赖它
OPENAI_API_KEY=... # 仅运行 app_openai.py 时需要,本地运行可忽略
| 变量 | 是否必需 | 说明 |
|---|---|---|
| BROWSERBASE_API_KEY | 必需 | 用于 wss://connect.browserbase.com 的鉴权,代码在 browserbase.py 中直接以 os.environ["BROWSERBASE_API_KEY"] 读取 |
| OPENAI_API_KEY | 仅 app_openai.py 需要 | app_openai.py 未显式指定 LLM,CrewAI 默认走 OpenAI 接口 |
README 建议以 .env.example 为参照创建自己的 .env 文件,再运行应用。需要说明的是:当前仓库快照的 hotel-booking-crew 目录中并未附带 .env.example 文件(目录内仅有 README、四个 .py、pyproject.toml 与 uv.lock),因此实际操作时只需自行按上表在项目目录下手动创建 .env 并填入 BROWSERBASE_API_KEY 即可。
两个应用文件都通过 load_dotenv()(app.py、app_openai.py)在启动时把 .env 内容加载进 os.environ。此外,app.py 还在 Streamlit 侧边栏提供了密码输入框,用户可以直接在界面上输入 Browserbase API Key,代码随即写入 os.environ["BROWSERBASE_API_KEY"](app.py),因此存在两条配置通道:环境变量文件,或运行时 UI 输入。
模型接入:本地 DeepSeek-R1 与云端 API 两种模式
README 描述的设计意图是 app.py 走本地 DeepSeek-R1(Ollama),而 app_openai.py 需要 OpenAI Key。从源码结构看,两个入口文件只有少量差异,可以交叉印证这种双后端设计:
- app_openai.py:Agent 与 Crew 均未指定
llm参数,CrewAI 框架会使用默认的 OpenAI 模型配置,这正是它要求配置 OPENAI_API_KEY 的原因; - app.py:通过
load_llm()封装并缓存 LLM 实例(app.py):
@st.cache_data(show_spinner=False)
def load_llm():
"""Initialize and return Groq LLM with caching"""
return LLM(model="groq/meta-llama/llama-4-scout-17b-16e-instruct")
需要如实指出一个仓库内部的不一致:README 声称 app.py 面向本地 Ollama DeepSeek-R1,但当前仓库快照中 app.py 的 load_llm() 实际上把模型字符串指向了 Groq 托管的 llama-4-scout-17b-16e-instruct,且 pyproject.toml 依赖中也未包含 ollama 相关依赖。因此,就当前代码而言,"100% 本地"取决于你如何配置这一模型入口。若希望与 README 描述保持一致、真正改成本地推理,需要做的核心改动点非常集中——只有 app.py 中 LLM(model=...) 这一处:将模型字符串替换为指向本地 Ollama 服务的标识(如 ollama/<模型名>,前提是已用 Ollama 拉取 DeepSeek-R1 模型),即可让整个 Crew 的 Agent 与规划过程都落到本地模型上,这也是该项目"本地化优先"设计意图的落点。
从工程角度,这种"把 LLM 实例抽成一个函数、并在 Agent 和 Crew 两级统一传入"的做法值得借鉴:它让模型选型成为一个单点可切换的配置项,Agent 逻辑本身与后端模型解耦,从而在同一套多智能体代码上快速实验本地模型与云端模型。
Agent 工具拆解:浏览器抓取与搜索 URL 构造
CrewAI 的 Agent 本身没有联网能力,全部"行动力"来自工具。本项目只有两个自定义工具,职责非常清晰。
Browserbase 无头浏览器工具(browserbase.py)
browserbase.py 定义了被两个 Agent 共用的核心工具,用于把"打开网页、读取结果"变成 LLM 可直接调用的函数:
@tool("Browserbase tool")
def browserbase(url: str):
"""
Loads a URL using a headless webbrowser
:param url: The URL to load
:return: The text content of the page
"""
with sync_playwright() as playwright:
browser = playwright.chromium.connect_over_cdp(
"wss://connect.browserbase.com?apiKey="
+ os.environ["BROWSERBASE_API_KEY"]
)
context = browser.contexts[0]
page = context.pages[0]
page.goto(url)
# Wait for the flight search to finish
sleep(25)
content = html2text(page.content())
browser.close()
return content
实现要点分析:
- 连接方式:使用 Playwright 的
connect_over_cdp通过 WebSocket(wss://connect.browserbase.com+ API Key)连接 Browserbase 托管的 Chromium,而不是本地拉起浏览器,因此无需在本地维护浏览器二进制与无头环境; - 等待策略:
page.goto(url)后sleep(25)硬编码等待约 25 秒,让目标站点的搜索结果完成渲染。从源码注释看,这段逻辑最初是为航班搜索("Wait for the flight search to finish")设计的,直接复用在酒店搜索上。由于是固定延时而非等待选择器,实测中如页面加载更慢可自行调大该值,这是后续可优化的点; - 文本化输出:调用
html2text(page.content())把页面 HTML 转成 Markdown 风格纯文本后返回——LLM 无法消化整段 HTML,但能高效阅读结构化文本,工具与模型的输入输出边界由此闭合; - 注册方式:函数外层用
crewai.tools的@tool("Browserbase tool")装饰器把普通函数升级为 CrewAI 工具,工具名称与 docstring 会一并进入 Agent 的提示词,作为 LLM 决定"何时调用、传什么参数"的依据。
Kayak 搜索 URL 构造工具(kayak.py)
kayak.py 的工具逻辑更简单——它不抓取页面,而是把搜索参数拼成标准化的 Kayak 酒店搜索 URL,作为 browserbase 工具的前置输入:
@tool("Kayak Hotel Tool")
def kayak_hotel_search(
location_query: str, check_in_date: str, check_out_date: str, num_adults: int = 2
) -> str:
"""
Generates a Kayak URL for hotel searches based on location, dates, and number of adults.
:param location_query: The location string used by Kayak (e.g., 'Hisar,Haryana,India-p15321')
:param check_in_date: The check-in date in 'YYYY-MM-DD' format
:param check_out_date: The check-out date in 'YYYY-MM-DD' format
:param num_adults: The number of adults (defaults to 2)
:return: The Kayak URL for the hotel search
"""
URL = f"https://www.kayak.co.in/hotels/{location_query}/{check_in_date}/{check_out_date}/{num_adults}adults"
return URL
可以看到 URL 模板的组成:/hotels/{地点}/{入住日期}/{退房日期}/{成人数}adults,地点需要符合 Kayak 内部编码(如 Hisar,Haryana,India-p15321 这种 城市,地区,国家-编码 形态,docstring 给出了示例),日期使用 YYYY-MM-DD。该工具导出的实例被重命名为 kayak_hotels(kayak_hotels = kayak_hotel_search),两个 app 文件中 Hotels Agent 的 tools=[kayak_hotels, browserbase] 引用的正是它。
典型调用链可以归纳为:LLM 先调用 kayak_hotels(地点, 日期, 成人数) 得到目标 URL → 再把该 URL 传给 browserbase(url) 让真实浏览器打开并抓取文本 → 抓取结果回流给 LLM 提炼成结构化酒店信息。两个工具一个负责"定位目标页面",一个负责"获取页面内容",职责互补。
从 Streamlit 表单到 Crew 执行:完整调用链
应用在 Streamlit 中提供了完整的酒店搜索表单(app.py):地点(文本输入)、成人数量(1-10 的数值输入,默认 2)、入住与退房日期(datetime.date 选择器,退房默认比入住晚一天)。
点击 "Search Hotels" 按钮后,进入点击回调执行逻辑,包含三层前置校验与一次 Crew 执行(app.py):
- 校验 API Key:
os.environ中没有BROWSERBASE_API_KEY时直接提示先到侧边栏输入; - 校验日期合法性:
check_out_date <= check_in_date时报错"退房日期必须晚于入住日期"; - 构造请求文本:把表单字段拼装成一句自然语言指令——
"hotels in {地点} from {月 日} to {月 日} for {N} adults"; - 执行 Crew:以该请求为
inputs["request"]、以当年年份为inputs["current_year"]调用crew.kickoff(...),任务模板中的{request}、{current_year}占位符在此被替换; - 渲染结果:成功后
st.markdown(result)将最终汇总展示在页面,异常则捕获并展示错误信息。
current_year 的注入是个细节:酒店价格与评分页面随时间变化,LLM 若不知道"现在"是哪一年,容易把陈旧常识当成实时信息。把当前年份作为任务上下文显式传入,是提示工程中对抗时间幻觉的实用手法。
若按 README 的指引运行:
streamlit run app.py
即启动 Streamlit 服务并在浏览器打开控制台界面(如默认的 http://localhost:8501),此时按侧边栏顺序输入 BROWSERBASE_API_KEY、填写表单、点击搜索,等待后台 Agent 完成"搜索 → 加载详情与比价 → 汇总"的完整流程,即可看到最终推荐结果。app_openai.py 以同样方式运行,只是需要额外的 OPENAI_API_KEY 环境变量。
从源码看到的注意事项与扩展方向
结合对源码的逐文件核对,有几点值得读者在实际运行时注意:
- 图片资源:app.py 与 app_openai.py 的侧边栏都引用了
./assets/browser-base.png用于展示 Browserbase 图标,但当前仓库快照的 hotel-booking-crew 目录下并未包含 assets/ 目录。若在你的环境中该文件同样缺失,可能影响侧边栏渲染,可选择自行补充图片或删除对应st.image(...)调用(位于两个 app 文件第 29 行附近)。 - 固定等待时长:browserbase 工具内的
sleep(25)是整体硬编码的,网络波动或目标站点改版都可能改变实际需要的等待时间;页面结构变化后html2text提取的内容质量也需要重新验证。 - 地点编码:Kayak URL 需要特定地点编码,LLM 是否总能正确推断
-p15321这类后缀存在不确定性,必要时可在工具或提示词中提供地点字典作为兜底。 - 同一套代码的复用价值:由于 Agent 的工具被拆分为独立模块(browserbase.py、kayak.py),这套"URL 构造工具 + 浏览器抓取工具 + CrewAI 多角色编排"的模式可以低成本迁移到机票、租车、餐厅预订等其他垂直场景——仓库中同类的 flight-booking-crew 项目即是这种复用的直接体现。
综上,hotel-booking-crew 是一个小而完整的工程范本:它以 CrewAI 定义"检索 + 汇总"两个职责分明的 Agent,用 @tool 把 Browserbase 无头浏览器能力暴露给 LLM,再通过 Streamlit 把多智能体执行封装成普通用户可操作的搜索表单,并用统一的 LLM() 入口保留了切换本地 DeepSeek-R1 与云端模型的灵活性。对照 README 的指引、沿着 app.py → kayak.py/browserbase.py 的源码逐层阅读,即可完整掌握从"用户输入"到"真实网页检索"再到"结构化汇总输出"的整条多智能体落地链路。
更多推荐
所有评论(0)