用 CloudBase 云函数代理 Tavily AI 搜索
一句话定义:用
@tavily/core在 CloudBase 云函数里调 Tavily Search API,拿到带answer/results[]/score的 LLM-friendly 搜索结果,前端通过 HTTP 调用,Tavily key 留在云端不暴露。预计耗时:25 分钟 | 难度:进阶
适用场景
- 适用:给 LLM agent 加实时联网检索能力,结果直接喂给模型做 search-augmented generation
- 适用:不想让前端 / 小程序直接持有 Tavily API key,需要一层后端代理
- 适用:跟 add-rag-with-pgvector-cloudbase 互补——RAG 答私域,Tavily 答外部公开网页
- 不适用:纯私域知识检索(用 RAG + 向量库更合适,Tavily 不会爬你的内网)
- 不适用:用户问 LLM 自身参数化记忆里就有的内容(比如经典算法解释),多此一举
环境要求
| 依赖 | 版本 |
|---|---|
@tavily/core | latest(npm view @tavily/core version 查最新版) |
| Node.js(云函数运行时) | ≥ 18 |
@cloudbase/cli | latest |
| 云函数类型 | Web 云函数(HTTP 触发) —— 前端要直接 fetch |
| 公网出口 | 云函数默认能访问公网;配过 VPC 的环境需要确认 NAT 已挂 |
需要准备:
- 一个 Tavily API key(tavily.com 注册后免费额度可以跑通,超过免费额度按调用计费,单价以官方 pricing 页面 为准)
- 一个用于校验前端调用方的 token(最简单就是 32 字节的随机串,下文叫
PROXY_ACCESS_TOKEN)
关于包名:Tavily 的官方 JS SDK npm 包名叫
@tavily/core,而不是 GitHub 仓库名tavily-js,装错包是首踩坑。
第一步:申请 Tavily key + 在 CloudBase 配环境变量
- 去 tavily.com 注册,控制台「API Keys」生成一个 key,形如
tvly-xxxxxxxxxxxxxxxxxxxx - 本地生成一个 proxy access token:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
- 暂存这两个值,下面部署完函数后到控制台「云函数 → 环境变量」里填入:
TAVILY_API_KEY= 刚拿到的 Tavily keyPROXY_ACCESS_TOKEN= 刚生成的随机串
第二步:写云函数(完整 search + 参数校验 + 结果裁剪)
mkdir tavily-search && cd tavily-search
npm init -y
npm install --save express @tavily/core
package.json 把 main 改成 index.js,加 start 脚本:
{
"name": "tavily-search",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.18.2",
"@tavily/core": "^0.5.0"
}
}
index.js:
const express = require('express');
const { tavily } = require('@tavily/core');
const app = express();
app.use(express.json({ limit: '1mb' }));
const TAVILY_API_KEY = process.env.TAVILY_API_KEY;
const PROXY_ACCESS_TOKEN = process.env.PROXY_ACCESS_TOKEN;
if (!TAVILY_API_KEY || !PROXY_ACCESS_TOKEN) {
console.error('Missing TAVILY_API_KEY or PROXY_ACCESS_TOKEN env');
process.exit(1);
}
const client = tavily({ apiKey: TAVILY_API_KEY });
// 鉴权:校验 Authorization: Bearer <PROXY_ACCESS_TOKEN>
function requireAuth(req, res, next) {
const auth = req.headers.authorization || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
if (token !== PROXY_ACCESS_TOKEN) {
return res.status(401).json({ error: 'unauthorized' });
}
next();
}
// 允许从前端透传的搜索参数白名单
// 不在这里就丢掉,防止前端乱传放大成本
const ALLOWED_OPTIONS = new Set([
'searchDepth', // "basic" | "advanced"
'topic', // "general" | "news" | "finance"
'timeRange', // "day"|"week"|"month"|"year"|"d"|"w"|"m"|"y"
'startDate', // YYYY-MM-DD
'endDate',
'maxResults', // 0-20
'chunksPerSource', // advanced 才生效
'includeImages',
'includeAnswer', // false | "basic" | "advanced"
'includeRawContent', // false | "markdown" | "text"
'includeDomains', // max 300
'excludeDomains', // max 150
'country',
'timeout', // 秒
'exactMatch',
'includeFavicon',
]);
function pickOptions(input = {}) {
const out = {};
for (const [k, v] of Object.entries(input)) {
if (ALLOWED_OPTIONS.has(k)) out[k] = v;
}
// 强制兜底,防止 maxResults 被前端开到很大
if (typeof out.maxResults !== 'number' || out.maxResults < 1) out.maxResults = 5;
if (out.maxResults > 10) out.maxResults = 10;
return out;
}
app.post('/search', requireAuth, async (req, res) => {
const { query, options } = req.body || {};
if (typeof query !== 'string' || !query.trim()) {
return res.status(400).json({ error: 'invalid_query', message: 'body.query must be a non-empty string' });
}
const safeOptions = pickOptions(options);
let response;
try {
response = await client.search(query, safeOptions);
} catch (err) {
console.error('tavily search failed', err);
// Tavily SDK 的错误一般是 HTTP error,把状态码透出去方便前端判断
const status = err.status || err.response?.status || 502;
return res.status(status).json({
error: 'tavily_error',
message: err.message || 'tavily request failed',
});
}
// 裁剪一下返回体:LLM 一般只关心 query / answer / results 几个字段
// 原样把 rawContent 直接吐回去会很大,默认丢掉,前端要的话自己再开 includeRawContent
const slim = {
query: response.query,
answer: response.answer,
results: (response.results || []).map((r) => ({
title: r.title,
url: r.url,
content: r.content,
score: r.score,
publishedDate: r.publishedDate,
})),
responseTime: response.responseTime,
requestId: response.requestId,
};
res.json(slim);
});
app.get('/health', (_req, res) => res.json({ ok: true }));
const PORT = process.env.PORT || 9000;
app.listen(PORT, () => {
console.log(`tavily-search listening on ${PORT}`);
});
几个有意思的点:
searchDepth: "basic"单次返回够 LLM 用,速度快、单价低;searchDepth: "advanced"会做多次抓取并按 chunk 切片,质量更高但耗时和单价都涨——按场景在前端二选一,函数侧不要写死includeAnswer: "basic"让 Tavily 直接返回一句总结,省掉前端再调一次 LLM 做 summary 的成本;要更长的总结用"advanced"- 过滤白名单很重要:Tavily 部分参数(
includeImages/includeRawContent)会显著放大返回体和单价,不该让前端随便开 - 裁剪
rawContent字段是性价比最高的优化——很多 results 的 raw HTML 加起来能到上百 KB,浏览器解析会卡
第三步:部署到 CloudBase
tcb login
tcb fn deploy tavily-search --httpFn -e your-env-id
部署完到控制台「云函数 → tavily-search」做两件事:
- 「环境变量」里加
TAVILY_API_KEY和PROXY_ACCESS_TOKEN(值就是第一步生成的两个) - 「触发方式」确认 HTTP 网关已启用,记下访问 URL,类似
https://your-env.service.tcloudbase.com/tavily-search
改了环境变量要等下一次冷启动才生效;想立即生效可以在控制台对函数做一次「重新部署」,或者把内存 / 超时随便改一下保存触发实例重启。
第四步:前端 / agent 端调用 + 把结果喂给 LLM
最简单的 fetch 调用:
const resp = await fetch('https://your-env.service.tcloudbase.com/tavily-search/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer YOUR_PROXY_ACCESS_TOKEN',
},
body: JSON.stringify({
query: '2026 年 4 月美联储议息会议结果',
options: {
searchDepth: 'basic',
topic: 'news',
timeRange: 'week',
maxResults: 5,
includeAnswer: 'basic',
},
}),
});
const data = await resp.json();
console.log(data.answer);
console.log(data.results.map((r) => `${r.title} — ${r.url}`));
接到 LLM 上下文里时,建议把 results 拼成有出处编号的 prompt 片段,让模型在回答里引用:
const sources = data.results
.map((r, i) => `[${i + 1}] ${r.title}\n${r.content}\n来源: ${r.url}`)
.join('\n\n');
const userPrompt = `
基于以下检索结果回答用户问题,引用时用 [1] [2] 这种编号:
${sources}
用户问题: 2026 年 4 月美联储利率决议结果
`;
完整的 search-augmented chatbot 串起来需要再接一个 LLM 代理,可参考 connect-openai-api-cloud-function。
运行验证
curl -X POST 'https://your-env.service.tcloudbase.com/tavily-search/search' \
-H 'Authorization: Bearer YOUR_PROXY_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"query": "CloudBase 是什么",
"options": { "searchDepth": "basic", "maxResults": 3, "includeAnswer": "basic" }
}'
预期返回结构:
{
"query": "CloudBase 是什么",
"answer": "CloudBase 是腾讯云推出的云开发平台...",
"results": [
{ "title": "...", "url": "https://...", "content": "...", "score": 0.91, "publishedDate": null }
],
"responseTime": 1.09,
"requestId": "uuid"
}
responseTime 一般在 0.5-3 秒之间;searchDepth: "advanced" 会到 5-15 秒,函数超时记得调到 30 秒以上。
Tavily 不只是 search
@tavily/core 同一个 client 对象上还有几个方法,用法跟 search 类似:
client.extract(["url1", "url2"], { ... })— 抓指定 URL 的正文(可选extractDepth: "advanced"做更彻底的解析)client.crawl("base-url", { maxDepth, maxBreadth })— 整站爬取,返回多页内容client.map(...)— 只生成站点 URL 结构图,不取正文
需要这些能力直接在云函数里加新的 route 复用同一个 client 实例就行,不需要再 tavily() 一次。
常见错误
| 现象 | 原因 | 修复 |
|---|---|---|
Cannot find module 'tavily-js' | 装错了包名 | 卸掉重装 npm i @tavily/core;tavily-js 是 GitHub 仓库名不是 npm 包名 |
401 Unauthorized from Tavily | TAVILY_API_KEY 没设 / 写错 / key 已 revoke | 控制台「环境变量」核对;去 Tavily Dashboard 看 key 是否还 active |
429 Too Many Requests | 超过当前套餐的并发或月度调用上限 | 降并发 / 升 plan / 在云函数侧加节流;具体限额看 Tavily 控制台 |
Invalid value for parameter searchDepth | 传了 "deep"、"full" 等不存在的值 | searchDepth 只接受 "basic" 或 "advanced";老版本 Python SDK 用的是 search_depth,JS SDK 是驼峰 searchDepth |
topic: "news" 返回结果不带 publishedDate | 个别站点没有发布时间字段 | 不是 bug,前端要按 r.publishedDate 是否存在做兜底;想强制要时间就在 prompt 里只用有 publishedDate 的 results |
getaddrinfo ENOTFOUND api.tavily.com | 云函数没公网出口 | 控制台 →「网络配置」开启公网访问;配过 VPC 的需要 NAT 网关 |
502 tavily_error 偶发,重试就好 | 上游网络抖动或 Tavily 后端短暂 5xx | 在调用方做最多 2 次指数退避重试;不要无限重试,会把单价撑爆 |
错误码完整列表:https://docs.cloudbase.net/error-code/
相关文档
- connect-openai-api-cloud-function — 用云函数代理 LLM API,跟本篇组合就是完整的 search-augmented chatbot
- add-rag-with-pgvector-cloudbase — 私域知识 RAG,跟 Tavily 互补
- secure-secrets-in-cloud-function —
TAVILY_API_KEY等敏感值的本地 / CI / 生产分层管理 - HTTP 云函数 — Web 云函数(HTTP 触发)的快速上手
- 函数环 境变量 — 配置注入和读取
下一步
search 代理跑起来后下一步是接进 LLM 形成完整 search-augmented 链路:前端先调 tavily-search,把 results[] 拼进 messages 调 connect-openai-api-cloud-function 里搭的 LLM 代理函数,模型基于检索结果作答;要做成 Perplexity 那种"边搜边答"的 SSE 流式响应,套一层 add-vercel-ai-sdk-streaming-chatbot 即可。