Prompt engineering in .NET

In this article, you explore essential prompt engineering concepts. Many AI models are prompt-based, meaning they respond to user input text (a prompt) with a response generated by predictive algorithms (a completion). Newer models also often support completions in chat form, with messages based on roles (system, user, assistant) and chat history to preserve conversations.

Work with prompts

Consider this text generation example where prompt is the user input and completion is the model output:

Prompt: "The president who served the shortest term was "

Completion: "Pedro Lascurain."

The completion appears correct, but what if your app is supposed to help U.S. history students? Pedro Lascurain's 45-minute term is the shortest term for any president, but he served as the president of Mexico. The U.S. history students are probably looking for "William Henry Harrison". Clearly, the app could be more helpful to its intended users if you gave it some context.

Prompt engineering adds context to the prompt by providing instructions, examples, and cues to help the model produce better completions.

Models that support text generation often don't require any specific format, but you should organize your prompts so it's clear what's an instruction and what's an example. Models that support chat-based apps use three roles to organize completions: a system role that controls the chat, a user role to represent user input, and an assistant role for responding to users. Divide your prompts into messages for each role:

  • System messages give the model instructions about the assistant. A prompt can have only one system message, and it must be the first message.
  • User messages include prompts from the user and show examples, historical prompts, or contain instructions for the assistant. An example chat completion must have at least one user message.
  • Assistant messages show example or historical completions, and must contain a response to the preceding user message. Assistant messages aren't required, but if you include one it must be paired with a user message to form an example.

Use instructions to improve the completion

An instruction is text that tells the model how to respond. An instruction can be a directive or an imperative:

  • Directives tell the model how to behave, but aren't simple commands—think character setup for an improv actor: "You're helping students learn about U.S. history, so talk about the U.S. unless they specifically ask about other countries."
  • Imperatives are unambiguous commands for the model to follow. "Translate to Tagalog:"

Directives are more open-ended and flexible than imperatives:

  • You can combine several directives in one instruction.
  • Instructions usually work better when you use them with examples. However, because imperatives are unambiguous commands, models don't need examples to understand them (though you might use an example to show the model how to format responses). Because a directive doesn't tell the model exactly what to do, each example can help the model work better.
  • It's usually better to break down a difficult instruction into a series of steps, which you can do with a sequence of directives. You should also tell the model to output the result of each step, so that you can easily make granular adjustments. Although you can break down the instruction into steps yourself, it's easier to just tell the model to do it, and to output the result of each step. This approach is called chain of thought prompting.

Primary and supporting content add context

You can provide content to add more context to instructions.

Primary content is text that you want the model to process with an instruction. Whatever action the instruction entails, the model will perform it on the primary content to produce a completion.

Supporting content is text that you refer to in an instruction, but which isn't the target of the instruction. The model uses the supporting content to complete the instruction, which means that supporting content also appears in completions, typically as some kind of structure (such as in headings or column labels).

Use labels with your instructional content to help the model figure out how to use it with the instruction. Don't worry too much about precision—labels don't have to match instructions exactly because the model will handle things like word form and capitalization.

Suppose you use the instruction "Summarize US Presidential accomplishments" to produce a list. The model might organize and order it in any number of ways. But what if you want the list to group the accomplishments by a specific set of categories? Use supporting content to add that information to the instruction.

Adjust your instruction so the model groups by category, and append supporting content that specifies those categories:

prompt = """
Instructions: Summarize US Presidential accomplishments, grouped by category.
Categories: Domestic Policy, US Economy, Foreign Affairs, Space Exploration.
Accomplishments: 'George Washington
- First president of the United States.
- First president to have been a military veteran.
- First president to be elected to a second term in office.
- Received votes from every presidential elector in an election.
- Filled the entire body of the United States federal judges; including the Supreme Court.
- First president to be declared an honorary citizen of a foreign country, and an honorary citizen of France.
John Adams ...' ///Text truncated
""";

Use examples to guide the model

An example is text that shows the model how to respond by providing sample user input and model output. The model uses examples to infer what to include in completions. Examples can come either before or after the instructions in an engineered prompt, but the two shouldn't be interspersed.

An example starts with a prompt and can optionally include a completion. A completion in an example doesn't have to include the verbatim response—it might just contain a formatted word, the first bullet in an unordered list, or something similar to indicate how each completion should start.

Examples are classified as zero-shot learning or few-shot learning based on whether they contain verbatim completions.

  • Zero-shot learning examples include a prompt with no verbatim completion. This approach tests a model's responses without giving it example data output. Zero-shot prompts can have completions that include cues, such as indicating the model should output an ordered list by including "1." as the completion.
  • Few-shot learning examples include several pairs of prompts with verbatim completions. Few-shot learning can change the model's behavior by adding to its existing knowledge.

Understand cues

A cue is text that conveys the desired structure or format of output. Like an instruction, a cue isn't processed by the model as if it were user input. Like an example, a cue shows the model what you want instead of telling it what to do. You can add as many cues as you want, so you can iterate to get the result you want. Cues are used with an instruction or an example and should be at the end of the prompt.

Suppose you use an instruction to tell the model to produce a list of presidential accomplishments by category, along with supporting content that tells the model what categories to use. You decide that you want the model to produce a nested list with all caps for categories, with each president's accomplishments in each category listed on one line that begins with their name, with presidents listed chronologically. After your instruction and supporting content, you could add three cues to show the model how to structure and format the list:

prompt = """
Instructions: Summarize US Presidential accomplishments, grouped by category.
Categories: Domestic Policy, US Economy, Foreign Affairs, Space Exploration.
Accomplishments: George Washington
First president of the United States.
First president to have been a military veteran.
First president to be elected to a second term in office.
First president to receive votes from every presidential elector in an election.
First president to fill the entire body of the United States federal judges; including the Supreme Court.
First president to be declared an honorary citizen of a foreign country, and an honorary citizen of France.
John Adams ...  /// Text truncated

DOMESTIC POLICY
- George Washington: 
- John Adams:
""";
  • DOMESTIC POLICY shows the model that you want it to start each group with the category in all caps.
  • - George Washington: shows the model to start each section with George Washington's accomplishments listed on one line.
  • - John Adams: shows the model that it should list remaining presidents in chronological order.

Example prompt using .NET

.NET provides various tools to prompt and chat with different AI models. Use Semantic Kernel to connect to a wide variety of AI models and services, as well as other SDKs such as the official OpenAI .NET library. Semantic Kernel includes tools to create prompts with different roles and maintain chat history, as well as many other features.

Consider the following code example:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

// Create a kernel with OpenAI chat completion
#pragma warning disable SKEXP0010
Kernel kernel = Kernel.CreateBuilder()
                    .AddOpenAIChatCompletion(
                        modelId: "phi3:mini",
                        endpoint: new Uri("http://localhost:11434"),
                        apiKey: "")
                    .Build();

var aiChatService = kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = new ChatHistory();
chatHistory.Add(
    new ChatMessageContent(AuthorRole.System, "You are a helpful AI Assistant."));

while (true)
{
    // Get user prompt and add to chat history
    Console.WriteLine("Your prompt:");
    chatHistory.Add(new ChatMessageContent(AuthorRole.User, Console.ReadLine()));

    // Stream the AI response and add to chat history
    Console.WriteLine("AI Response:");
    var response = "";
    await foreach (var item in
        aiChatService.GetStreamingChatMessageContentsAsync(chatHistory))
    {
        Console.Write(item.Content);
        response += item.Content;
    }
    chatHistory.Add(new ChatMessageContent(AuthorRole.Assistant, response));
    Console.WriteLine();
}

The preceding code provides examples of the following concepts:

  • Creates a chat history service to prompt the AI model for completions based on author roles.
  • Configures the AI with an AuthorRole.System message.
  • Accepts user input to allow for different types of prompts in the context of an AuthorRole.User.
  • Asynchronously streams the completion from the AI to provide a dynamic chat experience.

Extend your prompt engineering techniques

You can also increase the power of your prompts with more advanced prompt engineering techniques that are covered in depth in their own articles.

  • LLMs have token input limits that constrain the amount of text you can fit in a prompt. Use embeddings and vector database solutions to reduce the number of tokens you need to represent a given piece of text.
  • LLMs aren't trained on your data unless you train them yourself, which can be costly and time-consuming. Use retrieval augmented generation (RAG) to make your data available to an LLM without training it.