<?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:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Adarsh's Substack]]></title><description><![CDATA[My personal Substack]]></description><link>https://adarshgodiyal.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!vUCT!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40b3ee0b-2ffa-4a7b-89ba-2fafbde492f2_144x144.png</url><title>Adarsh&apos;s Substack</title><link>https://adarshgodiyal.substack.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 12 Jun 2026 23:35:09 GMT</lastBuildDate><atom:link href="https://adarshgodiyal.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Adarsh Godiyal]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[adarshgodiyal@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[adarshgodiyal@substack.com]]></itunes:email><itunes:name><![CDATA[Adarsh Godiyal]]></itunes:name></itunes:owner><itunes:author><![CDATA[Adarsh Godiyal]]></itunes:author><googleplay:owner><![CDATA[adarshgodiyal@substack.com]]></googleplay:owner><googleplay:email><![CDATA[adarshgodiyal@substack.com]]></googleplay:email><googleplay:author><![CDATA[Adarsh Godiyal]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Why I Separated Memory from Reasoning in My Tax Advisory AI — and Why It Was the Right Call]]></title><description><![CDATA[Most AI systems that touch financial data eventually fail the same way: the LLM hallucinates a number it was never given, and someone files the wrong return.]]></description><link>https://adarshgodiyal.substack.com/p/why-i-separated-memory-from-reasoning</link><guid isPermaLink="false">https://adarshgodiyal.substack.com/p/why-i-separated-memory-from-reasoning</guid><dc:creator><![CDATA[Adarsh Godiyal]]></dc:creator><pubDate>Sat, 06 Jun 2026 17:08:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!vUCT!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40b3ee0b-2ffa-4a7b-89ba-2fafbde492f2_144x144.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most AI systems that touch financial data eventually fail the same way: the LLM hallucinates a number it was never given, and someone files the wrong return. I wanted to build something that simply could not do that, even if the prompt was ambiguous or the client history was thin. That constraint shaped every architectural decision in CAI &#8212; Chartered Accountant Intelligence.<br><br>What CAI Actually Does</p><p>Chartered Accountants in India manage a brutal cognitive load. A single CA often handles dozens of clients across multiple Assessment Years, each with their own salary slips, GST notices, deduction declarations, capital gains schedules, and scrutiny intimations from the Income Tax Department. Every new client interaction requires excavating old Form 16s, cross-referencing prior filings, and mentally reconstructing the client&#8217;s financial picture from scratch.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://adarshgodiyal.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Adarsh's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>CAI is built to carry that context instead. It&#8217;s a multi-agent system where each agent is specialized: one routes intent, one synthesizes advisory responses, one parses uploaded PDFs, one tracks notice deadlines, one computes year-over-year deltas, and one flags anomalies. All of them are anchored to a persistent memory store powered by <a href="https://github.com/vectorize-io/hindsight">Vectorize Hindsight</a> &#8212; a purpose-built agent memory system that provides vector-backed recall across sessions.</p><p>The frontend is a React/Vite SPA with a split-screen layout: on one side, a conversational advisory chat panel where CAs ask questions and upload documents; on the other, a Memory Audit View showing the indexed fact graph for the active client, including confidence scores and Assessment Year tags. The backend is FastAPI, and every agent execution is traced end-to-end in LangSmith.</p><p>Show Image <em>The CAI dashboard. Left: the advisory chat interface with multi-agent routing tags visible beneath each response. Right: the Memory Audit View showing client facts indexed by namespace, confidence score, and Assessment Year.</em></p><div><hr></div><h2>The Core Problem: Stateless LLMs in a Stateful Domain</h2><p>Before committing to the architecture, I had to be precise about what &#8220;context&#8221; actually means in tax work. It&#8217;s not just conversation history &#8212; it&#8217;s structured financial facts that need to survive not just within a session, but across years. When a CA asks &#8220;what was Ramesh&#8217;s effective tax rate in AY2023-24 compared to this year?&#8221;, the model needs to recall gross income figures, deductions claimed, and tax paid for two different Assessment Years and compare them without inventing either number.</p><p>Standard approaches fall apart here:</p><ul><li><p><strong>In-context stuffing</strong>: You could try including the full client history in the system prompt every time. For a three-year client with multiple documents, that&#8217;s thousands of tokens on every single query, most of which are irrelevant to the question at hand.</p></li><li><p><strong>RAG over a general vector store</strong>: You get retrieval, but you lose namespace isolation. Client A&#8217;s tax history should never surface in a retrieval call for Client B. You also lose the structured metadata &#8212; Assessment Year, confidence, document source &#8212; that makes a retrieved fact trustworthy rather than just plausible.</p></li><li><p><strong>Conversation memory APIs</strong>: These record <em>what was said</em>, not <em>what was verified</em>. A CA telling the model &#8220;Ramesh&#8217;s gross salary was &#8377;15 lakh&#8221; in one session and &#8220;&#8377;14 lakh&#8221; in another creates a contradiction the model has no way to resolve.</p></li></ul><p>What I needed was a dedicated memory layer that could store structured, verified facts, tag them with domain-specific metadata, filter recalls by namespace, and &#8212; critically &#8212; expose staleness so the model could flag uncertainty rather than paper over it.</p><p>That&#8217;s what <a href="https://hindsight.vectorize.io/">Hindsight&#8217;s persistent agent memory architecture</a> gives you.</p><div><hr></div><h2>The Architecture: Reasoning Layer and Memory Layer Are Decoupled</h2><p>The single most important design decision in CAI is that the LLM has no direct access to the database. It only sees what Hindsight retrieves.</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;         &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;    REASONING LAYER    &#9474;         &#9474;     MEMORY LAYER      &#9474;
&#9474;   (Groq / Llama-3)    &#9474;         &#9474; (Vectorize Hindsight) &#9474;
&#9474;                       &#9474;         &#9474;                       &#9474;
&#9474; &#8226; Intent Routing      &#9474;&#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9658;&#9474; &#8226; tax_history         &#9474;
&#9474; &#8226; Advisory Synthesis  &#9474;         &#9474; &#8226; notices             &#9474;
&#9474; &#8226; Anomaly Detection   &#9474;         &#9474; &#8226; deductions          &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;         &#9474; &#8226; income              &#9474;
                                  &#9474; &#8226; preferences         &#9474;
                                  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;</code></code></pre><p>Show Image <em>Full system architecture. The FastAPI backend is the only component that talks to both layers. LLM agents never query the vector store directly &#8212; they receive retrieved context as plain text injected into their prompts.</em></p><p>Every client fact lives in Hindsight under a hierarchical namespace key:</p><pre><code><code>client:{client_id}:{namespace}:{record_id}
# e.g. client:abcri1234d:tax_history:ay2024-25</code></code></pre><p>When a new Form 16 arrives, the Document Extraction Agent runs PyMuPDF over the PDF, applies compliance-specific regex patterns to pull out gross salary, TDS, and PAN, then writes the result to Hindsight via <code>aretain()</code>:</p><p>python</p><pre><code><code># From document.py &#8212; after regex extraction
fact_str = (
    f"Form 16 uploaded. Gross salary verified at &#8377;{gross:,}. "
    f"Total TDS: &#8377;{tds:,}. PAN: {pan}. AY: {ay}."
)
content = json.dumps({"key": key, "value": {"fact": fact_str}})
await hindsight.aretain(
    content=content,
    namespace=f"client:{client_id}:tax_history",
    tags=[client_id, "conf_95"]
)</code></code></pre><p>The <code>conf_95</code> tag is used downstream to signal retrieval quality. If the Advisory Agent gets back facts tagged with lower confidence, or where the vector timestamp indicates the memory is over nine months old, it&#8217;s instructed to include an explicit uncertainty warning in the response &#8212; rather than generating numbers with false authority.</p><div><hr></div><h2>How the Orchestrator Routes Intent</h2><p>The Orchestrator Agent is the entry point for every query. It runs <code>llama-3.3-70b-versatile</code> at temperature zero &#8212; deterministic, no creativity. Its only job is to read the query and a 500-character &#8220;mental model&#8221; snapshot of the client from Hindsight, then output a rigid XML routing schema.</p><p>python</p><pre><code><code># From orchestrator.py &#8212; the prompt schema instruction
"""
Output ONLY valid XML:
&lt;routing&gt;
  &lt;intent&gt;tax_query|notice|anomaly|advisory|yoy|document|general&lt;/intent&gt;
  &lt;agents&gt;memory,advisory&lt;/agents&gt;
  &lt;urgency&gt;high|normal|low&lt;/urgency&gt;
  &lt;context_needed&gt;tax_history,notices,deductions,income,preferences&lt;/context_needed&gt;
&lt;/routing&gt;
"""</code></code></pre><p>The mental model snapshot is important. Rather than making the Orchestrator completely blind to client context before routing, Hindsight provides a 500-character synthetic summary &#8212; enough to distinguish &#8220;this client has open GST notices&#8221; from &#8220;this client has capital gains &#8212; route to the YoY agent&#8221; without dumping the full history into every routing call.</p><p>Once the XML is parsed, the backend fires concurrent <code>arecall()</code> calls for each required namespace:</p><p>python</p><pre><code><code># From main.py &#8212; parallel namespace retrieval
recall_tasks = [
    hindsight.arecall(
        query=user_query,
        namespace=f"client:{client_id}:{ns}",
        top_k=5
    )
    for ns in context_needed_namespaces
]
results = await asyncio.gather(*recall_tasks)</code></code></pre><p>This is where the <a href="https://vectorize.io/what-is-agent-memory">vector-backed agent memory approach</a> pays off concretely. The recall isn&#8217;t keyword search &#8212; it&#8217;s semantic. A query about &#8220;Section 80D health insurance deductions&#8221; will retrieve the right fact even if it was stored as &#8220;medical insurance premium for family, &#8377;25,000&#8221; because the embeddings capture meaning, not text overlap.</p><div><hr></div><h2>Zero-Hallucination Guardrails in the Advisory Agent</h2><p>The Advisory Agent is the only one that generates the final text the CA sees. Its system prompt contains an explicit, hard rule:</p><blockquote><p>If <code>memory_context</code> is empty for a given query, do not infer or estimate. State: &#8220;No verified data found for this client on this topic.&#8221;</p></blockquote><p>This is not a soft guideline. The prompt is structured so that the model receives the retrieved memory blocks under a labeled <code>[VERIFIED MEMORY]</code> section and the current-session document extractions under <code>[DOCUMENT CONTEXT]</code>. It is explicitly instructed that numbers outside these two sections cannot appear in the response.</p><p>In practice, this makes the system behave differently from a general-purpose chatbot in a very specific way: it will sometimes say &#8220;I don&#8217;t know&#8221; and that&#8217;s by design. A CA would rather see a clear &#8220;No prior data on Section 148 notice for this client&#8221; than a plausible-sounding but fabricated notice date.</p><p>The Anomaly Agent takes a similarly strict output discipline: it either emits <code>FLAG: &lt;description&gt; | SEVERITY: &lt;level&gt;</code> or a bare <code>CLEAR</code>. No narrative, no hedging. This early termination on <code>CLEAR</code> means the full anomaly analysis pipeline branch is skipped, saving tokens and latency on clean datasets.</p><div><hr></div><h2>Lessons Learned</h2><p><strong>1. Memory is a schema problem before it&#8217;s an embeddings problem.</strong> The temptation with vector stores is to throw everything in and let retrieval figure it out. That approach collapses quickly when facts from different years or different clients start bleeding into each other. Designing the namespace schema (<code>client:{id}:{type}:{record}</code>) before writing a single <code>retain()</code> call saved a lot of pain.</p><p><strong>2. Deterministic routing is worth the rigidity.</strong> Using temperature zero and a strict XML output format for the Orchestrator Agent means routing failures are parse errors, not semantic confusions. When something breaks, you get a clear stack trace, not a mysteriously wrong downstream response.</p><p><strong>3. Structured confidence metadata is what separates memory from history.</strong> The difference between a fact tagged <code>conf_95</code> from a verified Form 16 upload and a fact inferred from a chat message is enormous in a compliance context. <a href="https://hindsight.vectorize.io/">Hindsight&#8217;s</a> tagging system makes this distinction first-class. The Advisory Agent can &#8212; and does &#8212; treat them differently.</p><p><strong>4. Staleness detection should be a first-class output, not an afterthought.</strong> We added the nine-month timestamp check after realizing that a CA acting on a year-old TDS figure without knowing it was stale was arguably worse than getting no answer at all. Hindsight&#8217;s vector timestamps made this a two-line implementation rather than a separate data pipeline.</p><p><strong>5. Exponential backoff at the LLM call site isn&#8217;t optional.</strong> With six agents potentially firing in one request, any one of them hitting a rate limit becomes a user-facing error without retry logic. The <code>groq_call_with_retry</code> wrapper (max three retries, 1/2/4-second backoff) turned what would have been intermittent 500s into transparent, self-healing pauses.</p><div><hr></div><p>The broader principle CAI demonstrates is that <a href="https://vectorize.io/what-is-agent-memory">persistent agent memory</a> isn&#8217;t a nice-to-have for domains where facts have legal weight &#8212; it&#8217;s load-bearing infrastructure. Tax advisory, medical records, legal case management: any system that needs to be right about specific numbers across time and clients needs a memory layer with real structure, not just a longer context window.</p><p>The <a href="https://github.com/chhhee10/CAI">CAI source is on GitHub</a> if you want to dig into the agent implementations, the Hindsight namespace schema, or the LangSmith tracing setup. The <a href="https://hindsight.vectorize.io/">Hindsight documentation</a> covers the <code>retain()</code> / <code>recall()</code> API surface in depth if you&#8217;re thinking about applying the same memory pattern to your own domain.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://adarshgodiyal.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Adarsh's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>