A minimal proof-of-concept showing how two LLM-powered agents (a Developer and a Reviewer) can collaborate in natural language to solve a programming task end-to-end.
The demo loads CodeLlama-7B-Instruct to write code and CodeLlama-13B-Instruct to review it, runs unit tests automatically, and iterates until the solution is APPROVED.
- Multi-agent loop driven by plain Python – no hidden frameworks.
- Deterministic(ish) generation with low-temperature sampling for code.
- Offline execution (CPU-only is possible, ~16 GB RAM per model when loaded in 4-bit).
- Unit-test feedback injected into the Reviewer’s context.
- Docker + docker-compose setup that mounts your local Hugging Face cache.
- A ready-made
tests.pyexample (Sieve of Eratosthenes).
├── download_models.py # robust HF snapshot downloader with resume
├── multi_agent_codellama.py # core demo script
├── tests.py # sample unit tests
├── Dockerfile # slim Python image with CPU PyTorch + transformers
└── docker-compose.yml # single-service stack mounting HF cache# 1. clone the repo
git clone https://github.com/YOUR_USERNAME/multi-agent-codellama.git
cd multi-agent-codellama
# 2. (one-time) download the models to ~/.cache/huggingface
python download_models.py # ≈ 13 GB total – resumable
# 3. build and launch the container (CPU image)
docker compose build
docker compose run --rm llama-agents \
python multi_agent_codellama.py \
"Write a function primes_up_to(n) that returns all primes ≤ n using the Sieve of Eratosthenes."The compose file mounts your local HF cache into the container, so the heavy model files are reused across runs.
Below is the full control-flow executed by multi_agent_codellama.py.
make_agents()loads two Code Llama checkpoints via Hug Transformers:- Developer →
codellama/CodeLlama-7b-Instruct-hf - Reviewer →
codellama/CodeLlama-13b-Instruct-hf
The models are instantiated withdevice_map="auto"and 4-bit GPTQ so they fit in CPU RAM.
- Developer →
- A dedicated
AutoTokenizeris created for each model; both are stored inside anAgentdataclass together with:- a system prompt (role instructions),
- a private chat history list.
-
Task ingestion from the CLI
When you launch the script you pass a natural-language brief, e.g.:python multi_agent_codellama.py \ "Write a function primes_up_to(n) ..."That string is stored in the local variable
requirement. -
Tagging the brief as a system message
The script rewrites the raw text into the fixed pattern:"System: Requirement — " + requirementWhy?
- Using the System label tells the LLM “this is immutable context, not a turn you must answer.”
- Prefixing with Requirement — makes the intent crystal-clear, even if the prompt later grows.
-
Injecting into the Developer’s memory
developer.hear(f"System: Requirement — {requirement}")
Under the hood this is a simple list.append() on developer.history, so the Developer now holds a single entry:
["System: Requirement — Write a function primes_up_to(n) ..."]The Reviewer’s history remains an empty list at this stage.
-
Zero tokens spent so far No model call happens during seeding. The first inference is triggered later by:
dev_reply = developer.speak()
At that moment the Developer’s prompt becomes:
<system_prompt> System: Requirement — ... Developer:Only then does Code Llama start generating.
| Phase | What happens | Key implementation details |
|---|---|---|
| Developer speaks | 1. The helper Agent._format_prompt() concatenates:• system prompt • full history • literal “`Developer:`” 2. The string is passed to llama_generate(), which calls the 7 B model with:• low temperature = 0.15 (makes answers stable) • top_p=0.9, repetition_penalty=1.05.3. The raw completion is appended to history and printed to the terminal. |
Developer’s prompt explicitly forbids commentary and demands only one fenced code block. |
| Code extraction | A simple regex pulls the first triple-fenced block (optionally tagged “python”). | Regex pattern lives in CODE_BLOCK_RE. |
| Unit-test harness | 1. Code is written to a temp file. 2. The temp file is imported as module solution.3. The chosen tests.py is exec’d with that module in scope.4. Stdout + traceback get captured so the Reviewer can see failures. |
Entire routine happens in run_tests() and returns a boolean + feedback string. |
| Reviewer context update | Two messages are appended to the Reviewer’s history: 1. A fenced code dump of what the Developer wrote. 2. Plain text: “Unit tests passed: <True/False>” plus captured output. |
Keeps the review process reproducible and transparent. |
| Reviewer speaks | Reviewer’s formatted prompt (similar to Developer’s) is run through the 13 B model. The reply can only be: • APPROVED — tests passed and code looks clean.• REJECTED — followed by specific bullet-point suggestions. |
If the Reviewer writes APPROVED, the script exits successfully. Otherwise the whole Reviewer reply is appended to the Developer’s history and the next round begins. |
The loop terminates as soon as:
- Reviewer says
APPROVED, or - The maximum number of rounds (CLI flag
--rounds, default 3) is reached.
- Few-shot memory – Every turn’s transcript is re-fed to the respective agent; this creates an implicit shared memory without an external vector DB.
- Role separation – Although both agents use Code Llama, divergent system prompts make them behave differently.
- Explicit test feedback – Piping the unit-test outcome into the Reviewer dramatically increases the chance of meaningful reviews compared to asking the model to “imagine” runtime errors.
- Resume downloads –
download_models.pyskips files already present and usesresume_download=Trueto survive network drops. - Footgun guard – The Reviewer must literally start with “APPROVED”; accidental praise that lacks the keyword will keep the loop alive.
- Scalability – Swap in larger/smaller Llama variants, or add more agents (e.g. “Optimizer”) by instantiating extra
Agentobjects and adjusting the turn order.
That’s it ~200 lines of Python orchestrate two LLMs, tests, and an approval workflow without any heavyweight dependencies.