CrewAI + Scrapeless:使用本地LLM运行多智能体抓取团队
Advanced Bot Mitigation Engineer
TL;DR:
- 这是真正的多代理工作流程。 三个CrewAI代理获取实时页面,提取结构化记录,并通过顺序任务交接验证结果。
- 只有获取器可以访问网络。 网页获取器获得Scrapeless MCP的
scrape_markdown工具;提取器和验证器仅依赖于先前任务的输出。 - LLM在本地运行。 所有三个代理通过Ollama使用
qwen2.5:0.5b,因此工作流程不需要云LLM API密钥。 - 成功执行并不证明正确提取。
crew.kickoff()完成,但捕获的验证器输出自相矛盾,必须被拒绝。 - 生产验证必须是确定性的。 在Python中解析模型输出,验证所需字段,并将提取的值与原始MCP响应进行比较。
- 免费开始。 新的Scrapeless账户包括免费的Scraping Browser运行时 — 在app.scrapeless.com注册。
介绍:一个完成的Crew还不一定是可靠的管道
一个连接到Scrapeless MCP工具的CrewAI Agent已经可以获取实时页面。而CrewAI的crew则是一个不同的概念:两个或更多具有独立职责的代理必须相互交接工作,最终以crew.kickoff()返回结果。
本指南构建第二个工作流程。
您将创建一个由三个代理组成的crew,其目的为:
- 通过Scrapeless MCP服务器获取实时页面。
- 从返回的Markdown中提取结构化引用记录。
- 在记录离开工作流程之前验证这些记录。
所有三个代理都运行在本地Ollama模型上。不需要OpenAI、Anthropic或其他云LLM密钥。唯一的外部凭证是MCP工具使用的Scrapeless API密钥。
集成确实成功完成。然而,捕获的最终答案在内部是不一致的。这一区别是本教程中最重要的教训:一个完成的代理工作流程是执行的证据,而不是结果数据可信的证据。
您可以用这个Crew做什么
工作流程将一个责任分配给每个代理:
- 网页获取器: 调用Scrapeless MCP的
scrape_markdown工具针对实时URL并返回页面内容。 - 数据提取专家: 读取获取的Markdown并将其转换为JSON数组。
- 质量检查验证器: 检查提取的记录是否缺少字段并报告结果。
CrewAI通过显式的任务上下文传递任务输出。提取器接收获取任务的输出,验证器接收提取任务的输出。
三者之间无需手动复制任何内容。
这与将一个本地模型直接接入获取和提取Python函数不同。Ollama网页爬虫指南涵盖了这种更简单的模式。
在这里,CrewAI负责协调:
- 三个代理
- 三个范围明确的提示
- 三个独立的令牌限制
- 一个框架管理的顺序交接
为什么使用Crew而不是一个代理?
使用一个爬虫工具的单个代理必须决定抓取什么,如何解释页面,如何格式化结果,以及该结果是否可接受。
将这些职责划分为不同角色可以让您对工作流程有更清晰的控制。
每个代理接收:
- 一个狭窄的目标
- 一项专注的任务
- 自己的输出限制
- 仅访问其所需的工具
这在处理较小的本地模型时尤其有帮助。与其让一个模型同时进行抓取、提取、格式化和验证,不如让每个回合处理更有限的工作。
这种分离还使您有更清晰的检查点。在生产管道中,您可以在每个任务后保存或验证输出,并确定在获取、提取或验证过程中是否发生了失败。
为什么选择Scrapeless MCP服务器?
模型上下文协议规范定义了一种标准方式,让AI客户端发现并调用服务器公开的工具。
Scrapeless MCP服务器通过该工具接口公开网络数据和浏览器功能。CrewAI无需直接实现页面渲染、代理路由或浏览器基础设施。它只需连接到服务器并将所需工具附加到代理上。
Scrapeless MCP服务器概述解释了更广泛的工具家族,包括页面检索、搜索和浏览器控制工具。
对于这个工作流程,crew只需要一个工具:
text
scrape_markdown
它检索目标页面并以下游提取代理可以读取的格式返回内容。
您可以查看 Scrapeless 开发者文档 获取最新的服务器连接详情和工具参数。
前提条件
在运行 Crew 之前,您需要:
- Python 3.10 或更高版本
- CrewAI 和 CrewAI 工具
- 一个 Scrapeless API 密钥
- Ollama 在本地安装并运行
- 在 Ollama 中下载的
qwen2.5:0.5b模型
您不需要 OPENAI_API_KEY、ANTHROPIC_API_KEY 或其他托管的 LLM 凭证。这个例子中的每个 CrewAI LLM 对象都指向本地 Ollama 服务器。
第一步 — 安装 CrewAI 和 MCP 支持
经过验证的示例使用固定的 CrewAI 软件包:
bash
pip install "crewai==1.15.4" "crewai-tools[mcp]==1.15.4"
[mcp] 附加项安装了 MCPServerAdapter 所需的 MCP 客户端依赖项。适配器将 MCP 工具定义转换为 CrewAI 代理可以调用的工具。
第二步 — 配置本地 Ollama 模型
拉取本示例中使用的模型:
bash
ollama pull qwen2.5:0.5b
确认 Ollama 可以看到它:
bash
ollama list
Ollama 通常将其本地 API 暴露在:
text
http://localhost:11434
如果 Ollama 已安装但 Crew 无法连接,请在启动 Python 脚本之前确认 Ollama 服务正在运行。
接下来,在您的 shell 中配置 Scrapeless API 密钥:
bash
export SCRAPELESS_API_KEY="your_api_key_here"
从环境变量读取密钥可以将其保留在 Python 源文件之外。
第三步 — 将 CrewAI 指向 Ollama
为每个代理创建一个单独的 LLM 配置:
python
from crewai import LLM
fetch_llm = LLM(
model="ollama/qwen2.5:0.5b",
base_url="http://localhost:11434",
max_tokens=180,
temperature=0,
)
extract_llm = LLM(
model="ollama/qwen2.5:0.5b",
base_url="http://localhost:11434",
max_tokens=100,
temperature=0,
)
validate_llm = LLM(
model="ollama/qwen2.5:0.5b",
base_url="http://localhost:11434",
max_tokens=70,
temperature=0,
)
这三种配置使用相同的模型,但它们的输出限制反映了各自的职责:
- 提取器有更多空间返回页面内容。
- 提取器需要足够的令牌表示一个短 JSON 数组。
- 验证器仅需要生成一个简明的报告。
设置 temperature=0 可以减少输出的变化。这并不保证会得到确定性或正确的结果,特别是在使用小型本地模型时。
在仅使用 CPU 进行本地推理时,max_tokens 也是一个重要的运行时控制。降低限制可以防止代理生成不必要的长响应,尽管模型大小、硬件、上下文长度和代理回合数也会影响执行时间。
第四步 — 将 CrewAI 连接到 Scrapeless MCP 服务器
导入 MCPServerAdapter 并定义远程 MCP 连接:
python
import os
from crewai_tools import MCPServerAdapter
server_params = {
"url": "https://api.scrapeless.com/mcp",
"transport": "streamable-http",
"headers": {
"x-api-token": os.environ["SCRAPELESS_API_KEY"]
},
}
该配置包含三个重要值:
url指向 Scrapeless MCP 端点。transport告诉客户端使用可流式传输的 HTTP。x-api-token用于使用您的 Scrapeless 密钥对请求进行身份验证。
该密钥通过以下方式访问:
python
os.environ["SCRAPELESS_API_KEY"]
如果缺少环境变量,Python 将立即停止,这比在没有有效工具凭证的情况下静默启动 Crew 更可取。
第五步 — 验证可用的 MCP 工具
在构建完整 Crew 之前,验证适配器是否可以发现所请求的工具:
python
with MCPServerAdapter(server_params, "scrape_markdown") as tools:
print([tool.name for tool in tools])
验证运行返回:
text
['scrape_markdown']
这确认了:
- MCP 客户端成功连接到服务器。
- 服务器接受了身份验证头。
- 请求的工具可供 CrewAI 使用。
这并不能证明每个未来的页面请求都将包含预期的数据,或下游模型将正确解析响应。
本文中使用的 MCP 工具界面
MCPServerAdapter 可以将服务器工具暴露给 CrewAI 代理。传递 "scrape_markdown" 将此工作流程限制为它所需的单个工具:
python
with MCPServerAdapter(server_params, "scrape_markdown") as tools:
...
这种较窄的工具界面对代理的可靠性很有帮助。提取器不需要从不相关的浏览器或搜索工具中进行选择,而提取和验证代理根本不接收任何工具。
权限边界非常简单:
| 代理 | 工具访问 | 职责 |
|---|---|---|
| 网络页面抓取器 | scrape_markdown |
获取目标页面 |
| 数据提取专家 | 无 | 将抓取的内容转换为JSON |
| 质量验证员 | 无 | 检查提取的记录 |
只有抓取器可以进行实时网络请求。
如何实际使用:构建并运行团队
定义三种代理角色
为每个阶段创建一个 Agent:
python
from crewai import Agent
fetcher = Agent(
role="网络页面抓取器",
goal=(
"使用 scrape_markdown 工具获取给定的确切网址,并返回其原始输出。"
),
backstory=(
"为无法自己浏览网络的团队成员获取公共网页。"
),
tools=tools,
llm=fetch_llm,
max_iter=2,
)
extractor = Agent(
role="数据提取专家",
goal=(
"将抓取的页面markdown转化为每个引用及其作者的清晰结构化记录。"
),
backstory=(
"读取原始抓取的markdown,准确提取数据库所需的字段。"
),
llm=extract_llm,
max_iter=2,
)
validator = Agent(
role="质量验证员",
goal="在交付之前检查提取的引用记录的完整性。",
backstory=(
"拒绝不完整或格式错误的记录,并准确报告检查情况。"
),
llm=validate_llm,
max_iter=2,
)
只有 fetcher 接收 tools=tools。
提取器和验证员必须在任务上下文内工作。它们不能独立浏览目标页面或发起另一个MCP请求。
为什么设置 max_iter=2?
max_iter 限制了代理在执行任务期间可以进行的推理和行动循环的次数。
值为 2 使得抓取器有足够的空间进行工具调用并最终产生答案。它还防止小模型在过多的内部循环中继续运行。
该限制是一种控制机制,而不是正确性的保证。如果代理生成格式错误的JSON或接受空字段,成功达到迭代限制并不能使该输出有效。
只将MCP工具附加到抓取器
抓取器的角色定义明确:
- 接收目标URL。
- 调用
scrape_markdown。 - 返回工具输出,而无需额外评论。
提取器从未收到实时URL作为浏览指令。它读取抓取器返回的内容。
验证员从未接收到MCP工具。它只读取提取器的输出。
这种分离使数据流更容易理解和审计。
连接顺序任务上下文
CrewAI任务可以通过 context 参数引用先前的任务:
python
extract_task = Task(
...,
context=[fetch_task],
)
validate_task = Task(
...,
context=[extract_task],
)
因此,交接流程是:
text
fetch_task → extract_task → validate_task
提取器接收抓取任务的最终答案。验证员接收提取任务的最终答案。
不需要手动传递字符串。
使用 crew.kickoff() 运行团队
以下是完整脚本:
python
import os
from crewai import Agent, Crew, LLM, Process, Task
from crewai_tools import MCPServerAdapter
TARGET_URL = "https://quotes.toscrape.com/tag/obvious/"
server_params = {
"url": "https://api.scrapeless.com/mcp",
"transport": "streamable-http",
"headers": {
"x-api-token": os.environ["SCRAPELESS_API_KEY"]
},
}
fetch_llm = LLM(
model="ollama/qwen2.5:0.5b",
base_url="http://localhost:11434",
max_tokens=180,
temperature=0,
)
extract_llm = LLM(
model="ollama/qwen2.5:0.5b",
base_url="http://localhost:11434",
max_tokens=100,
temperature=0,
)
validate_llm = LLM(
model="ollama/qwen2.5:0.5b",
base_url="http://localhost:11434",
max_tokens=70,
temperature=0,
)
with MCPServerAdapter(server_params, "scrape_markdown") as tools:
fetcher = Agent(
role="网络页面抓取器",
goal=(
f"使用 scrape_markdown 工具获取确切网址 {TARGET_URL} 并返回其原始输出。"
),
backstory=(
"为无法自己浏览网络的团队成员获取公共网页。"
),
tools=tools,
llm=fetch_llm,
max_iter=2,
)
extractor = Agent(
role="数据提取专家",
goal=(
"将抓取的页面markdown转化为每个引用及其作者的清晰结构化记录。"
),
backstory=(
"读取原始抓取的markdown,准确提取数据库所需的字段。"
),
llm=extract_llm,
max_iter=2,
)
validator = Agent(
role="质量验证员",
goal=(
"在交付之前检查提取的引用记录的完整性。"
),
backstory=(
"拒绝不完整或格式错误的记录,并准确报告检查情况。"
“报告确切地说明了它检查的内容。”
集成结果仍然是有用的:
- CrewAI 创建了三个代理。
- 提取器收到了一个实时的无抓取的 MCP 工具。
- 任务上下文连接了三个阶段。
crew.kickoff()完成。- 未使用云 LLM 密钥。
然而,此运行不支持相信结果字段。最终输出结合了零计数、空值和成功声明。
一个小于 1B 的模型对于测试编排可能有用,但这个运行展示了为什么它不应该被自动视为可靠的结构化数据提取器。
在 Crew 之后添加确定性验证
自然语言验证仍然是模型输出。验证代理可能误解格式不正确的数据、忽视空值,或生成与其自身报告相矛盾的结论。
因此,生产验证应在模型工作流之后的普通代码中进行。
解析提取器输出
使用 json.loads 解析提取器的响应。
如果以下情况,则拒绝结果:
- 这不是有效的 JSON。
- 顶层值不是数组。
- 响应包含 JSON 之外的散文。
- 数组意外为空。
验证必需字段
对于每条记录,验证:
text存在。author存在。- 两个值都是字符串。
- 修剪空格后两个值都保持非空。
- 任一值都不是明显的占位符。
不要让像“所有字段都完整”的自然语言陈述覆盖程序检查失败。
拒绝矛盾计数
如果验证者报告计数,请将其与实际 JSON 数组长度进行比较。
如果报告声称有一个完整条目,同时报告计数为零,应该立即失败。
将提取的值与源进行比较
保留 scrape_markdown 返回的原始 Markdown。
对于诸如引号和作者姓名等复制字段,确认提取的值出现在源响应中。这有助于检测虚构、截断或替换的内容。
保留中间任务输出
示例仅打印最终的 crew 输出。为了调试和生产监控,保留每个任务的输出。
这给你三个独立的工件:
- 原始抓取的 Markdown
- 提取的 JSON
- 验证报告
有了这些输出,你可以识别数据丢失的确切阶段,而不是从最终答案中推断。
在必要时增加模型容量
更强的验证可以拒绝不良结果,但无法恢复模型未能保留的源文本。
如果小模型在提取或验证过程中不断丢失数据,请使用更大的本地模型或更强大的托管模型。不要仅仅为了让管道看起来成功而削弱检查。
结论
一个运行在本地 Ollama 模型上的 CrewAI 团队可以连接到 Scrapeless MCP 服务器,调用实时抓取工具,通过三个代理传递数据,并在不使用云 LLM 密钥的情况下完成 crew.kickoff()。
这就是集成结果。
捕获的最终输出仍然错误得足以被拒绝:它报告了零计数和空值,同时声称数据是完整的。
将工作流完成与数据正确性视为独立条件。保留中间输出,在 Python 中验证结构化记录,将提取值与 MCP 响应进行比较,并在这些检查与模型结论不一致时使管道失败。
准备构建一个调用工具的团队吗?
创建一个免费的 Scrapeless 账户以获得 API 密钥,并将 CrewAI 连接到实时网络数据工具。
一旦你了解工作流所需的页面和代理调用数量,查看 Scrapeless 定价计划。
如有实施问题和社区支持:
常见问题解答
问:CrewAI 团队需要云 LLM 密钥才能运行吗?
不需要。设置 model="ollama/qwen2.5:0.5b" 和 base_url="http://localhost:11434" 将每个代理指向本地 Ollama 服务器。该团队不需要 OpenAI、Anthropic 或其他托管 LLM 密钥。
此工作流仍然需要 Scrapeless API 密钥,因为提取器调用远程 Scrapeless MCP 服务器。
问:CrewAI 如何将输出从一个代理传递到下一个?
每个下游的 Task 接收一个包含早期任务的 context 列表。
例如:
python
extract_task = Task(
...,
context=[fetch_task],
)
CrewAI 在提取器的上下文中包含了提取任务的最终答案。同样的机制将提取结果传递给验证器。
问:为什么只有提取器接收到 MCP 工具?
获取器是唯一负责检索实时页面内容的代理。提取器和验证器应基于现有任务输出进行操作。
限制工具访问减少了不必要的选择,并使工作流程更易于审计。
问:max_iter=2 控制什么?
max_iter 限制了代理在一个任务中可以执行的推理和行动周期的数量。
值为 2 使获取器有空间进行一次工具调用和一次最终响应,同时防止开放式循环。它限制了执行,但并不保证代理的最终答案是正确的。
问:小型本地模型是否提取了正确的引用?
没有。团队完成了任务,但最终的验证器输出以以下内容开始:
text
0 ["", "", ""]
之后声称存在一个完整的条目且所有字段均为非空。由于这些陈述相互矛盾,因此结果必须被拒绝。
问:如果团队已经有QA代理,为什么还要使用确定性验证?
QA代理仍然是一个LLM。它可能忽视缺失的字段或生成与其被要求检查的数据相冲突的结论。
确定性Python检查提供了基于JSON语法、数组长度、所需字段和源匹配的独立通过/失败决策。
问:本地Ollama运行有多慢?
完整验证运行在测试的主机上耗时542秒。该测量仅适用于测试中使用的特定硬件、模型、页面、提示和代理配置。
当涉及多个代理回合时,本地CPU推断可能需要几分钟。
问:生产团队中应记录什么?
至少保留以下内容:
- MCP工具响应
- 每个中间任务输出
- 解析的结构化数据
- 确定性验证错误
- 最终团队结果
- 每个阶段的执行时间
这使得能够定位更改或丢失源数据的阶段。
问:在抓取实时网站之前我应该检查什么?
查看网站的条款和 /robots.txt 指令,遵循 机器人排除协议。
保持目标列表的边界,使用公共页面,并避免给自主代理开放式的爬虫指令。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



