How to Craft System Prompts that Guide Multi‑Step AI Agents with Real RSS Data

You’re tired of chasing flaky LLM responses—agents that skip a step, repeat work, or hallucinate facts—especially when you’re feeding them live RSS feeds for finance‑oriented analytics. The fix isn’t a bigger model; it’s a disciplined system prompt that isolates context, defines a precise role, and forces the model through an explicit step list. Below I break down the anatomy of such a prompt, show how to chunk the Stripe blog RSS feed without blowing the token budget, and give you a production‑ready script that assembles the prompt, validates each stage, and logs the plan for audit.

System Prompt Anatomy for Multi‑Step Agents

The prompt consists of four logical blocks:

  1. ROLE: tells the model what it is (e.g., “You are a data‑extraction assistant for fintech blogs”).
  2. CONTEXT: the raw material the model may reference—here a deterministic chunk of title + summary from each RSS entry.
  3. CONSTRAINTS: token budget, no external calls, and a requirement to output JSON.
  4. STEPS: a numbered list with clear delimiters (e.g., ---STEP 1---) that forces the model to finish one task before moving to the next.

When each block lives on its own line and the steps are wrapped in unmistakable markers, the model’s “reasoning path” becomes visible and can be validated before the next stage runs.

Prompt Chaining Patterns and Token Management

With an RSS feed you typically have dozens of short articles. Feeding them all at once exceeds most LLM context windows, so we chunk deterministically:

  • Collect title + summary for each entry.
  • Estimate tokens with a cheap word‑count proxy (tokens ≈ words × 1.3).
  • Accumulate chunks until MAX_WORDS (default 3000) is reached; truncate the last chunk safely.

The script below implements this logic, prints the estimated token count, and then builds the system prompt using the four blocks described earlier.

Key Takeaways

  • Separate role, context, constraints, and step list in the system prompt; this structural clarity cuts hallucinations.
  • Use explicit step delimiters (e.g., ---STEP 1---) to force the model to follow a prescribed order.
  • Limit context length with deterministic chunking and a simple word‑count token budget.
  • Validate each step’s JSON output before passing it to the next stage; malformed output aborts the pipeline.
  • Log the final composed prompt and its metadata (token estimate, entry IDs) for reproducibility and audit.

Data and Sources

Data accessed on 2026-11-05.

Limitations and Tradeoffs

The approach deliberately trades off exact token budgeting for simplicity. A word‑count proxy is fast but can be off by 10‑15 % for languages with many short tokens. In production you would replace it with the provider’s tokenizer (e.g., tiktoken for OpenAI models). The script also stops at prompt construction; it does not verify LLM output offline, so you still need a downstream validation step when you call the model. Finally, for extremely short tasks (single‑sentence extraction) a monolithic prompt can be cheaper than a multi‑step chain, because the overhead of step delimiters adds tokens without benefit.

Frequently Asked Questions

Do I need an OpenAI key for the demo?

No. The script only assembles and prints the system prompt; it does not invoke any LLM endpoint.

Can I replace the RSS source with another feed?

Yes. Any URL that returns a valid RSS/Atom XML containing title and summary (or description) can be used—just update FEED_URL.

How do I measure hallucination reduction?

Run the full pipeline with an LLM, capture each step’s JSON output, and compare the fields against the known titles and metrics from the feed. A lower mismatch rate indicates the prompt is doing its job.

What if the feed is empty?

The script logs a warning and exits gracefully, prompting you to check the source URL.

Is word‑count a reliable token estimate?

It is sufficient for illustration but not production‑grade. For precise budgeting switch to the official tokenizer of your LLM provider.

Recommendation and Next Step

Adopt the three‑section system‑prompt template (ROLE | CONTEXT | STEPS) for any multi‑step agent that consumes semi‑structured text. Integrate step‑wise JSON validation as shown, then move to real LLM calls (e.g., gpt‑4o) while monitoring token usage with tiktoken. The next concrete step is to replace the word‑count proxy with tiktoken.encode() and wire the script to an OpenAI endpoint, logging each step’s response for continuous quality monitoring.

Complete Script

import json
import logging
import sys
import urllib.error
import urllib.request
from pathlib import Path

import feedparser

# ----------------------------------------------------------------------
# Configuration
# ----------------------------------------------------------------------
FEED_URL = "https://stripe.com/blog/feed.rss"
MAX_WORDS = 3000               # Rough token budget (words * 1.3 ≈ tokens)
OUTPUT_JSON = Path("prompt_plan.json")
TIMEOUT_SECONDS = 10

# ----------------------------------------------------------------------
# Logging setup
# ----------------------------------------------------------------------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    handlers=[logging.StreamHandler(sys.stdout)],
)

def fetch_feed(url: str) -> feedparser.FeedParserDict:
    """Fetch RSS feed with timeout handling."""
    try:
        with urllib.request.urlopen(url, timeout=TIMEOUT_SECONDS) as resp:
            if resp.status != 200:
                logging.error("HTTP %s returned for %s", resp.status, url)
                sys.exit(1)
            data = resp.read()
    except urllib.error.URLError as e:
        logging.error("Network error while fetching feed: %s", e)
        sys.exit(1)
    return feedparser.parse(data)

def word_count(text: str) -> int:
    """Simple word count proxy for token estimation."""
    return len(text.split())

def build_chunks(entries):
    """Deterministically accumulate title+summary chunks until MAX_WORDS."""
    chunks = []
    current_words = 0
    for entry in entries:
        title = entry.get("title", "").strip()
        summary = entry.get("summary", entry.get("description", "")).strip()
        if not title or not summary:
            continue
        chunk_text = f"Title: {title}\nSummary: {summary}"
        chunk_words = word_count(chunk_text)
        if current_words + chunk_words > MAX_WORDS:
            # Truncate the last chunk to fit the budget
            excess = (current_words + chunk_words) - MAX_WORDS
            # Rough truncation by cutting words from the end
            truncated_words = chunk_words - excess
            truncated_text = " ".join(chunk_text.split()[:truncated_words])
            chunks.append(truncated_text)
            logging.info("Reached token budget; last chunk truncated.")
            break
        chunks.append(chunk_text)
        current_words += chunk_words
    return chunks, current_words

def assemble_prompt(chunks, token_estimate):
    """Create a system prompt with ROLE, CONTEXT, CONSTRAINTS, STEPS."""
    role = "You are a fintech‑focused data extraction assistant."
    constraints = (
        "Only use the provided CONTEXT. Do not make up information.\n"
        "Output must be valid JSON matching the schema defined in STEP 3.\n"
        f"Estimated token usage: {token_estimate:.0f} (budget ≈ {MAX_WORDS * 1.3:.0f})."
    )
    steps = (
        "---STEP 1---\n"
        "Extract the article title from the CONTEXT and return it as a JSON field `title`.\n"
        "---STEP 2---\n"
        "Summarize the article in 2‑3 sentences, store as `summary`.\n"
        "---STEP 3---\n"
        "Identify any numeric metric mentioned (e.g., dollar amount, percentage) and return it as `key_metric`.\n"
        "If none is found, set `key_metric` to null.\n"
        "Return a single JSON object containing all three fields."
    )
    context = "\n---CONTEXT---\n".join(chunks)
    prompt = f"{role}\n\n{constraints}\n\n{context}\n\n{steps}"
    return prompt

def main():
    feed = fetch_feed(FEED_URL)
    if not feed.entries:
        logging.warning("No entries found in the RSS feed at %s", FEED_URL)
        sys.exit(0)

    # Use only the first 5 entries for demonstration (adjust as needed)
    entries = feed.entries[:5]
    chunks, total_words = build_chunks(entries)
    token_estimate = total_words * 1.3  # Rough conversion

    prompt = assemble_prompt(chunks, token_estimate)

    logging.info("Processed %d entries.", len(chunks))
    logging.info("Estimated token count: %.0f", token_estimate)
    print("\n---SYSTEM PROMPT---\n")
    print(prompt)

    plan = {
        "feed_url": FEED_URL,
        "entries_used": len(chunks),
        "total_words": total_words,
        "token_estimate": token_estimate,
        "system_prompt": prompt,
    }
    OUTPUT_JSON.write_text(json.dumps(plan, indent=2))
    logging.info("Prompt plan saved to %s", OUTPUT_JSON)

if __name__ == "__main__":
    main()

Post a Comment

Hi! How can we help you? Send us a message and we'll get back to you.