Merge pull request #3376 from googio/feat/serply-search

feat(web_search): 新增Serply搜索源
This commit is contained in:
hrz authored and GitHub committed 2026-09-28 09:36:42 +08:00
commit 1211f8202b
5 files changed
+71 -7

No files matched your search

+12 -4
View File
@@ -2,13 +2,14 @@
## 功能简介
联网搜索插件 `web_search` 支持在对话过程中实时联网搜索信息并返回结果。插件支持两个搜索源:秘塔(Metaso)和Tavily,用户可根据需要选择其中一个。
联网搜索插件 `web_search` 支持在对话过程中实时联网搜索信息并返回结果。插件支持三个搜索源:秘塔(Metaso)、Tavily和Serply,用户可根据需要选择其中一个。
## API Key申请指南
目前我们适配了`秘塔搜索`和`Tavily搜索`。
目前我们适配了`秘塔搜索`、`Tavily搜索`和`Serply搜索`。
- Tavily搜索:每个月1000次免费额度。
- 秘塔搜索:拥有较为优质的国内数据源。
- Serply搜索:返回谷歌搜索结果,注册赠送2500次免费额度,额度用完后需要付费。
## API Key申请指南
@@ -24,6 +25,13 @@
- 在控制台中创建API Key
- 复制生成的API Key(以 `tvly-` 为前缀),这是配置所需的关键信息
### 方式三:使用Serply搜索
- 访问 [Serply](https://serply.io),注册并登录账号
- 在控制台中创建API Key
- 复制生成的API Key,这是配置所需的关键信息
- 接口说明见 [Serply文档](https://serply.io/docs)
## 配置方式
### 方式1. 使用智控台部署(推荐)
@@ -32,14 +40,14 @@
- 进入"配置角色"页面,选择要配置的智能体
- 点击"编辑功能"按钮,在右侧参数配置区域找到"联网搜索"插件
- 勾选"联网搜索"
- 填入搜索源(`metaso`或`tavily`),并将对应的`API Key`填入配置项
- 填入搜索源(`metaso`、`tavily`或`serply`),并将对应的`API Key`填入配置项
- 保存配置,再保存智能体配置
### 方式2. 单模块xiaozhi-server部署
在 `data/.config.yaml` 中配置:
- 将搜索源填入 `provider`,可选值为 `metaso` 或 `tavily`
- 将搜索源填入 `provider`,可选值为 `metaso`、`tavily` 或 `serply`
- 将申请到的API Key填入 `api_key`
```yaml
@@ -0,0 +1,9 @@
-- 联网搜索插件新增Serply搜索源
UPDATE `ai_model_provider`
SET `fields` = JSON_SET(
`fields`,
'$[0].label',
'搜索源:metaso / tavily / serply'
)
WHERE `id` = 'SYSTEM_PLUGIN_WEB_SEARCH';
@@ -746,3 +746,10 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202609021000.sql
- changeSet:
id: 202609172100
author: googio
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202609172100.sql
+4 -3
View File
@@ -172,17 +172,18 @@ plugins:
# ragflow知识库id
dataset_ids: ["123456789"]
# 联网搜索插件配置
# 支持秘塔(metaso)和Tavily两个搜索源
# 支持秘塔(metaso)、Tavily和Serply三个搜索源
# 秘塔API Key申请:https://metaso.cn/search-api/api-keys
# Tavily API Key申请:https://app.tavily.com/home
# Serply API Key申请:https://serply.io
web_search:
# 搜索源:metaso / tavily
# 搜索源:metaso / tavily / serply
provider: "metaso"
# 工具描述,用于告知LLM何时调用此工具,可根据需要自定义
description: "联网搜索工具。当用户明确需要联网搜索问题时使用此工具。"
# 返回结果数量(默认5)
max_results: 5
# 秘塔API Key(mk-前缀) / Tavily API Key(tvly-前缀)
# 秘塔API Key(mk-前缀) / Tavily API Key(tvly-前缀) / Serply API Key
api_key: "mk-xxx"
# 声纹识别配置
voiceprint:
@@ -112,6 +112,43 @@ async def _search_tavily(api_key: str, query: str, max_results: int) -> str:
return "\n".join(lines)
async def _search_serply(api_key: str, query: str, max_results: int) -> str:
"""调用Serply搜索API"""
url = "https://api.serply.io/v1/search"
headers = {
"X-Api-Key": api_key,
"User-Agent": "xiaozhi-esp32-server",
}
# Serply单页最多返回10条,且返回条数可能略多于num,需要本地再截断一次
params = {"q": query, "num": min(max_results, 10)}
logger.bind(tag=TAG).debug(f"Serply搜索请求 | URL: {url} | params: {params}")
async with httpx.AsyncClient(timeout=httpx.Timeout(15.0, connect=3.0)) as client:
response = await client.get(url, params=params, headers=headers)
data = response.json()
logger.bind(tag=TAG).debug(f"Serply搜索响应 | status: {response.status_code}")
if response.status_code != 200:
detail = data.get("detail", "") if isinstance(data, dict) else ""
logger.bind(tag=TAG).error(
f"Serply搜索失败 | status: {response.status_code} | detail: {detail}"
)
return "联网搜索请求失败,请检查API Key是否正确。"
results = data.get("results", [])
if not results:
return "未找到相关搜索结果。"
lines = ["【联网搜索结果】"]
for i, item in enumerate(results[:max_results], 1):
title = item.get("title", "无标题")
snippet = item.get("description", "")
lines.append(f"{i}. 标题:{title}")
if snippet:
lines.append(f" 摘要:{snippet}")
return "\n".join(lines)
@register_function("web_search", WEB_SEARCH_FUNCTION_DESC, ToolType.SYSTEM_CTL)
async def web_search(conn: "ConnectionHandler", query: str = None):
logger.bind(tag=TAG).info(f"web_search 被调用 | query={query}")
@@ -136,6 +173,8 @@ async def web_search(conn: "ConnectionHandler", query: str = None):
result_text = await _search_metaso(api_key, query, max_results)
elif provider == "tavily":
result_text = await _search_tavily(api_key, query, max_results)
elif provider == "serply":
result_text = await _search_serply(api_key, query, max_results)
else:
return ActionResponse(
Action.REQLLM,