Sitemap

Learning Python by Building Agent Patterns — The Augumented LLM

6 min readJust now

--

Press enter or click to view image in full size

A bit about me

I’m an aerospace engineering grad. I graduated right as COVID hit, and aviation went down the drain. So I pivoted to software engineering.

That was six years ago. I’ve been a full-stack engineer ever since, mostly in JavaScript. I’ve written some Python along the way, but I wouldn’t call myself someone who can write production-grade Python.

Why I’m doing this

The main reason is simple: I want to get good at Python. Most machine learning libraries and most AI engineering work are done in Python.

Anthropic’s Building Effective Agents is an article I keep coming back to. I’ve already built some of its patterns in n8n. This time I want to build them in code, so I actually understand what’s happening under the hood.

I also wanted to start small. Instead of jumping straight into LangChain or LangGraph, I’m using just the Anthropic Python SDK. No framework, no commitment, just the patterns.

How this series works

There are seven patterns in the article: the augmented LLM, prompt chaining, routing, parallelization, orchestrator-workers, evaluator-optimizer, and autonomous agents. I’m building one pattern at a time, in Python, with Claude guiding me, and writing one post per pattern.

In each post I’ll explain my understanding of the pattern, show what I built, and point out the Python choices I’m unsure about. All the code is on GitHub: agent-playbook

This is where I’d love your help. If you write Python for a living, tell me what you’d do differently. If my understanding of a pattern is off, tell me that too.

Without further ado, let’s get into my augmented LLM implementation.

Pattern 1: The augmented LLM

My understanding

On its own, an LLM only knows what it was trained on and what you put in the prompt. It can’t look things up, it can’t do reliable maths, and it forgets everything between API calls.

An augmented LLM is the same model with three things added: retrieval, so it can pull in information it was never trained on; tools, so it can compute or act instead of guessing; and memory, so it remembers what happened earlier in the conversation.

The big difference from just calling the API is who’s in charge. The model decides when to search, which tool to use, and what to do with the result. My code just runs what it asks for and hands back the answer.

Get Reshs’s stories in your inbox

Join Medium for free to get updates from this writer.

Anthropic calls this the basic building block. Every other pattern in this series is built out of augmented LLMs.

What I built

I’ve always toyed with the idea of an agentic team: several agents, each with a specific role, all doing RAG over source documents so you can guarantee a bit more accuracy in the output.

This was my first step toward that. I built a research assistant over three PDFs on prompt engineering, including Anthropic’s guide and Lee Boonstra’s whitepaper. Think of it as the first member of that future team: a prompt engineer that answers from real sources instead of from memory.

Under the hood it does three things. It searches my PDFs semantically using Voyage AI embeddings, it has a safe calculator tool, and it remembers the conversation.

How it flows

The choices I made

Voyage AI for embeddings. I’m a fan of MongoDB, and when MongoDB brought Voyage AI in for its vector search, I wanted to try it. This project gave me the excuse.

Pydantic and pydantic-settings. Coming from TypeScript, I like my code to be type-safe. Pydantic gave me that for my data models, and pydantic-settings did the same for config and API keys. It was a no-brainer.

An ast-based calculator instead of eval(). The quick way to build a calculator tool in Python is eval(). The problem is that eval() runs any Python code, not just maths. If the model, or a sneaky instruction hidden in one of my PDFs, sent something like __import__('os').system('rm -rf ~'), it would just run. So I parse the expression with the ast module and only allow numbers and basic arithmetic operators. Anything else gets rejected.

My folder structure

This is the part I’d most like feedback on. Coming from JavaScript, I had no idea what a “proper” Python project layout looks like, so I went with a src/ layout managed by uv:

augmented-llm
├── data
│ ├── prompt engineering anthropic.pdf
│ ├── prompt engineering lee boonstra.pdf
│ └── prompt engineering.pdf
├── src
│ └── augmented_llm
│ ├── agent
│ │ ├── __init__.py
│ │ ├── loop.py
│ │ └── tools.py
│ ├── knowledge
│ │ ├── __init__.py
│ │ ├── chunking.py
│ │ ├── embeddings.py
│ │ ├── extraction.py
│ │ └── retrieval.py
│ ├── __init__.py
│ ├── config.py
│ ├── main.py
│ └── models.py
├── README.md
├── pyproject.toml
└── uv.lock

agent/ holds the tool-use loop and the tools. knowledge/ is the RAG side: extracting text from the PDFs, chunking it, embedding it and retrieving it. config.py and models.py hold the pydantic settings and data models.

Honestly not sure if splitting into agent/ and knowledge/ subpackages sensible for a project this size, or overkill?

Seeing it work (sample output)

(augmented-llm) naresh@Nareshs-MacBook-Pro augmented-llm % uv run augmented-llm
Built augmented-llm @ file:///Users/naresh/Documents/Github/agent-playbook/augmented-llm
Uninstalled 1 package in 0.73ms
Installed 1 package in 2ms
Hello from augmented-llm!
(augmented-llm) naresh@Nareshs-MacBook-Pro augmented-llm % uv run python -m augmented_llm.main

User: What is few-shot prompting? Search the documents to answer.
[tool call] search_documents( {'query': 'few-shot prompting'} )
[tool call] search_documents( {'query': 'what is few-shot prompting definition examples'} )
Claude: Based on the search results from the documents, **few-shot prompting** is a technique in prompt engineering where you provide the language model with a few examples of the task you want it to perform before asking it to complete a similar task.

The key characteristics of few-shot prompting are:

1. **Learning from examples**: You include several demonstration examples in the prompt that show the model the pattern or format you want it to follow.

2. **Improves performance**: By providing these examples, the model can better understand the task requirements and generate more accurate responses.

3. **No training required**: Unlike traditional machine learning that requires retraining models, few-shot prompting leverages the model's existing capabilities by simply showing it examples in the prompt itself.

For instance, if you want the model to classify sentiment, you might provide 2-3 examples of text with their sentiment labels (positive/negative) before asking it to classify a new piece of text. This helps the model understand the task and expected output format without any additional training.

User: What's 234 * 18?
[tool call] calculator( {'expression': '234 * 18'} )
Claude: Let me calculate that for you:

234 × 18 = **4,212**

User: Search the document for a specific number it mentions, then multiply that number by 3.
[tool call] search_documents( {'query': 'number specific value mentioned'} )
Claude: I found some results, but I need more specific guidance. The document likely contains many numbers. Could you please specify:

1. What type of number are you looking for? (e.g., a percentage, a year, a quantity, a measurement)
2. What context is it in? (e.g., related to prompting techniques, examples, statistics, etc.)

This will help me search for the specific number you have in mind so I can multiply it by 3.

User: Going back to my first question — can you give one more example of when you'd use that technique?
Claude: Sure! Here's another practical example of when you'd use **few-shot prompting**:

**Language Translation with Specific Style:**

Let's say you want the model to translate English phrases into French, but with a casual, conversational tone rather than formal language. You could use few-shot prompting like this:

```
English: "Hey, what's up?"
French: "Salut, ça va?"

English: "Want to grab some food?"
French: "Tu veux manger un truc?"

English: "That's awesome!"
French: "C'est génial!"

English: "See you later!"
French: [model completes this]
```

By providing these few examples, the model learns:
- The translation task (English to French)
- The informal/casual tone you want
- The formatting pattern you expect

This is much more effective than just asking "Translate this to casual French" without examples, because the model can see exactly what style and format you're looking for. Few-shot prompting is especially useful whenyou need consistent formatting, specific styles, or when the task might be ambiguous without concrete examples to guide the model.

Wrapping up

That’s pattern one: an LLM that can search my notes, do maths safely, and remember the conversation. Every other pattern in this series builds on it.

I’d really like your take on the folder structure questions above, or anything else you’d do differently. Leave a comment or open an issue on the repo.

--

--