35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Rails AI Agents: Autonome Multi-Step Workflows met Claude en Tool Use

Rails AI Agents: Autonome Multi-Step Workflows met Claude en Tool Use

Roger Heykoop
Ruby on Rails, Artificial Intelligence
Rails AI agents die werken in productie — agent loop, tool registry, state machines, retries, kostenbeheersing en observability met Claude tool use.

Een founder huurde me afgelopen winter in om “een AI agent te redden”. De pitch was sterk: een Rails-app die een klantmail oppakte, de bestelling in hun database opzocht, de carrier-API checkte, een refund of vervanging opstelde en het antwoord stuurde. De demo werkte. Productie niet. Het ding bleef oneindig loopen op vage mails, sloeg OpenAI 1.000 keer aan voordat het doorhad dat het in de war was, en mailde af en toe vier refunds voor dezelfde bestelling. Het team noemde het “de agent” en zat twee maanden voor het opraken van hun Anthropic-credits toen ik binnenkwam.

We hebben het in drie weken herbouwd. De nieuwe versie verwerkt 4.200 mails per dag, kost 71% minder, en heeft in zes maanden niemand dubbel gerefund. De truc was geen slimmere prompt — het was de Rails AI agent behandelen als een state machine met een budget, niet als een chat-loop met goede hoop. Na negentien jaar Rails heb ik een uitgesproken mening over agents in 2026: het is een software-engineeringprobleem dat zich vermomt als een prompt-engineeringprobleem, en Rails is bijzonder goed in het engineering-gedeelte.

Deze post is de architectuur die ik gebruik voor productie-Rails AI agents: de agent loop, de tool registry, state-persistentie, retries, kostenbeheersing en de valkuilen die teams treffen die uit een blogpost copy-pasten en gaan deployen.

Wat een Rails AI Agent Werkelijk Is

Een Rails AI agent is een controller-van-zijn-eigen-uitvoering die, gegeven een doel, tools kiest om aan te roepen tot het doel bereikt is of een budget op is. Het model bepaalt de volgende stap; jouw code voert hem uit; het resultaat voedt de volgende beslissing. Dat is het. Al het andere — geheugen, planning, “reflectie” — wordt bovenop deze loop gebouwd.

Drie onderdelen maken het verschil tussen een demo en een productiesysteem:

  • Een begrensde loop. Elke beurt roept ofwel een tool aan ofwel geeft een eindantwoord. Totaal aantal beurten en totaal aantal tokens zijn hard gecapt. Geen onbegrensde recursie, ooit.
  • Een tool registry met expliciete contracten. Elke tool is een Ruby-class met een JSON-schema, een deterministische executor en een permission check. Het model raakt jouw database nooit direct aan.
  • Duurzame state. De run van de agent leeft in Postgres, niet in process-geheugen. Als de server midden in een run herstart, pakt de agent op waar hij gebleven was — of niet, en wordt er iemand gepiept.

Als je mijn eerdere post over LLM function calling in Rails hebt gelezen, heb je het fundament al. Een Rails AI agent is function calling in een loop, met state, budgetten en observability erbovenop.

De Agent Loop

De kern van elke Rails AI agent is de loop. De mijne past in zo’n 80 regels Ruby en heeft precies drie exit-condities: het model geeft een eindantwoord, we raken het beurtenbudget, of een tool werpt een onherstelbare fout.

# app/services/agent_runner.rb
class AgentRunner
  MAX_TURNS = 12
  MAX_TOKENS = 100_000

  def initialize(run:, tools: ToolRegistry.default)
    @run = run
    @tools = tools
    @client = Anthropic::Client.new(api_key: ENV.fetch("ANTHROPIC_API_KEY"))
  end

  def call
    @run.start!

    MAX_TURNS.times do |turn|
      response = call_model(messages: @run.messages_for_api)
      @run.record_assistant_turn!(response, turn:)

      return finalize!(response) if response.stop_reason == "end_turn"

      tool_results = execute_tools(response.tool_uses)
      @run.record_tool_results!(tool_results)

      raise BudgetExceeded if @run.tokens_used >= MAX_TOKENS
    end

    @run.fail!(reason: "max_turns_exceeded")
  end

  private

  def call_model(messages:)
    @client.messages.create(
      model: "claude-sonnet-4-6",
      max_tokens: 4_096,
      system: AgentPrompt.for(@run),
      tools: @tools.to_anthropic_schema,
      messages: messages
    )
  end

  def execute_tools(tool_uses)
    tool_uses.map do |tu|
      tool = @tools.find!(tu.name)
      tool.authorize!(@run.user)
      result = tool.call(**tu.input.symbolize_keys)
      { tool_use_id: tu.id, content: result.to_s, is_error: false }
    rescue ToolError => e
      { tool_use_id: tu.id, content: e.message, is_error: true }
    end
  end

  def finalize!(response)
    text = response.content.find { |c| c.type == "text" }&.text
    @run.complete!(final_answer: text)
    text
  end
end

Een paar dingen om op te merken. De loop is begrensd door MAX_TURNS, niet door “tot het model klaar is”. Tokengebruik wordt elke beurt afgezet tegen een hard plafond — Claude’s tool-use output is JSON-zwaar en loopt warm. Tool errors crashen de loop niet; ze gaan terug naar het model als is_error: true, waardoor het model kan herstellen of opgeven. En de run wordt elke beurt gepersisteerd, dus een process-restart verliest geen werk.

Dit is de saaie kern. Het interessante werk gebeurt in de tool registry en het state-model.

De Tool Registry

De grootste fout die ik zie in Rails AI agent-code is het model rechtstreeks met Active Record laten praten. “Geef het gewoon een find_user-tool die elke SQL accepteert.” Dat is geen agent, dat is een SQL-injectievector met een budget.

Elke tool die ik uitlever heeft vier eigenschappen: een stabiele naam, een JSON-schema voor input, een permission check gekoppeld aan de gebruiker van de agent, en een executor die een klein gestructureerd resultaat teruggeeft. De registry is een gewone Ruby-class:

# app/agents/tools/base.rb
module Tools
  class Base
    class_attribute :tool_name, :description, :input_schema

    def initialize(run)
      @run = run
    end

    def authorize!(user)
      raise UnauthorizedTool unless permitted_for?(user)
    end

    def permitted_for?(user) = true

    def call(**) = raise NotImplementedError

    def self.to_anthropic_schema
      {
        name: tool_name,
        description: description,
        input_schema: input_schema
      }
    end
  end
end

Een concrete tool — een bestelling opzoeken op nummer voor de customer-support agent:

# app/agents/tools/lookup_order.rb
module Tools
  class LookupOrder < Base
    self.tool_name = "lookup_order"
    self.description = "Look up an order by its order number. Returns status, " \
                       "items, total, and the most recent shipping event."
    self.input_schema = {
      type: "object",
      properties: {
        order_number: { type: "string", description: "Order number, e.g. ORD-12345" }
      },
      required: ["order_number"]
    }

    def permitted_for?(user)
      user.role.in?(%w[support admin])
    end

    def call(order_number:)
      order = Order.find_by(number: order_number)
      return { error: "not_found" } if order.nil?
      return { error: "forbidden" } unless order.account_id == @run.account_id

      OrderSummarySerializer.new(order).as_json
    end
  end
end

Drie dingen die hier goed gaan. De tool retourneert alleen de data die de agent nodig heeft — de serializer is een bewuste keuze. De permission check koppelt de tool aan de gebruiker van de run, niet aan de gril van het model. En het resultaat is kleine JSON; je voert de agent geen ActiveRecord-blob van 4 KB tenzij je het leuk vindt om voor tokens te betalen.

De registry zelf is gewoon een lijst:

# app/agents/tool_registry.rb
class ToolRegistry
  def self.default
    new([
      Tools::LookupOrder,
      Tools::CheckShipmentStatus,
      Tools::IssueRefund,
      Tools::DraftReply
    ])
  end

  def initialize(tool_classes)
    @tool_classes = tool_classes
  end

  def find!(name)
    klass = @tool_classes.find { |c| c.tool_name == name }
    raise UnknownTool, name if klass.nil?
    klass.new(@run)
  end

  def to_anthropic_schema
    @tool_classes.map(&:to_anthropic_schema)
  end
end

Ik namespace tools per domein (Tools::Support::*, Tools::Billing::*) zodra een app meer dan ~15 tools heeft. Daaronder is plat prima.

Duurzame State Met Active Record

Demo’s houden agent-state in geheugen. Productie houdt het in Postgres. Een AgentRun-model is de ruggengraat van een Rails AI agent:

# db/migrate/20260506_create_agent_runs.rb
class CreateAgentRuns < ActiveRecord::Migration[8.0]
  def change
    create_table :agent_runs do |t|
      t.references :user, null: false, foreign_key: true
      t.references :account, null: false, foreign_key: true
      t.string :agent_kind, null: false
      t.string :status, null: false, default: "pending"
      t.string :failure_reason
      t.jsonb :input, null: false, default: {}
      t.jsonb :messages, null: false, default: []
      t.text :final_answer
      t.integer :turns_used, null: false, default: 0
      t.integer :tokens_used, null: false, default: 0
      t.integer :cost_cents, null: false, default: 0
      t.timestamps
    end

    add_index :agent_runs, [:status, :agent_kind]
    add_index :agent_runs, :created_at
  end
end

Status gebruikt een state machine. Ik houd hem expliciet:

# app/models/agent_run.rb
class AgentRun < ApplicationRecord
  STATUSES = %w[pending running completed failed cancelled].freeze
  validates :status, inclusion: { in: STATUSES }

  belongs_to :user
  belongs_to :account

  def start!
    update!(status: "running")
  end

  def complete!(final_answer:)
    update!(status: "completed", final_answer:)
  end

  def fail!(reason:)
    update!(status: "failed", failure_reason: reason)
  end

  def record_assistant_turn!(response, turn:)
    self.messages = messages + [serialize_assistant(response)]
    self.turns_used = turn + 1
    self.tokens_used += response.usage.input_tokens + response.usage.output_tokens
    self.cost_cents += CostCalculator.cents_for(response.usage)
    save!
  end

  def record_tool_results!(results)
    self.messages = messages + [{ role: "user", content: results }]
    save!
  end

  def messages_for_api
    [{ role: "user", content: input.fetch("prompt") }] + messages
  end
end

Elke beurt wordt gepersisteerd voordat de volgende API-call gedaan wordt. Als de worker tussen beurten crasht, pakt een recovery-job running runs ouder dan vijf minuten op en hervat ze of markeert ze als failed. Deze ene beslissing — Postgres als geheugen van de agent — is wat een Rails AI agent veilig genoeg maakt om op productieverkeer los te laten.

Ik draai de agent in een Solid Queue background job. Inline uitvoering vanuit een controller is prima voor prototypes; productie-agents hebben retries, queues en concurrency-limieten nodig.

Kostenbeheersing en Prompt Caching

Agents zijn duur. Een naïeve customer-support agent draaide ons op €0,18 per mail tot we twee dingen toevoegden: prompt caching en agressief snoeien in de system prompt.

De system prompt voor een Rails AI agent is lang — die somt elke tool, het beleid en de guardrails op. Cachen betekent dat je één keer per vijf minuten de volle prijs betaalt en 10% bij elke volgende call. De besparing op een run van 12 beurten is fors:

@client.messages.create(
  model: "claude-sonnet-4-6",
  max_tokens: 4_096,
  system: [
    {
      type: "text",
      text: AgentPrompt.policy_block,
      cache_control: { type: "ephemeral" }
    },
    {
      type: "text",
      text: AgentPrompt.tools_block(@tools),
      cache_control: { type: "ephemeral" }
    }
  ],
  tools: @tools.to_anthropic_schema,
  messages: messages
)

Het volledige patroon staat in mijn prompt caching guide. Voor agents specifiek: cache het beleid en de tooldefinities; cache nooit user input. Gecombineerd met kleinere tool-resultaten brachten we de kosten per run met 71% omlaag.

Failure Modes Die Agents in Productie Slopen

Dit zijn de failures die ik bij elke Rails AI agent-rescue tegenkom.

Oneindige tool-loops. Het model blijft lookup_order aanroepen met licht andere input omdat het beleid onduidelijk is. Fix: harde turn-cap, plus een dedup-check op zelfde-tool-zelfde-input die teruggeeft “je hebt deze tool al met deze argumenten aangeroepen — probeer een andere aanpak”.

Gehallucineerde tools. Het model verzint een toolnaam. Fix: de registry werpt UnknownTool, de loop geeft de fout terug aan het model, en in 95% van de gevallen herstelt het. Log elke UnknownTool — meestal betekent het dat je toolbeschrijvingen slecht zijn.

Permission drift. De agent heeft tools die zijn user-rol niet zou moeten hebben. Fix: authorize! op elke tool-call, scoped op de user van de run. Behandel het model als een untrusted client.

Tokenexplosie door tool-resultaten. Een tool retourneert 200 KB JSON; de volgende beurt klapt. Fix: elke tool retourneert een resultaat kleiner dan 4 KB, gepagineerd indien nodig. Als een tool meer moet teruggeven, sla de data op, retourneer een referentie-token en voeg een read_data(token)-tool toe.

Stille overschrijdingen. De agent eindigt “succesvol” maar is onderweg ver over budget. Fix: kosten en tokens zijn first-class velden op AgentRun, alerten op het 80e percentiel, dashboards in Grafana. Je controleert niet wat je niet meet.

Dubbele acties door restarts. Een tool-call stuurde een mail, de worker crashte, de run hervat en stuurt ‘m opnieuw. Fix: side-effect tools zijn idempotent — gekeyd op agent_run_id en een stabiele operation key. Hetzelfde als webhook idempotency, exact.

Wanneer Wel en Niet Een Rails AI Agent Bouwen

Agents zijn niet het antwoord op elke AI-feature. De eerlijke test die ik bij klanten draai: als een deterministische state machine, één enkele prompt of een RAG-lookup het probleem kan oplossen, bouw dan geen agent. Agents zijn de juiste tool wanneer het pad vertakt op basis van wat je vindt — wanneer stap drie afhangt van wat stap twee teruggaf en je de takken niet vooraf kunt opsommen.

Voorbeelden waar agents hun kosten verdienen:

  • Customer-support triage die kiest tussen refund, vervanging, escalatie of hand-off naar een mens.
  • Onderzoeks-workflows die websearch, database-lookups en samenvattingen in onvoorspelbare volgorde combineren.
  • Code review- of QA-bots die bestanden lezen, checks draaien en beslissen wat ze flaggen.
  • Sales-ops automatiseringen die een account opzoeken, openstaande opportunities checken en een follow-up opstellen.

Voorbeelden waar ik tegengas geef:

  • “Genereer een omschrijving uit deze velden.” Dat is één prompt.
  • “Vind gerelateerde documenten.” Dat is RAG, geen agent.
  • “Verwerk vannacht 50.000 records.” Dat is Anthropic’s batch API, geen real-time agent.
  • Alles waarbij de kosten van een verkeerde actie hoog zijn en het model geen mens in de loop heeft. Agents die refunds kunnen uitvoeren hebben expliciete menselijke approval-gates boven een drempel nodig. Altijd.

Productie-Setup Die Ik Aanraad

Een Rails AI agent die ik op echt verkeer zou zetten ziet er zo uit: dedicated Solid Queue worker voor agent-runs, concurrency gecapt op 5 per gebruiker en 50 globaal, een cancel!-endpoint dat de run-status omzet en de volgende turn-check kortsluit, prompt caching op system + tools blocks, gestructureerde logging op elke tool-call, en een Grafana-dashboard met turns-per-run, tokens-per-run, kosten-per-run en failure-reason breakdown.

Ik monitor drie metrics obsessief: p95 turns-per-run (zou moeten dalen naarmate prompts rijpen), p95 kosten-per-run (zou vlak moeten zijn), en tool error rate (zou onder 5% moeten blijven). Als één van die drift, haal ik de laatste honderd runs erbij en lees ik ze. Agent-debugging is transcripten lezen; tooling vervangt dat nog niet.

FAQ

Wat is het verschil tussen een Rails AI agent en gewoon een LLM aanroepen met tools?

Een agent draait de tool-use loop autonoom over meerdere beurten tot een doel bereikt is of een budget op is. Eén enkele LLM-met-tools call retourneert één tool-keuze of één antwoord. Agents voegen de loop toe, de state-persistentie, de budgetcontrole en de recovery-logica. Als je maar één tool-call nodig hebt, heb je geen agent nodig — dan heb je function calling nodig.

Moet ik LangChain gebruiken of zelf een Rails AI agent framework bouwen?

Voor Rails-apps: bouw je eigen — het is 200 regels Ruby en je controleert elk primitive. LangChain lost Python-ecosysteemproblemen op die Ruby niet heeft, en voegt abstracties toe die productie-debugging lastiger maken. De Anthropic- en OpenAI-Ruby-SDK’s geven je alles wat je nodig hebt; de agent loop is werkelijk een klein stuk code.

Hoe voorkom ik dat een Rails AI agent een gigantische rekening oploopt?

Harde caps op beurten, harde caps op tokens, prompt caching op de system prompt, en concurrency-limieten per gebruiker. Track cost_cents op elke run en alert wanneer de dagelijkse spend per agent-soort een drempel overschrijdt. Ik heb nog nooit een runaway-rekening gezien van een agent die een MAX_TOKENS-check op elke iteratie van de loop had.

Kan ik een Rails AI agent tegen open-source modellen draaien in plaats van Claude?

Ja — de loop is model-agnostisch. Het knelpunt is tool-use betrouwbaarheid. Frontier-modellen (Claude, GPT-4-class) roepen tools 95-99% van de tijd correct aan; kleinere open-source modellen zakken naar 70-85% en beginnen toolnamen of argumenten te hallucineren. Gebruik open-source modellen voor de goedkope onderdelen van de pipeline (extractie, classificatie) en Claude voor de orchestratie-laag die beslist welke tool wordt aangeroepen.

Bezig met bouwen of redden van een Rails AI agent? TTB Software levert AI-features in Rails-apps als fractional CTO-engagement — agents, RAG, tool use, de productie-hardening die demo’s omzet in systemen. Negentien jaar Rails, met de bonnen erbij.

#rails-ai-agents #ai-agents-ruby #claude-tool-use #llm-multi-step-workflows #autonomous-agents-rails #ruby-on-rails #rails-8
R

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation