Scaling AI Prompts: A Guide to Building a Prompt Management Engine in Rails
October 19, 2025
As AI features grow, hardcoded prompts become a significant source of technical debt. They are difficult to track, impossible to version, and a nightmare to A/B test. This guide presents a solution: treating your prompts as software by building a dynamic, database-backed management system in Rails.
We’ll design a versioned Prompt model and a PromptManager service that transforms prompt engineering from a chaotic art into a disciplined engineering practice.
When Do You Need a Prompt Engine?
This pattern is not for every project. Consider it when:
- You have more than a handful of prompts.
- Multiple teams or developers need to collaborate on prompts.
- You want to A/B test different prompt versions to improve performance.
- You need to update prompts without deploying new code.
If you only have one or two stable prompts, this is likely overkill.
Step 1: Designing a Version-Aware Prompt Model
Our model needs to track not just the prompt text, but also its version and status. A more advanced design might use separate Prompt and PromptVersion models, but for this guide, we’ll use a single model with a version number and an active flag.
db/migrate/YYYYMMDDHHMMSS_create_prompts.rb:
create_table :prompts do |t|
t.string :name, null: false
t.integer :version, null: false, default: 1
t.text :template, null: false
t.string :description
t.boolean :active, default: false, null: false
t.jsonb :metadata
t.timestamps
t.index [:name, :version], unique: true
t.index [:name, :active], where: "active = true", unique: true
end
This structure allows multiple versions of a prompt (summarizer v1, v2, etc.) but ensures only one version can be active for a given name at any time.
Step 2: The PromptManager Service
This service is the sole entry point for accessing prompts. It handles finding the correct version and rendering it with variables.
app/services/prompt_manager.rb:
class PromptManager
# Custom error for better handling
class PromptNotFoundError < StandardError; end
# Fetches the active prompt by name, or a specific version if provided.
def self.render(name:, version: nil, variables: {})
prompt = find_prompt(name, version)
# Using ERB for templating. For more complex logic, consider Liquid.
template = ERB.new(prompt.template)
template.result_with_hash(variables)
end
private
def self.find_prompt(name, version)
scope = Prompt.where(name: name)
prompt = version ? scope.find_by(version: version) : scope.find_by(active: true)
raise PromptNotFoundError, "Prompt '#{name}' not found." unless prompt
prompt
end
end
# Usage:
# Renders the active version
PromptManager.render(name: "summarizer", variables: { text: "..." })
# Renders a specific version for testing
PromptManager.render(name: "summarizer", version: 2, variables: { text: "..." })
Step 3: A Developer Workflow for Managing Prompts
How do you get prompts into the database? Relying on manual entry in a production console is not a scalable or repeatable process. The best practice is to define prompts in version-controlled files and use a Rake task to sync them.
db/prompts/summarizer.yml:
- version: 1
description: "Initial summary prompt."
active: false
template: |
Summarize this: {{text}}
- version: 2
description: "More detailed summary prompt with bullet points."
active: true
template: |
Summarize the following text into three concise bullet points:\n\n{{text}}
lib/tasks/prompts.rake:
namespace :prompts do
desc "Syncs prompts from YAML files into the database."
task sync: :environment do
Dir[Rails.root.join("db/prompts/*.yml")].each do |file_path|
name = File.basename(file_path, ".yml")
versions = YAML.load_file(file_path)
versions.each do |config|
prompt = Prompt.find_or_initialize_by(name: name, version: config['version'])
prompt.update!(config)
puts "Synced prompt: #{name} v#{config['version']}"
end
end
end
end
Now, your prompts live in version-controlled files, and rake prompts:sync becomes part of your deployment process, providing a reliable workflow.
Step 4: A/B Testing and Continuous Improvement
This engine makes A/B testing straightforward. To compare v2 and v3 of your summarizer prompt:
- Deploy both versions via the sync task, with
v2marked asactive. - In your application code, divert a percentage of traffic to explicitly render
v3. - Log the evaluation scores (using the framework from our first post!) for both versions.
- Compare the average scores to decide on a winner.
# In a controller or service
version_to_render = should_use_test_version? ? 3 : nil # nil renders the active version
prompt = PromptManager.render(name: "summarizer", version: version_to_render, ...)
# ... then log the results of this interaction against the version used.
Prompts as Code
By moving your prompts from hardcoded strings into a version-controlled, database-backed system, you treat them with the same discipline as the rest of your codebase. This engine provides a foundation for collaboration, testing, and continuous improvement, allowing you to scale your AI features responsibly. It adds a layer of complexity, but for any project serious about prompt engineering, the benefits in stability and manageability are well worth the investment.