
在 AI Agent 项目中Web Search API 和传统搜索引擎 API 的差异比很多人预想的大。Keenable 是一个以 Show HN 形式出现的 Web Search API定位是给 AI Agent 提供服务而不是给人类用户展示一个带链接、广告和推荐卡片的搜索页面。它的核心价值在于AI Agent 不需要“好看”的页面它需要的是结构化、可引用、低 token 消耗、适合直接拼进上下文的数据。这篇文章围绕 Keenable 这类面向 Agent 的搜索 API讲清楚它为什么和传统搜索 API“不同”以及工程上如何接入、解析、调用、排错和落地到生产环境。如果只是把一个普通网页搜索接口包一层 JSON问题并不会消失。真正需要变化的是搜索结果的粒度、字段、去重逻辑、时效控制、引用方式和错误处理这些细节直接决定了 Agent 是能把搜索结果变成答案还是把一堆噪声塞进上下文。1. AI Agent 为什么需要一套“不同”的 Web Search API1.1 人类搜索和 Agent 搜索的边界在哪里人类在搜索引擎里输入关键词面对的是一个结果列表。用户会自己判断哪个链接值得点、哪个摘要足够解决问题甚至能忍受广告和偏移。Agent 则不同它没有主动浏览页面的能力通常只能拿到一段文本然后基于这段文本继续推理。搜索结果对 Agent 来说不是一个“入口”而是“上下文的一部分”。这意味着搜索 API 的返回格式必须做几件事把网页标题、摘要、来源 URL、发布时间等字段分离出来而不是混杂在一堆 HTML 中。直接返回 JSON避免 Agent 再做一层 HTML 解析。控制 snippet 的长度避免一个结果吃掉大量上下文窗口。保留可追溯的引用编号让 Agent 在回答中能标明信息来源。提供 freshness、region、safe_search 等参数让 Agent 能在不同场景下缩小范围。传统的网页搜索 API 也能返回 JSON但从设计目标看它往往服务于应用开发者开发者再把结果渲染成页面或列表。Keenable 这类面向 Agent 的 API会把“为 LLM 生成答案”作为第一目标因此返回结构、字段命名和交互方式都会更贴近工具调用。1.2 从传统搜索 API 到 Agent 搜索 API 的核心差异对比维度面向人类应用的搜索 API面向 AI Agent 的搜索 API主要消费者Web 应用、移动端页面LLM、Agent 工具调用代码返回格式JSON 是基础有时还需 HTML 片段JSON并且字段尽量扁平、稳定单条结果内容摘要、缩略图、站点信息Snippet、发布时间、来源 URL、引用编号关键成本点击、转化、广告收入Token 消耗、延迟、引用准确性去重需求通常按 URL 去重需要按信息内容去重避免一句换多个网页重复引用要求不严格必须能映射到具体来源防止编造来源时效参数有但不一定暴露给调用方需要 fine-grained比如 1h、1d、1w错误处理可容忍出现“无结果”Agent 需要明确知道为什么失败才能决定下一步表格右边这一列就是 Keenable 这类 API 努力解决的问题。它不是说“搜索引擎换个皮”而是把搜索能力的输出从“给人看的结果”改造成“给模型用的证据”。2. Keenable 这类搜索 API 的概念模型2.1 一个典型的 Agent 搜索请求在接入任何搜索 API 之前先要确认三件事鉴权方式、基础域名、参数协议。下面用一组通用 REST 请求示例演示Keenable 如果遵循同类设计替换 base_url 和 key 就可以。如果实际接口字段不同以服务方提供的文档为准。一个典型的 Agent 搜索请求通常包含以下部分query查询词必须显式编码。top_k 或 limit返回多少条结果。freshness只返回最近多长时间的结果。region限定搜索区域或语言。dedup是否对相似信息去重。safe_search是否启用安全搜索。用 curl 发一个请求效果如下export KEENABLE_API_KEYyour_api_key export KEENABLE_BASE_URLhttps://api.keenable.example.com curl -G $KEENABLE_BASE_URL/v1/search \ -H Authorization: Bearer $KEENABLE_API_KEY \ -H Accept: application/json \ --data-urlencode qLLM function calling best practices \ --data-urlencode top_k5 \ --data-urlencode freshness1w \ --data-urlencode deduptrue这段命令的关键点-G配合--data-urlencode避免 query 里有中文、空格或特殊符号时破坏 URL。鉴权放在请求头里而不是拼到 URL。明确Accept: application/json告诉服务端只接受 JSON 响应。参数freshness1w表示只看一周内内容适合时效型问题。参数deduptrue表示去除相同或高度相似的结果避免多个网页反复表达同一件事。2.2 响应结构如何为 Agent 服务面向 Agent 的搜索响应通常不是{ status: 0, list: [{...}] }这种只有业务码的空壳而会包含更多可消费字段。一个常见的响应结构如下{ query: LLM function calling best practices, total_results: 128, items: [ { citation_index: 1, title: A Practical Guide to Function Calling in LLMs, url: https://example.com/practical-function-calling, snippet: Function calling is most useful when the model needs to trigger a tool..., published_date: 2025-06-10, crawled_at: 2025-06-11T08:12:00Z, site_name: example.com, language: en, relevance_score: 0.93, deduplicated_from: null }, { citation_index: 2, title: How Agents Use Search Results, url: https://example.org/agents-search, snippet: Search APIs for agents should return compact text snippets and source metadata..., published_date: 2025-06-08, crawled_at: 2025-06-11T07:55:00Z, site_name: example.org, language: en, relevance_score: 0.88, deduplicated_from: null } ] }这些字段对 Agent 的价值分别是citation_index在生成回答时使用[1]、[2]这样的标记让用户能回到原网页。title用于快速判断结果是否相关。url结果的原始来源也是引用链接的最终目标。snippet给 LLM 的核心文本长度越短占用上下文越少。published_date判断结果是否过期尤其是新闻、政策、技术版本类问题。crawled_at搜索引擎第一次抓到该页面的时间和发布时间不同。site_name站点名可用来做黑名单或白名单过滤。language避免在多语言场景下混入无关语言结果。relevance_score如果服务提供调用方可以按得分二次筛选。deduplicated_from如果启用去重这个字段记录它原本和哪条结果合并。2.3 为什么字段设计比“返回多少条”更重要很多接入者在第一次对接搜索 API 时只关心top_k和响应速度忽略了字段语义。一旦到了真实 Agent 项目最痛苦的问题不是搜索不到结果而是搜索到了结果但不知道该怎么用没有published_dateAgent 无法判断“消息是否最新”。没有citation_indexAgent 生成回答时无法注明来源。snippet过长导致 5 条结果就把上下文占满。没有去重字段同样的信息从 10 个网站重复出现Agent 会误以为这是共识。所以在选型或接入阶段第一件事不是急着发请求而是检查响应结构是否具备这些“Agent 友好字段”。这正是 Keenable 这类“不同”搜索 API 的核心卖点。3. 准备环境和发出第一次调用3.1 环境准备清单我在实际项目中建议按下面这套清单准备环境避免后续反复排查配置问题检查项推荐值说明编程语言Python 3.9 / Node.js 18以自己团队技术栈为准HTTP 客户端requests / fetch / axiosPython 项目优先 requestsAPI Key从服务方后台获取建议用环境变量保存不进代码库Base URL从服务方文档获取测试环境和生产环境分开配置超时时间5 到 15 秒搜索接口不应无限等待编码UTF-8处理中文 query 时必须注意如果原始项目没有给出明确版本和域名落地前先确认当前 Keenable 的文档地址、鉴权方式、是否区分沙箱环境。直接用一个未经验证的 base_url 写进生产代码是很多接入事故的起点。3.2 Python 最小调用示例安装依赖pip install requests用一个 Python 文件把搜索 API 包成函数import json import os import requests def search_web(query, top_k5): api_key os.environ[KEENABLE_API_KEY] base_url os.environ.get( KEENABLE_BASE_URL, https://api.keenable.example.com ) resp requests.get( f{base_url}/v1/search, params{ q: query, top_k: top_k, }, headers{ Authorization: fBearer {api_key}, Accept: application/json, }, timeout10, ) resp.raise_for_status() return resp.json() if __name__ __main__: result search_web(Python async web crawling, top_k5) print(json.dumps(result, ensure_asciiFalse, indent2))运行前设置环境变量export KEENABLE_API_KEYyour_api_key export KEENABLE_BASE_URLhttps://api.keenable.example.com python search_demo.py这里的几个工程决策要说明os.environ[KEENABLE_API_KEY]强制要求环境变量存在避免不小心把 key 提交到 Git。timeout10设置总超时防止搜索接口无响应时挂起整个 Agent。resp.raise_for_status()会在 HTTP 4xx/5xx 时抛异常便于后续统一处理。打印时使用ensure_asciiFalse避免中文片段被转成一堆\uXXXX。正常结果应该是一个 JSON 对象其中包含items数组。如果返回的是网页 HTML 或格式不符合预期先检查Accept请求头、请求域名和路径。3.3 用 Node.js 调用时的注意事项如果 Agent 服务基于 Node.js可以用自带 fetchconst apiKey process.env.KEENABLE_API_KEY; const baseUrl process.env.KEENABLE_BASE_URL || https://api.keenable.example.com; async function searchWeb(query, topK 5) { const url new URL(${baseUrl}/v1/search); url.searchParams.set(q, query); url.searchParams.set(top_k, String(topK)); const resp await fetch(url, { method: GET, headers: { Authorization: Bearer ${apiKey}, Accept: application/json, }, signal: AbortSignal.timeout(10000), }); if (!resp.ok) { throw new Error(Search API error: ${resp.status} ${resp.statusText}); } return resp.json(); }Node.js 18 以上自带fetch和AbortSignal.timeout不需要额外依赖。URLSearchParams会自动处理 query 编码中文和空格都不会破坏 URL。4. 把搜索 API 封装成 Agent 的 Tool4.1 先定义函数签名而不是直接拼 Prompt智能体应用现在普遍使用 Function Calling / Tool Calling。接入 Web Search API 时第一步是给模型定义一个清晰的工具函数{ type: function, function: { name: web_search, description: Search the live web and return structured results with source URLs., parameters: { type: object, properties: { query: { type: string, description: The search query in natural language. }, freshness: { type: string, enum: [1h, 1d, 1w, 1m, any], description: Only return results published within this time. }, top_k: { type: integer, description: Number of results to return., minimum: 1, maximum: 20 } }, required: [query] } } }enum限制 freshness 的取值范围能明显减少模型生成无效参数的概率。top_k设置最大 20避免一次请求返回过多结果把上下文撑爆。4.2 在 Agent 循环里调用搜索工具下面是一段简化但完整的 OpenAI Function Calling 调用流程。它展示了 Agent 如何决定搜索、调用工具、把工具结果重新交给模型import json import openai from search_api import search_web def tool_web_search(arguments: str): args json.loads(arguments) query args[query] freshness args.get(freshness, any) top_k args.get(top_k, 5) result search_web(query, top_ktop_k) # 只保留核心字段避免把无意义的大对象塞进上下文 items result.get(items, []) compressed [] for i, item in enumerate(items[:top_k], start1): compressed.append({ citation_index: item.get(citation_index, i), title: item.get(title), url: item.get(url), snippet: item.get(snippet), published_date: item.get(published_date), site_name: item.get(site_name), }) return json.dumps(compressed, ensure_asciiFalse) def run_agent(user_input: str): messages [{role: user, content: user_input}] tools [ { type: function, function: { name: web_search, description: Search the live web for current information., parameters: { type: object, properties: { query: {type: string}, freshness: {type: string, enum: [1h, 1d, 1w, 1m, any]}, top_k: {type: integer, minimum: 1, maximum: 20} }, required: [query] } } } ] response openai.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools, tool_choiceauto, ) message response.choices[0].message if message.tool_calls: messages.append(message) for tool_call in message.tool_calls: if tool_call.function.name web_search: tool_output tool_web_search(tool_call.function.arguments) messages.append({ role: tool, tool_call_id: tool_call.id, content: tool_output, }) final_response openai.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools, ) return final_response.choices[0].message.content return message.content这段代码看起来简单但它把几个关键问题都固定住了搜索结果不是直接把 API 响应原样返回而是做了一层字段裁剪。保留了citation_index让模型可以在回答中写[1]、[2]。用tool_call_id把工具调用和工具结果绑定符合 OpenAI 的协议要求。4.3 把搜索结果紧凑地拼进 Prompt很多 Agent 框架会在后台把tool消息直接塞给模型。如果搜索 API 返回 20 条完整网页摘要每条几百字加上 JSON 键名一次搜索就可能消耗 5000 到 8000 token。这个代价在真实应用中非常高。推荐做成一个压缩函数def compact_results(result, max_items5, max_snippet_chars300): lines [] for i, item in enumerate(result.get(items, [])[:max_items], start1): title item.get(title, ) url item.get(url, ) snippet (item.get(snippet) or )[:max_snippet_chars] date item.get(published_date, ) lines.append( f[{i}] {title}\n fSource: {url}\n fDate: {date}\n fSnippet: {snippet} ) return \n\n.join(lines)压缩后的文本可以直接放进 System Prompt 或 Tool Result[1] A Practical Guide to Function Calling in LLMs Source: https://example.com/practical-function-calling Date: 2025-06-10 Snippet: Function calling is most useful when the model needs to trigger a tool...这样既保留信息又降低 token 成本和模型理解成本。5. 按场景调整搜索参数和策略5.1 常见场景的参数对照表场景推荐 freshness推荐 top_k推荐设置原因实时新闻、热点事件1h 或 1d10按发布时间排序需要最新信息旧结果没有价值技术文档、API 用法any5优先官方站点文档类内容时效要求低商品对比、价格咨询1d 或 1w10限定 region价格和库存随时变化学术概念、深度解释1m 或 any20关闭高严格去重需要多角度讨论不能过度去重医疗、法律、金融问题any5高 safety强引用内容准确性和来源可靠性优先需要确认 Keenable 实际支持哪些参数。不同 API 可能使用不同的参数名比如top_k可能叫limitfreshness可能叫time_period。接入时先查文档再用最小样例参数验证。5.2 时效参数怎么选择freshness是 Agent 搜索最容易忽略的参数。模型本身有知识截止日期搜索的目的通常是补充“模型不知道的新信息”。如果用户问的是“今天某地天气”而 API 返回一周前的文章答案会明显错误。在代码里可以根据问题类型动态决定 freshnessdef decide_freshness(question: str): question_lower question.lower() if any(word in question_lower for word in [today, latest, breaking, now, 最新, 今天, 新闻]): return 1d if any(word in question_lower for word in [this week, this month, 本周, 本月]): return 1w return any这种做法简单有效但它依赖关键词判断不一定准确。更可靠的做法是让 LLM 在 Function Calling 参数里自行决定freshness因为它能理解问题的隐含时效。5.3 要不要开启去重面向 Agent 的去重核心不是让 URL 列表变短而是让“信息观点”不重复。比如 10 个网站都在转载同一篇新闻如果不做去重Agent 会看到 10 条相似内容误以为“大量来源都这么说”。开启去重后API 会保留权威或最早来源把其他转载合并或丢弃。不过去重也有副作用。在需要多元观点的选题场景比如“某技术方案的优缺点”过度去重会让 Agent 只看常见观点忽略少数派意见。所以去重不是越强越好而是要看场景。6. 错误处理、限流和可观测性6.1 常见 HTTP 错误和应对方式HTTP 状态常见原因建议处理400参数错误比如 top_k 超范围检查请求参数修正后重发401API Key 无效或缺失检查环境变量和请求头403无权限、IP 白名单不通过检查账号权限和来源 IP429触发限流或并发额度增加缓存使用退避重试500服务端内部错误等待后重试持续则报障502 / 503网关或服务过载退避重试考虑降级方案504网关超时降低 top_k或改用异步查询错误处理的核心原则是不要把 HTTP 错误直接抛给 Agent 当最终答案也不要无限重试。Agent 需要知道“搜索失败”然后决定是换个 query 还是直接基于已有知识回答。6.2 带退避的重试函数在封装搜索 API 时建议写一个带退避的重试函数import random import time import requests def search_with_retry(query, top_k5, retries3): last_error None for attempt in range(retries): try: return search_web(query, top_ktop_k) except requests.HTTPError as exc: status exc.response.status_code if status in (400, 401, 403): # 这些错误重试也没用直接抛 raise if status in (429, 502, 503, 504) and attempt retries - 1: sleep_seconds 2 ** attempt random.uniform(0, 1) time.sleep(sleep_seconds) continue last_error exc raise last_error注意两点400、401、403 这类“调用方需要修正”的错误不要重试重试只会浪费请求。429、502、503、504 这类“过载”错误可以指数退避重试。6.3 生产环境的可观测性设计搜索引擎 API 进入生产环节后日志要比“成功了/失败了”更细。至少记录以下字段{ timestamp: 2025-06-11T08:20:00Z, request_id: req_123456, query: latest python asyncio version, freshness: any, top_k: 5, http_status: 200, latency_ms: 1240, result_count: 5, error_code: null }这份日志可以回答几个问题搜索接口最近是不是变慢了哪些 query 一直返回空结果触发了多少 429Agent 最终是否引用了搜索结果如果 API 响应里有服务端 request_id也要保留。后续排查时把客户端日志和服务端日志关联起来会快很多。7. 常见坑和排查链路7.1 坑一query 没有正确编码问题现象query 里有中文、空格、或请求返回 500 或结果为空。可能原因手动拼接 URL比如url ?q query特殊符号破坏了 query 参数。检查方式打印最终请求 URL看q参数是否被正确编码。解决方案使用requests的params、curl的--data-urlencode、Node 的URLSearchParams。7.2 坑二把 snippet 里的内容当成事实问题现象Agent 根据搜索 snippet 生成了答案但答案是错的或者引用了来源却对应不上。可能原因snippet 只是网页的一段截取可能不完整甚至和页面整体结论冲突调用方没有保留引用编号导致模型乱标来源。检查方式把 Agent 输出中的引用编号和搜索结果里的citation_index对齐。解决方案在传给模型的文本里保留编号。生成答案后对引用编号做一次校验。重要结论尽量让 Agent 再访问原始 URL而不是只依赖 snippet。7.3 坑三忽略 freshness 导致时效性答案陈旧问题现象用户问“最新的 Python 版本”Agent 给出的是几个月前的结果。可能原因调用web_search时没有设置freshness默认返回了综合排序的结果。检查方式查看请求日志里的 freshness 字段看是否传了。解决方案在工具定义中默认freshness1w或根据问题意图动态调整“今天/本周/本月”。7.4 排查顺序建议如果搜索接口表现异常按下面顺序排查确认 query 是否正确编码。确认请求域名和路径是否正确。确认 API Key 是否有效、权限是否足够。确认参数名是否与文档一致。查看 HTTP 状态码和错误体。查看服务端 request_id 和客户端日志。降低 top_k减少超时概率。检查网络出口和防火墙是否放行白名单。不要一上来就怀疑 Agent 框架或 Prompt 写得不好。很多“搜索效果差”的问题根源是请求参数、编码或响应解析。8. 生产落地清单和扩展方向8.1 发布前检查清单检查项是否完成说明API Key 使用环境变量禁止硬编码到代码库Base URL 按环境区分测试、预发、生产分开超时时间显式设置建议 5 到 15 秒错误类型完整处理调用方错误不重试限流错误退避日志包含 query、latency、request_id保证事后可排查搜索结果有本地缓存降低成本减轻接口压力有备用搜索源主源故障时可降级上下文做字段裁剪防止 token 爆炸引用编号保留生成答案可溯源有内容安全过滤涉及医疗、法律等内容要提示核实生产环境监控告警429、5xx、耗时超阈值要告警8.2 扩展方向从单个搜索工具走向 RAG PipelineKeenable 这类 API 本身解决了“怎么把搜索结果给 Agent”但生产级智能体还面临三个问题缓存相同 query 要不要复用结果记忆Agent 在长对话中已经搜索过什么要不要合并证据评估怎么判断搜索结果最终帮模型答对了可以把搜索 API 嵌入到 RAG 管道中用户问题 - 判断是否需要实时搜索 - Keenable 搜索 - 压缩结果 - 向量化入库或直接进 Prompt - 生成带引用的回答 - 校验引用和事实在扩展阶段还可以加入结果重排序。搜索 API 返回 20 条结果后不一定按原始顺序全部使用可以用一个本地排序模型或 LLM 对结果重新打分选择最相关的 5 条进入最终上下文。8.3 最后要记住的技术判断Keenable 这类面向 AI Agent 的 Web Search API真正的价值不是“能搜到网页”而是“能让模型低成本、可追溯地使用网页信息”。接入时的关键不是把 HTTP 调通而是设计好结果字段、上下文长度、引用编号、错误退避和时效控制。对于刚接触这个方向的团队建议先做一个最小工具定义web_search函数请求一次打印返回结果再把结果压缩后交给模型。跑通这条链路后再逐步加入缓存、限流、重排、评估和备用源。这样既不会在初期被复杂框架绑住也能很快暴露搜索数据里最影响 Agent 效果的问题。