Hey there! In this article, we're gonna talk about building an agentic RAG workflow with Rust! We'll be building an agent that can take a CSV file, parse it and embed it into Qdrant, as well as retrieving the relevant embeddings from Qdrant to answer questions from users about the contents of the CSV file.
Interested in deploying or just want to see what the final code looks like? You can find the repository here.
What is Agentic RAG?
Agentic RAG, or Agentic Retrieval Augmented Generation, is the concept of mixing AI agents with RAG to be able to produce a workflow that is even better at being tailored to a specific use case than an agent workflow normally would be.
Essentially, the difference between this workflow and a regular agent workflow would be that each agent can individually access embeddings from a vector database to be able to retrieve contextually relevant data - resulting in more accurate answers across the board in an AI agent workflow!
Getting Started
To get started, use shuttle init to create a new project.
Next, we'll add the dependencies we need using a shell snippet:
We'll also need to make sure to have a Qdrant URL and an API key, as well as an OpenAI API key. Shuttle uses environment variables via a SecretStore macro in the main function, and can be stored in the Secrets.toml file:
Next, we'll update our main function to have our Qdrant macro and our secrets macro. We'll iterate through each secret and set it as an environment variable - this allows us to use our secrets globally, without having to reference the SecretStore variable at all:
Building an agentic RAG workflow
Setting up our agent
The agent itself is quite simple: it holds an OpenAI client, as well as a Qdrant client to be able to search for relevant document embeddings. Other fields can also be added here, depending on what capabilities your agent requires.
Next we'll want to create a helper method for creating the agent, as well as a system message which we'll feed into the model prompt later.
File parsing and embedding into Qdrant
Next, we will implement a File struct for CSV file parsing - it should be able to hold the file path, contents as well as the rows as a Vec<String> (string array, or more accurately a vector of strings). There's a few reasons why we store the rows as a Vec<String>:
- Smaller chunks improve the retrieval accuracy, one of the biggest challenges that RAG has to deal with. Retrieving a wrong or otherwise inaccurate document can hamper accuracy significantly.
- Improved retrieval accuracy leads to enhanced contextual relevance - which is quite important for complex queries that require specific question.
- Processing and indexing smaller chunks
While the above parsing method is serviceable (collecting all the lines into a Vec<String>), note that it is a naive implementation. Based on how your CSV files are delimited and/or if there is dirty data to clean up, you may want to either prepare your data so that it is already well-prepared, or include some form of data cleaning or validation. Some examples of this might be:
unicode-segmentation- a library crate for splitting sentencescsv_log_cleaner- a binary crate for cleaning CSVsvalidator- a library crate for validating struct/enum fields
Next, we'll go back to our agent and implement a method for embedding documents into Qdrant that will take the File struct we defined.
To do this, we need to do the following:
- Take the rows we created earlier and add them as the input for our embed request.
- Create the embeddings (with openAI) and create a payload for storing alongside the embeddings in Qdrant. Note that although we use a
uuid::Uuidfor unique storage, you could just as easily use numbers by adding a number counter to your struct and incrementing it by 1 after you've inserted an embedding. - Assuming there are no errors, return
Ok(())
Document searching
Now that we've embedded our document, we'll want a way to check whether our embeddings are contextually relevant to whatever prompt the user gives us. For this, we'll create a search_document function that does the following:
- Embed the prompt using
CreateEmbeddingRequestand get the embedding from the results. We'll be using this embedding in our document search. Because we've only added one sentence to embed here (the prompt), it will only return one sentence - so we can create an iterator from the vector and attempt to find the first result. - Create a parameter list for our document search through the
SearchPointsstruct (see below). Here we need to set the collection name, the vector that we want to search against (ie the input), how many results we want to be returned if there are any matches, as well as the payload selector. - Search the database for results - if there are no results, return an an error; if there is a result, then return the result back.
Now that everything we need to use our agent effectively is set up, we can set up a prompt function!
Hooking the agent up to our web service
Because we separated the agent logic from our web service logic, we just need to connect the bits together and we should be done!
Firstly, we'll create a couple of structs - the Prompt struct that will take a JSON prompt, and the AppState function that will act as shared application state in our Axum web server.
We'll also introduce our prompt handler endpoint here:
Then we need to parse our CSV file in the main function, create our AppState and embed the CSV, as well as setting up our router:
Deploying
To deploy, all you need to do is use shuttle deploy (with the --ad flag if on a Git branch with uncommitted changes), sit back and watch the magic happen!
Finishing Up
Thanks for reading! With the power of combining AI agents and RAG, we can create powerful workflows to be able to satisfy many different use cases. With Rust, we can leverage performance benefits to be able to run our workflows safely and with a low memory footprint.
Read more:







