news 2026/6/9 10:30:32

联网能力:让AI看见更广阔的世界 ——CogitoAgent开发实战(四)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
联网能力:让AI看见更广阔的世界 ——CogitoAgent开发实战(四)

联网能力:让AI看见更广阔的世界

——CogitoAgent开发实战(四)

📖 本文是专栏的第四篇。前三篇我们让AI学会了思考、学会了操作文件,但它仍然被困在你的电脑里——它不知道今天有什么新闻,不知道某个网站写了什么,不知道网上有什么资源。这一篇,我们给AI装上“眼睛”,让它能看见互联网上的信息。


📌 从一个场景开始

想象你的助理很能干,但你问他“今天有什么大新闻?”他回答:“我不知道,我没联网。”

你会不会觉得这个助理缺了点啥?

本地AI也有同样的问题。它能读你电脑里的文件,能帮你整理文件夹,但它对“外部世界”一无所知。

AI需要三种联网能力:

能力场景类比
搜索“帮我查一下最新的AI论文”像你用百度/Google
浏览“打开这个链接给我看看”像你点击链接
抓取“这个网页里写了什么?”像你阅读文章

这一篇,我们逐个实现这三种能力。


一、搜索:让AI能“查资料”

1.1 问题:AI怎么搜索?

AI本身不能上网。它需要程序帮它去搜索,然后把搜索结果告诉它。

流程是这样的:

AI说:“帮我搜索AI新闻” ↓ 程序调用搜索API ↓ API返回搜索结果 ↓ 程序把结果整理成文字 ↓ AI“看到”结果,继续回答

关键问题:搜索API从哪里来?

1.2 搜索API的选择

CogitoAgent使用了Moark的搜索API(/web-search-v2)。但这不是唯一的方案,你可以换成任何搜索服务:

方案优点缺点
Moark API开箱即用,与LLM同一供应商需要付费
Bing Search API微软官方,结果质量高需要申请密钥
Google Custom Search覆盖面广配置复杂
SerpAPI聚合多种搜索引擎价格较高
本地爬虫免费需要自己维护,容易被封

设计要点:CogitoAgent把搜索API封装成可配置的,你可以替换成任何提供HTTP接口的搜索服务。

1.3 搜索API的调用

constAPI_URL="https://api.moark.com/v1/web-search-v2";constAPI_TOKEN="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";constheaders={"Content-Type":"application/json","Authorization":`Bearer${API_TOKEN}`}asyncfunctionquery(data){constresponse=awaitfetch(API_URL,{headers,method:"POST",body:JSON.stringify(data)});returnresponse.json();}query({"content":"搜索查询内容","model":"search","search_recency_filter":"根据网页发布时间进行筛选","search_site_filter":"指定站点搜索条件,仅在设置的站点中进行内容搜索,每行一个站点"}).then((response)=>{console.log(JSON.stringify(response));});

1.4 关键设计:搜索结果的格式化

API返回的搜索结果通常是复杂的JSON结构:

{"snippets":[{"title":"2025年AI十大突破","url":"https://example.com/ai-news","content":"今年AI领域发生了..."},// 更多结果...]}

把这个JSON直接丢给AI,AI能理解,但有三个问题:

  1. 太冗长:JSON有很多括号、引号,占用大量token
  2. 不够直观:AI需要解析JSON结构才能提取信息
  3. 难以阅读:人类调试时眼睛疼

所以我们需要一个格式化函数,把JSON转成易读的文本:

functionformatSearchResults(raw){letoutput=`搜索关键词:${query}\n\n`;// 尝试提取结果列表constresults=raw.snippets||raw.results||raw.organic_results||raw.web;if(Array.isArray(results)){for(leti=0;i<results.length;i++){constitem=results[i];consttitle=item.title||item.name||'无标题';consturl=item.url||item.link||'';constsnippet=item.snippet||item.content||item.description||'';output+=`[${i+1}]${title}\n`;if(url)output+=`${url}\n`;if(snippet)output+=`${snippet}\n\n`;}}returnoutput;}

格式化后的输出:

搜索关键词: AI新闻 [1] 2025年AI十大突破 https://example.com/ai-news 今年AI领域发生了... [2] OpenAI发布新模型 https://example.com/openai OpenAI近日发布了...

这样AI一眼就能看懂每条结果的核心信息。

1.5 思考题:为什么要格式化?

想象一下,如果直接返回JSON:

{"snippets":[{"title":"2025年AI十大突破","url":"https://...","content":"..."}]}

AI需要:

  1. 解析JSON结构
  2. 找到snippets数组
  3. 遍历每个元素,提取titleurlcontent

虽然AI能做到,但这个过程消耗“思考时间”和token。格式化成纯文本后,AI可以直接使用,不需要额外解析。

设计原则:给AI的数据,越接近人类可读的自然语言越好。


二、浏览:在浏览器中打开网页

2.1 场景

用户说:“帮我打开百度。”

AI应该能启动你的默认浏览器,打开指定的网址。

2.2 实现:调用系统命令

不同操作系统打开浏览器的命令不同:

系统命令
Windowsstart "" "url"
macOSopen "url"
Linuxxdg-open "url"

CogitoAgent当前只实现了Windows版本:

// src/agent/tools/web.jsasyncfunctionbrowse(url){returnnewPromise((resolve)=>{// Windows:使用 start 命令exec(`start "" "${url}"`,(error)=>{if(error){resolve({success:false,error:`打开链接失败:${error.message}`});}else{resolve({success:true,data:`已在浏览器中打开:${url}`});}});});}

注意start ""的写法:start命令的第一个参数是窗口标题,我们传空字符串,第二个参数才是URL。

2.3 为什么用exec而不是spawn

exec把整个命令放在一个字符串里,适合简单命令。spawn更适合参数复杂的场景。对于start “” “url”这种命令,exec足够。

2.4 跨平台实现

如果要支持Mac和Linux,可以这样写:

asyncfunctionbrowse(url){returnnewPromise((resolve)=>{letcommand;if(process.platform==='win32'){command=`start "" "${url}"`;}elseif(process.platform==='darwin'){command=`open "${url}"`;}else{command=`xdg-open "${url}"`;}exec(command,(error)=>{if(error){resolve({success:false,error:`打开链接失败:${error.message}`});}else{resolve({success:true,data:`已在浏览器中打开:${url}`});}});});}

process.platform返回当前操作系统:

  • win32:Windows
  • darwin:macOS
  • linux:Linux

2.5 设计决策:打开还是抓取?

browsefetchPage的区别:

工具行为适用场景
browse打开浏览器,用户自己看用户想看网页内容
fetchPage程序抓取内容,AI读给用户AI需要分析网页内容

用户说“帮我打开这个链接” → 用browse
用户说“这个网页里写了什么” → 用fetchPage


三、抓取:让AI能“阅读”网页

3.1 问题:AI怎么读取网页内容?

browse只是打开浏览器,AI自己看不到网页内容。要让AI“读”网页,程序需要:

  1. 下载网页的HTML
  2. 解析HTML,提取正文
  3. 把正文交给AI

3.2 下载网页:fetch API

Node.js 18+ 原生支持fetch,不需要安装额外库:

constresponse=awaitfetch(url,{headers:{'User-Agent':'Mozilla/5.0...'// 有些网站需要模拟浏览器}});consthtml=awaitresponse.text();

为什么要设置User-Agent?

有些网站会拒绝非浏览器的请求(返回403)。设置User-Agent模拟浏览器,可以绕过这种限制。

3.3 解析HTML:Cheerio

Cheerio是一个服务端版的jQuery。语法几乎一样,但不需要浏览器环境。

import*ascheeriofrom'cheerio';const$=cheerio.load(html);consttitle=$('title').text();constparagraphs=$('p').text();

Cheerio vs 正则解析

方案优点缺点
正则快,无依赖难以处理复杂HTML,容易出错
Cheerio强大,类似jQuery需要安装,稍慢(但可接受)

对于抓取网页,Cheerio是标准选择。

3.4 提取正文:不只是取所有文本

一个网页包含很多无关内容:导航栏、广告、侧边栏、评论区、版权信息……

如果我们简单地把所有文本都提取出来,AI会读到大量垃圾信息:

导航:首页 产品 关于我们 广告:点击领取优惠券 正文:这篇文章主要讲... 评论区:张三说好,李四说不好 版权:©2025 某某公司

我们需要正文提取——只提取文章的核心内容。

策略一:优先找语义标签

现代网页常用语义化的HTML5标签:

// 优先取 article 或 mainletcontentElement=$('article').first();if(contentElement.length===0){contentElement=$('main').first();}if(contentElement.length===0){contentElement=$('body');}

策略二:移除干扰元素

// 删除这些标签及其内容$('script, style, nav, header, footer, aside, .ad, .sidebar, .comment').remove();

策略三:提取段落

constparagraphs=contentElement.find('p, h1, h2, h3, h4, h5, h6, li');consttexts=[];paragraphs.each((_,el)=>{consttext=$(el).text().trim();if(text&&text.length>10){// 过滤太短的文本texts.push(text);}});constcontent=texts.join('\n');

3.5 提取链接

有时候,用户需要知道网页里有哪些链接:

constlinks=[];$('a[href]').each((_,el)=>{consthref=$(el).attr('href');consttext=$(el).text().trim();if(href&&text&&href.startsWith('http')){// 只取外部链接links.push({text,url:href});}});

3.6 大小限制

网页可能非常大。一篇长文章可能有几万字,加上HTML标签,原始HTML可能达到几MB。

我们需要限制输出大小:

constMAX_CONTENT=3000;// 3000字符letcontent=extractedText;if(content.length>MAX_CONTENT){content=content.substring(0,MAX_CONTENT)+'\n\n[内容过长已截断...]';}

3.7 完整实现

asyncfunctionfetchPage(url){try{// 1. 下载HTMLconstresponse=awaitfetch(url,{headers:{'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}});if(!response.ok){return{success:false,error:`HTTP${response.status}:${response.statusText}`};}consthtml=awaitresponse.text();// 2. 解析HTMLconst$=cheerio.load(html);// 3. 提取标题consttitle=$('title').text().trim()||$('h1').first().text().trim()||'无标题';// 4. 移除干扰元素$('script, style, nav, header, footer, aside, .ad, .sidebar, .comment').remove();// 5. 定位正文容器letbody=$('article').first();if(body.length===0)body=$('main').first();if(body.length===0)body=$('body');// 6. 提取段落constparagraphs=body.find('p, h1, h2, h3, h4, h5, h6, li');consttexts=[];paragraphs.each((_,el)=>{consttext=$(el).text().trim();if(text&&text.length>10){texts.push(text);}});letcontent=texts.join('\n');// 7. 截断过长内容if(content.length>3000){content=content.substring(0,3000)+'\n\n[内容过长已截断...]';}// 8. 提取链接(最多10个)constlinks=[];$('a[href]').each((_,el)=>{consthref=$(el).attr('href');consttext=$(el).text().trim();if(links.length>=10)returnfalse;if(href&&text&&href.startsWith('http')){links.push({text,url:href});}});// 9. 组装输出letoutput=`标题:${title}\n\n正文:\n${content}`;if(links.length>0){output+='\n\n相关链接:\n';output+=links.map(l=>`-${l.text}:${l.url}`).join('\n');}return{success:true,data:output};}catch(error){return{success:false,error:`抓取失败:${error.message}`};}}

3.8 局限性:动态网页

当前实现只能抓取静态网页——即服务器直接返回完整HTML的页面。

很多现代网站是动态渲染的:服务器返回一个空壳,JavaScript再去加载内容。对于这类网站,fetchPage只能抓到空壳。

解决方案

  • 使用Puppeteer(无头浏览器)来渲染JavaScript
  • 但Puppeteer很重(下载Chromium),会增加项目体积

CogitoAgent选择了简单方案:只支持静态网页。对于动态网站,可以用browse打开浏览器让用户自己看。


四、配置与开关

4.1 为什么需要开关?

不是所有用户都想用搜索功能。有些人可能:

  • 没有搜索API密钥
  • 担心隐私(不想把搜索词发给第三方)
  • 只需要本地功能

所以搜索应该可以关闭

4.2 配置结构

{"search":{"enabled":true,"baseURL":"https://api.moark.com/v1/web-search-v2","recencyFilter":"","siteFilter":""}}
字段含义
enabled是否启用搜索(false时调用search会返回错误)
baseURL搜索API地址(空则自动拼接)
recencyFilter时间过滤,如“day”“week”“month”
siteFilter站点过滤,如“github.com”

4.3 在工具中检查开关

asyncfunctionsearch(query){constcfg=loadConfig();if(!cfg.search||cfg.search.enabled===false){return{success:false,error:'搜索功能未启用,请在配置中开启'};}// 继续执行搜索...}

这样,如果用户没配置搜索API或者主动关闭了搜索,AI会收到明确的错误提示。


五、三个工具的协同

三个联网工具各自解决不同问题,但可以协同工作:

用户:帮我查一下最新的AI新闻,然后打开最有趣的那篇 AI: 1. [TOOL] search("2025年AI新闻") [/TOOL] → 得到结果列表 2. 分析结果,选出最有趣的一篇 3. [TOOL] fetchPage("https://example.com/ai-news") [/TOOL] → 读取文章内容,摘录给用户 4. [TOOL] browse("https://example.com/ai-news") [/TOOL] → 在浏览器中打开,让用户自己看

这就是工具组合的威力——AI可以串联多个工具完成复杂任务。


六、设计决策回顾

决策原因
搜索API可配置不同用户用不同服务商
搜索结果要格式化节省token,AI理解更快
browse用系统命令简单可靠,不依赖额外库
fetchPage用Cheerio轻量级,语法熟悉
只抓取静态网页避免引入Puppeteer的巨大体积
搜索有开关尊重用户隐私和选择

七、小结

工具做什么核心技术
search联网搜索fetch + 搜索API + 格式化
browse打开浏览器exec + 系统命令
fetchPage抓取网页fetch + Cheerio + 正文提取

核心设计原则

  1. 搜索结果要格式化,不要让AI解析JSON
  2. 网页抓取要提取正文,不要一股脑全给
  3. 给用户关闭搜索的选项
  4. 工具可以串联使用

下一篇预告:终端UI

我们将深入terminal.js,看看:

  • 如何用ANSI颜色代码实现彩色输出
  • 思考区、内容区、工具区的分区展示
  • 流式输出的实时渲染
  • readline如何实现“按Enter打断”

如果这篇文章对你有帮助,欢迎 ⭐Star 支持一下开源项目!

👉 https://gitee.com/cnt-code/cogito-agent 👈

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/9 10:29:08

2026企业网盘横评坚果云vs亿方云vs巴别鸟

在工程设计、建筑施工、软件开发这类行业里&#xff0c;CAD图纸、3D模型、源代码这类文件动不动就几百兆&#xff0c;多人异地协作时&#xff0c;同步冲突、版本混乱、权限失控是三个最常见的坑。选错企业网盘&#xff0c;团队协作效率直接打骨折&#xff1b;选对了&#xff0c…

作者头像 李华
网站建设 2026/6/9 10:17:27

Sqribble深度解析:规则驱动的云原生文档自动化系统

1. 项目概述&#xff1a;这不是“一键生成”&#xff0c;而是一套被精心封装的出版流水线你有没有过这种经历&#xff1a;手头有一篇写得不错的博客&#xff0c;想把它变成一本像模像样的电子书发给客户当赠品&#xff1b;或者团队刚做完一个行业调研&#xff0c;需要快速出一份…

作者头像 李华
网站建设 2026/6/9 10:07:28

解密BabelDOC:如何实现学术PDF文档的精准格式保留翻译

解密BabelDOC&#xff1a;如何实现学术PDF文档的精准格式保留翻译 【免费下载链接】BabelDOC Yet Another Document Translator 项目地址: https://gitcode.com/GitHub_Trending/ba/BabelDOC 当科研人员面对一篇包含复杂数学公式、化学结构式和专业术语的英文学术论文时…

作者头像 李华