I've spent years building systems that attempt to cut through the noise of information overload. We've all seen generic content recommendations that miss the mark, or agents that feel stuck in their initial programming. The real challenge isn't just summarizing content; it's getting an agent to *understand* what truly matters to a specific user and adapt as those interests evolve. This post will walk you through architecting a feedback-driven AI agent that learns your preferences from direct input, iteratively refining its understanding to curate highly personalized insights from dynamic sources like the Cloudflare blog's RSS feed. You'll learn how to move beyond static prompts and build an agent that feels genuinely attuned to your needs, tackling the complexity of dynamic preference modeling and resilient data ingestion.
Key Takeaways
- An explicit human feedback loop is critical for AI agents to dynamically learn and adapt user preferences over time.
- Latent preference profiles, modeled as vector embeddings, offer a robust way to capture nuanced user interests beyond explicit keywords.
- Iterative refinement of the preference profile, driven by cosine similarity and weighted feedback, allows the agent to continuously improve its understanding.
- Proactive content curation can be achieved by comparing new content embeddings against the dynamic user preference profile.
- Production-grade agents require robust error handling for external APIs and careful management of state to ensure resilience against data volatility.
The Problem
In a world of ever-increasing information, generic content summaries and rule-based filtering fall short. Data scientists and ML engineers often struggle to build AI agents that truly understand and adapt to evolving user preferences, leading to irrelevant recommendations and user fatigue. This post addresses the challenge of architecting a system where an agent *learns* a user's interests through direct feedback, iteratively refining its understanding to deliver highly personalized and valuable insights from dynamic data sources like RSS feeds, rather than relying on predefined categories or one-off prompts. My previous posts, like
Architecting Self-Discovering AI Agents, touched on dynamic tool use, but here we focus on *dynamic preference modeling*, a distinct and often overlooked aspect of agent intelligence.
Data and Sources
For this agent, we'll be ingesting real-time content from the Cloudflare Blog via its RSS feed. This provides a rich, dynamic stream of technical articles that are highly relevant to our target audience (data scientists, ML engineers).
Data accessed on 2026-09-23.
Step 1 — Establishing the Baseline: Ingesting Dynamic Content and Generic Summarization
The first step for any content agent is to get the content itself. We need a reliable way to ingest articles from a dynamic source and then process them into a consumable format. For RSS feeds, `feedparser` is an excellent, battle-tested library. Once we have the raw articles, a quick summary helps us understand their essence without reading the full text, a task perfectly suited for a Large Language Model (LLM).
Here's how we fetch the RSS feed and prepare a basic summarization for each entry:
import feedparser
from typing import List, Dict, Any, Optional
def fetch_rss_feed(url: str) -> List[Dict[str, Any]]:
"""Fetches and parses an RSS feed, returning a list of entry dictionaries."""
try:
feed = feedparser.parse(url)
if feed.bozo: # Checks for well-formedness issues
print(f"Warning: RSS feed parsing issues for {url}: {feed.bozo_exception}")
return [{
"title": entry.title,
"link": entry.link,
"summary": entry.get("summary", ""), # Use get to handle missing summary
"published": entry.get("published", "")
} for entry in feed.entries]
except Exception as e:
print(f"Error fetching or parsing RSS feed from {url}: {e}")
return []
class LLMService:
"""A mock LLM service for summarization and keyword extraction."""
def summarize_and_keywords(self, text: str) -> Dict[str, Any]:
# In a real scenario, this would call an LLM API (e.g., OpenAI, Anthropic)
# For demonstration, we'll simulate a basic summary and keywords.
if "post-quantum" in text.lower():
return {"summary": "A deep dive into post-quantum cryptography advancements.", "keywords": ["post-quantum", "cryptography", "security"]}
elif "workers" in text.lower() or "node.js" in text.lower():
return {"summary": "Updates on Cloudflare Workers platform and Node.js compatibility.", "keywords": ["cloudflare workers", "node.js", "serverless"]}
elif "casb" in text.lower() or "remediation" in text.lower():
return {"summary": "New features for Cloudflare CASB, focusing on automated policy remediation.", "keywords": ["casb", "security", "automation"]}
else:
return {"summary": "A generic technical update from Cloudflare.", "keywords": ["tech", "cloudflare", "update"]}
llm_service = LLMService()
def process_feed_entry(entry: Dict[str, Any]) -> Dict[str, Any]:
"""Applies LLM processing to a single feed entry."""
full_text = f"{entry['title']}. {entry['summary']}"
llm_output = llm_service.summarize_and_keywords(full_text)
entry["llm_summary"] = llm_output["summary"]
entry["llm_keywords"] = llm_output["keywords"]
return entry
This code snippet defines `fetch_rss_feed` to pull content reliably and `LLMService` which, in a production environment, would abstract calls to a large language model API. For this demonstration, our `LLMService` provides rule-based summaries and keywords, simulating the output format we'd expect from a real LLM. The `process_feed_entry` function then enriches each article with these LLM-derived insights.
Step 2 — Modeling User Intent: Architecting the Latent Preference Profile
Generic summaries are a start, but true personalization requires understanding the user. Rather than relying on a static list of keywords, I model user intent as a "latent preference profile" – essentially, a vector embedding that captures the semantic essence of what the user finds interesting. This vector can then be updated and refined over time. We'll use a pre-trained sentence transformer model to generate these embeddings.
This is how we initialize our embedding model and create an initial (or load a persistent) user profile:
from sentence_transformers import SentenceTransformer
import numpy as np
import json
import os
# Initialize a sentence transformer model for generating embeddings
# 'all-MiniLM-L6-v2' is a good balance of speed and performance for many tasks.
embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
class UserPreferenceProfile:
"""Manages a user's evolving preference profile as a vector embedding."""
def __init__(self, profile_path: str = "user_preference.json"):
self.profile_path = profile_path
self.preference_vector: Optional[np.ndarray] = None
self._load_profile()
def _load_profile(self):
if os.path.exists(self.profile_path):
with open(self.profile_path, 'r') as f:
data = json.load(f)
self.preference_vector = np.array(data['preference_vector'])
print(f"Loaded user profile from {self.profile_path}.")
else:
print("No existing user profile found. Will initialize