Home / Articles / Giving a LangGraph Support Agent Its First Tool: Keyword Docs Search

This article is published in English.

Giving a LangGraph Support Agent Its First Tool: Keyword Docs Search

How a LangGraph support agent gets one search tool over Markdown docs: why it skips RAG, the tool and implementation split, and how state carries evidence to the reply.

975 words

The LEPA support agent's graph can already branch on how clear a question is:

vague question  → ask for clarification
clear question  → continue

But "continue" is hollow if the agent has nothing to continue with. The next addition gives it something concrete to do: search the school's support documentation and base each answer on what turns up.

Why bother with tools

A language model can sound confident about anything, and a support agent for LEPA mustn't invent school procedures. Tools are the answer. A tool is an action the agent, or one graph node, can take outside the model's frozen weights. Picture a receptionist who only talks versus one who can open the staff handbook; the second is the useful one. In an AI workflow the pattern looks like this:

User question
     ↓
Decide what is needed
     ↓
Use a tool (search docs, call an API, …)
     ↓
Read the tool result
     ↓
Answer with evidence

The same pattern showed up in earlier projects. TAM used plain Python functions as tools, and DevAssist used MCP to expose them to clients. In this project the tool lives inside the LangGraph app and writes into state. Same idea, different plumbing.

The single tool

There's exactly one:

search_knowledge(query, category?)

It searches Markdown files in a knowledge/ folder:

knowledge/
  authentication.md
  students.md
  attendance.md
  grades.md
  reports.md

Each file holds realistic LEPA support content: login problems, enrollment, attendance sheets, grade publishing and reports.

What it deliberately isn't

No vector database, no embeddings and no full RAG architecture. The search is kept simple on purpose:

  • Docs are split at ## headings.
  • Chunks are scored by keyword overlap.
  • Up to 4 snippets come back, truncated for length.
  • When classification already knows the topic, the matching {category}.md file is preferred.

The reason is that this project is about LangGraph and not about retrieval research. The README says so directly: the lesson is tool plus graph, and retrieval quality isn't the point.

Two layers in the code

The logic exists in two forms. search_knowledge_impl(...) is ordinary Python returning a list of snippets, while @tool search_knowledge(...) wraps that logic for LangChain. The graph's knowledge node skips the wrapper and calls the implementation:

def run_knowledge(state: SupportState) -> dict:
    query = last_user_text(state)
    category = state.get("issue_category") or "unknown"
    snippets = search_knowledge_impl(query, category=category)
    return {"retrieved_documents": snippets}

That choice has a purpose. Retrieval stays visible, tests run offline, and the @tool wrapper remains available for later experiments with LLM tool calling. The graph therefore uses a tool-shaped capability even though the LLM isn't the one picking the tool call.

How the flow changes

Before tools, the continue branch was a stub. With the tool in place it becomes this:

classify → continue
              ↓
          knowledge   (search_knowledge_impl → retrieved_documents)
              ↓
          support     (turn snippets into final_answer)
              ↓
             END

A LEPA example

Take a user who asks why grades aren't showing:

User: "Why aren't grades showing?"
        ↓
classify → grades
        ↓
knowledge searches grades.md
        ↓
snippets mention Draft vs Publish
        ↓
support answers using those snippets

That's the difference between guessing why grades are missing and answering from a handbook page saying guardians only see published grades.

Support depends on what the tool returns

The support node doesn't create a second knowledge base. It reads retrieved_documents from state. With no documents, it asks the user to name the LEPA area. With documents, it builds an answer from them. If an API key is configured, it may also ask an LLM to rewrite the same snippets more cleanly, and it still falls back to a template when the LLM fails.

The boundary is simple: the tool supplies evidence, the support step formats the reply and the graph decides when the tool runs. That separation helped with debugging. For any wrong answer you could ask whether the routing, the retrieval or the wording was at fault.

A lesson carried over from MCP work

An earlier MCP project taught that a tool should be shaped for the model reading its output, and that holds even for keyword search. Snippets from search_knowledge carry a label such as [grades.md] and then truncated section text, which shows the next step, or a future LLM, where the evidence came from. Snippet size is capped too. Flooding the next step with an entire handbook would recreate the too-much-context problem hit while designing DevAssist's search_code. The project is different but the design pressure is the same.

What one tool taught

  1. A graph without tools can route but can't ground its answers.
  2. One boring tool is enough to change the product.
  3. Calling a tool from a node is a valid way to learn before moving to full LLM tool calling.
  4. For a LangGraph tutorial, "not RAG yet" is a feature and not a failure.
  5. State works as the bus: tool output becomes retrieved_documents, and support reads it.

In short

With a tool, the LEPA agent stops being a flowchart and can pull up actual school procedures. Concretely that means search_knowledge and search_knowledge_impl, a basic Markdown search invoked from the knowledge node that passes results to the support node through state. The harder question comes next: on the following message, does the agent remember the conversation? That's where checkpoints, thread_id and short-term conversation state come in.