Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
161 changes: 161 additions & 0 deletions tools/tool_deepseek_search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# DeepSeek 联网搜索工具

基于 DeepSeek 官方 Search API(<https://api-docs.deepseek.com/zh-cn/guides/search>)封装的联网搜索工具,为 MaxKB 智能体提供实时网页检索能力。

## 一、功能说明

- **实时检索**:调用 `https://api.deepseek.com/v1/tools/search`,返回标题、链接、短摘要、长摘要、站点名、发布时间、权威性与重排分数
- **搜索语法**:支持 `site:` / `-site:` / `"精确短语"` / `-关键词` / `lang:` / `after:` / `before:`,可在一次 query 中组合使用
- **正文可选**:`need_content=true` 时额外返回网页正文,适合需要深读的场景;默认关闭以节省上下文
- **双形态返回**:同时给出结构化 `results` 数组与拼装好的 `markdown`,后者带链接与来源标注,模型可直接用于回答
- **降级友好**:任何失败都返回 `success: false` 而非抛异常,工作流可据此决定是否退化为"不联网回答"

## 二、参数说明

### 2.1 启动参数

| 参数 | 组件类型 | 必填 | 说明 |
| :--- | :--- | :--- | :--- |
| `api_key` | 密码框 | 是 | DeepSeek API Key,在 <https://platform.deepseek.com/api_keys> 获取 |

> 启动参数与输入参数分离,工作流中只需关注 `query`,可避免 Key 在编排界面反复暴露。

### 2.2 输入参数

| 参数名 | 数据类型 | 必填 | 来源 | 说明 |
| :--- | :--- | :--- | :--- | :--- |
| `query` | string | 是 | 引用参数 | 搜索关键词,1~128 个字符,不能全为空白 |
| `count` | int | 否 | 自定义参数 | 返回条数,默认 10,范围 1~50,越界自动夹逼 |
| `need_content` | string | 否 | 引用参数 | 是否返回网页正文,`false`(默认)/ `true` |
| `ori_question` | string | 否 | 引用参数 | 用户原始问题,有助于搜索意图理解,可留空 |

### 2.3 输出参数

返回一个 dict(已可 JSON 序列化):

| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `success` | bool | 是否成功 |
| `query` | string | 实际发出的搜索词 |
| `count` | int | 实际返回条数 |
| `results` | array | 结果数组,元素字段见下 |
| `markdown` | string | 拼装好的 Markdown,含序号、站点、时间、权威性、链接与摘要 |
| `notice` | string | 无结果时的提示信息 |
| `error` / `hint` / `status` | string / string / int | 失败时的错误信息、排查建议与原样状态码 |

`results` 元素字段:

| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `title` | string | 标题 |
| `url` | string | 结果链接 |
| `snippet` | string | 短摘要(100~200 字符),适合前端展示 |
| `summary` | string | 长摘要(600~800 字符),适合给模型阅读 |
| `content` | string | 网页正文,`need_content=false` 时为空串 |
| `site_name` | string / null | 站点名称 |
| `date` | string / null | 发布时间,UTC RFC 3339 |
| `authority` | int | 权威性 0~4,`≥3` 为极权威,`-1` 表示未知 |
| `rerank_score` | number | 相关度 0~3,`≥1.5` 为优质结果 |

## 三、工具内容(Python)

完整源码见本目录下 `deepseek_search.py`(同时内嵌于 `1.0.0/DeepSeek联网搜索.tool`)。

核心实现约 200 行,结构如下:

```python
def main(api_key, query, count=10, need_content="false", ori_question=""):
# 1. 参数校验:api_key / query 必填,count 夹逼到 1~50
# 2. POST https://api.deepseek.com/v1/tools/search
# headers: Authorization: Bearer <key>, Content-Type: application/json
# 3. 状态码分级:400/401/402/429/500/504 分别给出排查提示
# 4. 结果整形:抽取字段 + 拼接 Markdown
return {"success": True, "query": ..., "count": ..., "results": [...], "markdown": "..."}
```

编写时遵守的三条 MaxKB 规范:

1. 入口函数名必须是 `main`,形参名与工具面板中的参数名逐字一致(MaxKB 以 `main(**keywords)` 调用)
2. `main` 必须位于代码末尾(MaxKB 取最后一个定义的对象作为入口)
3. 返回值需可 JSON 序列化;`print` 会被重定向到 devnull,不要依赖它调试

## 四、调用示例

### 4.1 工作流节点调用

工具节点参数配置:

| 节点参数 | 取值 |
| :--- | :--- |
| `api_key` | 启动参数中已配置,无需重复填写 |
| `query` | 引用「开始」节点的 `question` |
| `count` | 5 |
| `need_content` | false |

下游 AI 对话节点提示词中加入:

```
参考资料:
{{搜索节点.markdown}}
```

### 4.2 直接调用(Python)

```python
from deepseek_search import main

r = main(
api_key="sk-xxxxxxxx",
query="DeepSeek 模型 site:github.com lang:zh after:2025",
count=5,
need_content="false",
)
print(r["markdown"])
```

### 4.3 典型返回

```json
{
"success": true,
"query": "DeepSeek V4 模型 发布",
"count": 2,
"results": [
{
"title": "DeepSeek V4 模型发布",
"url": "https://example.com/deepseek-v4",
"snippet": "DeepSeek V4 已上线,支持工具调用与长上下文。",
"summary": "DeepSeek 发布了 V4 模型。此次发布改进了工具调用与长上下文能力,适合 Agent 工作流。",
"content": "",
"site_name": "Example News",
"date": "2026-06-01T00:00:00+00:00",
"authority": 3,
"rerank_score": 2.4
}
],
"markdown": "### 1. DeepSeek V4 模型发布\n- 站点:Example News | 时间:2026-06-01T00:00:00+00:00 | 权威性:3/4 | 相关度:2.4/3\n- 链接:https://example.com/deepseek-v4\n\n长摘要..."
}
```

## 五、错误处理

| 现象 | 原因 | 处理 |
| :--- | :--- | :--- |
| `HTTP 401 Authentication Fails` | Key 错误或失效 | 检查启动参数中的 `api_key`,去掉首尾空格 |
| `HTTP 402` | 账户余额不足 | 充值后重试 |
| `HTTP 429` | 超过账号级 QPS(Search 5 QPS) | 稍后重试,或在编排中增加延时 |
| `HTTP 400` | query 或 count 不合法 | 检查 query 长度与 count 范围 |
| `HTTP 5xx` | 服务端错误或下游超时 | 稍后重试 |
| 请求异常 / 超时 | MaxKB 容器无出网权限 | 在容器内执行 `curl -I https://api.deepseek.com` 验证 |
| `ModuleNotFoundError: requests` | 精简镜像缺包 | `docker exec -it maxkb pip install requests`(代码已内置 urllib 回退,通常无需安装) |

## 六、计费与限速

- 计费:Search 按成功请求计费,**5 元 / 1000 次**;`count` 不影响单价;请求失败、无结果、超时不计费
- 限速:每个账号独立 **5 QPS**,与模型并发限制互不影响

## 七、版本记录

| 版本 | 日期 | 说明 |
| :--- | :--- | :--- |
| 1.0.0 | 2026-10-19 | 首发:Search 接口封装,支持搜索语法、可选正文、Markdown 输出与分级错误处理 |
5 changes: 5 additions & 0 deletions tools/tool_deepseek_search/data.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: DeepSeek 联网搜索
tags:
- 联网搜索
title: 使用 DeepSeek Search API 执行联网搜索,返回结构化结果与带引用的 Markdown
description: 基于 DeepSeek 官方 Search API(https://api.deepseek.com/v1/tools/search)的联网搜索工具。支持 site:、lang:、after:、before:、-关键词 等搜索语法,可控制返回条数并选择是否抓取网页正文;结果同时提供结构化 JSON 与可直接用于回答的 Markdown(含链接、站点、发布时间与权威性)。
203 changes: 203 additions & 0 deletions tools/tool_deepseek_search/deepseek_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
# =============================================================================
# MaxKB 脚本工具:DeepSeek 联网搜索(Search API)
# 接口文档:https://api-docs.deepseek.com/zh-cn/guides/search
#
# MaxKB 工具规范要点(照抄即可生效):
# 1. 代码里必须有一个 main() 作为入口,参数名/顺序须与工具「参数」面板完全一致;
# 2. MaxKB 内部以 main(**keywords) 的关键字方式调用,参数名不一致会直接 TypeError;
# 3. main() 必须写在代码的【最后】,MaxKB 取「最后一个定义的对象」作为入口函数;
# 4. 返回值需可 JSON 序列化,建议统一返回 dict(success / results / markdown / error);
# 5. 不要在此文件里调用 main(),也不要写 `if __name__ == '__main__'`。
# =============================================================================
import json

SEARCH_URL = "https://api.deepseek.com/v1/tools/search"
TIMEOUT = 30 # 秒;MaxKB 函数执行默认超时为 3600s,这里只限制单次 HTTP
MAX_CONTENT_CHARS = 1500 # 单条正文截断长度,避免把上下文塞爆

STATUS_HINT = {
400: "请求参数不合法(检查 query 长度 1~128 字符、count 在 1~50 之间)",
401: "API Key 缺失或无效,请检查工具参数中的 api_key",
402: "DeepSeek 账户余额不足,请充值后重试",
429: "触发 QPS 限速(Search 每账号 5 QPS),请稍后重试",
500: "DeepSeek 服务端内部错误,建议稍后重试",
504: "DeepSeek 下游请求超时,建议稍后重试",
}


def _to_int(value, default):
"""把模型传进来的 "5" / 5.0 / None 统一成 int。"""
try:
return int(float(str(value).strip()))
except Exception:
return default


def _to_bool(value):
"""把 "true" / 1 / True 统一成 bool。"""
if isinstance(value, bool):
return value
return str(value).strip().lower() in ("1", "true", "yes", "y", "on")


def _http_post_json(url, headers, payload, timeout=TIMEOUT):
"""POST JSON,返回 (status_code, text)。优先 requests,缺失时回退 urllib。"""
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")

try:
import requests
except Exception:
requests = None

if requests is not None:
resp = requests.post(url, headers=headers, data=body, timeout=timeout)
return resp.status_code, resp.text

import urllib.request
import urllib.error

req = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, r.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", "replace")


def _build_markdown(items):
"""把结构化结果拼成给模型看的 Markdown,减少模型二次解析成本。"""
if not items:
return "未检索到相关网页。可尝试更换关键词、放宽 site:/after: 等限定条件。"

blocks = []
for i, it in enumerate(items, 1):
lines = ["### %d. %s" % (i, it.get("title") or "(无标题)")]
meta = []
if it.get("site_name"):
meta.append("站点:%s" % it["site_name"])
if it.get("date"):
meta.append("时间:%s" % it["date"])
if it.get("authority") is not None:
meta.append("权威性:%s/4" % it["authority"])
if it.get("rerank_score") is not None:
meta.append("相关度:%s/3" % it["rerank_score"])
if meta:
lines.append("- " + " | ".join(meta))
lines.append("- 链接:%s" % (it.get("url") or ""))
summary = it.get("summary") or it.get("snippet") or ""
if summary:
lines.append("")
lines.append(summary.strip())
content = (it.get("content") or "").strip()
if content:
if len(content) > MAX_CONTENT_CHARS:
content = content[:MAX_CONTENT_CHARS] + "……(正文已截断)"
lines.append("")
lines.append("> " + content.replace("\n", "\n> "))
blocks.append("\n".join(lines))
return "\n\n".join(blocks)


def main(api_key, query, count=10, need_content="false", ori_question=""):
"""DeepSeek 网页搜索:返回结构化结果 + 可直接喂给大模型的 Markdown。"""
api_key = (api_key or "").strip()
query = (query or "").strip()
ori_question = (ori_question or "").strip()

# ---------- 参数校验 ----------
if not api_key:
return {
"success": False,
"error": "缺少 api_key:请在工具参数中填入 DeepSeek API Key",
"results": [],
"count": 0,
"markdown": "",
}
if not query:
return {
"success": False,
"error": "query 不能为空:请把用户问题提炼成 1~128 字符的搜索词",
"results": [],
"count": 0,
"markdown": "",
}

count = _to_int(count, 10)
count = max(1, min(50, count))

payload = {"query": query[:128], "count": count}
if _to_bool(need_content):
payload["need_content"] = True
if ori_question:
payload["ori_question"] = ori_question[:128]

headers = {
"Authorization": "Bearer " + api_key,
"Content-Type": "application/json",
"Accept": "application/json",
}

# ---------- 调用接口 ----------
try:
status, text = _http_post_json(SEARCH_URL, headers, payload)
except Exception as e:
return {
"success": False,
"error": "请求异常:%s" % e,
"hint": "检查 MaxKB 容器到 api.deepseek.com 的网络连通性(出网/代理/防火墙)",
"results": [],
"count": 0,
"markdown": "",
}

try:
data = json.loads(text) if text else {}
except Exception:
return {
"success": False,
"status": status,
"error": "响应不是合法 JSON:%s" % (text or "")[:300],
"results": [],
"count": 0,
"markdown": "",
}

# ---------- 错误处理 ----------
if status != 200:
err = data.get("error") or {}
message = err.get("message") or data.get("message") or ("HTTP %s" % status)
return {
"success": False,
"status": status,
"error": message,
"hint": STATUS_HINT.get(status, "未预期状态码,请对照官方文档排查"),
"results": [],
"count": 0,
"markdown": "",
}

# ---------- 结果整形 ----------
raw_items = data.get("results") or []
items = []
for r in raw_items:
items.append({
"title": r.get("title") or "",
"url": r.get("url") or "",
"snippet": r.get("snippet") or "",
"summary": r.get("summary") or "",
"content": r.get("content") or "",
"site_name": r.get("site_name"),
"date": r.get("date"),
"authority": r.get("authority"),
"rerank_score": r.get("rerank_score"),
})

return {
"success": True,
"query": payload["query"],
"count": len(items),
"results": items,
"markdown": _build_markdown(items),
"notice": "" if items else "搜索正常结束但无匹配结果(本次不计费)",
}
Binary file added tools/tool_deepseek_search/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading