Introduction0:00
Hey folks, my name is Nico, I work on the AI SDK at Vercel, and in this session today we're going to be looking at building agents with the AI SDK. Now, this session is roughly divided into 2 sections; we're going to start with a fundamentals section.
This is going to introduce you to the building blocks that you need to understand about using the AI SDK before we jump into building agents, and then we're going to be building a deep research clone, uh, in Node.js.
So, without further ado, let's getright into it. To follow along, the first thing that you're going to have to do is clone the repository here, install the dependencies, copy over, uh, any environment variables, um, and then you'll be ready to go.
In this project we have just one file, uh, index.ts, and you can run this file by just running pnpm run dev. I have this alias to just pnd, so if you see me typing that, that's just running the script.
Generating Text1:01
Great, so let's start with the first primitive that we're going to be looking at: the generateText function. Now this is, as it sounds, a way for you to call a large language model and generate some text. So let's take a look.
In this session, rather than typing everything out line by line, I'm going to be copying over snippets so we can get through things a little bit faster and focus on the core concepts rather than necessarily remembering to type everything out properly.
So let's start with the first snippet.
What we've got here is a single function called main. It's asynchronous, and inside this function we call generateText, which we import from the AI SDK. We specify the model we want to use, in this case OpenAI's GPT-4o mini, um, and then we pass in a prompt: "Hello world."
Finally, we log out the resulting text that is generated and call the function. So, we can head into the terminal, run pnpm run dev, and we should see a message back from GPT-4o mini: "Hello, how can I assist you today?"
Now, each of these generateText functions and, uh, streamText and generateObject and streamObject, as we'll see later, uh, can take in either a prompt as input or messages. And in this case, messages is just an array of messages where a message has a role and then some content.
So this would be the same as we had before if we changed this to user. For the rest of this session, we'll be using mostly the prompt key. So one of the core features of the AI SDK is its unified interface.
And what that means is that we're able to switch between language models by just changing one single line of code. Now, there are many reasons why you might want to do this. It might be because, uh, a model is cheaper, faster, um, better at your specific use case.
Um, and speaking of better, one thing that we can try asking this model in particular is, uh, something that we know it might struggle with, like: when was the AI Engineer Summit in 2025? Now I know for a fact that GPT-4o mini is not going to be able to do this because it doesn't have access to the web, and its training data cut-off is somewhere in 2024.
So we can see: "I'm sorry, but I don't have information about events scheduled for 2025, including the AI Engineer Summit." So how could we solve this? Well, we could, and we'll look into later, add a tool, and that tool could call the web and return those results and type those into the context of the conversation, and then the language model can deduce from there.
But we could also just pick a model that has web search built in, something like Perplexity. So how do we change to a different model? Well, all we have to do is
change the model that we specify here to Perplexity, invoke that model provider instance, and then select the model that we want to use. So in this case, we've imported a new, uh, provider, this time from aisdk/perplexity, uh, and then we specify we want to use Sonar Pro.
So if we run this again now, the model will come back: "The AI Engineer Summit in 2025 took place from February 19 to February 22, 2025, in New York City." This isright. One thing that's interesting here is that unlike the OpenAI response, because Perplexity is using sources, it's actually named them, or referenced them, in its response.
How do we access them? Well, the AI SDK makes this accessible with the
sources property. So we can save, run the script again,
and we'll see the sources in line. Very, very, very cool. And this isn't just limited to Perplexity; we support a ton of providers, and you can check all of them out at our documentation. If you head to aisdk.vercel.ai and head to our providers page, you can see we have a whole host of providers here, um, many of which support web search as well.
So if, for example, you wanted to use, let's say, Google's Gemini, so we can import the Google provider here and use Gemini Flash, uh, 1.5, and then specify that we want to use search grounding. Save, run the script again, and we'll see the same prompt with the same configuration going off to a different model at a different provider and getting what we hope is a similar, accurate answer: "AI Engineer Summit took place from February 19 to 2025," and a bunch of sources in line to confirm this.
Really, really cool, and really powerful stuff. So that was the first primitive that we looked at, something as basic and simple as just generating text from a language model. But what if we want to go beyond generating text and use language models to interact with the outside world and to perform actions?
Tool Calling6:22
This is where tools or function calling comes in. While tools may seem complicated, at the core it's a very simple idea: we give the model a prompt, and then we also pass, as part of the conversation context, a list of tools that it has available to it.
Each of these tools will be provided with the name of the tool, as well as a description of what the tool does so the model knows when to use it, and finally any data that it requires in order to use those tools.
Let's say the model decides it needs to use one of the tools to solve the user's query. Rather than generating some text as a response, it would generate a tool call, meaning it would generate the, the name of the tool it wants to use and any arguments or data that it can parse from the context of the conversation necessary to run those tools.
It's then on you, the developer, to parse that tool call, run that code, and then do with that as you please. So how do we do this with the AI SDK? Well, let's check out a very simple example, and when I say simple, I mean simple.
We're going to ask a language model to add two numbers together. So copy in some code here. Uh, we've got our same main function as before. We're calling generateText again. We're specifying our model is GPT-4o. Our prompt this time is "What's 10 plus 5?"
Very difficult question, but this time we are passing a tool. Now, to pass tools to the generateText or streamText function, you pass a tools object. Within that object, you specify a key or the name of the tool, in this case addNumbers.
Then you can, as the value, use this tool utility function, which I'll explain in a second, to then, uh, define what the tool should be. So the tool is going to have a description. This is what the tool does, and this is really important because this is what the language model uses to decide whether it should invoke that tool.
Uh, parameters. This is the data necessary for the language model to run this tool, and this is what the language model is going to have to parse from the conversation context in order to use the tool. And then you have the execute function.
This can be any arbitrary asynchronous JavaScript code that will be run when the language model generates a tool call. And what this tool function does here is, this is completely unnecessary but is a really nice DX, um, improvement here, and what it does is that it provides type safety between the parameters that you define here and the arguments that, uh, come into your execute function.
So, to showcase this, if we change this to string, if I can spell, you can see I can't spell, um, but we can see that num1 is still a number where num2 is a string. Uh, so again, this is not necessary at all, um, but it does make building and, uh, working with these tools a lot easier.
Now, what's going on behind the scenes is that the AI SDK is going to parse any tool calls, and then it's going to automatically invoke the execute function and return that in a toolResults array, as we can seeright here.
And that's what we're logging out to the console. So if we head to the console, clear it out, and run the script, we should see a toolResult logged out to the console, and that indeed we do. We see toolResult, uh, addNumbers.
We see the arguments that are parsed from the conversation context, and then we see the result itself.
But notice that we have logged out the toolResults this time, and instead of, instead of the, the text generated. And, and I had said before, it's kind of a nuanced thing here, but the language model isn't generating text anymore, it's generating a tool call.
And we can see that if we said console.log, actually we just delete this altogether, and we log out the resulting text, we won't see anything in the console. It's completely empty. So how can we get the model to actually incorporate the toolResults into a generated text answer,right?
We want it to synthesize this action, whatever it's performed, and communicate that back to the user. We could look at the last toolResult. So cons last toolResult equals result.toolResults, and we pop, we take that last one, and then we say if last toolResult equal, uh, dot dot name, dot toolName, uh, equals, and it's cool, we've got full type safety here.
Um, and then we can say, oh, maybe we want to away and, and generate text further and return that to the user. But as you can see, this is error prone, I've made like three mistakes already, but also it doesn't scale,right?
If we add 10, 15, 20, 100 tools in here, we don't want to write out 100 of these conditional statements. So what we can do instead is use a property called maxSteps, and we're going to set this to a number.
Now, maxSteps can seem, again, a little bit confusing, but what it is at its core is that if the language model decides to generate a tool call, and therefore there is a toolResult, we are going to send that toolResult alongside the previous conversation context back to the model and trigger another generation.
And the model will continue doing this until it either generates just plain text, i.e., there's no toolResult, or we reach the threshold for the maximum number of steps. So this is quite a, a, um, a, a simple but powerful way to allow the model to keep running autonomously, picking the next step in the process, um, without necessarily having to add any logic or rerouting, repiping outputs here.
So if we run this code now, and, and what we're going to do is we're actually going to log out the result.steps.length, and actually we could even log out the whole
step so we can see what's really happening. And we're going to stringify this so it looks nice. Uh, we should see that the language model is going to respond to our, if we go up all of the, the crux, uh, we can see 10 plus 5 equals 15.
So it responded with plain text, and now we can see the different steps. We've got the initial step, which generated a tool call, addNumbers, 10 and 5, and then we can see the, the, the toolResult that we had logged out to the console.
A bunch of, uh, stuff here. Uh, cool note that you can tap into, literally, the, the raw request and response body with the AI SDK, so you can see exactly what's being sent to language models if you ever need to debug, but that's aside.
Um, and then we should see the second step. Uh, and the second step was stepType toolResult, um, and we can see that there is text actually in this, uh, generation.
So cool, we've now seen how we can build what we're going to call a multi-step agent, because that's what this is. This, this language model has been given the ability to choose the number of steps that it wants to use and choose the, the tools or the direction it wants to go down.
But obviously, having just one tool here doesn't showcase it, doesn't showcase this very well. So why don't we add another tool here to see this agentic behavior in action? What we're going to do is, again, I'm going to just copy over some code.
We are going to add a tool that has the ability to get weather. Description here, kind of obvious. Get the current weather at a location. The parameters here, we need the latitude, longitude, and city. Um, and then finally, in the execute function, we're going to make a fetch request, passing in the latitude and longitude, and then we are going to unwrap the weather data and return it in the, in the toolResult.
Now, a lot of people might be, uh, thinking in their head, "We're not providing the latitude and we're not providing the longitude, like, is this going to fail?" And this taps into a really cool, uh, inference capability that we can use, uh, in these parameters in that we can let the language model infer these parameters from the context of the conversation.
So we're fairly certain that the user is going to give city as, as part of their prompt, and in this case we're going to be asking for the weather in two cities, and then we're going to want to add them together.
So we'll know that we'll have the city, and we'll let the la, the language model use its training data to effectively infer what these might be. Now, I wouldn't suggest doing this particular example in production, but there are a lot of really cool use cases that you can use this pattern with.
So I foreshadowed a little bit. Our prompt here is going to be get the weather in San Francisco and New York, and then add them together. So we have two tools available here, getWeather and addNumbers, and we have a maximum number of steps of three.
So we'd expect probably, well, actually, let's not even guess. Let's see if we can, um, print out the steps, the length of the steps, uh, the steps themselves, and then, uh, the resulting text generation. So I save, run the script, and we'll see a response.
The current temperature in San Francisco is 12.3 degrees Celsius. In New York, it's 15.2. When you add these temperatures together, you get 27.5 degrees Celsius. So let's see how many steps we had. Three steps. We had an initial step, which was a tool call step.
Uh, we didn't unwrap this in the, in the, in the console log, but I know exactly what these are because I have done this demo before. Uh, and this is cool, we're using parallel tool calls here. Um, so the, actually we can see in the body.
We can see in the bodyright here. Um, uh, or it's probably going to be in the, in the response actually, which is also wrapped in, in here, not unwrapped. Uh, but what's happening here is that we're doing our getWeather, our, our getWeather call twice.
We're doing for New York and then San Francisco. And then in our second step, we're going to be doing our addTwoNumbers together. And then for our final step, we actually, uh, have the, the text generation itself. Um, and I can even, if we do it again here, just to show you, uh, let's stringify this out.
Um, just in case people don't believe me, we'll do it one more time, and this time we'll see exactly what is, what is coming out.
So if we jump, it's good, good, good sign that we're getting exactly the same response. It means that the data is correct. Um, and if we scroll up to the, where are we? The first step.
So three steps here. And yeah, you can see, so we've got two tool calls. We've got getWeather for, uh, the latitude and longitude and the city of San Francisco. The same for, oops, I scrolled too fast, for New York.
And then we have the addTwoNumbers step. If we get there, let's scroll down. There we go. addNumbers, number one, number two. We get our result. And then finally, we have our actual text generation, which comes at the very end.
Structured Outputs18:30
So awesome. That's two major fundamental building blocks out of the way, generating text and using tool calls. The final thing that we're going to look at is generating structured data, also known as structured outputs. There are two ways to generate structured outputs with the AI SDK.
We can, one, use generateText with its experimental output option, which we'll look at first, and two, which we'll be using more later in the session, using the generateObject function, which is a function dedicated to structured outputs. The generateObject function, uh, I will, I will tell you, is my favorite function in the entire AI SDK, absolute workhorse of a function, and we'll see it later.
So let's take a look at our existing tool calling example where we're getting the weather in two locations and then finally getting the sum of those two numbers added together and see how we could add structured outputs to make the eventual output easier to use maybe later on in our program.
So the way that we're going to introduce structured outputs here is using the experimental, I think we have to head up here, the experimental outputs, um, output key. We can pull in output.object from AI, um, and then we define in here our schema.
We're going to be using Zod to define our schema. And if you've never used Zod before, it's a TypeScript validation library that is super powerful and, uh, particularly paired with the AI SDK, seems like a match made in heaven.
We'll be looking at it a lot in this session, and it makes working with structured outputs an absolute breeze. So let's define what we want our output to actually be. In this case, we know we want the sum, which is going to be a number.
So let's define a key of sum and make it a Z.number. Finally, we add our comma. And now, instead of logging out all of this, all of the steps, and instead of logging out the text, I'm just going to log out the experimental output.
And let's run the script and see what we get.
So this time, instead of getting all of that extra text, which we maybe could have prompted away, we just have a simple typeSafe object that we can access. In this case, we've got experimentalOutput.sum, which we know is going to be a number, otherwise it will throw an error.
So you can combine tool calling with structured output to build out some really, really awesome, uh, use cases. Now let's very quickly look at the generateObject function. So I'm going to delete everything in our main function, uh, and copy over a generateObject example.
So we'll save, we'll clear everything away. And in this case, uh, we're going to be looking and seeing if AI can help us with a very invoke problem, and that is defining what an AI agent is. I know Simon, um, had, had posted a, a survey on Twitter asking for definitions and had crowdsourced something like 250 different definitions, which ended up being six categories of different definitions.
Anyway, nobody can, can agree on it. And so let's see if AI can help us. Uh, spoiler alert, I don't think it will, but. So what we've got here is our main function as per usual. We're importing generateObject from the AI SDK.
Uh, we specify our model as GPT-4.0 Mini. This should start to look quite, uh, uh, similar to how we've been doing it previously. Pass in a prompt. Please come up with 10 definitions for AI agents. And now with generateObject, we can define a schema.
In this case, our schema is going to have one key. It's going to be an object. That key is definitions. Every, um, and this is defined as an array of strings. And then that will give us a typeSafe resulting object that has our definitions key on it, which is an array of strings.
So if we run the script, wait a second, we should see 10 probably pretty bad definitions of what an AI agent is. An AI agent is a software entity that uses artificial intelligence techniques to perform tasks autonomously or semi-autonomously.
Not as bad as I thought, but why don't we see if we can alter this slightly? And, and the way I want to do this is provide some more details to the language model of exactly what I want it to do for each of the strings in this array.
Now we could jump into the prompt and say, each of these definitions should be X. But one of the really, really cool features we can tap into with Zod is the .describe function. And with .describe, we can, as it sounds, go to any key or any value, sorry, and we can chain on this .describe function.
And in here, we can describe exactly what we want for that exact value. So I'm going to say something, uh, kind of cheeky, and I'm going to say, um, I'm just going to paste this in. I want the language model to use as much jargon as possible.
It should be completely incoherent. So let's see what this can do. Spoiler alert, this is how I spend a lot of my time hacking on, on language model stuff, just like asking really ridiculous stuff. And so let's see what we got here.
Autonomous entities that leverage algorithmic heuristics to optimize decision-making processes in dynamic environments. That is complete BS. Um, and we've got 10 of them. So, um, so this is a really cool thing about working with, uh, structured outputs with the AI SDK and particularly with that Zod integration.
It makes defining these schemas and providing context super simple and really easy to maintain as well. Great. And with that, we've gone through the fundamentals of building agents with the AI SDK. Now we're going to move on to a more practical project.
Deep Research24:22
We're going to be trying to build a deep research clone. Now, obviously, this isn't going to be a full crazy application implementation. We're just going to be doing it in Node. So we're going to have a terminal script that we're going to build.
We're going to pass a query, and then we're going to conduct a bunch of deep research and write a, a markdown report into our file system. But in doing this, this should introduce us to a few things. It should introduce us to, one, how we would break down this idea of, of deep research into like a structured workflow.
Um, and within this workflow, uh, we're going to have some autonomous agentic elements. So we'll see what a production AI system might look like. Um, and we'll also look at how we can combine different AI SDK functions together to build these more complex AI systems that can really cater to interesting and honestly cool use cases that you can use in production to help build cool stuff.
I don't know how, how better to, to, to say it than that. So without further ado, I'm going to head to the deep research section of the companion site because I've provided a nice explanation of how the workflow is going to work in, in natural language and also a nice diagram to explain exactly what's going to be happening.
Now, also, if you haven't used deep research before, one, I highly suggest you check it out. Um, OpenAI has version. Deep research, uh, Gemini has version as well. But roughly speaking, what's happening is you give these products a, a topic to think about, and it will go off for an extended period of time, searching the web, aggregating resources, going down like webs of thought, and then it will aggregate all of that information together and finally return it into a report to hopefully solve your query.
And the cool thing is this really taps into a fundamental, uh, a fundamentally strong part of, of language models, of this like synthesizing troves and troves of, of information. Um, so yeah, let's look at how this workflow is going to look like.
So the rough steps are going to be, we're going to take an input, uh, a rough query, a prompt. We're then going to, for that prompt, generate a bunch of subqueries. So let's think about, like, we want to do research on electric cars.
Uh, the queries or the search queries that we might generate are like, what is an electric car? Who are the biggest electric car producers? And so on. Then for each of those queries, we're going to search the web for a relevant result.
And then we're going to analyze that result for learnings and follow-up questions. And then if we want to go into more depth, which we'll look at in a second how that works, uh, we will take those follow-up questions as well as like the existing research, generate a new query, and like completely recursively complete that process, meaning like we basically start again while keeping all of the accumulated research.
And in this way, we can go down these, like, these webs of, of thought and of questions and, and, and build a comprehensive aggregated, a comprehensive set of information about a, a given topic. So what this might look like in theory, I like this little explanation.
Uh, so let's say we have electric cars. We're calling this level zero, just like this is the initial query. And at level one, uh, we're going to have a breadth. Now, breadth is just like how many different, uh, queries do we want at each step.
So let's say we're at, we're at level one, um, and we're doing electric cars. And the three search queries that we generate are Tesla Model 3 specification, electric car charging infrastructure, and electric vehicle battery technology. And then for each of those different queries, we'd complete the research.
And maybe for Tesla Model 3, we would then start generating, uh, go down the web, have two breadth, uh, like we want to generate two different, uh, lines of inquiry to go down. And then we go down, okay, we want to know Model 3 range capacity and Model 3 pricing.
And then for electric car charging infrastructure, we want to know fast charging stations in the US and home charging installation and so on. And so depending on what depth and breadth settings we set, um, we're able to control the level of information, um, and the depth of information that we gather.
So that was a lot. Let's, uh, let's jump into actually trying to build this. And the first thing that we're going to look at is building a function that can generate some search queries. So I'm going to dive back into the, back into the code editor.
We're going to clear out this file, and we are going to copy in our first function. So the first thing, like we said, we want to generate a bunch of search queries. This is going to be a single function called generateSearchQueries.
I'm very creative. Uh, it's going to take in a query, uh, which is of type string. And the number of search queries that we actually want to generate, we're going to set this to be three just as default.
This is for you to play around with later if you'd like. Um, then we use the generateObject function. We're specifying a main model so that we can just, uh, set it once, reference it everywhere, and if we want to update it later, we can in just one place.
So that's why we're doing that. We have a prompt here. Uh, we, we use template strings all over the place in this, and you'll probably end up using them a ton. Super helpful, uh, for this use case. So we say, generate N number of search queries for the following query, passing in the, the search query.
And then we ask for a structured output, which should have a, uh, an array of strings, minimum of one, maximum of five, although we're setting it kind of loosely here at, uh, three. And then we return those queries.
So that's our generateSearchQueries function. If we go back to our companion site, we see the next thing that we have to do is map through each of those queries and search the web for relevant results, analyze the result for learning and follow-up questions, and then follow up with new queries.
So let's go one at a time. The first thing actually we'll, we'll want to do is have a main function so that we can actually, uh, specify a prompt and then call this generateSearchQueries. So our prompt is going to be, if you were at the AI Engineer Summit, there's an awesome talk, and actually it's, I think it's available online from the Gemini team where they walked through, uh, how they went about building deep research.
It was a really, really good talk. And one of my favorite things from it was hearing the, the prompt that they used as like the, the kind of the gold standard to evaluate the progress of the, the product.
And that was, what do you need to be a D1 shotput athlete? So in that similar vein, we're going to be using that today. So we take that prompt and then we pass it into our generateSearchQueries function, uh, because we specified a default number of search queries as three.
We, we don't need to specify one here. Uh, and then that's going to return our queries. So we can do just a little check-in to see what's happening here and log out our queries, run the script, and we should see fairly quickly we've got three potential search queries for our prompt of what do you need to be a D1 shotput athlete.
These are obviously geared for a search engine. So like requirements to become a D1 shotput athlete, training regiment for D1 shotput athletes, qualifications for NCAA division one shotput. So three pretty good queries that I think would make sense to, to look for if you wanted to learn more about this.
Allright, back to our companion site. We'll see the next thing that we need to do. Map through these queries and search the web for a relevant result. First thing that we're going to implement now is a function to actually search the web for relevant results.
And the service we're actually going to use today to do this is called Xa. Uh, if you haven't used it before, highly rate it. Um, I really enjoy using it. It's fast, cheap, um, but judge for yourself, like we'll use it now.
And, uh, I think they've got a great API. And so yeah, let's build a function to search the web with Xa.
So I'm going to copy over some code. We'll head back to, uh, our codebase, and we're going to make a few new lines here. Uh, so what we're going to do is we're going to import Xa from the Xa.js package.
We're going to instantiate Xa, Xa, Xa, passing in our, uh, Xa API key. Uh, we're going to define a type, which we're going to be using heavily later. And then finally, we are going to actually search the web.
So our search web function takes in a query, which is a type string, and then it's going to use the Xa search and contents function, uh, passing in our query and then specifying some optional config. Two optional config we have here is one, the number of results we want.
I've left this as one just as this is a simple demo, but you'd probably want to set this as configurable and allow the language model to infer what is necessary based on maybe like how complicated something might be.
I don't know. That's something for you to experiment with. Um, and then the second option is live crawl. Um, and live crawl is kind of as it sounds, allows you to, or ensures that the results that you're getting are live, uh, rather than something that's in their cache.
So you obviously take a little bit of a hit on performance or time for this, uh, result to be executed, but you're sure that everything that is executed is, is, is live, is up to date. Now, the other thing that I'm doing here is that I'm actually mapping through the results and only returning the information that I feel is relevant or necessary for completing this process.
And there are a few reasons for doing this, but the main reason is to reduce the number of tokens that I'm sending to OpenAI. Um, two reasons for that. One is that it's cheaper, fewer tokens, uh, just going to be cheaper.
Um, but second, and more important, I found that the language model is so much more effective when you trim away all of the irrelevant information. So, um, things like a Favicon, Favicon link is going to take up a decent amount of space, not necessary at all for the language model to actually use these resources.
So I tend to do this a lot whenever I have tool calls or, or building out tools for working with language models, ensuring that the information that I'm returning or providing as part of the context is entirely relevant to the generation or to, to the task at hand, we should say.
Evaluation Loop35:54
Cool. So that's our search web function. And now if we go back to again, our, we can think of this a bit like a checklist. The next thing that we're going to have to do is analyze the results, um, the search results for learnings and follow-up questions.
Now, this is going to be the most complicated part of the entire workflow, and this is also going to be the agentic part of the workflow. And so what we're going to do is we're going to use generate text as we did before, giving it two tools.
We're going to have a tool for searching the web, and then we're going to have a tool for evaluating the relevance of that tool call. And this is kind of, I, I say this is the most interesting and equally also the, the agentic part, um, because it's going to continue doing that flow for as long as it takes to get a, a relevant search result.
So let's see how we can implement this. I'm going to copy over this code and bear with me 'cause there's a lot, there's a, a decent amount of code here, uh, but we'll walk through all of it and see how it works.
So I'm going to also, uh, make sure that we've got our imports all good. I'm going to close off, uh, some of our tools. And now we're ready to go. So this function is called search and process. It is going to search and process.
It's going to take in our query. Uh, again, remember, search the web for a relevant result and analyze, uh, search the web for map through each query and search the web for a relevant result. So we're taking in that query.
Uh, we're going to take, we're going to create two local variables for this, for this function, uh, pending search results. And if you can think about it, like this process of searching the web and trying to figure out if it's relevant, when you search the web, you're going to add that result to the pending search results array.
And then for evaluate, you're going to pull out whatever the most recent, uh, pending result is, check if it's relevant. If it is, pop it into the final search results array. If it's not, just discard it. Um, so that's the rough flow that we're going to be building here.
We use our main model as the model here. Our prompt is very complicated. Search the web for information about our query. Uh, we give it a system prompt. You are a researcher for each query. Search the web and then evaluate if the results are relevant and will help answer the following query.
We then pass in the query, which isright here. I was looking for it below. It'sright above. Um, we set our max steps. So we want this agentic loop to run up to five times. You could set this higher and we can set this higher for now.
Um, and that will just allow that process of finding a relevant link to continue onwards and onwards. I like to keep these relatively low just to ensure that it doesn't go off the deep end, but experiment, experiment, experiment is my like main advice for building with, with these tools.
Our first tool that we have here, I'm saying tool a lot, uh, is the search web. This searches the web for information about a given query. We take in a query, which is a string, uh, our string, uh, we then pass into the search web function that we just created earlier.
Uh, we get back some search results, that type we declared, uh, at the beginning, which I can show you here, this typeright there. Um, and then finally, as I was saying, we, we take this local variable, this pending search results, and we push whatever these new, uh, searched web results that we get.
And we also add those to the, uh, conversation context by returning them from the tool. And then we have the evaluate tool. And this is to evaluate the search results. It doesn't take in any parameters. Um, and in the execute function, we are going to pull out the latest pending result, uh, then run it through generate object, asking it to evaluate whether the search results are relevant and will help answer the following query.
Uh, and then we pass in with this XML syntax, our stringified pending result. Now, uh, the final thing here with this generate object, rather than specifying the schema with Zod, uh, because we just want to know whether it's relevant or irrelevant, we can actually use generate objects enum mode to specify just the two values that we need.
Something's either going to be relevant or irrelevant. Uh, this is super ergonomic. You could use Zod here. I just find this very easy, particularly if you have just two values. Um, and it's also easier for the language model when it's literally restricted to two models, to two values rather than generating a full structured object.
Uh, then if the evaluation is relevant, uh, we will push the pending result to the final result because we know that it's now relevant. So that's going to be a good source that we want to keep. Uh, we have some logs here.
And then this is interesting here. We say if the evaluation is irrelevant, we return the following string from this tool call. Search results are irrelevant. Please search again with a more specific query. So when max steps triggers the next generation, the language model is going to see the most recent step that took and the last part of that most recent step, i.e., the tool result was please search again with a more specific query.
So we have this system that is repeating, um, but with feedback based on what's going on. The last thing to mention here is the fact that I did not use the parameters here to parse out the search result.
And, uh, there's a very, uh, specific reason for doing that. Search result can be super long,right? This could be a whole entire crawled webpage that could be, I don't know, 10,000 tokens. And we don't want the model to have to actually parse that out.
It's literally generating the text that already exists in the context above. One, costs money. Two, it takes a long time. Three, it's probably error prone because it's literally just writing out stuff that's above. It's not referencing it. And that's why I like to do this, um, these local variables within this function, faster, cheaper, more accurate, and so on.
So this is the search and process function, and we can add it to our main function and see how it works. So we're going to console.log our search results and see, see what we got. So we'll clear this out, run the function again.
Searching the web for requirements to become a D1 shotput athlete.
Found Throws University benchmark lifts to throw 50 feet from school shotput. Evaluation completed. It's irrelevant.
So it found a new one, considered it irrelevant.
Found another one, men's track recruiting standards, and considered that irrelevant or considered that relevant, sorry. And then it returned a bunch of, oh boy, we returned the whole result. So this is like the whole scraped, uh, webpage in here.
So we can see, very cool. It is working. Great. So let's move on to the next step. And what is the next step? Uh, the next step is to analyze the results for learnings and follow-up questions. So to do this, we're going to again create a new function.
This new function is going to be called generate learnings. It's going to take in a query, which is a string, and then, uh, the search result itself. So this is like the, the webpage and, and the scraped content.
Recursive Research43:41
We're then going to use, uh, surprise, surprise, my favorite function, generate object using, uh, our main model again and providing a prompt here. This time the user is researching query. The following search results were deemed relevant. Generate a learning and a follow-up question from the following search result.
We use those XML tags again to nest in our search result that is stringified. And then finally we specify as our schema, we want, uh, a, a string, which is the learning itself, the insight we could probably call it.
And then, uh, any follow-up questions which are an array of strings. And we return that object, which is this type safe, uh, structured output. Cool. So we can incorporate that into our main function again. Let's replace all of that.
And we can see now that we are, we have our prompt, we have our queries, we, we for each query, we are going to search the web, search and process. So we're searching and making sure that the, the result is relevant.
And then for each of the results that are returned, we're then going to pass it to our generate learnings function, passing in that original query and then the, um, the search result. So we could again test to see how things are looking.
Run the script. Searching the web for requirements to become a D1 shotput athlete. Let's see how many times, uh, I have said that in, um, in this video. So it's deemed that this is irrelevant, but the next link that it finds is relevant.
So it's now processing this search result. And then we're going to get a learning. So what do we have here? To become a D1 shotput athlete, high school athletes typically need to have four years of varsity experience, achieve high state finishes, or be state champions and participate in national events like USATF National Junior Olympic Outdoor Track and Field Championships.
They should also aim for a shotput distance of around, I think that's 60 feet, um, for tier one recruits. Um, and then we've got some follow-up questions. What are the specific training regimens for shotput athletes? How do division one recruiting standards differ from division two and three?
Some like really interesting threads to, to go down. So what we're going to have to do naturally now is introduce recursion to this whole process. Meaning now that we have our learnings, we effectively want to take the follow-up questions, create a new query, and call the entire process again until, uh, effectively we, we are happy with the, the depth of information that we've gathered.
So there are going to be a few things that we need to do. One, we need to probably create a new function that can handle this recursion rather than doing it all in the main function. Two, we're going to have to, we're not going to be able to track all of our, uh, accumulated research in, in the local function anymore.
We're going to have to create some state or a variable outside in, in the global state to be able to track this as we go through. And then we're probably going to need some types as well just to help make this all a bit easier.
So let's add those now. First things first, we're going to create a new function, uh, called, uh, deep research to, um, to actually that will be able to handle our recursion. So all we've done here is taken our previous logic that was within the main function and put it into a new function called deep research.
Um, so as you can see, we're, we're generating search queries. We are then searching and process, then we're generating learning. So same as before. And then in our main function, this time we define our prompt and then we, um, call that deep research function.
So what we're going to have to do now is actually call this function recursively. But first things first, we're going to need to create, uh, a, a place to store our accumulated research state.
So I'm going to copy in some code here. Uh, we're defining two types, uh, a learning type, which we've seen before. This was the, the, the learning, the insight from the research and then follow-up questions. Um, and then research.
This is the core accumulated research object, um, or store. Um, and we store in like our original query, the queries that have been performed, um, or the queries, the active queriesright now, the search results and accumulation of all of them, uh, learnings, all the insights, and then completed queries.
And at the very end, as this develops, we're going to pass all of that state, that store of information to one large model that can synthesize all of that and generate our report. But first we need to update our deep research function to actually update the store as it's iterating through these levels of recursion.
So let's head to our deep research function. I'm going to copy all of this and we'll look at what we've updated here. So the first thing that we've done, um, is we've changed the name of one, uh, parameter from query to prompt, just so you know.
Uh, we've also updated our depth and breadth, uh, numbers. So depth, remember the levels deep that we go into the, the, the, the strands that will, or the levels of the strands that will go down. And then breadth are those like individual forks, how many of them will go down at each level.
So we first check to see if, uh, query is undefined. That will be on the first time we're ever running this. Um, and if it is, we'll set it to the, the, the prompt. We then, like last time, we generate the search queries.
This time we update our queries to be that, that, uh, whatever is returned from the search queries. Um, and that's a theme as we go through here. We're going to update our accumulated research to take in our, our search results, our learnings, and our completed queries.
And now that we have this all out of the way, we can actually call deep research recursively. Um, and at the same time, we're going to decrement the depth and breadth so that we can eventually get to a resolution, uh, so this doesn't run forever and we don't run out of all of our API credits and leave the user waiting for, for way too long.
So let's add that final recursion. I'm going to replace the entire function again, um, just so I don't make any mistakes here. We've added two things here. First, we've said, uh, if the depth is, is zero, uh, just return effectively like recursion is done at this point, uh, return the accumulated research.
Um, and now for the new stuff, where before we had the comment saying perform the deep research, now instead we create a new query saying the overall research goal is this. These are the previous search queries that have been performed.
Here are some follow-up questions. Um, and then we pass that back and call the entire function again with that new prompt, uh, again, decrementing the depth and the breadth at a more exponential rate.
So I think that's a good, as good a time as any to, to run the function again and, and kind of see what's happening. We should see it now going into various levels of depth. Again, I'm saying requirements to become a D1 shotput athlete.
So this, uh, source is deemed irrelevant. It keeps liking to use that one. Instead, the track recruiting standards is deemed relevant. We're now processing the search result. We're going to be generating some learnings. We're now searching the web for what physical and technical skills are essential for a D1 shotput athlete.
So we're going a level of depth here. We found, uh, some, some relevant.
Now we're searching again, training and skills needed for D1 shotput athletes, and so on and so forth. So we can see that it is indeed working.
One thing though that we haven't done yet is that we haven't incorporated into, and funny enough, we didn't see this in this example, but the way that our logic worksright now, when the model is trying to decide whether a link is relevant, it doesn't have context as to the links that have already been used in other steps.
And naturally, we've been talking about context length, price, speed. We don't, we sh, we surely do not want to provide the same source twice, particularly if it's taking up 10, 20,000 tokens in length. So what we're going to do is we're going to go to our search and process function.
We're going to head into our evaluate tool. And in this evaluate tool, we're also going to pass in previously used search results. And if the search result exists in that previously used search results, we're going to say that's an irrelevant source and, and try again.
So I'm going to copy the entire function, replace the entire function here. Um, this time we're taking in a new parameter here, accumulated sources. Um, and if we scroll down, we should see our updated prompt. We say, if the page already exists in the existing results, mark it as irrelevant.
And we can see we're passing it in, in XML tags, existing results, and stringifying through, passing just the URL because also we don't need all the content from the page here. We literally just need the URL. We have one error here, and that's because we added a new argument, but we haven't passed it in here.
So I'm going to just pass that in here. Uh, this will be the accumulated research. Um, I think this is the search results. Yeah, search results. Perfect. Uh, so if we were to, to run this again, what we would see, we didn't see it in the previous example before, but if it reused a source or if XOR returned a source that it had already used in a previous step, that would now be marked as irrelevant and that agentic process would continue on in a loop.
Final Report54:21
The last thing that we're going to want to do, which I actually don't have in, in this process, is that now that we have all this accumulated research, we want to give it to a big model. I, I prefer a, a reasoning model in this case to synthesize all of this information and put it into a report that we can consume to hopefully solve our query.
So let's create a new function. This function is going to be called, uh, very creative name as per usual, uh, generate report. It's going to take in that accumulated research, which is typed as such. Um, and then we're going to call generate texts.
This time we're using O3 mini rather than our main model. Uh, again, play around with this, clone this, and, and see which models, uh, you like best for this kind of thing. I found O3 mini very good at this.
And then our prompt here, we're saying, generate a report based on the following research data. We've got two new lines, and then we stringify the report, returning the final generated text. So let's actually refactor our main function very quickly to use this new, uh, this new function.
So what we're doing here, uh, we've just inlined our prompt. So we say, uh, what do you need to, to be a D1 shotput athlete? We log out some status updates, and then we pass that research into the generate report function, await it, and get out a report, which we'll finally write to the file system to a Markdown file.
So I'm going to import FS from FS, and then we're ready to go. So let's see, let's see what this does.
Great. So after about a minute, we've got our report. Below is an integrated report summarizing the research findings on what it takes to become a division one shotput athlete, along with a rel with related insights on technique, training, and common beginner pitfalls.
So, okay, we've got our, our entire report here. Um, and to be honest, given all of the information that we gave it, it's, it's pretty, it's pretty great. But one thing that we'll note here is that we didn't give the model any guidance on what exactly we wanted this report to look like.
And so the model had to infer. Um, and when you're working with language models, in order to get the best response, you want to leave as little up to the model to infer as possible. So one thing, I want this to be in a Markdown format.
Two, I'd like it to, uh, to form a bit more of a structure, um, that I want to specify. And so what we're going to do here is we're going to head back to our, uh, generate research function, and we're going to do two things.
We're going to actually just one thing, sorry. We're going to create a system prompt. We're going to tell it, give it a persona, you are an expert researcher. We're going to give it today's date. We're going to tell it to follow these instructions exactly.
Um, and a few key things in here. We're going to say, use Markdown formatted. Uh, you may use high levels of speculation or prediction, just flag it. Um, and, and in general, we just give it these, these, uh, guidelines that are very research, uh, analyst oriented.
So I'm going to run this entire thing again, and we're going to see the difference in the output.
And here it is. We have our new report. So let's jump into it. And what can we noticeright off the bat? We're using Markdown, which is awesome, much more structured. Uh, we can see that we've got. Date in line, kind of helpful to have.
Um, but like the, the level of quality of this kind of report, even just with this basic workflow, really astounds me. So you can see to be considered likeright at the top level, to be considered a division one shotput prospect, athletes are expected to demonstrate a high level of competitive success, varsity experience, um, typically four years of varsity participation, competitive exposure.
Uh, we have performance benchmarks, a benchmark throw of 55 feet. Elite or top tier recruits tend to have distances around 60 feet and 18 inches. Like this is, uh, and the difference between men's and women. It, it is so, so, so cool how, uh, we were able to build this in just 218 lines of code.
Um, so yeah, this is, this is it. This is a session. I hope you enjoyed this. If you have any questions, uh, you can reach me on, um, X, the everything platform at, uh, Nico Albanese 10. Uh, feel free to send me a DM.
Uh, if you have any questions on building with the SDK, head to sdk.vercel.ai. Check out our docs. We've got some awesome, uh, guides in the cookbook as well. Um, and yeah, I hope you enjoyed this. I want to thank Swix for asking me to, to do this, uh, session, and I really hope you enjoyed it and hope to see you at the next one.
Take care.





