做 AI 应用的同学应该都有这种体会:模型本身再强,不联网的时候就像一台断网的电脑,很多问题只能靠训练数据里的旧知识回答。尤其是做 Agent、RAG、舆情监控、竞品分析这类项目时,实时拿到网页内容几乎是刚需。最近 Keenable 推出了独立的网页搜索 API,还带了一个叫 Time Machine 的能力,接口设计和过去的搜索 API 不太一样。这篇就把从账号准备、接口调用、参数解释到常见报错排查的完整过程整理出来,给正在做搜索接入的同学做个参考。
1. Keenable 网页搜索 API 与 Time Machine 到底是什么
1.1 网页搜索 API 解决了什么问题
网页搜索 API 本质上是把“搜索网页”这件事从浏览器里拆出来,变成一个可供程序调用的接口。过去我们在代码里拿到网页内容,通常有两种方式:
- 自己去爬搜索引擎结果页,再解析 HTML。
- 直接对接某些大厂搜索产品的非正式接口。
这两种方式的问题都很明显。爬页面要处理反爬、验证码、页面结构变化,而且搜索引擎的页面结构经常改,今天能解析的字段明天可能就失效了。非正式接口更不稳定,随时可能因为调用频率过高被封。独立网页搜索 API 的价值就在于把“关键字 -> 网页搜索结果列表”这个过程标准化的封装好,程序只需要传参数、收结果,不需要关心搜索服务端是怎么实现的。
这种能力最常见的落地场景包括:
- AI Agent 在回答问题时实时检索最新信息,减少模型幻觉。
- RAG 应用的知识库定期抓取指定主题的新内容。
- 舆情监控系统定时搜索品牌词、竞品词。
- 学术或投资研究中的信息采集。
- 自动化测试中的页面关键词校验。
1.2 Time Machine 是什么概念
Time Machine 是这次推出的比较有意思的功能。从命名和定位来看,它解决的是“网页历史状态检索”的问题。
普通搜索 API 拿到的是当前时间点的搜索结果,而 Time Machine 允许调用方指定一个历史时间点,去查询“在那个时间点,这个关键词能搜到什么”。这对需要追溯信息发布时间的场景很有用,比如:
- 还原某个事件在特定日期的舆论热度。
- 查看某个产品页面在改版前的内容。
- 对比竞品在不同月份的宣传口径。
- 审计某条信息最早出现的时间和渠道。
需要提醒的是,历史网页数据的覆盖范围、最早可回溯时间、快照更新频率,不同产品的差异很大。在正式使用前,建议先阅读官方文档确认时间范围和数据覆盖说明,不要默认所有域名和所有时间点都有快照。
1.3 为什么需要独立的搜索 API 而不是大模型内置搜索
很多人会问:现在很多大模型平台已经提供了带联网搜索的接口,为什么还要单独对接一个网页搜索 API?
原因主要有几点。
第一,解耦。把搜索能力从模型调用中拆出来,意味着可以自由组合不同的模型。今天用 A 模型,明天换成 B 模型,搜索逻辑不用改。这在大模型 API 更新频繁的当下非常实用。
第二,可控。独立 API 的请求参数、返回结构、缓存策略都由自己掌控,更容易嵌入现有系统。
第三,可观测。搜索是搜索,模型是模型,分开后每一次搜索请求的耗时、成功率、结果质量都能单独统计,方便排查问题。
第四,成本核算更清晰。搜索调用量和模型 Token 消耗量分开计量,预算管理更直观。
2. 环境准备与版本说明
2.1 开发环境
本文的示例使用 Python 编写,运行环境如下。版本可以根据你的实际情况调整,重点看调用思路。
- 操作系统:Windows 10 / macOS / Linux 均可。
- Python:3.8 及以上,示例代码使用了
requests库和标准库中的json、time。 - 网络环境:能正常访问 Keenable API 服务域名。
- IDE:Visual Studio Code 或任何你习惯的编辑器。
如果你的项目是 Java、Go、Node.js,也不必担心,HTTP 接口的调用方式是一致的,只是发送 HTTP 请求的库不同。
2.2 注册账号与获取 API Key
调用任何付费 API 之前,第一步都是注册账号并获取密钥。常规流程如下:
- 打开 Keenable 官方网站,注册账号。
- 进入控制台或 API 管理页面。
- 创建一个应用或项目,系统会生成对应的 API Key。
- 根据需要开通网页搜索 API 的套餐,确认是否包含 Time Machine 能力。
这里强调一个安全原则:API Key 等同于账号的访问凭证,绝对不要写死在代码里,更不要提交到 Git 仓库。推荐通过环境变量或配置文件管理。
# 在终端中设置环境变量,示例为 macOS / Linux 写法 export KEENABLE_API_KEY="your_api_key_here"2.3 接口地址与通用约定
由于产品接口细节可能会调整,本文不写死真实的生产地址,而是用示例域名代替。真实地址请以官方文档为准。
一个典型的 REST 风格搜索 API 会遵循以下约定:
- 请求方法:
POST或GET。如果查询参数复杂、包含较多筛选条件,通常用POST。 - 请求头:包含
Authorization: Bearer <API_KEY>和Content-Type: application/json。 - 响应格式:
JSON。 - 字符编码:
UTF-8。
下面是一个通用的调用约定示例:
| 约定项 | 说明 |
|---|---|
| 请求方式 | POST |
| 认证方式 | Bearer Token |
| 请求体格式 | application/json |
| 响应体格式 | application/json |
| 超时设置 | 建议 10 到 30 秒 |
3. 核心概念与参数拆解
在写完整代码之前,先拆解一下最核心的几个概念。
3.1 认证方式
绝大多数 API 使用 API Key 作为身份凭证。常见的有两种传递方式:
- 请求头方式:
Authorization: Bearer <API_KEY>。 - 查询参数方式:
?api_key=<API_KEY>。
推荐使用请求头方式,因为查询参数可能被服务端日志、代理日志记录,存在泄露风险。
3.2 搜索请求参数
搜索 API 最核心的参数通常包括:
| 参数名 | 是否必填 | 说明 |
|---|---|---|
query | 是 | 搜索关键词 |
language | 否 | 搜索结果语言,例如zh-CN、en-US |
region | 否 | 搜索地区,影响结果的地域偏向 |
market | 否 | 市场或站点范围,部分产品会使用 |
page_size | 否 | 每页返回结果数量,常见范围是 1 到 20 |
page | 否 | 页码,用于翻页 |
sort | 否 | 排序方式,例如relevance(相关度)、date(时间) |
time_range | 否 | 结果的时间过滤,例如过去一天、过去一周 |
freshness | 否 | 部分产品用该参数控制结果的新鲜度 |
需要注意,不同产品的参数命名差异很大。比如有些产品用q而不是query,有些产品用count而不是page_size。接入前一定要先看官方接口文档的 Request Parameters 表格,确认参数名和取值枚举。
3.3 Time Machine 时间参数
Time Machine 通常需要额外指定一个时间参数。常见的设计方式有两种:
- 使用
timestamp:传入 Unix 时间戳。 - 使用
date:传入 ISO 8601 格式的日期时间,例如2024-06-01T00:00:00Z。
从工程角度,ISO 8601 的可读性更好,也方便在日志中直接查看。但如果你需要在代码中频繁比较时间,Unix 时间戳更高效。
一个假设的请求体设计如下:
{ "query": "Keenable API", "language": "zh-CN", "page_size": 10, "timestamp": 1717200000 }这里的timestamp: 1717200000对应的是 2024 年 6 月 1 日 00:00:00 UTC。具体含义是:查询这个时间点的搜索结果快照。
3.4 响应结构
搜索 API 的响应通常会包含以下几个部分:
results:搜索结果列表,每一项包含标题、链接、摘要、发布时间等字段。total:结果总数,用于分页。query_id:本次请求的唯一标识,方便排查问题。cached:本次响应是否命中了服务端缓存。
一个假设的响应示例:
{ "query_id": "a1b2c3d4-1234-5678-9abc-def012345678", "total": 128, "results": [ { "title": "Keenable 推出独立网页搜索 API", "url": "https://example.com/news/keenable-api", "snippet": "Keenable 发布了独立网页搜索 API,并附带 Time Machine 能力……", "published_at": "2025-01-10T08:30:00Z", "source": "example.com" } ], "cached": false }响应里的字段可能比这更多,也可能字段名不同,核心思路是根据官方文档把字段名映射到你自己的数据结构中,避免在后端硬编码字段名。
4. 完整实战:用 Python 调用 Keenable 搜索 API
接下来用一个完整示例,从零开始写一个可运行的 Python 搜索客户端。
4.1 创建项目结构
先创建目录结构:
keenable-search-demo/ ├── main.py ├── search_client.py ├── requirements.txt └── .env.example4.2 安装依赖
requirements.txt内容如下:
requests==2.31.0 python-dotenv==1.0.0然后执行:
pip install -r requirements.txtpython-dotenv用于读取.env文件中的环境变量,方便本地调试。
4.3 编写核心搜索客户端
文件路径:search_client.py
import os import time import requests class KeenableSearchClient: def __init__(self, api_key: str, base_url: str = "https://api.example.com/v1"): self.api_key = api_key self.base_url = base_url self.session = requests.Session() self.session.headers.update({ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }) def search(self, query: str, page_size: int = 10, **kwargs): """ 基础搜索接口。 :param query: 搜索关键词 :param page_size: 每页结果数 :param kwargs: 其他参数,如 language、region、timestamp 等 :return: 解析后的 JSON 响应 """ payload = { "query": query, "page_size": page_size, } payload.update(kwargs) response = self.session.post( f"{self.base_url}/search", json=payload, timeout=20, ) response.raise_for_status() return response.json() def search_history(self, query: str, timestamp: int, **kwargs): """ Time Machine 历史搜索接口。 :param query: 搜索关键词 :param timestamp: Unix 时间戳,表示要查询的历史时间点 """ payload = { "query": query, "timestamp": timestamp, } payload.update(kwargs) response = self.session.post( f"{self.base_url}/search/history", json=payload, timeout=30, ) response.raise_for_status() return response.json()这个类做了三件事:
- 初始化时把
API Key写入 Session 的请求头,后续所有请求自动携带认证信息。 search方法封装基础搜索,支持通过**kwargs扩展参数。search_history方法封装 Time Machine 调用,把timestamp作为必填参数。
4.4 编写入口程序
文件路径:main.py
import os import time from dotenv import load_dotenv from search_client import KeenableSearchClient def format_results(data: dict) -> None: """格式化打印搜索结果""" print(f"query_id: {data.get('query_id')}") print(f"total: {data.get('total')}") for idx, item in enumerate(data.get("results", []), start=1): print(f"\n[{idx}] {item.get('title')}") print(f" URL: {item.get('url')}") print(f" 摘要: {item.get('snippet')}") print(f" 发布时间: {item.get('published_at')}") def main(): load_dotenv() api_key = os.getenv("KEENABLE_API_KEY") if not api_key: raise ValueError("请先设置 KEENABLE_API_KEY 环境变量") client = KeenableSearchClient(api_key=api_key) # 1. 基础搜索 print("=" * 60) print("基础搜索:Keenable API") print("=" * 60) result = client.search("Keenable API", page_size=5, language="zh-CN") format_results(result) # 2. Time Machine 历史搜索 print("\n" + "=" * 60) print("Time Machine 搜索:查询 2024 年 6 月 1 日的结果") print("=" * 60) # 2024-06-01 00:00:00 UTC 对应的 Unix 时间戳 past_timestamp = int(time.mktime(time.strptime("2024-06-01", "%Y-%m-%d"))) history_result = client.search_history("Keenable API", timestamp=past_timestamp) format_results(history_result) if __name__ == "__main__": main()4.5 运行与验证
先准备环境变量文件.env:
KEENABLE_API_KEY=your_api_key_here然后运行:
python main.py预期会看到两个部分:第一部分输出当前时间的搜索结果,第二部分输出指定历史时间点的搜索结果。如果你的 API Key 没有开通 Time Machine 权限,第二个请求可能会返回权限错误,这时候需要到控制台确认套餐是否包含该能力。
如果返回结果为空,不要急着判断 API 有问题,先确认关键词、时间范围和地区参数是否合理。比如zh-CN语言环境下搜索一个英文新品名,结果可能本身就很少。
5. 进阶:在 AI Agent 中接入搜索能力
搜索 API 单独用价值有限,真正能放大效果的是把它接入 AI Agent 或 RAG 工作流。
5.1 把搜索结果格式化为模型上下文
大模型需要的是结构化、简洁的文本上下文。直接丢原始 JSON 给模型,既浪费 Token,又可能让模型被无关字段干扰。建议把搜索结果格式化为 Markdown 或纯文本列表。
核心片段如下:
def build_context(results: list) -> str: """把搜索结果列表转换为模型友好的上下文文本""" lines = [] for idx, item in enumerate(results, start=1): title = item.get("title", "") url = item.get("url", "") snippet = item.get("snippet", "") published = item.get("published_at", "") lines.append( f"{idx}. [{title}]({url})\n" f" 发布时间: {published}\n" f" 摘要: {snippet}" ) return "\n\n".join(lines)这样转换之后,可以拼进 Prompt:
prompt = f"""请根据以下搜索结果回答用户问题。 搜索结果: {build_context(result.get('results', []))} 用户问题:Keenable 的 Time Machine 是什么? 请用中文回答,并注明信息来源。 """5.2 结果缓存设计
网页搜索 API 通常按调用次数计费。同样一个关键词,在短时间内反复查询非常浪费。建议在应用层加缓存。
import time class SearchCache: def __init__(self, ttl: int = 300): self.cache = {} self.ttl = ttl def get(self, key: str): item = self.cache.get(key) if item and time.time() - item["time"] < self.ttl: return item["data"] return None def set(self, key: str, data): self.cache[key] = {"data": data, "time": time.time()}使用时,在调用client.search之前先查缓存,命中则直接返回。注意缓存时间不要设置太长,否则搜索结果的时效性会下降。
5.3 批量查询的注意事项
批量查询多个关键词时,要注意控制并发。大多数搜索 API 都有 QPS(每秒请求数)限制。你可以用一个简单的信号量控制并发:
from concurrent.futures import ThreadPoolExecutor, as_completed import threading semaphore = threading.Semaphore(2) # 同一时间最多 2 个请求 def safe_search(client, keyword): with semaphore: return client.search(keyword, page_size=5) keywords = ["AI Agent", "大模型", "RAG", "搜索 API"] with ThreadPoolExecutor(max_workers=4) as executor: futures = [executor.submit(safe_search, client, kw) for kw in keywords] for future in as_completed(futures): data = future.result() print(data.get("total"))这里的核心思路是:用Semaphore控制实际进入 API 的并发数,而不是盲目相信线程池的max_workers。
6. 常见报错与排查思路
接入任何 API,都绕不开报错排查。下面把最常遇到的错误整理成表格,方便对照处理。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| HTTP 401 Unauthorized | API Key 缺失、错误或已失效 | 检查环境变量中的 Key,确认没有多余空格;到控制台重新生成 Key |
| HTTP 402 Payment Required | 账户余额不足,或套餐配额耗尽 | 检查账户余额,确认计费套餐,及时充值或升级 |
| HTTP 403 Forbidden | 接口权限不足;IP 白名单拦截 | 确认是否开通了 Time Machine 权限;核实白名单配置 |
| HTTP 404 Not Found | 接口路径写错 | 对照官方文档确认 URL,尤其是版本号/v1部分 |
| HTTP 429 Too Many Requests | 请求频率超过限制 | 降低并发,增加退避重试,或申请提高 QPS 配额 |
| HTTP 5xx | 服务端内部错误 | 先等待几秒重试;持续出现则联系技术支持,并提供query_id |
| HTTP 529 Overloaded | 服务端负载过高,属于临时性故障 | 不要立刻高频重试,使用指数退避策略,间隔 1s、2s、4s 逐步重试 |
| 连接超时 / read timeout | 网络波动或响应时间过长 | 增大超时时间,检查网络代理设置,确认域名可以访问 |
响应中total为 0 | 关键词过于冷门;时间范围过窄;地区参数不当 | 扩大时间范围,调整 language/region,换更通用的关键词 |
6.1 排查清单
遇到报错时,建议按下面的顺序排查,不要一上来就改代码。
- 确认 API Key 是否正确,是否包含多余空格。
- 确认接口地址和环境(测试环境 / 生产环境)是否正确。
- 确认请求参数名和取值是否符合文档要求。
- 确认账户余额和套餐配额是否充足。
- 查看服务端返回的错误信息,而不是只看状态码。
- 如果错误信息包含
request_id或query_id,保留并提交给技术支持。
6.2 重试策略
对于 429、529、5xx 这类临时性错误,合理的重试可以显著提高成功率。一个简单的指数退避实现如下:
import time import requests def request_with_retry(func, max_retries: int = 3): for attempt in range(max_retries): try: return func() except requests.exceptions.HTTPError as e: status_code = e.response.status_code if status_code in (429, 500, 502, 503, 529) and attempt < max_retries - 1: wait_time = 2 ** attempt # 1s, 2s, 4s print(f"请求失败,状态码 {status_code},{wait_time} 秒后重试...") time.sleep(wait_time) continue raise重试不是万能的。如果是 401、403 这类认证或权限错误,重试多少次都没有意义,必须先去解决 Key 和权限问题。
7. 最佳实践与工程建议
7.1 异常处理分层
不要把requests的异常直接抛到业务层。建议在客户端的search方法内部先做一层封装,把网络异常、HTTP 错误、JSON 解析错误分别处理:
class APIError(Exception): def __init__(self, status_code, message, query_id=None): self.status_code = status_code self.message = message self.query_id = query_id super().__init__(f"[{status_code}] {message}")然后在调用处统一捕获APIError,转换成业务层的可读提示。
7.2 安全与凭证管理
API Key 的安全是底线。几条具体的建议:
- 生产环境使用密钥管理服务保存 API Key,而不是写进配置文件。
- 日志中禁止打印完整的 API Key 和请求头。
- 如果 API 支持子 Key 或多 Key,尽量按用途拆分,一个项目用一个 Key,方便单独回收。
- 在服务端配置 IP 白名单,限制 API Key 的使用来源。
7.3 日志与可观测性
每次搜索请求都建议记录以下信息:
- 请求关键词。
- 是否命中缓存。
- 请求耗时。
- 返回状态码。
- 结果数量。
query_id。
有了这些日志,才能回答“为什么这个关键词结果变少了”“为什么今天调用量突然涨了”这类问题。
7.4 成本控制
搜索 API 是按调用量计费的,控制成本可以从几个方向入手:
- 用缓存拦截重复关键词。
- 设置单用户或单任务的调用上限。
- 对批量任务做优先级队列,避免高峰时段集中请求。
- 定时统计各关键词的调用分布,停掉无效的采集任务。
7.5 Time Machine 的使用建议
Time Machine 属于比较重的能力,调用前先想清楚是否真的需要历史数据。需要注意:
- 历史数据覆盖范围可能有限,冷门关键词不一定有历史快照。
- 不建议把 Time Machine 作为实时搜索的默认方案,实时搜索走普通搜索接口即可。
- 历史查询结果建议做好持久化,因为同一时间点的历史快照未必每次返回都一致。
- 涉及法律合规、审计、取证等场景时,务必确认数据来源的合法性和授权边界。
8. 总结与后续学习建议
这篇围绕 Keenable 独立网页搜索 API 和 Time Machine,从概念、环境准备、参数拆解、Python 实战接入、AI Agent 场景集成,到报错排查和工程建议,整理了一条相对完整的路径。核心收获可以归纳为几点:搜索 API 的本质是标准化封装,调用之前先确认参数命名和权限;Time Machine 是面向历史数据检索的能力,适合有回溯需求的场景,但要注意数据覆盖范围;接入过程要把错误处理、重试、缓存、日志当成一等公民来设计,而不是等上线后再补。
如果接下来要继续深入,可以往这几个方向走:一是把搜索能力接入 LangChain、LlamaIndex 这类 Agent 框架,理解 Tool 调用的封装方式;二是研究搜索结果的去重与排序策略,提高喂给大模型的上下文质量;三是做一套完整的调用统计和告警系统,让 API 调用变得可观测、可治理。
动手实践是最好的学习方法,先申请一个 Key,跑通基础搜索,再试着加上 Time Machine 参数,感受一下历史检索和实时检索的区别。遇到报错也别慌,按第六节的排查清单逐项确认,大部分问题都能定位到原因。