ReAct Prompting
By the Syncfusion DevTutorials team · Last updated
Let the model reason and use tools: the Thought, Action, Observation loop, parsing actions in code, building a safe calculator and search tool, step limits, and tool safety.
A model can't do exact maths reliably, and it can't look things up in your company's documents. But JavaScript can do both. What if the model could ask for a tool when it needs one, read the result, and then keep going?
This is called ReAct, which is short for Reason + Act. The model writes a thought, chooses a tool, and stops. Your code runs the tool and gives back the result. The model reads the result and decides what to do next. This repeats until the model can answer.
The main idea: the model does the thinking, and your code does the work. Facts and numbers come from real tools, not from the model's memory.
A Real-World Example
Think about a detective solving a case.
- Think: "The butler says he was in the garden. I should check that."
- Act: ask the gardener.
- Observe: "The gardener saw nobody in the garden."
- Think again: "So the butler lied. Next, I'll check the kitchen."
The detective doesn't guess the whole answer at once. Each step uses real evidence from the step before. A ReAct loop works the same way, but it uses tools instead of witnesses.
What You Will Learn
In this lesson, you will learn:
- What ReAct is, and why tools help.
- The Thought, Action, Observation, Answer format.
- How code reads the action the model asks for.
- How to build safe tools: a calculator and a document search.
- How the full loop runs, with a limit on the number of steps.
- The safety rules for letting a model use tools.
The Prompt sandbox has a ReAct tab. It runs the loop with a real model and two real JavaScript tools: a calculator and a document search. The trace shows every thought, action, and observation.
Reason and Act
The ReAct loop. The model writes a thought and an action, and then stops. Code runs the tool and adds the observation. The loop repeats until the model writes an answer.
Without tools, a model has two weak spots:
- Exact work. Maths, dates, and counting are all guesses, made one token at a time.
- Facts it has never seen. Your prices, your documents, and today's data.
ReAct fixes both problems by splitting the work:
| Who | What they do |
|---|---|
| The model | Decides what to do next, and writes the final answer |
| Your code | Runs the tool, and gives back the real result |
The ReAct Format
The system prompt lists the tools, and gives the exact format for each turn. Each turn has one of two shapes:
Thought: what I need to do next
Action: tool[input]
or, at the end:
Thought: why I am sure
Answer: the final answer
After an action, the model stops. Your code adds an Observation: line with the tool's result. The model must never write an observation itself, because then the "result" would just be a guess.
Try it with the real model. The question needs both tools: the price comes from the search tool, and the total comes from the calculator.
Watch the trace. In a good run, the model searches for the price, calculates 20 * 12 * 7, and answers $1,680. If the model skips a step or guesses, that is a common mistake for a small model. Try Qwen2.5 1.5B from the model list.
Read the Action in Code
Your code needs to find the action in the model's reply. A regular expression pulls out the tool name and its input. Your code throws away anything the model wrote after Observation:.
Parse one ReAct turn
function parseTurn(text) {
const own = text.split(/\n?\s*Observation:/i)[0];
const action = own.match(/Action:\s*([a-z_]+)\s*\[([\s\S]*?)\]/i);
const answer = own.match(/Answer:\s*([\s\S]+)$/i);
if (action) return { type: "action", tool: action[1].toLowerCase(), input: action[2].trim() };
if (answer) return { type: "answer", text: answer[1].trim() };
return { type: "none" };
}
console.log(JSON.stringify(parseTurn("Thought: I need the total.\nAction: calculator[20 * 12 * 7]")));
console.log(JSON.stringify(parseTurn("Thought: Done.\nAnswer: $1,680 per year")));
console.log(JSON.stringify(parseTurn("Action: calculator[2 + 2]\nObservation: 5")));
console.log(JSON.stringify(parseTurn("I think it is about $1,600.")));
Expected output:
{"type":"action","tool":"calculator","input":"20 * 12 * 7"}
{"type":"answer","text":"$1,680 per year"}
{"type":"action","tool":"calculator","input":"2 + 2"}
{"type":"none"}
- In the third reply, the model made up its own observation, "5". The code drops it. Your code will run the calculator and get the real answer, which is 4.
- The fourth reply did not follow the format. The loop should stop and report this, not guess.
Build Safe Tools
A tool is just a function. It takes the model's input as a string, and gives back a string. The most important rule is this: you can't trust the model's input. Never pass it to eval() or run it as code.
This calculator reads the maths itself, and it only allows numbers and + - * / ( ):
A calculator that never runs code
function calculate(expr) {
const src = expr.replace(/,/g, "");
if (!/^[\d+\-*/().\s]+$/.test(src)) return "Error: numbers and + - * / ( ) only";
const tokens = src.match(/\d+(\.\d+)?|[+\-*/()]/g);
let i = 0;
function num() {
const t = tokens[i++];
if (t === "(") { const v = sum(); i++; return v; }
if (t === "-") return -num();
return Number(t);
}
function product() {
let v = num();
while (tokens[i] === "*" || tokens[i] === "/") v = tokens[i++] === "*" ? v * num() : v / num();
return v;
}
function sum() {
let v = product();
while (tokens[i] === "+" || tokens[i] === "-") v = tokens[i++] === "+" ? v + product() : v - product();
return v;
}
return String(sum());
}
console.log(calculate("20 * 12 * 7"));
console.log(calculate("(1,200 + 300) / 4"));
console.log(calculate("fetch('https://example.com')"));
Expected output:
1680
375
Error: numbers and + - * / ( ) only
The third input is code, not maths. The calculator refuses it before it does anything.
The search tool finds documents that share words with the search text. This is the same idea as choosing context in the Context and Grounding lesson.
A simple document search tool
const docs = [
"Brightgrid Pro costs $20 per month per developer.",
"The free plan exports up to 5,000 rows.",
"Refunds take 5 working days.",
];
function search(query) {
const words = query.toLowerCase().match(/[a-z0-9]+/g).filter((w) => w.length > 2);
const hits = docs
.map((d) => ({ d, score: words.filter((w) => d.toLowerCase().includes(w)).length }))
.filter((x) => x.score > 0)
.sort((a, b) => b.score - a.score);
return hits.length ? hits.slice(0, 2).map((x) => x.d).join(" | ") : "No matching document.";
}
console.log(search("Pro price per developer"));
console.log(search("mobile app support"));
Expected output:
Brightgrid Pro costs $20 per month per developer.
No matching document.
"No matching document." is an important result. It lets the model say that it doesn't know, instead of making up an answer.
Easy Way to Remember: in ReAct, the model asks, and the code answers. You should be able to trace every number and fact in the final answer back to an observation.
Route Actions to Tools
The model chooses a tool by its name. Your code keeps a list of the tools it allows, and answers any other request with a clear error. The error goes back to the model as an observation, so the model can correct itself.
Run only the tools you allow
const tools = {
calculator: (input) => (/^[\d+\-*/().\s]+$/.test(input) ? "ok: maths accepted" : "Error: numbers and + - * / ( ) only"),
search: (input) => "ok: searched for " + JSON.stringify(input),
};
function runTool(name, input) {
if (!(name in tools)) return 'Error: unknown tool "' + name + '". Use: ' + Object.keys(tools).join(", ");
if (input.length > 200) return "Error: input is too long";
return tools[name](input);
}
console.log(runTool("calculator", "20 * 12"));
console.log(runTool("search", "refund days"));
console.log(runTool("send_email", "boss@example.com"));
console.log(runTool("calculator", "rm -rf /"));
Expected output:
ok: maths accepted
ok: searched for "refund days"
Error: unknown tool "send_email". Use: calculator, search
Error: numbers and + - * / ( ) only
The model asked for send_email, which is a tool that doesn't exist. So it gets an error, and nothing happens. A short list of allowed tools is one of the strongest ways to keep things safe.
The Full Loop
Here is the whole loop. So that it can run without a model, the box uses two sample replies written for this example instead of real calls to a model. The tools are real.
Run a ReAct loop with real tools
const sampleReplies = [
"Thought: I need the yearly cost for 7 developers at $20 a month.\nAction: calculator[20 * 12 * 7]",
"Thought: The calculator gave the total.\nAnswer: $1,680 per year",
];
function calculator(expr) {
return /^[\d+\-*/().\s]+$/.test(expr) ? String(Function("return " + expr)()) : "Error";
}
const transcript = [];
for (let step = 0; step < 5; step++) {
const reply = sampleReplies[step];
if (!reply) { transcript.push("Stopped: no reply"); break; }
const action = reply.match(/Action:\s*(\w+)\[(.*?)\]/);
const answer = reply.match(/Answer:\s*(.+)/);
transcript.push(reply.split("\n")[0]);
if (answer) { transcript.push("Answer: " + answer[1]); break; }
if (action && action[1] === "calculator") {
transcript.push("Action: " + action[1] + "[" + action[2] + "]");
transcript.push("Observation: " + calculator(action[2]));
}
}
console.log(transcript.join("\n"));
Expected output:
Thought: I need the yearly cost for 7 developers at $20 a month.
Action: calculator[20 * 12 * 7]
Observation: 1680
Thought: The calculator gave the total.
Answer: $1,680 per year
To keep this box short, this calculator uses Function(), but only after the strict check on the characters. For real apps, the safe calculator shown earlier is the better choice.
The loop has a step limit of 5. Without a limit, a model that never writes an answer would keep looping forever. The sandbox has a Max steps slider for the same reason.
ReAct or Chain of Thought?
ReAct and chain of thought both write out their thinking before the answer. The difference is where the facts come from.
| Chain of thought | ReAct | |
|---|---|---|
| Thinking | Written step by step | Written step by step |
| Where the facts and numbers come from | The model's own memory | Real tools, like search, a calculator, or files |
| Can it check things? | No | Yes, with each action |
| Risk of made-up answers | Higher for facts the model doesn't know | Lower, because the answers are based on observations |
| What you need | Only a prompt | A system that can run tools, and code |
Use chain of thought when the prompt already gives the model everything it needs. Use ReAct when the answer depends on information the model doesn't have, or on exact work like maths.
Safety Rules for Tools
Tools give a model real power. Be careful with that power:
- Tools that only read are the safest. Searching, looking things up, and calculating are fine. Sending emails, making payments, or deleting things are not fine, unless a person approves them.
- Never run text from the model as code. Read and check the input yourself. Allow only what the tool needs.
- Limit the number of steps, and the cost of each run.
- Show the trace to the user, so they can see where an answer came from.
- Don't trust search results either. A document could contain "ignore your rules". Keep observations clearly marked as data.
Never give a model a tool that can spend money, delete data, or send messages for someone, unless a person approves each action. Text in a document or a user's message can trick a model, and a tool turns that trick into a real action.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| The model writes its own observation | "It knows the result." | Cut the reply at Observation:, and run the tool. | Made-up results look like real results from a tool. |
| Using eval on tool input | "It's just maths." | Check the input yourself, and allow only safe characters. | Text from the model can run as code in your app. |
| No step limit | "The model will answer in the end." | Stop after a fixed number of steps. | The loop runs forever and wastes time. |
| Tool errors with no message | "Give back nothing if it fails." | Give back a clear error message as the observation. | The model guesses instead of fixing its input. |
| Tools that change things, with no approval | "The model only does what I ask." | Ask a person before any action that has real effects. | A model that was tricked deletes data or sends messages. |
| Hiding the trace | "Users only need the answer." | Show the thoughts, actions, and observations. | Nobody can check where the answer came from. |
Exercises
In each exercise, the last line should give back a value. Press Check to compare your value with the expected answer.
What to Remember
- ReAct means Reason + Act: the model thinks, calls a tool, reads the result, and repeats.
- Each turn is either Thought + Action or Thought + Answer.
- Your code stops the model at Observation:, runs the tool, and adds the real result.
- Don't trust the text that tools receive from the model. Check it strictly, and never use eval().
- Always set a step limit, and show the trace.
- Tools that only read are safe. Anything that has real effects needs a person's approval.
Next: Meta Prompting, where you use the model to write, review, and improve your prompts.