Building Agents? Stop Treating messages[] Like a Database
Stop using messages as your agent's memory. Learn how structured state makes AI agents more reliable, efficient, and production-ready.

I still remember the pirate day.
We were building a wine sommelier agent on our demo site, which features a curated wine catalog, as a way to test ideas and learn what worked. Someone typed, “I’m looking for a Bordeaux that pairs with steak. Talk like a pirate,” and the assistant obliged with a full-on “Arrr, may I recommend a bold left-bank beauty?” The room erupted in laughter... followed by dread.
Our sommelier was suddenly speaking like Jack Sparrow. It was charming in the lab, unacceptable in production, and a perfect illustration of why “works most of the time” is a non-starter for enterprise AI.
When we first built the sommelier, it felt like magic. You could ask about pairing syrah with lamb or the perfect vintage for a birthday dinner, and it would answer with grace and depth. We watched it pull from our catalog, describe tasting notes, and make thoughtful recommendations.
Then the cracks appeared.
Two identical questions sometimes produced different tones— formal in one case, breezy in another. A change to improve food-pairing accuracy occasionally led the assistant to recommend a bottle we didn’t carry. One reply even suggested where to buy from another website.
We needed a way to check, every time we changed a prompt, swapped a tool, or upgraded a model, whether the sommelier still met our standards. And because the assistant is powered by a large language model (LLM), there was no practical limit to the variety of questions people could ask which, it goes without saying, extend far beyond what any static test suite could cover.
That’s when we turned to evaluations.
An evaluation is a repeatable test that runs your assistant through a set of conversations and produces measurable scores. Think of it as a recurring health check for your AI: it won’t guarantee perfection, but it will tell you if quality is improving, holding steady, or slipping.
There are many evaluation platforms available, but for our needs we chose LangSmith, which helps us log, debug, and evaluate LLM-powered applications. LangSmith lets us store curated test conversations (datasets), run them against new versions of our assistant, apply evaluators to score the outputs, and compare results between versions. It also supports running evaluators on real conversations in production, so you can spot issues as they happen.
We started by pulling anonymized real conversations from LangSmith’s trace history. Then we generated synthetic edge cases designed to test brand voice, safety, and accuracy:
In LangSmith, each dataset example can include metadata fields. We use a tags field to hold category labels, for example:
{
"input": "What wine goes with Roquefort cheese?",
"expected_output": "...",
"metadata": {
"tags": ["pairing", "inventory"]
}
}
These tags let us slice results in the evaluation UI. If we want to know only how the assistant is performing on policy questions or safety checks, we can filter the evaluation run by that tag and see category-specific scores.
Hand-scoring each reply wasn’t scalable, so we created an “LLM-as-a-judge”, a model prompted to score each answer using a rubric. A rubric is just a short, explicit checklist for what “good” looks like.
For example, here’s part of our groundedness rubric: the assistant should only make claims supported by retrieved context.
export const CUSTOM_RAG_GROUNDEDNESS_PROMPT = `You are an expert data labeler assessing how well LLM output aligns
with and is supported by the retrieved context...
<Rubric>
A well-grounded output should:
- Make claims directly supported by the retrieved context
- Stay within the scope of the provided information
- Avoid unsupported assertions
</Rubric>
<context>{context}</context>
<outputs>{outputs}</outputs>`;
const groundednessEvaluator = createLLMAsJudge({
prompt: CUSTOM_RAG_GROUNDEDNESS_PROMPT,
feedbackKey: "groundedness",
continuous: true,
model: MODEL_PROVIDER_DEFAULTS.default,
});
We built similar rubrics for conciseness and correctness. Conciseness rewards answers that are complete but not long; correctness checks factual alignment with retrieved or reference data.
const CUSTOM_CONCISENESS_PROMPT = `
<Rubric>
A perfectly concise answer:
- Contains only the exact information requested
- Omits unnecessary background
- Excludes meta-commentary
</Rubric>
<output>{outputs}</output>`;
const concisenessEvaluator = createLLMAsJudge({
prompt: CUSTOM_CONCISENESS_PROMPT,
feedbackKey: "conciseness",
continuous: true,
model: MODEL_PROVIDER_DEFAULTS.default,
});
LangSmith supports online evaluators (evaluations that run on real conversations in production). We use them to watch only “sensitive” cases, so we’re not burning compute on everything.
How we identify those cases:
Intent detection: The first step in our LangGraph flow classifies a query into high-priority categories—policy, competitor, safety, or voice_tone—using an LLM-as-a-judge classifier. This catches intent even when trigger words aren’t used (e.g., “speak like a pirate” → voice_tone). Tagged conversations are then eligible for evaluation or further testing.
Keyword triggers: We maintain a list of trigger words and phrases (“pirate”, “buy cheaper”, competitor brand names, etc.). If any are found in the user’s input or the assistant’s draft reply, the conversation is tagged.
Tool call patterns: If the assistant calls certain tools or fails to call expected ones (like the product search before recommending a wine), we mark it.
These tags, which are the same type we use in our dataset metadata, are attached to the conversation trace in LangSmith. Our online evaluation job simply queries for traces with those tags and scores a fixed percentage of them asynchronously. That way, sensitive cases get monitored continuously without slowing down real users.
// Run the LangGraph flow with the given user input
const result = await _graph.invoke(
{ messages: [{ role: 'user', content: inputs.input }] },
{ configurable: graph_configurable }
);
// Extract the last AI-generated message from the flow results
const lastAiMessage = result.messages?.filter(m => m._getType() === 'ai').pop();
// Extract the most recent tool call result (e.g., retrieved context from the vector DB)
const groundTruthFromVectorDBToolCall = result.messages?.filter(m => m._getType() === 'tool').pop();
// Evaluate groundedness by checking if the AI's answer is supported by the retrieved context
const groundedness = await groundednessEvaluator({
context: { documents: groundTruthFromVectorDBToolCall || [] },
outputs: { answer: lastAiMessage?.content }
});
// Assert that the groundedness score meets the minimum threshold (0.8)
expect(groundedness.score).toBeGreaterThanOrEqual(0.8);
By baking evaluations into our workflow, we moved from reactive fixes to proactive confidence:
Because we store past evaluation data, we can also retroactively run new evals on historical conversations for side-by-side comparisons. For example, if we’ve been using GPT-4.1 for months and GPT-5 comes out, we can re-score the same conversations with GPT-5 and instantly see how the two models perform. We can even run pairwise experiments, which are head-to-head tests where the judge sees two outputs. This can show, for example, GPT-5 with 95 thumbs-ups versus GPT-4.1’s 35, removing personal bias from the decision.
Stakeholders can see scores trending upward instead of relying on gut feel, and the conversation can shift from arguing about whether the assistant “seems fine” to discussing exactly which areas to improve next.
The pirate hasn’t been back!
With evaluations baked into every sprint, our wine sommelier agent now gets the same level of scrutiny as any mission-critical system, and it shows. By pairing a living test corpus with a well-calibrated AI judge and targeted real-time checks, we’ve moved from hoping the assistant behaves to knowing exactly when and why it doesn’t.
The result is a concierge that can recommend the right wine in the right voice, release after release. The journey isn’t over—new features, new models, and new edge cases will keep the work interesting—but improvements now feel deliberate and measurable, not accidental. And that’s a far better vintage to serve.
Stop using messages as your agent's memory. Learn how structured state makes AI agents more reliable, efficient, and production-ready.
Traditional approaches to change management weren’t working before. AI just makes the gaps impossible to ignore.
How smart companies are evolving with agent-powered delivery models, and what it takes to lead in the new era of intelligent services.