MindzKonnected – site header (partial)

Author: Shikhar

  • From ReAct to Deep Agent: When One Routine Is Not Enough

    Our chatbot had one way of thinking, and it used that one way for everything. This post is about the deep agent architecture we built to replace it.

    Ask it “hi” and it would stop, reason about what it needed to know, decide to search, run the search, read the results, and reason again about whether to search once more. Ask it to compare five vendors on price, latency, and compliance and it would do exactly the same thing. Same loop. Same tools. Same number of sources pulled back. The only difference between those two requests was how many times the loop spun before the model decided it had enough.

    We shipped it anyway, because it worked. People used it. But as the questions got harder, we kept hitting the same problem: slow on the easy things, shallow on the hard ones, and equally confident either way.

    The problem was never that the model was not smart enough. The problem was that our architecture handed it exactly one routine and no way to choose a different one. A greeting and a research assignment went through identical machinery, and the machinery could not tell them apart.

    This is the story of replacing that single routine with a deep agent: an architecture that first decides how much effort a question deserves, then brings only the machinery that effort actually requires.

    The original design: one ReAct loop for everything

    Our chatbot brain ran a ReAct loop. ReAct is short for Reason and Act. The model reasons about what it needs, takes an action (usually a tool call), observes what comes back, and reasons again. It repeats until it decides it can answer.

    Think of hiring a research assistant. You ask a question. They pause and think about what they need to know. They go look something up. They read it. They ask themselves whether they have enough. They decide, and they repeat.

    A greeting, a quick lookup and a five-way comparison all passing through the same Reason, Act, Observe loop at identical cost

    ReAct became the default for good reasons. It is a few dozen lines of code. It handles open-ended questions without anyone having to anticipate them in advance. And it degrades gracefully, because a model that does not need a tool simply answers.

    But a single ReAct loop has four structural properties that only reveal themselves as problems once real traffic hits it.

    • It has one routine. Nothing in the loop ever asks how hard the incoming question is. Every request enters the same machinery at the same depth.
    • It trusts its tools. Whatever a tool returns goes straight into context as fact. There is no gate between retrieval and reasoning.
    • It has one shared context. Every observation from every step piles into the same message history, and the final answer is written from that same pile.
    • It has no plan. The model may reason, but nothing obliges it to, and nothing tracks which parts of a question are still unanswered.

    What a deep agent actually is

    A deep agent is not a smarter model. It is a harness built around the same tool-calling loop, with scaffolding that handles the things a bare loop handles badly. LangChain’s deepagents library is the reference implementation, and it organises that scaffolding into four groups.

    Deep agent architecture: an unchanged tool-calling core wrapped in execution environment, context management, delegation and steering

    • Execution environment. The agent gets tools, but also a virtual filesystem it can read and write, permission rules over which paths it may touch, and optional sandboxed code execution. Work can live outside the context window, in files.
    • Context management. Skills load domain knowledge on demand rather than upfront. Memory files persist preferences and conventions across sessions. Summarization and offloading compress long histories and oversized tool results automatically. On supported models, the static parts of the prompt are cached.
    • Delegation. The agent can maintain a structured task list as it works, and it can spawn subagents. A subagent runs in its own fresh context, works autonomously, and hands back a single distilled report.
    • Steering. Sensitive tool calls can pause and wait for a human to approve, edit, or reject them before they run.

    The unifying idea is simple. A plain ReAct loop has one context, one routine, and one level of effort. A deep agent has several of each, plus the ability to decide which one a given request should get.

    We did not adopt all of it. We took the capabilities that mapped onto problems we actually had, and where the harness had no answer, we built our own on top of it, problem by problem, some fixes came free with the harness and some we added ourselves. Each section below says which is which.

    Problem 1: Every question got the heavyweight treatment

    The problem

    A greeting should be instant. A factual lookup should be quick. A five-way comparison should be thorough. We gave all three the same routine: reason, search, observe, reason again.

    The cost ran in both directions. Simple questions took seconds longer than they needed to and burned tokens on research that was never necessary. Worse, the loop’s bias toward acting meant that on easy questions the agent would go looking things up that it already knew, and come back with a weaker answer than if it had simply spoken.

    The deep agent fix: triage before effort

    Before any real work happens, one fast classification pass reads the question and sorts it into a lane.

    • DIRECT. Greetings, follow-ups, clarifications. Answer from what the model already knows. No tools at all.
    • TOOL. One clear factual question, like the capital of France or the height of the Eiffel Tower. One lookup which runs through the web search tool we built ourselves, one answer, stop.
    • DEEP_RESEARCH. Genuinely open-ended work. Compare these approaches, trace the history of this, investigate this properly. This lane gets the full loop.

    class Intent(str, Enum):

        DIRECT        = “DIRECT”          # no lookups needed

        TOOL          = “TOOL”            # one lookup

        DEEP_RESEARCH = “DEEP_RESEARCH”   # full investigation

    Each lane only assembles the machinery it needs. Simple messages skip the heavy path and come back faster and cheaper. Hard questions still get the full investigation. They just stop subsidising the easy ones.

    One rule governs the whole thing: when triage is unsure, it picks the most thorough lane. If we cannot tell how hard a question is, we spend more to be safe. Saving tokens never outranks being right.

    This is a piece we built ourselves. The harness gives you the machinery to run work at different depths. Deciding which depth a particular question deserves is application logic, and you have to write it.

    One triage pass sorting an incoming message into three lanes: DIRECT with no lookups, TOOL with one lookup, DEEP_RESEARCH with a wide sweep

    Problem 2: Bad sources went straight into the answer

    The problem

    When the agent looked something up, whatever came back went directly into its context. If a search returned four articles and one was only loosely related, all four arrived carrying equal authority. The model had no way to separate a strong source from a weak one, so it treated them alike and answered confidently from the mixture.

    This failure is worth naming precisely, because it usually gets mislabelled. The agent was not hallucinating. It was faithfully repeating what retrieval handed it. The fault was upstream of the model entirely.

    The deep agent fix: a grader between the tool and the loop

    We put a checking step between retrieval and reasoning. Every time the agent looks something up, that step grades how relevant the results actually are before any of them are allowed into context.

    If the sources are good, they pass through. If they are bad, the system rewrites the query and searches again from a different angle. If the second attempt also comes up short, it flags the gap so the agent answers cautiously instead of confidently. If the results are mixed, it keeps the good ones and searches again to fill what is missing.

    The important detail is that the grader does not know what answer the agent is hoping for. It judges sources on their merits alone, which means it cannot talk itself into approving weak research just because the weak research would be convenient.

    This is the second piece we built. A deep agent harness controls what enters context and when. Judging whether a specific retrieval result is good enough for your domain is a call only you can make. This is the same instinct we applied one level down, when we rebuilt our crawler to return only the parts of a page that match the query.

    A grader scoring search results before the assistant sees them, routing to four outcomes: pass, refill, retry, or answer with caution

    Problem 3: The agent searched instead of thinking

    The problem

    The original loop could reason. Nothing made it. Handed a hard question, it would fall into a searching rhythm: query, read, query, read, never pausing to ask whether the last result actually helped or what was still missing.

    Multi-part questions exposed this most clearly. Asked four things at once, the agent would answer two well, mention a third in passing, and quietly drop the fourth. Nothing anywhere in the architecture was keeping track of the fact that a fourth part existed.

    The deep agent fix: make planning a step, not a suggestion

    This is where a deep agent’s task planning earns its place. The harness offers a task list the agent maintains as it works, with every item tracked as pending, in progress, or complete. A four-part question becomes four tracked items, and none of them can go missing without it being visible.

    We paired that with a forced rhythm: reflect before searching, and reflect again after every result before deciding the next move.

    It sounds trivial. Forcing the rhythm is exactly what turns frantic searching into deliberate investigation. It also brought a benefit we did not expect to value as much as we now do. The reasoning is written down as structured state, so when an answer goes wrong we can see precisely where the thinking went wrong instead of guessing at it.

    Before and after: unbroken searching that quietly drops part of a multi-part question, versus reflect-search-reflect with every part covered

    Problem 4: Long investigations buried their own findings

    The problem

    A hard question needs many lookups. In the original design, the result of every lookup piled into one shared context, and none of it ever left.

    The effect was perverse: answers got worse the longer the agent worked. Early findings, often the most relevant ones, ended up buried under later raw material. By the time the agent sat down to write, the good material from step two was competing for attention with fifteen pages of noise from step nine. More effort, worse result.

    The deep agent fix: delegate to subagents with their own context

    This is the capability that made deep agents worth adopting rather than just patching the loop.

    For the heaviest questions, the work now splits between a manager and a specialist. The manager holds the question and the plan. It delegates the actual searching to a subagent that runs in a completely separate context, works through its subtask alone, and hands back only the distilled findings.

    The manager never sees the raw pile. Its context stays small and focused on what it is actually responsible for, which is the shape of the final answer. The subagent can go as deep as it needs to, because all of its clutter dies with it.

    Alongside that, summarization and offloading run automatically. History gets compressed as it ages, and oversized tool results are moved out of context and into files the agent can read back on demand.

    This part is native. Isolated subagent context, automatic summarization, and offloading to a filesystem are exactly what the harness is built to provide.

    Delegation in the deep agent architecture: a manager holds the question and plan while a specialist searches in its own workspace and returns only distilled findings

    Problem 5: Every lookup returned the same amount of material

    The problem

    Retrieval was configured once, globally. Every search pulled back the same fixed number of sources. That number was simultaneously too small for “list everything we know about X” and far too large for “what time does the gate open.”

    The deep agent fix: scale depth to the intent we already know

    We were already classifying every question in Problem 1, and that classification carries real information about breadth. So we let it set retrieval depth as well. Pinpoint questions get a small, precise set. Broad questions get a wide sweep.

    Same retrieval machinery, effort matched to the need, and no second classification pass to pay for.

    What changed

    The original design was not broken. It was undiscerning. It spent the same effort and ran the same strategy for every request, regardless of what the request actually was. Every fix we made added a piece of judgment.

    How the deep agent architecture changed five things: triage, a fact checker, required reflection, a manager and specialist split, and depth scaled to the question

    The part we are happiest about is what did not change. The original ReAct agent still exists in our system, untouched. The deep agent slots in as the default and calls the old loop when the old loop is the right tool for the job. We swapped the brain without rewiring anything around it.

  • Returning Only What Matters: Smarter Web Crawling with Semantic Search

    When our agent needed to answer a complex question, it could not rely on search results alone. Sometimes the answer was buried deeper in a webpage. For eg: “Give me minute by minute breakdown of the world cup 2026 Final game” requires going deeper into the content of a related article as compared to “Who was the top scorer for Spain in the world cup 2026 Final?” which could be found in search results alone. 

    We covered how our web search tool found relevant URLs in our previous blog.

    In this blog, we are going to cover how our web crawl tool finds the relevant content inside a URL through semantic search.

    When search results are not enough

    A user would ask a question, and web_search would return five URLs that looked relevant. The agent would check the first result, get the description from the preview, and give an answer based on that.

    Often the answer was right. A simple question like “how many goals did Ronaldo score against Croatia in World Cup 2026?” could be answered from the preview alone. The search result would have the number right there in the description or first line of the page. Done.

    Sometimes it was not enough. A more complex question like “give me a minute by minute breakdown of the events in the Portugal vs Croatia game” could not be answered from the preview. The preview might just say “exciting match with 3 goals” or something generic. The real breakdown of what happened at each minute was deeper in the page, buried in a full match report or article. We had to crawl the entire URL to find the detailed information.

    When that happened, we needed to go deeper. We needed a second tool called web_crawl. This tool takes a URL and reads the full page, looking for the specific information the user asked for.

    That is when the second problem started.

    The leftover problem

    Our early version of web_crawl did what we asked it to do. It took a URL and returned the entire content of that page. All of it. Every word.

    If a user asked “what is the return policy” and we had to crawl a product page, web_crawl would return not just the return policy, but the product description, customer reviews, navigation menu, footer, everything that was on the page.

    Or if a user asked “how many goals did Ronaldo score against Croatia” and we crawled a sports news article, it would return the entire article, including player biographies, team history, match statistics for other games, and commentary. All of it, even though the answer was just one number.

    This worked, technically. But it created two real problems.

    Why that hurt

    First, all that extra content wasted the model’s context tokens. If a user asked “how many goals did Ronaldo score against Croatia” and we crawled a full sports article with 5000 words of match details, player stats, and team history, we were burning tokens on 4999 words of garbage to answer a question that needed one number. With many queries running, those wasted tokens added up fast and made everything slower and more expensive.

    Second, dumping unrelated information confused the model. When a model reads a page full of product reviews mixed with the return policy mixed with shipping information, it gets confused about what matters. It might pull information from the wrong part of the page, or make up an answer based on the noise. The real answer gets buried.

    It is like asking a librarian for one specific fact and having them hand you the entire book instead of just the page you need. You have to read through all the noise to find the answer, and you might miss it or get lost in the details.

    The shift in thinking

    We realized that the problem was not with how we were getting the page. It was with what we were keeping from it.

    The goal was not to return everything. The goal was to return only what the user asked for. So instead of giving back the whole page when web_crawl ran, we needed a way to figure out which parts of the page actually matched the meaning of the user’s query.

    This was different from just searching for words. If a user asked “minute by minute breakdown of the Ronaldo vs Croatia game,” a simple word search would find pages with those words in them. But it would not know which parts of the page had the actual breakdown and which parts had other information like team stats or historical context. We needed web_crawl to understand meaning, not just match keywords.

     

    The fix

    We used a small AI model called sentence-transformers/all-MiniLM-L6-v2.

    Here is how it works. The model takes the user’s query and converts it into a set of numbers that capture the meaning of that query. Then it does the same thing for each chunk of the page. It breaks the page into smaller pieces and converts each one into numbers that capture its meaning.

    The magic is that these numbers put similar meanings close together. So “return policy” and “how to send items back” end up close to each other in this space, even though the words are different. “free shipping” and “no shipping cost” also end up close together. And “minute by minute breakdown” and “events that happened during the match” are also nearby in meaning, even though they use different words.

    Once we have these numbers for the query and all the chunks of the page, we find which chunks are closest to the query. Those are the chunks that mean the same thing as what the user asked for. We keep only those chunks and return them to the model.

    The function kept the same inputs. It still took a query and a URL. It just got smarter about what it gave back.

    The result 

    Now when a user asks about a return policy, web_crawl gives back only the return policy section. When they ask about a minute by minute breakdown of the Ronaldo game, it returns only the breakdown section from the article, not the player biographies or team history. When they ask about shipping, it returns the shipping information. No more entire pages full of unrelated content.

    Lessons Learned

    This fixed both problems. The context is clean and focused, so we do not waste tokens on garbage. The model reads only what it needs and does not get confused by unrelated text. There are fewer hallucinations because there is less noise to confuse the model. The results are faster and cheaper because we use fewer tokens.

    The real lesson is this. Our web_search tool gets us started with quick answers. But when we need to go deeper into a page, web_crawl now does it smart. It does not just dump the whole page. It finds only what matters.

    Matching by meaning beats matching by volume. Giving less, but more relevant, information is better than giving everything. In almost every case, thirty words on topic are better than a thousand words scattered all over the place.

  • Match the tool to the job, how we created our own web search tool

    Some engineering problems do not look like problems when they first arrive. Ours started with a simple request. Our product needed to search the web. That sounded easy. People search the web all the time, so it felt like something that was already solved. Once we started building it, we found that the simple request hid a real decision about which tool to use.

    The first attempt

    Our first version used a tool called Crawl4AI. Crawl4AI is an open-source Python library built for collecting web content for AI systems. The way it works is that it opens a full, real web browser in the background, a headless Chromium browser, and uses it to load and read a page the same way a normal browser would. It is a popular and widely used library, with more than 60,000 stars on GitHub, so it felt like a solid, well supported choice.

    The crash

    Because Crawl4AI drives a real browser, it can do things a plain request cannot. It can wait for the page to finish loading, scroll down to pull in content that only appears as you go, run the page’s own scripts, and then hand back a clean, readable version of the page. This is why it works so well on difficult websites, the ones that build their content with JavaScript after the page first loads, where a plain request would often return only an empty shell. That same power is also where our problem started.

    A real browser is heavy. A headless Chromium browser is not a small program. It is close to running a full copy of Chrome, with all of its moving parts loaded into memory at the same time. Each browser instance that Crawl4AI opened used a large amount of memory, around 300 MB every time.

    On top of that, our early script opened a brand new browser every single time it ran a search, instead of reusing one. So when many searches ran together, we were not opening one browser and sharing it. We were opening a separate browser for each search. Ten searches at the same time meant ten separate browsers. Each one carried its own 300 MB, and none of that memory was shared between them, so the total added up fast with every extra search running at once.

    Two other things made this worse. First, when you run Crawl4AI on your own server, you have to manage all of this yourself, including the browsers and how many of them run at once. Second, the memory cost does not go down as you do more work. Every extra page you want to read at the same time means another full browser, and another 300 MB on top.

    Reading one page at a time was fine. The trouble started the moment we needed to handle many searches at once, which is exactly what a real product has to do. We saw this clearly when we used JMeter to put more load on the application. As soon as we increased the number of concurrent runs, the many browsers running at the same time filled up the server memory, and the server crashed.

    The realisation

    When we looked at what had happened, we saw the real mistake. We had reached for the most powerful tool and made it our default. But most of our searches were ordinary. They did not need a full browser to read and render an entire page. They only needed a quick and light way to get search results. The powerful tool was not wrong. It was just the wrong choice for the common case.

    The fix

    We changed our default to a tool called SearXNG.

    SearXNG is a free and open-source metasearch engine. A metasearch engine does not keep its own index of the web. Instead, it sends the query to several other search engines, collects their results, and combines them into a single list. Because of this, it does not need to open a browser at all. It talks to the search engines directly and returns results, which is fast and light on memory. It is written in Python, it is released under the AGPL-3.0 open-source license, and it can be self hosted, which means we run it on our own server and stay in control of it. It also does not track or profile its users. It can pull results from many sources, including Google, Bing, Brave, DuckDuckGo, Qwant, Startpage, and Yahoo.

    Right now our default setup uses SearXNG together with DuckDuckGo. SearXNG is the layer that runs the search and gathers the results, and DuckDuckGo is one of the search engines it draws those results from. This combination gives us clean search results without the heavy cost of a browser.

    We did not throw Crawl4AI away. Some pages really do need a full browser to be read properly. So we kept Crawl4AI as a backup, ready to step in for those harder pages, instead of using it for every search.

    The lesson

    The main lesson for us was simple. The most powerful tool is not always the right default. It is better to match the tool to the job you do most often, and to keep the heavy tool only for the few cases that really need it.

    Our first setup worked for a single search but broke when many searches ran at once, which is exactly what happens in a real product. The fix was not a bigger server or a clever trick. It was stepping back and asking what the job actually needed. Most of the time, the answer was less than what we first reached for.