用 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 秒以上。