← Back to Agentic AI map
Lesson 1.11 · Agents From Scratch

Two Agents Talking

The simplest multi-agent setup: one agent’s output becomes the next agent’s input.

multi-agent

What you will be able to do

  • Build a two-agent pipeline with nothing but two functions
  • Give each agent a role through its system message
  • Explain what the agents share - and what they do not
  • Tell a fixed hand-off apart from a ReAct loop
  • Explain why an agent without tools is not doing research
  • Say what a reviewer agent can and cannot catch

The idea, in plain English

The simplest form of "multi-agent" needs no framework at all. Run two separate agents with different jobs, and pass what the first one produced into the second one’s prompt. In Python terms it is one function called with the result of another: notes = researcher_agent(topic), then article = writer_agent(topic, notes).

Each agent gets its own system message, which is what makes them different. The researcher is told to produce facts and nothing else. The writer is told to turn notes into an article. Neither knows the other exists.

They also share no memory - each one is a single call that starts fresh. The only connection between them is the text you choose to pass along. That is a real limitation, and one the frameworks in later modules exist to address; it is also why this pattern is so easy to reason about: there is no hidden state anywhere.

And it has a blind spot worth seeing for yourself. The "researcher" has no tools, so its facts come from the model’s memory - and the writer has no way to tell a true note from a false one. We ran exactly that experiment, and the results are below.

Worked example: A researcher agent hands off to a writer agent.

workflowHow a false fact travelsstep 1 / 4

1 - The notes contain a false fact

Four true points and one false one: "Local LLMs are encrypted by default, so a stolen laptop cannot expose your conversations." A tool-less researcher can produce exactly this kind of plausible error.

notes
5 bullets
false
1
looks false
no
source
none - no tools

A real run. One note handed to the writer was false - "local LLMs are encrypted by default". Step through what each agent did with it.

The hand-off is just function calls

researcher_agent(topic) makes one model call and returns text. writer_agent(topic, notes) makes another, with the notes pasted into its prompt. That is the whole pattern: output of one, input of the next.

It generalises to any pair with separate jobs - a planner and a coder, an analyser and a report writer, a support agent and an escalation agent - and to longer chains: add a third function that takes the article.

Each role is a system message

The two functions are nearly identical; the system message is what makes one a researcher and the other a writer. That makes roles cheap to create - and only as reliable as the model’s obedience to them.

In our runs the researcher followed "4-5 key facts as short bullet points" closely: five bullets every time, with an introductory line in one run of four. The writer followed "3-paragraph article" less well: two of three articles added a title line, and the one we read line by line added claims the notes never made, such as "even when sharing it with third-party services".

No shared memory, no loop

Each agent is a separate, one-shot call with its own system message. There is no shared messages list; the writer knows only what you put in its prompt. If it needs something, you have to pass it.

There is no loop either. A ReAct agent from Lesson 1.6 decides what to do next, again and again; this pipeline is two fixed calls in a fixed order. Nothing can go back to the researcher for more facts, and nothing decides when the article is good enough.

Hand-off and ReAct
Hand-off (1.11)Fixed sequence of calls. Each agent one job, one call.
ReAct (1.6)One agent looping: decide, act, observe, repeat.

A researcher without tools is not researching

The researcher has no search, no files, no database. Its "facts" are whatever the model generates - which is exactly the chatbot problem from Lesson 1.4, renamed.

We asked it about the history of the Python language three times. Alongside correct dates, it said Python 2.0 introduced "a just-in-time compiler" (it did not), that Python 3.7 introduced "optional static typing and improvements to the print function" (type hints arrived in 3.5, the print function in 3.0), and that Python 1.2 was "a major rewrite of the language". Every one was formatted as a crisp, confident bullet point.

Watch out: Calling an agent a "researcher" does not make it one. Without a tool that fetches real information, research notes are generated text - and a writer downstream cannot tell.

Handing on makes errors sound surer

We gave the writer five notes, one of them false: local LLMs are "encrypted by default, so a stolen laptop cannot expose your conversations". In three runs out of three, the article repeated it - and improved it: "designed with security in mind", "an added layer of protection".

This is the risk of chaining agents. Each stage trusts the one before, and a writer’s job is to make things read well. A shaky note goes in; a polished, confident paragraph comes out.

A reviewer agent is not a fact-checker

The natural fix is a third agent that reviews the article. We built one, told to list every claim the notes do not support. It approved the article with the false encryption claim - correctly, since the claim was in the notes. It also approved an article whose writer had added unsupported claims of its own, twice: "None. Every claim in the article is supported by the research notes."

Two lessons in one. A reviewer that sees only the notes can check consistency, never truth. And with a small model, "review this" can become a rubber stamp. Real verification needs real sources - which is what POC 3 adds by giving the researcher a search tool - and, for anything that matters, a person.

Step-by-step code

Researcher, then writer
import ollama def researcher_agent(topic): response = ollama.chat( model="llama3.1", messages=[ {"role": "system", "content": "You are a researcher. Given a topic, list 4-5 key facts as short bullet points. No commentary."}, {"role": "user", "content": topic} ] ) return response["message"]["content"] def writer_agent(topic, research_notes): response = ollama.chat( model="llama3.1", messages=[ {"role": "system", "content": "You are a writer. Turn research notes into a short, friendly 3-paragraph article."}, {"role": "user", "content": f"Topic: {topic}\n\nResearch notes:\n{research_notes}"} ] ) return response["message"]["content"] topic = "Benefits of local LLMs for privacy" notes = researcher_agent(topic) print("--- Research notes ---") print(notes) article = writer_agent(topic, notes) print("\n--- Final article ---") print(article)
Output - from a real run
--- Research notes --- • Local processing reduces data transmission and minimizes exposure to potential attackers. • It allows for more effective data anonymization and pseudonymization. • Local LLMs can better comply with data localization regulations and laws. • Reduced reliance on cloud services and centralized infrastructure improves overall system security. • Local processing enables more precise and personalized decision-making, reducing the risk of biased or unfair outcomes. --- Final article --- The Importance of Local LLMs for Your Privacy When it comes to processing large amounts of data, it's crucial to prioritize privacy. ... ... you can ensure that sensitive information is kept private and confidential, even when sharing it with third-party services. <- not in the notes ... Perhaps most importantly, local LLMs enable more precise and personalized decision-making, reducing the risk of biased or unfair outcomes. <- the weakest note, promoted ... making it a win-win for both your organization and your customers.
The researcher on a topic you can check
researcher_agent("The history of the Python programming language") - three runs • 1991: Python 0.9.1 is released. correct • 2000: Python 2.0 ... introduced a new garbage collector and a just-in-time compiler. wrong - no JIT • 2018: Python 3.7 was released, which introduced optional static typing and improvements to the print function. wrong - 3.5 and 3.0 • 1994: Python 1.2 is released, featuring a major rewrite of the language. wrong • Python 3.0 was released in 2008, ... breaking backward compatibility. correct
One false note in, one confident paragraph out
Notes passed to writer_agent - the third is false: - Prompts and documents never leave your own machine. - No third-party provider can log or train on your data. - Local LLMs are encrypted by default, so a stolen laptop cannot expose your conversations. - You can run them without an internet connection. - Sensitive data can stay inside your organisation's network. Three runs, false claim repeated in all three: "Local LLMs are designed with security in mind. By default, they're encrypted, which means that even if your laptop is stolen, your conversations remain protected." "Another significant benefit of local LLMs is their default encryption." "Local LLMs are also encrypted by default, providing an added layer of protection."
A third agent: the reviewer
def reviewer_agent(research_notes, article): response = ollama.chat( model="llama3.1", messages=[ {"role": "system", "content": ( "You are a fact-checking reviewer. Compare the article with the research notes. " "List every claim in the article that the notes do not support, one per line. " "If every claim is supported, reply with only the word APPROVED." )}, {"role": "user", "content": f"Research notes:\n{research_notes}\n\nArticle:\n{article}"}, ], options={"temperature": 0}, ) return response["message"]["content"] review = reviewer_agent(notes, article) print(review)
What the reviewer said
On the article with the false encryption claim: * None. Every claim in the article is supported by the research notes. -> true: the false claim IS in the notes. It checks consistency, not truth. On the article with the writer's own additions ("third-party services", "win-win"): * None. Every claim in the article is supported by the research notes. -> missed them, twice.

Watch out: The researcher has no tools, so its "facts" come from the model’s training and may be wrong. Handing them to a writer makes them sound more confident, not more true. POC 3 replaces it with a real search step for exactly this reason.

Tip: Print what passes between agents. The notes are the only thing the writer knows - reading them is how you find out whether a bad article was the writer’s fault or the researcher’s.

Two agents at a glance

Agent

A function: a role (system message) plus one model call.

def researcher_agent(topic): ...
Hand-off

Pass one agent’s output into the next agent’s prompt.

writer_agent(topic, notes)
Shared memory

None - only the text you pass.

f"Research notes:\n{research_notes}"
Loop

None - a fixed sequence of calls.

notes -> article
Grounding

Real facts need a real source: a tool.

search(query)  (POC 3)

Try it yourself

The code does not change. Swap the content string and the program does something else entirely.

Another topic

“Benefits of microservices architecture”

A checkable topic

“The history of the Python programming language”

Plant a false note

“Add one wrong fact to the notes by hand and see whether the article repeats it.”

Add a reviewer

“Chain reviewer_agent(notes, article) on the end and see what it approves.”

What usually goes wrong

Expecting the agents to share memory

Each is a separate call. The writer knows only what you put in its prompt.

✗ writer_agent(topic)  # hoping it remembers the notes
✓ writer_agent(topic, notes)
Calling a tool-less agent a researcher

Its notes are generated, not looked up - confident, and sometimes wrong. Give it a real source.

Trusting the last agent in the chain

Each stage inherits the errors of the one before, and a writer makes them read better. Check the notes, not just the article.

Treating a reviewer agent as a fact-checker

A reviewer that sees only the notes can confirm consistency, never truth - and a small model may approve everything.

Calling a fixed hand-off an autonomous agent

Two calls in a row cannot change course, ask for more, or decide they are done. That needs a loop (Lesson 1.6) or a graph (Module 3).

Key points

  • A multi-agent pipeline can be two plain functions: notes = researcher(topic); article = writer(topic, notes).
  • Each agent’s role is its system message.
  • Agents share nothing but the text you pass between them.
  • A hand-off is a fixed sequence; ReAct is a loop that chooses its next step.
  • A researcher without tools generates facts - in our runs, some were wrong.
  • A writer repeats and polishes whatever it is given - a false note came out stronger, 3 times of 3.
  • A reviewer checks consistency with the notes, not truth - real verification needs real sources.

Quick check before you move on

What is handed from the researcher to the writer?
The researcher’s output text - the notes - pasted into the writer’s prompt.
Do the two agents share conversation history?
No. Each is its own call with its own system message.
Does the researcher actually research anything?
No - it has no search tool, so its facts come from the model’s training.
Is there a loop?
No. It is two fixed calls in sequence.
Why did the reviewer approve an article with a false claim?
The false claim was in the notes, and the reviewer only checks the article against the notes.

Quiz

  1. 1.

    What is being "handed off" from the researcher agent to the writer agent?

  2. 2.

    Do the two agents share the same conversation history?

  3. 3.

    How is this different from the ReAct loop in Lesson 1.6?

  4. 4.

    The notes contain a wrong fact. What does the writer do with it?

  5. 5.

    What would make the researcher’s notes trustworthy?

Interview questions

What is a multi-agent system?

Several agents with specialised responsibilities that cooperate by passing information between them. The simplest form is a pipeline where one agent’s output becomes the next agent’s input.

Do you need a framework to build multiple agents?

No. Ordinary functions and model calls are enough for a pipeline. Frameworks add shared state, routing, loops between agents, and observability on top of the same data flow.

Do agents share memory?

Not unless you build it. Each call sees only its own messages, so anything one agent needs from another has to be passed explicitly.

What are the risks of chaining agents?

Errors propagate and get amplified: an ungrounded fact from one agent is repeated, often more confidently, by the next. Reviewer agents that see only upstream output check consistency, not truth. Ground agents in real sources and keep a human check for anything that matters.

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...