Imaginary Programming: Working with Large Language Models
Preamble and Acknowledgements
In Computer Science, we stand on the shoulders of giants – the internet is a series of tubes, computers are turtles all the way down.
Everything anybody can do in technology is, by definition, derivative. This article is no exception.
Programming is not new, Ada Lovelace did it first. Imaginary Programming was coined by Shane Mulligan in his 2021 EmacsConf talk.
My aim with this line of thought and questioning is not to reinvent the wheel, but instead to try and map the amorphous nature of LLMs onto existing terms and operations which Computer Scientists and Software Engineers alike find familiar. So many people have solved problems like this in the past, and the people that come after will undoubtedly solve them better.
Beware the temptation to dunk on those who do not know what they don’t know. The innovation curve we’re currently on in AI development is unlike one we’ve seen since the dawn of the internet, and there will be thousands of people onboarding daily.
Imaginary Programming has been around for a while as a concept, but if it’s news to you, welcome for you are one of today’s lucky 10,000.
Introduction
Large Language Models (LLMs) are a new and powerful tool for engineers that add truly limitless value to their workflow. LLMs like GPT3 and Codex speed up my workflow immensely, and enable me to force-multiply myself – I am a true LLM maxi.
That said, it’s easy to discount LLMs as a novelty due to a lack of understanding of their properties. Many engineers struggle to grasp the full potential of LLMs, and some may even find them intimidating.
LLMs have already demonstrated their ability to perform an impressive array of tasks, such as writing poetry, authoring code, and inventing novel combinations of insults and swear words. However, the challenge for programmers is understanding how they can harness this power in a practical way. Due to the emergent behavior of these neural networks, it’s completely opaque to the average engineer how to figure out what LLMs do, and how they can be used effectively. As a result, it’s easy to dismiss them as just another fad amidst the AGI hysteria.
In this article, we will explore how LLMs can be used effectively in programming by introducing a new approach called “imaginary programming.” I will explain the benefits and limitations of LLMs, and provide examples of how one can imagine their way into a working prototype. By the end of this article, you will have a better understanding of how LLMs can be harnessed to create innovative and powerful products quickly.
Problems with Prompts
One of the main challenges that programmers face when working with LLMs is crafting effective prompts. Prompts can be analogized as the instructions given to the language model that specify the task to be performed. However, outputs don’t necessarily get better with more words, what matters most is how you write them.
When speaking or writing, humans tend to describe desired outcomes, rather than the algorithmic processes required to achieve them, due to assumed shared cognitive schemas. This is at odds with a Transformer model’s training and means that programmers may struggle to create prompts that effectively communicate the task at hand to the model in a way it understands.
It’s not enough to simply describe the desired outcome (for now); the prompt must instead include examples or context and information that biases the model’s output towards the desired result. Large Language Model performance can vary widely based on the quality of the prompt – poor prompt design can result in inaccurate or nonsensical output, making it difficult to rely on the model for important tasks.
To overcome these challenges, programmers must learn to write prompts that succinctly communicate the necessary information to the model. This requires:
an understanding of the underlying algorithmic processes that the model uses to generate text
a creative approach to designing prompts that can elicit accurate and useful output
Simplicity Begets Complexity
LLMs such as GPT-3 are trained on collections of simple tasks, which means that they can perform well when given clear and simple instructions. As I mentioned previously, asking them to perform complex tasks results in inconsistent and undesired behavior.
Instead, prompt engineers must break down complex goals into simpler units and then chain them together to achieve the desired outcome. Note, that this is only required for now, as once LLMs become more sophisticated and incorporate ever larger neural networks this might no longer be a limitation.
A common operation in LLM-land is to use the latent knowledge of the model to make up text that fits a particular use-case. More often than not, we’d like to have this text returned to us in a structured format that is compatible with the programming language we’re using to wire the prompts together.
New Vocabulary Needed
As was mentioned previously, expecting the model to both be creative and produce structured output is at odds with its training (definitely go back and read that paper if you want to know more).
To help programmers think about prompts in this way, we can use the terms “generator” and “extractor” (ever so gracefully coined by yours truly) to describe simple atomic units of this larger task.
Generators
A generator creates unstructured information, such as writing a story. Increasing input temperature allows the model to produce less-probable output, which, when interpreted by humans is roughly analagous to creativity.
Output from this generator prompt might look like:
Extractors
An extractor then formats that unstructured text into a format that is machine-parsable, such as JSON.
This extractor might output a result like the following when ran against the previous generated output:
By thinking about prompts in this way, we can avoid parsing errors that are inherent in the generation step and build a model that allows one to simplify complex goals into simpler tasks that the LLM can more easily understand and accomplish. This opens us up to then chain together these simpler units to achieve this more complex behavior – sounds like programming, huh?
To facilitate this process, there are several prompt chaining libraries available, such as Langchain and LLamaIndex, which allow us to easily chain together different prompts and get this complex behavior we’re searching for. However, it’s important to keep in mind that even with these tools, creating effective prompts can still be a challenge. It requires creativity, knowledge of the underlying algorithmic processes, and a deep understanding of the task at hand.
Problematic Python
While Python is a popular language for machine learning and AI development, it can be problematic when working with LLMs. One of the main issues is that Python doesn’t, by default, protect developers from making mistakes pre-runtime. This can lead to bugs and errors that can be difficult to track down and result in pure headache. If you don’t take extreme care at the outset, as code complexity grows the time-to-debug also grows linearly.
Given the previously-stated issues with prompt engineering as it stands currently, LLM system builders run into the same kinds of problems normal engineers do. You end up with print statements all over and have to keep the entire application’s architecture in your head to infer where problems with the data model might be occuring.
Though Python 3 has a type system available, I personally find it cumbersome. Some might find this acceptable, but considering a situation where one is building a new system from scratch quickly, careful selection of your underlying programming language that biases towards development ease and velocity is incredibly prudent.
Typescript is “The Way”
Typescript is becoming an increasingly popular choice for machine learning and AI application development due to its strong type system and ability to catch errors at compile-time. This can be especially useful when working with LLMs, as it can help to catch errors in the wiring between prompts and ensure that the input and output from those prompts are properly formatted.
One of the challenges of working with LLMs in Typescript, however, is the difficulty in strongly typing prompt parameters and returns without a cohesive interface or abstraction. This is where libraries such as Promptable.js (the typescript companion library for Promptable.io) come in.
Promptable.js provides a set of abstractions for working with complex prompts that take multiple keyword arguments, making it significantly easier to manage changes to prompts and chain together the different atomic units of a larger, more complex task. Promptable’s templates are a huge step in the right direction, adding string keyword arguments to the prompts which allow them to be dynamically formatted at run-time and drastically improves an individual prompt’s modularity and reusability.
Here’s how one might implement the aforementioned prompt-primitives in Promptable.js:
Generator
By making some slight modifications to our original prompt, we now can tune the length of the generation programmatically – this is great! Now users have the ability to take user input and produce text that fits a wider variety of situations with a single prompt.
Extractor
As with the Generator, putting the Extractor prompt into the Promptable format allows for a more generally flexible LLM prompt which can take arbitrary content and a return type at runtime.
Now, you can simply wire the two prompts together like so:
I’ll omit the output this time around, considering the prompts are functionally identical, but I hope it’s clear to the reader that this is a drastically improves usability and composability of prompts.
Super Neat!!
It’s worth noting, that though this is a huge improvement, we’re not fully leveraging the type system here. Given the early state of the library, what it currently lacks is a feature that utilizes strong argument and return types. That said, we should cut Promptable.js some slack, it’s only been out for a month and the team is tirelessly adding new features daily! If you want more, check out their Twitter and let them know!
Despite this slight limitation, by using Typescript and libraries like Promptable, developers can more easily manage the complexity of LLMs and create more robust and error-free applications.
Imaginary Programming
Given the set of problems and constraints I have described thus far, it follows that we need an abstraction that allows prompt engineering to mesh more cleanly with existing software engineering workflows and practices. As is becoming the norm in my LLM-app builder journey, as soon as I begin pontificating on a cool idea, someone inevitably releases a project that executes on it.
Enter: Imaginary Programming – a name for the concept I have been thinking about all this time!
Literally last week, Sasha Aikin released his Imaginary.dev preview which implements a much more functional approach to prompt engineering and synthesis. The code is great, check out the repository here.
From the Imaginary.dev docs:
Imaginary programming is an approach to prompt development that leverages the power of LLMs and the simplicity of natural language to create functional prompts which slot in nicely to existing deterministic code. With imaginary programming, developers can simply describe the task they want to perform in natural language, and the LLM takes care of the rest.
The Imaginary.dev system consists of two parts:
A Typescript compiler plugin, which allows developers to simply write a function signature and a doc string that describes their desired functionality which is transformed at compile-time to code which manipulates the companion runtime library
A runtime library, which implements OpenAI API calls and permissive fuzzy JSON-parsing logic
The plugin, when combined with the Imaginary runtime library, leverages the native compilation step in Typescript to take care of writing the OpenAI generation boilerplate, parsing, and type-checking of inputs and outputs. This makes it incredibly easy to create prompts that can be composed together functionally and reduces the likelihood of runtime errors in glue-code.
One of the benefits of imaginary programming is that it allows developers to quickly build out the core business logic for their products, and worry less about the weird edge cases of prompt engineering as it currently stands. However, it’s important to remember that the quality of the output is still dependent on the quality of the prompt. Developers must still exercise creativity and a deep understanding of the task at hand in order to create effective prompts.
With imaginary programming and libraries like Imaginary.dev, developers can more easily harness the power of LLMs and create innovative and powerful applications with minimal effort.
Try it for yourself!
To illustrate how the Imaginary.dev system works, let’s re-write the previous prompts again:
Generator
Extractor
Now that the prompt, OpenAI code, input types, and output parsing are all abstracted away by the compile-time transformation, it’s much easier to reason about the prompts in a functional way. The glue code ends up being much simpler, the compiler and linter catch most simple mistakes, and all the LLM code is type-safe and documented.
With imaginary programming, you can quickly and easily build out complex applications without worrying about the underlying algorithmic processes. By simply describing the desired outcome in natural language, the LLM can take care of the rest.
Conclusions
Large Language Models like GPT-3 are powerful tools for natural language processing and have the potential to revolutionize the way we approach programming. However, the complexity of working with LLMs can be daunting, especially for developers who are new to the field.
Imaginary programming provides a new approach to working with LLMs, allowing developers to describe the discrete steps in a workflow in natural language and let the LLM take care of the rest. This approach can simplify the programming process and reduce the likelihood of errors.
To effectively use imaginary programming, developers must exercise creativity and a deep understanding of the task at hand to create effective prompts. However, with the right tools and approach, developers can harness the power of LLMs to create innovative and powerful applications with minimal effort.
As LLM technology continues to evolve, we can expect to see even more exciting developments in the field of imaginary programming and natural language processing.
Where Do We Go From Here?
I have no hard and fast answers, for I am just a meat-LLM, stochastically completing text. This ideation is left up to you, dear reader. Though, if you desire prompts to get your thinker thinking, here’s a couple:
Go Build a ChatGPT Clone with Promptable
Colin is a great human, and is building some really solid tools and workflows for dealing with LLMs in Typescript. I highly recommend you pick up the library he’s building and try it out. If it doesn’t quite work for you, drop by the Discord and let them know! Human Feedback Reinforcement Learning works wonders on open-source projects.
Use the NEW ChatGPT API in Typescript with @PromptableAI Usage ->System: “You're a helpful assistant:”User:💬Assistant:💬User: 💬Repo: https://t.co/1h28uQmQ3DExample: pic.twitter.com/P33wh8LiUe
— Colin (@colinfortuner) March 1, 2023
Build Out a Type-Safe LLM App Backend with Cognosis
The homie Matt Busigin has been thinking about type-safe LLM programming for a hot minute, his CognosisAI project presents a pretty solid example of building a production-ready, fault-tolerant application backend in Typescript.
Take your LLM Projects to the next level at the outset! (also go read his feed, the ideas you’re having right now have probably been thought before)
Check Out Some of My Other Favorites
There’s so much other cool stuff out there. Before getting started, do some research – you will find your level-up process will be sped up if you put the firehost of LLM content to your lips and suck. Otherwise, you will likely run into things grad students can do in their sleep already.
I am coming out of a week-long, 20h/day fugue state.All my toy apps now rewritten in TS + @PromptableAI, and I am approaching optimization.I am now asking you, dear reader, for links relating to prior art regarding LLM prompt-based extraction from context.My faves in 🧵 : pic.twitter.com/BBxvSSySOX
— Helping Robots Solve Problems (@YourBuddyConner) February 22, 2023
Please reach out if you find this useful and learned something. My brain-LLM benefits from human feedback, and it loves interacting with today’s 10,000.