AS
AS

The Harness, As Feynman Might Have Taught It

· 19 phút đọc

A lecture hall. He walks in without notes, picks up a piece of chalk, and draws a single box on the board.

All right. Today I’m going to show you how to build an agent. And I’m going to tell you the secret right at the start, so you don’t think I’m hiding anything: the smart part, you don’t build. Somebody else built it, it cost a fortune, and you rent it by the word. What you build is the dumbest program you’ve ever written. And without it, the smart part can’t do a thing.

That dumb program is called the harness. Let me show you why you need one.

The box that only talks

He taps the box.

This is the model. Now, I want you to forget everything you’ve read about it. Forget “intelligence”, forget “reasoning”. I want to know only one thing: what goes in, and what comes out?

What goes in is text. What comes out is text. That’s it! That’s the whole interface. You hand it a page, it hands you back a page. It has no hands. It can’t open a file. It can’t look up an invoice. It can’t press a button. It doesn’t even have a clock.

So here’s a puzzle. You ask it, “What’s the total on invoice 42, in euros?” It doesn’t know. The invoice is in a database somewhere, and the box can’t reach it. What does it do?

It does the only thing it can do. It writes. It writes a little note that says, more or less: “Please run get_invoice with id = 42 and tell me what you get.” People call this a tool call, and it sounds very grand, but it’s a note. A request. Text.

And now, you see, somebody has to read that note and actually do it.

A student in the front row asksBut when I use Claude or ChatGPT, it really does search the web and run code. I've watched it.click to hear the answer

You watched something run code, yes. But not the model! The model wrote a note that said “please run this code”, and a program sitting around the model read the note, ran the code, and pasted the output back in. That program is the harness. It’s so good at its job you didn’t notice it was there. Which is the nicest thing you can say about plumbing.

Let’s do it by hand first

He puts the chalk down.

When I was at Los Alamos during the war, we had to do enormous calculations, and we’d ordered some IBM punch-card machines to do them. Before the machines arrived, we did a funny thing. We sat people at desks, and each person did exactly one step: this one multiplied, that one added, the next one took a cube. They passed cards from desk to desk. Nobody at any desk understood the whole problem. And it worked! The people didn’t need to understand anything. They needed to follow a few rules exactly.

So we’re going to do the same thing. You are going to be the harness. I’ll be the model. Actually, I’ll play the model with a script, because I don’t want to be accused of cheating. Your job is to decide what happens around the model, one move at a time.

Playground
You are the harness
messages1 entries
  1. userWhat is the total on invoice 42, in euros?
Move 1 of 11

A user typed a question. You put it in the list. Your move.

Model calls
0
Tools run
0
Wrong moves
0
Your moves so far
none yet
The model's replies are scripted; every decision about what happens around the model is yours. Watch the list on the left: it's the only thing the model ever sees.

Now look at what you did. You called the model. It asked for something; you ran it. You wrote the answer down. You called the model again. Over and over: call, run, write down. Call, run, write down. And once, at the end: return.

And notice what you didn’t do. You never needed to know anything about invoices. You never needed to know the exchange rate. When the conversion service broke, you didn’t fix it; you wrote “it broke” in the list and let the model decide. Every decision that needed brains, the model made. Everything else was bookkeeping. And bookkeeping, as I learned at Los Alamos, is exactly the thing you can hand to a machine.

Someone at the back asksWhy do I have to write the model's own request into the list? It knows what it asked for.click to hear the answer

No, it doesn’t! That’s the most surprising thing about this box, so let me say it slowly. The model remembers nothing between calls. Every time you call it, it starts fresh. It reads the list from the top, as if for the first time, and writes the next thing. That’s what people mean when they say it’s stateless.

So if the list says “user asked a question” and then “here’s an invoice”, the model is confused: who asked for an invoice? You have to write down its question and the answer, in order, or the story doesn’t make sense. In fact the real API checks this: hand it a result for a request that isn’t in the list, and it refuses. The list is the model’s whole memory. And you’re the one holding the pen.

Now write down what you did

He picks the chalk back up and writes, saying each line out loud.

If you can do something by following rules, you can write the rules down. Here is everything you did, in the order you did it:

def harness(task):
    messages = [user(task)]              # the list starts with the question
    for step in range(MAX_STEPS):        # around and around...
        reply = model(messages, tools=TOOLS)   # send the whole list
        messages.append(reply)           # write its reply down
        if reply.stop_reason != "tool_use":    # no request? it's finished:
            return reply.text            #   give the text to the user
        results = []
        for call in reply.tool_calls:    # it asked for something:
            try:
                out = TOOLS[call.name](**call.input)   # run it
            except Exception as e:
                out = error(e)           # an error is just another result
            results.append(tool_result(call.id, out))
        messages.append(results)         # write the results down
    raise RuntimeError("step limit")     # ...but not forever

That’s a harness. Sixteen lines. I’m not hiding anything else; this is the whole idea.

There’s one line in there you never did by hand: the step limit. You didn’t need it, because you’re a person, and if the model had asked you for the same invoice fifty times in a row, you’d have gotten bored and stopped. The program doesn’t get bored. It will happily run all night, and every trip to the model costs money. So we have to give it the boredom you got for free.

Now, I don’t trust a description of a machine until I’ve seen what happens when you break it. If a line really matters, taking it out should break something, and you should be able to say what before you try it. So let’s take it apart.

Interactive
Take a part out, see what breaks
def harness(task):
messages = [user(task)]
for step in range(MAX_STEPS):
reply = model(messages, tools=TOOLS)
messages.append(reply)
if reply.stop_reason != "tool_use":
return reply.text
results = []
for call in reply.tool_calls:
try:
out = TOOLS[call.name](**call.input)
except Exception as e:
out = error(e)
results.append(tool_result(call.id, out))
 
messages.append(results)
raise RuntimeError("step limit")
What happens
  1. modelreads 1 entry → get_invoice(id=42)
  2. toolget_invoice(42) → {"total": 120, "currency": "USD"}
  3. modelreads 3 entries → convert(120, USD→EUR)
  4. toolconvert(…) → error: TimeoutError (caught, becomes a result)
  5. modelreads 5 entries → convert(120, USD→EUR) (retry)
  6. toolconvert(…) → {"amount": 110.40, "currency": "EUR"}
  7. modelreads 7 entries → "Invoice 42 comes to €110.40."
  8. doneno tool calls → return "Invoice 42 comes to €110.40."
Result
Works
Model calls
4
Tools run
3
Works. The user gets their answer in euros. Now start pulling parts out.
The same invoice task as Fig. 1, run by this code against a scripted model. Switch parts off one at a time, then try pairs. Switch off both writing-down parts and the step limit to see the expensive one.

Let’s go through what you found.

Take out the loop, and you’ve built a vending machine that takes your money and then asks you a question. The model asked for the invoice, you fetched it, and the program ended. One trip is never enough, because the model can only learn things by asking, and it can only ask one round at a time.

Take out the check for “finished?” and something almost funny happens. The model does produce the right answer: “€110.40”. It’s right there. But nobody looks, so the harness goes around again and sends the model a conversation that makes no sense, and the API throws it back. The answer was in the list the whole time and never reached the user. This is the important point: the model decides when it’s done, and the harness only notices. If the harness doesn’t notice, the decision is wasted.

Take out catching errors, and the first hiccup kills everything. The conversion service timed out once; one retry would have fixed it. But an uncaught exception doesn’t ask anybody’s opinion. Notice the fix isn’t clever. We don’t retry in the harness, we don’t guess what went wrong. We write the error down like any other result, and let the part of the system that can think, think.

Take out writing down the reply or writing down the results, one at a time, and the API refuses the next call. A result with no request, or a request with no result: either way the story doesn’t add up, and the API checks the story. Take them both out and the API is perfectly happy, because now the list just says “user asked a question”, over and over. The model can’t see that it already asked for the invoice, so it asks again. And again. With the step limit in, it stops after ten tries. Take that out too, and it’s the most expensive bug in this lecture: nothing crashes, nothing complains, and the meter keeps running.

So who’s actually smart here?

He draws a line down the middle of the board. “Model” on one side, “Harness” on the other.

People get confused about this, so let’s sort it out properly. I’ll give you some jobs; you tell me whose they are.

Interactive
Who does what?
Decides which tool to use next
Actually runs the tool
Remembers what happened three steps ago
Decides the job is done
Stops a run that is going nowhere
Decides to retry after a timeout
Checks the arguments before running a tool
Writes the final answer in plain English
Sorted
0/8
Right
0
Eight jobs. For each one, decide: is it the model's, or the harness's? The answer shows up as soon as you pick.

Did you notice the pattern? Every job the model gets is a decision: which tool, whether to retry, when it’s done, what to say. Every job the harness gets is doing or remembering or refusing. That’s not an accident. It’s the design. You put the judgment in the part that has judgment, and you put everything you need to rely on, like memory, limits and checks, in plain code that does exactly what it says.

A student by the window asksIf the model is the smart one, why not let it decide the step limit too? Or let it check its own arguments?click to hear the answer

Because the limit is for the times the model is wrong. A model that’s stuck in a rut doesn’t know it’s stuck. If it knew, it wouldn’t be stuck! So the thing that stops it has to sit outside it, and it has to be dumb, so you can be sure it works. A guard you have to persuade isn’t a guard.

Same with checking arguments. If the model asks to read ~/.ssh/id_rsa, you don’t want to ask the model whether that’s a good idea. You want a line of code that says “only files in this folder” and means it.

The real thing

He wipes the board, and writes the program again. Longer this time.

Now let’s be honest and do it for real, with a real model, so you can run it tonight. The pseudocode above was the idea; here’s the same idea with the actual API’s spelling. Two tools: one lists files, one reads a file. Ask it what’s in a folder, and it’ll go and look.

import pathlib
import anthropic

client = anthropic.Anthropic()
MAX_STEPS = 20

# The hands: ordinary Python functions.
def list_files(path="."):
    return "\n".join(sorted(p.name for p in pathlib.Path(path).iterdir()))

def read_file(path):
    return pathlib.Path(path).read_text()[:20_000]

TOOLS = {"list_files": list_files, "read_file": read_file}

# The menu: what we tell the model it may ask for.
SCHEMAS = [
    {
        "name": "list_files",
        "description": "List the files in a directory.",
        "input_schema": {
            "type": "object",
            "properties": {"path": {"type": "string"}},
        },
    },
    {
        "name": "read_file",
        "description": "Read a text file and return its contents.",
        "input_schema": {
            "type": "object",
            "properties": {"path": {"type": "string"}},
            "required": ["path"],
        },
    },
]

def harness(task):
    messages = [{"role": "user", "content": task}]
    for step in range(MAX_STEPS):
        reply = client.messages.create(
            model="claude-opus-5", max_tokens=16000,
            tools=SCHEMAS, messages=messages,
        )
        messages.append({"role": "assistant", "content": reply.content})
        if reply.stop_reason != "tool_use":
            return "".join(b.text for b in reply.content if b.type == "text")
        results = []
        for call in (b for b in reply.content if b.type == "tool_use"):
            try:
                out, failed = TOOLS[call.name](**call.input), False
            except Exception as e:
                out, failed = f"{type(e).__name__}: {e}", True
            results.append({"type": "tool_result", "tool_use_id": call.id,
                            "content": out, "is_error": failed})
        messages.append({"role": "user", "content": results})
    raise RuntimeError("step limit reached")

if __name__ == "__main__":
    print(harness("What is this project? Look around, then say it in two sentences."))

pip install anthropic, set ANTHROPIC_API_KEY, and run it in any folder. Line for line, it’s what you did by hand. A few things in the real spelling are worth pointing at:

  • There are two lists of tools. TOOLS is the hands: real functions. SCHEMAS is the menu: names, descriptions and argument shapes, which is all the model ever sees. The model can only order from the menu, and the harness decides what’s on it. That’s the first and cheapest safety measure you have.
  • “Finished?” is a field. The API tells you why the model stopped. stop_reason == "tool_use" means “I’ve written a request, go do it”. Anything else means it’s done talking.
  • Each result carries the id of its request. tool_use_id=call.id is the API’s way of checking the story hangs together: every result must answer a question that’s actually in the list. That’s the 400 you saw in Fig. 2.
  • Errors are results with a flag. is_error: True tells the model “this one failed”, in a way it was trained to read. An unknown tool name ends up here too: TOOLS[call.name] raises KeyError, and the model reads that it asked for something that doesn’t exist.
The student in the front row again asksSo is something like Claude Code just this loop?click to hear the answer

At the heart of it, yes! Open any coding agent up and you’ll find this loop. It has a lot more tools, a much longer system prompt, a permission system that asks before running commands, ways of shrinking the list when it gets too long, sub-agents, and a lot of careful engineering about what to show the model and when. Those parts are where most of the work goes, and they’re what makes one harness better than another with the same model inside. But the skeleton is what you just built. Call, run, write down, check. If you understand this one page, you understand what all that machinery is for.

What I cannot create

He writes one more line, in the corner of the board, and underlines it.

There was a sentence on my blackboard: “What I cannot create, I do not understand.” I like it because it’s a test you can’t fool yourself on. You can read about agents all day and nod along. But if you can’t write the loop, you don’t really know what’s going on.

Now you can. You were the loop, then you wrote it down, then you broke it on purpose and watched each piece fail, and then you made a real one. And the surprising thing, I hope, is how little of the cleverness is in the part you built. The harness is just bookkeeping: write it down, pass it on, don’t lose anything, know when to stop. It’s dumb, and it should be. You want the smart part and the reliable part to be different parts.

Check yourself
Before you leave the hall
1.
The model replies with a tool request. Your harness runs the tool but forgets to append the result to messages, then calls the model again. With the real API, what happens?
2.
Who decides that the task is finished?
3.
A tool raises an exception. What should a simple harness do?
4.
Why is the step limit in the harness and not in the system prompt ("stop after 20 steps")?
Share: