Everyone wants AI to clear their logistics bottlenecks, and most teams start by treating the problem as regression. It rarely is. Logistics shifts context constantly and runs into hard physical constraints, and that combination is where Multi-Agent Reinforcement Learning (MARL) earns its keep, as long as the implementation does not swallow the project.
The usual failure is one “mega-agent” asked to handle routing and box-packing at once. You get race conditions and very little learning. Splitting high-level strategy from low-level execution is less elegant on paper and much easier to debug.
A hybrid of RL strategy and LP execution
Your RL agent should not be spending gradient steps on how one parcel fits into one van. That burns compute and makes training worse. Hand the routing strategy to Reinforcement Learning and give the physical packing to Linear Programming (LP), which is what LP is good at.
It is a separation of duties. The RL agent decides where assets go; the LP solver works out how to pack them. Because that split holds, the system moves between warehouse layouts without retraining the whole model.
def bbioon_decide_and_dispatch(self, action):
# Parse RL actions into active destinations
neighb_action = {v_id: num_v for v_id, num_v in enumerate(action) if num_v > 0}
if not neighb_action:
return 0, 0
# Linear Programming handles the heavy lifting of packing
av_vehicles = self.get_available_vehicles()
parcels_result, edges_result = send_veh(neighb_action, available_parcels, av_vehicles)
# Update state based on physical execution
self.process_sent(parcels_result)
return edges_result.total_cost
Designing scale-invariant observations
The observation space is where Multi-Agent Reinforcement Learning usually goes wrong. Train an agent on raw counts, say 100 packages, and it falls apart the first time it sees 1,000. Ratios fix that. Rather than raw counts, track the share of the total backlog, or how urgent one deadline is relative to the others.
Divide local inventory by the total daily workload and you get an input that means the same thing everywhere. The same agent can run at a small rural hub and at a metropolitan sorting center, and its weights still mean something in both. I use the same reasoning for machine learning environment scaling on large WordPress deployments.
Keeping the MARL training loop stable
MARL is unstable because every agent’s actions depend on what the others do. When they all learn at the same time, the training signal is mostly noise. Libraries like Stable-Baselines3 also have no built-in support for multi-agent coordination.
A sequential training pipeline is the workaround. In any given episode, exactly one agent is in training mode and the rest run frozen inference. That stops agents from chasing a moving target, adapting to early-stage behavior their peers are about to unlearn.
# The sequential training loop
for training_ag_id in agents.keys():
# Switch context to the active agent
env.env_method('set_cur_training_agent', training_ag_id)
# Train ONLY this agent while others are frozen
agent_obj = agents.get(training_ag_id)
agent_obj.learn(total_timesteps=TS_PER_AGENT)
# Save weights and sync model cache
agent_obj.save(path)
Rotating the learner lets the network settle on a global strategy without simultaneous gradient updates fighting each other. Not theoretically pretty, but it trains.
If this kind of MARL work is eating your dev hours, I can take it off your plate. I’ve been working with WordPress since the 4.x days.
What generalizes, and what does not
The abstraction matters more than the size of the network. RL for strategy plus LP for execution gives you a system that keeps working when a snowstorm shuts a route, when tariffs jump, or when holiday orders surge, and not only on the bench.
I have written separately about machine learning pitfalls, and why a high accuracy number often hides the problem.