联网能力:让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的调用

javascript 复制代码
const API_URL = "https://api.moark.com/v1/web-search-v2";
const API_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const headers = {
	"Content-Type": "application/json",
	"Authorization": `Bearer ${API_TOKEN}`
}

async function query(data) {
	const response = await fetch(
		API_URL,
		{
			headers,
			method: "POST",
			body: JSON.stringify(data)
		}
	);
	return response.json();
}

query({
	"content": "搜索查询内容",
	"model": "search",
	"search_recency_filter": "根据网页发布时间进行筛选",
	"search_site_filter": "指定站点搜索条件,仅在设置的站点中进行内容搜索,每行一个站点"
}).then((response) => {
	console.log(JSON.stringify(response));
});

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

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

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

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

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

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

javascript 复制代码
function formatSearchResults(raw) {
  let output = `搜索关键词: ${query}\n\n`;
  
  // 尝试提取结果列表
  const results = raw.snippets || raw.results || raw.organic_results || raw.web;
  
  if (Array.isArray(results)) {
    for (let i = 0; i < results.length; i++) {
      const item = results[i];
      const title = item.title || item.name || '无标题';
      const url = item.url || item.link || '';
      const snippet = item.snippet || item.content || item.description || '';
      
      output += `[${i+1}] ${title}\n`;
      if (url) output += `    ${url}\n`;
      if (snippet) output += `    ${snippet}\n\n`;
    }
  }
  
  return output;
}

格式化后的输出:

复制代码
搜索关键词: AI新闻

[1] 2025年AI十大突破
    https://example.com/ai-news
    今年AI领域发生了...

[2] OpenAI发布新模型
    https://example.com/openai
    OpenAI近日发布了...

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

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

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

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

AI需要:

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

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

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


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

2.1 场景

用户说:"帮我打开百度。"

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

2.2 实现:调用系统命令

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

系统 命令
Windows start "" "url"
macOS open "url"
Linux xdg-open "url"

CogitoAgent当前只实现了Windows版本:

javascript 复制代码
// src/agent/tools/web.js
async function browse(url) {
  return new Promise((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,可以这样写:

javascript 复制代码
async function browse(url) {
  return new Promise((resolve) => {
    let command;
    if (process.platform === 'win32') {
      command = `start "" "${url}"`;
    } else if (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,不需要安装额外库:

javascript 复制代码
const response = await fetch(url, {
  headers: {
    'User-Agent': 'Mozilla/5.0...'  // 有些网站需要模拟浏览器
  }
});
const html = await response.text();

为什么要设置User-Agent?

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

3.3 解析HTML:Cheerio

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

javascript 复制代码
import * as cheerio from 'cheerio';

const $ = cheerio.load(html);
const title = $('title').text();
const paragraphs = $('p').text();

Cheerio vs 正则解析

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

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

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

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

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

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

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

策略一:优先找语义标签

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

javascript 复制代码
// 优先取 article 或 main
let contentElement = $('article').first();
if (contentElement.length === 0) {
  contentElement = $('main').first();
}
if (contentElement.length === 0) {
  contentElement = $('body');
}

策略二:移除干扰元素

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

策略三:提取段落

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

3.5 提取链接

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

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

3.6 大小限制

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

我们需要限制输出大小:

javascript 复制代码
const MAX_CONTENT = 3000;  // 3000字符

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

3.7 完整实现

javascript 复制代码
async function fetchPage(url) {
  try {
    // 1. 下载HTML
    const response = await fetch(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}` };
    }
    
    const html = await response.text();
    
    // 2. 解析HTML
    const $ = cheerio.load(html);
    
    // 3. 提取标题
    const title = $('title').text().trim() || $('h1').first().text().trim() || '无标题';
    
    // 4. 移除干扰元素
    $('script, style, nav, header, footer, aside, .ad, .sidebar, .comment').remove();
    
    // 5. 定位正文容器
    let body = $('article').first();
    if (body.length === 0) body = $('main').first();
    if (body.length === 0) body = $('body');
    
    // 6. 提取段落
    const paragraphs = body.find('p, h1, h2, h3, h4, h5, h6, li');
    const texts = [];
    paragraphs.each((_, el) => {
      const text = $(el).text().trim();
      if (text && text.length > 10) {
        texts.push(text);
      }
    });
    
    let content = texts.join('\n');
    
    // 7. 截断过长内容
    if (content.length > 3000) {
      content = content.substring(0, 3000) + '\n\n[内容过长已截断...]';
    }
    
    // 8. 提取链接(最多10个)
    const links = [];
    $('a[href]').each((_, el) => {
      const href = $(el).attr('href');
      const text = $(el).text().trim();
      if (links.length >= 10) return false;
      if (href && text && href.startsWith('http')) {
        links.push({ text, url: href });
      }
    });
    
    // 9. 组装输出
    let output = `标题: ${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 配置结构

json 复制代码
{
  "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 在工具中检查开关

javascript 复制代码
async function search(query) {
  const cfg = 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 👈

相关推荐
机器人零零壹1 小时前
南京越擎科技iRobotCAM:探索国产机器人离线编程工业软件的破局与赶超
人工智能·机器人·工业软件·离线编程·irobotcam
Cosolar1 小时前
保姆级 CrewAI 教程:从零构建多智能体协作系统
人工智能·python·架构
树上有只程序猿2 小时前
主流低代码管理平台深度解析(最新)
人工智能·低代码·软件开发·软件需求
宅小年2 小时前
你不会输给 AI,只会输给更会用 AI 的人
人工智能
wb043072012 小时前
厨房实况直播——从阿明的“外卖骑手追踪系统“,看实时系统与事件驱动架构
架构
武子康2 小时前
调查研究-165 vLLM 深入浅出:从 PagedAttention 到生产级大模型推理服务
人工智能·openai
冬奇Lab2 小时前
每日一个开源项目(第126篇):turbovec - 向量索引的内存杀手,1千万文档从31GB压到4GB
人工智能·开源·llm
继续商行2 小时前
模型量化实践:GPTQ 与 AWQ 在生产环境的精度与速度权衡
人工智能
知识浅谈2 小时前
人工智能日报 每日AI新闻(2026年6月8日):OpenAI安全加码、苹果AI升级前夜与国产AI应用落地
人工智能·安全·chatgpt