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.
Originally inspired by or excerpted from the source article ↗.
