<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Rod Johnson on Medium]]></title>
        <description><![CDATA[Stories by Rod Johnson on Medium]]></description>
        <link>https://medium.com/@springrod?source=rss-de5947672915------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*w7FhiB6igokPDlHh.jpeg</url>
            <title>Stories by Rod Johnson on Medium</title>
            <link>https://medium.com/@springrod?source=rss-de5947672915------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 12 Nov 2026 05:24:14 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@springrod/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[The Graph Is a View, Not a Database: Cypher Over Everything with Embabel]]></title>
            <link>https://medium.com/@springrod/the-graph-is-a-view-not-a-database-cypher-over-everything-with-embabel-88e1053fd20d?source=rss-de5947672915------2</link>
            <guid isPermaLink="false">https://medium.com/p/88e1053fd20d</guid>
            <category><![CDATA[semantic-layer]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[knowledge-graph]]></category>
            <category><![CDATA[context-engineering]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[Rod Johnson]]></dc:creator>
            <pubDate>Fri, 06 Nov 2026 10:23:29 GMT</pubDate>
            <atom:updated>2026-11-06T10:23:29.708Z</atom:updated>
            <content:encoded><![CDATA[<p>I <a href="https://medium.com/@springrod/a-new-layer-for-a-new-client-introducing-embabel-worlds-616ccde93533">recently discussed</a> why the <strong>context or semantic layer</strong> is critical AI infrastructure, and introduced <a href="http://worlds.embabel.com">Embabel Worlds</a>.</p><p>Let’s look under the hood and consider some important design decisions.</p><p><a href="https://blog.cloudflare.com/code-mode/">Code mode</a> — where LLMs write small programs rather than call single-verb tools —is the future. It’s far superior to traditional tool calling, and Embabel Worlds embraces it.</p><p>This raises the question of <em>what language should we ask LLMs to code in?</em></p><p>There need not be a single choice. LLMs are proficient in every viable language, so we can even let them choose — assuming we can provide necessary surfaces for each language. Even second or third tier languages have more than enough available training data.</p><h4>Why a Declarative Query Language?</h4><p>We can also choose between <em>types</em> of language. Choosing between Python and TypeScript is largely about libraries; choosing between different language <em>families</em>, like query or logic programming languages, has different pros and cons that are important to consider.</p><blockquote><strong>We chose a declarative query language as a core choice for Embabel Worlds</strong>.</blockquote><p>Declarative query languages have a number of benefits. They are:</p><ul><li><strong>Optimisable:</strong> The query specifies the intent and the engine chooses the execution plan, with the freedom to improve it.</li><li><strong>Analysable before execution:</strong> Queries can be inspected, costed, permission-checked and rewritten. Code in regular high level languages cannot be to the same extent.</li><li><strong>Safe:</strong> They can be limited to be read-only and bounded, with no arbitrary side effects.</li><li><strong>Joins are first-class:</strong> Combining data is a declaration, and doesn’t require looping.</li><li><strong>Data independent:</strong> With an appropriate planner, queries can be independent of how data is stored, surviving changes to storage, schema layout and access paths.</li><li><strong>Concise and verifiable:</strong> Short queries, validated against a schema, allow fewer places for bugs (or LLM errors) to hide.</li></ul><p>In short, they’re perfect for AI agents to write in.</p><blockquote>You can’t do everything an agent might want to do in a declarative query language, but what you can do, you should. It‘s an ideal way to ground LLM responses.</blockquote><h4>Much Bigger Than a Database</h4><p>But aren’t we limited to working with data in a database that supports our query language?</p><p>Absolutely not.</p><blockquote>As we build out a critical new infrastructure layer, the query language should not dictate data representation.</blockquote><blockquote>Rather than try to put everything in a particular database — which is both impossible and undesirable — we should intentionally separate query language from data sourcing.</blockquote><p>The answer is federation. This is not a new idea. However, it’s more important than ever before, given the proliferation of systems that people now care about. It’s particularly well suited to working with LLMs:</p><ul><li>The value of getting the right data to an LLM is so great that the cost of a federated query is more acceptable than in traditional architectures.</li><li>It offers a novel way to get model intelligence inside query paths, where it directly acts on data.</li></ul><p>Making federation performant is a hard engineering problem. Solving hard engineering problems is the job of infrastructure vendors, and we welcome the challenge. A core part of Embabel Worlds is our sophisticated query planner that can work with a variety of sources, considering security, performance, rate limiting, batching, and other considerations.</p><h4>Which Query Language?</h4><p>The obvious choice is SQL. Immense amounts of training data; many implementations; federation-ready projects like the excellent <a href="https://calcite.apache.org/">Apache Calcite</a>.</p><p>But is SQL the right fit? Sure, it enables us to join data between systems and perform useful functions like counting, grouping, and math in a concise and readable way. But it is verbose and costly to perform deep navigation between entities — something that LLMs may need to do to answer questions. And modeling rich relationships between entities via join tables is complex and costly.</p><p>We considered SQL, but chose <a href="https://en.wikipedia.org/wiki/Cypher_(query_language)">Cypher</a>, the graph query language. Cypher is widely adopted, and is the starting point for the <a href="https://www.gqlstandards.org/">GQL standard</a>.</p><p>Cypher offers SQL-inspired filtering and grouping, but also has edges that carry meaning. Relationships are first class. It’s much more natural for querying dynamic data and following deep traversals.</p><p>When relationships are first class, they can have properties. This is a intuitive model. For example, consider the concept of an actor appearing in a movie, and how naturally it’s expressed in Cypher:</p><pre>MATCH (a:Actor)-[r:ACTED_IN]-&gt;(m:Movie)                                                                                                             <br>  WHERE m.year &gt; 2000                                                                                                                                 <br>  RETURN a.name, r.role, m.title    </pre><p>To do this in SQL we’d need a join table. Likely costlier, and certainly less intuitive and more verbose:</p><pre>SELECT a.name, ai.role, m.title                                                                                                                     <br>  FROM actors a                                                                                                                                       <br>  JOIN acted_in ai ON ai.actor_id = a.id                                                                                                              <br>  JOIN movies m    ON m.id = ai.movie_id                                                                                                              <br>  WHERE m.year &gt; 2000;       </pre><p>In many cases, Cypher is so compelling that the main objection to adopting it has so far been the need to introduce a new database. Data tends to stick where it lands, so by decoupling the query language from the storage, we remove that understandable objection.</p><p>A context layer is critical infrastructure that <em>must</em> support multiple client types. Cypher is compelling as a primary choice, but we don’t need to reject others. We can also add SQL, GraphQL and other surfaces.</p><blockquote>A sophisticated context layer provides insights independent of the data representation.</blockquote><blockquote>When we want SQL, we can put a view on top of our context layer, as Embabel Worlds supports. This gives us the best of both worlds — the intuitive and expressive Cypher for agents, and the incredible reach of SQL into existing, established tools.</blockquote><h4>Cypher Over Everything With Embabel Worlds</h4><p><a href="http://worlds.embabel.com">Embabel Worlds</a> provides federated Cypher across a wide and extensible range of sources.</p><blockquote>We don’t limit our Cypher queries to what sits in a particular graph database. The graph is a view, which should not dictate data storage. We have our own blazingly fast <a href="https://mvnrepository.com/artifact/org.opencypher/tck">openCypher TCK-compliant</a> Cypher implementation that can work over a range of sources.</blockquote><p>Embabel Worlds can run on a graph database but doesn’t need one. We can store data in SQLite, Postgres and Oracle, allowing you to use a database you already run, while also supporting tools that speak Cypher.</p><p>It enables Cypher over a range of sources — existing databases, APIs, tabular data — whatever you choose to expose. Consider the following example around Australian Electoral Commission data, querying Australian House of Representatives seats:</p><pre>MATCH (e:Electorate) <br>RETURN e.division, e.member, e.marginPct, e.state</pre><p>Straightforward…so far, not magical. But consider how simple the query is to see which are the most marginal seats held by the government:</p><pre>MATCH (e:Electorate) <br>WHERE e.marginPct &lt; 3 AND e.heldByAbbrev = &#39;ALP&#39;<br>RETURN e.division, e.member, e.marginPct, e.state<br>ORDER BY e.marginPct ASC</pre><p>Compared to a Python, JavaScript or other program, this is simple and intuitive.</p><p>Having the ability to operate over APIs is surprisingly powerful, but it gets <em>much </em>better the more systems you relate.</p><p>Consider a query that joins multiple systems. We need to agree on a key to join on — Embabel provides a sophisticated mechanisms for that. But the result is powerful and intuitive. Imagine we want to find which customers owe us money while they’re waiting on an resolved support case. This joins systems that know nothing about each other, providing insight above them. SupportCase comes from a ticketing system, while BillingInvoice is from a payment system:</p><pre>MATCH (c:SupportCase {status: &#39;open&#39;})&lt;-[:HAS_CASE]-(a:CustomerAccount)                                                                             <br>        -[:BILLED_AS]-&gt;(:BillingCustomer)-[:HAS_INVOICE]-&gt;(inv:BillingInvoice)                                                                        <br>  WHERE inv.paymentStatus &lt;&gt; &#39;succeeded&#39;                                                                                                              <br>  RETURN a.name AS account,                                                                                                                           <br>         count(DISTINCT c) AS openCases,                                                                                                              <br>         sum(DISTINCT inv.amountDueCents) / 100.0 AS owed                                                                                             <br>  ORDER BY owed DESC   </pre><p>All of these nodes are <em>virtual, </em>although we can apply the full power of Cypher to them. None is copied into persistent storage. The Cypher — and the LLM that is likely generating it — knows nothing about how these nodes are retrieved and materialized, which involves API calls and secrets. That information is provided by the Embabel realm definition that extends our ontology. The same metadata also supports exploration in the Embabel Worlds Query Studio, with autocompletion and tool tips:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DIdKbz_F7gtHkTREXl7jyA.png" /></figure><h4>Adding Model Intelligence</h4><p>Queries in Embabel Worlds are legal Cypher, which helps LLMs generate them. We considered our own DSL to extend Cypher capabilities, but decided against it. There are more than enough languages already.</p><p>However, this does not prevent us extending the capabilities of Cypher for the AI era. For example, we add functions that apply model intelligence <em>inside</em> queries. Take this query, which classifies issues. The ai.classify function uses a model to classify issues based on a prompt. The virtual Cypher execution engine is responsible for parallelizing the model calls and getting them the right, focused, content. The user can choose the model — even models other than LLMs for some tasks:</p><pre>MATCH (:GitHubRepository {full_name: &#39;embabel/me&#39;})-[:HAS_ISSUE]-&gt;(i:GitHubIssue)                                                                   <br>  WHERE i.state = &#39;open&#39;                                                                                                                              <br>    AND coalesce(i.isChangeRequest, false) = false                                                                                                    <br>    AND ai.relevant(i, &#39;describes a defect or missing capability, not a question, chore or discussion&#39;)                                               <br>  RETURN i.number                                                          AS issue,                                                                  <br>         i.title                                                           AS title,                                                                  <br>         ai.classify(i, &#39;kind: bug, performance, usability or documentation&#39;) AS kind,                                                                <br>         ai.classify(i, &#39;blast radius: one user, one realm or every appliance&#39;) AS reach,                                                             <br>         i.comments                                                        AS comments                                                                <br>  ORDER BY ai.score(i, &#39;how much this hurts people using the product today&#39;) DESC                                                                   <br>  LIMIT 15</pre><p>Embabel Worlds also supports extracting structures from text, with the graph shape specified in Cypher:</p><pre>CALL ai.fromText(                                                                                                                                   <br>    &#39;Ada Lovelace worked with Charles Babbage on the Analytical Engine, which he designed in London.&#39;,                                                <br>    &#39;(p:Pioneer)-[w:WORKED_ON]-&gt;(m:Machine)&#39;                                                                                                          <br>  ) YIELD p, w, m                                                                                                                                     <br>  RETURN p, w, m     </pre><p>Normally, as in this case, the types would be defined in an Embabel realm, but it’s possible to define them inline, using our <em>other</em> preferred programming language — TypeScript:</p><pre>CALL ai.fromText(<br>  &#39;Ada Lovelace worked with Charles Babbage on the Analytical Engine, which he designed in London.&#39;,<br>  &#39;(p:Pioneer)-[w:WORKED_ON]-&gt;(m:Machine)&#39;,<br>  {types: &quot;<br>    class Pioneer extends Entity {<br>      declare name: string<br>      @Relationship({ type: &#39;WORKED_ON&#39; })<br>      declare worked: Machine[]<br>    }<br>    /** A machine or invention. */<br>    class Machine extends Entity {<br>      declare name: string<br>    }<br>  &quot;}) YIELD p, w, m<br>RETURN p, w, m</pre><p>Both these two queries produce the following graph, as shown in the Embabel Worlds UI:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/942/1*w_T-ZmIa21QzFA_yEuwF9A.png" /></figure><p>The ability to move between text and structure, and introduce as much structure as possible, is critical to making Gen AI dependable. <a href="https://github.com/embabel/embabel-agent">Embabel agent framework</a> and Embabel Worlds provide a sophisticated solution.</p><p>Of course using model intelligence inside Cypher queries, and federating sources that may be rated limited, means ensuring that queries can run without locking any database. Our runtime takes care of this.</p><p>The full virtual Cypher spec is public: see the <a href="https://worlds.embabel.com/cheatsheet">cheat sheet</a> for other examples of what’s possible.</p><p>Our Cypher implementation also provides excellent error messages. Because queries are predominantly LLM-generated, there’s even greater value in explaining precisely why a query is invalid, to help the model retry. This feedback goes beyond syntax — as in a traditional database — to semantic errors.</p><h4>Adding Views and Watches</h4><p>We build beyond Cypher in various other ways, such as:</p><ul><li><em>Introducing Views</em>. Unlike SQL, Cypher does not offer views. We do, and it’s a great unit of query composition and reuse.</li><li>Allowing views to be <em>watched. </em>Changes in their results can have business importance and</li><li><em>Establishing query pipelines</em> using the <a href="https://medium.com/@springrod/ai-for-your-gen-ai-how-and-why-embabel-plans-3930244218f6">GOAP planning capabilities</a> of Embabel.</li></ul><h4>Your Context Layer Awaits</h4><p><a href="http://worlds.embabel.com">Embabel Worlds</a> offers a powerful semantic layer you can try today. Experiment in our Query Studio to see just how powerful our Cypher implementation is. You don’t need to introduce a new database. It can query whatever you care about — your databases, APIs, external systems, or anything else you care about. You’ll be up and running in minutes. What will you build?</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=88e1053fd20d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Vibe Coding, Meet Reality]]></title>
            <link>https://medium.com/@springrod/vibe-coding-meet-reality-0fb1e4e04053?source=rss-de5947672915------2</link>
            <guid isPermaLink="false">https://medium.com/p/0fb1e4e04053</guid>
            <category><![CDATA[semantic-layer]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[knowledge-graph]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[Rod Johnson]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 11:50:38 GMT</pubDate>
            <atom:updated>2026-11-03T11:50:38.412Z</atom:updated>
            <content:encoded><![CDATA[<p>By now we’ve all heard plenty of vibe coding horror stories. One of the more amusing:</p><p><a href="https://mybroadband.co.za/news/software/672602-south-african-coffee-franchise-exposes-company-and-private-data-in-vibe-coded-web-application.html">South African coffee franchise exposes company and private data in vibe-coded web application</a></p><p>It’s not surprising that unconstrained vibe coding is dangerous. While the failures are entertaining, they shouldn’t lead us to dismiss amazing new potential. Agents’ ability to generate code <em>should</em> be a huge net positive for non-technical knowledge workers. It’s incumbent on skilled developers and software infrastructure companies to make it safe.</p><h4>Mirage Becomes Reality</h4><p>For more than forty years, the software industry has promised to eliminate programmers, and failed. Today this promise can — and <em>should</em> — be delivered — to a point<em>.</em></p><p>From <a href="https://en.wikipedia.org/wiki/James_Martin_(author)">James Martin</a>’s 1982 <em>Application Development Without Programmers</em>, championing fourth-generation languages to empower business users, through <a href="https://en.wikipedia.org/wiki/Computer-aided_software_engineering">CASE tools</a> in the 1990s, <a href="https://en.wikipedia.org/wiki/IBM_Rational_Rose">Rational Rose</a> and other modelling tools in the 2000s, BPM vendors such as Appian promising “zero-code” development in the 2010s, through Google and Microsoft talking of “citizen developers” in the early 2020s, the promise has been restated.</p><p>Each generation delivered some useful technology. None delivered the promised escape from programming. In September 2026, Gartner reported that “few citizen developers can build applications safely and effectively.”</p><p>Today, however, coding agents can build sophisticated applications in real languages. Non-technical users can create apps quickly. The challenge is <em>safely</em> and <em>effectively</em>.</p><p>Vibe coding without guardrails is dangerous, and anything but a step forward. However, that doesn’t mean we should give up on its promise. We need to work out where it applies, where it doesn’t, and architect appropriately.</p><p>The solution is realism and layered infrastructure.</p><p>Wholly replacing professional developers is dangerous and undesirable. While coding agents can greatly increase their productivity, their skills — especially in design — remain important to solid outcomes.</p><p>Everything depends on good architecture. Everything we know about layering, design patterns, loose coupling, testability etc. still matters. In fact it matters <em>more</em> than ever.</p><p>With sound architectural layering, we can create systems that allow non-technical users to safely vibe code <em>real </em>applications — not just prototypes — that benefit from their domain understanding and reflect their personal preferences. This finally realizes the promise of empowering business users.</p><p>Let’s look at some of the implications.</p><h4>Everyone gets their Own Pane of Glass</h4><p>Every user will have their own personalized interface to make them happier and more productive, and this can be achieved without risk.</p><p>We’ve all suffered from UIs that weren’t built for us, and the need to jump between disconnected UIs. We all know the Microsoft Word problem, where the menus are so complex because of a range of features no individual user needs; and the multiple-SaaS problem, where you need to jump between different interfaces. These problems will soon be a thing of the past.</p><blockquote>Every user will have their own personalized UI, or as many UIs as they want. They will be able to evolve their apps easily and share them where appropriate. No human — not even skilled developers — will look at code in this layer.</blockquote><h4>Applications Will Proliferate, But Application Builders are In Trouble</h4><p>Building applications will decline as a business model.<em> </em>On top of a strong underlying architecture, AI can build anything quickly, removing many moats.</p><blockquote>Companies delivering applications will survive only if they accept that they are not really technology companies, and radically reduce their development costs by building on shared infrastructure.</blockquote><p>Viability will depend on non-technical factors, such as:</p><ul><li><em>Deep understanding of and relationships within a domain</em>. Especially in niche markets, specialist expertise retains value.</li><li><em>Access to proprietary data or feeds. </em>Lack of good data is a huge barrier to AI. This barrier to entry will likely diminish as owners of data and feeds enable a wider range of agentic clients.</li></ul><h4>What Infrastructure Does This Future Rest On?</h4><p>These benefits require understanding of architecture, and a new infrastructure layer.</p><p>I recently blogged about the <a href="https://medium.com/@springrod/a-new-layer-for-a-new-client-introducing-embabel-worlds-616ccde93533">need for a context or semantic layer</a> that sits between AI models and relevant data and systems, presenting a coherent, scriptable, AI-ready view of everything underneath. While it can be built in house by large businesses (and presently often is), a context layer is generic infrastructure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*R1uXOOamEPfKTOWZkzMYZw.jpeg" /><figcaption>Vibe coding over a context layer is great</figcaption></figure><p>The context layer is the cutoff point above which vibe coding becomes not only safe, but the best way to build. It provides models with understanding of what’s underneath and a solid way of accessing data, managing usage and enforcing guardrails.</p><p>Users should be able to build against a context layer with familiar tools such as coding agents.</p><p>Let’s look at some examples of what can be built easily, and safely, over a strong context layer.</p><p>Consider this application based on UK public data. It took about half an hour to write on top of <a href="http://worlds.embabel.com">Embabel Worlds</a>, with the help of Claude Code. It uses six disconnected UK government APIs, relating them via <a href="https://en.wikipedia.org/wiki/ONS_coding_system">ONS local authority district code</a>. Embabel’s federated query capability illustrates the power of a strong context layer to bring insight across many systems:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qVfgJq_dUV2liAJtzRFvjQ.png" /><figcaption>L</figcaption></figure><p>A context layer also makes it easy to put model intelligence over existing apps: a great alternative to paying your SaaS vendor for its own expensive AI features. For example, a quickly built intelligence layer over Odoo using queries that incorporate LLM summarization:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9sm9ChBg2TO8UlM6WzsnHw.png" /></figure><p>The value compounds when you join across more than one system.</p><p>A context layer should allow app hosting as a first class citizen. Apps built on top of a strong context layer avoid the potential dangers of vibe coding, and enjoy important advantages. For example:</p><ul><li><em>They can be hosted securely, with a stable URL</em>. A non-technical user doesn’t need to deploy them.</li><li><em>They can be shared easily with colleagues or even published </em>more widely.</li><li><em>Their value compounds as the data and actions available to the underlying infrastructure layer broadens</em>. Sharing can unlock further value.</li><li><em>They can be validated consistently through static analysis, </em>ensuring they’re safe and acceptably performant.</li><li><em>An underlying platform gives visibility of what an organization is running</em>, versus allowing a mishmash of insecure applications.</li><li><em>Access to them can be logged and audited</em>.</li><li><em>They benefit from multi-channel communications, event streams</em> and other infrastructure services.</li></ul><p><a href="http://worlds.embabel.com">Embabel Worlds</a> offers these and other benefits today. You can be up and running in a few minutes, ready to connect your coding agent to build apps against the systems you care about. What do you want to build?</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0fb1e4e04053" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[FDEs Considered Harmful]]></title>
            <link>https://medium.com/@springrod/fdes-considered-harmful-d71f7a96d5ce?source=rss-de5947672915------2</link>
            <guid isPermaLink="false">https://medium.com/p/d71f7a96d5ce</guid>
            <category><![CDATA[forward-deployed-engineer]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[genai]]></category>
            <dc:creator><![CDATA[Rod Johnson]]></dc:creator>
            <pubDate>Mon, 19 Oct 2026 12:23:28 GMT</pubDate>
            <atom:updated>2026-10-19T12:23:28.820Z</atom:updated>
            <content:encoded><![CDATA[<p>In 2001, I chose not to evaluate WebSphere because IBM wouldn’t let me install it, but insisted on sending someone to help. Meanwhile I’d set up WebLogic in a few minutes. As time proved, I’d dodged a bullet.</p><p>By the mid 2000s, it was understood that technologies should be easy to evaluate and if they weren’t, they probably sucked.</p><p>Twenty-five years later, we’ve gone backwards. The industry now accepts — and sometimes even <a href="https://a16z.com/services-led-growth/?utm_source=chatgpt.com">seems to celebrate</a> — armies of <strong>FDEs</strong> (Forward Deployed Engineers).</p><p>Announcements like <a href="https://www.anthropic.com/news/claude-frontier-academy?utm_source=chatgpt.com">this</a> are now taken seriously:</p><blockquote>Today we are launching Claude Frontier Academy to solve one of the most pressing issues in AI implementation: talent. Backed by a $100 million commitment, Anthropic aims to train 10,000 Frontier Deployed Engineers (FDEs) by the end of 2027</blockquote><p>This is in Anthropic’s interests — what vendor wouldn’t want important companies to sign up to be indoctrinated and work around a gap in their offering— but not customers’.</p><p><strong>This is not how software problems should be solved at scale</strong>. It also rests on the questionable assumption that enterprise developers are country cousins who can’t be expected to understand how to connect the systems they’ve built to forward-looking technologies. <a href="https://a16z.com/services-led-growth"><em>Enterprises buying AI are like your grandma getting an iPhone: they want to use it, but they need you to set it up</em></a><em>. </em>Really?</p><p>A little less AI arrogance and exceptionalism is in order. We can and should learn from the experience of previous technology waves. Whenever we’ve seen countless non software companies try to solve the same problems, it’s been an indication that a new product category is needed. Consider ERP, CRM, business intelligence, application servers or web frameworks.</p><p>History strongly suggests that these ad hoc in house systems will be ripped out and replaced with better, generic solutions that allow customers to choose models and model classes. There is zero competitive advantage to any business outside software in building generic infrastructure; active <em>disadvantage</em> in tying themselves to a single frontier model vendor.</p><blockquote>Centering on consulting in a world when products can literally talk to users to help them onboard is absurd.</blockquote><p>The FDE surge indicates several things:</p><ul><li><em>Excessive influence by frontier model vendors on business users</em>. Model intelligence is rapidly becoming fungible and open weights models are sucking the oxygen out of an already questionable business model. Betting on any a frontier model vendor is questionable.</li><li><em>The gaping product gap between the frontier model vendors and business customers</em>. Models alone are not enough and customers need an additional infrastructure in a <a href="https://medium.com/@springrod/a-new-layer-for-a-new-client-introducing-embabel-worlds-616ccde93533">context layer</a>.</li><li><em>Existing products attempting to fill the context layer gap are too hard to use</em>. “Call sales for a demo” is the norm, and customers deserve better in 2026.</li><li><em>Lack of imagination in fully leveraging new technology</em> to help onboarding. Frontier models are great; let’s use them to help achieve their full potential in working with our existing systems.</li></ul><p><a href="http://worlds.embabel.com">Embabel Worlds</a> takes a very different approach. We aim to empower users to self service as far as possible. And use coding agents connected to our MCP surface to help them do so.</p><p>Want to add AI to your business? Just ask:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4tw1ndPZrfxd3zJuP1o63Q.jpeg" /></figure><p>You get the full value from your frontier model, while building a lasting understanding of your business that can be used with any model of your choice, and will deliver value without any models.</p><p>And you don’t need to call us — you can go to <a href="http://worlds.embabel.com">worlds.embabel.com</a> and evaluate today.</p><p>We hope that Embabel Worlds will become the dominant context layer. Whether or not it is the category winner, the category will win. <em>Product</em> functionality, rather than consulting, will succeed in AI-enabling the enterprise.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d71f7a96d5ce" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A New Layer for a New Client: Introducing Embabel Worlds]]></title>
            <link>https://medium.com/@springrod/a-new-layer-for-a-new-client-introducing-embabel-worlds-616ccde93533?source=rss-de5947672915------2</link>
            <guid isPermaLink="false">https://medium.com/p/616ccde93533</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[knowledge-graph]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[model-context-protocol]]></category>
            <dc:creator><![CDATA[Rod Johnson]]></dc:creator>
            <pubDate>Thu, 15 Oct 2026 23:04:00 GMT</pubDate>
            <atom:updated>2026-10-15T23:04:00.845Z</atom:updated>
            <content:encoded><![CDATA[<p>While AI is in every conversation and on every billboard, its potential in enterprise remains unfulfilled.</p><p>Besides ad hoc personal productivity enhancement, coding agents remain the sole home run. Enterprise workflows remain largely untouched, and superior insights across the business largely theoretical.</p><p>There are various reasons for this, but it’s useful to step back and think about architecture. <strong>Does AI add most value <em>inside</em> our existing systems, or <em>on top </em>of them?</strong></p><p>Many systems can be enhanced by AI: for example, given the ability to process natural language, or make subjective decisions that previously required human input. This is what frameworks like LangChain help with, and what our <a href="https://github.com/embabel/embabel-agent">Embabel agent framework</a> offers the Java community. Given the volume and importance of Java applications, Java is especially important for this kind of enhancement.</p><p><strong>However, AI on top of enterprise systems is arguably more important</strong>, enabling users to talk to the business, uncover insights across formerly siloed systems, and leverage the unprecedented ability of coding agents to compile their intelligence into custom UIs. Unlike building AI into existing codebases, AI on top of existing systems doesn’t necessarily require development skills, meaning its potential audience is larger. Code in this layer need not be produced (even indirectly) by developers, but can be generated by LLMs, and run within constrained, safe, environments.</p><blockquote><strong>LLMs are a becoming the default client</strong>, as web browsers did a generation ago.</blockquote><p>New client types typically require new architectural layers. In the 90s that brought us middleware, as hundreds of internal users who could directly connect to databases and other internal systems were replaced by millions of untrusted external users with browsers. Today it demands something new, as clever but even more untrustworthy LLMs replace browsers: a <strong>context layer </strong>that sits on top of the business to safely and efficiently expose it to AI.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZGFtQ6v34y-y9A6mrfFBkg.jpeg" /></figure><h4>The New Architectural Layer, and New Product Category</h4><p>AI models need to get at what matters in a business, in a way that captures meaning and relationships, and is governed, explainable and auditable.</p><p>This layer should provide:</p><ul><li><strong>Understanding</strong> of individual systems and the relationships between them</li><li><strong>Efficiency</strong> in allowing LLMs to interact with underlying systems with a minimum number of tool calls and without excessive token usage</li><li><strong>Governance</strong> in what is permitted and who can authorize it</li><li><strong>Explainability </strong>of why was something done</li><li><strong>Auditability </strong>around who did what</li></ul><p>As the need for and shape of this new architectural layer have become evident, several names for it have emerged, with no clear winner as yet. Besides context layer<em>, </em>they include <em>semantic layer, work graph, knowledge layer</em>, and <em>ontology — </em>the latter associated with Palantir.</p><blockquote>To illustrate the role and importance of the context layer, let’s consider why coding agents work so well. They were one of the first proof points of the necessity of a layer above even the best LLMs, in the agent harness. But coding harnesses and the models they worked with benefited from something else that came for free — a powerful context layer. Models have been trained on your programming language, git, GitHub, your operating system and the scripting languages that likely run on it. They know what to call and when, and how to call it most efficiently. By contrast, they don’t know your business, and a comparable understanding must be provided to them in a context layer.</blockquote><p>Consider questions across systems. If a coding agent needs to ask questions across multiple files or GitHub issues, it writes a script. There is no existing scripting language for your business.</p><p>Imagine if you want to ask questions across your CRM, payment and ticketing systems, such as <em>which customers have upsell deals which have slipped or late payments, and are angry about unresolved issues? </em>An LLM doesn’t know how to write a query or script to answer that. A context layer gives it that ability.</p><h4>Why a Context Layer vs MCP?</h4><p>Suppose we work with three SaaS products and two internal systems. The SaaS vendors probably ship MCP servers. Can’t we just task our internal teams to put MCP servers on top of their applications and declare victory?</p><p><strong>No</strong>. With this naive approach, we’re asking models to improvise the solution to every problem. They need to call individual tools on each system and coordinate the results. Tools will either be too narrow (forcing chatty calling) or too prescriptive (limiting what LLMs can achieve). A good enough model may still arrive at the right result, but it won’t be repeatable or efficient. And smaller models will likely not be an option.</p><blockquote>MCP was a big step forward in connecting LLMs to the real world. But it is a tactical, rather than strategic, solution that does not provide the ideal substrate over complex estates.</blockquote><p>A decisive proof point is how coding agents constantly write scripts. Their tool use looks nothing like typical MCP tool use.</p><p>They naturally use <strong>code mode </strong>— a far better approach to tool calling well explained in an <a href="https://blog.cloudflare.com/code-mode/">excellent Cloudflare blog</a>. In code mode, instead of calling a bag of tools, each to perform a point operation, LLMs write scripts against a known substrate. This preserves context and takes advantage of the fact that LLMs are fluent in all major programming languages.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UF8VGfcGEJDt-vjnwqRimg.png" /></figure><p>Code mode is usually more efficient, as each tool call is costly in tokens and time. The more we can do in each tool call, via a script, the better. The better the API surface area we expose to the LLM, the greater the win.</p><blockquote>The job of a context layer is partly to expose interfaces that LLMs can code against.</blockquote><h4>Who Builds the Context Layer?</h4><p>When an essential new architectural layer appears, everyone needs it, so they build it themselves or get someone to build it for them. It then becomes apparent that the problems it solves are generic, and that in-house build is not a competitive advantage, but a liability.</p><blockquote>Important new architectural layers inevitably become products. The context layer is a product category, not an in-house build.</blockquote><p>We’re seeing this play out now. The industry is accepting as normal armies of <em>FDEs</em> (Forward Deployed Engineers)— the fashionable AI-speak for <em>consultant</em>. This is a classic indication of a missing or ill-fitting product, where everyone is thrashing around trying to build something that they should find off the shelf.</p><p>We’ve seen this movie before. For example, in the early years of enterprise Java, every company built its own Java framework, trying to solve generic problems that had nothing to do with their business. Spring and Hibernate proved the immense value of common solutions the industry could converge on, while end developers focused on on solving their unique business problems.</p><p>We’re presently seeing two classes of vendor offer context layers:</p><ul><li><strong>Vendors trying to expose their own services</strong>, such as Microsoft, Salesforce and Workday</li><li><strong>Infrastructure providers</strong> such as Palantir and Glean, who help the customer work with multiple systems</li></ul><p>The latter vendor category is better aligned with the interests of customers. The context layer should span more than one vendor’s products, embracing internal systems and whatever the customer cares about.</p><p>However, customers deserve better than present context layers, which are often difficult and expensive to deploy. So we built a better solution.</p><h4>Introducing Embabel Worlds</h4><p>Today we’re announcing early access to <a href="http://worlds.embabel.com">Embabel Worlds</a>, our innovative context layer built on the JVM, on top of the Embabel agent framework.</p><blockquote>Embabel Worlds is a layer above your business that understands it and exposes it to however you want AI to work with it.</blockquote><p>Embabel Worlds has a unique approach, which we feel has major advantages:</p><ul><li><em>It’s dynamically extensible</em>: Each user can have their own <strong>world</strong> — their own extensible knowledge graph.</li><li><em>It’s contribution based </em>— worlds can be extended by <strong>realms</strong>. Each realm exposes a system or capability, and realms can be provided by us, third parties or authored by any user.</li><li><em>It emphasizes a declarative query language</em>. Queries are often the best way to ground LLM responses.We chose Cypher — the graph query language — as it is well suited to the dynamic navigation required, and LLMs are good at generating it.</li><li><em>It is federated</em> over whatever you need to work with.</li><li>It’s <em>easy to roll out</em>. You won’t need an army of FDEs — you can largely self serve.</li><li><em>It unifies RAG and all other functionality </em>through the same query language, offering <a href="https://medium.com/@springrod/rethinking-rag-pipelines-are-the-past-agentic-is-the-future-77c887414621">modern agentic RAG</a>.</li><li><em>It’s AI first from top to bottom — </em>not just<em> </em>in the surface the context layer provides, but in how it’s meant to be extendedvia first-class support for coding agents.</li><li>In a rapidly moving area, Embabel Worlds is not encumbered by legacy, hence <em>its surface is based around code mode</em>. LLMs can safely code against your dynamic knowledge graph, exposed via graph schema or TypeScript interfaces</li></ul><p>With Embabel Worlds, your knowledge graph can be extended through anything that matters to you, such as:</p><ul><li>Databases</li><li>APIs, including SaaS APIs and your internal systems</li><li>Tabular data</li><li>Anything you can imagine</li></ul><p>Embabel Worlds enables you to create a range of reusable artifacts that increase determinism and reduce cost, such as:</p><ul><li><strong>Views</strong>: Reusable Cypher queries that can be watched for deltas.</li><li><strong>Agents</strong>: Intelligent componentsthat can respond to events, chat with users and execute workflows.</li><li><strong>Apps</strong>: Custom UIs that users can safely vibe code on our infrastructure.</li></ul><p>Embabel Worlds is commercially licensed. We’re still working on pricing, but it will be free for individual use.</p><h4>Code Mode In Action</h4><p>What does it mean to script your business?</p><p>Let’s consider two examples: one a declarative query, the other in TypeScript.</p><p>First, a query across multiple systems. A support desk knows how many replies a case has, but not what they mean. A 42-reply thread about dark mode is a popular feature request, while a 7-reply thread from a customer who cannot close the month is an account about to leave. No count can distinguish between them, and keyword matching will be unreliable.</p><p>This query returns a row for each open support case, prioritizing and including two model-judged labels: how much the problem is hurting the customer’s business now, and how they currently feel about us.<br> <br>Mood is judged on the last three things the customer wrote, because the question is how they feel now, not how they felt when they opened the case.</p><p>Everything here is standard Cypher (making it easy for an LLM to generate), although reliant on the Embabel virtual Cypher engine to federate API calls, manage caching and execute the model calls.</p><pre>MATCH (d:ChatwootDesk {status:&#39;open&#39;})-[:HAS_CASE]-&gt;(c:ChatwootConversation)                                                                     <br>  WITH c, c.accountKey AS domain                                                                                                                   <br>  ORDER BY c.replyCount DESC                                                                                                                       <br>  LIMIT 150                                                                                                                                        <br>  MATCH (a:CustomerAccount {accountKey: domain})                                                                                                   <br>  WITH a, c                                                                                                                                        <br>  MATCH (c)-[:HAS_MESSAGE]-&gt;(m:SupportMessage)                                                                                                     <br>  WHERE m.messageType = 0 AND NOT coalesce(m.internal, false)                                                                                      <br>  WITH a, c, m                                                                                                                                     <br>  ORDER BY m.sentAt, m.id                                                                                                                          <br>  WITH a, c, collect(m.body) AS wrote                                                                                                              <br>  WITH a, c, size(wrote) AS customerMessages,                                                                                                      <br>       reduce(t = &#39;&#39;, x IN wrote[0..1] + wrote[1..][-7..] | t + &#39; | &#39; + x) AS said,                                                                <br>       reduce(t = &#39;&#39;, x IN wrote[-3..] | t + &#39; | &#39; + x) AS saidLately                                                                              <br>  WITH a, c, customerMessages,                                                                                                                     <br>       classify(said, &#39;blocked,degraded,inconvenienced,asking&#39;,                                                                                    <br>         &#39;Each item is what one business customer wrote across a support case about a software product: their messages oldest first, separated by  <br>  |. Judge how much the problem hurts THEIR business NOW. A later message outweighs an earlier one: a workaround or a fix reported mid-thread      <br>  lowers the impact, and a report that it got worse raises it. blocked: something they rely on does not work and they have no workaround. degraded:<br>  it works badly or intermittently, or a workaround is costing them. inconvenienced: a nuisance or cosmetic fault. asking: a question, or a        <br>  request for something that does not exist yet.&#39;) AS impact,                                                                                      <br>       classify(saidLately, &#39;frustrated,neutral,positive&#39;,                                                                                         <br>         &#39;Each item is the most recent thing one business customer wrote on a support case, up to three messages oldest first, separated by |.     <br>  Judge the tone of the writer toward the vendor NOW from the words actually used, not from how serious the problem is.&#39;) AS mood <br>RETURN a.accountKey AS accountKey, a.name AS account, c.id AS caseId, c.subject AS subject,                                                      <br>         c.priority AS priority, c.replyCount AS replies, customerMessages, c.openedOn AS openedOn,                                                <br>         impact, mood                                                                                                                              <br>  ORDER BY CASE impact WHEN &#39;blocked&#39; THEN 0 WHEN &#39;degraded&#39; THEN 1 WHEN &#39;inconvenienced&#39; THEN 2 ELSE 3 END,                                       <br>           CASE priority WHEN &#39;urgent&#39; THEN 0 WHEN &#39;high&#39; THEN 1 WHEN &#39;medium&#39; THEN 2 ELSE 3 END,                                                  <br>           CASE mood WHEN &#39;frustrated&#39; THEN 0 WHEN &#39;neutral&#39; THEN 1 ELSE 2 END,                                                                    <br>           replies DESC</pre><p>This is intelligence made toolable, understandable and reusable.</p><p>Ordering falls naturally out of using a full featured query language with ORDER BY.The query joins with the CRM to find the canonical customer identification. A focused prompt is used for the two invocations of the classify function.</p><p>In the Embabel Worlds query studio, results will look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4Q9X_qnUP-0dS4005yPEnw.png" /></figure><p>Such queries are typically saved as reusable views and used to inform LLM responses or agent actions.</p><p>TypeScript scripts work against dynamic, user-specific generated interfaces. Scripts can issue Cypher queries and work with exposed methods, whether on returned entities, as below, or defined elsewhere in a realm:</p><pre>const suppliers = await world.query(`<br>  MATCH (s:Supplier)-[:BILLED_BY]-&gt;(i:Invoice)<br>  WHERE i.disputed IS NULL RETURN s, i`);<br><br>for (const s of suppliers) {<br>  if (s.exposure() &gt; 250_000 &amp;&amp; s.is(Watchable)) {<br>    await s.watch({ reason: &#39;exposure over threshold&#39; });<br>    await s.owner().notify({ template: &#39;exposure-review&#39; });<br>  }<br>}</pre><p>All code artifacts are validated before being executed. TypeScript scripts are executed in a sandbox.</p><p>Being able to script across the business provides significant benefits:</p><ul><li><em>LLMs (or humans) can easily author queries and scripts</em>, as they’re in familiar languages.</li><li><em>Scripts can be reasoned over by people and tools </em>far more easily than an improvised sequence of MCP tool calls.</li><li><em>Scripts can be saved and reused</em>. When they are LLM-authored, this provides a way to compile LLM intelligence. For example, a script may be generated when a frontier model is available, but executed with a lesser model or without the need for any model.</li><li><em>Scripts naturally decompose a problem into multiple steps</em>: a key requirement for making AI usage as deterministic as possible. Many scripts don’t need any AI model at runtime— the cheapest and best option, where available. Those that <em>do</em> need an LLM — like the classification steps in the Cypher example — can often be executed by a smaller, or specialized model.</li></ul><h4>Evaluate Today</h4><p>You can download the early access today, and connect your internal systems to it behind your firewall. Go to <a href="http://worlds.embabel.com">worlds.embabel.com</a>. If you have Docker Desktop installed already, you’ll be up and running in under 10 minutes.</p><p>The industry has become much too tolerant of high touch product onboarding. While we understand that early users may need some hand-holding, we aim to ensure that users can ultimately self service.</p><h4>Embabel Worlds and Java</h4><p>Embabel worlds builds on the JVM and on our <a href="https://github.com/embabel/embabel-agent">embabel-agent</a> and other open source projects. However, users do not deploy code in Java or any other language to an Embabel Worlds server. You don’t need to write code to expose your custom applications to Embabel Worlds.</p><p>However, if you are a Java developer, you can use your skills to make Worlds more efficient, by adding behavior and metadata inside your apps to ensure that they are the most performant possible realms. See <a href="https://github.com/embabel-worlds/publisher">https://github.com/embabel-worlds/publisher</a> for more information, and our <a href="https://github.com/embabel-worlds/spring-petclinic">example of AI over the Spring Pet Clinic</a>. For example, a declaration in the OwnerService exposes a search operation, specifying pagination characteristics:</p><pre>@RealmQuery(maxPageSize = 200)<br> public Page&lt;OwnerView&gt; search(OwnerSearch search, Pageable pageable) {<br>  String prefix = (search.lastNamePrefix() != null) ? search.lastNamePrefix() : &quot;&quot;;<br>  return this.owners.findByLastNameStartingWith(prefix, pageable).map(OwnerView::of);<br> }</pre><p>As the Pet Clinic does not have a REST API, this provides a direct, performant, Embabel-ready alternative.</p><p>You can also expose actions you’ve built using embabel-agent.</p><h4>A New Layer for a New Era</h4><p>We are early in the most exciting era in our industry’s history. We’re now starting to see the shape of the infrastructure it requires. LLMs made the revolution possible, yet are ultimately fungible. Agent harnesses are now widely recognized as necessary; context layers will ultimately prove more important. With Embabel Worlds we believe we’ve brought something unique and compelling, and we’d love you to join us in the journey to making it one of the defining technologies of our time.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=616ccde93533" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Agent Memory Is Not A Greenfield Problem: Ground it in your Existing Data]]></title>
            <link>https://medium.com/embabel/agent-memory-is-not-a-greenfield-problem-ground-it-in-your-existing-data-9272cabe1561?source=rss-de5947672915------2</link>
            <guid isPermaLink="false">https://medium.com/p/9272cabe1561</guid>
            <category><![CDATA[genai]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Rod Johnson]]></dc:creator>
            <pubDate>Fri, 20 Feb 2026 10:01:27 GMT</pubDate>
            <atom:updated>2026-02-21T01:32:05.192Z</atom:updated>
            <content:encoded><![CDATA[<p>LLMs must be grounded to avoid hallucinations and access factual content and real time data. <a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">RAG</a> tackles one part of the problem; agent memory another.</p><p>However, the dominant approach to agent memory is deeply flawed. Memory systems such as <a href="https://mem0.ai/">Mem0</a>, <a href="https://www.getzep.com/">Zep</a>, <a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html">AWS AgentCore Memory</a>, and Google <a href="https://docs.cloud.google.com/agent-builder/agent-engine/memory-bank/overview">Vertex Memory Bank</a> start from the assumption that agent memory should be built from scratch by extracting facts from conversations.</p><p>This rests on two dangerous fallacies. It overemphasizes chat (<a href="https://sublight.substack.com/p/chat-interfaces-are-bad">a common mistake)</a>, and neglects the critical importance of <a href="https://medium.com/@springrod/on-memory-why-ai-agents-risk-forgetting-what-business-already-knows-51b2c902e91c">how data is already stored and managed</a>, and the necessity of integrating with it.</p><p>Even <a href="https://www.ibm.com/think/topics/ai-agent-memory">IBM’s article on agent memory</a> reflects these fallacies, talking about agent memory in a vacuum. If <em>IBM</em>, of all companies, can’t get their head around the relationship between agents and existing enterprise systems, the <a href="https://fortune.com/2025/08/18/mit-report-95-percent-generative-ai-pilots-at-companies-failing-cfo/">disastrous results of enterprise AI rollouts</a> become more understandable. It is also less surprising when we consider that, to this point, Gen AI has largely been driven by people with little interest in or understanding of the enterprise systems that power our everyday life.</p><p><a href="https://github.com/embabel/embabel-agent">Embabel</a> introduces a different approach, reflecting the understanding that Gen AI can only succeed in enterprise through close integration with valuable existing assets.</p><p>In previous blogs I’ve introduced <a href="https://medium.com/@springrod/context-engineering-needs-domain-understanding-b4387e8e4bf8">Domain Integrated Context Engineering</a> (<strong>DICE</strong>).</p><p>Today I’ll explain how we approach these concepts in the Embabel <a href="https://github.com/embabel/dice">Dice module</a>.</p><p>In my next blog I’ll show how Dice can be used in a chatbot. Head to the <a href="https://github.com/embabel/urbot">urbot repository </a>right away if you’re impatient to see working code.</p><h3>Agent Memory Today</h3><p>Agent memory approaches draw heavily on cognitive science. <a href="https://en.wikipedia.org/wiki/Endel_Tulving">Endel Tulving</a>’s distinction between <strong>episodic memory</strong> (specific events and experiences) and <strong>semantic memory</strong> (general facts and knowledge) <a href="https://alicekim.ca/12.EpSem72.pdf">dates from 1972 </a>and remains a useful framework for what agents need to remember. Nearly every system in this space models these categories.</p><p>However, cognitive science helps with only part of the problem. Agent memory approaches are overwhelmingly text based, lacking the structure core to proven business systems.</p><p>Mem0 extracts memories into vector stores. AgentCore stores semantic facts and user preferences. Google Memory Bank uses Gemini to pull key details from session history. All treat memory as a greenfield concern: text in, embeddings out, similarity search to retrieve.</p><blockquote>Freeform text is opaque and costly to work with. LLMs and vector search are slow and unpredictable. Cheaper approaches like BMI25 or regex searching are imprecise and offer limited capability. The precision, performance and predictability of a database query is lacking.</blockquote><p>A more sophisticated approach is to use a <strong>knowledge graph</strong>, as in the various takes on <a href="https://neo4j.com/blog/genai/what-is-graphrag/">Graph RAG</a>.</p><p>Zep builds a temporal knowledge graph, ingests structured JSON business data alongside conversations, and recently added custom entity types. But even Zep treats business data as something you <em>push into</em> its graph — a JSON snapshot of a customer record is ingested, extracted, and stored as a Zep entity node copy.</p><blockquote>None of these solutions link<strong> to real domain objects.</strong></blockquote><p>Existing businesses aren’t starting from scratch.</p><p>You already have a Customer entity. It probably lives in a relational database. It likely has a Spring Data repository, and a service layer above it. It has validation rules, business logic, and relationships to Order and Product entities, backed by years of careful domain modelling and rigorous testing.</p><p>Pure text solutions have no solid way of linking between a memory and a customer. Even with Zep, when it ingests a customer JSON document, it creates a <em>new</em> node in its graph, which doesn’t know about the JPA entity it came from. It can’t call existing business logic or enforce established validation rules. It doesn’t update when the real entity changes. It’s a text-and-embedding snapshot of something that was once a rich domain object, encapsulating behaviour.</p><blockquote>Even in genuine greenfield scenarios, data will build up over time and the more structure the better.</blockquote><h3>Grounding Memory in Existing Data</h3><p>How <em>should</em> we think about memory for business applications in an ideal world?</p><h4>A simple thought experiment</h4><p>What memories does my bank have about me? They probably have some text notes about my patterns and preferences that staff can retrieve, but these are far from the most valuable things they know about me. <em>Those</em> are highly structured, such as my account types and balances, payment history, and address. They are updated in structured ways, rather than through loose text. And they are definitely <em>not</em> in a vector database — although it’s now likely that they’re in a database that has a vector capability. The most valuable information doesn’t relate closely to cognitive science thinking about memory.</p><blockquote>A mature memory solution must consider existing structured data along with free-form text, and must attempt to bring as much structure as possible to memory capture.</blockquote><h4><strong>What if memories could reference your existing entities?</strong></h4><p>What if, instead of copying data into a fresh graph, we link to existing data <em>in whatever store it is held. </em>What if this also enables us to benefit from the business logic around it?</p><p>An entity node in the knowledge graph that holds a typed link to the real Customer record in your RDBMS. A PREFERS relationship that the agent discovered in conversation — but pointing to a Cuisine entity your recommendation engine already knows about. Memory as a <em>projection over</em> knowledge the business already has, not a parallel store that duplicates it.</p><p>This profoundly improves how memory works in practice:</p><ul><li><strong>Agents don’t start from zero</strong>. On day one, every agent has access to what your domain model already knows — purchase history, loyalty tier, dietary restrictions — through real entity references, not extracted text approximations. Visibility can be limited for safety.</li><li><strong>Contradictions become visible</strong>. If the agent extracts “user prefers Nike” but your transaction data shows they’ve bought Adidas exclusively for three years, the graph surfaces that tension because both the proposition and the domain entity are present and connected.</li><li><strong>Business logic stays where it belongs</strong>. Your validation rules, authorization checks and repository methods are accessible through the actual entity, not reimplemented in the memory layer.</li></ul><figure><img alt="Agent memory is not a greefield problem" src="https://cdn-images-1.medium.com/max/1024/1*fp8ALdYXdfVbNfkKyoAhJA.jpeg" /><figcaption>Not a greenfield problem</figcaption></figure><h3>The problem with hosted services</h3><p>AWS AgentCore Memory and Google Memory Bank are managed cloud services. Your agent’s memories live in <em>their</em> infrastructure. This is fine for many users, but problematic in enterprise, when domain entities are in databases behind firewalls, accessed by Java applications — and agent memory is in a separate hosted service that can’t access them.</p><p>If you don’t care too much about data sovereignty and security, it may be possible to serialize your business data, ship it across a network boundary to someone else’s memory store, and hope the LLM-powered extraction pipeline captures what matters. You now have two sources of truth: your actual domain model and a lossy, eventually-consistent text approximation of it sitting in a managed service. When your Customer record updates, the memory service doesn&#39;t know. When your business rules change, the memory service doesn&#39;t care. You&#39;re paying for the privilege of maintaining a stale copy of knowledge you already have.</p><p>This is architecturally backwards. <strong>Memory needs to be co-located with the entities it references, </strong>not across a network behind an API boundary. It should be in process, with direct access to your repositories, your domain logic, and your data.</p><p><strong>This is where the JVM has a huge advantage.</strong></p><p>An agent memory system running on the JVM doesn’t need to call an external API to learn about your customers. It can simply call its own services and repositories, benefiting from their business logic and tested behaviour. Nor does it need to go through MCP, when it can find answers faster in process. It can access the same CustomerRepository your application already uses. It can resolve a proposition like &quot;the user prefers Italian food&quot; against the actual Customer entity and the actual Cuisine entity your recommendation engine already depends on — in the same process, with the same transaction guarantees, through the same Spring context.</p><p>The memory system and the domain model are one, running together, sharing the same type system and the same runtime. While we notionally have a knowledge graph (for which Neo4j is a brilliant store), data tends to stick where it lands, and the approach supports multiple stores.</p><p>Python-based memory frameworks can’t do this — not because Python is a bad language, but because the enterprise domain model doesn’t live in Python, but in the JVM.</p><blockquote>A Python memory service will always be on the outside looking in, calling APIs to get approximations of what your Java code already knows directly.</blockquote><p>Existing frameworks build memory as if the business starts knowing nothing — and as if memory should live somewhere other than where your business logic runs.</p><p>What’s needed is a memory system that treats what your business already knows as the foundation, conversation as something that extends it, and the JVM as exactly the right place for all of it to live.</p><p>That’s what Embabel Dice aims to provide.</p><h3><strong>Learning from Experience and Research</strong></h3><p>The Dice project is based on internal knowledge graph work we’ve done at Embabel over the last two years, as well as thorough exploration of the state of the art in memory solutions.</p><p>Among the influences, <a href="https://neo4j.com/labs/genai-ecosystem/llm-graph-builder/">Neo Technology’s LLM Knowledge Graph Builder</a> demonstrated that LLMs can extract entities and relationships from unstructured text into a knowledge graph — documents to chunks to entities to relationships, with schema-guided extraction. It doesn’t address what happens when those extracted entities correspond to things your business already knows about in existing databases, or how to apply this to memory versus RAG.</p><p>We owe a particular debt to the <a href="https://arxiv.org/abs/2505.10831">General User Models (GUM) </a>research from Stanford and Microsoft. GUM showed that you can build rich user models from unstructured observations using confidence-weighted propositions — atomic statements about a user, each carrying a confidence score and a decay rate, maintained through a pipeline of proposal, retrieval, and revision. The insight that propositions are the right atomic unit for extracted knowledge, and that they should carry calibrated uncertainty, directly informs our approach.</p><p>But GUM’s propositions are unstructured. They’re natural language strings in a vector store. There’s no schema constraining them, no formal ontology connecting them, and no mechanism for linking a proposition like “the user loved the trip to Italy last year” to a Trip entity in an existing business system.</p><p>Our approach in Dice takes GUM’s proposition model and asks:</p><ul><li>What if propositions could be <em>promoted</em> to typed relationships in a knowledge graph when confidence is high enough and a matching schema exists?</li><li>What if entity nodes in that graph could hold references to real domain objects in existing data stores?</li><li>What if memory, knowledge graph, and domain model were three projections of the same underlying system of record?</li></ul><p>The result combines Tulving’s memory taxonomy, GUM’s confidence-weighted proposition, and Neo4j’s knowledge graph extraction. Further, promotion is generalized so that propositions can be projected not just to a Neo knowledge graph, but to other representations, including an experimental Prolog fact base.</p><p><strong>The most important, novel piece: integration with existing, typed, production domain entities that the business already trusts</strong>.</p><h3>Memory is an Outgrowth of Your application</h3><p>The memory problem is a symptom of something larger. The AI agent ecosystem has been built on a premise that enterprise should reject: that AI is a separate stack, a new silo, something you bolt onto your business from the outside.</p><blockquote>AI should grow out of your application, not land on it from space.</blockquote><p>This explains the many failures. Not because AI doesn’t work, but because the approach is wrong. Companies are standing up parallel infrastructure — Python services, vector databases, hosted memory layers, prompt chains — that have no organic connection to the systems that actually run their business. The AI doesn’t know your domain. It doesn’t understand your constraints. It doesn’t share your type system, transaction boundaries or security model. It’s an alien stack that you spend months trying to integrate, and mostly fail.</p><p>What if AI agents grew <em>out of</em> your existing application instead?</p><p>That’s the bet we’re making with Embabel. Not a Python framework that hopes to connect to your enterprise. A JVM-native agent platform that lives inside it. Your domain entities become the agent’s world model. Your Spring services and repositories furnish it with tools. Your type system becomes its contract for correctness. Memory isn’t a separate service — it’s a structured projection over what your application already knows, extended by what the agent learns from interaction.</p><p>This goes beyond memory. Embabel uses <a href="https://medium.com/@springrod/ai-for-your-gen-ai-how-and-why-embabel-plans-3930244218f6">Goal-Oriented Action Planning</a> — deterministic, inspectable plans built from type-safe preconditions and effects, not probabilistic prompt chains that you can’t explain to an auditor. Actions operate on your domain types. The planner reasons about your domain state. Every step is traceable, testable, and grounded in the same business logic your organization already trusts.</p><p>Python agent frameworks make nice demo infrastructure for greenfield AI experiments. We’re building infrastructure for AI that works inside the enterprise systems that already exist. Those are not the same thing, and the 95% failure rate tells you which one the industry actually needs.</p><blockquote>Enterprise AI doesn’t fail because models aren’t smart enough. It fails because frameworks either hope that enterprise systems will magically go away, or treat the them as an integration problem instead of a valuable starting point. Your domain model, your business rules, your data — these aren’t obstacles to work around. They’re the most valuable assets an agent can have.</blockquote><h3>Embabel DICE</h3><p><strong>We call this approach DICE — Domain Integrated Context Engineering.</strong> It’s still early. We’re building the proposition-to-entity promotion pipeline, confidence-weighted memory projections and schema-guided extraction linking to live domain objects in the open. There’s a way to go before it’s hardened. But the architecture is clear, the foundations are solid, and the direction is right.</p><p>We believe DICE will be the m emory solution that helps change enterprise Gen AI from its current failure mode to something that actually delivers. Not by making LLMs smarter, but by giving them something the industry has overlooked: the structured domain knowledge that enterprises have spent decades building.</p><blockquote>The future of enterprise AI isn’t a smarter hosted memory service. It’s agents that speak your domain language because they were born inside your application.</blockquote><p>That’s the vision Embabel exists to make real. And if this resonates with how you think about enterprise AI — if you’ve felt the frustration of trying to bolt a Python demo onto a real business system — we’d love you to join us on this journey. Star the <a href="https://www.github.com/embabel/dice">repo</a>, try <a href="https://www.github.com/embabel/embabel-agent">the framework</a>, help us refine the ideas. The best enterprise AI infrastructure won’t be built in isolation. It’ll be built by the people who understand and are currently delivering what enterprise actually needs.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9272cabe1561" width="1" height="1" alt=""><hr><p><a href="https://medium.com/embabel/agent-memory-is-not-a-greenfield-problem-ground-it-in-your-existing-data-9272cabe1561">Agent Memory Is Not A Greenfield Problem: Ground it in your Existing Data</a> was originally published in <a href="https://medium.com/embabel">Embabel</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Power of Russian Dolls: Rethinking Tool Discovery]]></title>
            <link>https://medium.com/@springrod/the-power-of-russian-dolls-rethinking-tool-discovery-1e79eba1543a?source=rss-de5947672915------2</link>
            <guid isPermaLink="false">https://medium.com/p/1e79eba1543a</guid>
            <category><![CDATA[genai]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[spring-boot]]></category>
            <dc:creator><![CDATA[Rod Johnson]]></dc:creator>
            <pubDate>Mon, 19 Jan 2026 07:21:55 GMT</pubDate>
            <atom:updated>2026-01-19T07:21:55.805Z</atom:updated>
            <content:encoded><![CDATA[<p>Tool calling is central to nearly every use of Gen AI, powering <a href="https://medium.com/@springrod/rethinking-rag-pipelines-are-the-past-agentic-is-the-future-77c887414621">agentic RAG</a>, exposing the functionality of existing systems and domain models, and enabling a wide range of integrations.</p><p>Today’s LLMs are trained for tool calling and are good at it. However, tool descriptions can consume a significant portion of the context window, and any LLM can struggle to determine which tools to call. <a href="https://www.anthropic.com/engineering/advanced-tool-use">Testing at Anthropic</a> showed that Opus 4 scored only 49% accuracy on MCP evaluations when working with large tool libraries: essentially a coin toss.</p><p><strong>Thus tool discovery is a core problem that agent frameworks need to address in order to help developers build more efficient and reliable applications.</strong></p><p><a href="https://github.com/embabel/embabel-agent">Embabel</a> now solves this problem in a particularly elegant way. The innovation: Like the famous <em>matryoshka</em> Russian dolls, Embabel can organize tools into a hierarchy where each level exposes a single tool that opens up the level below. The result: <strong>You can use as many tools as you want without using an excessive number of tokens or sacrificing reliability</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jJ0T9YsUToeEJSAo5lxFUQ.jpeg" /><figcaption>Matryoshka for your LLM</figcaption></figure><h3>Existing Tool Discovery Mechanisms</h3><p>Any agent framework mitigates the tool discovery problem by breaking flows into steps that can use local tools. In many workflows this sidesteps the problem altogether. But there remain scenarios such as chatbots where sufficient decomposition isn’t possible.</p><p>Let’s examine the prior art.</p><h4>LangChain / LangGraph: Vector Similarity Search</h4><p>As you’d expect, LangGraph addresses the tool proliferation problem in the most obvious way — through vector similarity search. Tool descriptions are embedded and stored in a vector store, and tools are matched to user queries via semantic search:</p><pre># Source: https://langchain-ai.github.io/langgraph/how-tos/many-tools/<br>tool_documents = [<br>    Document(<br>        page_content=tool.description,<br>        id=id,<br>        metadata={&quot;tool_name&quot;: tool.name},<br>    )<br>    for id, tool in tool_registry.items()<br>]<br><br>vector_store = InMemoryVectorStore(embedding=OpenAIEmbeddings())<br>document_ids = vector_store.add_documents(tool_documents)</pre><p>This introduces additional infrastructure and latency. Furthermore, semantic similarity isn’t always the right criterion for tool selection.</p><h4>LangChain4j: Dynamic Tool Provider</h4><p>LangChain4j has a <a href="https://docs.langchain4j.dev/tutorials/ai-services/">ToolProvider interface</a> that allows runtime tool selection based on the incoming message:</p><pre>// Source: https://github.com/langchain4j/langchain4j/issues/637<br>Assistant assistant = AiServices<br>    .builder(Assistant.class)<br>    .chatLanguageModel(model)<br>    .toolProvider(request -&gt; {<br>        if (request.userMessage().singleText().contains(&quot;booking&quot;)) {<br>            return ToolProviderResult.builder()<br>                .add(bookingToolSpecification, new BookingToolExecutor())<br>                .build();<br>        }<br>        return null;<br>    })<br>    .build();</pre><p>The Embabel equivalent is the common pattern of building a dynamic list of tools to add to a PromptRunner when we’re about to make an LLM call. However, it’s not a complete solution to the tool discovery problem.</p><h4>Claude Tool Search Tool: Deferred Loading</h4><p>Anthropic recently introduced the <a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool">Tool Search Tool</a> for Claude Code and the API. This marks tools with defer_loading: true so they remain discoverable without consuming context tokens at session start. Claude then discovers relevant tools on-demand via a search mechanism.</p><pre>// Source: https://www.anthropic.com/engineering/advanced-tool-use<br>{<br>  &quot;tools&quot;: [<br>    {&quot;type&quot;: &quot;tool_search_tool_regex_20251119&quot;, &quot;name&quot;: &quot;tool_search_tool_regex&quot;},<br>    {<br>      &quot;name&quot;: &quot;github.createPullRequest&quot;,<br>      &quot;description&quot;: &quot;Create a pull request&quot;,<br>      &quot;input_schema&quot;: {...},<br>      &quot;defer_loading&quot;: true<br>    }<br>  ]<br>}</pre><p>Anthropic <a href="https://www.anthropic.com/engineering/advanced-tool-use">reports</a> an 85% reduction in token usage — from ~134k to ~5k tokens in internal testing — while maintaining full tool access. This is a significant improvement, with accuracy on MCP evaluations jumping from 49% to 74% for Opus 4 and from 79.5% to 88.1% for Opus 4.5.</p><p>However, this is still a “search-then-select” approach, introducing an additional inference step to discover tools before using them.</p><h3>A Better Solution: Tool Hierarchy</h3><p>You can use all these approaches with Embabel. But we can do better.</p><p>Instead of searching through tools, we <strong>organize</strong> them into a hierarchy. This can be done by the tool developer or by the developer building the agent — who of course may be the same person.</p><p>Each <strong>matryoshka tool</strong> acts as a facade: when invoked, it’s replaced in the message history by its children. This lets the LLM navigate a tree of capabilities, drilling down only into the areas relevant to the current task.</p><h4>The Unbeatable Math of Binary Search</h4><p>Consider the difference. With 100 tools using flat selection:</p><ul><li>The LLM must process 100 tool descriptions</li><li>Token consumption scales linearly: O(n)</li><li>Selection accuracy degrades with n</li></ul><p>With hierarchical tools organized into10 groups of 10:</p><ul><li>First level: 10 facade tools</li><li>After selection: 10 actual tools</li><li>Maximum tools considered: 20</li><li>Token consumption: O(log n)</li><li>Selection accuracy remains high at each level</li></ul><p>You can’t beat the math of logarithmic scaling. Embabel apps can offer an <strong>unlimited number of tools</strong> to any LLM interaction while keeping each selection step manageable.</p><h3>Code Example: Spotify Integration</h3><p>Here’s how this looks in practice with the <a href="https://github.com/embabel/impromptu/blob/main/src/main/java/com/embabel/impromptu/integrations/spotify/SpotifyTools.java">Spotify integration in our Impromptu</a> classical music exploration chatbot:</p><pre>@MatryoshkaTools(<br>    name = &quot;spotify&quot;,<br>    description = &quot;Access Spotify music features. Invoke this tool to enable &quot; +<br>        &quot;Spotify operations like playing music, searching tracks, &quot; +<br>        &quot;managing playlists, and controlling playback.&quot;<br>)<br>public record SpotifyTools(<br>    ImpromptuUser user,<br>    SpotifyService spotifyService<br>) {<br><br>    @LlmTool(description = &quot;Check if user has linked their Spotify account&quot;)<br>    public String checkSpotifyStatus() {<br>        // Implementation<br>    }<br><br>  // 10 other tools...</pre><p>The @MatryoshkaTools annotation transforms this class into a hierarchical tool. Initially, the LLM sees only the top level description:</p><pre>spotify: Access Spotify music features. Invoke this tool to enable Spotify operations...</pre><p>When the user says “play some Brahms,” the LLM invokes thespotifyparent tool. This facade is replaced by all the child tools: checkSpotifyStatus, getPlaylists, searchTracks, playTrack, etc. Now the LLM can select playTrack and proceed.</p><h3>Scaling with Multiple Integrations</h3><p>The power becomes clear when you have many integrations. In the Impromptu chatbot, we have matryoshka tools for several concerns.</p><pre>// Source: https://github.com/embabel/impromptu/blob/main/src/main/java/com/embabel/impromptu/integrations/youtube/YouTubeTools.java<br>@MatryoshkaTools(<br>    name = &quot;youtube&quot;,<br>    description = &quot;Access YouTube to play or search for videos&quot;<br>)<br>public record YouTubeTools(...) { /* 5 tools */ }<br><br><br>@MatryoshkaTools(<br>    name = &quot;metmuseum&quot;,<br>    description = &quot;Access the Metropolitan Museum of Art collection. &quot; +<br>        &quot;Invoke this tool to search artworks, get artwork details, &quot; +<br>        &quot;explore departments, and discover highlighted works...&quot;<br>)<br>public record MetMuseumTools(...) { /* 6 tools */ }<br><br><br>@MatryoshkaTools(<br>    name = &quot;imslp&quot;,<br>    description = &quot;Access IMSLP (International Music Score Library Project) &quot; +<br>        &quot;to find and download public domain music scores...&quot;<br>)<br>public record ImslpTools(...) { /* 4 tools */ }<br><br>// more tools...</pre><p>Each integration adds just <strong>one tool</strong> to the top-level selection, regardless of how many operations it supports internally. The Spotify integration has 12 internal tools, YouTube has 5, Met Museum has 6, IMSLP has 4 — that’s 27 tools total, and the number will only grow. But the LLM only sees 4 facades initially, keeping selection simple and accurate.</p><h3>Benefit: Rich Tool Descriptions</h3><p>Another key benefit of this architecture is that <em>lower level tools can have as detailed descriptions as they need</em>. Since they’re only expanded occasionally, we don’t need to worry much about token consumption in the descriptions. This enables better tool use accuracy.</p><p>Compare these tool descriptions:</p><p><strong>Without hierarchy (must be brief):</strong></p><pre>findScore: Find a score on IMSLP</pre><p><strong>With hierarchy (can be comprehensive):</strong></p><pre>@LlmTool(description = &quot;&quot;&quot;<br>    Find a musical score on IMSLP given composer and work title.<br>    COMPOSER FORMAT: Use last name only (e.g., &#39;Brahms&#39;, &#39;Bach&#39;, &#39;Beethoven&#39;).<br>    IMSLP page titles use format: Work_Title_(Composer,_First_Last)<br>    WORK TITLE FORMAT: Use standard titles with proper spacing:<br>    - &#39;Symphony No. 5&#39; (not &#39;Symphony No.5&#39;)<br>    - &#39;Violin Sonata No. 1, Op. 78&#39; or just &#39;Violin Sonata Op. 78&#39;<br><br>   TROUBLESHOOTING - if no results:<br>    1. Simplify: Try just &#39;Violin Sonata&#39; instead of &#39;Violin Sonata No. 1 in G major&#39;<br>    2. Try searchWorks with a flexible query like &#39;Brahms violin sonata&#39;<br><br>  ALWAYS share the IMSLP page URL with the user - they can browse editions directly.<br>    &quot;&quot;&quot;)<br>public String findScore(String composer, String workTitle) {<br>    // ...<br>}</pre><p>Most tools don’t need this level of detail, but this one was needed to work around what had been a real reliability problem with a shorter description, due to the quirks of the IMSLP API. We would not want to waste this many tokens in interactions that didn’t call about score finding.</p><p>The hierarchical approach lets us invest in detailed, helpful descriptions that guide the LLM to correct usage — but only pay the token cost when that specific tool category is relevant.</p><h3>Conclusion</h3><p>Embabel makes it easy to expose <em>targeted</em> tools through selective addition to a PromptRunner. Two particularly import mechanisms for selection are:</p><ul><li>Exposing tools on previously retrieved entities.<a href="https://medium.com/@springrod/context-engineering-needs-domain-understanding-b4387e8e4bf8"> Easy integration with existing domain models</a> is a core goal of Embabel.</li><li>Matryoshka tools that conceal one or more layers of children.</li></ul><p>Together, Embabel enables you to work with as many tools as you want in your applications.</p><p>Still think Python agent frameworks are the state of the art?</p><p>Writing a good application framework is not a data science problem — it’s a software engineering problem. Type safety, deterministic execution, hierarchical organization, and enterprise-grade tooling matter.</p><p>Matryoshka tools demonstrate what’s possible when you approach the many-tools problem from first principles rather than reaching for the familiar “just add vector search” solution. The result is simpler, more predictable, and more scalable.</p><p><a href="https://github.com/embabel/embabel-agent">Check out Embabel today.</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1e79eba1543a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Chatbot with Embabel Agentic RAG]]></title>
            <link>https://medium.com/@springrod/building-a-chatbot-with-embabel-agentic-rag-b26a8346cb16?source=rss-de5947672915------2</link>
            <guid isPermaLink="false">https://medium.com/p/b26a8346cb16</guid>
            <category><![CDATA[java]]></category>
            <category><![CDATA[genai]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[rags]]></category>
            <category><![CDATA[spring-boot]]></category>
            <dc:creator><![CDATA[Rod Johnson]]></dc:creator>
            <pubDate>Wed, 14 Jan 2026 09:08:33 GMT</pubDate>
            <atom:updated>2026-01-17T21:38:14.299Z</atom:updated>
            <content:encoded><![CDATA[<p>In my <a href="/https/medium.com/p/rethinking-rag-pipelines-are-the-past-agentic-is-the-future">last blog</a>, I covered the advantages of agentic RAG over legacy pipelines. Let’s see how to build an agentic RAG chatbot with Embabel. You can find the complete code in the <a href="https://github.com/embabel/ragbot">Ragbot repository</a>.</p><h3>Responding to Messages</h3><h4>Chat Actions</h4><p>In Embabel there’s nothing special about a chatbot. It’s backed by Embabel’s core AgentProcess—a long-running process that can receive messages and respond to them over time. Thus the same action-based architecture that handles business workflows also handles chat interactions. Chat processes have a Conversation object in their blackboard, but nothing else about them is special. (Remember that Embabel doesn’t otherwise need to maintain a conversation thread, because it emphasizes structure over loose text.) Chat processes can also respond to incoming events other than user messages, which can help inform the conversation.</p><p>A chatbot action responds to an incoming UserMessage and sends one or more assistant messages. The simplest possible example:</p><pre>@Action(canRerun = true, trigger = UserMessage.class)<br>void respond(Conversation conversation, ActionContext context) {<br>    var assistantMessage = context<br>      .ai()<br>       withAutoLlm()<br>      .withSystemPrompt(&quot;You are a helpful assistant&quot;)<br>      .respond(conversation.getMessages());<br>    context.sendMessage(conversation.addMessage(assistantMessage));<br>}</pre><p>The @Action annotation with trigger = UserMessage.class means this action fires whenever a user message arrives. The canRerun = true flag allows it to run repeatedly. The ActionContext parameter provides access to the LLM through context.ai(), as well as the ability to send messages.</p><p>So little code looks cute, but we want something useful, rather than the millionth and first “you are a helpful assistant” toy demo.</p><p>Here’s the entire ChatActions class from Ragbot, with a realistic chat action:</p><pre>@EmbabelComponent<br>public class ChatActions {<br><br>    private final ToolishRag toolishRag;<br>    private final RagbotProperties properties;<br><br>    public ChatActions(<br>            SearchOperations searchOperations,<br>            RagbotProperties properties) {<br>        this.toolishRag = new ToolishRag(<br>                &quot;sources&quot;,<br>                &quot;Music criticism&quot;,<br>                searchOperations);<br>        this.properties = properties;<br>    }<br><br>    @Action(canRerun = true, trigger = UserMessage.class)<br>    void respond(Conversation conversation, ActionContext context) {<br>        var assistantMessage = context<br>                .ai()<br>                .withLlm(properties.chatLlm())<br>                .withReference(toolishRag)<br>                .withTemplate(&quot;ragbot&quot;)<br>                .respondWithSystemPrompt(conversation, Map.of(<br>                        &quot;properties&quot;, properties,<br>                        &quot;voice&quot;, properties.voice(),<br>                        &quot;objective&quot;, properties.objective()<br>                ));<br>        context.sendMessage(conversation.addMessage(assistantMessage));<br>    }<br>}</pre><p>The key additions are the use of a prompt template, and the call to .withReference(toolishRag). This gives the LLM access to RAG tools. The LLM decides when and how to search—true agentic retrieval rather than a blind pipeline.</p><h4>Managing Prompts with Templates</h4><p>Chatbot prompts tend to grow complex. You need persona definitions, guardrails, response guidelines, objective instructions. Embedding all this even in multi-line strings is painful.</p><p>When prompts get complex, Embabel makes it easy to use <a href="https://jinja.palletsprojects.com/en/stable/">Jinja</a> templates to externalize them. The main Ragbot template is built from reusable elements:</p><pre>{% include &quot;elements/guardrails.jinja&quot; %}<br>{% include &quot;elements/personalization.jinja&quot; %}<br><br>Keep your responses under {{ properties.voice().maxWords() }} words unless they<br>MUST be longer for a detailed response or quoting content.</pre><p>The personalization template dynamically loads persona and objective:</p><pre>============<br># Important: Your voice<br>{% set persona_template = &quot;personas/&quot; ~ properties.voice().persona() ~ &quot;.jinja&quot; %}<br>{% include persona_template %}<br>============<br><br>============<br># Important: Your objectives<br>{% set objective_template = &quot;objectives/&quot; ~ properties.objective() ~ &quot;.jinja&quot; %}<br>{% include objective_template %}<br>============</pre><p>The separation is useful. <strong>Voice</strong> defines <em>how</em> the chatbot communicates — its personality and tone. <strong>Objective</strong> defines <em>what</em> it should accomplish — the domain expertise and task instructions. You can mix and match: the same legal objective answered in the voice of Shakespeare, a sarcastic teenager, or a formal lawyer.</p><p>Here’s a simple persona (personas/music-guide.jinja):</p><pre>You are Sforzando, an enthusiastic guide to classical music.<br>You want to share your passion for this genre with others.<br>Your tone is friendly and engaging. You are not formal or academic. <br>You want to make classical music accessible and enjoyable for everyone.</pre><p>An objective (objectives/music.jinja):</p><pre>Answer questions about music critics in a clear and concise manner.<br>The tools available to you look into the archives of critics.<br>You must always use these tools to find answers, as your general knowledge <br>will not extend to everything they wrote.<br><br>Always back up your points with direct quotes from the criticism.<br>DO NOT RELY ON GENERAL KNOWLEDGE unless you are certain a better answer <br>is not in the provided sources.</pre><p>Ragbot allows switching personas in application.yml without touching code:</p><pre>ragbot:<br>  voice:<br>    persona: shakespeare  # Now responds in Elizabethan English<br>    max-words: 250<br>  objective: music</pre><h4>Configuring a Chatbot</h4><p>So that our action(s) are invoked, we need to configure a Chatbot.</p><p>Embabel’s <a href="https://en.wikipedia.org/wiki/Utility_system"><strong>Utility AI</strong></a> planning type is ideal for chatbots. Utility AI scores available actions and selects the most appropriate one for each situation. (An action can never be invoked unless its type requirements and other preconditions are satisfied, so type safety remains guaranteed.) For a simple chatbot with a single action, scoring isn’t needed. But it means you can easily add more actions later without restructuring your code. When a user message comes in, the platform will choose the highest value action at that moment.</p><p>We define a Spring bean that uses the AgentProcessChat helper class to create a utility chatbot:</p><pre>@Configuration<br>class ChatConfiguration {<br><br>    @Bean<br>    Chatbot chatbot(AgentPlatform agentPlatform) {<br>        return AgentProcessChatbot.utilityFromPlatform(agentPlatform);<br>    }<br>}</pre><p>The utilityFromPlatform() method automatically discovers all @Action methods in your @EmbabelComponent classes. Your chatbot can gain capabilities just by adding new actions.</p><h3>Configuring SearchOperations to back RAG</h3><p>We need to define the SearchOperations instance injected into ChatActions and provided to ToolishRag. Here we use the LuceneSearchOperations fluent API:</p><pre>@Configuration<br>@EnableConfigurationProperties(RagbotProperties.class)<br>class RagConfiguration {<br><br>    @Bean<br>    LuceneSearchOperations luceneSearchOperations(<br>            ModelProvider modelProvider,<br>            RagbotProperties properties) {<br>        var embeddingService = modelProvider.getEmbeddingService(<br>            DefaultModelSelectionCriteria.INSTANCE);<br>        return LuceneSearchOperations<br>                .withName(&quot;docs&quot;)<br>                .withEmbeddingService(embeddingService)<br>                .withChunkerConfig(properties.chunkerConfig())<br>                .withChunkTransformer(AddTitlesChunkTransformer.INSTANCE)<br>                .withIndexPath(Paths.get(&quot;./.lucene-index&quot;))<br>                .buildAndLoadChunks();<br>    }<br>}</pre><p>This creates a Lucene-backed text and vector store that persists to disk. The LuceneSearchOperations implements our SearchOperations interface, which ToolishRag wraps to expose fine-grained search tools to the LLM.</p><h3>Running The App</h3><p>Ragbot starts with Spring Shell.</p><p>Run ./scripts.shell.sh from the command line, or run RagbotApplication from your IDE.</p><p>Once it starts up, we need to ingest data, which will survive restarts.</p><p>The sample data is 19th and early 20th century music criticism. An obscure choice helps prove that the LLM is not just relying on general knowledge, which can be hard to do with familiar content. It also reflects the fact that I have a PhD on Romantic piano music, and am currently listening to <a href="https://www.youtube.com/watch?v=jq8oxIgyp8E">Glazunov’s wonderful violin concerto</a>.</p><p>To use the sample data, begin by running ingest, which will default to ingesting Robert Schumann’s music criticism. Then ingest the Project Gutenberg URL of Philip Hale’s legendary early 20th century program notes. Run these commands in the shell:</p><pre>&gt; ingest<br>&gt; ingest https://www.gutenberg.org/files/56208/56208-h/56208-h.htm</pre><p>You can also ingest your own content using the shell. See the <a href="https://github.com/embabel/ragbot">Ragbot README</a> for documentation.</p><p>You can now type chat in the shell for text-based chat, but a UI is more fun. I got Claude to produce a simple <a href="https://javelit.io">Javelit</a> web interface for Ragbot. The uichat shell command launches it. The port defaults to 8888but you can specify your own:</p><pre>&gt; uichat<br>Web UI started at http://localhost:8888</pre><p>You can now chat and ask questions. The LLM will use the RAG tools to search the indexed content and provided grounded responses. You’ll see the tool calls as they are made.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/875/0*HkORHxKZvep53EL_.jpeg" /></figure><p>To demonstrate Ragbot’s understanding of the sources, try questions like:</p><ul><li><em>What did Schumann think of Liszt?</em></li><li><em>What did Philip Hale think of Liszt?</em></li><li><em>Contrast Hale and Schumann’s attitude to Liszt?</em></li></ul><p>The latter is difficult for traditional pipeline RAG, but not agentic RAG.</p><h3>Store Choice</h3><p>Embabel’s RAG module supports multiple stores through the SearchOperations interface. Currently we support:</p><p><strong>Apache Lucene</strong> — In-memory or disk-persisted vector storage. No external dependencies. Good for development and smaller datasets.</p><p><strong>Neo4j</strong> — Graph database with vector indices. Enables entity relationships and document structure navigation alongside vector search.</p><p><strong>Spring AI VectorStore</strong> — Adapts any Spring AI vector store (pgvector, Pinecone, Chroma, etc.) to work with Embabel’s agentic RAG. <em>Does not support full text search or structure, which limits it usefulness.</em></p><p><strong>pgvector</strong> — coming soon.</p><p>The SearchOperations interfaces are simple, so it’s easy to implement your own search and benefit from the same programming model.</p><p>The API remains consistent, regardless of the backend. However, the store affects which tools ToolishRag exposes. Lucene gets vector search, text search, regex, and chunk expansion. Neo4j adds document structure navigation. Spring AI adapters provide basic vector search. The LLM always gets the best available toolkit for your store.</p><h3>Ingesting Data</h3><p>Ragbot includes shell commands for ingestion. Like the API, ingestion is also independent of the backing store.</p><p>Embabel RAG uses <a href="https://tika.apache.org/">Apache Tika</a> by default for content extraction:</p><pre>var ingested = NeverRefreshExistingDocumentContentPolicy.INSTANCE<br>        .ingestUriIfNeeded(<br>                luceneSearchOperations,<br>                new TikaHierarchicalContentReader(),<br>                uri<br>        );</pre><p>Tika handles HTML, PDF, Word documents, and dozens of other formats. It extracts hierarchical structure — titles, sections, paragraphs — which Embabel preserves in the index.</p><p>For complex documents that Tika doesn’t handle well, <a href="https://ds4sd.github.io/docling/">docling</a> is a solid choice. It converts PDFs, Word docs, and PowerPoints to clean Markdown that ingests reliably. Preprocessing with docling, then ingesting the markdown, often produces better results than direct ingestion of complex formats.</p><blockquote><strong><em>Pipeline architectures are fine for ingestion.</em></strong><em> An agentic approach is essential at at query time, when the LLM needs to reason about what to retrieve. Ingestion is a batch process where predetermined steps make sense. Embabel offers a RAG pipeline where chunks can be transformed — for example, to add information about the documents and sections they were taken from, as Ragbot does.</em></blockquote><h3>Exploring This Yourself</h3><p>The complete example is available at <a href="https://github.com/embabel/ragbot">github.com/embabel/ragbot</a>. Clone it, set your API key, and you’ll have a working agentic RAG chatbot in minutes. Change the prompt templates and ingest your own data.</p><p>The architecture, and agentic RAG, generalizes beyond chatbots. Any Embabel agent can use ToolishRag to search and reason about documents. The same pattern works for document analysis workflows, research assistants, compliance checkers, and anywhere you need grounded LLM responses.</p><p>To start from scratch with your own Embabel project, create a new Embabel repository from the <a href="https://github.com/embabel/java-agent-template">Java agent template</a> GitHub template repository.</p><h3>Coming Soon</h3><p>I’m working on <a href="https://github.com/embabel/impromptu"><strong>Impromptu</strong></a> — a full-featured classical music exploration application that integrates with YouTube, Spotify, Wikipedia and other sources. It combines agentic RAG with entity extraction, knowledge graphs and sophisticated memory system for a comprehensive music research and discovery platform.</p><p>Watch this space.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b26a8346cb16" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Rethinking RAG: Pipelines Are the Past, Agentic Is the Future]]></title>
            <link>https://medium.com/@springrod/rethinking-rag-pipelines-are-the-past-agentic-is-the-future-77c887414621?source=rss-de5947672915------2</link>
            <guid isPermaLink="false">https://medium.com/p/77c887414621</guid>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[agentic-rag]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[langchain]]></category>
            <dc:creator><![CDATA[Rod Johnson]]></dc:creator>
            <pubDate>Thu, 08 Jan 2026 08:50:29 GMT</pubDate>
            <atom:updated>2026-01-08T08:50:29.855Z</atom:updated>
            <content:encoded><![CDATA[<p><a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">RAG</a> is essential. It’s also hard to get right. Production RAG systems often disappoint, and the response is typically to add more and more workarounds, such as <a href="https://www.pinecone.io/learn/series/rag/rerankers/">reranking</a>, <a href="https://docs.haystack.deepset.ai/docs/hypothetical-document-embeddings-hyde">HyDE</a> (Hypothetical Document Embeddings), query expansion, and chunk overlap tuning. Complexity grows, while results don’t necessarily improve.</p><p>The problem isn’t the workarounds. It’s what they’re working around. Popular RAG frameworks such as LangChain use an obsolete model that dates back to 2023, before LLM tool calling emerged and changed everything. They use static pipelines when what’s needed is fundamentally different: L<strong>etting the LLM reason about retrieval rather than blindly executing a predetermined flow</strong>.</p><p>Recent research is clear. The future of RAG is agentic.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cLi1kNcjeDA-WQc6p2husw.jpeg" /></figure><h3>The Legacy Pipeline Approach</h3><p>Traditional RAG follows a rigid pipeline:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1A9JErH-VvpQRfryupEp3g.jpeg" /></figure><h4>LangChain (Python): The OG Pipeline</h4><p>As I’ve <a href="https://medium.com/@springrod/build-better-agents-in-java-vs-python-embabel-vs-langgraph-f7951a0d855c">previously noted</a>, LangChain owes its prominence to historically being the first mover. In this case, this has become a major liability, as its RAG approach is obsolete.</p><p>Here’s how LangChain implements RAG using <a href="https://python.langchain.com/docs/concepts/lcel/">LCEL (LangChain Expression Language)</a>:</p><pre>from langchain_core.runnables import RunnablePassthrough, RunnableParallel<br>from langchain_core.output_parsers import StrOutputParser<br><br># The canonical RAG pipeline<br>rag_chain = (<br>    {&quot;context&quot;: retriever, &quot;question&quot;: RunnablePassthrough()}<br>    | prompt<br>    | llm<br>    | StrOutputParser()<br>) <br><br># Or with RunnableParallel for slightly more control<br>rag_chain = (<br>    RunnableParallel(context=retriever | format_docs, question=RunnablePassthrough())<br>    | qa_prompt <br>    | llm<br>)</pre><p>This pattern appears across tutorials, documentation, and production systems. Welcome to 2023.</p><h4>LangChain4j: Different Language, Same Old Model</h4><p>As you would expect from its name, <a href="https://docs.langchain4j.dev/tutorials/rag/">LangChain4j</a> brings the same model to the JVM. The tutorial explicitly defines RAG in this limited, obsolete way: “Simply put, RAG is the way to find and inject relevant pieces of information from your data into the prompt before sending it to the LLM.”</p><p>Here’s example code from the <a href="https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_1_easy/Easy_RAG_Example.java">Easy RAG example</a>:</p><pre>public class Easy_RAG_Example {<br>    public static void main(String[] args) {<br>        List&lt;Document&gt; documents = loadDocuments(toPath(&quot;documents/&quot;), glob(&quot;*.txt&quot;));<br><br>  Assistant assistant = AiServices.builder(Assistant.class)<br>            .chatModel(CHAT_MODEL)<br>            .chatMemory(MessageWindowChatMemory.withMaxMessages(10))<br>            .contentRetriever(createContentRetriever(documents))<br>            .build();<br>        startConversationWith(assistant);<br>    }<br><br>    private static ContentRetriever createContentRetriever(List&lt;Document&gt; documents) {<br>        InMemoryEmbeddingStore&lt;TextSegment&gt; embeddingStore = new InMemoryEmbeddingStore&lt;&gt;();<br>        EmbeddingStoreIngestor.ingest(documents, embeddingStore);<br>        return EmbeddingStoreContentRetriever.from(embeddingStore);<br>    }<br>}</pre><p>The ContentRetriever supplements the prompt to help answer queries. The <a href="https://docs.langchain.com/oss/python/langchain/rag">documentation explicitly describes</a> this as a &quot;two-step chain&quot; that provides &quot;reduced latency at the expense of flexibility.&quot;</p><p>Spring AI uses a <a href="https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html">similar</a> (if more flexible) pipeline model.</p><h3>What’s Wrong With Pipelines?</h3><p>True, these are simple examples. You can build more elaborate pipelines with these old school RAG frameworks, but can’t fix the core problem with the approach.</p><p>The pipeline model has fundamental problems that no amount of tuning can fix:</p><ul><li><strong>Static retrieval </strong>The retriever retrieves once and hopes for the best. If the initial query doesn’t match how documents are indexed, you get poor results. Even if hybrid search is available, the balance between vector and full-text search will be fixed for all queries. The LLM never gets a chance to try a different, highly contextual approach.</li><li><strong>Lack of self-correction </strong>If retrieved chunks don’t actually answer the question, the pipeline has no mechanism to recognise this and try again. It has to trust whatever comes back from the vector store. Nor can it decide how hard to try in a particular scenario: Is this a legal matter or a casual question? Should it dig deep, regardless of latency, or give up quickly?</li><li><strong>Context blindness </strong>The retriever may not know what the LLM learned from previous turns in a conversation. Retrieval is often isolated, unable to build on prior context.</li></ul><p>To compensate for these limitations, teams bolt on increasingly complex preprocessing: HyDE (Hypothetical Document Embeddings) to bridge the query-document vocabulary gap, rerankers to fix retrieval ordering, query expansion to catch more results, overlap tuning to hope chunk boundaries don’t split relevant content.</p><blockquote><em>Chunk boundaries are an especially serious problem. No chunk size can be optimal for all documents. Retrieved chunks are often split in the wrong place, making them misleading.</em></blockquote><h3>Recent Research Agrees</h3><p>Recent research is unambiguous:</p><ul><li><a href="https://www.anthropic.com/engineering/multi-agent-research-system"><strong>Anthropic</strong></a>: <em>Traditional approaches using RAG use static retrieval… You can’t hardcode a fixed path for exploring complex topics, as the process is inherently dynamic and path-dependent.</em> Anthropic’s multi-agent research system <strong>outperformed single-agent approaches by 90.2%</strong>.</li><li><a href="https://developer.nvidia.com/blog/traditional-rag-vs-agentic-rag-why-ai-agents-need-dynamic-knowledge-to-get-smarter/"><strong>NVIDIA</strong></a>: Agentic RAG “refines queries using reasoning, turning RAG into a sophisticated tool” versus traditional RAG’s “lack of reasoning” and “context blindness.”</li><li><a href="https://arxiv.org/abs/2501.09136"><strong>arXiv survey</strong></a> (January 2025): <em>Traditional RAG systems are constrained by static workflows and lack the adaptability required for multistep reasoning and complex task management.</em></li><li><strong>Comparative studies</strong> (<a href="https://www.techrxiv.org/users/876974/articles/1325941-traditional-rag-vs-agentic-rag-a-comparative-study-of-retrieval-augmented-systems">TechRxiv</a>, <a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5188363">SSRN</a>): 80% improvement in retrieval quality and 90% of users preferring agentic systems.</li></ul><p>Pipeline RAG is not state of the art. It’s a first-generation approach that is now largely obsolete.</p><h3>The Future: Agentic RAG</h3><p>What if we let the LLM control the retrieval process? Instead of a rigid pipeline, give the model tools to search, evaluate results, refine queries, and iterate until it finds what it needs.</p><p>This is <strong>agentic</strong> RAG. The LLM becomes an active participant in retrieval rather than a passive consumer of whatever the pipeline produces.</p><h3>Embabel: Agentic RAG Done Right</h3><p>Embabel’s modern RAG support sits cleanly on top of the core framework through our LlmReference abstraction. The ToolishRag class enables easy agentic RAG over any a range of back end stores, which may have different capabilities.</p><h4>ToolishRag: Fine-Grained Search as LLM Tools</h4><p>ToolishRag wraps any SearchOperations implementation and exposes its capabilities as individual tools the LLM can invoke.</p><p>ToolishRag inspects what interfaces the underlying store implements and only exposes tools for operations the store actually supports.</p><p>For example, a Lucene store supports vector search, text search, regex search, and result expansion tools. A Neo4j database supports vector and full text search, plus valuable tools to navigate source document structure from chunks. A simple vector database adapter gets only vector search. The LLM will always be equipped with the best possible toolkit for the store in use, with the developer only needed to write the same simple, intuitive code.</p><h4>Key Tools</h4><p><strong>Vector Search</strong> — Semantic similarity search with configurable top-K and threshold:</p><pre>@LlmTool(description = &quot;Perform vector search. Specify topK and similarity threshold from 0-1&quot;)<br>fun vectorSearch(query: String, topK: Int, threshold: ZeroToOne): String</pre><p><strong>Text Search</strong> — BM25 search with Lucene syntax support:</p><pre>@LlmTool(description = &quot;Perform BM25 search with Lucene syntax: +term, -term, \&quot;phrases\&quot;, wildcards (*), fuzzy (~)&quot;)<br>fun textSearch(query: String, topK: Int, threshold: ZeroToOne): String</pre><p><strong>Chunk Expansion</strong> — Broaden context around a retrieved chunk:</p><pre>@LlmTool(description = &quot;Given a chunk ID, expand to surrounding chunks&quot;)<br>fun broadenChunk(chunkId: String, chunksToAdd: Int = 2): String</pre><p>Similarly, for document-structured stores such as Neo4j, <strong>Zoom Out</strong> — Navigate to parent sections for broader context:</p><pre>@LlmTool(description = &quot;Given a content element ID, expand to parent section&quot;)<br>fun zoomOut(id: String): String</pre><blockquote>The zoom tools are valuable, as they mitigate the problem of chunk boundaries splitting relevant content. If the LLM sees the start or end of what appears to be a promising seam of content, it can continue mining.</blockquote><p>The LLM decides which tools to use, in what order, and with what parameters. It can try a vector search, evaluate the results, decide they’re not quite right, and try a text search with different terms. It can find a relevant chunk, then broaden it to see surrounding context. It can zoom out to understand where a chunk fits in the document structure.</p><p>The LLM is able to reason about retrieval to help achieve its overall goal, rather than being forced to rely on predetermined results.</p><h4>Using ToolishRag in Practice</h4><p>The API is simple, consistent and elegant. Rag tools can be added to any LLM interaction via the Embabel PromptRunner. Here&#39;s an example from a production chatbot:</p><pre><br>  var toolishRag = new ToolishRag(<br>          &quot;sources&quot;,<br>          &quot;The music criticism written by Robert Schumann: His own writings&quot;,<br>          searchOperations);<br> <br>  var response = ai<br>          .withLlm(properties.chatLlm())<br>          .withReference(toolishRag)<br>          .generateText(&quot;&quot;&quot;<br>              You are a helpful authority on Robert Schumann&#39;s writings.<br>              Use the tools available to you to answer the user&#39;s questions.<br>              &quot;&quot;&quot;);<br>   </pre><p>1. SearchOperations, like the Ai interface, is normally injected into the class—it could be Lucene, a Spring AI vector store, Neo4j, or any other implementation<br>2. ToolishRag wraps it and exposes appropriate tools based on the store&#39;s capabilities<br>3. .withReference(toolishRag) gives the LLM access to search tools</p><p>The LLM has full control. It can search multiple times with different queries. It can evaluate whether results are relevant. It can expand context when needed. It can give up gracefully if nothing works — the default goal explicitly says <em>Continue search until the question is answered, or you have to give up. Be creative, try different types of queries.</em> You can customize this part of the prompt.</p><p>This example comes from our <a href="https://github.com/embabel/ragbot">Ragbot</a> sample application. Our <a href="https://github.com/embabel/guide">guide chatbot and MCP server</a> helping users build Embabel applications also use ToolishRag for RAG, backed by the Embabel documentation and related content.</p><h3>What’s Next: Entities in RAG</h3><p>Chunks are necessary but not always sufficient. Real documents have structure: sections, headings, entities, relationships, and refer to entities. A chunk that mentions “the CEO” loses meaning without knowing who “the CEO” refers to. A document discussing the user’s phone account should be linked to their customer and service entities.</p><p>We’re extending Embabel’s agentic RAG to include entity extraction and graph integration. Entities provide structure above chunks. When the agent retrieves a chunk mentioning “the acquisition,” it can traverse to the entity representing that acquisition, find the companies involved, the date, the value — context that pure chunk retrieval would miss.</p><p>This is another area where agentic approaches excel. An agent can decide when entity lookup is valuable, when graph traversal helps, when plain text search is enough. A pipeline must choose one approach for all queries.</p><p>I’ll write more about this soon.</p><h3>Conclusion</h3><p>RAG is essential. Pipeline RAG is inadequate.</p><p>Frameworks such as LangChain rely on a pipeline model that treats retrieval as a fixed preprocessing step.</p><p>As a newer framework, Embabel takes a fundamentally different approach that reflects recent research and experience. ToolishRag exposes fine-grained search operations as tools the LLM controls. The LlmReference abstraction integrates RAG into the core agent framework rather than bolting it on as an afterthought. The result is retrieval that adapts, iterates, and reasons—not retrieval that executes a predetermined flow and hopes for the best.</p><p>If you’re building production RAG systems, you have a choice. You can keep adding workarounds to a fundamentally limited architecture. Or you can adopt an approach that addresses the root cause: letting intelligent agents reason about retrieval rather than executing blind pipelines.</p><p>The research is clear. The results are dramatic. Pipeline RAG is the past. Agentic RAG is the future.</p><p><a href="https://github.com/embabel/embabel-agent">Embabel</a> is <a href="https://docs.embabel.com/embabel-agent/guide/0.3.2-SNAPSHOT/#reference.rag">how you build it</a>, on the JVM.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=77c887414621" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Embabel Year-End Update: Building The Best Agent Framework]]></title>
            <link>https://medium.com/@springrod/embabel-year-end-update-building-the-best-agent-framework-25ed98728e79?source=rss-de5947672915------2</link>
            <guid isPermaLink="false">https://medium.com/p/25ed98728e79</guid>
            <category><![CDATA[genai]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[spring-boot]]></category>
            <dc:creator><![CDATA[Rod Johnson]]></dc:creator>
            <pubDate>Mon, 22 Dec 2025 08:25:33 GMT</pubDate>
            <atom:updated>2025-12-22T08:25:33.179Z</atom:updated>
            <content:encoded><![CDATA[<p>I’ve been quiet recently, because we’ve been focused on engineering — perfecting the <a href="https://github.com/embabel/embabel-agent">Embabel</a> API and extending the framework’s capabilities.</p><p>From day one, Embabel has offered what no other framework does: sophisticated, <a href="https://medium.com/@springrod/ai-for-your-gen-ai-how-and-why-embabel-plans-3930244218f6">deterministic GOAP planning</a>; type-safe domain modelling at the core; and the ability to chain actions <em>safely </em>in ways you never explicitly coded. Given our goal of being <em>the</em> agent framework for enterprise, we’re focused on minimizing nondeterminism and integrating with existing business systems.</p><p>On this solid foundation, we’re building richer and richer functionality, with a consistent, integrated API. We’re emphasizing ease of use and removing friction points in using Embabel, informed by building real applications.</p><p><strong>We’re now ensuring that anything valuable you can do with any other framework, you can do better with Embabel.</strong> Not just the sophisticated deterministic flows we pioneered — <em>everything</em>. Utility-based exploration, the supervisor pattern, state machines, chatbots, RAG. All of it, with the type safety and testability that the JVM, and our unique design, provides.</p><p>Here are some recent advances. More depth on each coming soon.</p><h3>Planner Enhancements</h3><p>We’ve added two new planner types: <strong>Utility</strong> and <strong>Supervisor</strong>, and introduced a state machine model that works with either. Both use our existing programming model, with strongly typed @Action methods.</p><h4>Utility AI Planner: Great for Exploration</h4><p>GOAP is great when you know what goal you want to achieve, and want the framework to figure out a path to achieve it.</p><p><a href="https://en.wikipedia.org/wiki/Utility_system">Utility AI</a> — another algorithm from <a href="https://www.aiandgames.com/p/ai-101-introducing-utility-ai">gaming</a> — is great for <em>exploration</em> — when you don’t have a predetermined goal. It makes decisions based on immediate value — perfect for event-driven systems, chatbots, or reactive processing where the “right” action depends on what you discover as you progress. The planner chooses the most valuable action it can run at a given time, executes it, then replans. It may discover an unexpected path, although it will never execute an action whose conditions were not satisfied. Or it may find that exploration has concluded and there’s nothing more to do, and this is not an error.</p><p>Utility AI works especially well with our recently introduced dynamic cost and value. This enables the value of actions to change in real time — for example, if you want different behaviour outside core business hours, or want to respond differently depending on the load on an external system.</p><pre>@Cost(name = &quot;triageCost&quot;)<br>public double computeProcessingCost(GithubIssue issue) {<br>   // return calculation based on objects from Blackboard<br>}<br><br>@Action(costMethod=&quot;triageCost&quot;)<br>IssueResponse triageIssue(GitHubIssue issue, Ai ai) {<br>    return ai.createObject(&quot;Triage this issue&quot;, IssueResponse.class);<br>}</pre><h4>Supervisor Planner: Like LangGraph or Google ADK, But Typed</h4><p>We now support the popular <strong>supervisor</strong> pattern — an LLM deciding which actions to call, viewing them as tools.</p><p><strong>Supervisor planning is convenient, but not deterministic, hence is not suitable for many business critical tasks</strong>. But our implementation is superior to that of other frameworks in that it is <em>type-informed</em>: the LLM sees schemas, outputs are validated, and you get semi-deterministic behaviour, as action tools are only exposed if their preconditions are satisfied. Also interim structure is written to the blackboard, so you can persist it or respond to events with structure.</p><p>Consider this Google ADK code:</p><pre># Define sub-agents<br>greeter = LlmAgent(<br>    name=&quot;greeter&quot;,<br>    model=&quot;gemini-2.5-flash&quot;,<br>    instruction=&quot;You handle greetings warmly.&quot;,<br>    output_key=&quot;greeting_result&quot;  # String key<br>)<br><br>task_executor = LlmAgent(<br>    name=&quot;task_executor&quot;,<br>    model=&quot;gemini-2.5-flash&quot;,<br>    instruction=&quot;Process data from {greeting_result}.&quot;,  # Magic string reference<br>    output_key=&quot;task_result&quot;<br>)<br><br># Create coordinator<br>coordinator = LlmAgent(<br>    name=&quot;Coordinator&quot;,<br>    model=&quot;gemini-2.5-flash&quot;,<br>    description=&quot;I coordinate greetings and tasks.&quot;,<br>    sub_agents=[greeter, task_executor]<br>)</pre><p>In Embabel it would look as follows:</p><pre>@EmbabelComponent<br>public class CoordinatorActions {<br><br>   @Action<br>    GreetingResult greet(UserInput input, Ai ai) {<br>        return ai.createObject(&quot;Handle this greeting warmly&quot;, GreetingResult.class);<br>    }<br><br>    @AchievesGoal(&quot;user has been greeted&quot;)<br>    @Action<br>    TaskResult executeTask(GreetingResult greeting, Ai ai) {<br>        return ai.createObject(&quot;Process: &quot; + greeting.message(), TaskResult.class);<br>    }<br>}<br><br>// Run with Supervisor planner<br>var result = SupervisorInvocation.on(agentPlatform)<br>    .returning(TaskResult.class)<br>    .withScope(AgentScopeBuilder.fromInstance(coordinatorActions))<br>    .invoke(new UserInput(request));</pre><p>No magic output keys; typed and testable.</p><blockquote>The same actions work across all planners. Write once, deploy with GOAP for determinism, Utility for exploration, or Supervisor for flexibility.</blockquote><h4>@State: State Machines Done Right</h4><p>LangGraph popularized state machines for agents. We’ve made them type-safe, refactorable, and maintainable.</p><p>Our@State annotation enables branching, looping, and human-in-the-loop without magic strings. Any @Action method can return a class annotated with @State to trigger a transition. Only the actions on the target state will be available until the agent process terminates, or another state transition occurs. Polymorphism support helps routing between states, allowing us to declare a supertype as action return:</p><pre>@State<br>record AssessStory(String content) implements Stage {<br>    @Action<br>    Stage assess() {<br>        if (isAcceptable()) return new Done(content);<br>        return new ReviseStory(content);  // Loop back<br>    }<br>}<br><br>@State<br>record ReviseStory(String content) implements Stage {<br>    @Action<br>    AssessStory revise() {<br>        return new AssessStory(improvedContent());<br>    }<br>}<br><br>@State<br>record Done(String content) implements Stage {<br>    @AchievesGoal(description = &quot;Story finalized&quot;)<br>    @Action<br>    Output complete() {<br>        return new Output(content);<br>    }<br>}</pre><p>States take the data they need in their constructors. Transitions are type-checked. IDEs understand them.</p><p>Let’s see how this would look in LangGraph:</p><pre># Define the state schema<br>class State(TypedDict):<br>    content: str<br>    status: str<br><br># Define node functions<br>def assess_story(state: State) -&gt; State:<br>    # ... assessment logic<br>    return {&quot;content&quot;: state[&quot;content&quot;], &quot;status&quot;: &quot;acceptable&quot;}  # or &quot;needs_revision&quot;def revise_story(state: State) -&gt; State:<br>    # ... revision logic<br>    return {&quot;content&quot;: revised_content, &quot;status&quot;: &quot;pending&quot;}<br><br><br>def finalize(state: State) -&gt; State:<br>    return state<br><br># Routing function<br>def should_revise(state: State) -&gt; str:<br>    if state[&quot;status&quot;] == &quot;acceptable&quot;:<br>        return &quot;done&quot;<br>    return &quot;revise&quot;<br><br># Build the graph<br>workflow = StateGraph(State)<br>workflow.add_node(&quot;assess&quot;, assess_story)<br>workflow.add_node(&quot;revise&quot;, revise_story)<br>workflow.add_node(&quot;done&quot;, finalize)<br>workflow.set_entry_point(&quot;assess&quot;)<br>workflow.add_conditional_edges(&quot;assess&quot;, should_revise, {&quot;revise&quot;: &quot;revise&quot;, &quot;done&quot;: &quot;done&quot;})<br>workflow.add_edge(&quot;revise&quot;, &quot;assess&quot;)<br>workflow.add_edge(&quot;done&quot;, END)<br><br>graph = workflow.compile()</pre><p>To say this doesn’t compare well is an understatement. LangGraph = magic strings everywhere. Routing logic is separated from the states it routes between. No compile-time checking that “revise” actually exists. Good luck testing it; rename a node and watch your graph break.</p><blockquote>Embabel now implements the core idea of LangGraph in a far superior way.</blockquote><h4>Putting it all together</h4><p>Not only is the programming model identical between all planner types; you can combine them. If an action is run with the GOAP planner and returns an @State type, control goes to the state machine, but its ultimate return will be passed back to the GOAP planner; each state defaults to GOAP planning internally. You can use any set of actions with any planner, depending on your requirements.</p><p>Looping in particular becomes much easier, as it’s the one common thing that is hard to express purely in GOAP.</p><h3>Tool Enhancements</h3><p>We’ve significantly enhanced our tool support, along with shipping powerful tools for developers to use.</p><h4>AgenticTool: Familiar Pattern, Better Model</h4><p>If you’re coming from LangChain or Google ADK, AgenticTool provides a familiar pattern: an LLM orchestrating sub-tools. It maps directly to LangChain&#39;s supervisor-with-workers or ADK&#39;s coordinator pattern.</p><pre>var researcher = new AgenticTool(<br>        &quot;research-assistant&quot;,<br>        &quot;Research topics using web search and summarization&quot;)<br>    .withTools(searchTool, summarizeTool)<br>    .withLlm(LlmOptions.withModel(&quot;gpt-4&quot;));<br>context.ai()<br>    .withTool(researcher)<br>    .generateText(&quot;What&#39;s the latest on quantum computing?&quot;);</pre><p>It operates at the <em>tool</em>, not <em>agent,</em> level. It’s <strong>composable</strong> and reusable — just a tool, usable anywhere tools can be used. And it’s simple— no graph-based workflows or explicit Sequential/Parallel/Loop patterns.</p><blockquote>In largely detyped frameworks like LangGraph and ADK, the distinction between a “supervisor agent” and an “agentic tool” is often meaningless. It’s all strings passing around. The hierarchy is conceptual, not enforced. In Embabel, this distinction matters because we have typed composition at the agent level (GOAP, <em>@State</em>) versus simpler tool-level orchestration (<em>AgenticTool</em>). You choose the right abstraction for the job.</blockquote><p>For anything beyond simple orchestration, use GOAP, Utility, or @State—deterministic, type-safe planning that&#39;s more powerful than LLM-driven orchestration.</p><h4>Agentic RAG API</h4><p>Although it’s one of the oldest applications of Gen AI, RAG is notoriously hard to get right. We believe that the best approach is <strong>agentic RAG</strong>, and offer a simple, elegant API for it.</p><p>The new ToolishRag class exposes search operations as a set of LLM tools, along with instructions on their use. The LLM decides when to search, what queries to run, and how to expand context.</p><p>You give it a name, a description and a SearchOperations implementation. Depending on the capabilities of that implementation, ToolishRag will expose tools for vector search, text search, chunk expansion, and any operations unique to that store.</p><pre>var response = context.ai()<br>    .withReference(<br>      new ToolishRag(&quot;docs&quot;, &quot;Embabel Documentation&quot;, searchOperations)<br>    )<br>    .respondWithSystemPrompt(conversation, bindings);</pre><p>Agentic RAG without boilerplate, consistent across stores. You can even use multiple ToolishRag instances in one LLM interaction and the framework will disambiguate their tools.</p><p>We ship SearchOperations implementations for Neo4j, Lucene, local directories and over any Spring AI VectorStore. We will add a full-featured PostgreSQL implementation using pgvector and full text search. It’s also easy to implement your own SearchOperations and benefit from the same elegant programming model.</p><h3>Chatbots and Developer Resources</h3><h4>Easy Chatbot Authoring</h4><p>As of 0.3.1 it is much easier to <a href="https://docs.embabel.com/embabel-agent/guide/0.3.1-SNAPSHOT/#building-a-chatbot">write chatbots in Embabel</a>.</p><p>The Utility planner is ideal for chatbots, and the new trigger field on @Action enables reactive chatbot patterns, causing the action to fire only when a specific type is most recent on the blackboard:</p><pre>@Action(canRerun = true, trigger = UserMessage.class)<br>void respond(Conversation conversation, ActionContext context) {<br>    var reply = context.ai()<br>        .withTemplate(&quot;chatbot&quot;)<br>        .respondWithSystemPrompt(conversation, Map.of());<br>    context.sendMessage(conversation.addMessage(reply));<br>}</pre><p>Multiple actions can respond to messages with different values. The planner picks the most valuable. The @State mechanism also works well with chatbots.</p><p>See the <a href="https://github.com/embabel/ragbot">https://github.com/embabel/ragbot</a> repository for an example of a chatbot using RAG.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tOuOd2luYMLywHpn44wwUQ.jpeg" /><figcaption>RAG chat with ragbot</figcaption></figure><h4>The Guide: MCP Server for Embabel Development</h4><p>We’ve used our RAG support to build an MCP server in the new guide project that helps you build with Embabel. It uses RAG against documentation and source code, exposing the knowledge to Claude Desktop, Claude Code, Cursor, Windsurf or any other MCP client.</p><p>It helped write this blog, and provided all the examples.</p><p>You can clone the <a href="https://github.com/embabel/guide">https://github.com/embabel/guide</a> project and run it as a Spring Boot application or via Docker. The README describes how to integrate it with Claude Desktop and popular coding agents.</p><p>This project will be the basis for a public chatbot that will help users learn and develop with Embabel.</p><h4>Other New Features</h4><p>Among many other improvements:</p><ul><li>We’ve taken our first steps toward supporting the emerging <a href="https://agentskills.io/home">Agent Skills</a> standard. See Embabel <a href="https://docs.embabel.com/embabel-agent/guide/0.3.1-SNAPSHOT/#reference.agent_skills">documentation</a> for more information.</li><li>We’ve added support for image generation and response streaming</li><li>The Embabel <a href="https://docs.embabel.com/embabel-agent/guide/0.3.1-SNAPSHOT/">documentation</a> is much improved</li></ul><h3>Looking Forward</h3><p>We’re working towards a 1.0 GA release around late January.</p><blockquote>Embabel aims to make it impossible to justify choosing Python for production-grade agent systems not already committed to that ecosystem. The JVM is where most of the world’s valuable business logic runs. Gen AI enabling it matters. Embabel is the agent framework to unlock the enterprise value of Gen AI.</blockquote><h3>References</h3><ul><li><strong>Embabel Agent Framework</strong>: <a href="https://github.com/embabel/embabel-agent">https://github.com/embabel/embabel-agent</a></li><li><strong>Guide MCP Server and chatbot</strong>: <a href="https://github.com/embabel/guide">https://github.com/embabel/guide</a></li><li><strong>Ragbot chatbot</strong>: <a href="https://github.com/embabel/rag-demo">https://github.com/embabel/ragb</a>ot</li><li><strong>Examples (Java &amp; Kotlin)</strong>: <a href="https://github.com/embabel/embabel-agent-examples">https://github.com/embabel/embabel-agent-examples</a></li><li><strong>Documentation</strong>: <a href="https://docs.embabel.com/embabel-agent/guide/0.3.1-SNAPSHOT">https://docs.embabel.com/embabel-agent/guide/0.3.1-SNAPSHOT</a></li><li><strong>Discord</strong>: Join our community Discord at <a href="https://discord.gg/t6bjkyj93q">https://discord.gg/t6bjkyj93q</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=25ed98728e79" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Build Better Agents in Java vs Python: Embabel vs LangGraph]]></title>
            <link>https://medium.com/@springrod/build-better-agents-in-java-vs-python-embabel-vs-langgraph-f7951a0d855c?source=rss-de5947672915------2</link>
            <guid isPermaLink="false">https://medium.com/p/f7951a0d855c</guid>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[langgraph]]></category>
            <category><![CDATA[agents]]></category>
            <category><![CDATA[genai]]></category>
            <dc:creator><![CDATA[Rod Johnson]]></dc:creator>
            <pubDate>Tue, 04 Nov 2025 13:19:35 GMT</pubDate>
            <atom:updated>2025-11-04T13:28:44.293Z</atom:updated>
            <content:encoded><![CDATA[<p>Many people suffer from the misconception that Python is the natural language for building Gen AI <em>applications, </em>as opposed to doing data science.</p><p>In fact, if you’re already on the JVM, doing Gen AI on the JVM is a no brainer. And if you’re not in either camp, a fair comparison of the two might surprise you.</p><p>In previous blogs, I’ve taken <a href="https://www.crewai.com/">CrewAI</a> and <a href="https://ai.pydantic.dev/">Pydantic AI</a> examples and reimplemented them in Java with <a href="https://github.com/embabel/embabel-agent">Embabel</a>, mirroring the functionality in cleaner, more extensible code.</p><p>Today it’s the turn of <a href="https://www.langchain.com/langgraph">LangGraph</a>. LangGraph is an outgrowth of <a href="https://www.langchain.com/">LangChain</a>, the first and most popular Python Gen AI framework. LangChain’s popularity, and the promise of LangGraph, has made the company hot property, <a href="https://sacra.com/c/langchain/">recently raising a Series B at a $1.1bn valuation</a>. Can its frameworks do things Java cannot?</p><figure><img alt="Not such a clever snake?" src="https://cdn-images-1.medium.com/max/700/1*8bK66sBTYMCxwjRRB3ZDeQ.jpeg" /></figure><h3>LangGraph Architecture</h3><p>LangGraph offers a code-centric approach to multi-agent workflows. It uses a <a href="https://en.wikipedia.org/wiki/Finite-state_machine">finite state machine (<strong>FSM</strong>)</a> to order actions. <strong>Nodes</strong> (states) are functions; <strong>edges</strong> (transitions) can be automatic or determined dynamically from function call results.</p><p>Nodes and edges break long flows into multiple steps, making execution more deterministic and workflows easier to reason about.</p><p>A state machine is one of the most obvious ways to define workflows, so it’s not surprising that LangChain, whose market position is largely due to being <em>first</em>, rather than best, would leap to adopt it. But is it the best model, or might a less obvious solution be superior? At Embabel we built several iterations of state machines before settling on a different approach, based on <a href="https://medium.com/@springrod/ai-for-your-gen-ai-how-and-why-embabel-plans-3930244218f6">Goal Oriented Action Planning (GOAP)</a>.</p><blockquote>Sometimes it’s best not to be the first mover, but to learn from the successes and mistakes of others.</blockquote><h3>Pattern Showdown</h3><p>I’ve been meaning to do a methodical comparison between Embabel and LangGraph for some time.</p><p>To make it fair, I’ve not chosen examples of my own, but reimplemented patterns from a recent blog on <a href="https://pub.towardsai.net/agentic-design-patterns-with-langgraph-5fe7289187e6">Agentic Design Patterns with LangGraph</a> by Hamza Boulahia. As the author notes:</p><blockquote>If there’s one thing I’ve learned building AI systems over the last couple of years, it’s this: patterns matter. Whether we’re designing traditional software or experimenting with large language model (LLM) agents, the way we structure workflows determines how robust, flexible, and scalable they’ll be.</blockquote><blockquote>…In this post, I’ll walk you through some of the most common agentic design patterns you can implement in LangGraph.</blockquote><p>Boulahia demonstrates six agent design patterns:</p><ul><li><strong>Prompt chaining </strong>— breaking complex tasks into manageable steps</li><li><strong>Routing and parallelization </strong>for control flow</li><li><strong>Reflection</strong>, where agents critique and improve outputs</li><li><strong>Tool use</strong> to integrate with external systems</li><li><strong>Planning</strong> — structuring goals into clear, executable sequences</li><li><strong>Multi-agent collaboration</strong></li></ul><p>Let’s go through each pattern in turn and show how it can be better expressed in Java using Embabel.</p><h3>Prompt Chaining</h3><p>Prompt chaining is an essential pattern where one model’s output becomes another model’s input. It makes workflows more predictable. The example suggests topics based on user input, and generates several blog titles for each topic.</p><h4>LangGraph Implementation</h4><p>The example uses the underlying LangChain API for LLM invocation, while LangGraph adds workflow management.</p><p>Like any workflow engine, LangGraph (unlike the lower level LangChain) needs to maintain process state. This is expressed in the State class.</p><p>The flow is defined via nodes and edges. Each node is a Python method that calls an LLM.</p><pre># Define LLM client<br>llm = ChatGoogleGenerativeAI(<br>    model=&quot;gemini-2.0-flash&quot;,<br>    api_key=GOOGLE_API_KEY,<br>    system_instruction=&quot;&quot;&quot;You are an expert technical writer. Always give clear,<br>     concise, and straight-to-the-point answers.&quot;&quot;&quot;<br>)<br><br># Define the graph state<br>class State(dict):<br>    text: str<br>    topics: str<br>    title: str<br><br># Define nodes<br>def extract_topics(state: State) -&gt; State:<br>    prompt = f&quot;Extract 1-3 key topics from the following text:\n\n{state[&#39;text&#39;]}&quot;<br>    resp = llm.invoke(prompt)<br>    state[&quot;topics&quot;] = resp.content.strip()<br>    return state<br><br>def generate_title(state: State) -&gt; State:<br>    prompt = f&quot;Generate two catchy blog titles for each one these topics:\n\n{state[&#39;topics&#39;]}&quot;<br>    resp = llm.invoke(prompt)<br>    state[&quot;title&quot;] = resp.content.strip()<br>    return state</pre><p>The workflow is built in code, with nodes mapping to methods and edges linking them. In this case the edges are simple: Automatic transition to the next state. As the workflow is expressed in terms of magic strings, there is little checking of its validity at compile time.</p><pre># Build the graph<br>workflow = StateGraph(State)<br>workflow.add_node(&quot;extract_topics&quot;, extract_topics)<br>workflow.add_node(&quot;generate_title&quot;, generate_title)<br><br># Define the node connections<br>workflow.set_entry_point(&quot;extract_topics&quot;)<br>workflow.add_edge(&quot;extract_topics&quot;, &quot;generate_title&quot;)<br>workflow.add_edge(&quot;generate_title&quot;, END)<br><br># Compile runnable graph<br>graph = workflow.compile()</pre><p>We run the workflow as follows:</p><pre>input_text = (<br>    &quot;LangGraph introduces a graph-based paradigm for building LLM-powered agents. &quot;<br>    &quot;It allows developers to create modular, debuggable, and reliable agent workflows &quot;<br>    &quot;using nodes, edges, and state passing.&quot;<br>)<br><br>result = graph.invoke({&quot;text&quot;: input_text})<br>print(&quot;Topics:&quot;, result[&quot;topics&quot;])<br>print(&quot;\n&quot;+&quot;=&quot;*50+&quot;\n&quot;)<br>print(&quot;Suggested Blog Title:&quot;, result[&quot;title&quot;])</pre><p>This will produce output like this:</p><pre>Topics: Here are 3 key topics extracted from the text:<br><br>1.  **Graph-based LLM Agents:** The core concept of using a graph structure to build LLM agents.<br>2.  **Modularity, Debuggability, and Reliability:** The benefits of using LangGraph for agent development.<br>3.  **Nodes, Edges, and State Passing:** The fundamental components of the LangGraph framework.<br><br>==================================================<br><br>Suggested Blog Title: Okay, here are two catchy blog titles for each of the three topics, aiming for a mix of clarity and intrigue:<br><br>**1. Graph-based LLM Agents:**<br><br>*   **Title 1:** Unleashing the Power of Graph Minds: Building Smarter LLM Agents<br>*   **Title 2:** Beyond Chains: How Graph-Based Agents are Revolutionizing LLMs<br><br>**2. Modularity, Debuggability, and Reliability:**<br><br>*   **Title 1:** LangGraph: The Secret to Building Reliable and Debuggable LLM Agents<br>*   **Title 2:** Stop Chasing Bugs: LangGraph&#39;s Modular Approach to LLM Agent Development<br><br>**3. Nodes, Edges, and State Passing:**<br><br>*   **Title 1:** LangGraph Demystified: Nodes, Edges, and the Flow of Intelligent Conversations<br>*   **Title 2:** Building Blocks of Brilliance: Mastering Nodes, Edges, and State in LangGraph</pre><h4>Embabel Implementation</h4><p>It’s hard to do a direct port of this because, albeit trivial, it’s poorly designed. The domain model is particularly lacking. Python approaches to Gen AI tend to underemphasize domain modeling, <a href="https://medium.com/@springrod/context-engineering-needs-domain-understanding-b4387e8e4bf8">which is a serious mistake</a>. All fields of the State class are modelled as strings, where there should be a structure. Expressing topics as a single string is particularly misleading.</p><p>I made the minimum changes to be able to live with the solution. Here’s the Embabel implementation, expressed in terms of a <strong>domain model</strong>, <strong>actions</strong> and a <strong>goal</strong>.</p><p>The domain objects are Java records, correctly representing the structure implied by the requirements.</p><p>The @Agent annotation causes the class to be instantiated and injected by Spring, and processed by Embabel. The @Action methods are steps, and the goal the desired output:</p><pre>@Agent(description = &quot;Blog Titler Agent&quot;)<br>public class BlogTitler {<br><br>    private final Actor&lt;?&gt; techWriter = new Actor&lt;&gt;(<br>    &quot;&quot;&quot;<br>            You are an expert technical writer. Always give clear,<br>            concise, and straight-to-the-point answers.<br>            &quot;&quot;&quot;,<br>            LlmOptions.withAutoLlm());<br><br>    public record Topics(<br>            List&lt;String&gt; topics<br>    ) {<br>    }<br><br>    public record TopicTitles(<br>            String topic,<br>            List&lt;String&gt; titles<br>    ) {<br>    }<br><br>    public record BlogTitles(<br>            List&lt;TopicTitles&gt; topicTitles<br>    ) {<br>    }<br><br>    @Action<br>    public Topics extractTopics(UserInput userInput, Ai ai) {<br>        return techWriter.promptRunner(ai)<br>                .creating(Topics.class)<br>                .fromPrompt(&quot;&quot;&quot;<br>                        Extract 1-3 key topics from the following text:<br>                        %s<br>                        &quot;&quot;&quot;.formatted(userInput.getContent()));<br>    }<br><br>    @Action<br>    @AchievesGoal(description=&quot;Generate Titles for Topics&quot;)<br>    public BlogTitles generateBlogTitles(Topics topics, OperationContext context) {<br>        var titles = context.parallelMap(<br>                topics.topics(),<br>                10,<br>                topic -&gt; techWriter.promptRunner(context)<br>                .creating(TopicTitles.class)<br>                .fromPrompt(&quot;&quot;&quot;<br>                        Generate two catchy blog titles for this topic:<br>                        %s<br>                        &quot;&quot;&quot;.formatted(topic)));<br>        return new BlogTitles(titles);<br>    }<br><br>}</pre><p>The Actor field combines LLM selection with hyperparameters and an instruction.</p><p>The extractTopics and generateBlogTitles methods are the equivalent of the LangGraph node methods. However, the workflow is put together quite differently.</p><blockquote>With Embabel we don’t need to specify the workflow in code. Embabel plans an execution path by analyzing our code.</blockquote><p>The Embabel planner can infer the correct execution plan from the flow of types. For example, it can see that the goal can only be achieved if a Topics and BlogTitles object is available in workflow scope. Thus it can correctly determine the order of methods to call. This planning step is deterministic.</p><p>We model the generateBlogTitles step in terms of parallel operations, more naturally reflecting the requirements and resulting in cleaner, more focused interactions with the LLM.</p><p>The output will look like this. Unlike the LangGraph example’s output, the structure accurately reflects the requirements:</p><pre>{<br>  &quot;topicTitles&quot; : [ {<br>    &quot;topic&quot; : &quot;LangGraph graph-based paradigm for LLM agents&quot;,<br>    &quot;titles&quot; : [ &quot;Unlocking Smarter AI: How LangGraph’s Graph-Based Paradigm Revolutionizes LLM Agents&quot;, &quot;Navigating the Future of LLM Agents with LangGraph’s Innovative Graph-Based Approach&quot; ]<br>  }, {<br>    &quot;topic&quot; : &quot;Modular and debuggable agent workflows&quot;,<br>    &quot;titles&quot; : [ &quot;Streamlining Success: Building Modular and Debuggable Agent Workflows&quot;, &quot;Mastering Agent Workflows: The Power of Modularity and Debugging&quot; ]<br>  }, {<br>    &quot;topic&quot; : &quot;Comparison with Embabel&#39;s GOAP planning&quot;,<br>    &quot;titles&quot; : [ &quot;Mastering GOAP: How Embabel’s Planning Stacks Up Against the Rest&quot;, &quot;Embabel&#39;s GOAP Planning Compared: What Makes It Stand Out?&quot; ]<br>  } ]<br>}</pre><h4>Comparison</h4><p>Both implementations break the flow into small, focused steps, which makes it more predictable. There is slightly more code in the Embabel version, because it properly models cardinality.</p><p>The Embabel implementation is superior in several respects. It’s type safe. It has a true domain model versus misleading strings. It’s easier to test. And parallelization is only possible because of the additional structure.</p><p>Some enhancements that would be trivial with the Embabel, but not LangGraph, implementation:</p><ul><li><strong>Externalizing LLM choice and persona</strong>: With Embabel and Spring, it’s trivial to externalize the Actor definition into an application.yml file, along with all the other sophisticated configuration options that are lacking in Python.</li><li><strong>Persisting topics and blog titles</strong>: Because we have correctly structured domain objects, we could easily persist them if we wished.</li></ul><h3>Routing</h3><p>Another fundamental pattern is routing within a workflow. Here’s a LangGraph implementation, first performing sentiment analysis and then choosing one or other function to generate a response:</p><pre>llm = ChatGoogleGenerativeAI(<br>    model=&quot;gemini-2.0-flash&quot;,<br>    api_key=GOOGLE_API_KEY,<br>    system_instruction=&quot;&quot;&quot;You are a helpful assistant that can classify text <br>    sentiment and respond accordingly.&quot;&quot;&quot;<br>)<br><br>class State(dict):<br>    text: str<br>    sentiment: str<br>    response: str<br><br># Nodes<br>def calssification(state: State) -&gt; str:<br>    &quot;&quot;&quot;Classify sentiment.&quot;&quot;&quot;<br>    prompt = f&quot;Is the following text positive or negative? Answer with one word only: Positive or Negative.\n\n{state[&#39;text&#39;]}&quot;<br>    resp = llm.invoke(prompt)<br>    sentiment = resp.content.strip().lower()<br>    state[&quot;sentiment&quot;] = sentiment<br>    return state<br><br>def positive_node(state: State) -&gt; State:<br>    prompt = f&quot;Generate an encouraging reply to this positive text:\n\n{state[&#39;text&#39;]}&quot;<br>    resp = llm.invoke(prompt)<br>    state[&quot;response&quot;] = resp.content.strip()<br>    return state<br><br>def negative_node(state: State) -&gt; State:<br>    prompt = f&quot;Generate a supportive reply to this negative text:\n\n{state[&#39;text&#39;]}&quot;<br>    resp = llm.invoke(prompt)<br>    state[&quot;response&quot;] = resp.content.strip()<br>    return state<br><br>def router_func(state: State) -&gt; Literal[&quot;positive&quot;, &quot;negative&quot;]:<br>    &quot;&quot;&quot;Return next node name.&quot;&quot;&quot;<br>    return &quot;positive&quot; if &quot;positive&quot; in state[&quot;sentiment&quot;] else &quot;negative&quot;<br><br># Build the graph<br>workflow = StateGraph(State)<br><br>workflow.add_node(&quot;calssification&quot;, calssification)<br>workflow.add_node(&quot;positive&quot;, positive_node)<br>workflow.add_node(&quot;negative&quot;, negative_node)<br><br># classify node decides the next step<br>workflow.set_entry_point(&quot;calssification&quot;)<br>workflow.add_conditional_edges(&quot;calssification&quot;, router_func, {<br>    &quot;positive&quot;: &quot;positive&quot;,<br>    &quot;negative&quot;: &quot;negative&quot;,<br>})<br><br># Both branches lead to END<br>workflow.add_edge(&quot;positive&quot;, END)<br>workflow.add_edge(&quot;negative&quot;, END)<br><br>graph = workflow.compile()</pre><p>As usual with LangGraph, the workflow is built with unchecked strings. It’s fortunate that the author has consistently misspelt “classification” because typos matter a lot with something this fragile.</p><p>This example shows a more sophisticated LangGraph state transition, mapping the output of a function to the next state name, with still more error-prone magic strings as a bonus:</p><pre>workflow.add_conditional_edges(&quot;calssification&quot;, router_func, {<br>    &quot;positive&quot;: &quot;positive&quot;,<br>    &quot;negative&quot;: &quot;negative&quot;,<br>})</pre><p>We run this as follows:</p><pre>input_text = &quot;I&#39;m so happy with how my project turned out!&quot;<br>result = graph.invoke({&quot;text&quot;: input_text})<br>print(&quot;Sentiment:&quot;, result[&quot;sentiment&quot;])<br>print(&quot;Response:&quot;, result[&quot;response&quot;])</pre><h4>Embabel Implementation</h4><p>Explicit wiring of nodes and edges seems overcomplicated for something so simple. Let’s express the steps in a more natural manner.</p><p>Because the steps are different, we can simply use a focused prompt each time, rather than a common system message associated with the LLM.</p><p>First we’ll create types for Sentiment. This will enable the flow to be expressed in terms of data types:</p><pre>@Agent(description = &quot;Perform sentiment analysis&quot;)<br>public class ClassificationAgent {<br><br>    @JsonTypeInfo(<br>            use = JsonTypeInfo.Id.SIMPLE_NAME,<br>            include = JsonTypeInfo.As.PROPERTY,<br>            property = &quot;type&quot;<br>    )<br>    public sealed interface Sentiment {<br>    }<br><br>    public static final class Positive implements Sentiment {<br>    }<br><br>    public static final class Negative implements Sentiment {<br>    }<br><br>    public record Response(String message) {<br>    }<br><br>    private enum SentimentType {<br>        POSITIVE,<br>        NEGATIVE;<br><br>        public Sentiment toSentiment() {<br>            return switch (this) {<br>                case POSITIVE -&gt; new Positive();<br>                case NEGATIVE -&gt; new Negative();<br>            };<br>        }<br>    }</pre><p>Now we can implement the first step as a classify method that returns Sentiment. The Embabel planner is smart enough to know that Sentimenthas subtypes and route appropriately.</p><p>The encourage and help methods each take a Sentiment subtype. Like the Python functions, they invoke an LLM to generate a response.</p><pre>@Action<br>  public Sentiment classify(UserInput userInput, Ai ai) {<br>      var type = ai.withAutoLlm()<br>              .createObject(&quot;&quot;&quot;<br>                              Determine if the sentiment of the following text is positive or negative.<br>                              Text: &quot;%s&quot;<br>                              &quot;&quot;&quot;.formatted(userInput.getContent()),<br>                      SentimentType.class);<br>      return type.toSentiment();<br>  }<br><br>  @Action<br>  public Response encourage(<br>          UserInput userInput,<br>          Positive sentiment,<br>          Ai ai) {<br>      return ai.withAutoLlm()<br>              .createObject(&quot;&quot;&quot;<br>                      Generate an encouraging response to the following positive text:<br>                      %s<br>                      &quot;&quot;&quot;.formatted(userInput.getContent()), Response.class);<br>  }<br><br>  @Action<br>  public Response help(<br>          UserInput userInput,<br>          Negative sentiment,<br>          Ai ai) {<br>      return ai.withAutoLlm()<br>              .createObject(&quot;&quot;&quot;<br>                      Generate a supportive response to the following negative text:<br>                      %s<br>                      &quot;&quot;&quot;.formatted(userInput.getContent()), Response.class);<br>  }<br><br>  @AchievesGoal(<br>          description = &quot;Generate a response based on discerning sentiment in user input&quot;)<br>  @Action<br>  public Response done(Response response) {<br>      return response;<br>  }<br><br>}</pre><h4>Comparison</h4><p>Defining this flow via data types with Embabel is more natural and less error-prone than the LangChain state machine. Again, with Embabel we don’t need to define the flow, as the planner will work it out.</p><h3>Parallelization</h3><p>LLM interactions can be slow. Thus parallelization is important in Gen AI applications.</p><p>LangGraph lets us parallelize operations through “parallel edges.”</p><pre># Build the graph<br>builder = StateGraph(State)<br>builder.add_node(&quot;summarize&quot;, summarize)<br>builder.add_node(&quot;critique&quot;, critique)<br>builder.add_node(&quot;combine&quot;, combine)<br><br># Parallel edges: summarize and critique run side by side<br>builder.add_edge(START, &quot;summarize&quot;)<br>builder.add_edge(START, &quot;critique&quot;)<br><br># Both join into combine<br>builder.add_edge(&quot;summarize&quot;, &quot;combine&quot;)<br>builder.add_edge(&quot;critique&quot;, &quot;combine&quot;)<br><br>builder.add_edge(&quot;combine&quot;, END)p</pre><p>With Embabel, we have two choices for parallelization:</p><ul><li><strong>Programmatic</strong>, using the parallelMap method that we saw in the blog title example.</li><li><strong>Automatic</strong>: If we set embabel.agent.platform.process-type=CONCURRENT, the planner will automatically run in parallel operations that are both necessary to achieve the current goal and which don’t depend on each other.</li></ul><h3>Reflection</h3><p>This is a common and important pattern that goes by various names. Anthropic’s <a href="https://www.anthropic.com/engineering/building-effective-agents">Building Effective Agents</a> article calls this pattern <strong>Evaluator-optimizer</strong>.</p><p>The key idea is to use one LLM call to provide feedback on the output of another, iterating until the result is satisfactory.</p><p>Here’s the LangGraph implementation, drafting text in response to a user-specified task and critiquing it until it’s satisfactory.</p><pre># State<br>class State(TypedDict):<br>    task: str<br>    draft: str<br>    feedback: str<br>    final: str<br><br># Nodes<br>def generator(state: State):<br>    &quot;&quot;&quot;Generate an initial or refined draft.&quot;&quot;&quot;<br>    prompt = f&quot;&quot;&quot;<br>You are an assistant helping to complete the following task:<br><br>Task:<br>{state[&#39;task&#39;]}<br><br>Current Draft:<br>{state.get(&#39;draft&#39;, &#39;None&#39;)}<br><br>Feedback:<br>{state.get(&#39;feedback&#39;, &#39;None&#39;)}<br><br>Instructions:<br>- If there is no draft and no feedback, generate a clear and complete response to the task.<br>- If there is a draft but no feedback, improve the draft as needed for clarity and quality.<br>- If there is both a draft and feedback, revise the draft by incorporating the feedback directly.<br>- Always produce a single, improved draft as your output.<br>&quot;&quot;&quot;<br>    resp = llm.invoke(prompt)<br>    return {&quot;draft&quot;: resp.content.strip()}<br><br>def evaluator(state: State):<br>    &quot;&quot;&quot;Evaluate the draft and give feedback or approval.&quot;&quot;&quot;<br>    prompt = f&quot;&quot;&quot;Evaluate the following draft, based on the given task. <br>If it meets the requirements, reply exactly &#39;APPROVED&#39;. <br>Otherwise, provide constructive feedback for improvement.<br>Task:<br>{state[&#39;task&#39;]}<br>Draft:<br>{state[&#39;draft&#39;]}&quot;&quot;&quot;<br>    resp = llm.invoke(prompt)<br>    print(f&quot;&quot;&quot;<br>================= DRAFT =================<br>{state[&#39;draft&#39;]}<br><br>================ FEEDBACK ===============<br>{resp.content.strip()}<br>========================================<br>&quot;&quot;&quot;)<br>    return {&quot;feedback&quot;: resp.content.strip()}<br><br>def decide(state: State) -&gt; str:<br>    &quot;&quot;&quot;Decide next step: either approve and finish, or refine again.&quot;&quot;&quot;<br>    if &quot;APPROVED&quot; in state[&quot;feedback&quot;].upper():<br>        return &quot;approved&quot;<br>    return &quot;refine&quot;<br><br>def finalize(state: State):<br>    &quot;&quot;&quot;Save the final approved draft.&quot;&quot;&quot;<br>    return {&quot;final&quot;: state[&quot;draft&quot;]}<br><br># Build the graph<br>builder = StateGraph(State)<br><br>builder.add_node(&quot;generator&quot;, generator)<br>builder.add_node(&quot;evaluator&quot;, evaluator)<br>builder.add_node(&quot;finalize&quot;, finalize)<br><br>builder.add_edge(START, &quot;generator&quot;)<br>builder.add_edge(&quot;generator&quot;, &quot;evaluator&quot;)<br>builder.add_edge(&quot;evaluator&quot;, &quot;finalize&quot;)<br><br># Conditional edges from decide<br>builder.add_conditional_edges(<br>    &quot;evaluator&quot;,<br>    decide,<br>    {<br>        &quot;approved&quot;: &quot;finalize&quot;,   # stop loop<br>        &quot;refine&quot;: &quot;generator&quot;,    # go back for improvement<br>    },<br>)<br><br>builder.add_edge(&quot;finalize&quot;, END)<br><br>graph = builder.compile()</pre><p>While the pattern is valuable, this implementation is far from ideal. For a start, it’s not guaranteed to terminate.</p><p>From the developer point of view, a state machine isn’t the ideal expression of this common workflow. To be fair, neither is Embabel’s GOAP planning.</p><blockquote>While an FSM or GOAP is well suited to coarse grained planning, well-known fine grained operations can be made easier by dedicated framework support.</blockquote><p>Thus Embabel provides out of the box implementations of such common patterns. (You can also write your own.) The fluent API for such builders emits GOAP plans, so the platform can manage the steps in its normal way.</p><p>We create an Agent programmatically using RepeatUntilAcceptableBuilder and expose it as a Spring bean in an @Configuration class. Embabel will automatically deploy it:</p><pre>@Bean<br>public Agent draftAndRefineAgent() {<br>  return RepeatUntilAcceptableBuilder<br>      .returning(Draft.class)<br>      .consuming(UserInput.class)<br>      .withMaxIterations(7)<br>      .withScoreThreshold(.9)<br>      .repeating(tac -&gt; {<br>          return tac.ai()<br>                  .withAutoLlm()<br>                  .createObject(<br>                          &quot;&quot;&quot;<br>                          You are an assistant helping to complete the following task:<br>                          <br>                          Task:<br>                          %s<br>                          <br>                          Current Draft:<br>                          %s<br>                          <br>                          Feedback:<br>                          %s<br>                          <br>                          Instructions:<br>                          - If there is no draft and no feedback, generate a clear and complete response to the task.<br>                          - If there is a draft but no feedback, improve the draft as needed for clarity and quality.<br>                          - If there is both a draft and feedback, revise the draft by incorporating the feedback directly.<br>                          - Always produce a single, improved draft as your output.<br>                          &quot;&quot;&quot;.formatted(<br>                          tac.getInput().getContent(),<br>                          tac.lastAttemptOr(&quot;no draft yet&quot;),<br>                          tac.lastFeedbackOr(&quot;no feedback yet&quot;)),<br>                          Draft.class);<br>      })<br>      .withEvaluator(tac -&gt; {<br>          return tac.ai()<br>                   .withAutoLlm()<br>                   .createObject(<br>                    &quot;&quot;&quot;<br>                      Evaluating the following draft, based on the given task.<br>                      Score it from 0.0 to 1.0 (best) and provide constructive feedback for improvement.<br>                      <br>                      Task:<br>                      %s<br>                      <br>                      Draft:<br>                      %s<br>                      &quot;&quot;&quot;.formatted(<br>                      tac.getInput().getContent(),<br>                      tac.getResultToEvaluate()),<br>                         TextFeedback.class);<br>      })<br>      .buildAgent(&quot;draft_and_refine_agent&quot;, <br>                      &quot;An agent that drafts and refines content&quot;);<br>}</pre><p>The Embabel Java version has significantly less code than the Python LangGraph version. It’s type safe, toolable, and there’s no way to mess up the flow. If maxIterations is exceeded, the loop will terminate, returning the highest scoring result seen so far. Because RepeatUntilAcceptableBuilder is parameterized, you can use your own result type and feedback type, while preserving type safety. LangGraph compares very poorly in this scenario.</p><h3>Tool Use</h3><p>Tool use is table stakes for any Gen AI framework and must be easy.</p><p>With LangGraph we can define tools in Python and bind them to an LLM:</p><pre>def calculator(expression: str):<br>    &quot;&quot;&quot;Evaluate a math expression.&quot;&quot;&quot;<br>    try:<br>        return str(eval(expression))<br>    except Exception as e:<br>        return f&quot;Error: {e}&quot;<br><br>llm = ChatGoogleGenerativeAI(<br>    model=&quot;gemini-2.0-flash&quot;,<br>    api_key=GOOGLE_API_KEY,<br>)<br><br>model_with_tools = llm.bind_tools([calculator])<br>tool_node = ToolNode([calculator])<br><br># Nodes<br>def call_model(state: State):<br>    &quot;&quot;&quot;Call the model; it may request a tool.&quot;&quot;&quot;<br>    messages = state[&quot;messages&quot;]<br>    response = model_with_tools.invoke(messages)<br>    return {&quot;messages&quot;: [response]}</pre><p>This is straightforward, although of course the example is dangerous because it evaluates generated Python without sandboxing.</p><p>Embabel allows us to add tools every time we use a PromptRunner, with less ceremony. We can define well known <strong>tool groups</strong> (which are often backed by MCP), or specify Java objects:</p><pre>var response = ai.withAutoLlm()<br>  .withToolGroup(CoreToolGroups.MATH)<br>  .withToolObject(myDomainObject);</pre><p>The <a href="https://medium.com/@springrod/domain-tools-direct-access-zero-ceremony-9a3e8d4cf550">ability to use tool objects is particularly important</a>. These could be domain objects we’ve retrieved from a database or services we injected into our agent with Spring. We can do that in Python also, but there’s an important difference in that it’s more likely that we have valuable business functionality implemented in Java.</p><p>Embabel also offers tool grouping with concepts like an LlmReference, which combines tools with prompt elements allowing sophisticated composition at runtime.</p><h3>Planning</h3><p>The LangGraph blog example of planning involves a final step that depends on the execution of two previous steps. This is expressed in the workflow builder like this:</p><pre># Set the entry point<br>workflow.set_entry_point(&quot;planner&quot;)<br><br># Define the parallel edges<br># After the planner, both research workers are called.<br>workflow.add_edge(&quot;planner&quot;, &quot;graphql_researcher&quot;)<br>workflow.add_edge(&quot;planner&quot;, &quot;rest_researcher&quot;)<br><br># Define the join point<br># The writer will only run after BOTH research workers are complete.<br>workflow.add_edge(&quot;graphql_researcher&quot;, &quot;writer&quot;)<br>workflow.add_edge(&quot;rest_researcher&quot;, &quot;writer&quot;)<br><br># The graph ends after the writer is done<br>workflow.add_edge(&quot;writer&quot;, END)</pre><p>In Embabel, you would simply define a method that takes both data types:</p><pre>Writeup writeUp(GraphQLResearch gr, RestResearch rr, Ai ai) {</pre><p>Clearer, type safe and easy to test.</p><p>The LangChain example also hardcodes the steps in the plan but notes that they could be generated by an LLM. We could implement this by having an LLM instantiate command objects in Embabel. However, relying on LLMs to perform planning makes flows less predictable, so should be done with care.</p><h3>Multi-Agent Collaboration</h3><p>The final pattern is an example of <strong>handoffs</strong> from a <strong>supervisor</strong> node to other agents. This is a common pattern in frameworks such as Google ADK. It’s also less deterministic than expressing the steps via a state machine or GOAP actions, hence not a pattern we tend to encourage. To achieve this behaviour, Embabel allows you to use agents as tools.</p><h3>Complete Code</h3><p>The complete code can be found in the <a href="https://github.com/embabel/langgraph-patterns">Embabel langgraph-patterns repository.</a></p><p>You could argue that <a href="https://pub.towardsai.net/agentic-design-patterns-with-langgraph-5fe7289187e6">the original blog</a> isn’t the greatest LangGraph showcase. But it’s a recent, well-written piece based on a sound understanding of agents, with hundreds of claps on Medium. It’s not hard to find the same deficiencies in other LangGraph applications, suggesting that the problem lies with the framework, language and ecosystem, not the author.</p><h3>The Reality</h3><p>It’s time the Java community stopped feeling it needs to play catch up, and demonstrates that it not only has a solution for building Gen AI agents, but has the <em>best,</em> on any platform.</p><blockquote>It’s time to stop settling for being downstream of mediocre ideas from Python frameworks, and do our own thinking. <strong>The Python emperor has no clothes.</strong> If you’re a JVM developer and your boss tells you to write agents in Python, stand ready to point out the inferiority of Python as a platform for business applications, Gen AI or other. And be ready to show how common patterns can look a whole lot better in Java than Python.</blockquote><p>Python is an important language that every developer should be fluent in. It’s great for many things. However, it has never been a particularly popular language for building nontrivial business applications, and there’s good reason for that.</p><p>The robustness of the JVM is well known; the ecosystem — especially Spring, which has no analog in Python — puts it in a very strong position. <a href="https://github.com/embabel/embabel-agent">Embabel</a> builds on top of this, bringing superior concepts to any Python framework. The rest is up to you. Create your first agent from our <a href="https://github.com/embabel/java-agent-template">Java agent template</a> today.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f7951a0d855c" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>