William Bowman: First class Prompt Engineering with llm lang! (This is a bad idea.)

Source

Let me warn you, this is more of a performance than a talk. This is a story, a science fiction, in which I explore my own beliefs and understanding. Are there any AI enthusiasts in the audience? It's okay.

Fiction in which I explore my own beliefs and understanding. Are there any AI enthusiasts in the audience? It's okay; it's a safe space. Okay, great. Great, great.

The contents of this talk are for you. Are there any AI skeptics in the audience? Great, me too. The delivery of this talk is for you. So, I'm an AI skeptic, but I'm also a higher-order skeptic.

I'm skeptical about my own skepticism. So I'm not sure AI is bad, although I think it is. For the rest of this talk, though, I'm going to be an AI enthusiast. Okay, let's get started.

As we all know, software engineering is dead. It's going to be replaced by generative AI and prompt engineering, right? In the future, we won't need programmers. Instead, we will just need people who can describe precisely to a computer what they want it to do—with prompts, with prompts, yeah.

So, what is prompt engineering, you may ask. I also wondered this, so I looked into what engineering is, anyway. Engineering is the practice of applying natural science, mathematics, and design processes. Right, so it's that, but specifically for prompts.

So, let me show you. Let's give a demo of the state-of-the-art tools for software engineering and see how this works in practice. Okay, let’s see here. Here is ChatGPT, which doesn't seem to be working, but that could be due to live demo issues. Okay, there we go.

Sure! Here’s a reformatted version of your text: --- Okay. I'm going to have ChatGPT help me with my demo here, so I'm going to ask it to write a script that will launch Safari, navigate to chatgpt.com, and enter the prompt: "Write a tail recursive implementation of factorial in Racket." Then I'll copy and paste the result into DrRacket. Let's see what it gives me. Oh, perfect.

Perfect. It gives me an AppleScript, which is convenient because I am running a Mac. It must have inferred that because it's extremely smart and clever. I'm going to take this AppleScript, copy it, and paste it into the Script Editor. If I can, where's my mouse? Am I to the left or the right?

Here you can see the script editor. Let's run it and see what happens. Okay, it's navigated to ChatGPT; that's good. I read a story recently about somebody who gave ChatGPT sudo access, so I'm not going to do that. But I can assure you, because I tested this three times, this script won't actually work.

Okay, let me return to my presentation. I did write a lot of code, and while it is close to working, it unfortunately doesn't function as intended. As we've just seen, here is the prompt engineering design process. Remember, it's engineering, and we have a process. Here’s the process.

I chat with a bot, I copy and paste some code. It may or may not work. I chat with the bot to refine it, etc.

In natural language, right? Just keep doing this until we have engineered something. Okay, so as a definitely real, serious engineering discipline, this process should be able to help a prompt engineer like me answer some questions. For instance, how do we measure, control, or predict the cost of a prompt? Prompts are expensive; I have to pay for them.

Every time I send a request to ChatGPT, it charges me by the token. How do I abstract over prompts? If I want to run this prompt multiple times, do I have to keep copying and pasting and putting things in the browser? If I want inputs to my prompt, how do I deal with that? If I want to integrate the output, do I have to copy and paste from the browser?

This is a terrible process, right? How do I deal with the non-determinism? Like, I'm pretty sure that script won't work because I've tried it three times, but maybe this time it will work; I don't know. So how do I engineer a prompt rather than just write one? Thankfully, of course, there are answers to these questions, so we just need to dive a little deeper.

We use the API instead of a chatbot. So instead of writing a prompt, we write a program in a programming language, like Python or JavaScript. Then we have all the power and abstractions of real programming, and we can write software. This is not the future I was promised. I don't have software anymore; I can just write prompts.

What? You should get 10 DBT's and write the script to pop in check. That's a good idea. I did try that, though. It just didn't work.

Okay, so thankfully, I know how to write languages in Racket. So maybe it's just that the tools I want as a prompt engineer don't exist yet. For some reason, all of the AI people don't know how to think about languages. It takes a programming languages person to come up with prompt engineering. If I want to engineer prompts, let's start building a new language where the fundamental abstraction is prompts.

And I'm going to give you a little spoiler of my takeaway slide. So LLM-lang will help us with the following: it will allow us to measure and control the cost of prompts to some extent. It will assist us in computing over prompts. Additionally, it will treat prompts as a first-class element that I can program with, rather than just copying and pasting them around, blah, blah, blah.

It will answer these parts of my questions. As a first-class tool, I can program with it rather than copy and paste around endlessly. I still have no idea how to predict the cost of prompts. There is some research on that which I have yet to identify, and I also don't know how to handle the non-determinism.

That's just, I don't know. You just keep interacting with it as a chatbot, I guess. Okay, so that's the spoiler. So, prompt engineering in LLM language. The prompt is the fundamental abstraction.

What is a prompt? Does anyone know what a prompt is? Is it that? Are you asking?

It's me asking, "What's that?" You're asking. It's me asking you. Yeah, that's it. It's a limit. It's a limit.

You. It's a good joke. It's a good joke about delimited continuations. Okay, so here's ChatGPT. Okay, here's ChatGPT.

Here is a prompt, right? Write a factorial in, oops, in Racket. Okay, so what did I just do? So, write a factorial in, oops, in Racket. Okay, so what did I just do?

I wrote completely unstructured text and sent it to the LLM, right? It did a pretty good job and gave me an explanation. So what I don't want is to start writing double quotes and then entering texts that I may or may not have to escape. I don't want strings. Strings are not prompts.

Prompts are just unstructured text that I write in natural language and send. So that's the main criteria for what a prompt is. All the other languages and APIs in JavaScript and Python require you to write strings, not prompts.

So, a prompt is unstructured text that is collected and sent to the LLM. How can I do that in a language, specifically in Racket? Well, thankfully, there are lots of people who have done similar things in Racket before. So, let's see if this works. I want the ability to write unstructured text, so I'm going with that; it's mostly unstructured, adding expressions.

Certainly! Here’s a summary of the limitations of large language models (LLMs) in software engineering: 1. **Context Understanding**: LLMs may struggle with understanding intricate project contexts or specific technical requirements, leading to inadequate solutions. 2. **Code Quality**: Generated code can be syntactically correct but may lack best practices, security considerations, or performance optimizations. 3. **Error Handling**: LLMs might not accurately predict or handle errors in code, which can result in runtime issues. 4. **Version Control**: They do not inherently manage version control, making it difficult to maintain or collaborate on code effectively. 5. **Up-to-Date Knowledge**: LLMs may not have the latest information on programming languages, frameworks, or libraries due to their training cut-off, causing them to provide outdated solutions. 6. **Debugging**: While LLMs can suggest fixes, they often lack the deep reasoning required to effectively debug complex issues. 7. **Limited Testing**: Generated code often requires additional validation and testing to ensure reliability and functionality. 8. **Deployment Knowledge**: LLMs might not provide complete guidance on deployment processes or environment-specific considerations. Overall, while LLMs can assist in software engineering, their limitations necessitate human oversight and critical judgment.

Precise. Scientific. Okay, let's run that and see what happens. So I just start writing my prompt just like I was writing it in ChatGPT, and I hit run, and I get something.

Let's see here. We've got some bias and ethical issues. Yes, that's probably true. I received interactive feedback. Is that a limitation?

I'm not sure that's a limitation, but okay. Scalability, safety and security, lack of reason. Okay, great. These are great. I'm going to use these in my talk coming up at RacketCon.

Sure! Here’s the reformatted text for your slideshow: --- **Slide 1: Introduction to Abstraction** - Overview of our first abstraction in [insert topic or acronym here]. --- **Slide 2: Working with Unstructured Text** - We have prompts that utilize unstructured text. --- **Slide 3: Interaction with Unstructured Text** - Ability to interact with unstructured text in my files. - Ability to interact with unstructured text in the REPL (Read-Eval-Print Loop). --- Feel free to adjust any part or let me know if you need additional slides!

Sure, here's the reformatted text with correct capitalization and spelling: "Let me do that. Okay, my second question is: well, prompts are my abstraction, so what operations do or should prompts support? Well, okay, these are the ones I have: append to the current prompt. As I'm writing, do or should prompts support..."

Well, okay, these are the ones I have. Append to the current prompt. As I'm writing, everything should be collected until it is somehow sent to the LLM. Right now, at the very end of every module, it can be sent. Also, I—well, oh, that was my second point—send to the LLM, right?

So after I've collected everything into a prompt, I want to send it to the LLM. Right now, that happens at the end of every module, every time you hit enter in the REPL, or there's an explicit call to prompt for other things. Additionally, I want to be able to compute part of my prompt. I want the ability to unquote out of this unstructured text and splice in some data. I also want to be able to capture the output of the prompt when sending it and perform computations with that as well.

Here are the operations I have so far. I may want more, but I'm not entirely sure. So, let's see how these work in Racket right now, or in LLMLing right now. I think it's this one, right? Okay, this example is taken from a Gen AI script, which inspired LLMLing.

This is a JavaScript library by Microsoft, and they provide us with an example where, suppose you are tired of writing changes with some options to hide irrelevant stuff. You can simply get the commits and splice that into this prompt as a string. I also want to provide all of the diffs since this is between versions 8.9 and 8.10, I believe. It will also retrieve the diff of all the code. Let's see what the commit history looks like.

Okay, that was really fast. That's because, by default, the LLM caches every product. I can even run sort of arbitrary computation, right? It can go off to the system to compute part of the values of my prompt.

Here are some notes about some limitations. Don't worry about those. Okay, so that's my second ability for prompts. The third ability is, well, I'm now working in Racket, and I have two kinds of values, right? I have my prompts.

Those get sent to the LLM. But obviously, values are also being returned to Racket somehow. So I have to work with these prompts, which are all sent to the LLM, so there's something like quasi-quotation going on. At the top level, every expression is wrapped in this `p` quasi-quote, which acts a lot like quasi-quote but for strings. It follows the following kinds of equations.

So there's an unprompt somewhere. Only when you unprompt can you actually send a value back to Racket. For example, if you explicitly send a prompt, there's an unprompt around that, and you can capture that value and perform some computation. Alternatively, if you just send a prompt, there is also an unprompt around that, allowing you to capture that value and do some computation. If you simply want to define regular Racket constructs, you can explicitly unprompt and carry out some computation.

Everything else gets turned into a string, more or less. Void is ignored because I don't want to be splicing voids in all the time, and it's a convenient thing to unprompt automatically. If you have multiple values at the top level, which happens in the REPL, all of those are collected, and all of the values are returned. So the REPL will be able to handle multiple return values. That doesn't really happen at the top level.

So, top interaction is essentially just this p quasi-quote. As we saw, the @ expression reader means I have a second layer of unquoting when I use @ to unquote. That's kind of a different thing than this unprompt unquoting. You'll see that we actually have to use both, right? We need @ to stop writing raw text, and we need unprompt to actually return the resulting value to Racket.

Sort of makes sense. Okay, let me give you a demo of that, hopefully shorter than this. Here’s a little example, so this definition, which I hope is shorter than this.

Okay, here is a little example. This is defined by the prompt. Let me go ahead and run this. It is a little macro that takes an identifier and a prompt, sends the prompt to the LLM at compile time, gets the resulting code back, splices it into your program, compiles it, and then you have some code generated by an LLM. I'm sure it's very safe and secure. Now, so far, I'm still in LLM length, so I'm still in the expression reader. If I want to actually call this normal function, I need to stop writing raw text, and I need to use "unprompt" to indicate that I want to return this value to Racket instead of adding it to the prompt. I get 120 as expected.

Hopefully, there are no side effects. I don't know. The TCP is complaining, so maybe it was trying to do something it shouldn't have. Hopefully not. I probably should.

Oh, yeah, yeah. Oh, okay, yeah, that makes sense. Oh, yeah, the online expander doesn't have my API key. That's what's going on. Okay, that's good.

I should probably run that in a sandbox or something. Okay, let's get back to my slides for the moment. So, that's that demo. Now, finally, we understand how prompts and non-prompt values interact—basically like string quasi-quotation. Lastly, how do I deal with the costs?

So we've already seen one example of how I deal with costs. Prompts somehow get implicitly cached, which is quite nice. However, what prompt cost actually is, is a complicated question. Prompts incur various costs. The most direct cost is monetary, but prompts also have an energy cost, a CO2 cost, and a water cost.

And if you've read any articles, these are apparently extremely high. I have no way of knowing that myself because I don't have any way; OpenAI doesn't provide me with that information. However, if I'm doing engineering, I might want to take these factors into consideration. So, LLM has a cost model built into it that considers these aspects using some very interesting research. This is work that I haven't done, but it's the kind of work that people who measure real-life systems and make estimates typically engage in.

How do I get this to the right? Oh, I got it. Okay. No, no, not that. Oh, yeah.

Here we go. Okay. So in the background, LLM Lang is keeping track of estimates based on research that are probably not real, but are approximately real, for what every query you run costs in terms of power, CO2 emissions, and milliliters of water. For this particular query, it costs about one watt-hour, which is approximately 0.23 grams of CO2 and about 0.69 milliliters of water to generate this factorial. Does that seem like a lot to you?

I have no idea. I also had a bunch of contextualizing information, which includes the training costs. The training costs are astronomical, but in principle, they're a one-time expense.

Unless you're training new models every year, then they're not really one-time. So it also includes some contextualizing information, like a U.S. household consumes 10 megawatt-hours per year, which is, well, it's less than the training cost, but it's just astronomically more than the query cost. So, I don't know, the queries seem kind of... Yes, yes. Oh, you're right. Yeah, the training costs are huge.

But the query costs are tiny, so it’s not like if those were actually one-time, maybe this is fine. Anyway, I don't know if this is good or bad, but at least I can now measure it and try to contextualize it. So, this is all dumped into the log with some contextualizing information. Now, you can actually engineer and make engineering decisions about the costs of your queries, the training costs, and whatever else.

If you run this, there are multiple backends. So, if you run this with local LLMs, the costs are much lower, which is much nicer. However, it's very slow. It will take about five minutes for me to run through this one little example. How do I lower this? Okay, I'm not going to lower it.

There’s a lot more to explore regarding the cost models. Additionally, you don’t just want this information logged; you may also want the ability to inquire about the current power usage or water usage while the program is running. There is an interface similar to the current memory usage and time, allowing you to access that information in the REPL. Everything is documented, so you can refer to that for further details.

And all the citations for where I calculate all this information are over here. Let me provide a quick example using one of these. So, let's try— Oh no, I need to unprompt. Unprompt current power use. Let me just do that.

Zero. Okay, I haven't used anything. It's probably rounding that off because I think it defaults to kilowatt-hours, and this is essentially no electricity. All right. So that's the cost model. Now, what is LLMLang good for?

I don't really know. I've got nothing. So, it can help you with prompt engineering in principle. We can measure and control the cost of prompts, sort of. We can compute over prompts and treat them as first-class, but we cannot predict or deal with non-determinism.

But I haven't actually found a good use for any of them. I've tried lots of examples, but none of them seem to compute over problems and treat them as first-class entities, yet they do not predict or deal with non-determinism. No problem I've tried to solve in my real life has made me want to reach for LLM (language model) solutions.

So, if you have good ideas, let me know. I just find making languages in Racket kind of cool. Thank you. Yeah, I have a couple of questions. So, first, did you talk? Thank you.

Sure! Here’s the text with correct capitalization and spelling: "Yeah, I have a couple of questions. So first, did you dock the food disk? You know, like writing documentation using your language? That's a great question."

Yeah, did I dog food this? Yes, dog food. Sort of. Okay, so let me go to the top level here.

Okay, yes, this introduction is written by LLM Lang. Introducing LLM Lang, a cutting-edge programming language designed with user experience at its core. This must be like the third or fourth iteration. This actually looks like a better version than the last one because it discusses caching, replay, and other features. So yes, I have used it in the documentation.

Not extensively, though. Okay, real question: when you have, like, you know, I'm not sure which specific one you used—was it "define by prompt"? Yes, "define by prompt."

Did you make a separate query? Did you need to send multiple queries, or is that somehow done in a way that can be accomplished in just one query? If it's done in a defined prompt, it sends exactly one query and caches the result, so if you rerun this program, it will read from the cache. I see. So is "defined by prompt" sending a new query?

Yes, I see. What is the context? Is there any context management involved? Yes, all of the large language models (LLMs) essentially remember your chat history.

The same is true within the scope of any REPL session or any file; it remembers the entire contents of your work. For instance, I haven't shown a single file that can actually send multiple prompts if you explicitly send the prompt. All of that will be in your context. In any REPL session, the entire session will be in your context. I see. However, in this case, it means that a prompt defined later would not know anything above it.

Oh, so defined by it does add a little bit of context. Let me run the macro stepper and hide this log. Now we just want to see the prompt it's generated from this. Don't worry.

Oh, macro steppers on the other screen. There we go. Okay, so let's see here. Let's just go to the end. Hopefully, that will give me some... Where's the string that gets sent?

Okay, let's take a step back—perhaps two steps. Oh no, this isn't working. Let's go back even further. Alright, maybe this won't work. Okay, maybe this won't work. Yeah?

Yeah, this is only part of the prompt, though. I need to finish expanding that. And I guess the last thing about the prompt is that I need to finish expanding it as well. I see. The last thing I want to mention is, you know how some software, like C2 Free, has features such as push and pop for managing context? It would be nice to have something similar here, where you can say, "Oh yeah, let's start with a new context," so that it’s not burdened by anything from before. So, there are two features I'm interested in adding: first, a lot more control over the context; and second, there is some context that is added here that asks GPT not to provide an explanation.

I only want the code. Don't format with markdown. Also, you're an expert in Racket, so write good Racket code. This is all just like I dumped the strings into the prompt; it would be better to have some control over the context and some control over other queries. Very meta variables. I don't have any of that. Right now, you can only have one prompt per module, basically, always interacting with the same LLM. There's no reason, in principle, that you couldn't have more; I just haven't done it. I'm curious if, for some reason, the LLM didn't produce perfect code, what the error would look like for maybe this factorial function. If it didn't produce code that was syntactically valid, you'd get some sort of syntax error involving the generated output that it did give you.

So really, defined by the prompt, there should be some kind of validation and error correction. There was a line in there that handled a little bit of that, but it stopped working when I changed something. So I just disabled it, and it started working again. In general, if you're going to take code written by an unknown third party and compile it into your program, I recommend having a lot of validation. This does not.

Because LLMs are quite unreliable, that validation should include things like even syntactic well-formedness. Earlier versions would essentially always ignore my instruction not to give me markdown, and it would provide markdown anyway. So I added a little tweak—if I get something wrong in my defined prompt, I ask, "Can I...?" and "How do I poke it to make it give me different versions?" One of the big problems with interacting with LLMs is that they are inherently iterative; you need to be able to look at the output and tweak it. There is an option to give it an output file, and it will dump the code it gets back into that output file. If the output file exists, it will always read it back. You could output that to a file, go and look at it, modify it, check that it's well-formed, maybe analyze it for security—whatever. I didn't do that, but yeah, that's an option.

So that's one way of dealing with this unreliability. Have you considered, instead of having sensible validation logic, just making the LLM do your validation by looking at the error message you receive and then telling the LLM that it's messed up, providing the error message, and then repeating until it compiles? Yeah, you could definitely do that. Instead of actually doing any work myself, I should just make the LLM do that work, iterating to some sort of fixed point until it's obviously producing the correct code.

I could do that. It wouldn't be hard. You just put this in a loop, capture the error message, and send that back. It's going to get expensive because you're sending tons and tons of an unbounded number of tokens, but maybe that's fine. Okay, last question.

Yes, sorry again. Oh, remember one idea that people have, right? So, you know, like, threatening that if they don't comply with you, then something is going to get harmed, like a puppy is going to get harmed. Or you say that you’re going to give them some money if they comply with your request or something like that. So you want me to bribe or threaten the LLM?

Is that what you're saying? Yeah. Have you considered that as a feature? I mean, that's something you could do. I haven't tried it.

You're fine by threatening prompting. Well, yeah, here, let's try it again. What was the example that it got wrong earlier? Let's see. Yes, this one.

Write an actual valid slideshow, or else. Slide down. Well, well. Being threatening doesn't always seem to help. Of course, that could be my fault.

I had a typo. Yeah, it doesn't know what "slide down" is. No, it does know what "slideshow" is, because it has produced valid slideshows for me before. So, this is another non-determinism issue. All right, are there any other questions?

All right, actually the last one. So, first, what was the smallest prompt that evaluated to itself that you found? I didn't write that, but I obviously don't know what I was thinking. Write a prompt that evaluates to itself.

Okay. I don't know; that's pretty long. I think it could be better. Oh, that's a good point; it's probably wrong. What do you mean? That's the whole prompt?

Surely. Okay, I'll give it just this prompt. Just the thing in quotes? Okay. I don't know what quotes are, though.

I'm a prompt engineer. Yeah! You know, it sounds like we're having a lot of fun with this. If there are more questions, I think we can keep going. This is the last talk of the day, so, you know, let's keep going for another five minutes.

Yeah, it might actually be. Here, let me try. Yeah, if I take the quotes off, right? The exact text. Yeah, I mean, I take the quotes off and write the exact text.

It is shorter. Yeah, I mean, I didn't ask it to produce the smallest possible prompt. Okay, there we go. Oh, yes. Yes.

So, yeah. Yeah. Yeah. That's the LLM.

That's not a screw-up. Try what? Try again. Nope. It doesn't really understand self-reference, I think.

Nope. It doesn't really understand self-reference, I think. Guys are going to burn through my GPT budget. All right. All right.

Let's hear it for Will Bowman!