Back to list
60 ラインの Python で Agent : Part 3
Agents in 60 lines of python : Part 3
Translated: 2026/3/21 4:00:37
Japanese Translation
Agent Loop
60 ラインの Python で AI Agent のスタックを構築します。
Claude がファイルを捜索し、それを読み、さらに捜索を行う様子を見たことはありませんか?ChatGPT コードインタープリタはコードを生成し、実行してエラーを検出し、修正し、再度実行します。この往来は魔法ではありません。それはループです。
第 2 回のレッスンで学んだ Agent は一度のツールコールで終了しました。これは「2+2 はいくつですか?」といった質問には十分ですが、実用的な anything には役立ちません。コードベースの解析や関数のデバッグ、トピックの研究など、多段階タスクは Agent にその作業が完了するまで続ける必要があります。
それを可能にするのが Agent Loop です。驚くほど単純です。
リアルな Agent ループ:ツールを呼び出し、結果を確認、次につなげる判断をし、完了するまで繰り返し。LLM が停止するかを決めます。
フローは以下の通りです:
- メッセージ構築 — システムプロンプト + ユーザータスク
- LLM へ問い合わせ — 迄今までのすべてを送信
- tool_calls? — LLM は答え(完了)を返すか、ツールの使用を求めます
- ツール実行 — 実行し、結果を追加
- LLM へ問い合わせに戻る — 成長するメッセージ配列とともに
「ツール実行」から「LLM へ問い合わせ」への循環矢印が、玩具的な Agent と有用な Agent の間のすべての違いです。
async def agent(task, max_turns=5):
messages = [
{"role": "system", "content": "Use tools."},
{"role": "user", "content": task},
]
for turn in range(max_turns):
msg = await ask_llm(messages)
if not msg.get("tool_calls"):
return msg.get("content", "")
for turn in range(max_turns) — これが全体のリープです。各反復が 1 つの「ターン」です:LLM を問い、ツールが必要かどうかを確認します。なければ、答えを返します。max_turns の安全制限がランディングループを防ぎます(永遠にツールを呼び続ける LLM)。
LLM がツールを要求する場合は、それを実行し、結果を再度与えます:
messages.append(msg)
for tc in msg["tool_calls"]:
name = tc["function"]["name"]
args = json.loads(tc["function"]["arguments"])
result = tools[name](**args)
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"content": str(result),
})
tool_call_id は各結果をそのリクエストとリンクします。LLM が一度に 2 つのツールを要求する場合、これはどの結果がどのコールに属するかを知る方法になります。
メッセージ配列は各ターンで成長します。これがタスク内で唯一の Agent のメモリです。
メッセージが入り、LLM が考え、ツールが実行され、結果がフィードバックされる。LLM が回答するのに十分なものを得たと判断するまで、そのループが回ります。
「3 を 4 に加え、次に hello を大文字にする」を試してみてください — LLM は複数のターンにわたって 2 つのツールコールをチェーンします。メッセージ配列の成長を見てください。
第 2 回のレッスンは 1 つの LLM コールでした:一度問い、ツールを使うかも、完了。これは「ファイルを捜索し、興味深いものを読み、要約する」という処理には対応できません。ループが Agent を実際には役に立たしめます。
これは LangChain の AgentExecutor、OpenAI の Agents SDK、AutoGen のすべてランタイムです。メッセージ上の while ループです。すべてのエージェントフレームワークはこのパターンをラップします。今あなたはそれが露わになってきました。
これで約 30 ラインの完全な Agent を見てください:
tools = {"add": lambda a, b: a + b, "upper": lambda text: text.upper()}
TOOL_DEFS = [
{"type": "function", "function": {"name": "add", "description": "Add two numbers",
"parameters": {"type": "object",
"properties": {"a": {"type": "number"}, "b": {"type": "number"}}}}},
{"type": "function", "function": {"name": "upper", "description": "Uppercase text",
"parameters": {"type": "object",
"properties": {"text": {"type": "string"}}}}},
]
async def ask_llm(messages):
resp = await client.chat.completions.create(
model="gpt-4o-mini", messages=messages, tools=TOOL_DEFS
)
return resp.choices[0].message
async def agent(task, max_turns=5):
messages = [
{"role": "system", "content": "Use tools to answer. Be concise."},
{"role": "user", "content": task},
]
for turn in range(max_turns):
msg = await ask_llm(messages)
if not msg.get("tool_calls"):
return msg.get("content", "")
messages.append(msg)
Original Content
The Agent Loop
The entire AI agent stack in 60 lines of Python.
You've seen Claude search files, read them, then search again. ChatGPT with Code Interpreter writes code, runs it, sees an error, fixes it, runs again. That back-and-forth isn't magic. It's a loop.
Lesson 2's agent made one tool call and stopped. That's fine for "what's 2 + 2?" but useless for anything real. Multi-step tasks — analyzing a codebase, debugging a function, researching a topic — need the agent to keep going until the job is done.
The agent loop is what makes that possible. And it's shockingly simple.
Real agents loop: call a tool, see the result, decide what's next, repeat until done. The LLM decides when to stop.
The flow is:
Build messages — system prompt + user task
Ask LLM — send everything so far
tool_calls? — the LLM either returns an answer (done) or asks for tools
Execute tool — run it, append the result
Back to Ask LLM — with the growing messages array
That loop-back arrow from "Execute tool" to "Ask LLM" is the entire difference between a toy and a useful agent.
async def agent(task, max_turns=5):
messages = [
{"role": "system", "content": "Use tools."},
{"role": "user", "content": task},
]
for turn in range(max_turns):
msg = await ask_llm(messages)
if not msg.get("tool_calls"):
return msg.get("content", "")
for turn in range(max_turns) — that's the whole loop. Each iteration is one "turn": ask the LLM, check if it wants a tool. If not, return the answer. The max_turns safety limit prevents runaway loops (an LLM that keeps calling tools forever).
When the LLM does ask for a tool, we run it and feed the result back:
messages.append(msg)
for tc in msg["tool_calls"]:
name = tc["function"]["name"]
args = json.loads(tc["function"]["arguments"])
result = tools[name](**args)
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"content": str(result),
})
The tool_call_id links each result to its request. When the LLM asks for two tools at once, this is how it knows which result belongs to which call.
The messages array grows with every turn. That's the agent's memory within a single task.
Message in, LLM thinks, tool runs, result feeds back. Around and around until the LLM decides it has enough to answer.
Try "add 3 and 4, then uppercase hello" — the LLM chains two tool calls across multiple turns. Watch the messages array grow.
Lesson 2 was a single LLM call: ask once, maybe use a tool, done. That can't handle "search for files, then read the interesting ones, then summarize." The loop is what makes agents actually useful.
This is the entire runtime of LangChain's AgentExecutor, OpenAI's Agents SDK, AutoGen — a while loop over messages. Every agent framework wraps this pattern. Now you've seen it bare.
Here's the complete agent in ~30 lines:
tools = {"add": lambda a, b: a + b, "upper": lambda text: text.upper()}
TOOL_DEFS = [
{"type": "function", "function": {"name": "add", "description": "Add two numbers",
"parameters": {"type": "object",
"properties": {"a": {"type": "number"}, "b": {"type": "number"}}}}},
{"type": "function", "function": {"name": "upper", "description": "Uppercase text",
"parameters": {"type": "object",
"properties": {"text": {"type": "string"}}}}},
]
async def ask_llm(messages):
resp = await client.chat.completions.create(
model="gpt-4o-mini", messages=messages, tools=TOOL_DEFS
)
return resp.choices[0].message
async def agent(task, max_turns=5):
messages = [
{"role": "system", "content": "Use tools to answer. Be concise."},
{"role": "user", "content": task},
]
for turn in range(max_turns):
msg = await ask_llm(messages)
if not msg.get("tool_calls"):
return msg.get("content", "")
messages.append(msg)
for tc in msg["tool_calls"]:
name = tc["function"]["name"]
args = json.loads(tc["function"]["arguments"])
result = tools[name](**args)
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"content": str(result),
})
return "Max turns reached"
Run this code yourself at tinyagents.dev. Lesson 3 has a live sandbox — type a task, watch the loop cycle through the graph in real time.
Next up: Lesson 4 — Conversation. The agent loop handles a single task. But what about back-and-forth dialogue? That's where conversation memory comes in.
This is Lesson 3 of A Tour of Agents — a free interactive course that builds an AI agent from scratch. No frameworks. No abstractions. Just the code.