
Imagine you're building a CLI tool. Your boss says: "Add a smart Q&A feature — users type questions, we call a large language model to answer them."
You roll up your sleeves, npm install openai, and write this:
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://api.deepseek.com',
apiKey: process.env.DEEPSEEK_API_KEY,
});
async function ask(question: string): Promise<string> {
const response = await client.chat.completions.create({
model: 'deepseek-chat',
messages: [{ role: 'user', content: question }],
});
return response.choices[0]?.message?.content ?? '';
}
console.log(await ask('What is the capital of France?'));
// → The capital of France is Paris.It works. Perfect. Your boss praises you and then says: "Great — now add a feature to let users ask 'What time is it right now?'"
Easy enough, you think — just send the question to DeepSeek:
console.log(await ask('What time is it right now?'));
// → Sorry, as an AI assistant, I cannot access real-time information.
// Please check the clock on your device...And there's the problem: the LLM doesn't know "what time it is now."
This isn't a DeepSeek issue — it's a universal limitation of all LLMs. An LLM's knowledge stops at the cutoff date of its training data. Think of it like a person locked inside a library — they can recite every book by heart, but if you ask "is it raining outside?", all they can do is guess.
Your boss continues: "Alright, different feature — let users enter a math expression, and we calculate it for them."
console.log(await ask('What is 128731 × 46291?'));
// → 128731 × 46291 = 5958834521
// (The correct answer is actually 5958833521... the LLM got it wrong)LLMs frequently make errors on complex calculations. They aren't actually "computing" — they're guessing what the next token is likely to be. For large-number multiplication, the chance of guessing wrong is high.
Finally, your boss adds: "Oh, also — users might ask to 'check what's written in README.md, then create a SUMMARY.txt to summarize it.'"
Now it clicks: a plain LLM call can only chat. It can't read files, can't write files, can't do math, can't check the time, can't perform any "action." It's just a text generator.
To summarize, LLMs have three clear shortcomings:
It's like having a brilliant person strapped to a chair — they can think, reason, and give you excellent advice, but their hands are tied. They can't do anything.
So here's the question: what if we untie them and let them reach for tools themselves?
Going back to the "what time is it" problem, you might think: easy — call new Date() first, inject it into the prompt, then send it to the LLM:
const now = new Date().toLocaleString('en-US', { timeZone: 'America/New_York' });
const answer = await ask(`The current time is ${now}. Please tell the user what time it is.`);
// → The current time is Tuesday, July 14, 2026 at 4:30:00 PM.This approach is correct — you prepare information the LLM doesn't know in advance, stuff it into the prompt, and the LLM answers based on it.
But the problem is: who decides "what information needs to be prepared, and when"?
In the example above, it was you — the developer — hardcoding new Date() in your code. But what if the user asks not "what time is it" but "what day of the week is it?" or "what's 12 times 17?" or "what's written in README.md?" Can you predict all possible cases ahead of time?
Obviously not. You need a smarter approach: let the LLM decide what information it needs, and have the program fetch it.
The idea goes like this:
This is the core idea of an Agent — the LLM plays the role of the brain (thinking, deciding), while the program plays the role of hands and feet (executing actions, gathering information). The two alternate.
Let's build the smallest possible version of this idea. We'll solve just one problem: make the LLM correctly calculate 128731 × 46291.
The approach is straightforward:
calculate(expression) functioncalculate('128731 * 46291') and return the result to the LLMWe'll break this into two steps.
Step 1: Prepare the tool function
function calculate(expression: string): string {
// Safety check: only allow digits and operators
if (!/^[\d\s+\-*/().%]+$/.test(expression)) {
return `Illegal expression: ${expression}`;
}
try {
const result = Function(`"use strict"; return (${expression})`)();
return `${expression} = ${result}`;
} catch {
return `Unable to compute: ${expression}`;
}
}
console.log(calculate('128731 * 46291'));
// → 128731 * 46291 = 5958833521Step 2: Teach the LLM to "ask for help"
We need to design a System Prompt that tells the LLM the rules:
const SYSTEM_PROMPT = `You are an intelligent assistant. You can use a calculator tool for math.
When you need to perform a calculation, you must output in the following format:
Action: calculate
Action Input: {"expression": "<expression>"}
When you have a final answer, output in the following format:
Final Answer: <your answer>
Remember: do not do mental math. Use the tool.`;Now let's implement the entire interaction flow — this is essentially the simplest possible ReAct loop:
async function miniAgent(question: string): Promise<string> {
const messages: { role: string; content: string }[] = [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: question },
];
for (let step = 0; step < 5; step++) {
// 1. Call the LLM
const response = await client.chat.completions.create({
model: 'deepseek-chat',
messages: messages as any,
});
const text = response.choices[0]?.message?.content ?? '';
console.log(`[Round ${step + 1}] LLM output:\n${text}\n`);
// 2. Check if a final answer was given
const finalMatch = text.match(/Final Answer:\s*([\s\S]*?)$/);
if (finalMatch) {
return finalMatch[1].trim();
}
// 3. Check if a tool needs to be called
const actionMatch = text.match(/Action:\s*([^\n\r]*)/);
const inputMatch = text.match(/Action Input:\s*([^\n\r]*)/);
if (!actionMatch || !inputMatch) {
// No Action and no Final Answer — return the text as-is
return text;
}
const action = actionMatch[1].trim();
const input = inputMatch[1].trim();
// 4. Execute the corresponding tool
let observation: string;
if (action === 'calculate') {
const { expression } = JSON.parse(input);
observation = calculate(expression);
} else {
observation = `Unknown tool: ${action}`;
}
// 5. Feed the tool result back to the LLM
messages.push({ role: 'assistant', content: text });
messages.push({ role: 'user', content: `Observation: ${observation}` });
console.log(`Observation: ${observation}\n`);
}
return 'Sorry, I thought for too many rounds and still couldn\'t reach an answer.';
}
// Give it a try
console.log(await miniAgent('What is 128731 × 46291?'));Run this code and your terminal will output something like:
[Round 1] LLM output:
Action: calculate
Action Input: {"expression": "128731 * 46291"}
Observation: 128731 * 46291 = 5958833521
[Round 2] LLM output:
Final Answer: 128731 × 46291 = 5,958,833,521It works! The LLM no longer tries to do mental math. Instead, it receives the question, determines "I need the calculator," outputs an Action instruction, we execute it and return the result, and the LLM produces a Final Answer based on that result.
The example above can only do calculation. But let's review what we actually did:
calculate is a ToolIf we swap calculate for readFile, writeFile, getCurrentTime, runShell... the Agent's capabilities expand infinitely.
Think of it like a person:
This is the ReAct (Reasoning + Acting) pattern — proposed by Google DeepMind in 2022, and still the core paradigm behind most LLM Agents today.
The minimal prototype we built above essentially implements the skeleton of a ReAct Agent. In the remaining articles, we'll progressively flesh out this skeleton:
Take the miniAgent code above, fill in the missing imports and configuration, and run it to completion. Watch the terminal as the LLM progresses through each round — observe the full Action → Observation → Final Answer flow.
We currently only have one tool: calculate. Following its format, add a getCurrentTime tool:
action === 'getCurrentTime' and execute itTry asking the Agent: "What time is it right now? How many hours until the end of the day?"
What's wrong with the current implementation? Try these inputs:
Try fixing these issues. You'll start to appreciate just how many edge cases a real-world Agent implementation has to handle.
Keywords: LLM, Agent, ReAct, Tool, Action, Observation, Final Answer, System Prompt, Agent Loop
In the next article, we'll dive into the core of the ReAct pattern: how to design a reliable output format so the LLM's responses can be precisely parsed by code. This includes careful System Prompt design, edge-case handling in regex parsing, and why "telling the LLM to follow rules" is harder than it sounds.
Next article: