Most reinforcement learning Python tutorials teach you to balance a cart-pole, print a rising reward curve, and stop. You leave with a working training loop and no bridge to any decision a production system actually makes. The gap is not the code — the code is fine. The gap is that the tutorial never connects the reward signal to a number anyone in a real deployment cares about. For real-time generative AI, there is a narrow but concrete place where reinforcement learning earns its keep: not inside the generative model, but as a controller sitting around the inference path. It decides when to flush a streaming buffer, how aggressively to batch incoming requests, or which model tier to route a query to — all under a latency budget. That is the useful frame, and it is the one a cart-pole tutorial can never give you, because cart-pole has no latency budget and no throughput ceiling. This tutorial keeps the RL loop honest by grounding every step in the streaming and latency primitives that a real inference service already exposes. If you build [generative AI systems that have to answer inside a latency budget](generative AI), the controller framing is the one worth learning. The mistake: treating RL as a model upgrade The naive reading of “reinforcement learning for GenAI” is that you use RL to make the generative model itself better — RLHF-style fine-tuning, reward models over token sequences, policy-gradient updates to a multi-billion-parameter transformer. That is a real technique, but it is a training-time concern, and it is not what this tutorial is about. The expert framing is smaller and far more deployable. The generative model is frozen. It runs on your GPU exactly as it does today — same weights, same TensorRT-LLM or vLLM serving stack, same KV cache. What you are learning is a policy that operates the serving path: a lightweight decision function that observes the current state of the queue and the GPU, then picks an action that trades latency against throughput. The divergence point between the two readings is the reward function. A tutorial that never defines reward against a measured latency budget produces a policy that looks trained — the loss goes down, the reward curve rises — but controls nothing real. This is the single most common failure we see when teams first reach for RL on an inference path: a beautiful training curve attached to a reward that has no operational meaning. What a reinforcement learning Python tutorial actually teaches Strip away the specific problem and every RL loop is the same three-part contract. An agent observes a state, chooses an action, receives a reward, and the environment transitions to a new state. The agent’s job is to learn a policy — a mapping from states to actions — that maximises cumulative reward over time. That is it. Cart-pole, Atari, and inference control are all instances of this contract; only the state, action, and reward differ. The reason cart-pole tutorials feel unsatisfying for production work is that they hand you a pre-built environment (the physics simulation) with an obvious reward (stay upright, +1 per timestep). In a real system, you have to define all three, and getting the reward wrong is where projects quietly fail. Here is the minimal working example most tutorials show, using Gymnasium for the environment interface and Stable-Baselines3 for the algorithm: import gymnasium as gym from stable_baselines3 import PPO env = gym.make("CartPole-v1") model = PPO("MlpPolicy", env, verbose=1) model.learn(total_timesteps=50_000) obs, _ = env.reset() for _ in range(1000): action, _ = model.predict(obs, deterministic=True) obs, reward, done, truncated, _ = env.step(action) if done or truncated: obs, _ = env.reset() Nine lines and you have a trained agent. The problem is that nothing here tells you how to swap CartPole-v1 for a decision your inference service makes. That swap — writing your own gym.Env — is the whole job. How do you frame a streaming inference decision as an RL environment? Take one concrete decision: dynamic batching under a first-token-latency budget. Requests arrive at variable rates. Batching more requests together raises GPU utilisation and throughput, but each request waits longer before its batch fires, which pushes up first-token latency. Batch too eagerly and you blow the budget on bursty traffic; batch too conservatively and you leave the GPU idle. A static threshold (“fire when 8 requests queue or 20ms elapses”) is the heuristic most teams ship. An RL controller learns the threshold as a function of live conditions instead of freezing it. To turn this into a gym.Env, you define the three pieces explicitly. RL component Batching-control instantiation State Current queue depth, arrival rate over the last window, GPU utilisation, time since last flush, running p95 first-token latency Action Discrete: {flush now, wait one more tick} — or continuous: a batch-size target Reward Positive for tokens served (throughput); large negative penalty whenever a request’s first-token latency exceeds the budget Episode A fixed traffic window (e.g. 60 seconds of replayed request traces) Transition The serving simulator advances one tick, admits arrivals, and fires the batch if the action says so The state is deliberately cheap to compute — every field is already a counter your serving stack exposes. The action space is small, which matters: A2C or PPO converge far faster on a handful of discrete actions than on a large continuous space, and for a batching gate a discrete flush/wait decision is usually enough. If you want the mechanics of the algorithm underneath, our explainer on how Advantage Actor-Critic works in practice covers the actor and critic split that PPO builds on. Designing a reward function tied to a measured latency budget This is the step the cart-pole tutorial cannot teach you, and it is the step that determines whether the trained policy is worth deploying. The reward has to encode the actual operational objective, which for streaming GenAI is almost always a two-sided constraint: hold first-token latency inside a budget and keep the GPU busy. A reward that only rewards throughput will learn to batch as aggressively as possible and violate the latency budget constantly. A reward that only penalises latency will learn to fire single-request batches and waste the GPU. You need both terms, and the penalty for a budget violation should dominate: def reward(tokens_served, p_latency, budget_ms): throughput_term = tokens_served if p_latency > budget_ms: # violation dominates: a busy GPU is worthless if it misses the budget return throughput_term - 50.0 * (p_latency - budget_ms) return throughput_term The budget_ms here is not a number you invent. It comes from the streaming latency budget your service already commits to — the same number an [A2 GenAI Feasibility Audit’s real-time variant](generative AI) pins down before any code is written. That is the honest link: the reward signal is the feasibility audit’s latency budget, expressed as a penalty. If you cannot state your budget in milliseconds at p95, you are not ready to train a controller, because you have nothing to optimise against. Three claims worth extracting from this: A latency-aware control policy can hold p95 first-token latency inside budget while raising GPU utilisation compared to a static batching threshold — this is an observed pattern across the inference-control work we do, not a benchmarked universal rate. The measurable win is fewer budget violations under bursty traffic without over-provisioning hardware — you buy headroom with a better policy rather than with more GPUs. The reward function, not the algorithm choice, is where a latency-aware RL controller succeeds or fails. PPO versus A2C is a second-order decision. The reward’s latency budget is defined against the same latency-optimisation discipline covered in our [GPU latency methodology work](generative AI) — the controller is optimising a number that GPU-side profiling produces. When is reinforcement learning the wrong tool here? Honesty about scope is what separates a tutorial you can act on from one you can’t. RL is not the default answer for inference control. It is the answer for a specific shape of problem. If the decision is stateless — each request routed independently on features you can observe up front — a contextual bandit is simpler and converges faster than full RL. Bandits skip the state-transition machinery entirely and are usually the right first thing to try for pure model-tier routing. If a static heuristic already holds the budget under your real traffic, ship the heuristic. A fixed “flush at 8 requests or 20ms” threshold is trivial to reason about, has no training pipeline, and cannot silently drift. RL is worth it only when traffic is bursty enough that no single static threshold holds the budget across load regimes. If you cannot replay realistic traffic to train against, the policy will learn your synthetic distribution and fail on production bursts. No traffic traces, no controller. The rule of thumb: reach for RL when the decision is sequential (this flush affects the next window’s state), when no static threshold covers all load regimes, and when you have real traffic traces to train and evaluate on. Miss any of the three and a simpler controller wins. How do you evaluate a trained controller before shipping it? Never promote a policy on training reward. Training reward tells you the optimiser did its job on the distribution it saw; it says nothing about production behaviour. Evaluate against the two numbers the budget is actually made of. Replay held-out traffic traces — bursts the policy never trained on — through the serving simulator, then compare the RL controller against your current static heuristic on: p95 first-token latency versus budget, per load regime (steady, bursty, sustained overload). The controller has to hold the budget where the heuristic breaks, or there is no reason to deploy it. Throughput / GPU utilisation at equal latency compliance. If the RL controller only holds the budget by idling the GPU, it has bought nothing the heuristic couldn’t. Budget-violation rate under burst — the count of requests that missed the budget during traffic spikes. This is the metric that justifies the whole exercise. If you want to reason about how token counts feed those latency numbers in the first place, our token size and latency budgeting guide walks through the token-to-latency relationship the reward function assumes. The trade-off worth naming out loud A reinforcement learning Python tutorial that ends at cart-pole gives you a loop. A tutorial that ends at a reward function tied to a measured p95 latency budget gives you a controller you can actually put in front of a GPU. The distance between the two is entirely in how you define state, action, and — above all — reward. The open question is never “which algorithm.” PPO, A2C, and a well-tuned bandit will all learn something. The question that decides the project is whether your latency budget is real enough to become a penalty term. If the streaming budget is vague, the learned policy inherits that vagueness and controls nothing. That is the failure class to watch — a policy that trains cleanly against a reward with no operational meaning — and the artifact that pins the budget down is the real-time variant of the GenAI feasibility audit, where the milliseconds get committed before the training loop ever runs. FAQ How should you think about a reinforcement learning python tutorial in practice? An RL loop follows one contract: an agent observes a state, chooses an action, receives a reward, and the environment transitions to a new state, with the policy learning to maximise cumulative reward. In practice, tutorials hand you a pre-built environment like cart-pole; the real work for production is defining your own state, action, and reward for a decision your system actually makes. What Python libraries do you actually use to build an RL loop, and what does a minimal working example look like? Gymnasium provides the environment interface and Stable-Baselines3 provides the algorithms (PPO, A2C). A minimal example is under ten lines: create the env, wrap it in a PPO model, call learn(), then predict actions in a loop. The catch is that swapping the built-in environment for your own gym.Env is the entire job. How do you frame a real-time GenAI decision as an RL environment with states, actions, and rewards? Take dynamic batching under a latency budget: state is queue depth, arrival rate, GPU utilisation, and running p95 latency; action is flush-now versus wait (or a batch-size target); reward is positive for tokens served and heavily negative when first-token latency exceeds budget. Keep the action space small and discrete so A2C or PPO converge quickly. How do you design a reward function tied to a measured latency budget? Encode both sides of the objective: a throughput term for tokens served plus a dominating penalty whenever p95 first-token latency exceeds the budget in milliseconds. The budget is not invented — it comes from the streaming latency budget your service commits to, the same number a real-time GenAI feasibility audit pins down. If you can’t state the budget in milliseconds, you’re not ready to train. When is reinforcement learning the wrong tool, and where does a heuristic beat an RL policy? RL is wrong when the decision is stateless (use a contextual bandit), when a static threshold already holds the budget (ship the heuristic), or when you can’t replay realistic traffic to train against. Reach for RL only when the decision is sequential, no single static threshold covers all load regimes, and you have real traffic traces. How do you evaluate a trained RL controller against p95 latency and throughput before shipping it? Replay held-out traffic bursts the policy never trained on and compare it against your current static heuristic on three axes: p95 first-token latency versus budget per load regime, throughput at equal latency compliance, and budget-violation rate under burst. Never promote a policy on training reward alone — it reflects the training distribution, not production behaviour.