Tutorial
You can use TensorZero to build virtually any application powered by LLMs.
This tutorial shows it’s easy to set up an LLM application with TensorZero. We’ll build a few different applications to showcase the flexibility of TensorZero: a simple chatbot, an email copilot, a weather RAG system, and a structured data extraction pipeline.
Part I — Simple Chatbot
We’ll start by building a vanilla LLM-powered chatbot, and build up to more complex applications from there.
Functions
A TensorZero Function is an abstract mapping from input variables to output variables.
As you onboard to TensorZero, a function should replace each prompt in your system. At a high level, a function will template the inputs to generate a prompt, make an LLM inference call, and return the results. This mapping can be achieved with various choices of model, prompt, decoding strategy, and more; each such combination is called a variant, which we’ll discuss below.
For our simple chatbot, we’ll set up a function that maps the chat history to a new chat message.
We define functions in the tensorzero.toml
configuration file.
The configuration file is written in TOML, which is a simple configuration language.
The following configuration entry shows the skeleton of a function. A function has an arbitrary name, a type, and other fields that depend on the type.
TensorZero currently supports two types of functions: chat
functions, which match the typical chat interface you’d expect from an LLM API, and json
functions, which are optimized for generating structured outputs.
We’ll start with a chat
function for this example, and later we’ll see how to use json
functions.
A chat
function takes a chat message history and returns a chat message.
It doesn’t have any required fields (but many optional).
Let’s call our function mischievous_chatbot
and set its type to chat
.
We’ll ignore the optional fields for now.
To include these changes, our tensorzero.toml
file should include the following:
That’s all we need to do to define our function. Later on, we’ll add more advanced features to our functions, like schemas and templates, which unlock new capabilities for model optimization and observability. But we don’t need any of that to get started.
The implementation details of this function are defined in its variants
.
But before we can define a variant, we need to set up a model and a model provider.
Models and Model Providers
Before setting up your first TensorZero variant, you’ll need a model with a model provider. A model specifies a particular LLM (e.g. GPT-4o or your fine-tuned Llama 3), and model providers specify the different ways you can access a given model (e.g. GPT-4o is available through both OpenAI and Azure).
A model has an arbitrary name and a list of providers. Let’s start with a single provider for our model. A provider has an arbitrary name, a type, and other fields that depend on the provider type. The skeleton of a model and its provider looks like this:
For this example, we’ll use the GPT-4o mini model from OpenAI.
Let’s call our model my_gpt_4o_mini
and our provider my_openai_provider
with type openai
.
The only required field for the openai
provider is model_name
.
It’s a best practice to pin the model to a specific version to avoid breaking changes, so we’ll use gpt-4o-mini-2024-07-18
.
Once we’ve added these values, our tensorzero.toml
file should include the following:
Variants
Now that we have a model and a provider configured, we can create a variant for our mischievous_chatbot
function.
A variant is a particular implementation of a function. In practice, a variant might specify the particular model, prompt templates, a decoding strategy, hyperparameters, and other settings used for inference.
A variant’s definition includes an arbitrary name, a type, a weight, and other fields that depend on the type. The skeleton of a TensorZero variant looks like this:
We’ll call this variant gpt_4o_mini_variant
.
The simplest variant type
is chat_completion
, which is the typical chat completion format used by OpenAI and many other LLM providers.
The weight
field is used to determine the probability of this variant being chosen.
Since we only have one variant, we’ll give it a weight of 1.0
.
We’ll dive deeper into variant weights in a later section.
The only required field for a chat_completion
variant is model
.
This must be a model in the configuration file.
We’ll use the my_gpt_4o_mini
model we defined earlier.
After filling in the fields for this variant, our tensorzero.toml
file should include the following:
Inference API Requests
There’s a lot more to TensorZero than what we’ve covered so far, but this is everything we need to get started!
If you launch the TensorZero Gateway with this configuration file, the mischievous_chatbot
function will be available on the /inference
endpoint.
Let’s make a request to this endpoint.
Sample Output
Sample Output
That’s it! You’ve now made your first inference call with TensorZero.
But if that’s all you need, you probably don’t need TensorZero. So let’s make things more interesting.
Part II — Email Copilot
Next, let’s build an LLM-powered copilot for drafting emails. We’ll use this opportunity to show off more of TensorZero’s features.
Templates
In the previous example, we provided a system prompt on every request. Unless the system prompt completely changes between requests, this is not ideal for production applications. Instead, we can use a system template.
Using a template allows you to update the prompt without client-side changes. Later, we’ll see how to parametrize templates with schemas and run robust prompt experiments with multiple variants. In particular, setting up schemas will materially help you optimize your models robustly down the road.
Let’s start with a simple system template. For this example, the system template is static, so you won’t need a schema.
TensorZero uses MiniJinja for templating. Since we’re not using any variables, however, we don’t need any special syntax.
We’ll create the following template:
Schemas
The system template for this example is static, but often you’ll want to parametrize the prompts.
When you define a template with parameters, you need to define a corresponding JSON Schema. The schema defines the structure of the input for that prompt. With it, the gateway can validate the input before running the inference, and later, we’ll see how to use it for robust model optimization.
For our email copilot’s user prompt, we’ll want to parametrize the template with three string fields: recipient_name
, sender_name
, and email_purpose
.
We want all fields to be required and don’t want any additional fields.
With a schema in place, we can create a parameterized template.
Functions with Templates and Schemas
Let’s finally create our function and variant for the email copilot.
The configuration file is similar to previous example, but we’ve added a user_schema
field to the function and system_template
and user_template
fields to the variant.
You can use any file structure with TensorZero. We recommend the following structure to keep things organized:
Directoryfunctions/
Directorydraft_email/
Directorygpt_4o_mini_email_variant/
- system.minijinja
- user.minijinja
- user_schema.json
- tensorzero.toml
Restart your gateway using the new configuration file, and you’re ready to go!
Let’s make an inference request with our new function.
Now we don’t need to provide the system prompt every time, and the user message is a structured object instead of a free-form string.
Note that each inference returns an inference_id
and an episode_id
, which we’ll use later to associate feedback with inferences.
Sample Output
Sample Output
Why did we bother with all this?
Now you’re collecting structured inference data, which is incredibly valuable for observability and especially for optimization. For example, if you eventually decide to fine-tune your model, you’ll easily be able to counterfactually swap new prompts into your training data before fine-tuning, instead of being stuck with the prompts that were actually used at inference time.
Inference-Level Metrics
The TensorZero Gateway allows you to assign feedback to inferences or sequences of inferences by defining metrics. Metrics encapsulate the downstream outcomes of your LLM application, and drive the experimentation and optimization workflows in TensorZero.
This example covers metrics that apply to individual inference requests. Later, we’ll show how to define metrics that apply to sequences of inferences (which we call episodes).
The skeleton of a metric looks like the following configuration entry.
Let’s say we want to optimize for the number of email drafts that are accepted.
Let’s call this metric email_draft_accepted
.
We should use a metric of type boolean
to capture this behavior since we’re optimizing for a binary outcome: whether the email draft is accepted or not.
The metric applies to individual inference requests, so we’ll set level = "inference"
.
And finally, we’ll set optimize = "max"
because we want to maximize this metric.
Our metric configuration should look like this:
Feedback API Requests
As our application collects usage data, we can use the /feedback
endpoint to keep track of this metric.
Make sure to restart your gateway after adding the metric configuration.
Previously, we saw that every time you call /inference
, the Gateway will return an inference_id
field in the response.
You’ll want to substitute this inference_id
into the command below.
Sample Output
To submit feedback, you need to provide the inference_id
from the inference response.
Sample Output
Over time, we’ll collect the perfect dataset for observability and optimization. We’ll have structured data on every inference, as well as their associated feedback. The TensorZero Recipes leverage this data for prompt and model optimization.
Experimentation
So far, we’ve only used one variant of our function. In practice, you’ll want to experiment with different configurations — for example, different prompts, models, and parameters.
TensorZero makes this easy with built-in experimentation features. You can define multiple variants of a function, and the gateway will sample from them at inference time.
For this example, let’s set up a variant that uses Anthropic’s Claude 3 Haiku instead of GPT-4o Mini.
Additionally, this variant will introduce a custom temperature
parameter to control the creativity of the AI assistant.
Let’s start by adding a new model and provider.
Finally, create a new variant using this model, and update the weight of the previous variant to match.
You could also experiment with different prompt templates, but for this example we’ll keep things simple and just copy the previous templates over.
Once you’re done, your file tree should look like this:
Directoryfunctions/
Directorydraft_email/
Directorygpt_4o_mini_email_variant/
- system.minijinja
- user.minijinja
Directoryhaiku_3_email_variant/
- system.minijinja copy from above
- user.minijinja copy from above
- user_schema.json
- …
- tensorzero.toml
That’s it!
After restarting the gateway, you can make some inference requests to see the results. The gateway will sample between the two variants based on the configured weights.
Part III — Weather RAG
The next example introduces tool use into the mix.
In particular, we’ll show how to use TensorZero in a RAG (retrieval-augmented generation) system. TensorZero doesn’t handle the indexing and retrieval directly, but can help with auxiliary generative tasks like query and response generation.
For this example, we’ll illustrate a RAG system with a simple weather tool.
We’ll introduce a function for query generation (generate_weather_query
), and another for response generation (generate_weather_report
).
The former will leverage tool (get_temperature
) use to generate a weather query.
Here we mock the weather API, but it’ll be easy to see how diverse RAG workflows can be integrated.
Tools
TensorZero has first-class support for tools. You can define a tool in your configuration file, and attach it to a function that should be allowed to call it.
Let’s start by defining a tool. A tool has a name, a description, and a set of parameters (described with a JSON schema). The skeleton of a tool configuration looks like this:
Let’s create a tool for a fictional weather API that takes a location (and optionally units) and returns the current temperature.
The parameters for this tool are defined as a JSON schema.
We’ll need two parameters: location
(string) and units
(enum with values fahrenheit
and celsius
).
Only location
is required, no additional properties should be allowed.
Finally, we’ll add descriptions for each parameter and tool itself — this is very important to increase the quality of tool use!
The file organization is up to you, but we recommend the following structure:
Directoryfunctions/
- …
Directorytools/
- get_temperature.json
- tensorzero.toml
Functions with Tool Use
We can now create our two functions. The query generation function will use the tool we just defined, and the response generation function will be similar to our previous examples.
Let’s define the functions, their variants, and any associated templates and schemas.
When the model requests a tool call, the response will include a tool_call
content block.
These content blocks have the fields arguments
, name
, raw_arguments
, and raw_name
.
The first two fields are validated against the tool’s configuration (or null
if invalid).
The last two fields contain the raw values passed received from the model.
Episodes
Before we make any inference requests, we must introduce one more concept: episodes.
An episode is a sequence of inferences associated with a common downstream outcome.
For example, an episode could refer to a sequence of LLM calls associated with:
- Resolving a support ticket
- Preparing an insurance claim
- Completing a phone call
- Extracting data from a document
- Drafting an email
An episode will include one or more functions, and sometimes multiple calls to the same function. Your application can run arbitrary actions (e.g. interact with users, retrieve documents, actuate robotics) between function calls within an episode. Though these are outside the scope of TensorZero, it is fine (and encouraged) to build your LLM systems this way.
Episodes allow you to group sequences of inferences in multi-step LLM workflows, apply feedback to these sequences as a whole, and optimize your workflows end-to-end. During an episode, multiple calls to the same function will receive the same variant (unless fallbacks are necessary).
In the case of our weather RAG, both query generation and response generation contribute to what we ultimately care about: the weather report. So we’ll want to associate each weather report with the inferences that led to it. The workflow of generating a weather report will be our episode.
The /inference
endpoint accepts an optional episode_id
field.
When you make the first inference request, you don’t have to provide an episode_id
.
The gateway will create a new episode for you and return the episode_id
in the response.
When you make the second inference request, you must provide the episode_id
you received in the first response.
The gateway will use the episode_id
to associate the two inference requests together.
Sample Output
The first inference request doesn’t require an episode_id
.
The response will contain a new episode_id
that we’ll use in the second request.
Sample Output
The second inference request requires the episode_id
you received in the first response.
Sample Output
Episode-Level Metrics
The primary use case for episodes is to enable episode-level metrics. In the previous example, we assigned feedback to individual inferences. TensorZero can also collect episode-level feedback, which can be useful for optimizing entire workflows.
To collect episode-level feedback, we need to define a metric with level = "episode"
.
Let’s add a metric for the weather RAG example.
We’ll use the user_rating
as the metric name, and we’ll collect it as a float.
Making a feedback request with an episode_id
instead of an inference_id
associates the feedback with the entire episode.
Sample Output
Set the episode_id
to the one returned in the inference response.
The value for a float metric can be any numeric type, but will be coerced to float under the hood.
Sample Output
That’s all you need for our weather RAG example. This is clearly a toy example, but it illustrates the core concepts of TensorZero. You can replace the mock weather API with a real API call — or if were searching over documents instead, anything from BM25 to cutting-edge vector search.
Part IV — Email Data Extraction
JSON Functions
Everything we’ve done so far has been with Chat Functions.
TensorZero also supports JSON Functions for use cases that require structured outputs. The input is the same, but the function returns a JSON value instead of a chat message.
Let’s create a JSON function that extracts an email address from a user’s message.
The setup is very similar to our previous examples, except that the function is defined with type = "json"
and requires an output_schema
.
Let’s start by defining the schema, a static system template, and the rest of the configuration.
Once you’ve set it up, your file tree should look like this:
Directoryfunctions/
- …
Directoryextract_email/
Directorysimple_variant/
- system.minijinja
- user.minijinja
- output_schema.json
- …
- …
- tensorzero.toml
Finally, let’s make an inference request.
The request format is very similar to chat functions, but the response will contain an output
field instead of a content
field.
The output
field will be a JSON object with the fields parsed
and raw
.
The parsed
field is the parsed output as a valid JSON that fits your schema (null
if the model didn’t generate a JSON that matches your schema), and the raw
field is the raw output from the model as a string.
Sample Output
Sample Output
Conclusion
This tutorial only scratches the surface of what you can do with TensorZero.
TensorZero especially shines when it comes to optimizing complex LLM workflows using the data collected by the gateway. For example, the structured data collected by the gateway can be used to better fine-tune models compared to using historical prompts and generations alone.
We are working on a series of examples covering the entire “data flywheel in a box” that TensorZero provides. Here are some of our favorites:
- Writing Haikus to Satisfy a Judge with Hidden Preferences
- Improving Data Extraction (NER) by Fine-Tuning a Llama 3 Model
- Improving Data Extraction (NER) with Dynamic In-Context Learning
- Improving LLM Chess Ability with Best-of-N Sampling
- Improving Math Reasoning with a Custom Recipe for Automated Prompt Engineering (DSPy)
You can also dive deeper into the Configuration Reference and the API Reference.
We’re excited to see what you build! Please share your projects, ideas, and feedback with us on Slack or Discord.