Agentic AI with LangChain — building agents that actually do the work

Most "AI" still just talks. This is how I built two agents that act — one that turns plain English into structured Jira stories, one that branches, commits, opens a GitLab MR and reviews it — using LangChain, local models, and one surprisingly small loop.

▶ Watch the video walkthrough

Part 1 — Setup & Installation
Part 2 — AI Agent With 2 Tools

A chatbot answers. An agent acts. That one-word difference is the whole story of agentic AI, and it’s easy to miss because both are powered by the same large language model. The chatbot says “here’s how you’d create that Jira ticket.” The agent creates the ticket.

I’ve been building a series of small, practical agents to make that difference concrete. Two of them are worth walking through, because together they show the entire pattern end to end:

Both run on a local model through Ollama — no cloud LLM bill, nothing leaves the machine — and both are built on the same small LangChain skeleton. Once you see that skeleton, agents stop being magic.

An agent is three things

Strip away the hype and an agent is just three parts working in a loop:

  1. A model that can decide to call functions (tool calling).
  2. Tools — ordinary functions the model is allowed to run.
  3. A loop that lets the model call a tool, see the result, and decide what to do next.

That’s it. Let me show each one from the real code.

1. The model

from langchain_ollama import ChatOllama

llm = ChatOllama(model="qwen3.5:9b", temperature=0)
llm_with_tools = llm.bind_tools(tools)

Two lines of intent. ChatOllama points at a model running locally via Ollama — so it’s free to run and private by default. temperature=0 keeps it deterministic, which you want when it’s taking real actions. bind_tools is the important one: it tells the model what functions exist and how to call them. After this, the model can reply with either text or a structured request to run a tool.

2. The tools

This is where LangChain is genuinely elegant. You write a normal Python function, decorate it with @tool, and the docstring becomes the model’s instruction manual for that capability:

from langchain_core.tools import tool

@tool
def create_jira_story(summary: str, description: str, epic_key: str = "",
                      story_points: int = 3, labels: str = "") -> str:
    """Creates a new Story in Jira from a plain-English description.

    Input:
      summary      — the story title (short, action-oriented)
      description  — user story + numbered acceptance criteria
      epic_key     — parent epic to link to (e.g. 'SCRUM-7')
      story_points — 1=trivial 2=small 3=medium 5=large 8=very large
      labels       — comma-separated (e.g. 'agent-core,backend')

    Output: the new issue key and a direct URL to view it.
    """
    ...

The model never sees the function body — it reads that docstring to decide when to call the tool and what to pass. So the docstring isn’t documentation you write for other humans; it’s a prompt you write for the model. Vague docstring, confused agent. This is the single most underrated skill in building agents.

The tool itself is plain code. The Jira agent talks to the REST API with nothing but Python’s standard urllib — no SDK, no heavy dependency:

def _jira_post(path, payload):
    body = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(url, data=body, headers={...}, method="POST")
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode("utf-8"))

There’s a real-world wrinkle hiding in that tool, and it’s the kind of thing tutorials skip. Jira’s API doesn’t accept a plain-text description — it demands Atlassian Document Format, a structured JSON tree. And the LLM, left alone, writes Markdown (## headings, **bold**) that Jira rejects. So the tool quietly strips the Markdown and rebuilds the text as ADF paragraphs before sending. Agents spend a lot of their code on exactly this: translating between how a model likes to talk and how an API insists on being spoken to.

3. The loop

Here’s the part people imagine is complicated. It isn’t:

def ai_agent_interaction(user_input):
    messages.append(HumanMessage(content=user_input))

    for _ in range(15):                      # safety cap on iterations
        response = llm_with_tools.invoke(messages)
        messages.append(response)

        if not response.tool_calls:          # no tool wanted → we're done
            break

        for tc in response.tool_calls:
            result = tools_by_name[tc["name"]].invoke(tc["args"])
            messages.append(ToolMessage(content=str(result),
                                        tool_call_id=tc["id"]))

    return messages[-1].content

Read it slowly and the whole idea lands. Send the conversation to the model. If it asks for a tool, run the tool, append the result to the conversation, and send the whole thing back. Repeat until the model stops asking for tools and just answers. The range(15) is a guard rail — without a cap, a confused agent can loop forever, which is a real failure mode worth respecting.

Everything else — the eight Jira tools, the ten GitLab tools — plugs into this same loop unchanged.

The trick that makes them feel smart: tools that return instructions

Here’s a pattern I didn’t expect to love as much as I do. A tool doesn’t have to return data. It can return a prompt that tells the agent what to do next.

The Jira agent has a create_multiple_stories tool. You’d assume it loops and creates stories. It doesn’t — it hands the work back to the model:

@tool
def create_multiple_stories(feature_description, epic_key="", project_key="SCRUM"):
    """Creates multiple related stories from one feature description."""
    return (
        f"Feature to decompose: '{feature_description}'\n"
        f"Please break this into 3-5 individual user stories, then call "
        f"create_jira_story for each one. Each should be independently "
        f"deliverable, in 'As a [user] I want [goal] so that [reason]' format."
    )

The tool returns an instruction; the loop feeds it back to the model; the model then calls create_jira_story three or four times on its own. You’ve used a tool to steer the agent’s own reasoning. The GitLab agent does the same with review_merge_request — that tool just gathers the diff and metadata, then returns a structured “write a review covering bugs, security, missing tests, verdict…” instruction, and the model writes the actual review. The tool fetches; the model thinks.

Chaining real actions: the GitLab agent

The Jira agent mostly does one thing per request. The GitLab agent is where “agentic” earns its name, because a single instruction —

“create a branch, commit this file, open an MR for SCRUM-10, and review it”

— makes it run a four-step workflow autonomously:

  1. create_branch → makes feature/SCRUM-10-… off main
  2. add_commit_push → commits the files via the API (it even checks whether each file exists to decide create-vs-update, the API equivalent of git add && commit && push)
  3. open_merge_request → opens the MR, and links the Jira ticket in the description
  4. review_merge_request → pulls the diff, the model writes the review, post_mr_comment posts it back to GitLab

No step is hardcoded into a script. The model decides the order from the system prompt’s described workflow and the results coming back from each tool. That’s the difference between automation and an agent: automation follows a fixed script; an agent is handed tools and a goal and works out the sequence.

And because the MR description carries the Jira key, the two agents form one connected loop: describe a feature → Jira story exists → branch, code, MR, review → linked back to the story. A one-person version of a whole team’s ceremony.

Why local, and why it matters

Both agents run on a model served by Ollama on my own machine. That’s a deliberate choice, not a limitation:

This is the same philosophy behind everything I build: use open tools well, and you get most of the capability at a fraction of the cost and none of the lock-in.

The honest caveats

Agents that act deserve more caution than chatbots that talk, so a few things I’d flag:

None of these are reasons not to build agents. They’re the difference between a demo and something you’d actually trust near your workflow.

The takeaway

Agentic AI isn’t a new kind of model. It’s the same model, given hands (tools) and a loop. Once that clicks, the recipe is small enough to hold in your head: bind some well-documented tools to a model, run the call-tool-feed-back-result loop, and let tools steer the agent when a task needs decomposing.

These two are Part 6 and Part 7 of a build-along series I’m writing. The full, runnable code is on my GitLab, and I walk through it on my YouTube channel — links are on the about page. If you build one of these and it does something useful (or hilariously wrong), I’d love to hear about it.

AILangChainAgentic AI
← All posts