Trail content
7 minEnglish narration, Portuguese subtitlesPodcast (RSS)
Chapters
  1. 0:00Opening
  2. 1:08The diagnosis
  3. 2:05Why it happens
  4. 2:59What it costs
  5. 3:51A challenge
  6. 4:43The treatment
  7. 5:48When to ignore
  8. 6:55Wrap-up
Transcript

Opening

Now that you can measure, we start fixing. First, a smell that almost every team building on models has, and almost nobody notices until month-end.

A company extracts data from supplier contracts with two steps. The first one reads the contract and lists the main data. The second takes that text and fills in the record in the company's management system.

And between the two steps, there's this. A regular expression that looks for the word Total, a colon, and a number.

It worked for three months. Then one day, the model started writing total contract value, instead of just total. The regular expression found nothing. The total became empty. And one hundred forty contracts went into the system with no amount, before anyone noticed.

Nothing broke. No exception, no alert. The data was just empty. In this lesson, you'll learn to recognize this smell, why it's so tempting, and the one refactoring that removes the whole class of errors.

The diagnosis

Here's the shape of it. The first step produces text meant for a person to read.

And that text is the only interface between the two steps.

Code, or sometimes the next model, tries to guess where each piece of data is.

When the guess is right, everything works.

When the model changes how it writes, the data disappears, with no alarm.

How do you spot it? One step returns prose, and the next step uses a regular expression, a split, or find the line that starts with.

The code tolerates several names for the same field. Total, total value, grand total.

The second step is another model call, whose only job is to understand the first one's format.

Changing one word in a prompt breaks a parser in another file.

And missing fields silently become empty, instead of raising an error.

Why it happens

It usually starts with a prototype in a chat window. Someone tested the prompt, liked the text answer, and pasted it into the code. In a chat, text is the product. In a pipeline, text is a live wire.

Text feels flexible. You don't have to decide names, types, or what's required before starting. A schema makes you decide.

And it works, for a while. Models often repeat the format, until the day they don't. The model changed, the prompt changed, or just the input did. A contract that says amount instead of total.

And here's the trap for someone starting out. Asking for JSON in the prompt, reply with JSON only, and thinking that settled it. That's still text. The model can wrap it in formatting, add a comma, or leave out a key.

What it costs

The worst cost is silent bad data. Nothing breaks. One hundred forty contracts with no total raise no exception. Finance only sees the hole at month-end close.

Then there's the coupling nobody can see. The first step's prompt becomes part of the second step's contract, and nobody knows it. The parser lives in another file, another repository, maybe another team.

Then fear. Any prompt tweak or model swap becomes a risk. The team stops improving the extractor, because finance's regular expression will break.

And wasted tokens. Prose meant for a parser spends output on words nobody reads. The total contract value, taxes included, is. Dozens of tokens a single field doesn't need.

A challenge

Here's one for you. The team asked for JSON in the prompt, with the keys total and date, and the code parses it. Once a week, the run breaks on an extra comma, or a formatting fence around the object. Which next step removes the whole class of error?

Use the API's structured output, with a schema, and validate the fields in code. Format stops being a request to the model, and becomes a guarantee from the API.

Retrying until it parses hides the instability. And on the day the variation is a string where a number belongs, the parse succeeds, and the bad data goes in. Adding three examples of valid JSON lowers the rate, but never to zero. You'd still depend on the model obeying a format instruction.

The treatment

The treatment is a refactoring called replace free text with structured output. The contract between the steps becomes explicit.

The supplier's tax ID, the invoice's access key, which may be missing, the total, typed as money, and the list of due dates.

The API guarantees the shape. The code reads the total field, not a sentence. A phrase like total contract value simply has nowhere to go anymore. And a missing required field becomes an error right away. The document goes to human review, instead of entering the system as zero.

And often there's a bonus. If the second step only existed to turn the text into fields, it can go away. The first call already returns the fields.

But a schema doesn't replace an eval. A correct shape can still carry the tax ID of a company that isn't the issuer. The eval measures whether the value is right. The schema guarantees the value arrived as a value.

When to ignore

When can you ignore it? When the destination is a person. A chat reply or an email to a customer should be text. The smell is text between machines.

When the text is the product. One step writes an article draft, and another reviews it. Passing text between them is perfectly legitimate. Structure what code actually uses, like the list of problems found, or whether it was approved. Not the article itself.

And a throwaway prototype is fine. The day the parser goes into the real system, the schema goes in with it.

One last case, to sharpen the eye. A ticket classifier answers with a single word, billing, order or other, and code compares the text. Sometimes it returns billing with a capital letter and a period, and the ticket lands in the wrong queue. Is that this smell? Yes. Even one word is free text, if your code depends on its exact form. A schema with a closed list of values fixes it, with just one field.

Wrap-up

So here's the idea to take with you. Text is for people. When one step hands data to code, or to another step, the contract has to be a schema, not a sentence. Otherwise your system works until the day the model finds a nicer way to say total.

In the next lesson, we'll do the refactoring itself, step by step, with an eval before and after: replace free text with structured output. See you there.

SmellCouplers

Free-Text Handoff

Passagem em Texto Livre

Free-Text Handoff is an AI code smell, a sign of trouble in LLM systems, in the Couplers group. One step hands loose text to the next, which has to guess the format.

Element
23/37
Level
Beginner
Read
4 min
Updated
On this page 7
  1. 01Signs and symptoms
  2. 02Reasons
  3. 03Cost of ignoring
  4. 04Treatment
  5. 05Payoff
  6. 06When to ignore
  7. 07Is it this smell?
  8. ··Quick answers

Signs and symptoms

A chain extracts data from supplier contracts and from DANFEs (the PDF printout of Brazil’s electronic invoice, the NF-e). The first step reads the PDF and “lists the main data”. The second step takes that text and fills in the record in the ERP. The code between the two (anonymized):

summary = llm("List the main data from the contract.", pdf_text)

# step 2: pull the total out of the text
match = re.search(r"Total:\s*\$\s*([\d,\.]+)", summary)
total = parse_usd(match.group(1)) if match else None

It worked for three months. One day, the model started writing “Total contract value: $48,200.00” instead of “Total: $48,200.00”. The regex found nothing, total became None, and 140 contracts went into the ERP with no amount before anyone noticed.

The same pattern shows up on an NF-e: the model writes “issuer tax id 12.345.678/0001-90” and the code looks for the line CNPJ:. The 44-digit access key disappears into a paragraph. Finance only sees the hole at month-end close.

Signs of a Free-Text Handoff:

  • One step returns prose or markdown, and the next step uses a regex, split, or “find the line that starts with”.
  • The code tolerates several formats for the same field (“Total”, “Total value”, “Grand total”).
  • The second step is another model call that takes the first step’s text and “understands” the format.
  • Changing one word in the prompt breaks a parser in another file.
  • Missing fields silently become None instead of raising an error.
flowchart LR
  A(["1. Step A writes text"]):::llm --> T["2. Free text"]:::tool
  T --> P["3. Regex guesses the format"]:::tool
  P --> B(["4. Step B"]):::llm
  P -->|"format changed"| X(["5. Empty field, no error"]):::io
  1. The first step produces text meant for a person to read.
  2. That text is the only interface between the steps.
  3. Code, or the next model, tries to guess where each piece of data is.
  4. When the guess is right, everything works.
  5. When the model changes how it writes, the data disappears with no alarm.
Check +20 XP on first tryThe parser looks for Total: and the model started writing Total contract value:. A hundred and forty records enter the ERP with no amount. Which design failure came first?

Reasons

  • The prototype was built in a chat UI. Someone tested the prompt in the interface, liked the text answer, and pasted it into the code. In chat, text is the product. In a pipeline, text is a live wire.
  • Text feels flexible. You do not need to define a schema before starting. You look at the PDF, ask for “the main data”, and move on. A schema asks you to decide names, types, and what is required.
  • It works for a while. Models often repeat the format, until the day they do not: the model changed, the prompt changed, the API version changed, or just the input did (a contract that says “amount” instead of “total”).
  • Structured output looked complicated. When the system started, maybe the feature did not exist or nobody knew it. Today provider APIs accept a schema. Staying on regex is inertia, not a technical limit.

The trap for someone starting out: asking for JSON in the prompt (“reply with JSON only”) and thinking that settled it. That is still text. The model can wrap the JSON in markdown, add a comma, omit a key. json.loads breaks, or worse, succeeds and the field arrives with the wrong type.

Cost of ignoring

  • Silent bad data. The worst kind of failure: nothing breaks, the data is simply empty or wrong in the target system. A hundred and forty contracts with no total raise no exception.
  • Hidden coupling. Step A’s prompt becomes part of step B’s contract, and nobody knows until it breaks. The parser lives in another file, another repo, another team.
  • Fear of change. Any prompt tweak, model swap, or version upgrade becomes a risk. The team stops improving the extractor because “finance’s regex will break”.
  • Wasted tokens. Prose meant for a parser spends output on words nobody will read. “The total contract value, taxes included, is” is dozens of tokens that a total field does not need.

In the invoice and contract extraction scenario, this smell is the first to show up: the model describes the invoice and the ERP expects fields.

Treatment

Refactoring Choose it when
Replace Free Text with Structured Output Whenever a step hands data to another step or to code. The target is the Structured Output pattern.

If the next step exists only to turn the text into fields, it can probably go away: the first call already returns the fields.

The contract becomes explicit. Example schema, for the same total:

schema Invoice {
  vendor_tax_id: string      # CNPJ, Brazil's company tax id
  access_key: string | null  # NF-e access key, 44 digits
  total: money
  due_dates: list[date]
}

The API guarantees the shape (JSON with those keys, types checked). ERP code reads invoice.total, not a sentence. A missing required field becomes an error immediately, and the document goes to human review instead of entering as zero.

This does not replace eval. A correct schema can still arrive with the tax id of someone who is not the issuer. Eval measures whether the value is right; the schema guarantees the value arrived as a number.

Check +20 XP on first tryThe team asked for JSON in the prompt ('reply with the keys total and date') and the code calls json.loads. Once a week the run breaks on an extra comma or a markdown fence around the object. Which next step removes the class of error?

Payoff

  • An explicit contract between steps, with types and required fields.
  • An immediate error when a field is missing, instead of empty data in the ERP.
  • Freedom to change prompt and model without breaking the parser.
  • Smaller, cheaper outputs.

When to ignore

  • The destination is a person. A chat answer or an email to a customer should be text. The smell is text between machines.
  • The text is the product. One step that writes a draft and another that reviews it legitimately exchange text. Structure the metadata (title, tags, points to review), not the text itself.
  • A throwaway prototype. If the code will be thrown away next week, that is fine. The day the parser goes into the ERP, the schema goes in with it.

Is it this smell?

Check +20 XP on first tryA ticket classifier answers with a single word ('billing', 'order', 'other'), and code compares the string. Sometimes it returns 'Billing.' with a capital letter and a period, and the ticket lands in the wrong queue. Is this a Free-Text Handoff?

Check +20 XP on first tryOne step writes an article draft in markdown and the next step, another model, reviews the draft and returns the corrected text. Is this a Free-Text Handoff?

Check +20 XP on first tryThe model returns JSON requested in the prompt ('answer in JSON with the keys total and date'), and code calls json.loads. Once a week the JSON has an extra comma and the run breaks. Is this a Free-Text Handoff?

Check +20 XP on first tryStep 1 returns a validated Invoice object (tax id, total, due dates). Step 2 writes to the ERP without reading prose. Is this a Free-Text Handoff?

Quick answers

What is Free-Text Handoff?

Free-Text Handoff is an AI code smell, a sign of trouble in LLM systems, in the Couplers group. One step hands loose text to the next, which has to guess the format.

How do you fix Free-Text Handoff?

With Replace Free Text with Structured Output. Each refactoring has safe step-by-step mechanics with an eval before and after.

Where does Free-Text Handoff show up in practice?

In the real-world scenarios Invoice and contract extraction.

Go further

Relations

Refactorings that treat it 1
Shows up in scenarios 1
Part of the tracks 1
Search