给本地 LLM 提供实时网络搜索与 Ollama + Scrapeless
Senior Web Scraping Engineer
一个在您自己机器上运行的模型不知道其训练截止日期之后的任何信息。询问本地的Llama、Qwen或Mistral检查点关于本周的头条新闻时,它要么拒绝回答,要么更糟糕的是,陈述一些合理但错误的内容。NIST的生成性人工智能风险档案对第二种失效模式有一个名称:虚构,定义为一个系统“在回应提示时‘生成并自信地呈现错误或虚假的内容’。”一个没有获取当前数据路径的模型在提问涉及今天时无法避免这种情况。
解决方案不是一个更大的模型,而是一个模型可以调用的搜索工具,连接到返回真实结果的搜索API,结果在模型写出最终答案之前反馈回同一个对话。这本指南将该模式从头到尾接线:Ollama完全在您自己的硬件上运行一个调用工具的模型,以及Scrapeless Deep SerpApi作为模型调用的搜索后端。下面展示的每个请求和响应都是一个真实的捕获运行,而不是伪造的记录。
这个集成所能实现的
一个带有搜索工具的本地模型能够回答其权重无法单独回答的问题:
- 时事和价格。 训练于几个月前的模型无法知道本季度的数字;实时搜索请求可以。
- 自我回忆的事实核查。 模型给出一个答案,然后搜索请求在最终响应发送之前确认或纠正它。
- 离线优先的代理,其依赖于一个窄网络。 一切——模型权重、推理、工具选择逻辑——都在本地机器上运行。唯一的出站调用是搜索请求本身,准确限于模型选择提出的查询。
- 小模型超越其训练数据的表现。 本指南中的模型有5亿个参数。它不需要知道关于火星任务的任何信息;它需要识别问题需要搜索并构造一个合理的查询。
为什么选择Deep SerpApi作为搜索工具
Ollama提供自己的托管搜索功能(ollama.com/api/web_search),这是快速原型的合理默认选项。它还需要一个Ollama账户,一个OLLAMA_API_KEY,并通过Ollama自己的云服务路由每个查询——模型保持本地,但搜索步骤不会比与任何其他托管搜索供应商时更少依赖Ollama的基础设施。其文档中默认的结果上限是每次调用5条结果,最多10条。
Deep SerpApi是一个专用的结构化搜索端点:一个经过身份验证的POST返回谷歌的自然结果、相关搜索、分页和(根据查询而定的)视频和知识面板的数据,作为解析的JSON——而不是修剪过的摘要。Scrapeless自己的产品页面列出了“20多个谷歌SERP场景和主流搜索引擎”(搜索、新闻、地图、购物、趋势等)的覆盖范围,响应时间为“1-2秒”,并提供“2000次免费的API调用”的免费层,且无需银行卡。如果一个项目已经依赖Scrapeless进行其他数据收集工作,或者需要完整的自然结果架构而不是简短的答案摘要,将同一账户的Deep SerpApi密钥连接到调用工具的循环中,可以保持一个供应商和一个账单,而不是两个。
通过注册获取免费API密钥——无需银行卡:app.scrapeless.com。
前提条件
- 一台运行Linux、macOS或WSL2的机器,具备至少2GB的空闲RAM(本指南中的模型在加载后需要远低于1GB;GPU是可选的,仅加快推理速度)。
curl和Python 3.9或更高版本。- 从仪表板的API密钥管理页面获得的Scrapeless账户和API密钥。
- 无Ollama账户和无
OLLAMA_API_KEY——此路径从不调用Ollama的托管服务。
本地安装和运行调用工具的模型
安装Ollama:
bash
curl -fsSL https://raw.githubusercontent.com/ollama/ollama/main/scripts/install.sh | sh
在一个由systemd管理的Linux主机上(包括启用了systemd的WSL2),安装程序会自动注册并启动一个ollama服务,监听127.0.0.1:11434。确认二进制文件和服务:
bash
ollama --version
# ollama版本为0.31.2
拉取一个小型的具备调用工具能力的模型。Qwen2.5的指令调优检查点支持最低到5亿参数的函数调用,这样可以保持下载和内存占用小:
bash
ollama pull qwen2.5:0.5b
ollama list之后确认模型已经在本地:一个名为qwen2.5:0.5b的397 MB条目,准备服务而无需进一步的网络访问。
并非所有本地可运行的模型都支持工具调用——在构建围绕其的代理循环之前,请检查Ollama库中模型页面是否有“工具”标签。如果0.5B对于给定任务来说太小,则较大的Qwen2.5、Llama 3.1和Mistral检查点也会携带相同的标签。
在模型下载时获取一个免费的Scrapeless API密钥——无需信用卡:app.scrapeless.com。
验证Deep SerpApi端点
Deep SerpApi在每种情况下都有一个形状:一个actor名称加上一个input对象,具体文档见docs.scrapeless.com。Google搜索场景使用的actor是scraper.google.search:
bash
curl -s -X POST "https://api.scrapeless.com/api/v1/scraper/request" \
-H "x-api-token: $SCRAPELESS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"actor": "scraper.google.search",
"input": {"q": "关于火星样本返回任务的最新头条", "gl": "us", "hl": "en"}
}'
对于该查询的实时调用返回HTTP 200,并且JSON主体包含以下顶级字段:organic_results、pagination、related_searches、search_information、inline_videos、video_results和metadata。organic_results中的每个条目都包含position、title、link、redirect_link、favicon、snippet、snippet_highlighted_words和source。响应是普通的JSON——没有流式传输,没有保持开放的会话,一个请求进来,一个文档输出。如果请求在服务器端尚未完成,则返回HTTP 201,并带有一个taskId;对于Google搜索查询的普通情况是上述同步的200。
定义并附加搜索工具
Ollama的/api/chat端点接受一个按JSON Schema格式定义的tools数组。定义一个工具web_search并将其与对话一起传递:
python
TOOLS = [
{
"type": "function",
"function": {
"name": "web_search",
"description": "在实时网络中搜索当前信息,返回顶级的有机结果,包括标题、链接和摘要。",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索查询"}
},
"required": ["query"],
},
},
}
]
将TOOLS附加到每个/api/chat请求,以便模型始终知道该工具存在:
python
import json
import urllib.request
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
MODEL = "qwen2.5:0.5b"
def ollama_chat(messages):
body = json.dumps({"model": MODEL, "stream": False, "messages": messages, "tools": TOOLS}).encode()
req = urllib.request.Request(OLLAMA_URL, data=body, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=180) as resp:
return json.load(resp)
该模型从未直接调用Deep SerpApi——它只会发出一个命名为web_search的tool_calls条目,并带有它选择的参数。一个简单的Python函数完成实际的HTTP工作,并将格式化的结果返回:
python
import os
def web_search(query: str) -> str:
body = json.dumps({
"actor": "scraper.google.search",
"input": {"q": query, "gl": "us", "hl": "en"},
}).encode()
req = urllib.request.Request(
"https://api.scrapeless.com/api/v1/scraper/request",
data=body,
headers={
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req, timeout=60) as resp:
data = json.load(resp)
results = data.get("organic_results", [])[:3]
shaped = [
{"title": r.get("title"), "link": r.get("link"), "snippet": r.get("snippet")}
for r in results
]
return json.dumps(shaped)
在返回工具输出之前修剪到前三个结果,保持第二个/api/chat调用小——一个具有5亿参数的模型具有有限的上下文窗口,而完整的响应携带分页链接、favicon和与搜索相关的块,模型不需要这些来回答问题。
基于提示的使用:观察模型决策
将所有部分组合在一起:发送一个需要当前信息的提示,让模型请求工具,针对Deep SerpApi执行该请求,并将结果返回以获得有依据的最终答案。
python
import json
import os
import urllib.request
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
SCRAPELESS_URL = "https://api.scrapeless.com/api/v1/scraper/request"
MODEL = "qwen2.5:0.5b"
TOOLS = [
{
"type": "function",
"function": {
"name": "web_search",
"description": "在实时网络上搜索当前信息,并返回顶部的有机结果,包括标题、链接和摘要。",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索查询"}
},
"required": ["query"],
},
},
}
]
def ollama_chat(messages):
body = json.dumps({"model": MODEL, "stream": False, "messages": messages, "tools": TOOLS}).encode()
req = urllib.request.Request(OLLAMA_URL, data=body, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=180) as resp:
return json.load(resp)
def web_search(query: str) -> str:
body = json.dumps({
"actor": "scraper.google.search",
"input": {"q": query, "gl": "us", "hl": "en"},
}).encode()
req = urllib.request.Request(
SCRAPELESS_URL,
data=body,
headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=60) as resp:
data = json.load(resp)
results = data.get("organic_results", [])[:3]
return json.dumps([
{"title": r.get("title"), "link": r.get("link"), "snippet": r.get("snippet")}
for r in results
])
messages = [{"role": "user", "content":
"今天关于火星样本回收任务的最新头条是什么?如果您需要当前信息,请使用搜索工具。"}]
first_turn = ollama_chat(messages)
print(json.dumps(first_turn["message"], indent=2))
tool_call = first_turn["message"]["tool_calls"][0]
query = tool_call["function"]["arguments"]["query"]
result = web_search(query)
messages.append(first_turn["message"])
messages.append({"role": "tool", "content": result, "tool_name": "web_search"})
second_turn = ollama_chat(messages)
print(json.dumps(second_turn["message"], indent=2))
第一次运行 `qwen2.5:0.5b` 打印的内容如下 — 还没有最终答案,仅有工具请求:
```json
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_dwndere1",
"function": {
"index": 0,
"name": "web_search",
"arguments": {
"query": "火星样本回收任务最新头条"
}
}
}
]
}
模型读取了提示,意识到它需要当前信息,并选择了它自己的搜索查询 — 本次运行的措辞来自模型,而不是用户句子的复制。调用代码针对 Deep SerpApi 执行 web_search(query="火星样本回收任务最新头条"),返回三个格式化的结果,并将它们作为 role: "tool" 消息附加,然后在第二次发送会话到 /api/chat 之前。
你将获得的返回结果
第二个 print 语句是模型的实际答案,基于搜索调用返回的内容:
json
{
"role": "assistant",
"content": "以下是关于火星样本回收任务的一些最新头条文章:\n\n- [火星样本回收 - 火星任务](https://www.jpl.nasa.gov/missions/mars-sample-return-msr/)\n - 此链接将带您访问 NASA 的网站,其中包含有关火星样本回收计划的信息。\n- [NASA-ESA 火星样本回收](https://en.wikipedia.org/wiki/NASA-ESA_Mars_Sample_Return)\n - 关于该项目的维基百科页面也可用,提供了有关任务及其成本的更详细信息。\n\n以下是与火星样本回收任务相关的文章链接:\n\n1. [NASA 的火星样本回收已死,铺平了通往中国的道路](https://www.universetoday.com/articles/nasas-mars-sample-return-is-dead-paving-the-way-for-china/)\n - 这篇文章提供了有关火星样本回收任务当前状态的更多信息。\n\n希望这能帮助你!如果你需要其他文章或更新,请告诉我。"
}
该答案中的每个链接都可以追溯到 web_search 实际返回的三条结果 — 模型汇总并重新组织了它们,而不是新创造任何东西。这并不是保证:一个 0.5 亿参数的模型仍然可能产生一个在工具结果中从未出现过的看似合理的细节,即使其上下文中有真实的搜索结果。基础降低了这种情况发生的概率;它并没有消除它们,生产系统在从模型的答案中提取链接时应检查每一个链接与工具输出的匹配情况。无论查询主题如何,请求和响应模式保持不变:模型决定何时搜索,工具调用是模型自己的权重不拥有的唯一网络访问,而第二次完成不会在工具结果进入会话之前运行。
结论
将本地模型连接到 Deep SerpApi 只需要一个工具定义、一个 HTTP 函数和两个 /api/chat 调用——模型处理关于何时搜索和询问什么的推理,方式与托管模型代理框架相同,但每个生成的令牌都保留在运行它的机器上。这种模式不仅限于这个例子:你可以更换提示、更换模型,或者扩展 TOOLS 以增加更多功能,整个请求-工具-调用-响应循环处理其余的。
开始免费 — 无需信用卡:app.scrapeless.com。完整参数参考见 docs.scrapeless.com,当前每次查询的定价见 scrapeless.com/en/pricing。有关此类 SERP 端点与 Scrapeless 的代理如何捕获托管 AI 平台自身回答的差异,请参见 SERP-API与LLM抓取器的比较。
常见问题
问:哪些本地模型支持工具调用?
Ollama 的模型库中标记为“Tools”的任何模型均可与此模式配合使用。Qwen2.5(降至 0.5B)、Llama 3.1 和 3.2、Mistral 以及 IBM Granite 都配备了支持工具调用的检查点。未标记该标签的模型仍可能以纯文本形式发出 tool_calls 形状的 JSON,调用代码必须手动解析,而不是读取结构化字段——在围绕某个模型构建之前,请检查标签。
问:这些是否需要模型本身的互联网连接?
不需要。模型、提示和推理都在本地机器上运行。唯一的外部请求是针对 Deep SerpApi 的 web_search 工具调用,范围正好是模型生成的查询——运行的其他内容与网络无关。
问:为什么不使用 Ollama 的内置网络搜索,而是使用单独的 API 密钥?
Ollama 的托管搜索(ollama.com/api/web_search)是一个合理的快速原型选项,不需要超出 Ollama 自身的单独供应商账户。它确实需要与免费 Ollama 账户相关联的 OLLAMA_API_KEY,每次调用的结果限制为 10 条,并返回普通的结果列表,而不是 Google 的完整自然结果架构(位置、相关搜索、分页、垂直领域)。当项目需要更完整的架构、非 Google 搜索场景或已经通过 Scrapeless 账户运行其他工作时,Deep SerpApi 更加适合。
问:如果搜索调用失败会发生什么?
请求格式不正确将返回 HTTP 400;无效或缺失的 API 密钥将返回身份验证错误;尚未完成服务器端的查询将返回 HTTP 201 及一个 taskId 而不是结果主体。在假设响应中存在 organic_results 之前,检查状态代码,与任何 HTTP 客户端在解析响应主体之前的检查方式相同。
问:我可以针对其他国家或语言而不是英语吗?
可以——gl 设置 Google 国家代码,hl 设置每个 scraper.google.search 请求的界面语言;两者都是 input 对象中的普通字符串字段,每个调用设置一次。
问:模型是否直接调用 Deep SerpApi?
不。模型仅生成描述要运行的函数及其参数的 tool_calls 项——它没有自己的网络访问权限。调用的 Python 代码拥有实际的 HTTP 请求,这也确保 API 密钥完全不在模型的上下文中。
问:将其指向抓取搜索结果而不是 API 是否安全?
Deep SerpApi 通过经过身份验证的端点返回已解析的 Google 数据,因此在调用方一侧没有 robots.txt 或速率限制的顾虑——这些基础设施工作在 Scrapeless 方面发生。任何通过直接抓取 Google 的结果页面构建等效功能的人都应该先阅读 Robots 排除协议 和目标自身的条款;专门存在一个受管搜索端点以避免这一类问题。
问:当模型“决定”进行搜索时,实际上发生了什么?
这是原始检索增强生成研究中描述的检索-然后-生成模式:模型在推理时以获取的文档作为条件生成最终输出,而不仅仅依赖于其权重中固有的内容。工具调用是现代聊天调整模型在对话过程中触发自我获取文档的机制,而不是在每个提示之前运行的固定检索步骤。
问:基础是否完全阻止模型捏造内容?
不。它减少了模型对搜索结果所涵盖的特定事实的虚构,但小模型仍然可能错误归因、过度总结或添加工具输出中不存在的细节。将第二轮视为受到真实数据启发的草稿,而不是权威引用——对于任何至关重要的内容,在相信模型的声明之前,将其与实际提供的organic_results有效载荷进行比较。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



