2024年的大部分时间里,我都在为毕业设计和寻找实习做准备。作为广州商学院软件工程专业的一名大四学生,我的日常基本就是看文档、写代码、调试bug。在这个过程中,ChatGPT成了我最常用的工具。它能帮我解释晦涩的源码,也能在我卡壳时提供一些重构的思路。
但用得越深,它的一个痛点就暴露得越明显:不能联网。
官方的ChatGPT Plus账号价格不菲,且支付门槛较高,因此我和周围的很多同学一样,更倾向于使用基于API搭建的第三方镜像站。这种方式按量付费,对学生党来说经济压力小很多。然而,镜像站有一个致命的缺陷——它们使用的是纯API,而OpenAI的API默认是不包含官方Browsing(联网搜索)插件功能的。
这就导致了一个很尴尬的局面。每当我询问它某个开源框架在2024年新发布的特性,或者查询某个刚刚报错的非典型异常时,它总是受限于训练数据的截止时间,要么诚实地告诉我它不知道,要么一本正经地胡编乱造出一些根本不存在的类名和方法。
为了解决这个问题,我开始寻找第三方的搜索API。市面上确实有一些成熟的商业方案,比如SerpApi或者Bing Search API。但它们要么免费额度极少,几百次调用后就要收费,要么需要绑定国际信用卡才能开通。对于一个只是想在个人项目中实现基础联网功能的学生来说,这些方案都显得过于沉重了。
既然找不到合适的,那就自己写一个。于是我花了一些时间,写了search_api这个项目,定位是一个免费的、无限制的搜索接口。
这个项目的核心思路其实非常简单。说白了,就是用程序模拟人的搜索行为:接收用户的查询词,代理发送搜索请求到公共搜索引擎,然后将返回的HTML页面剥离掉无关的广告和杂乱的标签,提取出有用的标题、链接和摘要,最后打包成结构化的JSON数据返回给调用方。
在最初的构想里,它的代码骨架大概只有十几行:
@app.route("/search")
def search():
query = request.args.get("q")
results = fetch_search_results(query)
return jsonify({
"results": [
{"title": r.title, "url": r.url, "snippet": r.snippet}
for r in results
]
})
这段基于Flask的代码看起来很清爽,但在实际落地的过程中,那个隐藏在背后的fetch_search_results函数却让我踩了不少坑。
第一个问题是目标搜索引擎的选择。直接去爬取Google的搜索结果是不现实的,它的反爬机制极其严苛,普通的请求没发几次就会遇到验证码拦截,甚至直接封禁IP。经过几轮测试,我最终选择了一些对自动化请求相对宽容,且搜索质量尚可的匿名搜索引擎作为底层数据源。
第二个问题是网络请求的稳定性。因为是代理搜索,请求往往需要经过多次路由,超时是家常便饭。一开始,我只是简单地用requests库发个GET请求,结果发现接口的失败率很高。后来我在代码里加入了重试机制,并设置了合理的超时时间。同时,为了防止被目标网站识别为恶意脚本,我还在请求头里构建了一个User-Agent池,每次请求时随机伪装成不同的浏览器和操作系统。这些看似不起眼的防御性编程,占据了开发过程的一半时间。
第三个问题是DOM树的解析。搜索引擎的页面结构并不是一成不变的。有时候他们为了做A/B测试,或者单纯为了防止被爬,会修改CSS类名或嵌套层级。我使用BeautifulSoup去定位搜索结果的列表项时,经常会遇到解析为空的情况。为了提高容错率,我不得不写了多套解析规则,如果第一种规则找不到数据,就降级使用第二种、第三种,直到提取出文本为止。
当这个API终于能够稳定返回干净的JSON数据时,我开始把它集成到各种场景中。
最直接的应用场景就是ChatGPT镜像站的联网搜索。现在的开源镜像站程序大多支持自定义插件。把search_api的接口地址填进去,当用户在聊天框里输入需要实时信息的问题时,前端会先调用这个API拿到最新的网页摘要,然后把这些摘要作为上下文,连同用户的问题一起发给OpenAI的接口。这样,原本停留在旧时光里的AI,瞬间就有了感知当下世界的能力。
另一个更深度的应用是GPT的Function Calling(函数调用)。在开发一些垂直领域的AI智能体时,我会把search_api封装成一个工具函数。我只需要在代码里向大模型描述清楚这个接口的输入参数和返回格式,大模型就能在对话过程中自主判断什么时候需要搜索。比如我问它“对比一下今天广州和北京的天气”,它会自动生成两次搜索请求分别查询两地的天气,拿到API返回的数据后再进行综合对比,最后输出一段连贯的回答。这种让AI自己决定使用工具的过程,看着它在终端里打印出一步步的思考和调用日志,是一种很奇妙的体验。
除此之外,任何需要实时搜索数据的AI应用,比如自动生成行业日报的脚本、或者自动回复邮件的机器人,都可以低成本地接入这个接口。
我把代码整理好,写了一份还算详尽的README,开源在了GitHub上。
到现在为止,这个项目获得了16个Stars。在这个动辄成千上万Star的开源世界里,16这个数字微小到几乎可以忽略不计。但对我来说,它的分量很重。
在广州商学院读软件工程的这四年里,我一直要求自己保持专注。我拿了两次国家奖学金,目前的GPA维持在4.02。我知道作为一名民办三本的学生,在学历背景上并没有优势,所以我只能在专业技能上尽量做到扎实。那些成绩单上的数字,或许能帮我在简历初筛时获得多看一眼的机会,但真正让我对写代码这件事感到踏实和确定的,是这种解决实际问题的过程。
后来,我通过后台的访问日志发现,这个API被好几个不知名的ChatGPT镜像站集成到了生产环境中。每天都有源源不断的请求发过来,转化成一条条有用的信息,呈现在不同用户的屏幕上。
看着服务器终端里不断跳动的200状态码,我意识到自己写的这几百行代码,正在真实地解决着一些人的困扰。这种能通过技术帮到别人的感觉,确实很好。它让我觉得,过去那些对着屏幕调试正则表达式、排查网络超时的枯燥夜晚,都有了具体的意义。