DEV Community

How we built a smart AI writing agent for blog articles

Most "AI writer" products do the same thing: prompt โ†’ blob of text โ†’ paste. It works for a demo and falls apart in production, because the value isn't the prose; it's the domain expert's judgment, and a one-shot generation has nowhere to put it. We build Larry, a tool that turns a lawyer's expertise into articles that get cited by Google, ChatGPT and Gemini. Here's the actual shape of our writing agent: three tools, an interview-first loop, and two patterns worth stealing for any "AI for experts" product.

The wrong shape: regenerate everything

The instinct is: user asks for a change โ†’ send the whole article back to the model โ†’ get a whole new article. This is bad because every regeneration drifts. The intro you liked changes. The one accurate statistic gets "improved" into a hallucination. You lose the expert's voice on every pass.

The fix is to treat the article like code: give the agent an editing tool that makes surgical changes to a document that already exists, not a firehose that rewrites it.

The stack (boring on purpose)

  • Next.js Route Handler (App Router) exposing the agent as a streaming SSE endpoint.
  • OpenRouter with the OpenAI SDK, so the model is a config value, not a rewrite. We use a cheap creative model for the first draft and a stronger tool-calling model for the interactive agent.
  • Supabase (Postgres): the article lives in a blog_posts row; the conversation and every tool call are persisted so the agent is stateful across turns.
  • Tavily for web search.

No agent framework. Just a while loop that calls the model, runs any tool calls, feeds the results back, and repeats until the model stops asking for tools.

The three tools

That's the entire toolbox. Resist adding more.

const tools = [
  {
    name: 'read_article',
    description: 'Read the current article (title, content, FAQ, tags, SEO metas) and author info.',
    parameters: {},
  },
  {
    name: 'replace_in_field',
    description: 'Replace text in a specific field of the article.',
    parameters: {
      field: ['title', 'content', 'faq', 'tags', 'excerpt', 'meta_title', 'meta_description', 'example_validation'],
      old_string: 'Exact text to replace. Empty = replace the whole field.',
      new_string: 'The new value.',
    },
  },
  {
    name: 'web_search',
    description: 'Search the web for current facts. Returns a source URL + snippet.',
    parameters: { query: 'string' },
  },
];

replace_in_field is the important one. old_string must match exactly one occurrence in the target field, the same contract as the Edit tool in a coding agent. The agent reads the article, finds the sentence to fix, and patches only that sentence. Everything else is byte-for-byte preserved. Voice and facts stop drifting because 95% of the document is never re-touched.

The workflow: interview first, write last

The system prompt enforces a strict sequence, and the golden rule is: never run a step that needs an answer without stopping for it.

  1. Analyze & research the topic.
  2. Capture expertise. Ask the lawyer 2โ€“3 precise questions, and always demand one concrete, anonymized real case or anecdote. Then wait.
  3. Propose a plan. Present the structure, then wait for approval.
  4. Write & optimize. Fill every field with replace_in_field.

Step 2 is where the product lives. Anyone can generate a generic article about a legal topic. The moat is the lawyer's own example: the exception, the mistake clients always make, the thing a Google search gets wrong. So the agent's first job isn't to write; it's to interview.

Here's the shape of that prompt:

You are capturing a lawyer's expertise on a specific topic. Ask 2โ€“3 precise questions, ONE exchange at a time. You MUST ask for one concrete, anonymized real case or anecdote that illustrates the topic. Wait for the answer before proposing a plan.

Two patterns worth stealing

1. A validation gate the model can't fake

Our quality bar requires a real anonymized example in the body. We enforce it with a boolean field, example_validation, that's also a tool target. The rule: the agent may only set it to true after re-reading the article with read_article and confirming the example is actually present. The model can't self-certify from memory; it has to go look. It's a cheap way to turn a soft "please include an example" into a hard state transition.

2. External URLs come from search only

The single biggest hallucination risk in legal content is invented case law and fake source links. The guardrail is a hard rule: every external URL in the article must come from a real web_search result, never from the model's memory. Research isn't optional polish; it's the only sanctioned source of links. (We also forbid citing specific law firms, for the same trust reason.)

There's also a silent self-critique pass after each edit: the agent re-checks its own output against the quality rules (direct-answer intro, question-form headings, short paragraphs, the example, the call-to-action) and patches the gaps before handing back, without narrating any of it to the user. The lawyer sees "done," not a checklist.

The takeaway

The model is a commodity you swap with an env var. The product is everything around it: capture the expert's judgment through an interview, edit the document with surgical patches instead of regenerating it, and gate quality with tools the model can't bluff past.

If you want to see the finished version aimed at lawyers, it's at larry-agent.com. Curious what other expert domains this shape would fit. What would you point it at?

Comments

No comments yet. Start the discussion.