The problem, clearly
A normal LLM can understand:
“Mark my task as complete”
But it cannot actually update your database.
Agentic AI solves this by introducing tool calling:
- The model decides what action to take
- Extracts parameters
- Calls your backend
- Uses the result to respond
What this article covers
- Agentic workflow
- Clean backend architecture
- Tool definition with schemas
- Dynamic execution layer
- Multi-step reasoning
- Handling hallucinations
Basic agentic flow
1. User sends prompt
2. Backend sends prompt + tools to LLM
3. LLM returns tool_call
4. Backend executes function
5. Result is sent back to LLM
6. LLM generates final response
Backend architecture (separation)
Keep AI logic separate from your API.
- API handles validation and database
- AI orchestrates actions
router.post('/', async (req, res) => {
try {
const { title, priority, tags } = req.body;
const task = await Todo.create({
title,
priority,
tags
});
res.status(201).json(task);
} catch (error) {
res.status(400).json({
error: error.message
});
}
});
Defining tools (strict schema)
const tools = [{
type: "function",
function: {
name: "createTodo",
description: "Create a task",
parameters: {
type: "object",
properties: {
title: { type: "string" },
priority: {
type: "string",
enum: ["low", "medium", "high", "urgent"]
},
tags: {
type: "array",
items: { type: "string" }
}
},
required: ["title", "priority"]
}
}
}];
Dynamic tool execution
const availableActions = {
createTodo,
getTodos,
updateTodo,
deleteTodo
};
const toolName = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
if (availableActions[toolName]) {
const result = await availableActions[toolName](args);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: String(result)
});
}
Multi-step reasoning (important)
Updating requires a valid MongoDB _id.
The model must not guess.
CRITICAL RULES:
1. Always call getTodos first
2. Wait for response
3. Extract correct _id
4. Then call update or delete
Handling hallucinations
Local models often generate invalid IDs.
Example:
id: "task_1"
Fix it with strict validation:
export async function updateTodo(params) {
const { id, ...updateData } = params;
const isValidMongoId = /^[0-9a-fA-F]{24}$/.test(id);
if (!id || !isValidMongoId) {
return "ERROR: Invalid ID. Use getTodos first.";
}
const response = await fetch(`${API_BASE_URL}/api/todos/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updateData)
});
const data = await response.json();
return JSON.stringify(data);
}
Common mistakes
- Giving direct database access to AI
- Weak schema definitions
- Not validating inputs
- Ignoring API errors
- Letting the model guess IDs
Final thoughts
Agentic systems are primarily about system design rather than just model capability.
Key principles:
- Strong backend validation
- Clear separation of concerns
- Controlled tool execution
- Strict schemas
- Error-driven correction
Using Ollama keeps the system local, private, and cost-efficient.
This architecture provides a solid foundation for building AI systems that perform real actions instead of only generating responses.
This is an excerpt from Abdulvahab Shaikh's Building an Agentic AI Todo App with Node.js and Local LLMs article. I highly recommend you give it a read!