Skip to main content
What you’ll learn: how to wire up an asynchronous multi-agent system in Miles, where two (or more) specialized agents take alternating turns and the joint outcome drives a single shared reward. This example uses a dual-agent setup that interleaves a “thinker” and a “verifier”, but the same pattern scales to:
  • Doctor / patient simulations.
  • Multi-step DeepResearch pipelines.
  • Adversarial games (proposer / solver).
The supporting framework for the production version of this is MrlX — Miles ships the kernel of the same idea so you can hack on it without pulling in MrlX’s full dependency tree.

Prerequisites

Files

Quick start

Configuration

Asymmetric reward weighting (0.8 / 1.2) gives a small bias toward upweighting “correct” trajectories, which empirically stabilizes early training when most attempts fail.

Launch script highlights

Two flags matter most:
  • --rollout-max-context-len — total context budget across all turns. Larger than --rollout-max-response-len because we accumulate.
  • --global-batch-size 256 = 32 × 8 — matches the rollout invariant.

Walkthrough — the agent loop

The shipped pipeline is solver → rewriter → selector: num_parallel solver attempts run in parallel, each rewriter rewrites the previous solutions, and a SelectorAgent picks one. Sampling params are set on args upstream by the rollout helper, so run_agent_system only takes (args, sample).
agent_system.py
Both roles share the same SGLang process — solver_worker, rewrite_worker, and SelectorAgent.select all post to the same engine, just with different prompts. So both agents are the same model updating in lockstep. For architecturally distinct agents (separate models), see the MrlX repo.

Walkthrough — rollout integration

rollout_with_multi_agents.py exposes generate_with_multi_agents(args, sample, sampling_params, evaluation=False). Internally it:
  1. Sets args.sampling_params = sampling_params and args.tokenizer, then loads the custom multi-agent function from args.custom_multi_agent_function_path.
  2. Calls await custom_multi_agent_func(args, sample) to get the list of samples produced by the solver / rewriter / selector pipeline.
  3. Returns the shuffled list of Samples for the trainer to pack.
The per-sample tokenization and reward already happen inside solver_worker / rewrite_worker / SelectorAgent.select (which call batched_async_rm), so the rollout integration itself is a thin wrapper.

Tuning knobs

What to watch

If loss_mask/role_split is heavily skewed, one role is dominating — typically the verifier becomes verbose. Tighten its system prompt or reduce its max_tokens.

Troubleshooting

Variations

VLM multi-turn

Replace call_role with a VLM-aware caller that includes images in messages. Miles supports VLM multi-turn natively — same pattern, just multimodal_train_inputs in the sample dict (see Customization #13).

True asymmetric agents

Run two SGLang services — one per agent — and have your rollout function call the appropriate URL per turn. The trainer can either train both jointly (one optimizer per model) or train one and freeze the other (PvE).

Adversarial pairing

Instead of a verifier, the second agent is an adversary that tries to find weaknesses in the thinker’s answer. Reward both: thinker for surviving, adversary for breaking. This is the seed of self-play RLHF.