<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: LLMGraph</title>
    <description>The latest articles on DEV Community by LLMGraph (@llmgraph).</description>
    <link>https://dev.to/llmgraph</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4012779%2Faf0e8457-5711-42f0-9b92-7a5482a7e50c.png</url>
      <title>DEV Community: LLMGraph</title>
      <link>https://dev.to/llmgraph</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/llmgraph"/>
    <language>en</language>
    <item>
      <title>Flowise is shutting down: a migration path for your visual LLM workflows</title>
      <dc:creator>LLMGraph</dc:creator>
      <pubDate>Wed, 05 Aug 2026 00:03:44 +0000</pubDate>
      <link>https://dev.to/llmgraph/flowise-is-shutting-down-a-migration-path-for-your-visual-llm-workflows-1jce</link>
      <guid>https://dev.to/llmgraph/flowise-is-shutting-down-a-migration-path-for-your-visual-llm-workflows-1jce</guid>
      <description>&lt;p&gt;Flowise announced on July 29 that it's winding down. The GitHub repo gets archived on August 10, npm packages and Docker images are being deprecated, and August 31 is end of life. The code stays Apache 2.0, and the team's guidance is to fork and maintain it yourself.&lt;/p&gt;

&lt;p&gt;If you're running Flowise chatflows in production, that leaves you with two real options: become the maintainer of your own fork (security patches, dependency upgrades, model API churn, all of it) or move to a platform that's actively maintained.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stated reason, and why I read it differently
&lt;/h2&gt;

&lt;p&gt;The Flowise team's stated reason for shutting down is that "rigid workflow low code approach quickly hits the limit when it comes to complexity." I'd frame it differently: visual workflow builders aren't the problem. Abandoned ones are. Most teams using a tool like Flowise chose it precisely because they don't want to hand-maintain orchestration code. That need didn't go away last week.&lt;/p&gt;

&lt;h2&gt;
  
  
  What migration actually looks like
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://llmgraph.ai" rel="noopener noreferrer"&gt;LLMGraph&lt;/a&gt;, a hosted visual LLM workflow builder, and the concepts map over directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chatflows map to Graphs.&lt;/strong&gt; Same node-and-edge mental model: models, prompts, logic, and data sources wired together on a canvas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment is built in.&lt;/strong&gt; A finished graph deploys as a public invoke API or an embeddable chat widget in one click. No Docker, no reverse proxy, no self-hosting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosted and maintained.&lt;/strong&gt; Model providers, infra, and security patches are our problem, not yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before August 31
&lt;/h2&gt;

&lt;p&gt;If you want to move a Flowise setup over before end of life, every LLMGraph tier starts with a 14-day trial, and I'm happy to personally help port your chatflows. Email &lt;a href="mailto:founders@llmgraph.ai"&gt;founders@llmgraph.ai&lt;/a&gt; or ping me in the comments.&lt;/p&gt;

</description>
      <category>lowcode</category>
    </item>
    <item>
      <title>Hexagonal architecture in Rust, and why it makes your codebase legible to coding agents</title>
      <dc:creator>LLMGraph</dc:creator>
      <pubDate>Tue, 28 Jul 2026 21:13:44 +0000</pubDate>
      <link>https://dev.to/llmgraph/hexagonal-architecture-in-rust-and-why-it-makes-your-codebase-legible-to-coding-agents-544a</link>
      <guid>https://dev.to/llmgraph/hexagonal-architecture-in-rust-and-why-it-makes-your-codebase-legible-to-coding-agents-544a</guid>
      <description>&lt;p&gt;Coding agents are good at local edits and bad at holding a whole system in their head. Ask one to fix a function and it does fine. Ask it to "add Postgres support" to a codebase where the database calls are smeared across HTTP handlers, and it starts editing in six places, guessing at each, and quietly breaking the seventh. The problem is not the model. The problem is that the codebase never told anyone, human or machine, where things are allowed to live.&lt;/p&gt;

&lt;p&gt;Hexagonal architecture is a way of telling them. It is an old idea (Alistair Cockburn named it "ports and adapters" in 2005) that turns out to be exactly the structure an agent needs: narrow contracts, bounded blast radius, and a spec it can read before it writes. This post walks through what it is, how it looks in Rust, and why the same boundaries that keep code readable for people make it tractable for agents.&lt;/p&gt;

&lt;p&gt;There is a companion repo that compiles, tests, and runs: &lt;a href="https://github.com/llmgraph-ai/hexagonal-rust-template" rel="noopener noreferrer"&gt;llmgraph-ai/hexagonal-rust-template&lt;/a&gt;. Everything below is in there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule
&lt;/h2&gt;

&lt;p&gt;Hexagonal architecture has a lot of vocabulary (ports, adapters, driving side, driven side) but only one rule that matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependencies point inward. The core depends on nothing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your business logic sits in the middle. It defines the interfaces it needs from the outside world (a place to store data, a way to send email) as traits. It does not know or care what implements them. Everything technological (the web framework, the database driver, the JSON) lives at the edge and depends on the core, never the other way around.&lt;/p&gt;

&lt;p&gt;Draw it as a picture and it is a set of rings with the arrows all pointing to the center:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server  -&amp;gt;  adapters  -&amp;gt;  application  -&amp;gt;  domain
                \______________________________^
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;domain&lt;/strong&gt; is the core: business types and the ports (traits).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;application&lt;/strong&gt; is the use cases, written against those ports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;adapters&lt;/strong&gt; are the edge: an HTTP handler, a Postgres client, an in-memory fake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;server&lt;/strong&gt; is the composition root: the one place that wires a specific adapter to the core.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In Rust, the compiler enforces it
&lt;/h2&gt;

&lt;p&gt;Most languages document the dependency rule and hope. Rust lets you make it a compile error. Put each ring in its own crate, and give each crate a dependencies list that only points inward. Now the core cannot import an adapter, because the adapter's crate is not in its dependency graph. The rule stops being a convention that erodes over time and becomes a fact the compiler checks on every build.&lt;/p&gt;

&lt;p&gt;Here is the core. Note what is not imported: no &lt;code&gt;axum&lt;/code&gt;, no &lt;code&gt;sqlx&lt;/code&gt;, no &lt;code&gt;serde&lt;/code&gt;. Just the standard library and two small utilities.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// crates/domain/src/lib.rs&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nf"&gt;ShortCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ShortCode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The inner String is private, so the only way to get a ShortCode is to&lt;/span&gt;
    &lt;span class="c1"&gt;// parse one. An invalid code cannot exist anywhere in the system.&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DomainError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;..=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="nf"&gt;.chars&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.all&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="nf"&gt;.is_ascii_lowercase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="nf"&gt;.is_ascii_digit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;DomainError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InvalidCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core also defines the ports: the interfaces it needs from the world, as traits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// crates/domain/src/ports.rs&lt;/span&gt;

&lt;span class="c1"&gt;// A driven (outbound) port: somewhere to store links. The core does not know&lt;/span&gt;
&lt;span class="c1"&gt;// if this is Postgres, Redis, or a HashMap.&lt;/span&gt;
&lt;span class="nd"&gt;#[async_trait]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;LinkRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Sync&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ShortLink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;DomainError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ShortCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShortLink&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DomainError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// A driving (inbound) port: the use cases the application offers to the world.&lt;/span&gt;
&lt;span class="nd"&gt;#[async_trait]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;Shortener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Sync&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;shorten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShortLink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DomainError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShortLink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DomainError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application layer implements the driving port by orchestrating the driven one. This file is the behavior of the system with none of the plumbing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// crates/application/src/lib.rs&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ShortenerService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;LinkRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// depends on the port, not a concrete DB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[async_trait]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Shortener&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ShortenerService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;shorten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShortLink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DomainError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;validate_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;ShortCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;ShortCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nf"&gt;generate_code&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.repo&lt;/span&gt;&lt;span class="nf"&gt;.find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.is_some&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;DomainError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CodeTaken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ShortLink&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.repo&lt;/span&gt;&lt;span class="nf"&gt;.save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTTP adapter depends on the &lt;code&gt;Shortener&lt;/code&gt; port, not the concrete service. Its only job is to translate HTTP into a call on the port and translate the result back. There is no business logic here, which is exactly why you can throw it away and replace it with a CLI without touching the core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// crates/adapters/src/http.rs&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;create_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateLinkRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LinkResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.shortener&lt;/span&gt;&lt;span class="nf"&gt;.shorten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.code&lt;/span&gt;&lt;span class="nf"&gt;.as_deref&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CREATED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the composition root, the single file that knows every concrete type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// crates/server/src/main.rs&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;LinkRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;InMemoryLinkRepository&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;shortener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;Shortener&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ShortenerService&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;http&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shortener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nn"&gt;axum&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want Postgres instead of memory? Write a &lt;code&gt;PostgresLinkRepository&lt;/code&gt; that implements &lt;code&gt;LinkRepository&lt;/code&gt;, and change line one. Want a CLI instead of HTTP? Change line three. Nothing upstream notices, because nothing upstream ever knew.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents thrive on this
&lt;/h2&gt;

&lt;p&gt;Now the payoff, and the reason this old pattern is worth revisiting in 2026. Every property that makes hexagonal architecture pleasant for a human maps directly onto something an agent needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tasks map to a single file with a precise contract.&lt;/strong&gt; "Add a Postgres adapter" is not a vague request that could touch anything. It is: implement this one trait in this one new file. The agent has the exact signature to satisfy and physically cannot reach into business logic while doing it, because the adapter crate does not depend on private application internals. The task is scoped by the architecture, not by your prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ports are the spec.&lt;/strong&gt; An agent can write a correct use-case test from the trait definition alone, before a single adapter exists. In the template, the use-case tests run against a five-line fake repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tokio::test]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;shorten_then_resolve_roundtrips&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ShortenerService&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FakeRepo&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="nf"&gt;.shorten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-code"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="nf"&gt;.resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-code"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No database, no HTTP server, no fixtures. An agent asked to raise test coverage on the core has everything it needs from the port and nothing it needs to mock away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius is bounded by construction.&lt;/strong&gt; This is the big one. When an agent makes a wrong turn in a well-layered codebase, the mistake stays inside the layer it was working in. A bad decision in the HTTP adapter cannot corrupt the domain, because the compiler will not let the domain see the adapter. You get to be wrong locally, which is the only kind of wrong that is cheap to fix. In a codebase where a database call can appear inside a request handler, there is no such floor. A confused edit propagates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The structure survives context limits.&lt;/strong&gt; An agent does not need to load the whole repository to work on one adapter. The relevant surface is the port it implements plus the crate it lives in. The architecture pre-chunks the codebase into units small enough to reason about in isolation, which is the same reason it was pleasant for humans in the first place. Good structure is good structure; agents just raise the stakes on having it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;Do not start by rewriting everything into four crates. Start with the one rule and one seam. Pick the dependency that hurts most (usually the database) and define it as a trait in your core. Move the concrete client behind an adapter that implements the trait. Wire it up in one place. You will feel the core get quieter almost immediately, because it stops importing things that have nothing to do with your business.&lt;/p&gt;

&lt;p&gt;Then, if it earns its keep, split the rings into crates so the compiler enforces the rule you have been maintaining by hand. Rust makes that enforcement free, and free enforcement is the difference between an architecture you have and an architecture you had six months ago.&lt;/p&gt;

&lt;p&gt;The full template compiles, tests, and runs: &lt;a href="https://github.com/llmgraph-ai/hexagonal-rust-template" rel="noopener noreferrer"&gt;github.com/llmgraph-ai/hexagonal-rust-template&lt;/a&gt;. Clone it, run &lt;code&gt;cargo test&lt;/code&gt;, delete the URL shortener, and keep the shape.&lt;/p&gt;




&lt;p&gt;This is the kind of structured backend we care about at &lt;a href="https://llmgraph.ai" rel="noopener noreferrer"&gt;LLMGraph&lt;/a&gt;, where we build LLM and AI-agent workflows that people deploy as APIs. Legible structure is what lets both our team and our tools move fast without breaking the parts that matter.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>architecture</category>
      <category>ai</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>What it actually costs to run a RAG chatbot</title>
      <dc:creator>LLMGraph</dc:creator>
      <pubDate>Tue, 28 Jul 2026 12:06:38 +0000</pubDate>
      <link>https://dev.to/llmgraph/what-it-actually-costs-to-run-a-rag-chatbot-2hoe</link>
      <guid>https://dev.to/llmgraph/what-it-actually-costs-to-run-a-rag-chatbot-2hoe</guid>
      <description>&lt;p&gt;"How much will this cost to run?" is the question that kills more AI side projects than any technical problem. People build a working RAG chatbot in a weekend, then freeze because they have no idea whether serving it costs 5 dollars a month or 5,000. This post breaks the bill into its real parts so you can estimate before you commit.&lt;/p&gt;

&lt;p&gt;I'll use round numbers and current-ish prices. Yours will differ. The point is the shape of the bill, not the exact digits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bill has four line items
&lt;/h2&gt;

&lt;p&gt;A retrieval-augmented chatbot spends money in four places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Embedding your documents (one time, plus updates)&lt;/li&gt;
&lt;li&gt;Storing the vectors&lt;/li&gt;
&lt;li&gt;Embedding each incoming question&lt;/li&gt;
&lt;li&gt;Generating each answer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three of those four are close to free. One of them is the whole bill. Guess which before you read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Embedding your documents
&lt;/h2&gt;

&lt;p&gt;You embed each chunk of your documents once. Embedding is cheap. Most providers charge a few cents per million tokens.&lt;/p&gt;

&lt;p&gt;Say you have 200 documents averaging 3,000 words each. That's roughly 800,000 tokens. At current embedding prices that is a fraction of a cent to a few cents, total, one time. Even 10,000 documents lands in the low single-dollar range.&lt;/p&gt;

&lt;p&gt;If your documents never change, you pay this once and forget it. If they change daily, you re-embed the changed ones, which is still pennies.&lt;/p&gt;

&lt;p&gt;Line item: negligible.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Storing the vectors
&lt;/h2&gt;

&lt;p&gt;Each chunk becomes a vector, usually 768 or 1536 numbers. A few thousand chunks is a few megabytes. You can hold it in memory, in a local file, or in a hosted vector database.&lt;/p&gt;

&lt;p&gt;For a small project, a local store (Chroma, SQLite with a vector extension, FAISS on disk) costs nothing beyond the machine you already run. Hosted vector databases start free and stay cheap until you're storing millions of vectors.&lt;/p&gt;

&lt;p&gt;Line item: zero to a few dollars a month until you're at real scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Embedding each question
&lt;/h2&gt;

&lt;p&gt;Every question a user asks gets embedded so you can search. A question is short, maybe 20 tokens. At embedding prices that is far less than a hundredth of a cent per question.&lt;/p&gt;

&lt;p&gt;A thousand questions a day is still cents per month.&lt;/p&gt;

&lt;p&gt;Line item: negligible.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Generating the answer
&lt;/h2&gt;

&lt;p&gt;Here's the bill.&lt;/p&gt;

&lt;p&gt;Every answer is a call to a language model, and the input to that call is not just the question. It's the question plus the chunks you retrieved plus your instructions. That's the expensive part, because generation models charge per token and you're feeding them a few thousand tokens of context every single time.&lt;/p&gt;

&lt;p&gt;Rough math. Say each answer sends 2,000 tokens of context and gets back 400 tokens. On a mid-tier model at a few dollars per million input tokens and a bit more for output, one answer costs somewhere around half a cent to two cents depending on the model.&lt;/p&gt;

&lt;p&gt;Now multiply by traffic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100 answers a day: roughly 1 to 6 dollars a month&lt;/li&gt;
&lt;li&gt;1,000 answers a day: roughly 15 to 60 dollars a month&lt;/li&gt;
&lt;li&gt;10,000 answers a day: roughly 150 to 600 dollars a month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That range is wide because model choice swings it more than anything else. A frontier model can cost 10 to 20 times what a smaller model costs for the same job. For a lot of support and lookup use cases, a smaller model with good retrieval beats a frontier model with sloppy retrieval, and costs a tenth as much.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moves the number
&lt;/h2&gt;

&lt;p&gt;Once you see that generation is the bill, the levers are obvious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model choice.&lt;/strong&gt; The single biggest lever. Try the cheapest model that passes your quality bar before reaching for the expensive one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context size.&lt;/strong&gt; Retrieving 15 chunks instead of 5 triples your input tokens for often worse answers. Fewer, better chunks cost less and read better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching.&lt;/strong&gt; If the same questions come up again and again, and in support they do, cache the answers. A cache hit costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trimming instructions.&lt;/strong&gt; A 600-token system prompt sent on every call adds up. Tighten it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A quick estimate you can do right now
&lt;/h2&gt;

&lt;p&gt;Take your expected answers per day, multiply by 30 for the month, multiply by roughly 1 cent. That's your rough monthly generation bill on a mid-tier model. Everything else on the bill is rounding error at small scale.&lt;/p&gt;

&lt;p&gt;So a chatbot answering 300 questions a day costs on the order of 10 dollars a month to run. That's the number that unfreezes the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;If you'd rather not stand up the embedding, storage, and generation plumbing yourself and meter each piece, that's the kind of thing &lt;a href="https://llmgraph.ai" rel="noopener noreferrer"&gt;LLMGraph&lt;/a&gt; handles. You build the workflow, it runs the retrieval and the model calls, and you get a deployed API and chat widget. The cost shape above is still what's happening underneath, so it's worth understanding either way.&lt;/p&gt;

&lt;p&gt;The takeaway: a RAG chatbot is cheap to run at small scale, generation is the only line item that matters, and model choice is the dial that turns a 10-dollar bill into a 200-dollar one. Estimate before you build, and the "what will this cost" freeze goes away.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Turn a folder of PDFs into a question-answering API in an afternoon</title>
      <dc:creator>LLMGraph</dc:creator>
      <pubDate>Tue, 28 Jul 2026 03:58:48 +0000</pubDate>
      <link>https://dev.to/llmgraph/turn-a-folder-of-pdfs-into-a-question-answering-api-in-an-afternoon-14kg</link>
      <guid>https://dev.to/llmgraph/turn-a-folder-of-pdfs-into-a-question-answering-api-in-an-afternoon-14kg</guid>
      <description>&lt;p&gt;Someone hands you 200 PDFs and asks for a "chatbot that knows this stuff." You could read all 200. Or you could build a small API that reads them for you and answers questions with citations. This post is the second option, start to finish, with the parts that usually break called out.&lt;/p&gt;

&lt;p&gt;I'll show the mechanics first so you understand what's happening, then a shorter path if you don't want to run the plumbing yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're actually building
&lt;/h2&gt;

&lt;p&gt;A question-answering API over your own documents is four moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingest.&lt;/strong&gt; Read each PDF, pull out the text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk.&lt;/strong&gt; Split that text into pieces small enough to search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed and store.&lt;/strong&gt; Turn each chunk into a vector, keep it in a store you can query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer.&lt;/strong&gt; At question time, find the closest chunks and hand them to a model with the question.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;People call the whole thing RAG (retrieval-augmented generation). The name makes it sound heavier than it is. It's search, then a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: get the text out
&lt;/h2&gt;

&lt;p&gt;PDFs are a mess. Some are clean text, some are scanned images, some have two columns that extract in the wrong order. Start simple with &lt;code&gt;pypdf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pypdf&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PdfReader&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where it breaks: scanned PDFs return empty strings because there's no text layer, only pixels. If &lt;code&gt;extract_text()&lt;/code&gt; gives you nothing, those files need OCR (Tesseract, or a hosted vision model). Check for empty output early so you don't silently index blank pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: chunk the text
&lt;/h2&gt;

&lt;p&gt;You can't embed a whole document as one vector and expect good answers. Retrieval works on pieces. The size of those pieces matters more than most tutorials admit.&lt;/p&gt;

&lt;p&gt;Too small (a sentence) and each chunk loses its context. Too big (a whole page) and the model gets a wall of mostly-irrelevant text and the answer drifts. A reasonable starting point is 500 to 800 tokens per chunk with a small overlap so a sentence split across a boundary isn't lost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This splits on whitespace, which is crude but fine to start. If your documents have clear structure (headings, sections), splitting on those boundaries beats a fixed window because a chunk that matches a section is a chunk that makes sense on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: embed and store
&lt;/h2&gt;

&lt;p&gt;An embedding turns a chunk into a list of numbers that captures its meaning. Similar text lands near similar text. You query by embedding the question the same way and finding the nearest chunks.&lt;/p&gt;

&lt;p&gt;For a folder of 200 PDFs you do not need a managed vector database. A local store is plenty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chromadb&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chromadb.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;embedding_functions&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chromadb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PersistentClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./store&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedding_functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DefaultEmbeddingFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_or_create_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding_function&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ef&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_texts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;))],&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;metadatas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the source filename in the metadata. That's what lets you cite where an answer came from, which is the difference between a demo and something people trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: answer with citations
&lt;/h2&gt;

&lt;p&gt;At question time: embed the question, pull the top handful of chunks, put them in the prompt, ask the model to answer only from what it was given.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_texts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;n_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadatas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer the question using only the context below. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If the context does not contain the answer, say so.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Question: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_your_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Claude, GPT, a local model, your choice
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Sources: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things that matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;n_results&lt;/code&gt;.&lt;/strong&gt; Five is a good default. More context is not better. Past a point you're paying for tokens and diluting the signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "say so" instruction.&lt;/strong&gt; Without it the model fills gaps with plausible fiction. With it you get "the documents don't cover this," which is the honest answer and the one that keeps people trusting the system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wrap &lt;code&gt;answer()&lt;/code&gt; in any web framework and you have an API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/ask&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this gets hard in production
&lt;/h2&gt;

&lt;p&gt;The afternoon version above works. The things that turn it into a real project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Re-indexing.&lt;/strong&gt; Documents change. You need a way to update or drop chunks without rebuilding everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bad retrieval.&lt;/strong&gt; When answers are wrong it's usually retrieval, not the model. The right chunk never made it into the context. Logging what got retrieved for each question is the first debugging tool you'll want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access control.&lt;/strong&gt; If different people should see different documents, retrieval has to filter by permission before it ranks. Bolting this on later is painful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Embeddings are cheap. The generation call is not, and it scales with traffic. Caching common questions helps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The shorter path
&lt;/h2&gt;

&lt;p&gt;If you'd rather not run the ingest, chunking, vector store, and prompt loop yourself, this is the exact shape of thing &lt;a href="https://llmgraph.ai" rel="noopener noreferrer"&gt;LLMGraph&lt;/a&gt; builds. You point it at your documents, it handles the chunking and retrieval, and you get a REST API and an embeddable chat widget out the other side. The four steps above still happen. You just don't maintain them.&lt;/p&gt;

&lt;p&gt;Either way, the mental model is the same: read, chunk, embed, retrieve, answer. Once that clicks, "chatbot that knows our stuff" stops sounding like a research project and starts sounding like an afternoon.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Add an AI Customer Support Assistant — Without Writing Code</title>
      <dc:creator>LLMGraph</dc:creator>
      <pubDate>Mon, 13 Jul 2026 22:55:46 +0000</pubDate>
      <link>https://dev.to/llmgraph/how-to-add-an-ai-customer-support-assistant-without-writing-code-38fp</link>
      <guid>https://dev.to/llmgraph/how-to-add-an-ai-customer-support-assistant-without-writing-code-38fp</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://llmgraph.ai/blog/no-code-customer-support-assistant" rel="noopener noreferrer"&gt;LLMGraph blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your customers expect answers fast — at 9am on a Tuesday and at 2am on a Saturday. Meanwhile your support team is finite, and a large share of the questions they field are the same ones, over and over: where's my order, how do I reset this, what does this plan include. Every leader has heard that an AI assistant can take that load off. Most assume it's a months-long engineering project. It isn't anymore.&lt;/p&gt;

&lt;p&gt;Here is the entire build, start to finish, in a short demo — a working customer support assistant, no code written:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/AVk7xhOiMCc"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What an AI customer support assistant actually does
&lt;/h2&gt;

&lt;p&gt;A good support assistant isn't a generic chatbot that guesses. It answers from &lt;strong&gt;your&lt;/strong&gt; knowledge and knows its limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Answers instantly, around the clock&lt;/strong&gt; — in your product's voice, without adding headcount for every timezone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounded in your own content&lt;/strong&gt; — your docs, help center, and policies, via retrieval (RAG), so it gives the real answer instead of a plausible-sounding one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knows its limits&lt;/strong&gt; — recognizes the complex or sensitive cases that belong with your team and points customers there instead of bluffing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lives where customers already are&lt;/strong&gt; — an embeddable chat widget on your site or app, or wired into your stack through an API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this used to take an engineering team
&lt;/h2&gt;

&lt;p&gt;The reason so many companies shelved the idea is that the traditional build is genuinely involved. To do it yourself you'd choose and host a model, build a retrieval pipeline (chunk your documents, generate embeddings, stand up a vector database, wire up search), tune prompts and guardrails, build the chat widget, then deploy, scale, and monitor the whole thing. That's specialized hires and a quarter of work before a single customer is helped.&lt;/p&gt;

&lt;h2&gt;
  
  
  How LLMGraph turns it into a visual build
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://llmgraph.ai" rel="noopener noreferrer"&gt;LLMGraph&lt;/a&gt; collapses that stack into a visual workflow. You design the assistant as a graph on a canvas — or just describe what you want in chat and let LLMGraph assemble it — and the plumbing is handled for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Point it at your documents; LLMGraph does the embeddings and retrieval so answers stay grounded.&lt;/li&gt;
&lt;li&gt;Shape the assistant's role, tone, and boundaries without writing code.&lt;/li&gt;
&lt;li&gt;Ship it in one click to a live &lt;strong&gt;chat widget&lt;/strong&gt; you paste onto any page, plus a &lt;strong&gt;REST API&lt;/strong&gt; for deeper integration. No infrastructure to manage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That flow — knowledge in, behavior shaped, deployed — is the whole video above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can stand up this afternoon
&lt;/h2&gt;

&lt;p&gt;A first working assistant is four steps, not four sprints:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add your knowledge.&lt;/strong&gt; Upload docs or connect the help content that already answers your common questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shape behavior.&lt;/strong&gt; Set the assistant's role, tone, and what it should leave to your team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on the canvas.&lt;/strong&gt; Ask it real customer questions and refine until the answers are right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy.&lt;/strong&gt; Drop the chat widget on your site, or call the API from your product.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The business case for leaders
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coverage&lt;/strong&gt; — 24/7 first-response without hiring for every shift and region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deflection&lt;/strong&gt; — repetitive questions get handled automatically, freeing your team for the complex, high-value cases only a person should touch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; — the same accurate answer every time, grounded in your single source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control&lt;/strong&gt; — your data, your brand, and a human in the loop where it matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed to value&lt;/strong&gt; — build and iterate in an afternoon, then improve it as you learn, instead of committing to a quarter-long project up front.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Start with one workflow
&lt;/h2&gt;

&lt;p&gt;You don't have to automate everything on day one. Pick the ten questions your team answers most, point an assistant at the docs that already answer them, and ship the widget on a single page. Watch what it handles, refine, and expand from there. The first version is the afternoon; the compounding value is everything you add after.&lt;/p&gt;

&lt;p&gt;If you want to see it against your own content, &lt;a href="https://llmgraph.ai/signin" rel="noopener noreferrer"&gt;start a free trial&lt;/a&gt; or &lt;a href="https://llmgraph.ai/pricing" rel="noopener noreferrer"&gt;look at the plans&lt;/a&gt; — every tier includes a 14-day free trial.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>nocode</category>
      <category>chatbot</category>
    </item>
  </channel>
</rss>
