Transcription
This video is brought to you by Squarespace.
If you're trying to build something with large language models, you know things can get messy pretty quickly. You start with one prompt, then you add a few tools, then you pass some data to another model, and before long, your logic is going to be scattered and all over the place. So, the question is, how do you structure AI systems the right way? In this video, I'm going to show you three design patterns that I've adapted for LLMs and agents that are going to help you build cleaner, more modular AI systems. Let's go.
The example that I'm going to use in this video is a travel agent. And this agent can help you find hotels and flights, etc., etc. Now, there is one prompt that I'm going to start with, which is this. I want a rainy city trip within Europe. I love towers. I don't want to cross water to get there. And this is Maria who lives in Berlin. So, let's run this particular example and see what happens.
The agent has decided that Maria needs to go to Paris, France, which is wrong. It should be Utre, the Netherlands. Anyway, okay, Paris. Fine. So, then the agent books a flight from Berlin to Paris, which arrives at 6:00 p.m. It looks for a hotel, which is the highest regency in Paris. I don't know if this is a good hotel or not. And then uh but it is quite expensive so hopefully it is. And then there are some activities for early evening explorers. You can visit the shopping mall, take an evening walk, uh enjoy a dinner, etc., etc. So the Asian has done like a bunch of different things here. And that's typically something that you're going to encounter in AI application, right? It's not just I send a prompt and I get back a response. I mean, we can just use a chat agent for that. We want something that's a bit more uh involved. And of course, in this particular situation, you don't want to handle everything in one massive prompt because that's simply not going to work. You need to break it down into separate agents and then each is responsible for a single step. So we have choosing a destination, we have planning the flight, recommending a hotel, and suggesting things to do.
The way I've built this is that I'm implementing each of these steps as its own function. And then you can execute them in order by storing them in a list. More about that in a minute. So how is this code actually set up? I'm using byic AI for the agents. So first I load the environment variables. In this case, there's just one which is the OpenAI API key. Then we have some dependencies which is we need to know the username. We need to know the origin city and then there is a context that's basically the information that each of these agents is going to need. So things like the destination, the uh origin city, the arrival time, the hotel name, etc. And this going to be filled in step by step by the agents.
Then for each step I have an agent. So in this case I have a destination agent that helps users find a ideal tra travel destination based on their preferences. Then I have a flight agent that can look for flights for a particular trip. Then I have a hotel recommendation agent that suggests a good hotel near the airport or city center. Then we have an activity agent that suggests local activities. So you have all these different agents that can do each a specific thing that also have their own system prompts that to adapt them. And then the pattern that I'm using to perform these tasks in order is the chain of responsibility. And what that pattern entails that you have a bunch of different functions that are executed in order. Now you might say, hey, design pattern should use classes and inheritance and things like that. But I'm sort of taking a bit of creative liberty here and using my own take on these patterns in this particular example. So instead of using classes here, I'm just using a function because I just think that's simpler.
Now the important thing here is that each of these functions, so I have handle destination which gets uh arguments and then uses the agent. We have handle flight, we have handle hotel and handle activities which all do a part of the job. But the important thing is that the arguments here are actually the same. We have the user input, we have the dependencies, we have the context of the trip that contains information about the trip. And as you can see the context is being filled in by each of these agents in steps. So for example, this destination agent sets the destination in the context object which is then again used by the flight agent to plan a flight to that particular destination and then same for the hotel. So the hotel also is recommended in the destination place. So all of these steps use results from the previous steps. And what makes it work is that the arguments here are always exactly the same. user inputs, dependencies and the context. And then once you have all this, then we can have a plan trip function that actually executes this chain. So it creates the trip context. It has a chain of functions and then I'm using a for loop to call each of these functions in turn with the particular user input.
Now, this way of setting it up is actually really flexible because we have these specific things that are done in these functions, but the structure is actually somewhere else. So, that's actually really nice. Also, it's really easy to add another handler here. You simply define the function and you insert it into the chain. You don't have to change anything else. You could even decide to make this dynamic and uh basically let the user select which things uh you want the AI agent to do by I don't know ticking some boxes or something and you can simply enable or disable them in the chain without having to do something somewhere else in your code. So that's the chain of responsibility. Now this is a very basic implementation of that. You can do more advanced things as well such as uh handling errors for example and make sure that if I don't know handle destination has an error that the chain isn't completed anymore. Or of course you could generalize this to more like a graph structure which is actually already built into pideantic AI. But typically what you're going to often need is a simple chain of things that needs to happen. And this is a pretty neat way of setting it up. So, that's chain of responsibility for AI agents.
Now, there are two more patterns that I'm going to show you. But before I do that, I want to talk about this video sponsor, Squarespace. Squarespace is an all-in-one website platform designed to help you stand out and succeed online. Whether you're just starting out or scaling your business, it gives you everything you need to claim your domain, build a professional site, grow your brand, and get paid all in one place. I've used Squarespace myself to launch websites for my businesses. And as someone who builds software and teaches software design, I really appreciate tools that are thoughtfully designed and just work. It's really easy to get started by using Blueprint AI, which generates a fully custom website based on just a few prompts. There are a ton of templates that all look really good. After that, it's trivial to add sections or change the appearance to your liking. And boom, you have a full professional website. Squarespace comes with integrated SEO tools, so you don't need to worry about optimizing meta descriptions or generating site maps. It handles all of that automatically. If you ever tried to get a new dev blog or documentation site indexed properly, you know how frustrating that can be. With Squarespace, it's baked in. And with the built-in analytics dashboard, you can track traffic, engagement, and even revenue if you're selling something. or you can use to figure out what landing pages are working and what needs to be improved. Whether you're building a site to showcase your portfolio or launch a software as a service that you just vibecoded, Squarespace makes it incredibly easy. Head over to squarespace.com/rincodes for a free trial. And when you're ready to launch, use offer code iron codes to save 10% off your first purchase of a website or domain.
Now, back to the video. The next pattern that I want to talk about is the observer pattern. This has to do with visibility. When you're calling multiple agents in a chain, just like I showed you before, it's actually pretty helpful to know like what prompt was sent, what response came back, how long did it take, was there a problem with the response. So, you can solve that by using the observer pattern. And what that allows you to do is that instead of scattering logging code throughout your app, you can define an observable agent. Basically, an agent that logs things. It can measure response time. It can call attached observer objects with structured info about the call. And that allows you to more easily inspect what is actually happening in your agent system.
So here's what I did. I have another example here which again uses pyic AI. In this case, I just have a single agent because I just want to show uh very basic example of how to set this up. But we have again our travel dependencies, username and the origin, city, and we have a travel response. So in this case, I'm just looking at the destination agent. And then what I've done is implement a sort of observer pattern. So the way to do that is that you have to specify what an observer actually is and how it's structured. So in this case I'm using a protocol class to define an agent observer and this has a single method called notify that gets things like the agent name the prompt that was used the current dependencies the output the duration etc etc and then I've created a class console logger that implements this protocol but of course you can add other classes as well you could also send it to a logging surface or store it in a file or do whatever you want. That's the power of the observer pattern. And there what I'm simply doing now is just printing the agent call log and that's all there's to it.
Then what I've done in order to make pyantic AI compatible with my observers is that I define a function called run with observers that gets an agent prompt dependencies and the list of observers. Now you could also go the more objectoriented route and actually create maybe a subclass of agent that is an observer. But in this case I've uh done it with a function because to me that also works pretty well. Like I said I'm not following design patterns to the tea here. I'm using my own interpretation of them. And what I've done here is that I record the time that it takes in order to do that request to the agent. And then for each of the observers in the list, I call the notify method with the information that it needs. And then finally, I return the output. The agent itself is the travel agent that basically recommends a destination just like in the previous example. And then I have the main function. So it creates the dependencies. In this case, it's Nina who's in Copenhagen. Copenhagen. And there's a prompt. Well, I want to escape to a cozy place in the mountains for the weekend. So that's definitely not going to be the Netherlands. And then we're going to call this run with observers function. And as the observers, I provide the console logger that I just defined. Let's run this code and see what happens.
So as you can see, we now get a log that's provided by our console log observer. And the nice thing is that because we've now abstracted this away, uh there is just this simple uh function call. If you have multiple agents, you can simply call this run function with the agent that you need and the prompt that you need and you supply the observers. And so it's pretty straightforward to do this. Like I mentioned, you can define any type of observer here. It can log to the console. It can log to a file, send it to database or logging service. The main thing here is that this setup, this pattern keeps your agents observable without coupling them to any specific logging implementation. And by the way, if you're serious about learning how to design better systems, AI or otherwise, I've put together a free guide at arn.code/design guide that walks you through the exact seven steps I use when designing software from scratch. The link is in the video description.
The final pattern that I want to show you that is useful in AI systems is the strategy pattern. Let's say your travel assistant should behave differently depending on the user. Maybe one user wants a formal professional travel agent, another one wants a more quirky or fun experience. And there is a third type of person that cares mostly about budget. Now instead of hard coding that behavior, you can actually use the strategy pattern to use different personalities. Now the strategy pattern again if you follow the Ganger 4 book it uses classes and inheritance where each class has one method. We can actually do it a bit simpler and then uh use a function instead that supplies the thing that we need. So, what I'm going to do is implement each travel agent personality as a function that returns a preconfigured agent. It's going to have the same input, the same output, just the system prompt is different. And this is what that example looks like. Again, by AAI, I'm loading the API key from the N file. I have my travel recommendation. I have the dependencies. And then I have my strategy function that each returns a different type of agent. So, I have the professional agent, I have the fun agent, and I have the budget agent. Now, like I said, the only thing that changes here is the system prompt, but you can imagine that you also want to have other settings in your agent that depend on the type of agent that you want. Maybe, I don't know, the temperature should be different for the fun agent because it should come up with more creative solutions or something like that. By using functions in this way, we can swap them out dynamically, which is really nice.
Then how do you use that functional version of the strategy pattern? Well, that's what this other function does. Run travel strategy. So this gets a strategy function which is a callable that returns an agent. Now you might say, why not simply pass in the agent? Well, that's a fair point. But the nice thing about the strategy pattern is that the agent is created here and not before. So you don't have to create all the agents before you actually run the strategy. And you can use this function to pass arguments. So you can even modify the behavior of how the agent is being created by calling the function here. So we pass this function as an argument to this function. We create the agent and then we run it with the prompt and the dependencies. And then what you can do is depending on the type of agent you want, you can run the strategy with that particular agent. So in this case I call run travel strategy with the professional agent function or here I call with the fun agent function or the budget agent function and we're going to get different results each time. So let me run this and you can see what the difference is.
So as you can see we have different types of responses. This is a pretty professional response. Then we have something that's a bit more fun with exclamation marks and yeah I don't really like that. Uh and then there's the uh budget agent that uh looks at cost of living which is very affordable etc etc. The nice thing about this pattern is that the run travel strategy function doesn't need to know anything about the particular agent that's being used. You can uh keep your business logic basically what is in here consistent while allowing for flexible behavior.
Now one thing you probably notice is that these patterns don't really change. They're not different for AI systems than for other systems, but some of these patterns are quite useful in particular for AI systems like the ones that I've shown you today. Again, I've used my own take on these. So, these are not like strictly following the gang of four book or anything like that. I typically tend to use functions, protocols, whatever works for me, whatever makes it simple and easy to work with. That's just my style. I know there are people that like more strict implementations of these patterns. I typically take a bit of freedom with them. I do think that patterns like this, designs like this, help you build systems that are a bit more scalable. They're going to let you decompose complex task into clearer steps. You can add observability without polluting your logic. You can inject behavior dynamically and cleanly with the patterns that I just shown you. When you when you combine this with pantic AI, you're going to get strongly typed validated outputs, fewer runtime errors, easier testing and reuse. And to me, this is how you go from, let's say, one-off hacks to scalable, maintainable AI systems.
But I'd like to hear from you. Are you using any of these design patterns in your projects? Do you implement them in this way as well? or do you use the more traditional implementation from the GA 4 book? Is there another design pattern that you use a lot in systems that work with LLMs or another pattern that you'd like to see applied to these LLMs? Then I can do a video about that. Let me know in the comments. Like I said, I've used Binantic AI in this example in a very basic fashion, but it's actually really powerful. If you want to learn more about how to use Binantic AI in your projects and what you can do with it, check out this video next.