Summary #
For adding web search to your LLM agent, use Tavily (@tavily/core)[1]. Unlike traditional SERP APIs (Serper, SerpAPI) that just return search result metadata, Tavily searches AND extracts content from pages in a single call, returning text optimized for LLM context windows[2].
Key metrics: Tavily is used by LangChain as their default search integration[3]. The free tier provides 1,000 API credits/month—enough for ~500-1000 searches depending on settings[4]. For comparison, Serper offers 2,500 free Google searches but returns only titles/snippets, requiring you to build separate content extraction[5].
Why Tavily wins for your use case: You need code/API lookup with minimal configuration. Tavily's include_answer parameter can even return an LLM-generated summary of results[1]. One API call does what would require 3-4 calls with Serper (search → get URLs → fetch pages → extract content).
Philosophy & Mental Model #
Tavily is designed as "the web access layer for AI agents"[2]. The mental model:
-
Search + Extract = One Call: Traditional flow requires: SERP API → URLs → HTTP fetch → HTML parse → clean text. Tavily does all of this internally and returns clean, relevant content snippets.
-
Relevance Scoring: Results include a
scorefield (0-1) indicating relevance. Use this to decide how many results to include in your LLM context. -
Depth Control:
search_depth: "basic"(1 credit) returns snippets.search_depth: "advanced"(2 credits) retrieves more sources and enables chunking[1]. -
Content Budget: The
max_resultsandchunks_per_sourceparameters let you control how much content you're injecting into your prompt. More isn't always better—aim for focused, relevant context.
Key abstractions:
search()- Main search function returning results with contentextract()- Fetch and parse content from specific URLs (1 credit per 5 URLs)crawl()- Site-wide crawling with natural language goals (beta)
Setup #
Step 1: Get API Key #
- Go to tavily.com
- Sign up (no credit card required)
- Copy your API key (starts with
tvly-)
Step 2: Install the SDK #
1npm install @tavily/core
Step 3: Configure Environment #
Add to mise.toml:
1[env]
2TAVILY_API_KEY = "{{env.TAVILY_API_KEY}}"
Or set directly:
1export TAVILY_API_KEY="tvly-your-api-key"
Step 4: Create Client #
1import { tavily } from "@tavily/core";
2
3const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY });
Core Usage Patterns #
Pattern 1: Basic Search for LLM Context #
The simplest pattern—search and get content ready for your LLM:
1import { tavily } from "@tavily/core";
2
3const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY });
4
5async function searchForContext(query: string): Promise<string> {
6 const response = await tvly.search(query, {
7 searchDepth: "basic",
8 maxResults: 5,
9 });
10
11 // Format results for LLM context
12 return response.results
13 .map((r) => `### ${r.title}\nSource: ${r.url}\n\n${r.content}`)
14 .join("\n\n---\n\n");
15}
16
17// Usage
18const context = await searchForContext("TypeScript zod validation examples");
Pattern 2: Search with LLM-Generated Answer #
Get Tavily to summarize results for you (uses their LLM internally):
1async function searchWithAnswer(query: string) {
2 const response = await tvly.search(query, {
3 includeAnswer: "advanced", // "basic" for shorter, "advanced" for detailed
4 maxResults: 5,
5 });
6
7 return {
8 answer: response.answer, // Direct answer to include in your response
9 sources: response.results.map((r) => ({ title: r.title, url: r.url })),
10 };
11}
12
13// Usage
14const { answer, sources } = await searchWithAnswer(
15 "How do I set up ESLint with TypeScript in 2025?"
16);
17
18console.log("Answer:", answer);
19console.log("Sources:", sources);
Pattern 3: Code/Documentation Search with Raw Content #
When you need the full page content (e.g., documentation pages):
1async function searchDocs(query: string) {
2 const response = await tvly.search(query, {
3 searchDepth: "advanced", // Required for better content extraction
4 includeRawContent: "markdown", // Get full page as markdown
5 maxResults: 3, // Fewer results since each has more content
6 includeDomains: [
7 "developer.mozilla.org",
8 "nodejs.org",
9 "typescriptlang.org",
10 "docs.github.com",
11 ],
12 });
13
14 return response.results.map((r) => ({
15 title: r.title,
16 url: r.url,
17 snippet: r.content,
18 fullContent: r.rawContent, // Full markdown content
19 relevance: r.score,
20 }));
21}
Pattern 4: News/Recent Content Search #
For finding recent updates, releases, or announcements:
1async function searchRecentNews(query: string) {
2 const response = await tvly.search(query, {
3 topic: "news", // Optimized for recent content
4 timeRange: "week", // "day", "week", "month", "year"
5 maxResults: 5,
6 });
7
8 return response.results.map((r) => ({
9 title: r.title,
10 url: r.url,
11 content: r.content,
12 publishedDate: r.publishedDate, // Only available with topic: "news"
13 }));
14}
15
16// Usage
17const releases = await searchRecentNews("Node.js release 2025");
Pattern 5: Integration with Gemini Agent #
Combine Tavily search with your Gemini agent as a function tool:
1import { GoogleGenAI, Type } from "@google/genai";
2import { tavily } from "@tavily/core";
3
4const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
5const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY });
6
7// Define search tool for Gemini
8const webSearchTool = {
9 name: "web_search",
10 description:
11 "Search the web for current information, documentation, code examples, or recent news",
12 parameters: {
13 type: Type.OBJECT,
14 properties: {
15 query: {
16 type: Type.STRING,
17 description: "The search query",
18 },
19 searchType: {
20 type: Type.STRING,
21 enum: ["general", "news", "docs"],
22 description: "Type of search: general, news (recent), or docs (technical)",
23 },
24 },
25 required: ["query"],
26 },
27};
28
29// Execute search when Gemini calls the tool
30async function executeWebSearch(
31 query: string,
32 searchType: string = "general"
33): Promise<string> {
34 const options: any = {
35 maxResults: 5,
36 includeAnswer: "basic",
37 };
38
39 if (searchType === "news") {
40 options.topic = "news";
41 options.timeRange = "week";
42 } else if (searchType === "docs") {
43 options.searchDepth = "advanced";
44 options.includeDomains = [
45 "developer.mozilla.org",
46 "docs.python.org",
47 "typescriptlang.org",
48 "nodejs.org",
49 ];
50 }
51
52 const response = await tvly.search(query, options);
53
54 // Format for LLM consumption
55 let result = "";
56 if (response.answer) {
57 result += `## Summary\n${response.answer}\n\n`;
58 }
59 result += "## Sources\n";
60 result += response.results
61 .map((r) => `### ${r.title}\n${r.url}\n\n${r.content}`)
62 .join("\n\n");
63
64 return result;
65}
66
67// Use in agent loop
68async function runAgent() {
69 const chat = ai.chats.create({
70 model: "gemini-3-pro-preview",
71 config: {
72 tools: [{ functionDeclarations: [webSearchTool] }],
73 systemInstruction: `You are a helpful coding assistant. Use web_search to find current documentation, code examples, or news when needed.`,
74 },
75 });
76
77 const response = await chat.sendMessage({
78 message: "How do I use the new Bun test runner?",
79 });
80
81 if (response.functionCalls?.length) {
82 const call = response.functionCalls[0];
83 const searchResult = await executeWebSearch(
84 call.args.query as string,
85 call.args.searchType as string
86 );
87
88 // Send search results back to Gemini
89 const finalResponse = await chat.sendMessage({
90 message: [
91 {
92 functionResponse: {
93 name: call.name,
94 response: { result: searchResult },
95 },
96 },
97 ],
98 });
99
100 console.log(finalResponse.text);
101 }
102}
Pattern 6: Extract Content from Specific URLs #
When you already have URLs and need to extract content:
1async function extractContent(urls: string[]) {
2 // Costs 1 credit per 5 successful URLs
3 const response = await tvly.extract(urls);
4
5 return response.results.map((r) => ({
6 url: r.url,
7 content: r.rawContent,
8 success: !r.error,
9 }));
10}
11
12// Usage: Extract from GitHub README or docs pages
13const content = await extractContent([
14 "https://github.com/anthropics/anthropic-sdk-python",
15 "https://docs.anthropic.com/claude/docs/intro-to-claude",
16]);
Anti-Patterns & Pitfalls #
Don't: Use advanced depth for simple queries #
1// Bad - wastes credits on simple lookups
2const response = await tvly.search("what is typescript", {
3 searchDepth: "advanced", // 2 credits instead of 1
4 includeRawContent: true, // Even more processing
5 maxResults: 20, // Overkill
6});
Why it's wrong: Basic search is sufficient for most queries. Advanced depth is only worth it when you need deeper content extraction or chunking. You're paying double for marginal benefit.
Instead: Match depth to need #
1// Good - basic search for simple queries
2const simple = await tvly.search("what is typescript", {
3 searchDepth: "basic",
4 maxResults: 3,
5});
6
7// Good - advanced only when you need full content
8const detailed = await tvly.search("typescript discriminated unions tutorial", {
9 searchDepth: "advanced",
10 includeRawContent: "markdown",
11 maxResults: 3,
12});
Don't: Request more results than you'll use #
1// Bad - 20 results but LLM context can only fit 3-5
2const response = await tvly.search(query, {
3 maxResults: 20,
4 includeRawContent: true,
5});
6
7// Then only using first 3...
8const context = response.results.slice(0, 3);
Why it's wrong: You're paying for content extraction on results you won't use. More results also slow down the API response.
Instead: Request what you need #
1// Good - request only what fits your context window
2const response = await tvly.search(query, {
3 maxResults: 5,
4 includeRawContent: true,
5});
Don't: Ignore the relevance score #
1// Bad - blindly using all results
2const context = response.results.map((r) => r.content).join("\n\n");
Why it's wrong: Low-relevance results add noise to your LLM context and can hurt response quality.
Instead: Filter by score #
1// Good - only use highly relevant results
2const relevantResults = response.results.filter((r) => r.score > 0.5);
3
4const context = relevantResults.map((r) => r.content).join("\n\n");
Don't: Hardcode API key #
1// Bad - exposed in source control
2const tvly = tavily({ apiKey: "tvly-abc123xyz" });
Why it's wrong: API keys in code get leaked via git history, logs, or bundle inspection.
Instead: Use environment variables #
1// Good
2const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY });
3
4// Even better - fail fast if missing
5const apiKey = process.env.TAVILY_API_KEY;
6if (!apiKey) throw new Error("TAVILY_API_KEY environment variable required");
7const tvly = tavily({ apiKey });
Don't: Use Tavily for raw SERP data #
1// Bad - Tavily abstracts away SERP structure
2const response = await tvly.search("best laptops 2025");
3// No access to: knowledge graphs, featured snippets, shopping results, image carousels
Why it's wrong: Tavily is optimized for LLM consumption, not SERP analysis. It doesn't expose Google's rich SERP features.
Instead: Use Serper for SERP analysis #
1// Good - use Serper when you need raw Google data
2const response = await fetch("https://google.serper.dev/search", {
3 method: "POST",
4 headers: {
5 "X-API-KEY": process.env.SERPER_API_KEY,
6 "Content-Type": "application/json",
7 },
8 body: JSON.stringify({ q: "best laptops 2025" }),
9});
10
11const data = await response.json();
12// Access: data.knowledgeGraph, data.peopleAlsoAsk, data.shopping, etc.
Caveats #
-
Free tier is generous but limited: 1,000 credits/month = ~500-1000 searches. Fine for development, but production agents may need a paid plan ($30/month for 4,000 credits)[4].
-
Not a Google replacement: Tavily uses its own search index plus web crawling. Results may differ from Google. For SEO research or SERP analysis, use Serper ($0.001/search) or SerpAPI instead[5][6].
-
No image/video/shopping verticals: Tavily focuses on text content. For image search, maps, or shopping results, use Serper's specialized endpoints[5].
-
Rate limits apply: Free tier has lower rate limits than paid plans. The API returns 429 errors when exceeded[1].
-
Content extraction isn't perfect: Some sites block crawlers or have complex JavaScript rendering.
rawContentmay be empty or incomplete for dynamic sites. -
include_answeradds latency: The LLM-generated answer takes extra time. Skip it if you're already using your own LLM to process results.
Alternative: Serper (When You Need Raw Google Data) #
If you need actual Google SERP data (knowledge graphs, featured snippets, People Also Ask), use Serper instead:
1async function googleSearch(query: string) {
2 const response = await fetch("https://google.serper.dev/search", {
3 method: "POST",
4 headers: {
5 "X-API-KEY": process.env.SERPER_API_KEY!,
6 "Content-Type": "application/json",
7 },
8 body: JSON.stringify({
9 q: query,
10 num: 10,
11 }),
12 });
13
14 const data = await response.json();
15
16 return {
17 knowledgeGraph: data.knowledgeGraph,
18 organic: data.organic, // { title, link, snippet, position }[]
19 peopleAlsoAsk: data.peopleAlsoAsk,
20 relatedSearches: data.relatedSearches,
21 };
22}
Serper pricing: 2,500 free searches, then $50/month for 50,000 searches ($0.001 each)[5].
References #
[1] Tavily Search API Reference - Official API documentation with full parameter and response schemas
[2] Tavily Homepage - "The Web Access Layer for AI Agents"
[3] Tavily vs Serper API Comparison - Detailed comparison of output formats and use cases
[4] Tavily Credits & Pricing - Credit costs and plan tiers
[5] Serper.dev - Fast and cheap Google Search API, 2,500 free queries
[6] The Complete Guide to Web Search APIs for AI Applications in 2025 - Comprehensive comparison of search API options
[7] Tavily JavaScript SDK Quick Start - Official SDK setup and examples
[8] @tavily/core on npm - Official npm package
[9] 7 Free Web Search APIs for AI Agents - Overview of free tier options
[10] Beyond Tavily - AI Search APIs in 2025 - Alternative options and when to use them