Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Predicting a review label answers one question about a request. Choosing what to do next raises another: how will this action change the evidence, delay, and outcome? At 09:00, access-change request REQ-10234 lands on the routing desk under fictional policy P-7. The router could approve immediately, request supporting evidence, or escalate to a human reviewer. Compare the futures those choices create.
The earlier tree-model lesson fitted completed-case labels from static snapshots; supervised labels can arrive much later than predictions. Here, requesting evidence changes the next observation. Reinforcement learning (RL) learns decisions from rewards and their consequences.[1] Experience can come from interaction, simulation, or historical logs. The agent chooses routing actions; the environment includes the requester, verification service, and reviewers. We optimize expected cumulative return under a declared reward and discount, rather than only the next reward.
Teaching scores below are synthetic reward units, not dollar amounts or live production parameters. The unsafe approval option stays in our simulator to expose failure patterns. Real authorization layers must enforce hard invariants through deterministic guardrails rather than hoping a learned penalty keeps policy violations at bay.
From one label to an episode
REQ-10234 is a stale access-change request. Security policy P-7 requires human review unless strong secondary evidence arrives. Three opening actions are available:
Before checking the mathematical totals, pause and predict: if evidence definitely arrives, does the fastest action win, or can a small upfront delay pay for a higher-value outcome?
| Initial action | Immediate effect | Reward now | Later best outcome |
|---|---|---|---|
auto_approve | Closes an unsupported required-review request | -120 | Terminal policy violation |
request_evidence | Asks for evidence before approving | -4 | Evidence supports safe automatic approval |
human_review | Enters the reviewer queue immediately | -18 | Reviewer inspects credentials and approves |
An episode starts at new_request and terminates at closed. Suppose evidence arrives with absolute certainty, giving these reward sequences:
| Route | Reward sequence | Concrete meaning |
|---|---|---|
| Approve immediately | [-120] | Fast, but violates compliance rules |
| Request evidence, then approval | [-4, +70] | Brief inquiry delay, supported resolution |
| Review, then approve | [-18, +80] | Reviewer handling cost, verified resolution |
Without discounting, evidence earns while human review earns . Now discount rewards observed one step later by a factor of . The evidence route earns . Human review earns . Immediate approval still incurs . The evidence route leads by five points when evidence arrives reliably.
The discounted return accumulates rewards across time steps with increasing powers of :
Here is received after action from state . The discount factor is in our model.[1] Two roles need distinguishing:
- A bounded continuing return: With and , . Undiscounted continuing sums may diverge; average-reward formulations are another legitimate objective.
- A chosen temporal preference: Discounting downweights later positive rewards and later penalties. It can favor earlier resolution, but can also make a delayed violation look cheaper. It isn't a general correction for uncertain dynamics.
Every branch here terminates within three transitions, so would also give finite returns. Each transition represents an operational step, not necessarily an equal duration: a constant per-step discount doesn't price hours consistently if step lengths differ. We model abandonment explicitly below. A discount has an equivalent survival interpretation only under suitable independent termination with zero reward on termination; it can't replace our action-dependent abandonment penalty.
The Python script applies those weights directly to each path:
1gamma = 0.90
2routes = {
3 "auto_approve": [-120],
4 "request_evidence": [-4, 70],
5 "human_review": [-18, 80],
6}
7
8def discounted_return(rewards):
9 return sum((gamma**step) * reward for step, reward in enumerate(rewards))
10
11assert discounted_return([-4, 70]) == 59.0
12assert discounted_return([-18, 80]) == 54.0
13for action, rewards in routes.items():
14 print(f"{action:16} return={discounted_return(rewards):7.1f}")1auto_approve return= -120.0
2request_evidence return= 59.0
3human_review return= 54.0That five-point advantage relies on evidence arriving every time. Because every opening action carries downstream consequences, we need a formal structure to model state transitions, rewards, and decision sequences. That structure is a Markov Decision Process.
Define the MDP
A Markov decision process (MDP) formalizes sequential decision-making through a 5-tuple :
- States : The set of all valid environment configurations. captures what's known at decision time .
- Actions : The set of choices available in state .
- Transition dynamics : The conditional probability distribution .
- Reward function : The expected immediate reward .
- Discount factor : The decay rate for future rewards.
Sequential interaction between the agent and environment repeats until termination:

Under the Markov assumption, the current state and action contain all information needed to predict the next state and reward distribution:
The past adds nothing to this distribution once the state and action are known.[1] If elapsed wait affects abandonment, omitting it can make our chosen representation non-Markov. An MDP environment may expose only partial observations to the agent; model that as a POMDP or use a sufficient history or belief representation. Dropping a field doesn't itself change the underlying environment.
Begin with a deterministic environment where transitions occur with probability :
| State | Available action | Next state | Reward |
|---|---|---|---|
new_request | auto_approve | closed | -120 |
new_request | request_evidence | evidence_ready | -4 |
new_request | human_review | review_queue | -18 |
evidence_ready | auto_approve | closed | +70 |
evidence_ready | human_review | review_queue | -10 |
review_queue | approve_request | closed | +80 |
Evidence changes whether auto_approve is permitted under policy P-7, so new_request and evidence_ready must be distinct states.

Why are new_request and evidence_ready separate states?
Answer
The presence of verified credentials alters the legality and reward of taking auto_approve. In new_request, approving violates policy P-7 and triggers a compliance penalty. In evidence_ready, the exact same action closes a verified request safely.
With the graph established, we can calculate the value of each state by working backward from terminal nodes.
Value and Bellman backups
A policy defines how the agent behaves. A deterministic policy maps each state directly to a single action: . A stochastic policy outputs an action distribution: .
Two foundational value functions measure performance under a policy :
- State-value function : The expected return starting from state and following policy thereafter:
- Action-value function : The expected return starting from state , taking action , and following policy thereafter:
The connection between state values and action values is defined by the Bellman expectation equations. We decompose the return into immediate reward plus the discounted value of the successor state:
When seeking optimal control, we aren't satisfied with an arbitrary policy. The optimal policy achieves the highest possible return across all states: for all . This produces the Bellman optimality equations:
The backward calculation updating a state's value from its successors is called a Bellman backup. Starting from terminal outcomes and walking backward:
- Terminal state
closedhas no future transitions: . review_queuehas one action (approve_request): .evidence_readycompares auto-approval against human review:new_requestcompares all three opening options:
The backup tree shows how action values feed into the root state:

The script performs this exact comparison and extracts the greedy action:
1gamma = 0.90
2successor_value = {
3 "closed": 0.0,
4 "evidence_ready": 70.0,
5 "review_queue": 80.0,
6}
7start_actions = {
8 "auto_approve": (-120, "closed"),
9 "request_evidence": (-4, "evidence_ready"),
10 "human_review": (-18, "review_queue"),
11}
12
13scores = {
14 action: reward + gamma * successor_value[next_state]
15 for action, (reward, next_state) in start_actions.items()
16}
17assert scores["request_evidence"] == 59.0
18assert scores["human_review"] == 54.0
19for action, score in scores.items():
20 print(f"Q(new_request, {action:16}) = {score:7.1f}")
21print("greedy action =", max(scores, key=scores.get))1Q(new_request, auto_approve ) = -120.0
2Q(new_request, request_evidence) = 59.0
3Q(new_request, human_review ) = 54.0
4greedy action = request_evidenceAt evidence_ready, why isn't the +80 reviewer reward enough to make human_review win?
Answer
Reaching that +80 requires paying -10 upfront and discounting the +80 to 72. That branch totals 62, whereas approving with existing evidence pays 70 immediately. Bellman backups compare complete discounted returns across entire trajectories, not isolated maximum rewards.
Manual backups work by hand here because the state space is tiny and acyclic. In larger systems, we automate this calculation through dynamic programming.
Dynamic programming: value iteration vs policy iteration
For a finite discounted MDP with known transition probabilities and expected rewards, dynamic programming can compute an optimal policy. Two standard algorithms are:
- Policy Iteration: Operates in two distinct alternating phases:
- Policy Evaluation: Solve the Bellman expectation equations for a fixed policy , using a linear solve or iterative evaluation. Practical iterations use a tolerance.
- Policy Improvement: Extract a strictly better or equal policy greedily: . Exact evaluation and greedy improvement give . There are finitely many deterministic policies, ; stochastic policies aren't a finite set. With consistent tie handling, exact policy iteration terminates at an optimum. Approximate evaluation needs its own error control rather than an automatic exact guarantee.
- Value Iteration: Avoids waiting for complete policy evaluation. Instead, it collapses evaluation and improvement into a single step by directly applying the Bellman optimality operator on every sweep:
For this finite model with bounded rewards and , the Bellman optimality operator contracts under the infinity norm: . Exact repeated backups converge geometrically to a unique from any finite initialization. Optimal actions can still tie, so uniqueness of the value doesn't imply a unique optimal policy.
Watch how credit propagates backward across three sweeps:
1gamma = 0.90
2states = ["new_request", "evidence_ready", "review_queue", "closed"]
3actions = {
4 "new_request": ["auto_approve", "request_evidence", "human_review"],
5 "evidence_ready": ["auto_approve", "human_review"],
6 "review_queue": ["approve_request"],
7}
8transitions = {
9 ("new_request", "auto_approve"): [(1.0, "closed", -120)],
10 ("new_request", "request_evidence"): [(1.0, "evidence_ready", -4)],
11 ("new_request", "human_review"): [(1.0, "review_queue", -18)],
12 ("evidence_ready", "auto_approve"): [(1.0, "closed", 70)],
13 ("evidence_ready", "human_review"): [(1.0, "review_queue", -10)],
14 ("review_queue", "approve_request"): [(1.0, "closed", 80)],
15}
16
17def action_value(state, action, values):
18 return sum(
19 probability * (reward + gamma * values[next_state])
20 for probability, next_state, reward in transitions[(state, action)]
21 )
22
23values = {state: 0.0 for state in states}
24for sweep in range(3):
25 updated = values.copy()
26 for state in actions:
27 updated[state] = max(
28 action_value(state, action, values) for action in actions[state]
29 )
30 values = updated
31 print(
32 f"sweep {sweep + 1}: "
33 f"new={values['new_request']:5.1f} "
34 f"evidence={values['evidence_ready']:5.1f} "
35 f"review={values['review_queue']:5.1f}"
36 )
37
38policy = {
39 state: max(actions[state], key=lambda action: action_value(state, action, values))
40 for state in actions
41}
42assert values["new_request"] == 59.0
43assert policy["new_request"] == "request_evidence"
44print("policy =", policy)1sweep 1: new= -4.0 evidence= 70.0 review= 80.0
2sweep 2: new= 59.0 evidence= 70.0 review= 80.0
3sweep 3: new= 59.0 evidence= 70.0 review= 80.0
4policy = {'new_request': 'request_evidence', 'evidence_ready': 'auto_approve', 'review_queue': 'approve_request'}On sweep 1, all successor estimates start at zero, so new_request sees only immediate costs (). On sweep 2, the newly calculated values of evidence_ready () and review_queue () propagate backward, lifting new_request to . On sweep 3, values stabilize completely.
Value iteration solves planning when dynamics are known. What happens when our assumption of certain evidence arrival breaks down?
Transition risk can reverse a decision
Real users don't always cooperate. When asked for additional authentication credentials, some users close the tab and abandon the flow. Let's incorporate an abandonment branch: with probability , the requester walks away. The ticket closes unresolved, incurring a penalty alongside the initial dispatch cost, totaling :
Action from new_request | Transition probability | Next state | Immediate reward |
|---|---|---|---|
request_evidence | 0.75 | evidence_ready | -4 |
request_evidence | 0.25 | closed (abandoned) | -54 |
Under this model, the expected counts in 100 independent requests are 75 arrivals and 25 abandonments; realized counts vary. We charge abandonment on the opening transition. If it occurred after a separate waiting step, its timing and discounted return would need changing. Weight the current branches by their probabilities:
Direct human review remains deterministic: . Comparing the two choices under risk:
The expected Bellman backup flips the optimal action from request_evidence to human_review!
We can solve for the exact indifference threshold. Let be the abandonment probability. The expected return for requesting evidence as a function of is:
Equating this expected return to human review's constant value of :
Above abandonment, human review has higher expected return under these rewards and timings. This isn't a universal product threshold.
The stochastic backup script reflects this calculation:
1gamma = 0.90
2values = {"closed": 0.0, "evidence_ready": 70.0, "review_queue": 80.0}
3transitions = {
4 "request_evidence": [
5 (0.75, "evidence_ready", -4),
6 (0.25, "closed", -54),
7 ],
8 "human_review": [(1.0, "review_queue", -18)],
9}
10
11def expected_action_value(action):
12 return sum(
13 probability * (reward + gamma * values[next_state])
14 for probability, next_state, reward in transitions[action]
15 )
16
17assert expected_action_value("request_evidence") == 30.75
18assert expected_action_value("human_review") == 54.0
19for action in transitions:
20 print(f"{action:16} expected return={expected_action_value(action):5.2f}")
21print("risk-aware action =", max(transitions, key=expected_action_value))1request_evidence expected return=30.75
2human_review expected return=54.00
3risk-aware action = human_reviewThe expectation isn't a return any single ticket achieves. Check a sample mean from 10,000 simulated episodes against it. One seeded run can check the simulator's arithmetic without proving convergence or validating real abandonment rates:
1import random
2
3gamma = 0.90
4rng = random.Random(7)
5
6def request_evidence_return():
7 if rng.random() < 0.75:
8 return -4 + gamma * 70
9 return -54
10
11evidence_returns = [request_evidence_return() for _ in range(10_000)]
12evidence_mean = sum(evidence_returns) / len(evidence_returns)
13abandoned = sum(value < 0 for value in evidence_returns)
14assert abs(evidence_mean - 30.75) < 1.0
15print(f"request_evidence mean={evidence_mean:5.2f}")
16print(f"human_review mean={-18 + gamma * 80:5.2f}")
17print("abandoned evidence routes =", abandoned)1request_evidence mean=30.73
2human_review mean=54.00
3abandoned evidence routes = 2502Unknown dynamics leave two choices: estimate a model and plan with it, or learn values or policies directly from sampled transitions. Model-free learning takes the latter route; unknown probabilities don't force it.
Model-free learning: temporal difference updates
Our model-free learner receives sampled experience tuples rather than a transition table.
Two primary approaches address this challenge:
- Monte Carlo (MC) methods: Update value estimates using the actual realized total return observed at the end of the episode:
- Target: For a fixed policy and correctly sampled completed episodes, has conditional expectation . That doesn't make an arbitrarily initialized, finite-data value estimate unbiased.
- Noise: The target includes all later randomness; long stochastic episodes can produce high variance, while deterministic returns can have none.
- Timing: Standard episodic MC waits for the completed return. A truncated discounted rollout can approximate a continuing return, but introduces tail error; appending a value estimate makes a bootstrapped target.
- Temporal Difference (TD) learning: Bootstraps future return using the agent's current estimate at the next state :
where the TD error is defined as:
The target replaces the unobserved tail of the episode.
- Target: Bootstrapping from an inaccurate successor value can introduce target bias. With exact , the one-step target has conditional expectation .
- Noise: Replacing a sampled tail with its exact conditional expectation reduces target variance. A learned approximation adds its own errors, so TD doesn't have universally lower finite-data variance or error than MC.
- Benefit: Updates immediately on every time step without waiting for episode termination.
For control tasks where we must select actions without a model, we learn action values rather than state values . Two algorithms frame the core design choice:
- Q-Learning (Watkins, 1989 [2]): An off-policy TD control algorithm. It updates toward the greedy maximum in the next state: Because the target uses , Q-learning directly estimates the optimal policy even while the agent follows an exploratory behavior policy (such as -greedy).
- SARSA (Sutton & Barto [1]): An on-policy TD control algorithm. It uses the transition : Here is the action actually selected by the current behavior policy. SARSA learns the value of the policy being executed, incorporating the cost of exploratory mistakes.
For a stationary finite MDP with bounded rewards and , tabular Q-learning converges to almost surely when every state-action pair is updated infinitely often and its own visit-indexed step sizes satisfy and .[2][1] A finite run isn't that asymptotic guarantee, and it doesn't extend automatically to neural Q-functions.
To ensure exploration, we apply epsilon-greedy exploration: with probability , choose an action uniformly at random; with probability , select the highest-scoring action.
The simulator's step function below contains the transition dynamics and rewards; the learner receives only sampled outcomes:
1import random
2
3gamma = 0.90
4epsilon = 0.15
5rng = random.Random(42)
6actions = {
7 "new_request": ["auto_approve", "request_evidence", "human_review"],
8 "evidence_ready": ["auto_approve", "human_review"],
9 "review_queue": ["approve_request"],
10}
11
12def step(state, action):
13 if state not in actions or action not in actions[state]:
14 raise ValueError("invalid state-action pair")
15 if state == "new_request":
16 if action == "auto_approve":
17 return "closed", -120
18 if action == "human_review":
19 return "review_queue", -18
20 if rng.random() < 0.75:
21 return "evidence_ready", -4
22 return "closed", -54
23 if state == "evidence_ready":
24 return ("closed", 70) if action == "auto_approve" else ("review_queue", -10)
25 return "closed", 80
26
27q = {
28 state: {action: 0.0 for action in available_actions}
29 for state, available_actions in actions.items()
30}
31visits = {
32 state: {action: 0 for action in available_actions}
33 for state, available_actions in actions.items()
34}
35for _ in range(20_000):
36 state = "new_request"
37 while state != "closed":
38 available_actions = actions[state]
39 if rng.random() < epsilon:
40 action = rng.choice(available_actions)
41 else:
42 action = max(available_actions, key=lambda candidate: q[state][candidate])
43 next_state, reward = step(state, action)
44 future = 0.0 if next_state == "closed" else max(q[next_state].values())
45 target = reward + gamma * future
46 visits[state][action] += 1
47 learning_rate = visits[state][action] ** -0.6
48 q[state][action] += learning_rate * (target - q[state][action])
49 state = next_state
50
51start = q["new_request"]
52assert max(start, key=start.get) == "human_review"
53assert 20.0 < start["request_evidence"] < 40.0
54for action, value in start.items():
55 print(f"Q(new_request, {action:16}) = {value:6.1f}")
56print("learned start action =", max(start, key=start.get))1Q(new_request, auto_approve ) = -120.0
2Q(new_request, request_evidence) = 30.5
3Q(new_request, human_review ) = 54.0
4learned start action = human_reviewAfter this seeded run, request_evidence is estimated near , close to the analytic . This isn't evidence that every finite run converges. Our visit-indexed rate satisfies the two step-size sums; constant exploration supplies repeated visits in this reachable toy graph. The greedy extracted policy chooses review, but the training behavior still explores. The simulator accepts only declared state-action pairs; its unsafe opening action remains solely for this experiment.
A stopped rollout isn't always a terminal request
The lab sets future value to zero when the ticket reaches closed. An external collection time limit is different: the ticket may still have a valuable future. Gymnasium distinguishes terminated from truncated; a one-step bootstrap masks genuine termination, not merely truncation.[3]
1reward, gamma, next_value = -18.0, 0.90, 80.0
2
3for label, terminated, truncated in (
4 ("external time limit", False, True),
5 ("terminal outcome", True, False),
6):
7 stopped = terminated or truncated
8 correct = reward + gamma * (not terminated) * next_value
9 erased_tail = reward + gamma * (not stopped) * next_value
10 print(f"{label}: target={correct:.1f}; stop-masked target={erased_tail:.1f}")1external time limit: target=54.0; stop-masked target=-18.0
2terminal outcome: target=-18.0; stop-masked target=-18.0The same observed reward produces different targets because the future differs. A deadline built into the task can be a genuine terminal condition; include remaining time in the state when it affects decisions. Use the actual final observation for a truncated bootstrap, rather than a reset observation from an automatically reset environment.
Exploration and action coverage
Why does action coverage matter? With zero initialization, deterministic tie handling, and no exploration, a positive learned action value can keep an untried zero-valued action from being selected. A stochastic return or optimistic initialization could change that behavior; the following deterministic experiment isolates the failure.
The following experiment isolates this failure mode by replacing whole paths with their true expected values:
1import random
2
3true_returns = {
4 "auto_approve": -120.0,
5 "request_evidence": 30.75,
6 "human_review": 54.0,
7}
8action_order = list(true_returns)
9
10def learn(epsilon, seed):
11 rng = random.Random(seed)
12 estimates = {action: 0.0 for action in action_order}
13 visits = {action: 0 for action in action_order}
14 for _ in range(400):
15 if rng.random() < epsilon:
16 action = rng.choice(action_order)
17 else:
18 action = max(action_order, key=lambda candidate: estimates[candidate])
19 visits[action] += 1
20 estimates[action] += (true_returns[action] - estimates[action]) / visits[action]
21 selected = max(action_order, key=lambda candidate: estimates[candidate])
22 return visits, selected
23
24greedy_visits, greedy_selected = learn(0.00, seed=5)
25explore_visits, explore_selected = learn(0.10, seed=1)
26assert greedy_selected == "request_evidence"
27assert greedy_visits["human_review"] == 0
28assert explore_selected == "human_review"
29for epsilon, visits, selected in (
30 (0.00, greedy_visits, greedy_selected),
31 (0.10, explore_visits, explore_selected),
32):
33 print(f"epsilon={epsilon:.2f} visits={visits} selected={selected}")1epsilon=0.00 visits={'auto_approve': 1, 'request_evidence': 399, 'human_review': 0} selected=request_evidence
2epsilon=0.10 visits={'auto_approve': 17, 'request_evidence': 28, 'human_review': 355} selected=human_reviewWith , human_review receives zero visits. The agent remains blind to the superior return and commits forever to request_evidence. With , exploration samples all actions, discovers review's higher value, and concentrates out of decisions on the true optimum.
In production access systems, you can't freely explore dangerous actions on live traffic. Instead, restrict the action space through policy guardrails prior to model evaluation, or conduct exploration inside high-fidelity staging environments.
In tabular Q-learning, why does the update use the maximum next-state Q-value rather than the action the behavior policy actually took?
Answer
Q-learning is off-policy: its target uses the maximum next-state action value rather than the exploratory next action. That defines an optimal-control target while exploratory behavior gathers data. It doesn't remove sampling noise, insufficient coverage, or maximization bias from finite estimates.
Reward specification is part of the system
Reinforcement learning agents optimize the numeric signal they receive, not the unspoken intent in an engineering spec. When rewards measure only convenient proxies, agents find unexpected loopholes.
Consider what happens if an optimization team rewards fast ticket closure:
1actions = ["auto_approve", "request_evidence", "human_review"]
2speed_only = {
3 "auto_approve": 100,
4 "request_evidence": 45,
5 "human_review": 15,
6}
7outcome_aware = {
8 "auto_approve": -120,
9 "request_evidence": 30.75,
10 "human_review": 54.0,
11}
12
13for name, reward in [("speed_only", speed_only), ("outcome_aware", outcome_aware)]:
14 choice = max(actions, key=lambda action: reward[action])
15 print(f"{name:13} chooses {choice:16} reward={reward[choice]:4}")1speed_only chooses auto_approve reward= 100
2outcome_aware chooses human_review reward=54.0The speed-only reward selects auto_approve, maximizing turnaround metrics by rubber-stamping compliance violations. This illustrates Goodhart's Law: when a proxy metric becomes the optimization target, it ceases to be a reliable measure of health. In high-stakes domains, non-negotiable security boundaries belong in the execution filter, not in the reward table.
Policy gradients and the LLM bridge
Tabular methods store a separate value for each state-action pair. In language models, the state space consists of all possible token prefixes (an astronomical domain where tables can't fit). Instead, we use parameterized policies , where neural network weights output action probabilities directly.
For an episode ending at , define from a fixed initial-state distribution. Assume differentiable policies and environment dynamics that don't depend directly on . The trajectory score identity and reward-to-go argument give:[1]
Sampling this expectation gives REINFORCE updates.[4] We don't need a transition table, but do need experience from the appropriate policy. The outer measures time from the episode's start; discounts only from the current step. It disappears for an undiscounted episodic objective with .
Check that distinction with a two-step episode: the opening action is fixed, the second action is Bernoulli with probability , and it earns reward 1 only when action 1 is selected. Thus . At , which derivative should REINFORCE reproduce?
1from math import exp
2
3gamma, theta = 0.90, 0.0
4
5def probability(parameter):
6 return 1 / (1 + exp(-parameter))
7
8p = probability(theta)
9second_step_gradient = sum(
10 action_probability * (action - p) * action
11 for action, action_probability in ((0, 1 - p), (1, p))
12)
13weighted_gradient = gamma * second_step_gradient
14delta = 1e-5
15finite_difference = gamma * (
16 probability(theta + delta) - probability(theta - delta)
17) / (2 * delta)
18assert abs(weighted_gradient - finite_difference) < 1e-9
19print(f"with the time weight: {weighted_gradient:.4f}")
20print(f"without the time weight: {second_step_gradient:.4f}")
21print(f"objective finite difference: {finite_difference:.4f}")1with the time weight: 0.2250
2without the time weight: 0.2500
3objective finite difference: 0.2250A baseline changes variance, not the intended gradient
Subtract an action-independent, state-dependent baseline , often an estimate of . The true advantage is:
The sampled quantity estimates that advantage when ; it isn't the exact action value. Because , an action-independent baseline leaves the score estimator's expectation unchanged.[1][5] Treat it as a fixed weight when differentiating the actor loss. A baseline fitted using the same action's return needs care; action independence isn't automatic.
Variance reduction depends on the baseline. In a one-step Bernoulli policy with and reward equal to the selected action, compare no baseline, the exact value baseline, and a badly chosen baseline:
1for baseline in (0.0, 0.5, 10.0):
2 gradients = [(action - 0.5) * (action - baseline) for action in (0, 1)]
3 mean_gradient = sum(gradients) / 2
4 variance = sum((value - mean_gradient)**2 for value in gradients) / 2
5 print(f"baseline={baseline:.1f}: mean={mean_gradient:.4f}; variance={variance:.4f}")1baseline=0.0: mean=0.2500; variance=0.0625
2baseline=0.5: mean=0.2500; variance=0.0000
3baseline=10.0: mean=0.2500; variance=22.5625All three have the correct mean derivative, but one makes sampling much noisier. The exact value baseline removes variance in this tiny example; it isn't universally the variance-minimizing baseline for every parameterization.
Return to the router. Freeze all downstream choices at the model's optimal actions and optimize only the opening softmax policy. The script sums over its three known action values exactly and checks the derivative against finite differences. This is an opening-policy calculation, not a sampled whole-policy REINFORCE trainer:
1import math
2
3actions = ["auto_approve", "request_evidence", "human_review"]
4action_values = [-120.0, 30.75, 54.0]
5logits = [0.0, 0.0, 0.0]
6
7def softmax(values):
8 peak = max(values)
9 weights = [math.exp(value - peak) for value in values]
10 total = sum(weights)
11 return [weight / total for weight in weights]
12
13probabilities = softmax(logits)
14baseline = sum(p * value for p, value in zip(probabilities, action_values))
15advantages = [value - baseline for value in action_values]
16n = len(actions)
17gradient = [0.0] * n
18for i, advantage in enumerate(advantages):
19 for j in range(n):
20 score = (1.0 if i == j else 0.0) - probabilities[j]
21 gradient[j] += probabilities[i] * score * advantage
22
23def objective(candidate_logits):
24 return sum(p * value for p, value in zip(softmax(candidate_logits), action_values))
25
26delta = 1e-5
27for j in range(n):
28 plus, minus = logits.copy(), logits.copy()
29 plus[j] += delta
30 minus[j] -= delta
31 finite_difference = (objective(plus) - objective(minus)) / (2 * delta)
32 assert abs(gradient[j] - finite_difference) < 1e-6
33
34updated = softmax([logit + 0.03 * value for logit, value in zip(logits, gradient)])
35assert abs(updated[0] - 0.089) < 0.001
36assert abs(updated[2] - 0.508) < 0.001
37for action, before, after in zip(actions, probabilities, updated):
38 print(f"{action:16} before={before:.3f} after={after:.3f}")
39print("exact gradient matches finite differences")1auto_approve before=0.333 after=0.089
2request_evidence before=0.333 after=0.403
3human_review before=0.333 after=0.508
4exact gradient matches finite differencesThe gradient step pushes the human review probability from to , while driving the policy-violating auto-approval down to .
For a text-only completion with no external tool interaction, token generation has a useful MDP representation:
- State : The prompt plus generated tokens, ; is just the prompt.
- Action : The next token , including an end-of-sequence token. Vocabulary size depends on the model.
- Transition dynamics: Deterministic token appending: .
- Reward : Typically a sparse scalar assigned at sequence completion by a trained reward model or an automated verifier (such as unit test execution or mathematical proof checks).
Tool-using agents also have external observations and actions; appending a token doesn't describe all their environment dynamics. Prompt difficulty, the reward's units, termination, and token-length weighting remain part of the training objective.
In classical RLHF (Ouyang et al., 2022 [6]), Proximal Policy Optimization (PPO, Schulman et al., 2017 [7]) optimizes the language model:
- An actor network generates candidate completions.
- A separate critic network predicts expected reward from partial token sequences, providing token-level baselines for generalized advantage estimation (GAE).
- A frozen reference model penalizes large divergence from the initial supervised model using a token-level Kullback-Leibler (KL) penalty: .
The human-preference reward model is a fourth role, scoring completions. These are conceptual roles, not a guarantee of four equally sized independent networks. A separate large critic adds weights, training state, compute, and communication; total memory doesn't automatically double.
PPO's clipped surrogate compares the current policy with the policy that sampled the rollout. That older sampling policy and the frozen reference serve different purposes. Clipping discourages some large probability-ratio changes; it doesn't impose a hard KL bound or guarantee improvement under repeated optimization.[7]
DeepSeek introduced Group Relative Policy Optimization (GRPO) in DeepSeekMath (2024) and used it in DeepSeek-R1 (2025). The original recipe provides a critic-free example, rather than a complete description of every current GRPO variant:[8][9]
- Eliminating the critic: GRPO doesn't train a separate value network .
- Group sampling: For each input prompt , the policy samples a group of independent completions: .
- Group-relative score: Each completion receives a reward . DeepSeekMath describes learned outcome and process reward models; R1's reasoning RL uses rule-based accuracy and format rewards. For outcome supervision, normalize the completion scores across the group, with a small numerical stabilizer:
- Objective: Use a PPO-style clipped token-ratio surrogate and a reference-policy KL term. In the original outcome-supervision recipe, each token in a completion receives the same group-relative score. Removing the value model saves its resources; rollout lengths, group size, and other models still determine total cost.
If every completion has the same reward, the normalized reward component is zero; a KL term may still contribute an update. This is an empirical group-relative statistic, not the exact advantage or automatically an unbiased REINFORCE estimate. Its group mean includes the sample being updated and its standard deviation rescales prompt contributions. State which variant, normalization, and length weighting you use.
The bridge is reward-based probability updates over sequences. A better surrogate score doesn't itself prove stronger reasoning, a better verifier, or useful behavior on held-out tasks.
What a policy scorecard can and can't establish
Use an oracle scorecard to check four constructed starting cases against our known environment equations. Each rule below chooses an action in those two starting states; the sole approve_request action at review_queue is fixed. The chosen mixture of two new_request and two evidence_ready cases is part of the calculation, not representative-traffic evidence:
1action_values = {
2 "new_request": {
3 "auto_approve": -120,
4 "request_evidence": 30.75,
5 "human_review": 54.0,
6 },
7 "evidence_ready": {"auto_approve": 70, "human_review": 62.0},
8}
9cases = ["new_request", "new_request", "evidence_ready", "evidence_ready"]
10policies = {
11 "always_auto": lambda state: "auto_approve",
12 "always_review": lambda state: "human_review",
13 "risk_aware": lambda state: (
14 "human_review" if state == "new_request" else "auto_approve"
15 ),
16}
17
18for name, policy in policies.items():
19 returns = [action_values[state][policy(state)] for state in cases]
20 unsupported = sum(
21 state == "new_request" and policy(state) == "auto_approve"
22 for state in cases
23 )
24 mean_return = sum(returns) / len(returns)
25 if name == "risk_aware":
26 assert mean_return == 62.0
27 assert unsupported == 0
28 if name == "always_review":
29 assert mean_return == 58.0
30 print(
31 f"{name:13} expected mean={mean_return:5.1f} unsupported_approvals={unsupported}"
32 )1always_auto expected mean=-25.0 unsupported_approvals=2
2always_review expected mean= 58.0 unsupported_approvals=0
3risk_aware expected mean= 62.0 unsupported_approvals=0The oracle scorecard knows counterfactual values because we specified the environment equations. In real operations, historical logs record only the action chosen and its resulting outcome. Evaluating an alternate policy from logs collected by a behavior policy requires off-policy evaluation (OPE):
- Importance sampling: Under common dynamics and a common initial-state distribution, an entire episode's return uses the trajectory weight . Per-decision estimators weight each reward by the cumulative product through the action that produced it, not merely that step's ratio.[1]
- Coverage requirements: If for an action the target chooses in a reachable state, importance sampling lacks support. A positive propensity with no observed visits is different: theoretical support can hold while a finite log still contains no usable samples for that choice.
- Unmeasured confounding: Private information that affects both action selection and outcomes can invalidate causal evaluation from the recorded states and propensities. This is separate from whether a state representation is Markov.
For two logged decisions with behavior probabilities 0.5, 0.5 and target probabilities 0.8, 0.6, the trajectory weight is (0.8/0.5)*(0.6/0.5)=1.92. Using only the last ratio, 1.2, misses how the first choice changes trajectory frequency. Products can have high variance; a large log doesn't guarantee enough effective samples. Model-based and doubly robust OPE offer other tradeoffs, but don't identify unsupported actions without additional assumptions or evidence.
Record the actual logging propensities, relevant decision-time information, and the desired starting-case distribution. Verify coverage and temporal relevance; a chronological split alone doesn't repair confounding or a misspecified environment.
Stress-test the assumptions
Test your reasoning by isolating individual components of the MDP:
Change the abandonment probability in stochastic-transition-backup.py. At what exact probability does human review overtake requesting evidence?
Answer
At abandonment probability p, the expected return of requesting evidence is (1-p)59 + p(-54) = 59 - 113p. Setting this equal to review's fixed return of 54 gives 59 - 113p = 54, yielding p = 5/113 ≈ 4.42%. Below this threshold, evidence wins. Above it, direct human review wins.
Remove auto_approve from new_request in the Q-learning lab while leaving it available at evidence_ready. What changes in learning and execution?
Answer
With the lab's Q-table and counters rebuilt from the restricted action dictionary, that state-action entry is absent, rather than a zero-valued available choice. Random exploration also uses the restricted list. The simulator must reject invalid actions, and a real execution layer must enforce the same rule independently; a larger penalty only changes an incentive.
A production log contains 50,000 cases routed to human review, but zero evidence requests. Can an offline evaluation measure how an evidence-seeking policy would perform?
Answer
Not from those episodes alone without extra assumptions or evidence. No evidence-request outcomes were observed. If the logging policy prohibited that action, importance sampling also lacks theoretical support; if it allowed the action with tiny probability, this finite log still supplies no samples for that choice. A time split can't create the missing outcomes.