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

  1. Agentic workflow
  2. Clean backend architecture
  3. Tool definition with schemas
  4. Dynamic execution layer
  5. Multi-step reasoning
  6. 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 ↗.