作为长期在做 Agent 类应用的开发者,我一直在寻找比“拿传统搜索接口强行套 Prompt”更顺手的方案。如果搜不准、返回噪音大、链接过期,Agent 再聪明也容易一本正经地胡说八道。Keenable 这类面向 AI Agent 的 Web Search API 出现后,思路确实不太一样:它不止是给你 10 条链接,而是把检索结果整理成更容易被大模型消费的上下文。
这篇文章会围绕“AI Agent 如何正确接入 Web Search API”展开,先讲清楚为什么 Agent 需要专用搜索接口,再拆解核心概念,最后给出完整的 Python 集成案例、常见报错和工程落地建议。无论你是在做聊天机器人、数据分析助手,还是自动化工作流,都可以直接参考这套思路。
1. 为什么 AI Agent 需要独立的 Web 搜索 API
1.1 从知识截止到实时检索
大语言模型最大的短板之一,就是“知识有截止日期”。不管是 GPT 还是开源模型,训练数据普遍停留在某个时间点之前。当用户问“今天有什么新闻”“最新版本改了什么”时,模型如果只靠内部参数推理,结果大概率是过时的,甚至直接编造。
常见的解决方案是 RAG(Retrieval-Augmented Generation,检索增强生成)。简单说,就是先通过检索系统拿到最新的外部信息,再把信息拼进提示词,让模型基于这些资料生成答案。而 Web Search API 就是 RAG 体系中负责“实时信息获取”的关键环节。
也就是说:没有 Web Search API,Agent 只能是一个“嘴强王者”;有了 Web Search API,Agent 才真正有了“眼睛”。
1.2 传统搜索 API 与 Agent 搜索 API 的差异
过去几年大家用得比较多的搜索 API,绝大多数是为“人类搜索结果页”设计的。返回结果通常是:
- 标题
- URL
- 摘要片段
- 站点信息
- 类似图片、视频、知识面板这类附加模块
这种结构对浏览器用户是够用的,人眼可以快速扫一下标题和摘要决定要不要点进去。但对 AI Agent 来说,问题很明显:
| 维度 | 传统搜索 API | 面向 Agent 的搜索 API |
|---|---|---|
| 输出结构 | 面向网页展示,字段多而杂 | 面向 LLM 消费,结构精简、语义化 |
| 结果相关性 | 偏向 SEO 排名 | 更强调答案完整度和上下文 |
| 上下文友好度 | 需要二次清洗 | 直接给出可拼接的文本块 |
| 工具调用 | 需要自己封装工具描述 | 原生适配 function calling |
| 引用来源 | 依赖自己拼接 | 返回结构化来源元数据 |
Keenable 这类产品的“different”,本质上是把过去开发者自己要做的一堆脏活、累活,比如结果清洗、去重、摘要截断、来源格式标准化,提前在 API 服务端处理掉了。
1.3 Keenable 的定位与设计思路
从项目定位看,Keenable 是“for AI agents”的 Web Search API,所以它的设计不是从传统搜索接口改一改参数,而是重新考虑了一次“检索结果最终是被谁消费的”。
我们可以把这类 API 的特点概括成三点:
- 面向工具调用场景:返回结果直接符合 function calling 的规范,Agent 拿到 JSON 后不用做复杂的二次解析。
- 内容结构化:每条结果包含稳定的 title、url、content 字段,甚至可能包含发布时间、作者、站点类型等更细的元数据。
- 上下文友好:API 返回的摘要内容经过裁剪,长度接近 LLM 的单条上下文窗口,不会被超长文本撑爆 Token。
另外,Keenable 在设计上对“多轮检索”场景比较友好。Agent 经常会根据初步结果继续追问,比如第一轮搜“Python 3.13 新特性”,第二轮可能搜“Python 3.13 free-threading 性能”。好的 Agent 搜索 API 需要能处理这种链条式检索,而不是每次都是独立的冷启动查询。
2. 面向 Agent 的搜索 API 核心概念拆解
2.1 搜索 API 的基本组成部分
不管是什么 Web Search API,基本组成都绕不开这几个要素:
Query(查询词)这是 Agent 传给搜索服务的关键词或自然语言问句。面向 Agent 的 API 一般会建议直接传自然语言问句,而不是必须拆成关键词。
引擎类型(Engine Type)有些 API 支持普通网页搜索、新闻搜索、图片搜索、学术搜索等不同引擎。Agent 可以根据任务类型选择合适的引擎。
结果条数(Max Results / Top K)控制每次请求返回多少条结果。结果太多会浪费 Token,太少又可能漏掉关键信息。
结构化数据(Structured Data)好的 Agent 搜索 API 会返回足够规范化的字段,比如标题、链接、发布时间、摘要正文等,方便 Agent 直接使用。
下面是一个通用的请求-响应模型示意:
// 请求体(通用结构,具体字段以你使用的API文档为准) { "query": "What's new in Python 3.13?", "max_results": 5, "engine": "web", "time_range": "year" }// 响应体(通用结构,具体字段以你使用的API文档为准) { "results": [ { "title": "Python 3.13.0 released", "url": "https://www.python.org/downloads/release/python-3130/", "content": "Python 3.13.0 is the newest major release of the Python programming language...", "published_at": "2024-10-07T00:00:00Z" } ] }这里要特别强调:不同 API 字段名的差异非常大。比如有的 API 返回snippet,有的返回content,有的返回text;真实集成时必须以服务商文档为准,以上只是通用参考。
2.2 面向 Agent 的优化设计点
如果要设计一个“面向 Agent 的搜索 API”,需要在下面几个方面做特殊优化。
第一,查询理解。传统关键词搜索要求用户把问题拆成词,而 Agent 搜索 API 要能接受完整自然语言。比如用户问“帮我查一下 2025 年全球 AI 芯片市场规模预测”,Agent 可能直接把这个完整句子传入 API,API 内部再做意图识别和关键词提取。
第二,结果压缩。网页原文可能非常长,一个网页几万字很正常,但 LLM 上下文窗口有限。API 需要在服务端做摘要、抽取关键词、识别核心实体,把一篇长文压缩成几百字以内的“上下文块”,这样 Agent 才能低成本地消费。
第三,引用追踪。RAG 应用最怕“模型编造来源”。面向 Agent 的搜索 API 会在返回结果中附带原始 URL、标题、发布时间等信息。这样 Agent 在回答时,可以明确说“根据 Python 官网 2024 年 10 月的公告……”而不是模糊地说“根据网络资料”。
第四,时间感知。对很多查询来说,时间是一个重要约束。比如“Python 最新版本”和“Python 最受欢迎的版本”是两个完全不同的问题。API 最好能自动识别查询对时效性的敏感度,决定是否限制时间范围。
2.3 一个搜索请求的完整生命周期
为了后面代码示例更容易理解,我们先梳理一次 Agent 搜索请求的完整流程。
用户问题 ↓ Agent 判断需要实时信息 ↓ 构造搜索工具描述,发给 LLM ↓ LLM 返回工具调用指令(query、top_k 等) ↓ Agent 调用 Web Search API ↓ 拿到结构化搜索结果 ↓ 拼接上下文,重新发给 LLM ↓ LLM 基于搜索内容生成最终答案这个流程看起来不复杂,但在真实代码里,每一步都有不少细节。比如工具描述怎么写才容易被 LLM 正确触发?搜索结果怎么裁剪?多轮检索时怎么保留上下文?这些我会在第 4 节的代码案例中逐一演示。
3. 环境准备与通用配置
3.1 运行环境版本说明
我默认使用如下环境,你在实际项目中可以根据自身情况调整:
- 操作系统:macOS / Linux / Windows 均可,示例代码没有平台依赖
- Python:3.10 及以上(示例使用类型注解,3.8 也可以运行,部分写法需微调)
- 依赖库:
requests(HTTP 客户端)、可选openai(如果你把 Agent 接 GPT 系列模型) - IDE:任意,推荐 VS Code 或 PyCharm
安装依赖命令:
pip install requests openai python-dotenv需要说明的是,上面这组版本只是常见组合,并不代表 Keenable 或任何具体 API 的强制要求。你使用的搜索 API 版本、LLM 模型版本要以对应官方文档为准。
3.2 获取 API Key
Web Search API 一般都需要 API Key 鉴权。不同平台的获取流程不同,但大同小异:
- 注册开发者账号。
- 在控制台创建一个应用。
- 获取 API Key(有时也叫 Access Token)。
- 根据套餐开通对应的搜索能力。
安全提醒:API Key 一定要放在环境变量或配置中心,不要硬编码到代码仓库里。
# 在项目根目录创建 .env 文件 SEARCH_API_KEY=your_search_api_key_here SEARCH_API_BASE_URL=https://api.example.com # 可选:如果你接 OpenAI OPENAI_API_KEY=your_openai_api_key_here然后通过python-dotenv加载:
# 文件路径:config.py import os from dotenv import load_dotenv load_dotenv() SEARCH_API_KEY = os.getenv("SEARCH_API_KEY") SEARCH_API_BASE_URL = os.getenv("SEARCH_API_BASE_URL") OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")3.3 项目基础结构
为了让代码清晰可维护,我建议按下面的结构组织项目:
agent-search-demo/ ├── .env # 环境变量,不要提交到仓库 ├── config.py # 配置读取 ├── search_client.py # 搜索 API 客户端封装 ├── agent.py # Agent 主逻辑 ├── main.py # 入口脚本 └── requirements.txt # 依赖声明这样做的原因是把“搜索能力”和“Agent 逻辑”解耦。如果以后从某个搜索 API 迁移到另一个,只需要改search_client.py,Agent 主流程不用动。
4. 完整实战:让 AI Agent 具备搜索能力
4.1 设计 Agent 搜索流程
我们这次要实现一个比较真实的最小 Agent:它接收用户问题,判断是否需要搜索,如果需要就调用 Web Search API,然后把搜索结果拼接进提示词,最后让 LLM 生成带引用的答案。
为了不把示例绑定到某个具体搜索服务商,我先把 HTTP 调用部分抽象出来。无论你用的是 Keenable 还是其他 web search API,只需要调整search_client.py里的base_url和请求体字段名。
4.2 封装搜索客户端
先写搜索客户端的核心类。这里使用通用的requests来实现 POST 请求,并将 JSON 响应解析为统一的数据结构。
# 文件路径:search_client.py from typing import Dict, List, Optional import requests class WebSearchClient: """通用 Web Search API 客户端封装。 这是一个通用骨架,真实对接时请以你使用的 API 文档为准, 主要调整 _request_params 方法和 _parse_response 方法即可。 """ def __init__( self, api_key: str, base_url: str, timeout: int = 10, ): self.api_key = api_key self.base_url = base_url.rstrip("/") self.timeout = timeout self.session = requests.Session() self.session.headers.update({ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }) def search( self, query: str, max_results: int = 5, time_range: Optional[str] = None, ) -> List[Dict]: """执行一次搜索,返回统一结构的结果列表。 参数: query: 自然语言查询 max_results: 返回结果条数 time_range: 时间范围,例如 day / week / month / year """ resp = self.session.post( f"{self.base_url}/search", json=self._build_payload(query, max_results, time_range), timeout=self.timeout, ) resp.raise_for_status() data = resp.json() return self._parse_response(data) def _build_payload( self, query: str, max_results: int, time_range: Optional[str], ) -> Dict: payload: Dict = { "query": query, "max_results": max_results, } if time_range: payload["time_range"] = time_range return payload def _parse_response(self, data: Dict) -> List[Dict]: """把不同 API 的响应统一成易消费结构。 很多 API 返回的字段可能是 title / url / snippet, 也可能是 title / link / content。这里统一做映射。 """ raw_results = data.get("results", data.get("items", [])) parsed = [] for item in raw_results: parsed.append({ "title": item.get("title", ""), "url": item.get("url") or item.get("link", ""), "content": ( item.get("content") or item.get("snippet") or item.get("text") or "" ), "published_at": item.get( "published_at", item.get("publish_time", ""), ), }) return parsed4.3 基于搜索结果增强提示词
拿到搜索结果之后,不能直接把整个返回体丢给 LLM,那样会浪费 Token,而且可能因为结构复杂影响模型理解。建议先做一步“上下文构建”。
# 文件路径:agent.py(部分代码) from typing import Dict, List def build_context(results: List[Dict], max_items: int = 5) -> str: """把搜索结果整理成提示词友好文本。""" blocks = [] for idx, item in enumerate(results[:max_items], start=1): title = item.get("title", "").strip() url = item.get("url", "").strip() content = item.get("content", "").strip() block = f"[{idx}] {title}\n来源: {url}\n内容: {content}" blocks.append(block) return "\n\n".join(blocks)这样拼接出来的文本,每一个来源都带序号和 URL,后续让 LLM 回答时可以按序号引用,便于生成可追溯的答案。
4.4 工具调用方式接入 Agent
现代 Agent 通常采用 function calling 模式:LLM 决定调用什么工具、传什么参数,Agent 执行工具后再把结果返回给 LLM。
下面是接入 OpenAI 风格 tool calling 的示例。如果你使用的是其他 LLM,比如 Claude、通义千问、DeepSeek,调用方式大同小异。
# 文件路径:agent.py import json from typing import Dict, List from config import OPENAI_API_KEY from search_client import WebSearchClient try: from openai import OpenAI except ImportError: OpenAI = None class SearchAgent: def __init__( self, search_client: WebSearchClient, model: str = "gpt-4o-mini", ): self.search_client = search_client self.model = model if OpenAI is None: raise RuntimeError("未安装 openai 库,请先执行 pip install openai") self.client = OpenAI(api_key=OPENAI_API_KEY) @property def tools(self) -> List[Dict]: """工具描述,用于让模型理解如何使用搜索能力。""" return [ { "type": "function", "function": { "name": "web_search", "description": "当用户问题涉及实时信息、最新新闻、外部网页内容时,使用此工具检索网络资料。", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "用户问题,建议使用自然语言完整问题,例如 'Python 3.13 有哪些新特性'", }, "max_results": { "type": "integer", "description": "返回结果数量,默认 5", "minimum": 1, "maximum": 10, }, }, "required": ["query"], }, }, } ] def run(self, question: str, max_turns: int = 3) -> str: """执行一问一答,支持最多多轮工具调用。""" messages = [{"role": "user", "content": question}] turn = 0 while turn < max_turns: resp = self.client.chat.completions.create( model=self.model, messages=messages, tools=self.tools, tool_choice="auto", ) msg = resp.choices[0].message if not msg.tool_calls: # 模型没有调用工具,直接返回最终答案 return msg.content or "" messages.append(msg) # 逐个执行工具调用 for tool_call in msg.tool_calls: if tool_call.function.name != "web_search": continue args = json.loads(tool_call.function.arguments) query = args.get("query", question) max_results = args.get("max_results", 5) print(f"[Tool] web_search: {query}") results = self.search_client.search( query=query, max_results=max_results, ) context = build_context(results) tool_message = { "role": "tool", "tool_call_id": tool_call.id, "content": context, } messages.append(tool_message) turn += 1 # 达到最大轮数仍未结束,做一次强制收尾 final_resp = self.client.chat.completions.create( model=self.model, messages=messages, ) return final_resp.choices[0].message.content or ""4.5 运行与验证
最后写一个入口脚本,把整个流程串起来。
# 文件路径:main.py from config import SEARCH_API_BASE_URL, SEARCH_API_KEY from search_client import WebSearchClient from agent import SearchAgent def main(): search_client = WebSearchClient( api_key=SEARCH_API_KEY, base_url=SEARCH_API_BASE_URL, ) agent = SearchAgent(search_client=search_client) question = "2025 年值得关注的 AI Agent 开发框架有哪些?" answer = agent.run(question) print("======== 最终答案 ========") print(answer) if __name__ == "__main__": main()运行命令:
python main.py如果一切正常,你会先看到工具调用的日志,然后看到最终答案。示例输出类似:
[Tool] web_search: 2025 年值得关注的 AI Agent 开发框架有哪些? ======== 最终答案 ======== 根据 2025 年的公开资料,以下框架值得关注: 1. LangGraph - 适合构建有状态的多步骤 Agent 工作流... 2. AutoGen - 微软开源的动态多 Agent 协作框架... 3. CrewAI - 面向任务编排的角色化 Agent 团队框架... ...如果你没有可用的 LLM Key,只想先测试搜索客户端,可以单独跑一行:
# 临时测试脚本 from config import SEARCH_API_BASE_URL, SEARCH_API_KEY from search_client import WebSearchClient client = WebSearchClient(SEARCH_API_KEY, SEARCH_API_BASE_URL) results = client.search("OpenAI o1 推理模型", max_results=3) for r in results: print(r["title"], r["url"])5. 常见问题与排查思路
实际接入时,你大概率会遇到下面几类问题。我整理了一个排查表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 请求返回 401 / 403 | API Key 错误、过期或权限不足 | 检查 Key 是否写对,确认账号是否开通搜索服务 |
| 返回结果为空 | 搜索词太生僻,或 API 未指定引擎 | 增加关键词、换通用引擎、检查 time_range 是否过窄 |
| Agent 不调用搜索工具 | 工具描述不够清晰,或模型不理解 | 改写工具 description,补充触发条件示例 |
| 前端请求超时 | 搜索 API 响应慢,或网络问题 | 调大 timeout,增加重试机制,或切换更近的服务节点 |
| 返回内容过长,Token 超限 | 结果条数太多或单条内容太长 | 限制 max_results,在 API 层或代码层做截断 |
| 回答中出现“幻觉引用” | 搜索结果拼接不完整,模型分不清来源 | 显式要求模型引用 [n] 序号,禁止编造链接 |
| 费用快速上涨 | 每次搜索都走多轮循环,Token 消耗大 | 增加缓存、控制 max_turns、搜索前先做意图判断 |
5.1 Agent 不调用工具怎么办
这是最常见的集成问题。大多数情况不是代码错了,而是工具描述不够“吸引”模型触发。
前文tools定义里,我把web_search的description写成了“当用户问题涉及实时信息、最新新闻、外部网页内容时”。这个描述已经比较明确,但在实际项目中,你还可以更具体:
"description": "当用户询问的内容非常有可能依赖最新数据、新闻事件、产品版本、市场行情, 或者你确认自己的训练数据中没有相关信息时,优先调用 web_search 获取最新资料。"另外,要把用户问题改写得更完整。很多 Agent 框架会内置一个“query rewriting”步骤,把“它会不会下蛋”改写成“企鹅会下蛋吗”,从而提升搜索效果。
5.2 搜索结果与问题不相关
如果 API 返回的内容偏了,可能是搜索 API 服务端的关键词抽取能力有限。这时可以人工改写查询词。
例如用户问题:“Python 比 Java 好在哪里?”
如果直接传这个句子,搜索 API 可能返回大量主观讨论帖。更稳的做法是先让 LLM 拆解成搜索词:
用户问题:Python 比 Java 好在哪里? 搜索词1:Python vs Java 性能 对比 搜索词2:Python Java 适用场景 区别然后分别搜索,再合并结果。
5.3 上下文过长怎么处理
搜索结果动辄几千字,如果一次返回 10 条,很容易超出上下文窗口。处理策略有三个:
- 减少
max_results,把结果数量从 10 降到 5。 - 对每一条结果再次调用 LLM 做摘要,只保留和用户问题相关的句子。
- 在 API 层配置内容长度上限,比如只取开头 500 字符。
其中第二点效果最好,但会消耗额外 Token,适合对答案质量要求较高的场景。
6. 最佳实践与工程建议
6.1 查询改写应该由 LLM 或规则完成
搜索 API 虽然能理解自然语言,但对“口语化、有歧义、指代不明”的提问,效果会打折扣。工程上建议至少做一层查询改写。
示例函数:
def rewrite_query(question: str) -> List[str]: """把口语问题拆成搜索关键词。这里演示规则写法,生产环境建议用 LLM。""" # 简单去问问号和礼貌用语 question = question.replace("我想知道", "").replace("帮我查一下", "") question = question.strip(",。?? ") return [question]更复杂的场景建议把“改写”作为一个独立工具,交给 LLM 完成。
6.2 缓存机制非常重要
Agent 场景中,模型经常会对同一个问题做多次工具调用,或者多个用户问相似问题。如果你的 API 按次计费,缓存能省下不少成本。
缓存策略建议:
- 以“归一化之后的查询词”为 key。
- 缓存时间按关键词类型区分:新闻资讯类缓存 10 分钟,技术文档类缓存 24 小时,常识类缓存 7 天。
- 缓存数据除了搜索结果,还要保留抓取时间,避免长期不更新。
示例使用 Python 内置functools.lru_cache做进程内缓存:
from functools import lru_cache @lru_cache(maxsize=256) def cached_search(query: str, max_results: int = 5): # 这里调用真实的搜索客户端 return tuple(client.search(query, max_results=max_results))注意lru_cache的参数必须是可哈希类型,返回结果最好转成不可变结构。生产环境建议使用 Redis。
6.3 结果去重与排序
同一个话题在不同站点可能被多次转载,搜索结果中经常出现“标题不同、正文相似”的内容。设计搜索引擎时一般会做去重;如果 API 没做,你就需要在代码里处理。
去重思路:
- 按 URL 去重,删除完全一样的链接。
- 按标题相似度去重,比如计算字符串相似度。
- 按内容摘要去重,取前 100 字符做 hash。
推荐使用difflib.SequenceMatcher做简单相似度判断:
from difflib import SequenceMatcher def is_similar(a: str, b: str, threshold: float = 0.85) -> bool: return SequenceMatcher(None, a, b).ratio() > threshold6.4 安全与合规注意事项
Web Search API 承担着 Agent 的“外部信息入口”角色,也意味着它同时是“风险入口”。工程上不能只关注能跑通,还要考虑安全和合规边界。
- 最小权限原则:为 Agent 申请独立 API Key,不要与生产主账号共用一个 Key;权限只开放给需要的引擎和接口。
- 内容安全:搜索结果可能包含恶意链接、钓鱼站点、违法内容。Agent 端可以做链接白名单校验,或对搜索结果做敏感词过滤。
- 输出校验:模型生成答案时,要校验引用的 URL 是否真的出现在搜索结果中;不允许模型凭空编造来源。
- 操作边界:如果 Agent 后续有写操作、购买操作、修改数据能力,必须增加人工确认环节,不能完全依赖模型判断。
6.5 可观测性:日志与追踪
Agent 接入搜索 API 之后,调试难度会明显上升。你看到“答案不对”,但很难直接判断是搜索没搜对、上下文拼坏了,还是模型理解错了。建议从第一阶段就加上结构化日志。
每次搜索记录:
- 原始用户问题
- 改写后的查询词
- API 返回条数与耗时
- 被拼接进 Prompt 的前几条结果 URL
- 最终答案与前若干字符
日志尽量输出为 JSON 格式,方便后续接入日志平台。
import logging logger = logging.getLogger("agent") logger.info( "search_finish", extra={ "url": url, "query": query, "result_count": len(results), "elapsed_ms": elapsed_ms, }, )7. 总结与下一步学习路线
这篇文章从一个真实的天使项目 Keenable 出发,聊清楚了面向 AI Agent 的 Web Search API 的几个关键问题:它和传统搜索 API 的区别、Agent 搜索流程是怎样的、如何用 Python 封装一个通用搜索客户端、怎么通过 function calling 让模型主动触发搜索,以及工程落地时的缓存、安全、日志和排错策略。
如果你正在构建自己的 Agent,下一步可以按这个顺序深入:
- 先用最简代码跑通“搜索 → 拼接上下文 → 模型回答”的最小闭环。
- 再做查询改写与结果裁剪,观察答案质量是否提升。
- 然后接入缓存和日志,评估成本与响应延迟。
- 最后考虑多工具协同,比如让 Agent 同时具备搜索、代码执行、数据库查询能力。
实际项目中最优先关注的三个风险点一定是:出口真实性与可追溯性、Token 成本控制、以及搜索 API 的鉴权安全。把这三件事做好,Agent 能力才能稳定可靠。
如果你觉得这套集成思路有帮助,建议先收藏备用,等真正动手接搜索能力的时候拿出来对照着写。