Module 3: From Chatbots to Coding Agents
Tool Use and the ReAct Pattern
Description
The previous capsule explained the agentic loop — the observe → think → act cycle every agent runs. But two questions were left open: how exactly does the LLM "use" tools if it only generates text? And how does it "think" before acting?
This capsule answers both. Tool calling is the technical mechanism that lets the LLM invoke external functions. ReAct (Reason + Act) is the pattern that lets it reason about which tool to use before using it. Together, they're what transforms a language model into a functional agent.
Tool Calling: How a Text Model "Uses" Tools
The fundamental problem
AN LLM ONLY GENERATES TEXT.
It can't:
→ Read files from disk
→ Run commands in the terminal
→ Make HTTP requests
→ Modify databases
→ Interact with any external system
It can only generate the most probable NEXT SEQUENCE OF TOKENS
given the input.
SO... how does it "use tools"?
The solution: structured output as an instruction
The trick is elegantly simple. Instead of having the LLM execute tools, we teach it to request that they be executed.
STEP 1: YOU TELL THE LLM WHICH TOOLS EXIST
"You have access to these tools:
- file_read(path: string): Reads a file and returns its content
- file_write(path: string, content: string): Writes content to a file
- shell_execute(command: string): Runs a command in the terminal
- search(query: string): Searches in the codebase"
STEP 2: THE LLM GENERATES A "TOOL CALL" INSTEAD OF TEXT
Instead of generating:
"I'm going to read the file auth.ts"
It generates something like:
{
"tool": "file_read",
"arguments": {
"path": "src/auth/auth.ts"
}
}
STEP 3: THE SYSTEM (NOT THE LLM) EXECUTES THE TOOL
→ The system receives the tool call
→ It executes file_read("src/auth/auth.ts")
→ It gets the file's content
→ It returns it to the LLM as input
STEP 4: THE LLM RECEIVES THE RESULT AND CONTINUES
→ "Now I have the content of auth.ts"
→ It can reason about it
→ It can request another tool call or give a response
The complete flow visualized
┌──────────┐ ┌──────────┐ ┌──────────┐
│ DEVELOPER│ │ LLM │ │ SYSTEM │
│ │ │ │ │ (runtime)│
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ "Read auth.ts" │ │
│───────────────────▶│ │
│ │ │
│ │ tool_call: │
│ │ file_read( │
│ │ "src/auth.ts") │
│ │───────────────────▶│
│ │ │
│ │ result: │
│ │ "export class..." │
│ │◀───────────────────│
│ │ │
│ │ [reasons about │
│ │ the content] │
│ │ │
│ "Auth.ts contains │ │
│ a class with..." │ │
│◀───────────────────│ │
│ │ │
The crucial part: the LLM doesn't execute anything
THIS IS CRITICAL TO UNDERSTAND:
The LLM generates TEXT that says "I want to run file_read"
The SYSTEM interprets that text and runs the real function
The RESULT is sent back to the LLM as text
The LLM never touches your disk, your terminal, or your network.
Everything goes through an intermediate layer (the runtime/system).
THIS ALSO EXPLAINS:
→ Why agents ask for permissions (the system verifies them)
→ Why there's a permission system (control over what it can do)
→ Why the agent can "hallucinate" tool calls (it generates one that doesn't exist)
→ Why the agent sometimes uses the wrong tool (it predicts badly)
How the LLM "Decides" Which Tool to Use
It's not a conscious decision
The LLM doesn't "decide" to use file_read the way you decide to open a file. What happens is:
GIVEN THE INPUT:
- System prompt with tool definitions
- Conversation history
- The developer's current message
THE LLM PREDICTS:
"What's the most probable next sequence of tokens?"
If the input suggests it needs to read a file:
→ The most probable tokens form: tool_call(file_read, ...)
If the input suggests it should respond:
→ The most probable tokens form: "The file contains..."
Why it works so well
1. TRAINING DATA
→ The models were trained with MILLIONS of examples
of correct tool calls
→ They've "learned" when it's appropriate to read, write, search, etc.
2. INSTRUCTION FOLLOWING
→ The system prompt describes the tools precisely
→ The model follows the instructions about when to use each one
3. PATTERN MATCHING
→ "The developer asks to read a file" → file_read
→ "The developer asks to run tests" → shell_execute
→ "The developer asks to search the code" → search
→ The patterns are clear and the model recognizes them
4. IN-CONTEXT LEARNING
→ If it already used file_read successfully in the conversation,
it learns that it works and uses it again
→ The context reinforces the correct behavior
When tool selection fails
THE LLM CHOOSES WRONG when:
1. AMBIGUITY
→ "Check the file" → file_read or search?
→ If the prompt is ambiguous, the tool can be incorrect
2. UNKNOWN TOOL
→ If the task requires something that isn't in the available tools
→ The LLM can try to use the most similar tool (incorrectly)
→ Or invent a tool that doesn't exist
3. FULL CONTEXT WINDOW
→ The tool definitions can fall out of the context
→ The LLM "forgets" it has certain tools available
4. HALLUCINATION
→ The LLM generates a tool call with incorrect arguments
→ file_read("file_that_does_not_exist.ts")
→ The system fails, the agent has to recover
The ReAct Pattern: Reason + Act
What ReAct is
ReAct comes from the paper "ReAct: Synergizing Reasoning and Acting in Language Models" (Yao et al., 2022). It's the pattern that combines explicit reasoning with concrete actions.
WITHOUT ReAct (just Act):
→ Input → Tool call → Result → Tool call → ...
→ The LLM acts without explaining why
→ A sequence of actions with no visible reasoning
WITH ReAct (Reason + Act):
→ Input → "I need to find the auth file"
→ Tool call(search) → "I found auth.ts. Now I read it"
→ Tool call(file_read) → "The regex on line 45 is incorrect"
→ Tool call(file_write) → "Fix applied. I verify with tests"
→ Tool call(shell_execute) → "Tests pass. Task complete."
→ The LLM REASONS between each action
→ Each action is justified by the reasoning
→ The process is traceable and debuggable
The ReAct format
THOUGHT: [reasoning about what to do]
ACTION: [tool call]
OBSERVATION: [result of the tool call]
THOUGHT: [reasoning about the result]
ACTION: [next tool call]
OBSERVATION: [result]
... (repeat until finished)
THOUGHT: [final reasoning]
ANSWER: [answer to the developer]
Real example with ReAct
INPUT: "Why does the UserService test fail?"
THOUGHT: I need to find the UserService test to see
what specific error it has. I'm going to search for related
test files.
ACTION: search(query="UserService test", file_type="test")
OBSERVATION: Found: src/__tests__/UserService.test.ts
THOUGHT: I found the test file. I need to read it to
understand which tests there are and which one fails. I should
also run the tests to see the exact error.
ACTION: shell_execute("npm test -- --testPathPattern=UserService")
OBSERVATION:
FAIL src/__tests__/UserService.test.ts
✕ should return user profile (TypeError: Cannot read
property 'email' of undefined)
✓ should create user
✓ should delete user
THOUGHT: The "should return user profile" test fails because
it tries to access .email of something that is undefined. I need
to read the test to see which variable is undefined — probably
the result of a function that doesn't return what's expected.
ACTION: file_read("src/__tests__/UserService.test.ts")
OBSERVATION: [file content, line 34:
expect(result.user.email).toBe("test@example.com")]
THOUGHT: The test expects result.user.email, but result.user
is undefined. This means UserService doesn't return an
object with .user. I need to see what UserService returns.
ACTION: file_read("src/services/UserService.ts")
OBSERVATION: [content, line 45: return { data: userData }]
THOUGHT: The service returns { data: userData } but the test
expects { user: userData }. The key is "data" instead of "user".
I should verify what other files use to know which is the
correct convention.
ACTION: search(query=".user.", path="src/")
OBSERVATION: 3 files use result.user: routes/profile.ts,
routes/admin.ts, middleware/auth.ts
THOUGHT: The consumers of the service expect .user, not .data.
The bug is in the service. Someone probably renamed the
key incorrectly. I'm going to fix it.
ACTION: file_write("src/services/UserService.ts",
line 45: "return { user: userData }")
OBSERVATION: File modified.
THOUGHT: Fix applied. I need to verify the tests pass.
ACTION: shell_execute("npm test -- --testPathPattern=UserService")
OBSERVATION: PASS (3/3 tests pass)
ANSWER: The test was failing because UserService.ts returned
{ data: userData } instead of { user: userData } (line 45).
The consumers of the service (routes and middleware) expect
.user. I fixed the key and all 3 tests pass.
Why ReAct is better than just Act
JUST ACT:
search → file_read → file_read → search → file_write → shell
→ Why did it search? I don't know
→ What did it conclude from the file_read? I don't know
→ Was the file_write correct? Hard to evaluate
→ I can't intervene because I don't know what it's thinking
ReAct (REASON + ACT):
"I need to find the test" → search
→ "I found it, I need to see the error" → shell
→ "The error is .email of undefined" → file_read
→ "The test expects .user but the service gives .data" → search
→ "The convention is .user, I fix the service" → file_write
→ "I verify" → shell → "Tests pass"
→ I can see the reasoning at each step
→ I can intervene if the reasoning is incorrect
→ I can evaluate whether the conclusion is correct
→ The process is transparent and auditable
Chain-of-Thought Reasoning for Code
What Chain-of-Thought (CoT) is
Chain-of-thought is a technique where the LLM reasons step by step before giving a final answer. Instead of jumping to the result, it breaks the problem into steps.
WITHOUT Chain-of-Thought:
"The bug is in UserService.ts, line 45. I changed 'data' to 'user'."
→ Correct, but how did it reach that conclusion?
→ Did it consider other possibilities?
WITH Chain-of-Thought:
"1. The error is 'Cannot read property email of undefined'
2. This means something.email is called on an undefined value
3. In the test, line 34: result.user.email
4. If result.user is undefined, the service doesn't return .user
5. In UserService.ts, line 45: return { data: userData }
6. The key is 'data', but the test expects 'user'
7. I verify: 3 files use .user → the convention is .user
8. Conclusion: the service has the wrong key"
→ Each step is verifiable
→ If step 7 showed that others use .data, the conclusion would be different
→ The reasoning is transparent
CoT applied to coding agents
Modern coding agents use CoT in two ways:
1. VISIBLE CoT ("thinking" or "reasoning")
→ Some agents show their reasoning to the developer
→ Claude Code: thinking visible in the interface
→ Cursor: can show reasoning in the panel
→ The developer can see WHAT the agent is thinking
2. INTERNAL CoT (inside the prompt)
→ The system prompt instructs the agent to reason step by step
→ "Before acting, think step by step about what to do"
→ The reasoning happens but may not be visible
→ It improves the quality of the agent's decisions
How CoT improves tool calling
WITHOUT CoT:
Developer: "Add email validation"
Agent: tool_call(file_write, "src/utils/validate.ts", [...new code])
→ Where should the validation go? It didn't reason
→ Is there existing validation? It didn't verify
→ What pattern does the codebase follow? It didn't investigate
WITH CoT:
Developer: "Add email validation"
Agent: [thinks]
"1. Is there already validation in the project? I search
2. Where is email validation used? I investigate it
3. Is there a validation library installed? I verify
4. Based on the above, I implement following the existing pattern"
→ tool_call(search, "validate") → finds src/utils/validators.ts
→ tool_call(file_read, "src/utils/validators.ts") → sees the pattern
→ tool_call(file_write, "src/utils/validators.ts", [...adds to the existing file])
WITH CoT: it extends the existing pattern
WITHOUT CoT: it possibly creates a new duplicate file
ReAct + CoT = The Agent's Reasoning
ReAct and CoT complement each other:
CoT: → Step-by-step reasoning (thinking)
ReAct: → Reasoning + Action (thinking AND doing)
CoT alone:
→ "The bug is probably in X because Y and Z"
→ It reasons but doesn't act to verify
ReAct alone:
→ search → file_read → file_write
→ It acts but with no explicit reasoning
ReAct + CoT:
→ "The bug is probably in X (reasoning)"
→ search(X) → "Confirmed, now I verify Y (reasoning)"
→ file_read(Y) → "Yes, the problem is Z. I fix it (reasoning)"
→ file_write(fix) → "I verify (reasoning)"
→ shell_execute(tests) → "Tests pass. Done (reasoning)"
EACH ACTION IS INFORMED BY REASONING
EACH REASONING IS VERIFIED BY ACTION
Tool Calling in Practice: What It Looks Like
What the developer sees
In Claude Code (terminal):
> Fix the UserService test failure
🔍 Searching for UserService test files...
📄 Reading src/__tests__/UserService.test.ts
💻 Running tests to see the exact error...
📄 Reading src/services/UserService.ts
🔍 Checking how other files use UserService...
✏️ Fixing the return key in UserService.ts
💻 Running tests... All pass ✅
The bug was in UserService.ts line 45: it returned
{ data: userData } instead of { user: userData }.
Fixed and all 3 tests pass.
What happens "under the hood"
LLM Input #1:
system: "You are a coding agent with access to: file_read,
file_write, shell_execute, search..."
user: "Fix the UserService test failure"
LLM Output #1:
thinking: "I need to find the test file first"
tool_call: search("UserService", file_type="test")
System executes: search("UserService", file_type="test")
System returns: "src/__tests__/UserService.test.ts"
LLM Input #2:
[everything above +]
tool_result: "src/__tests__/UserService.test.ts"
LLM Output #2:
thinking: "Found it. Let me read it"
tool_call: file_read("src/__tests__/UserService.test.ts")
[... repeats until it generates a final response ...]
The Typical Tools of a Coding Agent
Module 04 goes deeper into each tool, but here's the overview:
READING TOOLS (observe):
→ file_read: Read a file
→ search: Search in the codebase (by text, regex, or semantics)
→ list_directory: See what files are in a directory
WRITING TOOLS (act):
→ file_write: Create or modify a file
→ file_delete: Delete a file
EXECUTION TOOLS (verify):
→ shell_execute: Run any command (tests, builds, etc.)
→ Some agents limit which commands can be run
INFORMATION TOOLS (external context):
→ web_search: Search for information on the internet
→ web_fetch: Get the content of a URL
VERSION CONTROL TOOLS:
→ git_operations: Commit, diff, branch, etc.
→ Some agents handle git as a separate tool
→ Others do it via shell_execute("git ...")
How the tools are defined for the LLM
TYPICAL DEFINITION (simplified):
{
"name": "file_read",
"description": "Read a file from the filesystem",
"parameters": {
"path": {
"type": "string",
"description": "The path to the file to read"
}
},
"returns": "The content of the file as a string"
}
THE LLM RECEIVES THESE DEFINITIONS in the system prompt.
It knows which tools exist, which parameters they expect,
and what they return.
THIS IS EXACTLY what you'll implement in Module 07:
→ Define tools for your mini-agent
→ Pass them to the LLM as definitions
→ Execute the tool calls the LLM generates
→ Return the results to the LLM
Practical Exercise
Exercise 1: Observe the ReAct pattern
Give your coding agent a moderate task and observe the ReAct pattern:
SUGGESTED TASK:
"Find all the files that import [module X]
and tell me how many there are"
OBSERVE:
→ Does the agent reason before acting? (THINK visible)
→ Which tool does it choose first? Is it the correct one?
→ Does it adjust its approach based on the results?
→ How many Reason + Act iterations does it do?
DOCUMENT the pattern:
THINK 1: _______________________________________________
ACT 1: _______________________________________________
THINK 2: _______________________________________________
ACT 2: _______________________________________________
[...]
See guided reflection
Example of a ReAct pattern observed with the task "Find all the files that import UserService and tell me how many there are":
THINK 1: "I need to search for files that contain imports of UserService"
ACT 1: search("import.*UserService", file_type="ts")
THINK 2: "I found 5 results. I verify they're real imports, not usages"
ACT 2: file_read("src/routes/users.ts") → confirms import
THINK 3: "They're all valid imports. I can report the result"
ACT 3: Final response: "5 files import UserService"
What to observe:
- Does it reason before acting? A good agent explains why it chooses each tool before using it. If you only see actions with no reasoning, the agent is in "just Act" mode (less traceable).
- Does it adjust its approach? If the first search yields no results, does it change the query? Does it try another tool? That indicates adaptive reasoning.
- How many iterations? For this task, 2-4 iterations is reasonable. More than 6 suggests the agent isn't being efficient with its tool selection.
Exercise 2: Improve tool selection with your prompt
Compare these two prompts and observe how the tools the agent uses change:
VAGUE PROMPT:
"Check the project"
→ Which tools does it use? ________________
→ Are they the correct ones? _____________
PRECISE PROMPT:
"Read src/services/UserService.ts and tell me what methods
it has and what each one returns"
→ Which tools does it use? ________________
→ Are they the correct ones? _____________
Did the precise prompt produce a better tool selection?
See solution
Vague prompt: "Check the project"
- Likely tools:
list_directory(root),file_read(README, package.json),search(general structure), possibly morefile_readof random files. - Problem: the agent doesn't know what to look for, so it explores without direction. It can do 10+ iterations reading files without a clear goal. The tool selection is unpredictable.
Precise prompt: "Read src/services/UserService.ts and tell me what methods it has and what each one returns"
- Likely tools:
file_read("src/services/UserService.ts")→ final response. - Total: 1-2 iterations. The tool selection is obvious and direct.
Conclusion: Yes, the precise prompt produces a better tool selection because it reduces ambiguity. When the agent knows exactly what it needs, it chooses the correct tool from the first iteration. When the prompt is vague, the agent "guesses" which tools to use and often chooses suboptimally.
Practical rule: A good prompt makes the correct tool obvious. If you can predict which tool it should use, it's a good prompt.
Exercise 3: Design a tool
Imagine you're creating a mini-agent (Module 07). Design a tool:
NAME: _______________________________________________
DESCRIPTION (for the LLM):
_______________________________________________
PARAMETERS:
- name: ____________ type: ____________
- name: ____________ type: ____________
RETURNS:
_______________________________________________
EXAMPLE OF USE:
Input: _____________________________________________
Output: ____________________________________________
Would the LLM have enough information to use
this tool correctly? □ Yes □ No → what's missing?
See solution
Example of a well-designed tool:
NAME: run_tests
DESCRIPTION (for the LLM):
"Runs the project's tests. It can run all the tests
or filter by a filename pattern. It returns the
result with passed tests, failed tests, and detailed errors."
PARAMETERS:
- name: pattern type: string (optional)
- name: verbose type: boolean (optional, default: false)
RETURNS:
"An object with: total (int), passed (int), failed (int),
errors (array of strings with detail of each failure)"
EXAMPLE OF USE:
Input: run_tests(pattern="UserService", verbose=true)
Output: { total: 5, passed: 4, failed: 1,
errors: ["test 'should validate email':
expected true, got false at line 23"] }
Checklist for evaluating your design:
- Does the description explain when to use the tool? (Yes: "Runs the project's tests")
- Do the parameters have clear types? (Yes: string, boolean)
- Does the return describe the structure? (Yes: an object with specific fields)
- Does the example show real input AND output? (Yes)
- Could the LLM use the tool without more information? → Yes, it has everything it needs.
A poorly designed tool would have a vague description ("does things with tests"), parameters without types, or an unspecified return. The LLM needs precise information to generate correct tool calls.
Common Mistakes
| Mistake | Reality |
|---|---|
| "The LLM executes the tools" | The LLM only generates the request; the system executes |
| "The agent always chooses the correct tool" | It chooses by probability — sometimes it chooses wrong |
| "ReAct is something new from 2025" | The paper is from 2022; the application to coding agents is recent |
| "More tools = better agent" | More tools can confuse the LLM; quality > quantity |
| "The agent's reasoning is infallible" | It's next-token prediction — it can reason incorrectly |
Summary
TOOL CALLING:
→ The LLM generates text that REQUESTS running a tool
→ The system (runtime) executes the real tool
→ The result is returned to the LLM as text
→ The LLM never interacts directly with the world
ReAct PATTERN (Reason + Act):
→ THINK: reason about what to do and why
→ ACT: execute a tool call
→ OBSERVE: receive the result
→ Each action is justified by reasoning
→ The process is traceable and auditable
CHAIN-OF-THOUGHT:
→ Step-by-step reasoning
→ Improves the quality of the decisions
→ Makes the process transparent
→ ReAct + CoT = reasoning verified by action
FOR MODULE 07:
→ You'll define tools for your mini-agent
→ You'll implement the loop that executes tool calls
→ You'll see ReAct + CoT in action from the inside
Next capsule: 05 - Coding agents vs chatbots — the direct comparison that clarifies when to use each one.
Resources
- Yao et al.: ReAct Paper (2022) — The original ReAct pattern paper
- Wei et al.: Chain-of-Thought Prompting (2022) — The Chain-of-Thought paper
- OpenAI: Function Calling Guide — Practical implementation of tool calling
- Anthropic: Tool Use Documentation — Tool calling in Claude
- LangChain: Tools and Agents — A framework for implementing tools