Language models are fluent, and fluency is the problem. Ask one to write about a framework release and it will give you a confident version number, a feature name, and a benchmark, any of which may be invented. For a personal site that is worse than useless. A made-up fact under my name costs more than the article was ever worth.

I built a drafting pipeline for this site's Insights section on the Laravel AI SDK. Generating text was the easy part. The work was in stopping it from making things up. These are the rules that do that, and where each one lives in the code.

The pipeline in one paragraph

Feeds I trust are fetched into source_items. An ArticleWriter agent drafts one article from one source item and returns structured output. The result is stored as an AiDraft with the status pending_review. I read it, edit it, and publish it from the admin, and only then does it become a public article. The scheduled pipeline is off by default behind a config flag (ai_articles.auto_pipeline), so nothing runs on its own until I have checked the voice by hand.

Rule 1: one source of truth for the rules

The voice guide, the factual rules, and the format rules live in one class, ArticleGuidelines. The in-app agent builds its prompt from it, and the MCP tool that briefs external agents returns the same text. If I tighten a rule, both paths change together. Two copies of a prompt drift within a month, and then you cannot tell which version produced which draft.

Rule 2: factual rules that give the model an honest exit

The rules are short and blunt:

  1. State ONLY facts present in the SOURCE MATERIAL, or facts you retrieve with tools from the source's own pages.
  2. NEVER invent version numbers, release dates, feature names, benchmarks, quotes, statistics, or product capabilities.
  3. If a detail is not in the sources, either OMIT it, or explicitly frame it as YOUR opinion or experience.
  4. No fabricated citations or links.
  5. If the source material is too thin to write a credible piece, DO NOT pad with filler. Set confidence to "low" and explain the gap.

The fifth rule does the most work. A model told to produce an article will produce one, and if the facts run out it fills the space. Giving it a sanctioned way to say "this source is thin" turns padding into a flag I can see in the review queue.

Rule 3: ground in one source, not the internet

The agent receives one source item: its title, URL, date, summary, and the fetched body, capped at 8,000 characters so a long page cannot flood the context. It has two tools, both with hard limits:

public function tools(): iterable
{
    return [
        (new WebFetch)->max(3),
        (new WebSearch)->max(3),
    ];
}

WebFetch exists so the agent can read the full source when only an excerpt was fetched. WebSearch is allowed only for light corroboration, and the instructions say it must never be the basis for a primary fact. Unbounded search is how a grounded article quietly becomes a summary of whatever ranked first.

Rule 4: structured output, not prose

The agent does not return an article. It returns fields:

return [
    'title' => $schema->string()->required(),
    'slug' => $schema->string()->required(),
    'excerpt' => $schema->string()->required(),
    'body' => $schema->string()->required(),
    'topic_tag' => $schema->string()->required(),
    'meta' => $schema->object(fn ($schema) => [
        'confidence' => $schema->string()->enum(['low', 'medium', 'high'])->required(),
        'sources_cited' => $schema->array()->items($schema->string()),
        'notes_for_review' => $schema->string(),
    ])->required(),
];

The meta object is the useful part. Every draft arrives with the model's own confidence, the URLs it says it relied on, and notes for me. A draft that cites a URL the source never contained is easy to spot and reject.

Rule 5: pin the model and the limits

The agent's settings are explicit attributes rather than "use the smartest model available":

#[Provider(Lab::Anthropic)]
#[Model('claude-sonnet-4-6')]
#[MaxSteps(6)]
#[MaxTokens(4096)]
#[Temperature(0.6)]
#[Timeout(180)]
class ArticleWriter implements Agent, HasStructuredOutput, HasTools

The model comes from config at runtime, so changing it is one environment variable, but it never changes by itself. When a model update shifts behavior, I want to have chosen that moment. MaxSteps and Timeout work like a watchdog: a confused agent stops after a few tool round trips instead of looping on my API bill.

Rule 6: validate before anything touches the database

The action that runs the agent treats its output as untrusted input:

  • An empty title or body throws an exception before anything is saved.
  • The topic tag must come from an allowed list. An invalid one falls back to the source's tag, then to a default.
  • The slug is checked against both drafts and published articles and suffixed if taken.
  • A missing excerpt is derived from the body.

None of this is clever. It is the same validation you would write for a form, applied to a model, which is effectively an anonymous user with good grammar.

Rule 7: keep what the model wrote

Each draft stores two bodies: body_generated, exactly what the model returned, and body, which I edit. The difference between them is the most honest feedback I have on the prompt. If I keep deleting the same kind of sentence, that becomes a rule. If I keep adding the same kind of detail, the source material is missing something.

Rule 8: render untrusted Markdown safely

When a draft is published, its Markdown goes through a small helper that escapes raw HTML and strips unsafe links such as javascript: URLs. A model cannot smuggle a script tag or a malicious link onto the public page, whether by mistake or because a source page told it to.

Rule 9: a person publishes

Nothing above makes a draft trustworthy. It makes a draft reviewable. The final rule is that every public article passes through a person who checks the claims against the sources. The pipeline exists to make that review fast, not to replace it. I wrote more about where to put those checkpoints in Human-in-the-Loop Approval Gates for AI Agents.

The same rules beyond articles

None of this is specific to blog posts. At VividSol we are bringing the same approach to AI features in our own products: a product listing drafted from a few words or a photo in LaunchLancer, and an invitation message writer with Urdu translation in Invitation Arts. The source of truth changes, from a feed item to a merchant's product details or a couple's event, but the rules do not: ground the model in what the user gave it, return structured fields, validate them, and let a person approve the result.

My take

Grounding is a property of the system, not of the prompt. The prompt is one layer. The tool limits, the structured output, the validation, the stored original, the sanitizer, and the review queue are the others. Any one of them alone leaks. Together, they turn a fluent model into a useful first-draft writer that tells you when it is out of its depth, and that is the only kind I would put my name on.