Developer Instructions: Bake System Prompt into Ollama Modelfile

Overview

We want to bake the GrowReach v2 system prompt (14,253 characters) into an Ollama custom model so that:

  1. The developer only sends the user prompt in each API call (not the system prompt)
  2. The system prompt is versioned as part of the model (growreach-v2, growreach-v3, etc.)
  3. We maintain clean separation between system logic and dynamic user data

Step 1: Install Ollama (if not already installed)

# On the server where you run Ollama
curl -fsSL https://ollama.com/install.sh | sh
 
# Verify installation
ollama --version

If you are using Ollama Cloud (not self-hosted), check if your cloud provider supports custom Modelfiles. If they do not, you will need to self-host Ollama for this approach. See the “Ollama Cloud Limitation” section at the bottom.


Step 2: Pull the Base Model

# Pull Gemma 4 (or whichever Gemma model variant you use)
ollama pull gemma4:latest
 
# Verify it is available
ollama list

If you use a specific tag (e.g., gemma4:9b or gemma4:27b), use that tag instead of latest.


Step 3: Create the Modelfile

Create a file named Modelfile (no extension) in a directory like /opt/growreach/:

mkdir -p /opt/growreach
cd /opt/growreach
nano Modelfile

Paste the following content into the Modelfile:

FROM gemma4:latest
 
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER num_ctx 8192
 
SYSTEM """
[PASTE THE ENTIRE CONTENTS OF v2-system-prompt.md HERE]
"""

What each line does:

LinePurpose
FROM gemma4:latestBase model to build on
PARAMETER temperature 0.7Creativity level. 0.7 gives variety without going off the rails. Adjust between 0.5-0.8 based on testing.
PARAMETER top_p 0.9Nucleus sampling. 0.9 allows some diversity while avoiding very low probability tokens.
PARAMETER num_ctx 8192Context window size in tokens. System prompt is ~3,500 tokens. User prompt + post content is ~500-800 tokens. Output is ~100-200 tokens. 8,192 gives comfortable headroom. If the model supports a larger context window, you can increase this.

Important: Paste the FULL system prompt

Copy the entire contents of v2-system-prompt.md (the file from the GitHub repo at growreach-prompts-v2/v2-system-prompt.md) and paste it between the triple quotes of the SYSTEM parameter. Do not truncate, summarize, or compress it. Every character matters.

Example of what the final Modelfile should look like:

FROM gemma4:latest
 
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER num_ctx 8192
 
SYSTEM """
You are GrowReach, an AI that generates authentic LinkedIn comments on behalf of a real professional. Your comment will be posted publicly under a real person's name. The comment must be indistinguishable from something that person would have typed themselves.
 
You are NOT writing "content." You are writing a comment — a real human reaction to a real post. Think about how people actually comment on LinkedIn. They scroll, they react, they type something quick, they move on. They do not write essays. They do not follow templates. They do not always end with a question. They do not always mention where they work. They sound like themselves, not like a brand.
 
Your single most important goal: if someone sees 5 comments from this account, they should not be able to tell that a machine wrote any of them.
 
[... the rest of the entire v2-system-prompt.md content ...]
 
- Do not include any text outside the JSON object. No preamble. No postscript.
"""

Step 4: Create the Custom Model

cd /opt/growreach
ollama create growreach-v2 -f Modelfile

This creates a new model called growreach-v2 that has the system prompt baked in. It will take a few seconds to a minute depending on the model size.

Verify the model was created:

ollama list
# You should see:
# NAME            ID          SIZE    MODIFIED
# growreach-v2    abc123...   5.2GB   2 minutes ago
# gemma4:latest   def456...   5.2GB   5 minutes ago

Step 5: Test the Model Manually

Before integrating with the backend, test it from the command line:

# Test with a simple user prompt (simulating what your backend will send)
ollama run growreach-v2 'Generate a LinkedIn comment for the following user and post.
 
== COMMENTER PROFILE ==
Name: Rahul
Role: Founder at 10xers Labs
Industry: AI/Technology
Domain Expertise: AI-native engineering, product growth, first-principles problem solving
Years of Experience: 12
 
== BUSINESS CONTEXT ==
Website: https://10xers.co
 
== VOICE BRIEF ==
Rahul is the founder of 10xers Labs, a digital product studio specializing in AI-native engineering and product growth. He is a seasoned operator who scaled super-apps across India, Southeast Asia, and MENA, with significant leadership roles at Go-Jek, Careem, and Ola. His communication style is that of a technical founder—direct, analytical, and focused on MVP-driven value.
 
== COMMENTING STYLE ==
Tone: Conversational
Persona: Data-Driven Analyst, Industry Expert, Thought Leader
Priority: Metrics first
Target Audience: Consultants and Advisors, Executives and Business Leaders, Industry Peers, Startups
Content Goal: Build Authority, Find Potential Partners, Generate Leads
Use Emojis: No
 
== CUSTOM INSTRUCTIONS ==
DOs: Try to comment between 80 to max 150 words. Add the comment in a way that invites further engagement.
DON'"'"'Ts: Do not use Em Dashes, En Dashes and Hyphens. Do not make the comment longer than 150 words.
 
== POST TO COMMENT ON ==
Author: Nate
Author Title: Content Creator
Author Company: Independent
 
Post Content:
"""
AGI Is Already Here, And Anthropic Just Proved It.
 
Anthropic just dropped a report called "When AI Builds Itself."
 
I read it a few times, and I walked away convinced AGI is not some far-off thing we are all sitting around waiting for.
 
By the definition that actually matters to me, it is already here.
 
The gap is getting wider every single month.
"""
 
== INSTRUCTIONS ==
Read the post carefully. Pick the best comment structure from the 6 defined in the system prompt. Write the comment in the voice of the commenter. Return ONLY the JSON object.'

The model should return a JSON object like:

{
  "comment": "The gap between using AI as a search box versus a full team is where most companies are stuck. We see this constantly at 10xers. Leaders want productivity gains per task but the real shift is autonomous orchestration.\n\nThe 76% success rate on open-ended problems is the signal. The bottleneck is no longer the model. It is whether organizations trust the output without a human in every loop.",
  "strategy_name": "direct_observation",
  "strategy_used": "direct_observation",
  "voice_profile": "senior AI founder, direct and analytical",
  "voice_description": "Conversational tone, referenced 10xers naturally, no question ending, no dashes",
  "word_count": 62
}

What to check in the test output:

  1. Does the comment avoid em dashes, en dashes, and hyphens? (hard rule)
  2. Is the word count within the structure’s range?
  3. Is the JSON valid and complete?
  4. Does the comment sound like Rahul, not like an AI?
  5. Does it pick one of the 6 strategy tags?
  6. No banned phrases (“delve into”, “game changer”, etc.)?

Step 6: Update Your Backend API Call

Before (current approach — sending both system + user prompt):

# OLD WAY - sending system prompt every time
import requests
 
response = requests.post("http://localhost:11434/v1/chat/completions", json={
    "model": "gemma4:latest",
    "messages": [
        {
            "role": "system",
            "content": "<paste 14,253 char system prompt here>"
        },
        {
            "role": "user",
            "content": user_prompt_with_variables_filled
        }
    ],
    "temperature": 0.7
})

After (new approach — only sending user prompt):

# NEW WAY - system prompt is baked into the model
import requests
 
response = requests.post("http://localhost:11434/v1/chat/completions", json={
    "model": "growreach-v2",  # <-- changed from "gemma4:latest" to "growreach-v2"
    "messages": [
        {
            "role": "user",
            "content": user_prompt_with_variables_filled  # <-- only user prompt, no system message
        }
    ],
    "temperature": 0.7
})

That is the only change needed:

  1. Change "model" from "gemma4:latest" to "growreach-v2"
  2. Remove the system message from the messages array
  3. Keep everything else the same

The system prompt is already loaded in the model. Ollama sends it automatically with every request to growreach-v2.


Step 7: Version Management

When you update the system prompt (v3, v4, etc.):

# 1. Update the Modelfile with the new system prompt
cd /opt/growreach
nano Modelfile  # paste the new v3 system prompt into SYSTEM block
 
# 2. Create the new version
ollama create growreach-v3 -f Modelfile
 
# 3. Update your backend to point to the new model
# Change "growreach-v2" to "growreach-v3" in your API call
 
# 4. Test the new version
ollama run growreach-v3 "test prompt here"
 
# 5. Old version (growreach-v2) is still available for rollback
ollama list  # both v2 and v3 should appear

Versioning best practices:

  • Keep all Modelfile versions in git (the hermes repo)
  • Name them: Modelfile.v2, Modelfile.v3, etc.
  • Or keep one Modelfile and commit changes to git
  • Always test a new version before switching the backend to it
  • Keep the previous version available for at least 1 week after switching (rollback safety)

Step 8: Save the Modelfile to the GitHub Repo

# Copy the Modelfile to the hermes repo
cp /opt/growreach/Modelfile /opt/data/hermes-repo/growreach-prompts-v2/Modelfile
 
# Commit and push
cd /opt/data/hermes-repo
git add growreach-prompts-v2/Modelfile
git commit -m "Add Ollama Modelfile for growreach-v2 model"
git push origin main

Troubleshooting

”Error: model not found”

Make sure you ran ollama create growreach-v2 -f Modelfile and it completed without errors. Run ollama list to verify.

”The model is not following the system prompt rules”

  1. Check that the FULL system prompt was pasted into the Modelfile (not truncated)
  2. Check for any special characters that might have been escaped incorrectly
  3. Run ollama show growreach-v2 --modelfile to see what Ollama actually stored
  4. Test with ollama run growreach-v2 interactively

”JSON output is not valid”

Gemma 4 may sometimes add text before or after the JSON. If this happens:

  1. Add this to the Modelfile as a PARAMETER: PARAMETER stop "\n\n\n" (stops generation after triple newline)
  2. Or parse the JSON more defensively in your backend code (extract the first { to the last })

“Model is too slow”

  1. Check if you are using a quantized model (e.g., gemma4:9b-q4_K_M instead of gemma4:latest)
  2. Increase num_gpu parameter if you have a GPU: PARAMETER num_gpu 1
  3. Reduce num_ctx if you do not need a large context window

Ollama Cloud Limitation

If you are using Ollama Cloud (not self-hosted Ollama), check if your cloud provider supports custom Modelfiles. Some Ollama Cloud providers only expose pre-built models and do not allow creating custom models via ollama create.

If Ollama Cloud does NOT support custom Modelfiles:

Option A: Self-host Ollama on your VPS

  • Install Ollama on your Hostinger VPS
  • Run it as a background service
  • Create the custom model there
  • Point your backend to http://localhost:11434/v1/chat/completions
  • Cost: VPS compute (you already have the VPS), no per-token cloud cost

Option B: Keep sending the full system prompt via Ollama Cloud

  • Use Option 1 from the architecture doc (send system prompt every time)
  • At pre-revenue scale, the extra ~3,500 tokens per call is negligible
  • Switch to self-hosted when you want the Modelfile optimization

Option C: Use OpenRouter instead of Ollama Cloud

  • OpenRouter supports custom system prompts in API calls
  • But does not support Modelfiles (you send system prompt each time)
  • However, OpenRouter may have cheaper Gemma 4 pricing than Ollama Cloud

Recommendation:

If you are currently on Ollama Cloud, check Modelfile support first. If not supported, self-host Ollama on your Hostinger VPS. You already have the VPS, so the only cost is compute (which you are already paying for). This gives you full control, Modelfile support, prefix caching, and zero per-token API costs.