An LLM demo can fit in one API call. A usable product cannot. Between the text box and the answer are decisions about context, timeouts, cost, errors, and what the user is allowed to do next.
Before adding agents, memory, or a vector database, trace one prompt through the smallest version of the product. Save the artifacts at each boundary. You will get a system you can explain and debug.
Pick one narrow outcome
Use a task with an observable finish line, such as:
- turn release notes into three audience-specific bullets;
- classify an incoming message into a fixed set of queues;
- extract dates and owners from a project update;
- answer questions from one short, approved document.
“General assistant” is too broad for this exercise. A narrow task lets you tell the difference between a model failure and an undefined product requirement.
For the rest of this walkthrough, imagine a tool that converts release notes into a short customer update.
Artifact 1: the input contract
Write down what the application accepts before writing the prompt:
{
"release_notes": "string, 1..12000 characters",
"audience": "admin | developer | end_user",
"tone": "direct | friendly"
}
The contract belongs to the application, not the model. Validate length and enum values at the API boundary. Decide whether pasted secrets, HTML, or personal data are allowed. An input limit is also a product choice: it affects latency and spend.
Artifact 2: the assembled model request
Log a redacted preview of the actual request your code creates, not only the user’s text.
instruction version: release-summary-v3
model alias: summary-default
audience: developer
source characters: 4,280
requested format: JSON {headline, bullets[3]}
Keep the full prompt available in a protected development trace if policy allows, but do not make raw prompts the default production log. The metadata above is often enough to compare requests without storing customer content.
Artifact 3: the raw response envelope
Preserve the provider request ID, finish reason, token usage when supplied, and raw text before your UI transforms it. This distinguishes “the provider returned malformed JSON” from “our parser dropped a field.”
Your provider client should return a result or a typed error rather than throwing an undifferentiated string:
type ModelResult =
| { ok: true; text: string; requestId?: string }
| { ok: false; kind: "timeout" | "rate_limit" | "provider"; retryable: boolean };
Artifact 4: the parsed application object
Treat generated JSON as untrusted input. Parse it and validate the shape:
type Summary = {
headline: string;
bullets: [string, string, string];
};
Check string length, array length, missing fields, and any URLs. If parsing fails, choose an explicit behavior: retry once with a repair instruction, show an editable plain-text result, or ask the user to try again. Silent coercion makes failures difficult to see.
Artifact 5: the user-visible state sequence
Record the states the UI can enter:
idle → submitting → streaming → complete
↘ failed → retrying
↘ cancelled
Build every state on purpose. Disable duplicate submits while a request is active. Give the user a cancel control. Keep their source text after an error. If you stream output, label it as incomplete until validation finishes.
Artifact 6: one evaluation table
Start with ten representative inputs, not a vague feeling that the output “looks good.” A tiny table is enough:
| Case | Must preserve | Must avoid | Pass? |
|---|---|---|---|
| Breaking API change | endpoint and migration date | invented compatibility claim | |
| Security fix | affected version and action | exploit details not in source | |
| Empty notes | no fabricated update | confident filler |
Run the same cases when you change the prompt or model. Save outputs next to the version identifiers. This is a small regression suite, not a claim that quality can be reduced to one score.
Artifact 7: the cost and latency record
For each successful request, store input size, output size, end-to-end duration, provider duration when available, and the configured model alias. Calculate cost from the provider’s current pricing in your own configuration rather than hard-coding numbers into application logic.
Look at distributions, not a single fast request. A product feels different at the slow end of the distribution, and long inputs can dominate both latency and spend.
What the trace reveals
Once these seven artifacts exist, feature decisions become clearer:
- Retrieval is justified when the missing information is external to the request.
- Tool calling is justified when the product needs a bounded action, not just text.
- Conversation memory is justified when earlier turns materially change the task.
- A smaller model is viable when it passes the same evaluation cases within the required latency.
The OpenTelemetry semantic conventions for generative AI provide a useful vocabulary for request, response, token, and operation attributes. Adopt only fields you can collect safely; observability should not become an accidental prompt archive.
An inspectable one-prompt product teaches more than a pile of features. Trace the request, keep the boundaries visible, and add complexity only when a failed case gives you a specific reason.
Top comments (0)