The Problem
Netflix knows your viewing habits. Spotify knows your music taste. Your AI assistant knows... nothing. It asks for your expertise level on every session. It explains concepts you already understand. It gives verbose prose to someone who lives in bullet points. It ignores the project you've mentioned 20 times.
The gap is not model capability — it's memory infrastructure. An expert Python developer asking about async patterns should get a concise, high-signal answer that assumes deep knowledge and references their current project context. A junior developer asking the same question should get a step-by-step explanation with analogies.
The same model produces dramatically different quality depending on what context is injected into the system prompt. This lesson builds the infrastructure to inject the right context — automatically, at every turn, improving over time.
Structured user profile
The UserProfile schema captures everything that should influence agent behavior: expertise_level (novice/intermediate/expert), communication_style (prefers_brevity, prefers_bullets, wants_code_examples, detail_level), domain_focus, ongoing_projects, stated_goals, and explicit_preferences. It also tracks implicit_signals from behavioral data. This structured profile is compressed to under 500 tokens and injected into every system prompt.
Structured user profile schema
A minimal but production-ready profile schema:
@dataclass
class UserProfile:
user_id: str
name: str
expertise_level: str # novice | intermediate | expert
communication_style: CommunicationStyle # prefers_brevity, prefers_bullets, detail_level
domain_focus: list[str] # ["healthcare AI", "data engineering"]
ongoing_projects: list[Project]
stated_goals: list[str]
explicit_preferences: dict # {"sql_style": "use EXPLAIN ANALYZE"}
implicit_signals: dict # {"revision_count": 3, "code_copies": 7}
last_updated: float
Each field serves a purpose:
expertise_level determines how much background to assume and whether to include definitionscommunication_style controls format and length — a "prefers_brevity + prefers_bullets" user gets a different response than a "high detail_level + prefers_analogies" userongoing_projects lets the agent reference current work without the user re-explaining context each session ("for your Patient Record RAG System, you'd use pgvector's <-> operator...")explicit_preferences holds direct user statements ("I always use EXPLAIN ANALYZE")implicit_signals accumulates behavioral evidence without user intervention
The profile is updated by both explicit statements ("I prefer brevity") and implicit signals (user consistently revises responses to be shorter). Behavioral signals are more reliable than stated preferences for communication style — people often claim preferences they don't actually exhibit.
Implicit preference learning
Users reveal preferences through behavior, not just statements. Four key signals to track:
Revision signal: when a user rewrites or substantially modifies an agent response, that response didn't match their preferences. Track revision_count. After 3 revisions toward shorter/simpler responses, set prefers_brevity = True.
Copy code signal: when a user copies a code block, that response format was valuable. Set wants_code_examples = True and increment code_copies. This confirms the pattern even without explicit feedback.
Expand signal: when a user clicks "Show more" or asks "can you elaborate?", detail_level should increase toward "high".
Short request signal: when a user says "be more concise" or sends a very short follow-up message suggesting frustration — prefers_brevity = True, detail_level = "low".
The ImplicitSignalProcessor observes these signals and updates the profile automatically. Over 10 conversations, the profile adapts to match actual user behavior — not just what they said they wanted.
A user never asks "please be more concise" but consistently replaces agent responses with shorter, punchier versions when they use them. What should the implicit signal processor do?
Explanation: Behavioral signals often reveal preferences more reliably than stated preferences. A user who says "I want detailed explanations" but always revises them to be shorter is telling you something real through behavior. The implicit signal processor observes the gap between stated preferences and behavior and updates the profile accordingly. This is the difference between an assistant that gets better over time versus one that requires explicit re-instruction each session.
Personalization injection
The profile is injected into every system prompt as a compressed, human-readable summary — not raw JSON.
Why not raw JSON? JSON in a system prompt is parseable but unnatural. The model reads prose more fluidly than structured data. "Alex is an expert-level practitioner focused on healthcare AI, prefers concise bullet-point responses, and is currently working on a Patient Record RAG System (FastAPI, pgvector, ChromaDB)" integrates seamlessly into generation. The model responds as if it naturally knows this context.
Compression to <500 tokens: a full profile with many projects and preferences can be large. The PersonalizationInjector compresses it to the essentials:
- Name, expertise level, domain focus — 1 sentence
- Communication style — 1 sentence with key preferences
- Active projects — up to 3, with tech stack
- Top 2 stated goals
- Key explicit preferences
The total is typically 150–300 tokens — substantial context at minimal cost.
The before/after difference: without personalization, a question about adding a PostgreSQL index gets a textbook explanation starting with "An index in PostgreSQL is a data structure that..." With personalization for an expert user who prefers brevity and bullets: "Use CREATE INDEX CONCURRENTLY idx_name ON table(col);. CONCURRENTLY avoids table lock. For composite indexes: list high-cardinality columns first. Run EXPLAIN (ANALYZE, BUFFERS) after to verify hit."
Same model. Different context. Different response quality.
A user profile should be injected into the system prompt as:
Explanation: Raw JSON is parseable but not optimal — the model doesn't "read" JSON as naturally as prose. A well-crafted summary like "Alex is an expert Python developer who prefers concise bullets and is working on a healthcare RAG system" integrates more smoothly and naturally into the model's generation. Keep it under 500 tokens to preserve context budget for actual content. The 500-token ceiling is the key discipline: larger profiles need compression, not verbatim injection.