<?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[Gaudiy AI Lab - Medium]]></title>
        <description><![CDATA[The Gaudiy AI Lab, operated by Gaudiy’s staff and collaborators, specializes in advancing research and developments in LLM Agents &amp; GenAI. We are developing Gaudiy Fanlink, which involves research into wallets, tokenomics, GenAI, and LLM Agents. Articles in EN. - Medium]]></description>
        <link>https://medium.com/gaudiy-ai-lab?source=rss----096fc41648f8---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Gaudiy AI Lab - Medium</title>
            <link>https://medium.com/gaudiy-ai-lab?source=rss----096fc41648f8---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 12 Nov 2026 17:21:30 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/gaudiy-ai-lab" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[GenJAX: Probabilistic Programming with Programmable Inference]]></title>
            <link>https://medium.com/gaudiy-ai-lab/96f0f83684dc?source=rss----096fc41648f8---4</link>
            <guid isPermaLink="false">https://medium.com/p/96f0f83684dc</guid>
            <category><![CDATA[gaudiy]]></category>
            <category><![CDATA[probabilistic-programming]]></category>
            <category><![CDATA[gen]]></category>
            <category><![CDATA[genjax]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Gaudiy Lab]]></dc:creator>
            <pubDate>Wed, 24 Dec 2025 00:11:36 GMT</pubDate>
            <atom:updated>2025-12-24T03:01:51.789Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sbG7rmFDEiCpUdGjCKm72w.jpeg" /></figure><p><em>Posted by </em><a href="https://www.linkedin.com/in/shubham-saha-ln/"><em>Shubham Saha</em></a><em>, AI Researcher of </em><a href="https://gaudiy.com/en/"><em>Gaudiy Inc</em></a><em>.</em></p><h3>Introduction</h3><p>Probabilistic programming has emerged as a powerful paradigm for building models that reason under uncertainty. In traditional probabilistic ML, practitioners typically handcraft inference algorithms — a laborious and error-prone process that requires deep mathematical expertise. Probabilistic programming changes this: rather than hand-coding complex inference algorithms, practitioners can express their models as programs and let the system handle the mathematical heavy lifting. But here’s the catch: general-purpose inference is often too slow, while hand-tuned algorithms are tedious to implement and maintain.</p><p>Enter <strong>Gen</strong> — a probabilistic programming system developed by the MIT Probabilistic Computing Project that gives you the best of both worlds: expressive modeling with programmable inference.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oz1oy-95PFJM9-6X4zdp9g.png" /><figcaption>Probabilistic Programs as the intersection of Symbolic Languages, Probabilistic Models, and Neural Networks <em>Source: </em><a href="https://jpcca.org/en/probcomp"><em>https://jpcca.org/en/probcomp</em></a></figcaption></figure><h3>What is Gen?</h3><p>Gen is an open-source probabilistic programming system that automates the tricky math and low-level implementation details of inference algorithms while giving users the flexibility to customize solutions for their specific models.</p><p>Originally built in Julia (as <a href="https://github.com/probcomp/Gen.jl">Gen.jl</a>), Gen lets you write generative models using ordinary code — including stochastic branching, loops, and recursion — while providing building blocks for constructing efficient inference algorithms.</p><h3>Key capabilities:</h3><ul><li><strong>Programmable inference</strong>: Compose inference algorithms (MCMC, SMC, variational inference) tailored to your model’s structure — mix Gibbs sampling for discrete variables with HMC for continuous ones</li><li><strong>Hybrid inference</strong>: Combine neural network proposals with principled Bayesian inference (out of scope for this post, but a key capability for scaling to complex domains)</li><li><strong>Stochastic structure</strong>: Model problems where the number of latent variables is unknown (e.g., object detection, clustering with unknown K)</li><li><strong>Programmable trade-offs</strong>: Use high-level abstractions for prototyping, drop down to optimized code for production</li><li><strong>Clean API</strong>: Automatic differentiation and inference primitives</li></ul><h3>Enter GenJAX: GPU-Accelerated Bayesian Inference for Python</h3><p><strong>GenJAX</strong> reimplements Gen’s core abstractions directly on JAX, bringing principled uncertainty quantification to Python with the performance characteristics production AI demands. For practitioners already working in the Python ecosystem, this means programmable inference without leaving your stack — leveraging JAX’s strengths:</p><ul><li><strong>GPU acceleration</strong>: JIT compilation and automatic batching of inference computations onto GPU devices</li><li><strong>JAX transformations</strong>: Full compatibility with jit, vmap, and automatic differentiation</li><li><strong>Familiar syntax</strong>: Use the @gen decorator to define generative functions in Python</li><li><strong>Built-in algorithms</strong>: Sampling Importance Resampling (SIR) and other inference methods out of the box</li></ul><h3>A Quick Taste</h3><p>In GenJAX, you define generative functions — computational objects representing probability measures over structured sample spaces. These functions can be composed, traced, and used with various inference algorithms:</p><pre>@gen<br>def my_model():<br>    slope = normal(0.0, 1.0) @ &quot;slope&quot;<br>    intercept = normal(0.0, 1.0) @ &quot;intercept&quot;<br>    # ... rest of the model</pre><h3>Why This Matters</h3><p>Traditional probabilistic programming systems offer limited control over inference: you either accept slow, general-purpose algorithms or implement everything from scratch. Gen’s <strong>programmable inference</strong> approach lets you compose algorithm components — importance sampling, MCMC kernels, gradient-based methods — to build inference strategies matched to your model’s structure.</p><p>This is particularly valuable for:</p><ul><li><strong>Bayesian nonparametric models</strong> with unbounded parameter spaces</li><li><strong>Structured prediction</strong> where the number of latent variables is itself unknown</li><li><strong>Scientific modeling</strong> requiring interpretable uncertainty quantification</li><li><strong>Robotics and perception</strong> systems that must reason about 3D scenes</li></ul><h3>Hands-On Example: Bayesian House Price Prediction</h3><p>Let’s put GenJAX to work on a real problem: predicting house prices using the <a href="https://www.kaggle.com/competitions/house-prices-advanced-regression-techniques">Kaggle House Prices dataset</a>. This dataset contains 79 features describing residential homes in Ames, Iowa, with the goal of predicting SalePrice.</p><p>Instead of a point estimate from traditional regression, we’ll build a Bayesian model that gives us <strong>uncertainty quantification</strong> — knowing not just <em>what</em> the predicted price is, but <em>how confident</em> we should be in that prediction.</p><h3>The Problem Setup</h3><p>The Kaggle dataset includes features like:</p><ul><li>GrLivArea: Above-ground living area (square feet)</li><li>OverallQual: Overall material and finish quality (1-10)</li><li>YearBuilt: Original construction date</li><li>TotalBsmtSF: Total basement square footage</li></ul><p>For simplicity, we’ll focus on a subset of numerical features and build a Bayesian linear regression model.</p><h3>Defining the Generative Model</h3><p>In GenJAX, we express our beliefs about how house prices are generated:</p><pre>import jax<br>import jax.numpy as jnp<br>import jax.random as jrandom<br>from genjax import gen, normal, uniform, Target, ChoiceMap<br>from genjax.inference.smc import ImportanceK<br><br>@gen<br>def house_price_model(X):<br>    &quot;&quot;&quot;<br>    Bayesian linear regression for house prices.<br>    X: feature matrix (n_samples, n_features)<br>    &quot;&quot;&quot;<br>    # Priors on regression coefficients<br>    # We expect positive coefficients for quality/size features<br>    coef_0 = normal(0.0, 1.0) @ &quot;coef_0&quot;<br>    coef_1 = normal(0.0, 1.0) @ &quot;coef_1&quot;<br>    coef_2 = normal(0.0, 1.0) @ &quot;coef_2&quot;<br>    coef_3 = normal(0.0, 1.0) @ &quot;coef_3&quot;<br>    # Prior on intercept (log-scale, since we&#39;ll predict log prices)<br>    intercept = normal(12.0, 1.0) @ &quot;intercept&quot;  # ~$160k baseline<br>    # Prior on noise (houses vary in price even with same features)<br>    noise_std = uniform(0.1, 0.5) @ &quot;noise_std&quot;<br>    # Generate predictions for each house<br>    coeffs = jnp.array([coef_0, coef_1, coef_2, coef_3])<br>    predictions = X @ coeffs + intercept<br>    # Likelihood: observed prices given our linear model<br>    # GenJAX supports vectorized distributions - the normal distribution<br>    # broadcasts over the predictions array, treating each as independent<br>    log_prices = normal(predictions, noise_std) @ &quot;log_prices&quot;<br>    return predictions</pre><p>This model encodes several assumptions:</p><ol><li><strong>Coefficients are uncertain</strong>: We don’t know the true relationship, so we place priors</li><li><strong>Prices are log-normal</strong>: Prices can’t be negative and are right-skewed (we model log prices, making multiplicative effects additive)</li><li><strong>Heterogeneous noise</strong>: Even similar houses sell for different prices</li></ol><blockquote><strong><em>Note</em></strong><em>: The model uses explicit coefficient variables and vectorized likelihood for JAX compatibility. The vectorized </em><em>normal(predictions, noise_std) broadcasts over all samples efficiently. See the </em><a href="https://medium.com/%5B../src/house_price_genjax.py%5D(https://github.com/thebongcook/genjax-intro/blob/main/src/house_price_genjax.py)"><em>full example code</em></a><em> for the complete implementation.</em></blockquote><h3>Running Inference</h3><p>Given observed sale prices, we want to infer the posterior distribution over coefficients:</p><pre>import pandas as pd<br>import numpy as np<br><br># Load and preprocess data<br>train_df = pd.read_csv(&quot;train.csv&quot;)<br>features = [&quot;GrLivArea&quot;, &quot;OverallQual&quot;, &quot;YearBuilt&quot;, &quot;TotalBsmtSF&quot;]<br>X = train_df[features].fillna(0).values.astype(np.float32)<br>y = np.log(train_df[&quot;SalePrice&quot;].values).astype(np.float32)<br># Standardize features<br>X_mean, X_std = X.mean(axis=0), X.std(axis=0)<br>X = (X - X_mean) / X_std<br># Convert to JAX arrays<br>X_train = jnp.array(X)<br>y_train = y<br># Build constraint map with observed prices (vectorized)<br>constraints = ChoiceMap.d({&quot;log_prices&quot;: y_train})<br># Create inference target<br>target = Target(<br>    house_price_model,<br>    (X_train,),<br>    constraints<br>)<br># Run importance sampling with multiple particles<br>key = jrandom.PRNGKey(42)<br>k_particles = 1000<br>alg = ImportanceK(target, k_particles=k_particles)<br>sub_keys = jrandom.split(key, k_particles)<br>_, posterior_samples = jax.vmap(alg.random_weighted, in_axes=(0, None))(sub_keys, target)</pre><h3>What We Get: Uncertainty, Not Just Predictions</h3><p>The beauty of this approach is that we don’t get a single coefficient estimate — we get a <strong>distribution</strong>:</p><pre># Extract posterior samples for coefficients<br>coef_samples = posterior_samples[&quot;coef_0&quot;]  # GrLivArea coefficient</pre><p>Running the <a href="https://medium.com/src/house_price_genjax.py">full example</a> on the Kaggle dataset with 1,000 importance sampling particles (using a 70/30 train/test split with 1,021 training samples and 439 test samples) gives us:</p><pre>Posterior estimates for coefficients:<br>--------------------------------------------------<br>    GrLivArea      :   0.116 ± 0.247  (95% CI: [-0.346, 0.588])<br>    OverallQual    :   0.174 ± 0.284  (95% CI: [-0.395, 0.729])<br>    YearBuilt      :   0.091 ± 0.236  (95% CI: [-0.379, 0.542])<br>    TotalBsmtSF    :   0.043 ± 0.240  (95% CI: [-0.395, 0.513])<br>    Intercept      :  12.026 ± 0.197<br>    Noise Std      :   0.398 ± 0.075<br><br>Holdout predictions with uncertainty (test set):<br>--------------------------------------------------<br>    Showing 10 of 439 holdout predictions:<br>    House | Actual Price  | Predicted (Mean) | 90% Credible Interval<br>    -----------------------------------------------------------------<br>        1 | $    112,000 | $        97,850 | $    34,101 - $   271,070<br>        2 | $    123,600 | $       121,401 | $    41,657 - $   355,248<br>        3 | $    240,000 | $       161,917 | $    47,347 - $   497,824<br>        4 | $    117,000 | $       116,189 | $    51,273 - $   263,998<br>        5 | $    281,213 | $       291,776 | $   105,307 - $   835,386<br>        6 | $    275,500 | $       266,397 | $    99,238 - $   685,142<br>        7 | $    130,000 | $       122,954 | $    54,602 - $   296,081<br>        8 | $    345,000 | $       322,223 | $   117,288 - $   957,747<br>        9 | $    123,000 | $       125,042 | $    54,339 - $   300,636<br>       10 | $    179,200 | $       187,161 | $    82,738 - $   448,681<br>Test set summary metrics (all 439 samples):<br>--------------------------------------------------<br>    Mean Absolute Error (log prices): 0.1209<br>    Mean Absolute Error (actual prices): $21,535<br>    90% Credible Interval coverage: 99.8%</pre><p>Notice how the credible intervals (CI) capture the actual prices — even on held-out data the model has never seen. The high coverage on the credible intervals shows the model provides honest uncertainty quantification that’s far more useful for decision-making than a single point estimate.</p><h4>Why Uncertainty Changes Decisions</h4><p>Consider House 134 vs House 135 from our results:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/513/1*5H71eJ3fIy-Ofx0Q4cqdpg.png" /></figure><p>A point-estimate model would say both houses are worth ~$350k — virtually identical. But with uncertainty quantification, decision-makers see a completely different picture:</p><p><strong>Mortgage lender</strong>: For House 135, the wide interval ($97k-$1.3M) signals high valuation uncertainty — the model’s uncertainty reflects how well the observed features explain prices for similar houses in the training data. A lender might require a 25% down payment instead of the standard 20%, or order additional appraisals. For House 134, the tighter interval provides more confidence, allowing standard terms — even though the predicted price is the same.</p><p><strong>Home buyer</strong> (let’s assume our home buyer is both data savvy and has access to a lot of housing data of his neighborhood): If both houses are listed at $380k (about 9% above prediction), the uncertainty tells different stories. For House 134, with its tighter CI, $380k sits well within the plausible range — reasonable to proceed. For House 135, with its wide CI ($97k-$1.3M), the same $380k asking price is harder to evaluate — the true value could be much lower <em>or</em> the house could actually be a bargain at $380k. Either way, the buyer should investigate further before committing.</p><p><strong>Real estate investor</strong>: What represents risk to a lender can represent opportunity to an investor. House 135’s wide uncertainty flags it as a property where the model lacks confidence — potentially due to unique characteristics that create arbitrage opportunities. An investor might prioritize investigating this house to find whether it’s mispriced, while House 134’s tighter bounds suggest the market has likely priced it correctly.</p><p>The key insight: <strong>identical predicted prices with different uncertainty bounds should lead to different actions</strong>. Point estimates hide this crucial information.</p><h3>Extending the Model: Handling Outliers</h3><p>Real estate data often has outliers. Now imagine a 2,000 sqft house in a good neighborhood that our model predicts should sell for $450,000 — but it actually sold for $180,000 because it was a foreclosure. Without outlier handling, this single data point would drag our coefficients away from their true values, hurting predictions for all normal sales.</p><p>In a traditional workflow, you’d need to derive new inference equations from scratch. But with GenJAX, I can simply extend the model with stochastic branching:</p><pre>from genjax import switch<br><br># Normal houses: tight Gaussian around prediction<br>@gen<br>def inlier_obs(mean, noise_std):<br>    return normal(mean, noise_std) @ &quot;obs&quot;<br># Outliers: broad uniform distribution<br>@gen<br>def outlier_obs(mean, noise_std):<br>    return uniform(10.0, 14.0) @ &quot;obs&quot;<br># switch selects branch by index: 0 → inlier_obs, 1 → outlier_obs<br>obs_model = switch(inlier_obs, outlier_obs)<br>@gen<br>def robust_house_price_model(X):<br>    # ... coefficients and intercept as before ...<br>    for i in range(X.shape[0]):<br>        # Is this house an outlier?<br>        is_outlier = flip(0.05) @ f&quot;outlier_{i}&quot;<br>        # switch requires integer index; flip returns bool<br>        idx = is_outlier.astype(jnp.int32)<br>        log_price = obs_model(idx, (predictions[i], noise_std), (predictions[i], noise_std)) @ f&quot;y_{i}&quot;</pre><p>This is where Gen’s programmable inference shines — you can mix Gibbs sampling for the discrete outlier indicators with Hamiltonian Monte Carlo (HMC) for the continuous coefficients.</p><h3>Key Takeaways</h3><ul><li><strong>Practical applicability</strong>: As shown in the house price example, GenJAX can tackle real problems while providing honest uncertainty estimates</li><li><strong>Programmable inference</strong>: Gen lets you control the algorithm while automating the math — combine importance sampling with MCMC kernels as needed</li><li><strong>Uncertainty quantification</strong>: Instead of point estimates, get full posterior distributions with credible intervals — far more useful for real-world decision-making</li><li><strong>Expressive modeling</strong>: Handle complex scenarios like outlier detection through stochastic branching, mixing discrete and continuous inference</li></ul><h3>Conclusion</h3><p>Gen represents a shift in how we think about probabilistic programming: instead of treating inference as a black box, it gives practitioners the tools to build custom algorithms tailored to their models. With GenJAX, these capabilities are now accessible to the broader Python/AI community with the performance benefits of JAX.</p><p>Whether you’re building Bayesian models for scientific research or developing perception systems that need to reason under uncertainty, GenJAX offers a compelling middle ground between flexibility and automation. At Gaudiy, we focus heavily on AI Agents, and probabilistic programming offers a principled framework for quantifying uncertainty in such systems and hence help improve calibration and robustness.</p><h3>References</h3><ul><li><a href="https://www.gen.dev/">Gen Official Website</a></li><li><a href="https://genjax.gen.dev/">GenJAX Documentation</a></li><li><a href="https://github.com/genjax-community/genjax">GenJAX GitHub Repository</a></li><li><a href="https://github.com/probcomp/Gen.jl">Gen.jl GitHub Repository</a></li><li><a href="https://github.com/probcomp">MIT Probabilistic Computing Project</a></li><li><a href="https://www.gen.dev/tutorials/">Gen Tutorials</a></li><li><a href="https://josephausterweil.github.io/probintro/intro/index.html">Probability &amp; Probabilistic Computing Tutorial</a></li><li><a href="https://www.kaggle.com/competitions/house-prices-advanced-regression-techniques">Kaggle House Prices Competition</a></li><li><a href="https://www.cs.cmu.edu/~fsaad/assets/papers/2019-CusumanoTownerEtAl-PLDI.pdf">Cusumano-Towner, M. F., Saad, F. A., Lew, A., &amp; Mansinghka, V. K. (2019). Gen: A General-Purpose Probabilistic Programming System with Programmable Inference. PLDI 2019.</a></li></ul><p>More Articles：</p><p><a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a></p><p>About Company：</p><p><a href="https://gaudiy.com/en">Gaudiy Inc. | Moving forward in a new era with fans</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=96f0f83684dc" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gaudiy-ai-lab/96f0f83684dc">GenJAX: Probabilistic Programming with Programmable Inference</a> was originally published in <a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Improving Custom Pronunciation of Voice Agents]]></title>
            <link>https://medium.com/gaudiy-ai-lab/83fad6f31611?source=rss----096fc41648f8---4</link>
            <guid isPermaLink="false">https://medium.com/p/83fad6f31611</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[tts]]></category>
            <category><![CDATA[gaudiy]]></category>
            <category><![CDATA[voice-agent]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Gaudiy Lab]]></dc:creator>
            <pubDate>Sat, 13 Dec 2025 00:56:27 GMT</pubDate>
            <atom:updated>2025-12-13T00:56:26.071Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zb1RZXVLMIQSy8RZT3Wmtg.jpeg" /></figure><p><em>Posted by </em><a href="https://x.com/rockeystar"><em>Sandeep Kumar Nayak</em></a><em>, AI Engineer of </em><a href="https://gaudiy.com/en/"><em>Gaudiy Inc</em></a><em>.</em></p><h3>Introduction</h3><p>Hi, I’m an engineer on the AI team at Gaudiy.</p><p>When developing applications with Text-to-Speech (TTS), there’s one problem you’ll inevitably face: <strong>proper noun pronunciation issues</strong>.</p><p>For example, when an English TTS reads the character name “AI” (pronounced “Ah-ee”) from the anime “Oshi no Ko”, it might pronounce it as “A.I.” (artificial intelligence) instead of spelling out “A-I” as two separate letters. For user-facing audio experiences, this is a critical problem that can break immersion and confuse users.</p><p>In this article, I’ll provide a comprehensive guide covering:</p><ol><li><strong>The nature of TTS pronunciation problems</strong> — Why they occur and their impact</li><li><strong>Available solutions</strong> — Comparing different approaches</li><li><strong>Our implementation</strong> — A custom pronunciation dictionary for Cartesia TTS</li><li><strong>Best practices</strong> — Lessons learned from production deployment</li></ol><p>Whether you’re building a voice assistant, audiobook narrator, or any speech-enabled application, this guide will help you tackle pronunciation challenges effectively.</p><h3>Background: How TTS Works</h3><p>Before diving into the problem, let’s understand how modern TTS systems work.</p><h4>The TTS Pipeline</h4><p>Modern neural TTS systems typically follow this pipeline:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/887/1*37j50vDPGGMcyIpqUnYVMw.png" /></figure><p>The critical component for pronunciation is the <strong>Grapheme-to-Phoneme (G2P)</strong> conversion. This is where the system decides how to pronounce each word. G2P models are trained on dictionaries and text corpora, which means they learn “common” pronunciations but struggle with:</p><ul><li><strong>Out-of-vocabulary words</strong>: Names, brands, and neologisms not in training data</li><li><strong>Heteronyms</strong>: Words spelled the same but pronounced differently based on context</li><li><strong>Domain-specific terminology</strong>: Technical jargon, acronyms, and abbreviations</li></ul><h4>Why Neural TTS Makes This Harder</h4><p>Ironically, modern neural TTS systems can make pronunciation problems <em>worse</em> than older concatenative systems. Here’s why:</p><ol><li><strong>End-to-end learning</strong>: Neural models learn pronunciation implicitly, making it harder to override specific words</li><li><strong>Context-dependent</strong>: The model considers surrounding words, which can cause unexpected pronunciation changes</li><li><strong>Black box behavior</strong>: It’s difficult to predict how the model will pronounce a given input</li></ol><p>This is why explicit pronunciation control mechanisms are essential.</p><h3>The Nature of Pronunciation Problems</h3><h4>Why TTS Mispronounces Proper Nouns</h4><p>TTS engines are trained on massive text datasets, but proper noun pronunciation presents several challenges:</p><p><strong>Homographs</strong><br> Example: AI (person’s name)<br> Expected: A-I (spelled out)<br> Actual: “eye” or “Aa-ee”</p><p><strong>Acronym ambiguity</strong><br> Example: GUI<br> Expected: G-U-I<br> Actual: “gooey”</p><p><strong>Domain-specific terms</strong><br> Example: NFT<br> Expected: N-F-T<br> Actual: “nft” (as one word)</p><p><strong>Character names</strong><br> Example: Naruto<br> Expected: “Nah-roo-toh”<br> Actual: “Nah-roo-toe”</p><p>The most problematic cases are <strong>words that should be pronounced differently based on context</strong>.</p><h4>Real-World Impact</h4><p>Poor pronunciation has significant consequences:</p><ol><li><strong>User confusion</strong>: Users may not understand what’s being said</li><li><strong>Brand damage</strong>: Mispronouncing your own product name looks unprofessional</li><li><strong>Accessibility issues</strong>: Users relying on TTS for accessibility are most affected</li><li><strong>Immersion breaking</strong>: In entertainment applications, wrong pronunciations destroy the experience</li></ol><h4>Concrete Example: The “AI” Pronunciation Problem</h4><p>Let’s examine the “AI” problem in detail. The string “AI” requires different pronunciations depending on context:</p><p><strong>Context 1: Artificial Intelligence</strong></p><pre>Input: &quot;AI is transforming the world&quot;<br>Expected: &quot;A.I. is transforming the world&quot; (pronounced as initialism)</pre><p><strong>Context 2: Japanese Name</strong></p><pre>Input: &quot;AI hoshino is a famous idol&quot;<br>Expected: &quot;Ai(Ah-ee) Hoshino&quot; is a famous idol&quot; (pronounced as name)</pre><p><strong>Context 3: Our Application</strong></p><pre>Input: &quot;AI hoshino is a famous idol&quot;<br>Problem: TTS says &quot;eye hoshino&quot; or &quot;A.I. hoshino&quot;<br>We need: &quot;Ai(Ah-ee) Hoshino &quot; (letters spelled out, proper capitalization)</pre><p>The challenge is that “AI” appears in our training data overwhelmingly as “artificial intelligence”, so the TTS defaults to that pronunciation. We need to override this behavior for our specific use case.</p><h4>The Substring Problem</h4><p>Another critical issue is <strong>substring conflicts</strong>. Consider these dictionary entries:</p><pre>{<br>  &quot;AI&quot;: &quot;&lt;&lt;a|i&gt;&gt;&quot;,<br>  &quot;AI hoshino&quot;: &quot;&lt;&lt;a|i&gt;&gt; Hoshino&quot;<br>}</pre><p>If we’re not careful, “AI hoshino” might become “&lt;&lt;a|i&gt;&gt; hoshino” (note the lowercase “h”) because “AI” matches first. This is why <strong>pattern ordering matters</strong> — a theme we’ll return to throughout this article.</p><h3>The Solution: Custom Pronunciation Dictionary</h3><h4>1. Phonetic Symbols (IPA)</h4><p>International Phonetic Alphabet provides precise pronunciation:</p><pre>AI hoshino → /eɪ aɪ hoʊˈʃiːnoʊ/</pre><p><strong>Pros:</strong></p><ul><li>Linguistically accurate</li><li>Language-independent</li></ul><p><strong>Cons:</strong></p><ul><li>Requires specialized knowledge</li><li>Not all TTS systems support IPA input</li><li>Difficult to maintain for non-linguists</li></ul><h4>Text Preprocessing (Our Approach)</h4><p>Replace problematic text before sending to TTS:</p><pre>text = &quot;AI hoshino is a famous idol&quot;<br>text = apply_pronunciation_rules(text)<br># Result: &quot;&lt;&lt;a|i&gt;&gt; Hoshino is a famous idol&quot;</pre><p><strong>Pros:</strong></p><ul><li>TTS-agnostic (can switch providers)</li><li>Simple to implement and maintain</li><li>Dynamic updates without restart</li><li>Full control over replacement logic</li></ul><p><strong>Cons:</strong></p><ul><li>Requires TTS-specific notation for complex cases</li><li>Must handle pattern conflicts carefully</li></ul><p>We chose <strong>text preprocessing with TTS-specific notation</strong> because it offers the best balance of flexibility, maintainability, and control.</p><h3>Understanding Cartesia’s Phonetic Notation</h3><p>Cartesia TTS provides several mechanisms for controlling pronunciation:</p><h4>1. Sound Splitting with &lt;&lt; &gt;&gt;</h4><p>The most useful feature is splitting words into separate sounds:</p><pre>&quot;AI&quot; → pronounced as one sound (&quot;eye&quot;)<br>&quot;&lt;&lt;a|i&gt;&gt;&quot; → pronounced as separate letters (&quot;A&quot; then &quot;I&quot;)</pre><p>The | character separates distinct phonetic units. This is perfect for acronyms and initialisms.</p><h4>2. Emphasis and Pacing</h4><p>Cartesia also interprets punctuation for pacing:</p><pre>&quot;Hello, world&quot; → natural pause after comma<br>&quot;Hello... world&quot; → longer pause with ellipsis<br>&quot;Hello! World!&quot; → emphatic pronunciation</pre><h4>3. Handling Special Characters</h4><p>Some characters cause issues and should be removed:</p><pre>DISALLOWED_CHARACTERS = &quot;「」『』（）[]{}()&quot;</pre><p>These brackets and parentheses can confuse the TTS model and cause unexpected pauses or pronunciation changes.</p><h3>Implementation Details</h3><p>Now let’s dive into the implementation. We’ll build a complete pronunciation override system.</p><h4>System Architecture</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/758/1*VVPUPcguc8fJkTMUew0TUQ.png" /></figure><h4>Pronunciation Dictionary Data Structure</h4><p>The pronunciation dictionary uses a hierarchical JSON structure organized by language:</p><pre>{<br>  &quot;ja&quot;: {<br>    &quot;御社&quot;: &quot;おんしゃ&quot;,<br>    &quot;弊社&quot;: &quot;へいしゃ&quot;,<br>    &quot;漢字&quot;: &quot;かんじ&quot;,<br>    &quot;Gaudiy&quot;: &quot;ゴーディ&quot;<br>  },<br>  &quot;en&quot;: {<br>    &quot;AI hoshino&quot;: &quot;&lt;&lt;a|i&gt;&gt; Hoshino&quot;,<br>    &quot;Gaudiy&quot;: &quot;Gaudy&quot;,<br>    &quot;NFT&quot;: &quot;&lt;&lt;n|f|t&gt;&gt;&quot;,<br>    &quot;GUI&quot;: &quot;&lt;&lt;g|u|i&gt;&gt;&quot;,<br>    &quot;API&quot;: &quot;&lt;&lt;a|p|i&gt;&gt;&quot;<br>  },<br>  &quot;fr&quot;: {<br>    &quot;AI&quot;: &quot;a i&quot;,<br>    &quot;Gaudiy&quot;: &quot;Godi&quot;<br>  },<br>  &quot;es&quot;: {<br>    &quot;AI&quot;: &quot;a i&quot;<br>  },<br>  &quot;de&quot;: {<br>    &quot;AI&quot;: &quot;a i&quot;<br>  }<br>}</pre><p><strong>Design decisions:</strong></p><ol><li><strong>Language as top-level key</strong>: Allows language-specific pronunciations</li><li><strong>Simple string mapping</strong>: Easy to understand and maintain</li><li><strong>Extensible</strong>: Adding new languages requires no code changes</li><li><strong>Flat structure per language</strong>: Simplifies pattern matching</li></ol><h3>The Critical Rule: Longer Matches First</h3><p>This is the most important concept in pronunciation dictionary implementation.</p><h4>The Problem</h4><p>When you have overlapping patterns, the order of application determines the result:</p><pre># Dictionary entries<br>{<br>    &quot;AI&quot;: &quot;&lt;&lt;a|i&gt;&gt;&quot;,<br>    &quot;AI hoshino&quot;: &quot;&lt;&lt;a|i&gt;&gt; Hoshino&quot;<br>}</pre><p><strong>Scenario A: Shorter pattern first (WRONG)</strong></p><pre>Input:  &quot;AI hoshino is a famous idol&quot;<br>Step 1: Match &quot;AI&quot; → &quot;&lt;&lt;a|i&gt;&gt; hoshino is a famous idol&quot;<br>Step 2: Match &quot;AI hoshino&quot; → NO MATCH (already replaced!)<br>Result: &quot;&lt;&lt;a|i&gt;&gt; hoshino is a famous idol&quot;  ❌<br>        └── lowercase &quot;hoshino&quot; is wrong!</pre><p><strong>Scenario B: Longer pattern first (CORRECT)</strong></p><pre>Input:  &quot;AI hoshino is a famous idol&quot;<br>Step 1: Match &quot;AI hoshino&quot; → &quot;&lt;&lt;a|i&gt;&gt; Hoshino is a famous idol&quot;<br>Step 2: Match &quot;AI&quot; → no remaining matches<br>Result: &quot;&lt;&lt;a|i&gt;&gt; Hoshino is a famous idol&quot;  ✅<br>        └── proper capitalization preserved!</pre><h4>The Solution</h4><p>Always sort patterns by length (descending) before applying:</p><pre>sorted_items = sorted(<br>    replacements.items(),<br>    key=lambda x: len(x[0]),<br>    reverse=True  # Longest first<br>)</pre><p>This ensures that “AI hoshino” (11 characters) is always checked before “AI” (2 characters).</p><h3>Future Improvements</h3><h4>Planned Enhancements</h4><ol><li><strong>Context-aware pronunciation</strong>: Use NLP to determine pronunciation based on surrounding words</li><li><strong>User feedback integration</strong>: Allow users to report pronunciation issues directly</li><li><strong>A/B testing</strong>: Test pronunciation changes with subset of users</li><li><strong>Automatic dictionary suggestions</strong>: ML model to suggest new entries based on user queries</li><li><strong>Real-time updates</strong>: WebSocket-based dictionary updates without restart</li></ol><h4>Potential Architecture Evolution</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/497/1*fWdeKsxu7g7elcpeL6d2kg.png" /></figure><h3>Conclusion</h3><p>TTS pronunciation issues are a common challenge that can significantly impact user experience. In this article, we’ve covered:</p><h3>Key Takeaways</h3><ol><li><strong>Understand the problem</strong>: TTS models learn “common” pronunciations that may not fit your domain</li><li><strong>Choose the right approach</strong>: Text preprocessing with TTS-specific notation offers the best balance of flexibility and maintainability</li><li><strong>Implement correctly</strong>: The <strong>longer match first</strong> rule is critical for avoiding substring conflicts</li></ol><h3>Results</h3><p>After implementing this system:</p><ul><li><strong>100% accuracy</strong> on known pronunciation issues</li><li><strong>&lt; 1ms overhead</strong> for preprocessing</li><li><strong>Zero downtime</strong> dictionary updates via GCS</li><li><strong>Multi-language support</strong> with language-specific rules</li></ul><p>The custom pronunciation dictionary has become an essential component of our TTS pipeline, ensuring that proper nouns and domain-specific terms are pronounced correctly every time.</p><p>I hope this comprehensive guide helps you tackle TTS pronunciation challenges in your own applications. If you have questions or want to share your own solutions, feel free to reach out!</p><p>Thanks for reading!</p><h3>References</h3><ul><li><a href="https://docs.cartesia.ai/">Cartesia TTS Documentation</a></li><li><a href="https://www.internationalphoneticassociation.org/content/ipa-chart">International Phonetic Alphabet</a></li><li><a href="https://github.com/WorksApplications/Sudachi">Sudachi — Japanese Morphological Analyzer</a></li></ul><p>More Articles：</p><p><a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a></p><p>About Company：</p><p><a href="https://gaudiy.com/en">Gaudiy Inc. | Moving forward in a new era with fans</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=83fad6f31611" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gaudiy-ai-lab/83fad6f31611">Improving Custom Pronunciation of Voice Agents</a> was originally published in <a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</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 Contextual Datasets: Quantifying Memory Retrieval Performance]]></title>
            <link>https://medium.com/gaudiy-ai-lab/1fceba9ce689?source=rss----096fc41648f8---4</link>
            <guid isPermaLink="false">https://medium.com/p/1fceba9ce689</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[dataset]]></category>
            <category><![CDATA[gaudiy]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[context]]></category>
            <dc:creator><![CDATA[Gaudiy Lab]]></dc:creator>
            <pubDate>Mon, 10 Nov 2025 02:26:35 GMT</pubDate>
            <atom:updated>2025-11-10T02:26:33.653Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qE18AaOJWs31S0jFHDioTw.jpeg" /></figure><p><em>Posted by </em><a href="https://x.com/rockeystar"><em>Sandeep Kumar Nayak</em></a><em>, AI Engineer of </em><a href="https://gaudiy.com/en/"><em>Gaudiy Inc</em></a><em>.</em></p><h3>Introduction</h3><p>In recent years, with the growing adoption of LLM-based conversational AI, “contextual understanding of conversation” has become increasingly important. However, improving this ability takes more than new models. True contextual understanding depends on the <em>quality of the dataset</em> that trains and evaluates it. Real conversations are rarely self-contained — they rely on shared history, emotional tone, and subtle references. A strong dataset captures these nuances, enabling models to retrieve the right memories and respond meaningfully. In short, advancing contextual AI starts with data that reflects how people actually communicate. In this article, we introduce our approach to improving memory retrieval through contextual queries at Gaudiy by leveraging a carefully designed dataset. This dataset enables us to quantify performance on contextual memory query tasks and drive meaningful improvements in product quality.</p><h3>About Gaudiy and Its Product</h3><p>At Gaudiy, we’re integrating <strong>AI-based interactive chat agents</strong> into our products to enable meaningful, sustained conversations with users. Our goal is to make these AI-driven interactions feel deeply personal, responsive, and memorable.</p><p>The Gaudiy AI team consists of researchers and engineers working across natural language processing, retrieval systems, and human-AI interaction design. Our mission is to build emotionally intelligent AI that adapts to users in real time — leveraging memory and personalization to create rich, dynamic conversations. We combine academic research with product-driven experimentation to bring innovations from lab to life. One of our current focus areas involves experimental conversational features that allow users to engage with AI agents representing specific characters in collaboration with rights holders.</p><p>However, user messages are often vague or context-light (e.g., “Nice!” or “Really?”). In such cases, the AI risks retrieving irrelevant or generic content unless it understands the message in the context of recent dialogue. This presents a fundamental challenge in AI conversations: handling ambiguous user inputs accurately.</p><p>To tackle this, one of our key areas of improvement is the chat agent’s ability to understand and respond appropriately based on the context of prior conversations. A crucial part of this personalized experience is the AI’s capacity to retain and retrieve long-term memories — shaping ongoing dialogues into richer, more coherent, and contextually relevant interactions.</p><h3>Why Contextual Queries Matter — and Why the Right Dataset Is Key</h3><p>Understanding context lies at the heart of delivering meaningful conversational AI experiences. In some setups, such as when the entire prior conversation is stored directly in the prompt, there may be no need for separate memory retrieval steps.</p><p>However, to make systems scalable and cost-efficient, most agents rely on short-term context windows. For deeper personalization and long-term coherence, we must store and retrieve relevant long-term memories separately. Identifying which memories are contextually relevant — and retrieving them appropriately — requires an extra step: memory querying. To evaluate how well a system can perform this step, the role of the dataset becomes crucial.</p><p>We aim for our dataset to support evaluation across key dimensions that matter for real-world deployment:</p><ol><li><strong>More appropriate responses</strong>: Contextual understanding allows the AI to interpret queries in light of recent conversation history — improving clarity, reducing miscommunication, and increasing user engagement.</li><li><strong>Memory retrieval accuracy</strong>: A well-structured dataset helps benchmark how effectively AI agents retrieve relevant long-term memories.</li><li><strong>Quantitative Comparison across models</strong>: With ground truth and structured dialogue, our dataset enables model benchmarking using rigorous metrics such as MAP@10 .</li><li><strong>Model selection trade-offs</strong>: Our dataset lets us balance performance with cost and latency, guiding embedding model choices based on quantified results.</li></ol><h3>Illustrative Example: Why Context &amp; Memory Matters</h3><p>Consider this snippet from a conversation between AI assistant and a user.</p><pre>User: Have you read any new books recently?<br>AI: Yes, I recently started reading Harry Potter.<br>User: That&#39;s great. Which volume are you reading?<br>AI: The first one. It&#39;s really interesting!<br>User: Nice!</pre><p>This ensures that the AI retrieves relevant details — such as prior mentions of <em>Harry Potter</em> — and can respond with something like: “Yes! I’m excited to continue the story tonight.” rather than a generic affirmation.</p><h3>Long Term Memory History</h3><p>Now consider a case where long-term memory comes into play</p><blockquote><em>User: I’m feeling a bit anxious about tomorrow.</em></blockquote><p>Here, the phrase “your big audition” was not mentioned in the current chat. It was something the user mentioned weeks ago:</p><pre>Long-Term Memory in Database: &quot;User is preparing for a regional theater<br>audition on June 10th. It&#39;s important to them.&quot;</pre><p>Retrieving this information allows the AI to reference it meaningfully, even though it’s outside the recent dialogue window. Without this retrieval, the AI would respond generically. With long-term memory, it might say:</p><blockquote><em>“You’ve worked so hard for this audition — I believe in you.</em></blockquote><p><strong>What kind of dataset helps here?</strong></p><ul><li>It includes multiple turns of chat history.</li><li>It contains annotations or ground-truth for the intended memory reference. ie It simulates long-term memory entries linked to prior user sessions.</li><li>It contains evaluation annotations about which memory entries are contextually relevant given ambiguous prompts.</li></ul><p>This showcases the power of memory queries — not just for interpreting immediate context, but for maintaining emotional continuity and personalization over time.</p><p>In short, building a high-quality dataset isn’t just about data collection — it’s about unlocking measurable progress in contextual AI by enabling rich, context-aware evaluation</p><h3>Building on Previous Work</h3><p>In a prior internal experiment, one of our AI engineers explored contextual query rewriting techniques[1] — including single-shot, multi-shot, and hypothetical reply approaches. This exploration demonstrated how prompt engineering can significantly influence the relevance of memory retrieval, particularly by reducing embedding distances between user queries and relevant memories.</p><p>Cosine similarity remains a useful metric for evaluating retrieval effectiveness. This was complemented by qualitative checks — assessing whether the retrieved memory intuitively matched the user query within context. While this method supports rapid iteration and insight into model behavior, it is difficult to ensure consistent, comparable evaluation across different strategies.</p><p>To address this, our current work introduces a dataset specifically designed for grounded and measurable evaluation of contextual memory retrieval. Unlike existing public datasets, it targets ambiguous or open-ended Japanese conversational queries, providing ground-truth expectations for what should be retrieved. This allows us to objectively compare retrieval approaches — for example, using metrics such as <em>Mean Average Precision (MAP)</em> — and conduct comprehensive, reproducible evaluations of model performance.</p><h3>Our Solution</h3><p>To address these gaps, we developed a <strong>private Japanese conversation dataset</strong> with the following features:</p><ul><li><strong>Rich conversation history</strong> across multiple turns.</li><li><strong>Simulated long-term memory</strong> per user.</li><li><strong>Personalization layers</strong> including personality traits</li></ul><p>This dataset enables <strong>quantitative evaluation</strong> of how well embedding models perform when handling <strong>contextual memory retrieval</strong> tasks.</p><h3>Methodology</h3><h4>1. Data Preparation</h4><p>We created a <strong>context-enriched Japanese dialogue dataset</strong> involving:</p><p>- Multi-turn conversations.<br>- Ground-truth memory references.<br>- Annotated character personas.</p><p>Each user query in the dataset is paired with:</p><p>- A non-contextual version (standalone user input).<br>- A contextual version (query + prior conversation history).</p><p>This setup lets us measure retrieval quality under real-world, ambiguous input scenarios by directly comparing context-free and context-enriched queries.</p><h4>2. Performance Metric</h4><p>To evaluate retrieval, we used:<br>- <strong>Mean Average Precision @10 (MAP@10)</strong>: Measures how well the top 10 retrieved memories align with ground-truth relevant memories.</p><h3>Results</h3><h4>1. Contextual Queries Improve Model Performance</h4><p>Our analysis revealed compelling evidence that our dataset plays a crucial role in demonstrating the need for context-aware memory retrieval. Unlike public datasets, which often lack conversational continuity, our dataset includes rich contextual traces — making it possible to observe the discrepancies in memory retrieval performance when using different embedding models.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0TYHyYVmEoQ9u9AT.png" /></figure><p><em>Figure 1 : MAP@10 for contextual and non-contextual queries</em></p><p>We found that all models showed a clear improvement in precision when using contextual queries. Furthermore, some embedding models demonstrated significantly greater gains than others. Therefore, in Figure 2, we compare the relative performance improvements to identify which models achieved the highest increase.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*yAIdkz-8hdXDF9U2.png" /></figure><p><em>Figure 2 : MAP@10 improvement across models of various sizes.</em></p><p>As from both the figures, we see, across all models, <strong>adding context to the queries (contextual queries) improved retrieval performance significantly</strong>:<br><em>Average improvemen</em><strong><em>t</em></strong><em> in MAP: </em><strong><em>+20%<br></em></strong><em>Top model improvement: Nearly </em><strong><em>+30%</em></strong></p><blockquote><strong><em>Insight 1: </em></strong><em>Even weaker models with poor non-contextual performance showed consistent gains — highlighting how contextual understanding universally boosts performance.</em></blockquote><h4>2. Model Selection via Dataset Evaluation</h4><p>As from the Figure 1, open source models see <strong>huge jumps in MAP</strong> when context is included — bringing them close to top-tier models like:</p><ul><li>OpenAI/text-embedding-3-large</li><li>Google_gekkko_001</li></ul><p><strong>Context Reduces the Dependence on Model Scale</strong>:</p><ul><li>The performance advantage of expensive, large-scale models diminishes when contextual understanding is introduced at the query level.</li><li>This implies better ROI for real-world applications using smaller models with proper contextualization.</li></ul><blockquote><strong><em>Insight 2</em></strong><em><br></em><strong><em>Use a smaller model<br>Pre-process user queries with contextual enrichment<br></em></strong><em>Achieve </em><strong><em>similar accuracy with lower cost and latency</em></strong></blockquote><h3>Conclusion</h3><p>Through our newly introduced context-rich dataset, we have been able to quantitatively measure the performance of different text embedding models in our use case. The results not only validate the effectiveness of context-aware retrieval but also reveal each model’s capability to leverage conversational context — something that public datasets often fail to capture due to their lack of dialogue continuity.</p><p>While it has enabled evaluation of memory retrieval, we look ahead to also include the absence of end-to-end output scoring (e.g., user satisfaction) and unmeasured latency-cost tradeoffs — both of which we plan to address.</p><p>By enabling <strong>quantifiable evaluation</strong> on our dataset, we gain a <strong>surgical view</strong> into memory-retrieval methods and embedding models. Optimizing both in tandem yields systems that produce <strong>context-rich, personalized responses</strong> without inflating infrastructure cost — raising the bar for conversational AI beyond Gaudiy’s use case.</p><p><strong>Looking ahead,</strong> we aim to integrate <strong>user-level evaluation metrics</strong> and investigate <strong>lower-cost, high-context embedding models</strong> to further improve intelligence and scalability. To ensure these results stand up to scrutiny, <strong>we are preparing to submit our findings for peer review.</strong> We’ll share updates as that process unfolds — <strong>stay tuned</strong> as we continue to push the boundaries of context-aware AI.</p><h3>References</h3><p>[1] <a href="https://medium.com/gaudiy-web3-and-ai-lab/b628896107aa">https://medium.com/gaudiy-web3-and-ai-lab/b628896107aa</a></p><p>[2] Mean Average Precision, MAP@10 , <a href="https://www.evidentlyai.com/ranking-metrics/mean-average-precision-map">https://www.evidentlyai.com/ranking-metrics/mean-average-precision-map</a></p><p>[3] Embedding Models in NLP: <a href="https://aws.amazon.com/what-is/embeddings-in-machine-learning/">https://aws.amazon.com/what-is/embeddings-in-machine-learning/</a></p><p>[4] E5 :<a href="https://arxiv.org/abs/2402.05672">https://arxiv.org/abs/2402.05672</a></p><p>More Articles:</p><p><a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a></p><p>About Company:</p><p><a href="https://gaudiy.com/en">Gaudiy Inc. | Moving forward in a new era with fans</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1fceba9ce689" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gaudiy-ai-lab/1fceba9ce689">The Power of Contextual Datasets: Quantifying Memory Retrieval Performance</a> was originally published in <a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Computation and Language: The Semantic Calculator]]></title>
            <link>https://medium.com/gaudiy-ai-lab/5a9ca9fe4541?source=rss----096fc41648f8---4</link>
            <guid isPermaLink="false">https://medium.com/p/5a9ca9fe4541</guid>
            <category><![CDATA[gaudiy]]></category>
            <category><![CDATA[behavior-trees]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm-agent]]></category>
            <dc:creator><![CDATA[Gaudiy Lab]]></dc:creator>
            <pubDate>Fri, 03 Oct 2025 01:37:02 GMT</pubDate>
            <atom:updated>2025-10-03T04:40:27.865Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*80ZcGUwfmKMY3xZL3HjPfg.jpeg" /></figure><p><em>Posted by Malkoc Ognjen, AI Researcher of </em><a href="https://gaudiy.com/en/"><em>Gaudiy Inc</em></a><em>.</em></p><p>In the first part of this series on behavior tree agents, we looked at how behavior trees can bring structure to the often unpredictable world of Large Language Models (LLMs), enabling complex and emergent behaviors. Now, we’re building on that foundation to create something practical: a calculator that understands everyday language. Starting from that base, we’ll explore how the inherent prioritization in behavior trees can be harnessed to interpret natural language and guide meaningful computation.</p><p>The core idea put simply; when a task can be solved with explicit instructions in a programming language (rather than relying on the semantic interpretation of an LLM) the outcome is almost always more accurate and reliable. This is especially true for mathematics, where precision is critical.</p><p>An illustrative example is arithmetic operations in LLMs. This capability was an emergent property that appeared as models grew larger. More recent work has focused on pinpointing the mechanism: distinct activation circuits seem to support operations like addition or multiplication. This this also points to a limitation — while LLMs can manage basic arithmetic, extending that ability to perform more complex, less common, or out‑of‑distribution operations may be increasingly difficult to learn.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*tplZY_nPgImwOKeV.png" /></figure><p>Moreover, reinforcement learning that aligns LLMs by rewarding output containing convincing “reasoning” traces does not appear to improve things. The activation circuits responsible for basic operations seem to remain unchanged, or at least disconnected from the reasoning output⁰. In addition, recent studies ² ³ ⁴ ⁵ further suggest that the benefits of reasoning are not universal. Reference 5, in particular, even links performance gains to specific pre‑training. This implies that if a model is not sufficiently large, or has not been exposed to similar tasks during pre‑training, relying on reasoning alone may be ineffective. It is worth exploring alternatives.</p><p>With that in mind, let’s do so. Let’s make the LLM assume the role of a stressed-out doctoral student who does not want to, yet again, derive a symbolic expression, or calculate a well-known, but tedious, integral.<br>Let’s make use of the collective knowledge we already have at our disposal, tools that have been built specifically for this purpose. In particular, the Python programming language offers several advantages: it has a simple syntax, a vast ecosystem of scientific libraries — and, as luck would have it, there’s also an ever‑growing volume of Python code available online — ample material that feeds directly into the pre‑training phase.</p><p>With that back‑story, here’s the plan: we’ll use a behavior tree to enforce the core principle introduced earlier. The rule is straightforward: if a task can be solved programmatically, we solve it that way; for everything else, the LLM steps in as a fallback for solving tasks. <br>Here we will focus on mathematics and physics. When possible, we hand off numerical work to Python, where well‑chosen function can deliver dependable results. The high‑level idea is what we might call a <strong>semantic calculator</strong>: for tasks the model itself cannot reliably compute, the LLM acts as a translator between the user and the computer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/548/0*xeuodS0xAeaT7BAJ.png" /><figcaption>The high-level idea of our semantic calculator. For tasks the models is not capable of solving, the LLM serves as translator between computer and user.</figcaption></figure><p>The handing off work to Python, and resuming translation by an LLM is trivial since we can leverage the prioritization built into behavior trees. This setup lets us route queries intelligently: whenever a task is computable and expressible in Python, we default to that path, since the results are more dependable. Should that route fail, whether during function generation or result retrieval, we intentionally fall back to the LLM’s natural‑language reasoning. The design ensures a baseline accuracy no worse than using the LLM alone, while delivering clear gains whenever a task can be programmatically computed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/571/0*zM2-HjRCgocKWuCz.png" /><figcaption>Overview of how the assistant handles user queries by decomposing behaviour trees and reusing components like ‘Generate Response’ after solving programmatically. As in the first post, ? denote selectors, → are sequences, [C] conditional nodes and [A] are action nodes.</figcaption></figure><p>The figure above illustrates how user queries are handled. At the root, the system first checks whether the task can be solved programmatically. If so, the query is routed through the <em>Solve Programmatically</em> sequence, where a conditional node determines whether Python is suitable. If the conditional node return success, the query proceeds to the <em>Get Python Solution</em> node, and the result is passed to the shared <em>Generate Response</em> action.</p><p>If the task cannot be solved programmatically, the tree automatically the delegates the task to the LLM as a natural default, which generates a natural‑language response. This design highlights how components like <em>Generate Response</em> can be reused across branches, while the prioritization built into behavior trees ensures that dependable, programmatic solutions are attempted first.</p><p>If we peek into the elaborate <em>Get Python Solution</em> node, the explicit plan is,</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/973/0*MdY70LaZ7IvEMXfa.png" /><figcaption>The Python solution under to hood — the sequence finds suitable functions, executes and validates the result before packaging the result in a context to be used to generate a response.</figcaption></figure><p>Now let’s get back on track. We want to use this planning method to improve the accuracy in the output when a user is asking the assistant AI to solve mathematical tasks by leveraging programs to solve problems.</p><h3>In Practice.</h3><p>To illustrate the idea, we will instruct the LLM-based assistant, using the three billion parameter instruct-type Llama 3.2 model, to solve the following integral numerically,</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xqBzxMTjgrtuyOU8.png" /></figure><p>We get the Python solution by first looking for a suitable function defined in our tool-belt. For example, we prepare a chunk of text, where we include a set of functions that we can use from within Python to evaluate typical numerical problems. For this example, we include a segment showing what to do to numerically integrate a generic function f,</p><pre># Numerical integration of function f, in the interval [a,b]<br>def integrate(f, a=0, b=1): import scipy.integrate as quad; return quad(f, [a, b])</pre><p>Running this in our semantic calculator, we find that the LLM manages to package our question into an argument that can be used with the tool we prepared. And obtain an explanation of what calculations was done plus the final numerical result.</p><p>Already by now, the attentive reader will have some bells ringing; since we’re already working with a generative model, and have repeatedly highlighted Python as a natural fit for code generation in current LLMs, we don’t actually need to spell out the tools when they’re just Python programs. Even smaller LLMs are sufficiently capable to produce valid Python code and handle basic mathematics functions. I’m with the attentive reader on this one — so let’s go ahead and do exactly that.</p><p>Fortunately for us, adjusting our planning done by just swapping <em>Find Suitable Function</em> node for <em>Generate Python Code</em> . Running the same query, and leveraging the LLM to 1) interpret the query into arguments for a Python function, and 2) generate the code that outputs the answer we want, we get the following interaction</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DFabLKSAvoDYAD91.png" /></figure><p>An extra benefit from using an LLM to generate code, is that it also brings along a layer of common‑sense physics knowledge picked up during pre‑training. To show this in action, we can tackle a simple physics problem that calls for both general domain understanding and a numerical calculation carried out in Python.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*z-0wvV4DuqDJpSj2.png" /><figcaption>Simplify all objects as spheres, and imagine they are in vacuum.</figcaption></figure><p>For example, let’s study a simple gravitation problem with two solid spheres. This would need knowledge about the gravitational constant, and the simple formula used for calculation the potential energy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*aOiWaYmJGQP_GLt1.png" /></figure><p>The generated Python script will either define the gravitational constant explicitly or, drawing on Python’s rich scientific ecosystem, retrieve the value directly from a module like <em>scipy</em>. Following our plan, the LLM identifies this as a computable problem that satisfies the <em>Is Suitable for Python</em> condition.</p><p>It then produces a pedagogic output that includes:</p><ol><li>a problem statement, clarifying how the query is interpreted,</li><li>an outline of the approach, translating the executed program into a human‑readable explanation, and</li><li>final raw result.</li></ol><p>By leveraging the LLM’s ability to bridge natural language and code, it becomes much easier to spot whether the intended approach — or the calculation itself, is faulty. For the example above we get the code,</p><pre>import numpy as np<br>from scipy import constants as const<br>def solve_problem():<br>    &quot;&quot;&quot;<br>    Calculates the gravitational potential energy between two masses.<br>    &quot;&quot;&quot;<br>    # Constants<br>    G = const.G  # Gravitational constant<br>    # Define the positions of the masses<br>    m1 = 4  # mass 1 in kg<br>    m2 = 10  # mass 2 in kg<br>    x1, y1, z1 = 4, 1, 0  # position of mass 1 in meters<br>    x2, y2, z2 = 4, 4, 0  # position of mass 2 in meters<br>    # Calculate the distance between the masses<br>    r = np.sqrt((x2-x1)**2 + (y2-y1)**2 + (z2-z1)**2)<br>    # Calculate the gravitational potential energy<br>    U = -G * m1*m2 / r<br>    return U<br># Execute and store result<br>result = solve_problem()<br>print(&quot;Result:&quot;, result)</pre><p>In the background, the generated code is validated to ensure it is truly executable Python. This corresponds to the explicit <em>validation step</em> in the behaviour tree sequence: after the script is produced, it is either executed directly or checked against a set of common errors like non‑ASCII characters, stray markdown markers, or unbalanced parentheses. Once this validation node succeeds, the code is run and the result is formatted so that the final output can be interpreted when generating the response.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*e33PiXQaYKlyS02N.png" /></figure><p>The final step in the process is to take that raw number together with the code and translate it into a friendly, human-readable response. It’s this <em>generate response</em> node that turns a simple calculation into a complete answer for the user:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*_lkZbiJ7uofbyY_W.png" /></figure><p>As part of its code generation, the LLM is able to draw on pre‑training in basic physics, allowing it to supply the missing details needed to reach a valid result. Computation is clearly a strength here, but it is only one dimension of what’s possible. The Python ecosystem has accumulated a sea of open tools that extend beyond raw numerical evaluation, tools that also enable symbolic derivations and more advanced forms of analysis.</p><p>For example, we could use this to solve important questions, like asking what the field would look like from a hypothetical Minovsky particle⁶ field potential of a mobile suit armor,</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YEkq0XU-rW2q4LHn.png" /></figure><p>Here the Python script used to obtain the expression leverages the <em>sympy</em> Python module for symbolic mathematics to compute the partial derivatives, and return the full expression that is used as a result.</p><pre>import sympy as sp<br><br>def solve_problem():<br>    &quot;&quot;&quot;<br>    Calculates the gradient of the Minovsky particle field potential V(x,y,z) = kQ/?(x? + y? + z?).<br>    Returns the components of the gradient ?V = (-?V/?x, -?V/?partial y, -?V/?z).<br>    &quot;&quot;&quot;<br>    # Define symbols for variables<br>    x, y, z, k, Q = sp.symbols(&#39;x y z k Q&#39;)<br>    # Define the potential function<br>    V = k * Q / sp.sqrt(x**2 + y**2 + z**2)<br>    # Calculate the partial derivatives<br>    dV_dx = sp.diff(V, x)<br>    dV_dy = sp.diff(V, y)<br>    dV_dz = sp.diff(V, z)<br>    <br>    # Compute the gradient<br>    grad_V = (-dV_dx, -dV_dy, -dV_dz)<br>    # Convert the gradient to a list<br>    grad_V_list = [sp.simplify(v) for v in grad_V]<br>    return grad_V_list<br>result = solve_problem()<br>print(&quot;Result:&quot;, result)</pre><p>The key takeaway is that LLM‑driven code generation, together with Python and its ecosystem of tools, provides a unified space for both numerical and symbolic computation, making it possible to translate abstract problems into clear, executable outputs. Crucially, this approach avoids depending on the model to internally “activate the right circuits” for every mathematical operation - a process that is far less controllable, and for which we cannot be certain an LLM will ever achieve complete coverage. By grounding the reasoning in executable code, the results become more reliable, interpretable, and reproducible than those produced by raw language‑model reasoning alone.</p><p>But this raises a critical question: how does the system know when to generate code in the first place? The simple approach would be to scan the user’s request for keywords like “calculate” or “solve.” But that’s a brittle solution, language can be too nuanced for simple word matching. What we really want is for the assistant to infer from the broader context of the conversation when a computation is being requested.</p><h3>Semantic Classification: Orchestra of Models.</h3><p>At the moment, our system defaults to trying a Python-based solution for any given task. But what if the user is just asking a question or wants to have a conversation? To make our assistant smarter, we need a way to differentiate between a mathematical problem and a more general query. This is where semantic classification comes in. We classify the type of task with a conditional node, where we instruct the LLM to output a JSON containing what class the task has.</p><p>This can be difficult for smaller models, and even more so when the task is ambiguous or complex. Fortunately, we can use something more apt for this type of classification — the BERT-class of models. These models are very effective at text classification tasks and can be fine-tuned to recognise specific types of content; in our case, mathematics problems. <br>We note importantly, this removes the need to author the “perfect” prompt that anticipates the different ways of phrasing. Instead, the model can be fine‑tuned on a set of queries that collectively capture the pattern of interest, even when we ourselves may struggle to articulate it precisely.</p><p>Using a <em>IsMathProblem</em> node we decide the branch the LLM should pursue, either a regular language model, or a python generating and executing assistant. We use the classification as a condition by translating the confidence of a predicted class of problems as the criterium.</p><p>If we can classify the input as a type of mathematics problem, where any category has a confidence of c&gt;0.85, we will treat it as a computation problem and kick off the Python branch.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/985/0*mNTAPn1auMt8lj5J.png" /><figcaption>The behaviour tree preview during a chat session in the prototype.</figcaption></figure><p>By swapping the prompt-based filter with the fine-tuned BERT model, we gain some advantages. It improves our ability to detect a wider range of mathematical queries, eliminates the need to parse an intermediate JSON object, and accomplishes all this with a substantially smaller, more efficient model.</p><p>For our proof of concept here, we use a BERT-type model fine-tuned on a dataset of problems from mathematics competitions⁷, where we are interested in specifically which class of problem the user query relates to. The classes are <em>Algebra, Counting &amp; Probability, Geometry, Intermediate Algebra, Number Theory, Prealgebra, and Precalculus</em>. To demonstrate, we ask the LLM to try solving a probability problem involving donuts.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*d2WIfxRqQafU7EQ_.png" /><figcaption>The smaller three billion parameter LLM managed to push through without knowing the analytical solution, by arriving at approximate value via Monte Carlo simulation, solving the problem numerically.</figcaption></figure><p>The smaller LLM used in the back-end managed to push through without knowing the analytical solution, by arriving at approximate value via Monte Carlo simulation, solving the problem numerically. (In contrast to a single-shot attempt with Claude Sonnet 4.1, that gave the analytical solution directly)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/850/0*fxie9B4ZPUSJrucx.png" /><figcaption>A very possible scenario in real-life that needs an accurate answer.</figcaption></figure><p>Now the classifier uses the query to detect which type of problem this is about, and identifies that it with high certainty is about Counting &amp; Probability.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ZwSnkY7DQ3xhzZgu.png" /><figcaption>The classification for our problem formulation. One confidence exceeds the lower-bound for SUCCESS (more than 85%), and the query is processed by generating a Python script.</figcaption></figure><p>The classification contains a topic whose confidence exceeds the lower-bound for SUCCESS (more than 85%), and the query is processed by generating a Python script.</p><p>While the problem is solved with frontier LLMs by providing an analytical formula, the smaller LLM manages to find a brute force approximation using Monte Carlo simulations instead. The code, and accompanying result, is used as a basis for the response presented to the user. The response can be accompanied with the python script that was run, to allow for checking the steps — and it can be explained in a natural flowing conversation.</p><p>Now when we input any mathematical problem, we will get a clarification of how the LLM interprets the task, explanation of the approach taken to solve it and the final result; we have reached our goal of making a multi-model, behaviour tree-based semantic calculator!</p><h3>Concluding remarks</h3><p>For the sake of brevity, there are a couple of details left out, for example how to use a sandbox that is protected, to execute the python code we generate. Ideally, this allows us to avoid sending messages between frontend and backend if we have stateful behaviour, as we can operate using shared memory.</p><p>This proof of concept points to a fundamental shift in how we build intelligent systems. Instead of relying on a single, do-it-all LLM, we demonstrate the power of model orchestration: designing a behaviour plan where a curated set of models work in concert. This flexibility allows us to create an assistant that is not only more accurate and useful but also retains a natural, personalised feel.</p><p>Crucially, this approach makes powerful AI more accessible and private. By using smaller, efficient models for specific tasks, we can move away from the data centre and onto the user’s own device. With quantization, these systems can even be loaded into memory and run directly in a web browser, placing control back into the hands of the user.</p><p>From here, the path forward branches into exciting possibilities. We can boost performance by integrating cutting-edge code generators — either by leveraging large, closed models like Claude Sonnet and OpenAI’s o4-mini, or by staying unrestricted with dedicated open-source tools like Qwen2.5-Math⁸, or other fine-tuned for code generation⁹. System-wise, we can create more complex, event-driven behaviours that allow disconnected parts of the system to interact. Each path offers a different trade-off, but they all stem from the same core principle: intentional design, where every component is chosen to best serve its purpose.</p><p>We will continue to advance empirical research on the application of behavior trees.</p><h3>References</h3><p>[1] “Tracing the Thoughts of a Large Language Model” (2025)<br><a href="https://www.anthropic.com/research/tracing-thoughts-language-model">https://www.anthropic.com/research/tracing-thoughts-language-model</a></p><p>[2] “Let’s Think Dot by Dot: Hidden Computation in Transformer Language Models.” <a href="https://arxiv.org/pdf/2404.15758">https://arxiv.org/pdf/2404.15758</a></p><p>[3] “Reasoning Models Can Be Effective Without Thinking.” <a href="https://arxiv.org/pdf/2504.09858">https://arxiv.org/pdf/2504.09858</a></p><p>[4] “Does Reinforcement Learning Really Incentivize Reasoning Capacity in LLMs Beyond the Base Model?” <a href="https://arxiv.org/pdf/2504.13837">https://arxiv.org/pdf/2504.13837</a></p><p>[5] “Spurious Rewards: Rethinking Training Signals in RLVR.” <a href="https://arxiv.org/pdf/2506.10947">https://arxiv.org/pdf/2506.10947</a></p><p>[6] “Gundam Wiki: Technology — Minovsky Physics.” <a href="https://gundam.fandom.com/wiki/Gundam_Wiki:Technology#Minovsky_Physics">https://gundam.fandom.com/wiki/Gundam_Wiki:Technology#Minovsky_Physics</a></p><p>[7] Hugging Face model card: <em>bert-finetuned-math-prob-classification</em>. <a href="https://huggingface.co/lschlessinger/bert-finetuned-math-prob-classification">https://huggingface.co/lschlessinger/bert-finetuned-math-prob-classification</a></p><p>[8] Hugging Face model card: <em>Qwen2.5-Math-7B</em>. <a href="https://huggingface.co/Qwen/Qwen2.5-Math-7B">https://huggingface.co/Qwen/Qwen2.5-Math-7B</a></p><p>[9] Hugging Face model card: <em>CodeLlama-7b-Python-hf</em>. <a href="https://huggingface.co/codellama/CodeLlama-7b-Python-hf">https://huggingface.co/codellama/CodeLlama-7b-Python-hf</a></p><p>More Articles:</p><p><a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a></p><p>About Company:</p><p><a href="https://gaudiy.com/en/">Gaudiy Inc. | Moving forward in a new era with fans</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5a9ca9fe4541" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gaudiy-ai-lab/5a9ca9fe4541">Computation and Language: The Semantic Calculator</a> was originally published in <a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The need for a deterministic brain | Behavior tree agents.]]></title>
            <link>https://medium.com/gaudiy-ai-lab/25aedd1435fd?source=rss----096fc41648f8---4</link>
            <guid isPermaLink="false">https://medium.com/p/25aedd1435fd</guid>
            <category><![CDATA[gaudiy]]></category>
            <category><![CDATA[behavior-trees]]></category>
            <category><![CDATA[llm-agent]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Gaudiy Lab]]></dc:creator>
            <pubDate>Wed, 10 Sep 2025 02:29:10 GMT</pubDate>
            <atom:updated>2025-09-10T02:29:10.715Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8-E8XY4pbbS-VAn6qNc6tA.jpeg" /></figure><p><em>Posted by Malkoc Ognjen, AI Researcher of </em><a href="https://gaudiy.com/en/"><em>Gaudiy Inc</em></a><em>.</em></p><h3>Motivation</h3><p>Around the time ChatGPT emerged, very quickly a deceptively simple idea began to take shape — one that would prove surprisingly useful:</p><blockquote>If it talks to me like a human, can it also code like a human to control the computer?</blockquote><p>At first, the concept felt too simple: use an AI model to handle problem-solving by translating natural language requests into portable C-style code. The pipeline was clean — provide a set of predefined functions, spin up a separate process, compile the result into a dynamic library on the fly, load it, and invoke a <em>run</em> function. After some initial success, feeling confident about the approach, a more advanced assistant prototype was developed.</p><p>…It resulted in a catastrophic failure.</p><p>Compilation errors with hallucinated functions, “guessed” function arguments and unsafe memory access made it abundantly clear that this was not the proper approach, at least at the time of GPT3. Moreover, there were few utility functions that could be used for simple mathematical tasks that the tasks would need.</p><p>After some more experimentation, it became clear that the instruct type GPT models — the foundation of ChatGPT — were capable of pretty reliable Python code. It’s likely that the pre-training data included substantial amounts of Python code, which was growing in popularity. The premise was straightforward: by formatting instructions in Python, the model could produce more structured outputs and reduce hallucinated function calls.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0A-suQsxR_Ti8uUd.png" /><figcaption>From the <a href="https://www.tiobe.com/tiobe-index/">**TIOBE Index</a>** we see a clear increase in popularity of Python, something that is reflected in the Pull Requests by language in Github also. See e.g., <a href="https://madnight.github.io/githut/">Githut 2.0.</a></figcaption></figure><p>Pushing the idea further, rather than simply using the model to generate and run code, one could tap into the LLM’s intuitive “reasoning” capabilities — allowing it to interpret tasks and directly output results in a computer-readable format.</p><p>The new approach would be; format the prompt with a Python-like syntax, from inference generate a plausible output and replace the computation entirely. For multivariate or structured outputs, JSON was a quick way to make things portable — quick to set up and easy to parse in native applications.</p><p>… Unfortunately, that, too, failed.</p><p>At times, the output would not be generated, the remaining times there was a struggle to make it deterministic. In many cases, the complexity level of the task turned out to be a big source of errors. To narrow down the space of outputs, breaking down the task statements and using a language model to tackle individual pieces seemed like the way to go.</p><p>But this too had to be extended — causal relationships between tasks was also difficult to do reliably in a zero-shot way. Structuring functions in a way that made it easier to determine their order helped. Ensuring that a function like “add to scheduler” explicitly required “event” and “timeslot” as inputs meant that “make timed event” could be processed beforehand.</p><p>Still, despite all efforts to improve the performance, plenty of iterations were required to create a convincing visual demo. Eventually, tools like LangChain and LangGraph helped make the whole process stateful — which made the design a bit more manageable. Put in this framework, it is possible to build workflows where the nodes were linked together through a semantic interface powered by the LLM.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*TlDTAMGlJf75zjFb.png" /><figcaption><strong>A visual tool for constructing workflows, LangGraph Studio. Figure from LangGraph documentation</strong> <a href="https://langchain-ai.github.io/langgraphjs/concepts/langgraph_studio/">https://langchain-ai.github.io/langgraphjs/concepts/langgraph_studio/</a>.</figcaption></figure><p>After all the steps to make it work, when looking closely, a familiar pattern started to emerge; the workflows were essentially a sophisticated state machine. Viewed through the lens of a hypothetical agent, the LLM is made to follow a logical flow, or plan, where each state is equipped with its semantic context, equating an inference call with running the state action. The biggest perk of including an LLM here, is that there is now a semantic layer that can connect the different states. However, while the LLM helps — it suffers from the same known problem occurring when using state machines.</p><p>When looking at it from the perspective of AI in robotics or game AI, it appears to follow the same trend — increasing complexity often leads to chaotic and unintuitive code. Fortunately, this is a known issue, and a more structured approach, behavior trees, has already become somewhat of an industry standard. It is a more approachable way to model behavior.</p><p>Behavior trees provide a structured, and visually intuitive, approach to modeling plans that are easily represented in a graphical user interface. Compared to other methods, they enable the creation of complex behaviors using fewer nodes while maintaining clarity and control. Unsurprisingly, the idea of combining the flexibility of large language models, and the systematic planning of a behavior tree is already an active research topic, primarily in the robotics field¹ ² ³.</p><h3>Behavior tree primer</h3><p>First developed for character AI in video games⁴, it has had a lot of success in robotics thanks to straightforward implementation in code, and ultimately gained a strong foothold as the go-to method for designing robot behavior. The clear structure makes a visual scripting approach feasible, resulting in very accessible behavior modeling. With built-in prioritization (tasks execute from top to bottom, left to right), it is possible to develop an intuition for a consistent logical flow when designing the behavior of an agent.</p><p>Moreover, as the name suggests, it has a tree structure; which allows for complicated plans to be described as separate trees, that enter as branches of the full behavior tree. With this, a hierarchical behavior planning is straightforward, making the breaking down of tasks fairly easy.</p><p>Using the simple example, we quickly see the simplicity, and hopefully also spot the potential of the approach.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/834/0*GNFsfrQ_CynyZKOn.png" /><figcaption>Schematic description of behavior trees, from <a href="https://arxiv.org/pdf/1709.00084">Behavior Trees in Robotics and AI</a>⁴</figcaption></figure><p>In its most basic implementation, the behavior tree starts at a <strong>root</strong> node, and is comprised of two kinds of nodes. Composite nodes like <strong>Sequence</strong> and <strong>Selector</strong>, control the flow. Leaf nodes, <strong>Action</strong> and <strong>Condition</strong> nodes, do the actual work and checks. The action nodes return a status state such as <strong>Success</strong>, <strong>Failure</strong>, or <strong>Running</strong>, to its parents.</p><p>A sequence is defined as a control node that traverses its children in order (left to right) until a node returns Failure, all have Succeeded, or one of its children nodes are Running. A selector on the other hand will stop immediately after one of its child nodes returns Success, or traverses its children nodes while they return Failure.</p><p>And that’s essentially it.</p><p>Different implementations exist, ranging from nested to more declarative approaches. But in essence, most of the code to define complicated behavior will be as simple as the following Python code:</p><pre>def build_behavior_tree():<br>    root           = SequenceNode(name=&quot;Root&quot;)<br>    reasoning_tree = SequenceNode(name=&quot;ReasoningTree&quot;)<br> <br>    decomposer = LLMNode(<br>        prompt_template=DECOMPOSE_PROMPT, <br>        name=&quot;Decomposer&quot;,<br>        llm=default_llm) # 1. problem → subtasks<br>    <br>    analyzer = LLMNode(<br>        prompt_template=ANALYSIS_PROMPT,<br>        name=&quot;Analyzer&quot;,<br>        llm=default_llm) # 2. subtasks → analysis<br>   <br>    solution_generator = LLMNode(<br>        prompt_template=[REASONING_GENERATION, ...],<br>        name=&quot;Reasoning&quot;,<br>        llm=default_llm) # 3. analysis → sopylution<br>    <br>    validator = LLMNode(<br>        prompt_template=SOLUTION_VALIDATION_PROMPT,<br>        name=&quot;Validator&quot;,<br>        llm=default_llm) # 4. solution → validation<br><br>    speaker = LLMNode(<br>            prompt_template=REPLY_PROMPT,<br>            name=&quot;Speaker&quot;,<br>            llm=default_llm) # 5. validation → final answer<br>        <br>    reasoning_tree.add_child(decomposer)<br>    reasoning_tree.add_child(analyzer)<br>    reasoning_tree.add_child(solution_generator)<br>    reasoning_tree.add_child(validator)<br>    # Assemble full tree<br>    root.add_child(reasoning_tree)<br>    root.add_child(composer)<br>    return root</pre><p>This is the behavior that we outlined in the initial introduction. The way it is constructed is implicitly defining the behavior. If we expand the definition of the behavior tree, including also decorators like repeaters, we could easily incorporate behavior that is repeated for specific sub tasks. As shown in the example, by virtue of being a tree structure, the complicated reasoning behavior can be easily added as a separate tree that is defined elsewhere. Having the ability to define “reasoning_tree_a” and “reasoning_tree_b” enables very easy experimentation.</p><p>The approach also allows for implementing reactivity, where an agent can stop an ongoing action to attend to a more urgent task, but use cases which require this application is out of scope here — we leave it as an exercise for the reader.</p><h3>Putting into practice</h3><p>The primary use-case for LLMs as agents is its role as assistants. As mentioned above, we quickly run into complications when fully relying on the LLM to,</p><ol><li>Understand from the given context what logical steps are required to help with a task — especially if the task is growing in size.</li><li>Understanding from the context and query that more information is needed.</li></ol><p>Another big concern is the privacy aspect. There is a lot of sensitive data that may be needed to carry out some commands — making an assistant based on API to a cloud hosted LLM be unsuitable. With this in mind, we would ideally want all to be done on a locally-hosted model — we will try to be as lightweight as possible, setting an upper limit of model size to Llama 3.2 with three billion parameters.</p><p>With this restriction, let’s use the planning method we just discussed to facilitate the breaking down of large tasks into manageable chunks even for smaller models. To push the idea further, we also try to handle multi-turn behavior where we probe for necessary information that makes the assistant able to create an automation chain-of-tasks for us.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0mt3gqwz_WTT2nB_.png" /></figure><p>Let’s take a concrete example: In one of the communities of <strong>Gaudiy’s Fanlink platform</strong>, a user receives a message from a community manager asking them to update their expired email address and username. Since the current setup doesn’t account for this scenario, the user turns to the assistant to guide the process. Ideally, this should be handled as a one-click task. The more complex logic behind constructing the task chain can be prebuilt and handled separately.</p><p>In this use case, our aim is to make an LLM-powered assistant’s that can extract the intent from the user’s simple request, like updating an email, and break it down into clear, sequential steps that can run with minimal input, to minimize friction. Steps should be chronologically ordered, bundled together, and easy to execute.</p><p>To further drive the necessity of control, the packaging of tasks can sometimes not be made as concrete commands, as there is some lacking input. Here we leverage the ability to model multi-turn behavior, and make the assistant request additional inputs. This way we hope to reduce incidents of e.g., potentially using an email that was particularly prominent in the pre-training data of an LLM. In our example case, if in our prototype we type,</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KPV_EPYJ-4CXkxFZ.png" /></figure><p>the assistant would follow the request up with clarifications such as “What is your new email?”. But simply reacting to the user’s request isn’t enough. It also needs to determine whether the user is able to update their information in the settings, and how that process works.</p><p>To avoid overly detailed prompts that risk overwhelming smaller models, we treat this discovery as its own step. In short, this is a complex query that requires thoughtful handling. The assistant must assess how best to respond: can it act immediately, or does it need to coordinate a more elaborate sequence in the background?</p><p>Broadly speaking, we begin by evaluating the complexity of the request. If straightforward, we let the language model respond directly. But, if we identify that it involves multiple variables, requires multiple steps or needs to be broken down into smaller parts, we shift to explicitly defining the model’s behavior using a behavior tree and tackle the task through a structured step-by-step approach.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/575/0*Si8RaT_hz1etzE0v.png" /><figcaption>High-level overview of the simple plan we want to implement. If we can process the query as a simple chatbot, do so, otherwise plan each step.</figcaption></figure><p>In the figures, ‘<strong>→</strong>’ will denote sequences , ‘?’ denotes selectors, ‘[<strong>C</strong>]’ and ‘[<strong>A</strong>]’ denote conditional and action nodes respectively. We start from the root node “Process query”, which is a selector node. If we detect that the user query is complex we break down the task into sub-tasks, and invoke a multi-step solution. Since the multi-step approach will increase the latency substantially, we only want to follow this branch is necessary — if the conditional node “Is Task Complex” fails, we automatically move to the fallback of “Generate Response”.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3uD77v1ELm__j28y.png" /><figcaption>The behavior that is triggered when a complex command or query is detected.</figcaption></figure><p>As mentioned, we may need more information to fully understand the task. In our example, this means we need to know what is possible; and we pull the Fanlink service details, then retrieve user-specific settings to build a meaningful plan of action. In the demonstration, we used a raw sample HTML file that describes a hypothetical profile settings page.</p><p>Once we’ve mapped out what needs to happen, we can create a preliminary plan. However, to make it an executable plan we move on to <em>packaging the tasks</em>. Each task in our plan is defined, but may need input arguments, in order to create an executable chain of tasks.</p><p>Finally, from a user-perspective we want to be as transparent as possible, so we <em>present the plan</em>. Before executing anything, we make sure each task is clearly laid out and confirm it step by step — so there are no surprises.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/785/0*0SUwiBSsHycZgm6v.png" /><figcaption>The isolated task of preparing the task chain, allowing users to execute multiple actions at once. Note the ↻ decorator, ensuring it is run in sequence on all tasks.</figcaption></figure><p>We show the input handling as a loop for simplifying the illustration. In practice, we track the execution path and after flagging for input we traverse from the root again, and follow the recorded path to ensure that we can resume from the leaf node that is still running.</p><p>To enable this interactivity, we make the tree stateful using a blackboard to register the tree execution path and necessary input as persistent data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qd49oZ1qRUiDu0cz.png" /><figcaption>The user and LLM agent work in tandem to solve a complicated request.</figcaption></figure><p>In our example, as the preliminary plan shows that we need to know the actual email address to change to, when “<em>packaging</em>” the “<em>change email address</em>” task, we store the missing variable and flag the state for input required and the execution path until we reach this point. When the next input is made, tree nodes are traversed in the recorded order and the behavior tree can resume (if the input was valid).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fdQMFPcO9yqWWKV1.png" /><figcaption>A prototype using a Python backend, with a local instance of a Llama 3.2 3B model.</figcaption></figure><p>The confirmation state similarly needs input that is valid for the execution. Since we have already used the input handling when packaging the tasks into a chain, we can reuse the same method with the simple extension to allow declining the execution of the chain also.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Mdw9J99b6UCv9LcH.png" /></figure><p>After the executable plan is shown, in the confirmation stage the LLM is used to understand the intent of the user, whether they are okay or not with executing the shown plan. In action, it looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fYebf_UcAZ_cYePm.png" /><figcaption>The conversation mixes deterministic output, e.g., listing the identified tasks, and natural conversation. For example, understanding the confirmation.</figcaption></figure><p>The conversation mixes deterministic output, e.g., listing the identified tasks, and natural conversation. For example, understanding the confirmation.</p><p>The prototype demonstrates that even modestly-sized language models can execute tasks by following predefined scripts, enabling a modular way of designing interactive behavior. The next step in our designed plan requires the user to confirm the plan, which again makes use of the LLM to classify whether it should continue or not.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3erRaD0gFlHzUEcF.png" /><figcaption>As soon as we naively let the Llama-3.2–3B-Instruct model generate longer summaries, it manages to output things like [insert new email address] instead.</figcaption></figure><p><strong>And there we have it;</strong> a scripted conversation that blends deterministic planning with the natural flow of LLM responses. By making use of complex behavior nodes within a logical flow, we can make the scripted interactions feel more natural. This approach allows emergent, yet controlled, behaviors to show up, even though the scaffolding is manually designed.</p><p>Although the explicit behavior tree initially appears complex, there is a clear hierarchical structure which makes it digestible. At the highest level, the system evaluates whether a query is simple enough to resolve directly, our default fallback, or if it requires additional handling through a multi-step process. With this mental model, the rest of the implementation becomes finding the right building blocks.</p><h3>Conclusion</h3><p>We described one possible implementation here; but the complex sequences can be swapped. Each multi-step solution is itself modular and can be further broken down; in particular the segments responsible for task decomposition and user input handling, can be implemented in various ways.</p><p>Being able to separate preparing the tree nodes, and wiring the behavior opens up for leveraging the skillset of people with expertise in e.g., game AI or robotics. With the separation, it becomes less and less important to understand the underlying structure when designing the assistant AIs. It could potentially be semi-automated with a generative model for Behavior Trees.</p><p>And we didn’t need to use reinforcement learning to modify parameters to align the language model into generating the steps itself, which may potentially affect the storing of valuable information in smaller model.</p><p>However, there are two glaring points seen quickly when trying to implement this planning method.</p><ul><li>For the conditional nodes we still need prompt engineering to make a reliable filter. This can be difficult for ambiguous conditions, especially for smaller language models. Purposefully shown here by instructing the LLM above to process the task step-by-step.</li><li>The task decomposition in this scenario is far from optimal. There are scenarios where there is a clear benefit to having the innate prioritization of a behavior tree.</li></ul><p>These two points will be explored further in the next part — where we will try to improve the mathematical performance of AI assistants using smaller models with the outlined approach.</p><h3>References</h3><p>[1] Styrud, Jonathan, et al. “Automatic Behavior Tree Expansion with LLMs for Robotic Manipulation.” <em>arXiv:2409.13356</em> (2024).</p><p>[2] Izzo, Riccardo Andrea, Gianluca Bardaro, and Matteo Matteucci. “Btgenbot: Behavior tree generation for robotic tasks with lightweight llms.” <em>2024 IEEE/RSJ International Conference on Intelligent Robots and Systems (IROS)</em>. IEEE, 2024</p><p>[3] Ao, Jicong, et al. “LLM as bt-planner: Leveraging llms for behavior tree generation in robot task planning.” <em>arXiv:2409.10444</em> (2024).</p><p>[4] Colledanchise, Michele, and Petter Ögren. <em>Behavior trees in robotics and AI: An introduction</em>. CRC Press, 2018.</p><p>More Articles:</p><p><a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a></p><p>About Company:</p><p><a href="https://gaudiy.com/en/">Gaudiy Inc. | Moving forward in a new era with fans</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=25aedd1435fd" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gaudiy-ai-lab/25aedd1435fd">The need for a deterministic brain | Behavior tree agents.</a> was originally published in <a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Experimenting with Multi Agent Architectures — Can they outperform SingleAgent GPT-4o?]]></title>
            <link>https://medium.com/gaudiy-ai-lab/0513d44c9dc6?source=rss----096fc41648f8---4</link>
            <guid isPermaLink="false">https://medium.com/p/0513d44c9dc6</guid>
            <category><![CDATA[gaudiy]]></category>
            <category><![CDATA[multi-agent-systems]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[Gaudiy Lab]]></dc:creator>
            <pubDate>Wed, 09 Apr 2025 01:10:57 GMT</pubDate>
            <atom:updated>2025-04-09T01:10:57.765Z</atom:updated>
            <content:encoded><![CDATA[<h3>Experimenting with Multi Agent Architectures — Can they outperform SingleAgent GPT-4o?</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iiNOjTySOBmTfPQ0UlQvFQ.jpeg" /></figure><p><em>Posted by </em><a href="https://x.com/ShreyasGenAI"><em>Shreyas Shinde</em></a><em>, AI Engineer of </em><a href="https://gaudiy.com/en/"><em>Gaudiy Inc</em></a><em>.</em></p><h3>Introduction</h3><blockquote><strong><em>If you want to go fast, go alone; but if you want to go far, go together.</em></strong></blockquote><p>We all know this famous quote applies to humans. Does this hold true for LLM Agents, too? Let’s find out!</p><p>In <a href="https://medium.com/gaudiy-ai-lab/1b1778345ad9">part 1</a> of this Multi Agents Blog series, we saw an in-depth literature survey and industrial practices involving multi agents and their architectures. In part 2, I will share how we experimented with some of these architectures on one of our internal applications, which was originally developed as a SingleAgent. Ideas in papers may not always give good results on your specific use case, so evaluating, comparing, and verifying their effectiveness is important. We will also see how these architectures compare with SingleAgent gpt4o as well!</p><p>We will look at the experiments conducted on 3 architectures:</p><ol><li><strong>Chain of Agents and its variations</strong></li><li><strong>Mixture Of Agents</strong></li><li><strong>Multi Agent Debate</strong></li></ol><p>Last year, we developed an HRSystemAgent to support employees during the internal release of the Personnel Evaluation System. It was developed as a SingleAgent by giving it an appropriate persona prompt and domain knowledge about the Evaluation System. We created a multi-turn chat evaluation dataset by having the HR System owner annotate the Agent’s actual chat responses to employee messages during beta tests. We wrote LLM as a judge evaluator to score the correctness of the Agent’s responses. For the scope of this experiment, I implemented the core generation logic of this app by using the above multi-agent architectures, and we reused the evaluation dataset and evaluator to compare the scores between SingleAgent and MultiAgent architectures. LangSmith based helper library</p><p><a href="https://github.com/gaudiy/langsmith-evaluation-helper">GitHub - gaudiy/langsmith-evaluation-helper: Helper library for LangSmith that provides an interface to run evaluations by simply writing config files.</a></p><p>was used for running the experiments, and LangGraph was used to implement these architectures, but you can use any framework you choose!</p><p>You can read more about the purpose &amp; reception of HRSystemAgent here (only in Japanese)</p><p><a href="https://note.com/ryosuke_fujiwara/n/n69ce4bf72a4f">人事がAIエージェントと2か月間&quot;同僚&quot;になってわかった、AI × 組織作りの勘どころ｜Ryosuke Fujiwara</a></p><p>You can read more about how the evaluator was developed here:</p><p><a href="https://medium.com/gaudiy-ai-lab/032a47b0b017">LLM-as-a-Judge: A Technique for Evaluation Using LLMs</a></p><h3>1. Chain of Agents and its variations</h3><h4>Chain of Agents</h4><p><a href="https://arxiv.org/abs/2406.02818">Chain of Agents: Large Language Models Collaborating on Long-Context Tasks</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4VAO3ONY4-0b9aBgK2BSyw.png" /></figure><h4><strong>What does it do?</strong></h4><p>This technique outperforms RAG and Long Context Window Models on tasks, including QA. Input token reduction techniques like RAG suffer from retrieval accuracy. In contrast, Long Context Window Models suffer from a lost-in-the-middle problem where they are not effectively able to attend fully to the entire input context.</p><p>To solve this problem, Chain of Agents divides the long context into chunks of 8k tokens each and assigns one worker agent to process one chunk of context. The worker agent is prompted to summarize the relevant parts from its own chunk necessary to answer the user’s question. This summary is then passed to the next worker, who is prompted to summarize the relevant parts from its own chunk and the summary from the previous worker agent. At the end of this worker agent chain, the final summary is then processed by a manager agent to answer the user’s question.</p><h4>Implementing HRSystemAgent</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/490/1*InnSLtUWZVy8A1vD9piiBA.png" /><figcaption>Graph visualized in LangGraph Studio</figcaption></figure><p>LanGraph code(State and Workflow code. Domain Knowledge prompts have not been included):</p><pre>&quot;&quot;&quot;This module defines the state representation for the HR System Agent.&quot;&quot;&quot;<br><br>import operator<br>from typing import Annotated, Literal, TypedDict<br>from pydantic import BaseModel<br>class Message(BaseModel):  # noqa: D101<br>    role: Literal[&quot;user&quot;, &quot;assistant&quot;]<br>    content: str<br>class HRSystemAgentState(TypedDict):<br>    &quot;&quot;&quot;A state representation for the HR System Agent.&quot;&quot;&quot;<br>    previous_agent_response: str | None<br>    user_message: Message<br>    chat_history: Annotated[list[Message], operator.add]<br>    user_message_language: str<br>    agent_response: str<br>    <br>&quot;&quot;&quot;Define the graph for the HR System Agent.<br>This module defines a custom graph for the HR System Agent based on Chain of Agents (CoA) architecture.<br>&quot;&quot;&quot;<br>def detect_lang(state: HRSystemAgentState) -&gt; dict[str, str]:<br>    &quot;&quot;&quot;Detect the language of the user message.&quot;&quot;&quot;<br>    language = detect(state[&quot;user_message&quot;].content.replace(&quot;\\n&quot;, &quot;&quot;)).get(&quot;lang&quot;)<br>    return {&quot;user_message_language&quot;: language}<br>worker_agent_1_system_prompt = &quot;&quot;&quot;<br>{context}<br>You need to read the above source text and write the summary that can include the evidence for answering the user&#39;s last message in below chat history:<br>Chat History:<br>{chat_history}<br>&quot;&quot;&quot;<br>async def worker_agent_1(<br>    state: HRSystemAgentState,<br>) -&gt; dict[str, str]:<br>    &quot;&quot;&quot;Worker Agent 1.&quot;&quot;&quot;<br>    model = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)<br>    if state[&quot;user_message_language&quot;] == &quot;en&quot;:<br>        context = context_1_en<br>    else:<br>        context = context_1_jp<br>    chat_history = &quot;\\n&quot;.join(<br>        [f&quot;{message.role}: {message.content}&quot; for message in state[&quot;chat_history&quot;]]<br>    )<br>    chat_history = (<br>        chat_history<br>        + f&quot;\\n{state[&#39;user_message&#39;].role}: {state[&#39;user_message&#39;].content}&quot;<br>    )<br>    system_prompt = worker_agent_1_system_prompt.format(<br>        context=context,<br>        chat_history=chat_history,<br>    )<br>    prompt_messages: list[dict[Any, Any]] = [<br>        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt}<br>    ]<br>    response = await model.ainvoke(prompt_messages)<br>    return {<br>        &quot;previous_agent_response&quot;: str(response.content),<br>    }<br>worker_agent_2_system_prompt = &quot;&quot;&quot;<br>{context}<br>Here is the summary of the previous source text: {previous_agent_response}<br>You need to read above source text and summary of previous source text and generate a summary to include them both. Later, this summary will be used for other agents to answer the<br>the user&#39;s last message in below chat history. So please write the summary that can include the evidence for answering the user&#39;s last message in below chat history:<br>Chat History:<br>{chat_history}<br>&quot;&quot;&quot;<br>async def worker_agent_2(<br>    state: HRSystemAgentState,<br>) -&gt; dict[str, str]:<br>    &quot;&quot;&quot;Worker Agent 2.&quot;&quot;&quot;<br>    model = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)<br>    if state[&quot;user_message_language&quot;] == &quot;en&quot;:<br>        context = context_2_en<br>    else:<br>        context = context_2_jp<br>    chat_history = &quot;\\n&quot;.join(<br>        [f&quot;{message.role}: {message.content}&quot; for message in state[&quot;chat_history&quot;]]<br>    )<br>    chat_history = (<br>        chat_history<br>        + f&quot;\\n{state[&#39;user_message&#39;].role}: {state[&#39;user_message&#39;].content}&quot;<br>    )<br>    system_prompt = worker_agent_2_system_prompt.format(<br>        context=context,<br>        previous_agent_response=state[&quot;previous_agent_response&quot;],<br>        chat_history=chat_history,<br>    )<br>    prompt_messages: list[dict[Any, Any]] = [<br>        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt}<br>    ]<br>    response = await model.ainvoke(prompt_messages)<br>    return {<br>        &quot;previous_agent_response&quot;: str(response.content),<br>    }<br>async def manager_agent(state: HRSystemAgentState) -&gt; dict[str, str]:<br>    &quot;&quot;&quot;Manager Agent.&quot;&quot;&quot;<br>    if state[&quot;user_message_language&quot;] == &quot;en&quot;:<br>        domain_agent_initial_instructions = domain_agent_initial_instructions_en<br>        few_shots = combined_response_few_shots_en<br>    else:<br>        domain_agent_initial_instructions = domain_agent_initial_instructions_jp<br>        few_shots = combined_response_few_shots_jp<br>    system_prompt = f&quot;&quot;&quot;<br>{domain_agent_initial_instructions}<br>Keep in mind that the current date and time is {datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)}.<br>The following are given passages. However, the source text is too long and has been summarized. You<br>need to reply to the user&#39;s last message based on the below summary:<br>{state[&quot;previous_agent_response&quot;]}<br>{few_shots}<br>&quot;&quot;&quot;<br>    prompt_messages: list[dict[Any, Any]] = [<br>        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt}<br>    ]<br>    model = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)<br>    for message in state[&quot;chat_history&quot;]:<br>        if message.role == &quot;user&quot;:<br>            prompt_messages.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: message.content})<br>        else:<br>            prompt_messages.append({&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: message.content})<br>    prompt_messages.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: state[&quot;user_message&quot;].content})<br>    response = await model.ainvoke(prompt_messages)<br>    return {&quot;agent_response&quot;: str(response.content)}<br># Define a new graph<br>workflow = StateGraph(HRSystemAgentState)<br># Add the node to the graph<br>workflow.add_node(&quot;detect_lang&quot;, detect_lang)<br>workflow.add_edge(&quot;__start__&quot;, &quot;detect_lang&quot;)<br>workflow.add_node(&quot;worker_agent_1&quot;, worker_agent_1)<br>workflow.add_edge(&quot;detect_lang&quot;, &quot;worker_agent_1&quot;)<br>workflow.add_node(&quot;worker_agent_2&quot;, worker_agent_2)<br>workflow.add_edge(&quot;worker_agent_1&quot;, &quot;worker_agent_2&quot;)<br>workflow.add_node(&quot;manager_agent&quot;, manager_agent)<br>workflow.add_edge(&quot;worker_agent_2&quot;, &quot;manager_agent&quot;)<br>workflow.add_edge(&quot;manager_agent&quot;, &quot;__end__&quot;)<br># Compile the workflow into an executable graph<br>graph = workflow.compile()<br>graph.name = &quot;HR System Agent&quot;  # This defines the custom name in LangSmith</pre><p>There were several challenges in this implementation:</p><ol><li>The paper only includes prompts for Single Turn QA, so the prompts had to be appropriately updated for our use case of Multi Turn chat.</li><li>Deciding whether to add the chat history to the system prompt or pass it as user↔assistant pair messages in the chat model’s API was tricky. Based on the evaluation results, I realized it was best to include the chat history messages in the system prompt for worker agents and as chat messages for the final manager agent.</li><li>You must determine how many worker agents are ideal for your context length by evaluating multiple variations.</li></ol><p><strong>Evaluation:</strong></p><ol><li><strong>Dataset Schema:</strong></li></ol><p>We have a total of 60 examples in this dataset.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/832/1*qzOpOJgHGvz-S6m7rL8YKg.png" /><figcaption>Evaluation dataset schema</figcaption></figure><p>2.<strong> Evaluator:</strong></p><p>LLM-as-a-Judge was prompted to score the agent’s response to the user’s query based on the chat history context, domain knowledge, and evaluation criteria list.</p><p>3.<strong> Results:</strong></p><p>With the config file set up according to LangSmith’s evaluation helper library, I ran the same experiment five times to obtain a statistically significant average score.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/902/1*Yr25nIlGErvlbdf0rI-VIg.png" /><figcaption>CoA results</figcaption></figure><blockquote>💡<strong>Findings:</strong></blockquote><blockquote>1. As the domain knowledge context token length for my app was around 20k tokens 2 worker agents gave the best results whereas the performance declined if more worker agents were added. It seems distributing around 10–15k tokens of context per worker agent gives optimal results which aligns with the paper’s results. I noticed that when 6 worker agents were used there were some cases where hallucinations added by 1 worker agent was propagated throughout the chain which might be the intuition for performance declining if context is split too thinly.</blockquote><blockquote>2. The score was higher than SingleAgent gpt4o-mini but it didn’t outperform gpt4o SingleAgent. As the score is only slightly less than than of gpt4o but the cost is considerably lower (around 1/6th) it might still be used. The average latency however is ~2.5x as the agents are connected sequentially. It might be possible to improve cost and reduce latency without sacrificing performance too much by using gemini-1.5-flash or cheaper oss model instead.</blockquote><blockquote>3. I have tried to use the same prompts as much as possible from the paper but the wordings could be improved.</blockquote><h3>Variations of Chain Of Agents</h3><h4>What variations should you consider?</h4><p>If we abstract out the structure of the chain of agents, we can quickly realize that we can design some variations of it.</p><p>Specifically, we can consider,</p><ol><li>What if, instead of running each worker agent, we used a selector agent at the start to select and run only the ones relevant to the given user query?</li></ol><p>Which workers to use → All or Relevant Only</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/604/1*G_8Pw-hAJkBheLYd-cNGCQ.png" /></figure><p>2. What if instead of sequentially connecting the workers, we executed them in parallel and processed their outputs independently via the manager?</p><p>Workers Connection Type → Sequential Or Parallel</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/592/1*x_7EeISr_CaxsieudPqlaQ.png" /></figure><p>3. What if instead of summarizing the context, the worker agents performed a different task, like extracting relevant context and passing it to the manager or trying to directly answer the user message based on their own context, and then the manager formulating a final answer based on each worker’s answers?</p><p>Worker Agent’s Task → Summarize Or Extract Or Answer.</p><p>These are a total of 12 variations, including the original Chain Of Agents (All x Sequential x Summarize). Here are the scores of the variations that performed best(I skipped experimenting with some of them intuitively):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/736/1*9X8CNaZWGS9oPd42hqaHKA.png" /></figure><p>The scores of architectures where a selector agent is used to determine the relevant agents are heavily impacted by the quality of the selector agent. So, it will be critical to improve that component by prompt tuning to further improve such architectures. Also, since our evaluator is focused only on correctness, the higher possibility of precision in such architectures is not currently highlighted and remains to be explored in the future.</p><p>Instead as the score of Parallel Agents Extract seemed already promising i decided to focus on improving the extraction technique. The worker agent’s prompt for this version of PAE is below, which is a direct extraction prompting way:</p><pre>worker_agent_system_prompt = &quot;&quot;&quot;<br>Act like an experienced secretary for a small business. <br>Review the business document provided here: <br>{context}<br><br>Now, from the above document only, give me all the excerpts verbatim extracted <br>text paragraph strings that deal with the user&#39;s last message in below <br>chat_history:<br>{chat_history}<br>&quot;&quot;&quot;<br></pre><p>But can we improve this extraction method? I will share below about the implementation of this variation’s improved version.</p><h3>Parallel Agents Extract (Improved extraction technique)</h3><p><strong>Implementing HRSystemAgent</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/504/1*tVzp9TrqyVhcsxCPSElJQA.png" /><figcaption>Graph visualized in LangGraph Studio:</figcaption></figure><p>LangGraph Code:</p><pre>&quot;&quot;&quot;This module defines the state representation for the HR System Agent.&quot;&quot;&quot;<br>import operator<br>from typing import Annotated, Literal, TypedDict<br>from pydantic import BaseModel<br>class Message(BaseModel):  # noqa: D101<br>    role: Literal[&quot;user&quot;, &quot;assistant&quot;]<br>    content: str<br>class HRSystemAgentState(TypedDict):<br>    &quot;&quot;&quot;A state representation for the HR System Agent.&quot;&quot;&quot;<br>    worker_agent_responses: Annotated[list[str], operator.add]<br>    user_message: Message<br>    chat_history: Annotated[list[Message], operator.add]<br>    user_message_language: str<br>    agent_response: str<br>class WorkerAgentState(TypedDict):<br>    &quot;&quot;&quot;A state representation for a Worker Agent.&quot;&quot;&quot;<br>    current_agent: Literal[<br>        &quot;WorkerAgent1&quot;,<br>        &quot;WorkerAgent2&quot;,<br>        &quot;WorkerAgent3&quot;,<br>        &quot;WorkerAgent4&quot;,<br>        &quot;WorkerAgent5&quot;,<br>        &quot;WorkerAgent6&quot;,<br>    ]<br>    user_message: Message<br>    chat_history: Annotated[list[Message], operator.add]<br>    user_message_language: str<br>&quot;&quot;&quot;Define the graph for the HR System Agent.<br>This module defines a custom graph for the HR System Agent based on Chain of Agents (CoA) architecture.<br>&quot;&quot;&quot;<br>def detect_lang(state: HRSystemAgentState) -&gt; dict[str, Any]:<br>    &quot;&quot;&quot;Detect the language of the user message.&quot;&quot;&quot;<br>    language = detect(state[&quot;user_message&quot;].content.replace(&quot;\\n&quot;, &quot;&quot;)).get(&quot;lang&quot;)<br>    return {<br>        &quot;user_message_language&quot;: language,<br>    }<br>worker_agent_system_prompt = &quot;&quot;&quot;<br>You are given a PASSAGE containing segments numbered as &lt;#1#&gt;, &lt;#2#&gt;, &lt;#3#&gt;, etc.,<br>followed by a Chat History. Extract ONLY the segment-numbers from <br>the PASSAGE that are RELEVANT to the user&#39;s last message in the given Chat History.<br>{context}<br>Chat History:<br>{chat_history}<br>&quot;&quot;&quot;<br>def extract_text_for_markers(text: str, markers: list[int]) -&gt; str:<br>    &quot;&quot;&quot;Extract text for the given markers from the text.&quot;&quot;&quot;<br>    # Compile a pattern to match &lt;#NUMBER#&gt; followed by<br>    # any text (captured lazily) until the next &lt;#NUMBER#&gt; or string end.<br>    pattern = re.compile(r&quot;&lt;#(\\d+)#&gt;(.*?)(?=(&lt;#\\d+#&gt;)|$)&quot;, re.DOTALL)<br>    # dictionary to store marker -&gt; corresponding text<br>    marker_dict = {}<br>    for match in pattern.finditer(text):<br>        marker_id = int(match.group(1))  # The number inside &lt;#...#&gt;<br>        marker_text = match.group(2).strip()  # The text after this marker<br>        marker_dict[marker_id] = marker_text<br>    # Retrieve the texts for the requested markers, in the given order<br>    result = []<br>    for m in markers:<br>        if m in marker_dict:<br>            result.append(marker_dict[m])<br>    # Join by new line<br>    return &quot;\\n&quot;.join(result)<br>class RelevantSegmentNumbers(BaseModel):<br>    &quot;&quot;&quot;A list of relevant segment numbers.&quot;&quot;&quot;<br>    relevant_segment_numbers: list[int]<br>async def worker_agent(<br>    state: WorkerAgentState,<br>) -&gt; dict[str, list[str]]:<br>    &quot;&quot;&quot;Worker Agent.&quot;&quot;&quot;<br>    model = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)<br>    if state[&quot;user_message_language&quot;] == &quot;en&quot;:<br>        if state[&quot;current_agent&quot;] == &quot;WorkerAgent1&quot;:<br>            context = worker_agent_1_context_en<br>        elif state[&quot;current_agent&quot;] == &quot;WorkerAgent2&quot;:<br>            context = worker_agent_2_context_en<br>        elif state[&quot;current_agent&quot;] == &quot;WorkerAgent3&quot;:<br>            context = worker_agent_3_context_en<br>        elif state[&quot;current_agent&quot;] == &quot;WorkerAgent4&quot;:<br>            context = worker_agent_4_context_en<br>        elif state[&quot;current_agent&quot;] == &quot;WorkerAgent5&quot;:<br>            context = worker_agent_5_context_en<br>        else:<br>            context = worker_agent_6_context_en<br>    else:<br>        if state[&quot;current_agent&quot;] == &quot;WorkerAgent1&quot;:<br>            context = worker_agent_1_context_jp<br>        elif state[&quot;current_agent&quot;] == &quot;WorkerAgent2&quot;:<br>            context = worker_agent_2_context_jp<br>        elif state[&quot;current_agent&quot;] == &quot;WorkerAgent3&quot;:<br>            context = worker_agent_3_context_jp<br>        elif state[&quot;current_agent&quot;] == &quot;WorkerAgent4&quot;:<br>            context = worker_agent_4_context_jp<br>        elif state[&quot;current_agent&quot;] == &quot;WorkerAgent5&quot;:<br>            context = worker_agent_5_context_jp<br>        else:<br>            context = worker_agent_6_context_jp<br>    chat_history = &quot;\\n&quot;.join(<br>        [f&quot;{message.role}: {message.content}&quot; for message in state[&quot;chat_history&quot;]]<br>    )<br>    chat_history = (<br>        chat_history<br>        + f&quot;\\n{state[&#39;user_message&#39;].role}: {state[&#39;user_message&#39;].content}&quot;<br>    )<br>    system_prompt = worker_agent_system_prompt.format(<br>        context=context,<br>        chat_history=chat_history,<br>    )<br>    prompt_messages: list[dict[Any, Any]] = [<br>        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt}<br>    ]<br>    prompt_messages.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: state[&quot;user_message&quot;].content})<br>    response = await model.with_structured_output(RelevantSegmentNumbers).ainvoke(<br>        prompt_messages<br>    )<br>    relevant_excerpts = extract_text_for_markers(<br>        context, response.relevant_segment_numbers<br>    )<br>    return {&quot;worker_agent_responses&quot;: [relevant_excerpts]}<br>def execute_worker_agents(state: HRSystemAgentState) -&gt; list[Send] | Send:<br>    &quot;&quot;&quot;Execute the worker agents.&quot;&quot;&quot;<br>    # We will return a list of `Send` objects<br>    # Each `Send` object consists of the name of a node in the graph<br>    # as well as the state to send to that node<br>    return [<br>        Send(<br>            &quot;worker_agent&quot;,<br>            {<br>                &quot;current_agent&quot;: agent,<br>                &quot;user_message&quot;: state[&quot;user_message&quot;],<br>                &quot;chat_history&quot;: state[&quot;chat_history&quot;],<br>                &quot;user_message_language&quot;: state[&quot;user_message_language&quot;],<br>            },<br>        )<br>        for agent in [<br>            &quot;WorkerAgent1&quot;,<br>          &quot;WorkerAgent2&quot;,<br>          &quot;WorkerAgent3&quot;,<br>          &quot;WorkerAgent4&quot;,<br>          &quot;WorkerAgent5&quot;,<br>          &quot;WorkerAgent6&quot;,<br>        ]<br>    ]<br>manager_agent_instructions = &quot;&quot;&quot; <br>Act as the experienced HR professional that you are for Gaudiy.<br>Thoroughly review the HR system information provided here: <br>{context}<br>Find the most relevant parts from the above information and use it to answer the user&#39;s last message in this chat history:<br>{chat_history}<br>&quot;&quot;&quot;<br>async def manager_agent(state: HRSystemAgentState) -&gt; dict[str, str]:<br>    &quot;&quot;&quot;Manager Agent.&quot;&quot;&quot;<br>    if state[&quot;user_message_language&quot;] == &quot;en&quot;:<br>        domain_agent_initial_instructions = domain_agent_initial_instructions_en<br>        few_shots = combined_response_few_shots_en<br>    else:<br>        domain_agent_initial_instructions = domain_agent_initial_instructions_jp<br>        few_shots = combined_response_few_shots_jp<br>    chat_history = &quot;\\n&quot;.join(<br>        [f&quot;{message.role}: {message.content}&quot; for message in state[&quot;chat_history&quot;]]<br>    )<br>    chat_history = (<br>        chat_history<br>        + f&quot;\\n{state[&#39;user_message&#39;].role}: {state[&#39;user_message&#39;].content}&quot;<br>    )<br>    context = &quot;\\n&quot;.join(state[&quot;worker_agent_responses&quot;])<br>    system_prompt = f&quot;&quot;&quot;<br>{domain_agent_initial_instructions}<br>Keep in mind that the current date and time is {datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)}.<br>{manager_agent_instructions.format(<br>    context=context,<br>    chat_history=chat_history,<br>)}<br>&quot;&quot;&quot;<br>    prompt_messages = [{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt}]<br>    prompt_messages.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: state[&quot;user_message&quot;].content})<br>    model = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)<br>    response = await model.ainvoke(prompt_messages)<br>    return {&quot;agent_response&quot;: str(response.content)}<br># Define a new graph<br>workflow = StateGraph(HRSystemAgentState)<br># Add the node to the graph<br>workflow.add_node(&quot;detect_lang&quot;, detect_lang)<br>workflow.add_edge(&quot;__start__&quot;, &quot;detect_lang&quot;)<br>workflow.add_node(&quot;worker_agent&quot;, worker_agent)<br>workflow.add_conditional_edges(&quot;detect_lang&quot;, execute_worker_agents, [&quot;worker_agent&quot;])<br>workflow.add_node(&quot;manager_agent&quot;, manager_agent)<br>workflow.add_edge(&quot;worker_agent&quot;, &quot;manager_agent&quot;)<br>workflow.add_edge(&quot;manager_agent&quot;, &quot;__end__&quot;)<br># Compile the workflow into an executable graph<br>graph = workflow.compile()<br>graph.name = &quot;HR System Agent&quot;  # This defines the custom name in LangSmith</pre><p>As you can see in the above code, the key improvement is in the worker agent’s extraction prompt. The approach is as follows:</p><ul><li><strong>Segment and Number the Text</strong></li></ul><p>Break your context into small chunks (granularity depends on your context, but ideally, try to keep each segment max 1–2 paragraphs) and number them. For example:</p><ul><li>Original: “My name is Shreyas. I live in Osaka. I work at Gaudiy.”</li><li>Numbered: “&lt;#1#&gt;My name is Shreyas. &lt;#2#&gt;I live in Osaka. &lt;#3#&gt;I work at Gaudiy.”</li></ul><p>If the context is small, this could be done manually or in Python by using some heuristics or by an LLM.</p><ul><li><strong>Identify Relevant Segments via LLM</strong></li></ul><p>Instead of asking the LLM to return the text, prompt it to list the numbers of the chunks relevant to the query(for example, the output could be [5, 9, 19]). This reduces output tokens (saving cost and time) and avoids the risk of the LLM changing or adding information, which means more reliable extraction.</p><ul><li><strong>Extract the Chunks</strong></li></ul><p>Use a simple regex (or a similar method) to pull out the numbered segments based on the LLM’s output.</p><p>While searching for ways to improve LLM extraction, I found this post: <a href="https://www.reddit.com/r/MachineLearning/comments/17k6iha/comment/kiosgwr/">https://www.reddit.com/r/MachineLearning/comments/17k6iha/comment/kiosgwr/</a>, which is the basis for this improvement.</p><p>Let’s conclude this section by comparing the eval scores:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/976/1*faBFPI1k-vrdfL1G9y7RiA.png" /><figcaption>pae_results</figcaption></figure><blockquote>💡<strong>Findings:</strong></blockquote><blockquote>1. We can see a 6.2% (0.58 → 0.616) improvement in the score by the improved extraction technique compared to the direct prompt extraction in PAE.</blockquote><blockquote>2. The score is still slightly lower than that of SingleAgent gpt4o but the cost is significantly lower at almost comparable latency.</blockquote><blockquote>3. Parallel Agents Extract (Improved Extraction Technique) outperforms Chain Of Agents not only on score but also on latency as the worker agents operate in parallel.</blockquote><h3>2. Mixture Of Agents</h3><p><a href="https://arxiv.org/abs/2406.04692">Mixture-of-Agents Enhances Large Language Model Capabilities</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WF6Ne7vJFk4xm0NafhgBTg.png" /><figcaption>mixture of agents</figcaption></figure><h4>What does it do?</h4><p>Inspired by Mixture of Experts, the authors suggest the idea of arranging n agents in m layers, with each agent in a layer generating output to the input prompt while aggregating the answers generated by the agents in the previous layer. Naturally, the agents in the first layer do not have any prior layer outputs. Each of these layer of agents are called a Proposer agent, and the last layer has only 1 agent, which is called an Aggregator Agent. The model used for each of these agents is basically supposed to be a cheap/weak model, but overall, as a system, it seems to outperform stronger models like gpt4o. These models can be a mix of models or the same model.</p><h4>Implementing HRSystemAgent</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/890/1*SvqyoEI_9ehmhtk0voz4Fw.png" /><figcaption>Graph visualized in LangGraph Studio</figcaption></figure><p>LangGraph Code(MoA with each proposer processing entire context):</p><pre>&quot;&quot;&quot;This module defines the state representation for the HR System Agent.&quot;&quot;&quot;<br><br>import operator<br>from dataclasses import dataclass, field<br>from typing import Annotated, Any, Literal, TypedDict<br>from pydantic import BaseModel<br>class Message(BaseModel):  # noqa: D101<br>    role: Literal[&quot;user&quot;, &quot;assistant&quot;]<br>    content: str<br>@dataclass<br>class HRSystemAgentState:<br>    &quot;&quot;&quot;A state representation for the HR System Agent.&quot;&quot;&quot;<br>    worker_agent_responses: Annotated[list[str], operator.add]<br>    user_message: Message<br>    chat_history: Annotated[list[Message], operator.add]<br>    user_message_language: str = field(default=&quot;ja&quot;)<br>    agent_response: str = field(default=&quot;&quot;)<br>    proposer_agent_responses: Annotated[list[str], operator.add] = field(<br>        default_factory=list<br>    )<br>    previous_layer_response: str = field(default=&quot;&quot;)<br>    current_layer_index: int = field(default=0)<br>class MOAAgentState(TypedDict):<br>    &quot;&quot;&quot;A state representation for a Proposer Agent.&quot;&quot;&quot;<br>    worker_agent_responses: Annotated[list[str], operator.add]<br>    user_message: Message<br>    chat_history: Annotated[list[Message], operator.add]<br>    user_message_language: str<br>    previous_layer_response: str | None<br>    model: Any<br>&quot;&quot;&quot;Define the graph for the HR System Agent.<br>This module defines a custom graph for the HR System Agent based on Chain of Agents (CoA) architecture.<br>&quot;&quot;&quot;<br>def detect_lang(state: HRSystemAgentState) -&gt; dict[str, Any]:<br>    &quot;&quot;&quot;Detect the language of the user message.&quot;&quot;&quot;<br>    language = detect(state.user_message.content.replace(&quot;\\n&quot;, &quot;&quot;)).get(&quot;lang&quot;)<br>    return {<br>        &quot;user_message_language&quot;: language,<br>    }<br>manager_agent_instructions = &quot;&quot;&quot; <br>Act as the experienced HR professional that you are for Gaudiy.<br>Thoroughly review the HR system information provided here: <br>{context}<br>Find the most relevant parts from the above information and use it to answer the user&#39;s last message in this chat history:<br>{chat_history}<br>&quot;&quot;&quot;<br>aggregation_instructions = &quot;&quot;&quot;<br>You have been provided with a set of responses to the latest user query.<br>Your task is to synthesize these responses into a single, high-quality response.<br>It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect.<br>Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction.<br>Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability.<br>{previous_layer_response}<br>&quot;&quot;&quot;<br># common function for both proposer and aggregation agent<br>async def manager_agent(state: MOAAgentState) -&gt; dict[str, str]:<br>    &quot;&quot;&quot;Manager Agent.&quot;&quot;&quot;<br>    if state[&quot;user_message_language&quot;] == &quot;en&quot;:<br>        domain_agent_initial_instructions = domain_agent_initial_instructions_en<br>        few_shots = combined_response_few_shots_en<br>        context = context_en<br>    else:<br>        domain_agent_initial_instructions = domain_agent_initial_instructions_jp<br>        few_shots = combined_response_few_shots_jp<br>        context = context_jp<br>    chat_history = &quot;\\n&quot;.join(<br>        [f&quot;{message.role}: {message.content}&quot; for message in state[&quot;chat_history&quot;]]<br>    )<br>    chat_history = (<br>        chat_history<br>        + f&quot;\\n{state[&#39;user_message&#39;].role}: {state[&#39;user_message&#39;].content}&quot;<br>    )<br>    if state[&quot;previous_layer_response&quot;]:<br>        aggregation = aggregation_instructions.format(<br>            previous_layer_response=state[&quot;previous_layer_response&quot;]<br>        )<br>    else:<br>        aggregation = &quot;&quot;<br>    system_prompt = f&quot;&quot;&quot;<br>{domain_agent_initial_instructions}<br>Keep in mind that the current date and time is {datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)}.<br>{manager_agent_instructions.format(<br>    context=context,<br>    chat_history=chat_history,<br>)}<br>{aggregation}<br>&quot;&quot;&quot;<br>    prompt_messages = [{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt}]<br>    prompt_messages.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: state[&quot;user_message&quot;].content})<br>    if &quot;model&quot; in state:<br>        model = state[&quot;model&quot;]<br>    else:<br>        model = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)<br>    response = await model.ainvoke(prompt_messages)<br>    return {&quot;agent_response&quot;: str(response.content)}<br>async def proposer_agent(state: MOAAgentState) -&gt; dict[str, list[str] | int]:<br>    &quot;&quot;&quot;Proposer Agent.&quot;&quot;&quot;<br>    proposer_agent_response = (await manager_agent(state))[&quot;agent_response&quot;]<br>    return {<br>        &quot;proposer_agent_responses&quot;: [proposer_agent_response],<br>    }<br>async def aggregator_agent(state: MOAAgentState) -&gt; dict[str, str]:<br>    &quot;&quot;&quot;Aggregator Agent.&quot;&quot;&quot;<br>    return await manager_agent(state)<br>async def moa_layer_agent(state: HRSystemAgentState) -&gt; Any:<br>    &quot;&quot;&quot;MOA Layer Agent.&quot;&quot;&quot;<br>    if state.proposer_agent_responses:<br>        previous_layer_response = &quot;&quot;<br>        for i in range(1, 4): # vary here to change number of agents in 1 layer<br>            previous_layer_response += (<br>                f&quot;{i}. &quot; + state.proposer_agent_responses[-i] + &quot;\\n\\n&quot;<br>            )<br>        return {<br>            &quot;previous_layer_response&quot;: previous_layer_response,<br>            &quot;current_layer_index&quot;: state.current_layer_index + 1,<br>        }<br>async def execute_proposer_agents_or_aggregator_agent(<br>    state: HRSystemAgentState,<br>) -&gt; list[Send] | Send:<br>    &quot;&quot;&quot;Decide whether to run the proposer agents or the aggregator agent.&quot;&quot;&quot;<br>    if state.current_layer_index &lt; 1: # vary here to change number of proposer layers<br>        return [<br>            Send(<br>                &quot;proposer_agent&quot;,<br>                {<br>                    &quot;user_message&quot;: state.user_message,<br>                    &quot;chat_history&quot;: state.chat_history,<br>                    &quot;user_message_language&quot;: state.user_message_language,<br>                    &quot;worker_agent_responses&quot;: state.worker_agent_responses,<br>                    &quot;previous_layer_response&quot;: state.previous_layer_response,<br>                    &quot;model&quot;: model,<br>                },<br>            )<br>            for model in [<br>                ChatVertexAI(model_name=&quot;gemini-1.5-flash-001&quot;, temperature=0.7),<br>                ChatVertexAI(model_name=&quot;gemini-1.5-flash-001&quot;, temperature=0.7),<br>                ChatVertexAI(model_name=&quot;gemini-1.5-flash-001&quot;, temperature=0.7),<br>            ]<br>        ]<br>    else:<br>        return Send(<br>            &quot;aggregator_agent&quot;,<br>            {<br>                &quot;user_message&quot;: state.user_message,<br>                &quot;chat_history&quot;: state.chat_history,<br>                &quot;user_message_language&quot;: state.user_message_language,<br>                &quot;worker_agent_responses&quot;: state.worker_agent_responses,<br>                &quot;previous_layer_response&quot;: state.previous_layer_response,<br>            },<br>        )<br># Define a new graph<br>workflow = StateGraph(HRSystemAgentState)<br># Add the node to the graph<br>workflow.add_node(&quot;detect_lang&quot;, detect_lang)<br>workflow.add_edge(&quot;__start__&quot;, &quot;detect_lang&quot;)<br>workflow.add_edge(&quot;detect_lang&quot;, &quot;moa_layer_agent&quot;)<br>workflow.add_node(&quot;moa_layer_agent&quot;, moa_layer_agent)<br>workflow.add_node(&quot;proposer_agent&quot;, proposer_agent)<br>workflow.add_node(&quot;aggregator_agent&quot;, aggregator_agent)<br>workflow.add_conditional_edges(<br>    &quot;moa_layer_agent&quot;,<br>    execute_proposer_agents_or_aggregator_agent,<br>    [&quot;proposer_agent&quot;, &quot;aggregator_agent&quot;],<br>)<br>workflow.add_edge(&quot;proposer_agent&quot;, &quot;moa_layer_agent&quot;)<br>workflow.add_edge(&quot;aggregator_agent&quot;, &quot;__end__&quot;)<br># Compile the workflow into an executable graph<br>graph = workflow.compile()<br>graph.name = &quot;HR System Agent&quot;  # This defines the custom name in LangSmith</pre><p><strong>Details about implementation:</strong></p><ol><li>It was possible to use the prompt of aggregation in each agent from the paper for our use case.</li><li>To determine the optimal configuration, it was necessary to experiment with variations in the number of agents per layer and the number of layers.</li><li>As each proposer agent processing the entire context is token expensive, I also experimented with first using the PAE(enhanced version) workers to extract relevant context first and then feeding only the relevant context to each MoA proposer agent. In other words, MoA becomes the final manager agent of PAE. This is more efficient as for the small additional expense of running a PAE relevant context extractor chain we can substantially reduce the irrelevant token consumption per MoA proposer agent. Of course, the Vector similarity search is another alternative for reducing context per proposer agent if your app has too much context.</li></ol><p><strong>Evaluation Results(Average scores over 5 runs):</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WqdKMJebk1Ic77BV01TR9A.png" /><figcaption>MoA vs Single Agents</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CHrrOTwt0UaNppETexk02g.png" /><figcaption>MoA + PAE vs Single Agent</figcaption></figure><blockquote>💡<strong>Findings:</strong></blockquote><blockquote>1. First of all we can see that almost all variations of MoA outperform SingleAgent gpt4o which matches the claims in the paper. The configuration of 2 proposer layers with 6 agents each with Full context added to each proposer using gpt4o-mini gave the best score (+6% vs SingleAgent gpt4o!) while reducing the cost.</blockquote><blockquote>2. However the latency for the best configuration is 6x so there is need to choose the appropriate configuration by balancing the trade off between performance and latency based on a usecase or design the UX in a way that intermediate outputs are displayed to the user.</blockquote><blockquote>3. We see a clear pattern that performance improves with number of proposer agents with a saturation at 3x6.</blockquote><blockquote>4. We see that MoA with PAE improves cost and latency with almost similar performance to Full context MoA so it recommended for practical use in apps. In addition using gemini-flash could reduce cost and latency while maintaining performance but remains to be seen. MoA with PAE can be an indicator of how MoA performs for RAG apps which the paper hasn’t directly addressed!</blockquote><blockquote>5. It was unclear why there wasn’t a clear pattern of performance improving with number of proposer agents in MoA with PAE.</blockquote><h3>3. Multi Agent Debate</h3><ul><li><a href="https://arxiv.org/abs/2406.11776">Improving Multi-Agent Debate with Sparse Communication Topology</a></li><li><a href="https://arxiv.org/abs/2312.04854">Learning to Break: Knowledge-Enhanced Reasoning in Multi-Agent Debate System</a></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_9TnkUNhtd-zH7ermBMm7w.png" /></figure><p>The experiment has been conducted by merging ideas from both papers.</p><h4>What does it do?</h4><p>Multiple debate agents, after generating their own independent responses, exchange their responses with their neighbor agents and refine their responses based on the responses from their neighbor agents for multiple rounds. A number of rounds can be determined statically or dynamically by having a judge agent judge the responses of debate agents for accuracy, consistency, and consensus and order them to continue with more rounds if needed. Finally, at the end of the last debate round, the responses of all debate agents are sent to a Summarizer agent that aggregates their responses on the assumption that they are all correct and agree with each other. In certain cases, a majority vote (median answer) could be applied to choose the final answer, such as a numerical answer to a Math problem.</p><h4>Implementing HRSystemAgent</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/982/1*pQLWSIPaJqgzOcvjKA78aw.png" /><figcaption>Graph visualized in LangGraph Studio</figcaption></figure><p>LangGraph Code(4 Debate Agents debating for 3 rounds):</p><pre>&quot;&quot;&quot;This module defines the state representation for the HR System Agent.&quot;&quot;&quot;<br>import operator<br>from dataclasses import dataclass, field<br>from typing import Annotated, Literal, TypedDict<br>from pydantic import BaseModel<br>class Message(BaseModel):  # noqa: D101<br>    role: Literal[&quot;user&quot;, &quot;assistant&quot;]<br>    content: str<br>@dataclass<br>class HRSystemAgentState:<br>    &quot;&quot;&quot;A state representation for the HR System Agent.&quot;&quot;&quot;<br>    user_message: Message<br>    chat_history: Annotated[list[Message], operator.add]<br>    user_message_language: str = field(default=&quot;ja&quot;)<br>    agent_response: str = field(default=&quot;&quot;)<br>    debate_agents_responses: Annotated[list[str], operator.add] = field(<br>        default_factory=list<br>    )<br>    previous_round_responses: str = field(default=&quot;&quot;)<br>    current_round_index: int = field(default=0)<br>class MADAgentState(TypedDict):<br>    &quot;&quot;&quot;A state representation for the Multi Agent Debate Agent.&quot;&quot;&quot;<br>    user_message: Message<br>    chat_history: Annotated[list[Message], operator.add]<br>    user_message_language: str<br>    neighbor_agents_responses: str | None<br>    your_previous_answer: str | None<br>    debate_agent_index: int<br>&quot;&quot;&quot;Define the graph for the HR System Agent.<br>This module defines a custom graph for the HR System Agent based on Chain of Agents (CoA) architecture.<br>&quot;&quot;&quot;<br>def detect_lang(state: HRSystemAgentState) -&gt; dict[str, Any]:<br>    &quot;&quot;&quot;Detect the language of the user message.&quot;&quot;&quot;<br>    language = detect(state.user_message.content.replace(&quot;\\n&quot;, &quot;&quot;)).get(&quot;lang&quot;)<br>    return {<br>        &quot;user_message_language&quot;: language,<br>    }<br>initial_answer_instructions = &quot;&quot;&quot; <br>Act as the experienced HR professional that you are for Gaudiy.<br>Thoroughly review the HR system information provided here: <br>{context}<br>Find the most relevant parts from the above information and use it to answer the user&#39;s last message in this chat history:<br>{chat_history}<br>&quot;&quot;&quot;<br>debate_instructions = &quot;&quot;&quot;<br>There are a few other agents assigned the same task, it&#39;s your responsibility to discuss with them and think critically. You can update your previous answer with other agents&#39;<br>answers or given context as advice, or you can maintain your previous answer. Output your new answer to the user&#39;s last message in the chat history below.<br>Context:<br>{context}<br>Other agents&#39; answers:<br>{neighbor_agents_responses}<br>Your previous answer:<br>{your_previous_answer}<br>Chat History:<br>{chat_history}<br>&quot;&quot;&quot;<br>aggregation_instructions_v1 = &quot;&quot;&quot;<br>You are an intelligent summarizer agent, tasked with synthesizing the responses of other agents into a concise and comprehensive final answer. Your role is not to<br>generate original responses, but to condense the information provided by other agents into a succinct summary. You should maintain the writing style and tone of the other agent responses.<br>Responses from other agents:<br>{last_round_responses}<br>&quot;&quot;&quot;<br>async def debate_agent(state: MADAgentState) -&gt; dict[str, list[str]]:<br>    &quot;&quot;&quot;Debate Agent&quot;&quot;&quot;<br>    if state[&quot;user_message_language&quot;] == &quot;en&quot;:<br>        domain_agent_initial_instructions = domain_agent_initial_instructions_en<br>        context = context_en<br>    else:<br>        domain_agent_initial_instructions = domain_agent_initial_instructions_jp<br>        context = context_jp<br>    chat_history = &quot;\\n&quot;.join(<br>        [f&quot;{message.role}: {message.content}&quot; for message in state[&quot;chat_history&quot;]]<br>    )<br>    chat_history = (<br>        chat_history<br>        + f&quot;\\n{state[&#39;user_message&#39;].role}: {state[&#39;user_message&#39;].content}&quot;<br>    )<br>    if &quot;neighbor_agents_responses&quot; in state:<br>        debate_agent_instructions = debate_instructions.format(<br>            context=context,<br>            chat_history=chat_history,<br>            neighbor_agents_responses=state[&quot;neighbor_agents_responses&quot;],<br>            your_previous_answer=state[&quot;your_previous_answer&quot;],<br>        )<br>    else:<br>        debate_agent_instructions = initial_answer_instructions.format(<br>            context=context,<br>            chat_history=chat_history,<br>        )<br>    system_prompt = f&quot;&quot;&quot;<br>{domain_agent_initial_instructions}<br>Keep in mind that the current date and time is {datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)}.<br>{debate_agent_instructions}<br>&quot;&quot;&quot;<br>    prompt_messages = [{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt}]<br>    model = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)<br>    response = await model.ainvoke(prompt_messages)<br>    return {<br>        &quot;debate_agents_responses&quot;: [<br>            &quot;DebateAgent&quot;<br>            + str(state[&quot;debate_agent_index&quot;])<br>            + &quot;: &quot;<br>            + str(response.content)<br>        ]<br>    }<br>async def aggregator_agent(state: HRSystemAgentState) -&gt; dict[str, str]:<br>    &quot;&quot;&quot;Aggregator Agent.&quot;&quot;&quot;<br>    system_prompt = f&quot;&quot;&quot;<br>    {aggregation_instructions_v1.format(<br>        last_round_responses=state[&quot;previous_round_responses&quot;]<br>    )}<br>    &quot;&quot;&quot;<br>    prompt_messages = [{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt}]<br>    model = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)<br>    response = await model.ainvoke(prompt_messages)<br>    return {&quot;agent_response&quot;: str(response.content)}<br>async def mad_round_agent(state: HRSystemAgentState) -&gt; Any:<br>    &quot;&quot;&quot;MAD Round Agent.&quot;&quot;&quot;<br>    if state.debate_agents_responses:<br>        previous_round_responses = &quot;&quot;<br>        for i in range(1, 5): # vary here to change number of debate agents<br>            if previous_round_responses:<br>                previous_round_responses += &quot;\\n=====================\\n&quot;<br>            previous_round_responses += state.debate_agents_responses[-i]<br>        return {<br>            &quot;previous_round_responses&quot;: previous_round_responses,<br>            &quot;current_round_index&quot;: state.current_round_index + 1,<br>        }<br>async def execute_debate_agents_or_aggregator_agent(<br>    state: HRSystemAgentState,<br>) -&gt; list[Send] | Send:<br>    &quot;&quot;&quot;Decide whether to run the debate agents or the aggregator agent.&quot;&quot;&quot;<br>    if state.current_round_index == 0:<br>        return [<br>            Send(<br>                &quot;debate_agent&quot;,<br>                {<br>                    &quot;user_message&quot;: state.user_message,<br>                    &quot;chat_history&quot;: state.chat_history,<br>                    &quot;user_message_language&quot;: state.user_message_language,<br>                    &quot;debate_agent_index&quot;: i + 1,<br>                },<br>            )<br>            for i in range(4) # vary here to change number of debate agents<br>        ]<br>    elif state.current_round_index &lt; 3: # vary here to change number of debate rounds<br>        previous_round_responses_list = state.previous_round_responses.split(<br>            &quot;\\n=====================\\n&quot;<br>        )<br>        to_send: list[Send] = []<br>        for i in range(len(previous_round_responses_list)):<br>            your_previous_answer = previous_round_responses_list[i].split(&quot;: &quot;, 1)[1]<br>            debate_agent_index = (<br>                previous_round_responses_list[i]<br>                .split(&quot;: &quot;, 1)[0]<br>                .split(&quot;DebateAgent&quot;, 1)[1]<br>            )<br>            other_agents_responses = (<br>                previous_round_responses_list[:i]<br>                + previous_round_responses_list[i + 1 :]<br>            )<br>            neighbor_agents_responses = &quot;&quot;<br>            for response in other_agents_responses:<br>                if (<br>                    (<br>                        debate_agent_index in [&quot;1&quot;, &quot;4&quot;]<br>                        and response.split(&quot;: &quot;, 1)[0]<br>                        in [<br>                            &quot;DebateAgent2&quot;,<br>                            &quot;DebateAgent3&quot;,<br>                        ]<br>                    )<br>                    or (<br>                        debate_agent_index in [&quot;2&quot;, &quot;3&quot;]<br>                        and response.split(&quot;: &quot;, 1)[0]<br>                        in [&quot;DebateAgent1&quot;, &quot;DebateAgent4&quot;]<br>                    )<br>                ):<br>                    if neighbor_agents_responses:<br>                        neighbor_agents_responses += &quot;\\n\\n&quot;<br>                    neighbor_agents_responses += response<br>            to_send.append(<br>                Send(<br>                    &quot;debate_agent&quot;,<br>                    {<br>                        &quot;user_message&quot;: state.user_message,<br>                        &quot;chat_history&quot;: state.chat_history,<br>                        &quot;user_message_language&quot;: state.user_message_language,<br>                        &quot;neighbor_agents_responses&quot;: neighbor_agents_responses,<br>                        &quot;your_previous_answer&quot;: your_previous_answer,<br>                        &quot;debate_agent_index&quot;: debate_agent_index,<br>                    },<br>                )<br>            )<br>        return to_send<br>    else:<br>        return Send(<br>            &quot;aggregator_agent&quot;,<br>            {<br>                &quot;user_message&quot;: state.user_message,<br>                &quot;chat_history&quot;: state.chat_history,<br>                &quot;user_message_language&quot;: state.user_message_language,<br>                &quot;previous_round_responses&quot;: state.previous_round_responses,<br>            },<br>        )<br># Define a new graph<br>workflow = StateGraph(HRSystemAgentState)<br># Add the node to the graph<br>workflow.add_node(&quot;detect_lang&quot;, detect_lang)<br>workflow.add_edge(&quot;__start__&quot;, &quot;detect_lang&quot;)<br>workflow.add_edge(&quot;detect_lang&quot;, &quot;mad_round_agent&quot;)<br>workflow.add_node(&quot;mad_round_agent&quot;, mad_round_agent)<br>workflow.add_node(&quot;debate_agent&quot;, debate_agent)<br>workflow.add_node(&quot;aggregator_agent&quot;, aggregator_agent)<br>workflow.add_conditional_edges(<br>    &quot;mad_round_agent&quot;,<br>    execute_debate_agents_or_aggregator_agent,<br>    [&quot;debate_agent&quot;, &quot;aggregator_agent&quot;],<br>)<br>workflow.add_edge(&quot;debate_agent&quot;, &quot;mad_round_agent&quot;)<br>workflow.add_edge(&quot;aggregator_agent&quot;, &quot;__end__&quot;)<br># Compile the workflow into an executable graph<br>graph = workflow.compile()<br>graph.name = &quot;HR System Agent&quot;  # This defines the custom name in LangSmith</pre><p>Details about implementation:</p><ol><li>Prompts are basically as-is from the paper with minor modifications to incorporate sparse connections between debators and our chat agent use case.</li><li>Judge agent has been skipped and debate rounds are set statically but could be added trivially.</li></ol><p>Evaluation Results:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cWgoTHjecmhsnMGJ3yVyEA.png" /><figcaption>MAD results</figcaption></figure><blockquote>💡<strong>Findings &amp; promising possibilities:</strong></blockquote><blockquote>1. We see Multi Agent Debate using gpt4o-mini outperforms SingleAgent gpt4o. The cost though is half that of gpt4o with a latency of 5.5x. A configuration of 4 debate agents and 2 rounds might be more cost and latency effective while also giving better performance than gpt4o.</blockquote><blockquote>2. Similar to MoA using a cheaper model like gemini-flash or oss model could further reduce cost and latency. And it may be worth to experiment with only 2 debate agents as well to further reduce cost.</blockquote><blockquote>3. Adding a judge agent might further help reduce cost by dynamically controlling the appropriate rounds of debate!</blockquote><blockquote>Judge Agent Prompt:</blockquote><blockquote>As a judge agent, your primary responsibility is to impartially evaluate the responses of other agents for consistency. Please ensure your judgments are objective, relying solely on the coherence and alignment of the provided answers.</blockquote><h3>Conclusion</h3><p>We looked at some multi-agent architectures and realized that all of them outperform SingleAgent for the same LLM, and several of them even outperform SingleAgent GPT-4o at a much lower cost. Increased latency largely remains a problem for them, but we saw that certain promising ways exist to control it.</p><p>This was part 2 of a three-part series. In part 3, I will discuss some of the applications we have built internally using these Multi-Agent architectures!</p><p>More Articles:</p><p><a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a></p><p>About Company:</p><p><a href="https://gaudiy.com/en/">Gaudiy Inc. | Moving forward in a new era with fans</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0513d44c9dc6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gaudiy-ai-lab/0513d44c9dc6">Experimenting with Multi Agent Architectures — Can they outperform SingleAgent GPT-4o?</a> was originally published in <a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Considerations on Multi Agents(Part1)- A Comprehensive Survey]]></title>
            <link>https://medium.com/gaudiy-ai-lab/1b1778345ad9?source=rss----096fc41648f8---4</link>
            <guid isPermaLink="false">https://medium.com/p/1b1778345ad9</guid>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[multi-agent-systems]]></category>
            <category><![CDATA[gaudiy]]></category>
            <category><![CDATA[llm-agent]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Gaudiy Lab]]></dc:creator>
            <pubDate>Tue, 04 Mar 2025 12:52:43 GMT</pubDate>
            <atom:updated>2025-04-09T01:15:49.518Z</atom:updated>
            <content:encoded><![CDATA[<h3>Considerations on Multi Agents - A Comprehensive Survey</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NUEXpLfu7vowTYaRf6xdyA.jpeg" /></figure><p><em>Posted by </em><a href="https://x.com/ShreyasGenAI"><em>Shreyas Shinde</em></a><em>, AI Engineer of </em><a href="https://gaudiy.com/en/"><em>Gaudiy Inc</em></a><em>.</em></p><p>Building high-quality LLM applications is no easy task, especially when the latest advancements in AI multi-agent systems are scattered across research papers, blog posts, and experimental projects.</p><p>In this article, we cut through the noise and provide a state-of-the-art overview of multi-agent architectures, offering practical insights you can apply to your own AI development.</p><h3>About Gaudiy’s role in the world of AI Agents</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fzG--GZJZpLejUNgmzkeIg.jpeg" /></figure><p>Gaudiy’s AI team vision states that we aim to simulate complex societies through the evolution of generative AI and computing power.</p><p>Initially, we focus on breaking down specific tasks and workflows, enabling LLMs to achieve human-equivalent performance. Beyond that, we envision a world where Agents, as individual members, collaborate and coordinate to tackle higher-level challenges and goals related to society and communities.</p><h3>What is LLM Agents?</h3><p>The LLM-based system is capable of Context-aware Reasoning on a level of Autonomy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*h4dtCDMo51qzvLcwDF31gw.jpeg" /></figure><h4>Context Awareness</h4><ul><li>The Agent may be instructed to follow a specific Persona</li><li>The Agent may be provided with Memories and/or instructed to generate new ones and use them appropriately</li></ul><p>In terms of LLM Agent, these are achieved by specific techniques such as:</p><p>Prompt Engineering ⇒ Effective ways of instructing an LLM to behave (such as adding pairs of expected input/output as examples)</p><ol><li>Retrieval Augmented Generated ⇒ Provide custom context to the LLM by fetching based on the current input</li><li>Fine Tuning = Make the LLM learn by updating it’s hyperparameters</li></ol><h4>Reasoning</h4><ul><li>The Agent may generate a plan of several steps to take in order to achieve its goals. Or even generate several plans, act on each, evaluate, and refine them. It may want to divide the task at hand into sub tasks and so on..</li><li>The Agent may choose to use tools like web browsing/data fetching if it thinks it is needed</li></ul><p>In terms of LLM Agent, these are achieved by specific techniques such as:</p><ol><li><strong>Prompt Engineering</strong></li></ol><ul><li>Chain of Thought</li><li>Reflection</li><li>Tree of Thought, etc.（cf. <a href="https://www.promptingguide.ai/techniques">Prompting Guide Techniques</a>)</li></ul><p><strong>2. Function Calling</strong></p><p>Presenting a list of function descriptions to an LLM and making it decide if it needs to call any function and output the function. These functions could be web api calls or messages to humans, effectively allowing the LLM to interact with its environment.</p><p>In a practical sense, these cognitive capabilities are at the level of autonomy. Each level of autonomy has its pros/cons, and an appropriate choice needs to be made by creators</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9Gpyk_4pVXMMVsck53T1Vw.jpeg" /></figure><p>💡As you develop these Single-agent systems, they might grow more complex over time, making them harder to manage and scale. For example, you might run into the following problems:</p><ul><li>agent has too many tools at its disposal and makes poor decisions about which tool to call next</li><li>context grows too complex for a single agent to keep track of</li><li>there is a need for multiple specialization areas in the system (e.g., planner, researcher, math expert, etc.)</li></ul><p>To tackle these, you might consider breaking your application into multiple smaller, independent agents and composing them into a <strong>Multi-agent system</strong>.</p><p><strong>Sources:</strong></p><ul><li><a href="https://arxiv.org/abs/2308.11432">https://arxiv.org/abs/2308.11432</a></li><li><a href="https://arxiv.org/abs/2309.02427">https://arxiv.org/abs/2309.02427</a></li><li><a href="https://blog.langchain.dev/what-is-a-cognitive-architecture/">https://blog.langchain.dev/what-is-a-cognitive-architecture/</a></li><li><a href="https://langchain-ai.github.io/langgraph/concepts/multi_agent/">https://langchain-ai.github.io/langgraph/concepts/multi_agent/</a></li><li><a href="https://lilianweng.github.io/posts/2023-06-23-agent/">https://lilianweng.github.io/posts/2023-06-23-agent/</a></li></ul><p>Let’s take a small detour to how Anthropic defines Agents, which will not only be a good preview for the multi-agent architectures that will follow but will also help broaden understanding of the state of AI Agents in 2025.</p><p>Anthropic came out with it in December 2024 and created quite a buzz on X</p><h4>Summary of Agents on Anthropic</h4><p>Anthropic introduces the concept of “agentic systems,” which encompasses both workflows (LLMs and tools arranged in predetermined steps) and agents (LLMs directing their own process without rigid predefinition). Below are the key ideas:</p><p><strong>1. Core Building Block: The Augmented LLM</strong></p><p>An LLM enhanced with features like retrieval, tools, and memory is at the heart of any agentic system. This empowers the model to generate its search queries, decide which tools to use, and determine what information to store.</p><p><strong>2. Workflows: Predefined Code Paths</strong></p><ul><li><strong>Prompt Chaining</strong>: Break a task into sequential steps, with each LLM output feeding into the next. Useful for tasks easily split into subtasks (e.g., writing text and then translating it).</li><li><strong>Routing</strong>: Classify inputs first, then direct them to different specialized processes or models. Ideal for scenarios where certain types of queries need distinct handling.</li><li><strong>Parallelization</strong>: Run multiple subtasks at once or run the same task multiple times to gather diverse outputs. This speeds up execution or increases confidence in the final result.</li><li><strong>Orchestrator-Workers</strong>: One LLM breaks down tasks and delegates them to others, then compiles their results. Suitable for problems where you can’t predefine the exact number or nature of subtasks.</li><li><strong>Evaluator-Optimizer</strong>: One LLM generates output, another evaluates and offers feedback, and they loop until the quality meets a certain standard. Great for tasks benefiting from iterative refinement.</li></ul><p><strong>3. Agents: Autonomous, Flexible Systems</strong></p><ul><li>Agents can operate for many turns without a fixed path, relying on the LLM’s ability to plan, execute, and adapt. They may check back with a human or gather real-world data as they go.</li><li>While powerful for open-ended tasks, agents can accumulate errors over prolonged operations. Hence, thorough testing and guardrails are essential.</li></ul><p><strong>4. Putting It All Together</strong></p><p>These patterns are not strict recipes. Instead, developers can blend them to suit different use cases, adding complexity only when there is a clear performance benefit.</p><p><strong>💡</strong> In conclusion, different players in Academia and/or Industry do not agree on what terms to use for different concepts.</p><ul><li>What one calls an Agent, others may call an Augmented LLM; what one calls Multi-Agents, others may call Agentic Workflows, and purists may consider a system as an Agent only if it is completely Autonomous, and some might just accept that the Agentic behavior is on a level of Autonomy.</li><li>Each player in Academia/Industry seems to be marketing AI Agents from their own perspective to their respective user base for their respective motives.</li></ul><p>Now let’s look at how Multi-Agent Systems differ from Single Agent Systems</p><h3>How are Multi-Agent Systems different?</h3><ul><li>The construction of single-agent systems concentrates on formulating their internal mechanisms and interactions with the external environment.</li><li>On the other hand, LLM-MultiAgent systems emphasize <strong>diverse agent profiles/personas</strong>, <strong>inter-agent interactions</strong>, and <strong>collective decision-making processes</strong>. From this perspective, <strong>more dynamic and complex tasks</strong> can be tackled by the collaboration of multiple autonomous agents, each of which is equipped with unique strategies and behaviors and engaged in communication with one another. (Note that , again these capabilities are on a level of autonomy appropriately chosen by creators)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pVRKkovl7H-AQJtbqhYy4g.jpeg" /></figure><h4>Agents Profiling methods:</h4><ol><li>Pre-defined explicitly by creators</li><li>LLM generated</li><li>Data-derived: generated based on pre-existing datasets</li></ol><h4>Agents Communication:</h4><ol><li><strong>Paradigms:</strong></li></ol><ul><li>Cooperative → Exchanging info and working towards a common goal</li><li>Debate → Goal is to reach a consensus in a practical amount of duration</li><li>Conflicting/Competitive → Agents work towards their own goals which might conflict with other agent’s goals</li></ul><p><strong>2. Structures:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/860/1*NAQJPvGhIVm3SbrkuyTptQ.jpeg" /></figure><p><strong>3. May Acquire new capabilities:</strong></p><ol><li>Feedback from the environment they are operating in.</li><li>Feedback from other agents.</li><li>Feedback from humans ⇒ Human in the loop or Human-as-a-Tool</li><li>Adjust based on new Memory, Self-Evolution (Update its initial instructions), or even generate ad-hoc new agents at runtime!</li></ol><p><strong>4. Agents Orchestration Methodologies:</strong></p><ul><li>These methodologies aim to optimize agents&#39; workflows, task assignments tailored to different agents, and communication patterns across agents, such as communication constraints between agents. Effective agent orchestration facilitates harmonious operation among agents, minimizing conflicts and redundancies</li></ul><p>These systems are applied and studied in various complex domains:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sdXmJWgbnwAUwmTLl70BFw.jpeg" /></figure><p><strong>💡</strong>In summary, by varying Agent profiles, How they communicate, &amp; How they acquire new capabilities, we can design appropriate systems or architecture for a use case!</p><p>A simple Example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LE6oib5_ISz91RVs_pOuIQ.jpeg" /></figure><p><strong>Sources:</strong></p><ul><li><a href="https://github.com/taichengguo/LLM_MultiAgents_Survey_Papers">https://github.com/taichengguo/LLM_MultiAgents_Survey_Papers</a></li><li><a href="https://medium.com/@pallavisinha12/understanding-llm-based-agents-and-their-multi-agent-architecture-299cf54ebae4">https://medium.com/@pallavisinha12/understanding-llm-based-agents-and-their-multi-agent-architecture-299cf54ebae4</a></li><li><a href="https://techcommunity.microsoft.com/blog/aiplatformblog/the-future-of-ai-exploring-multi-agent-ai-systems/4226593">https://techcommunity.microsoft.com/blog/aiplatformblog/the-future-of-ai-exploring-multi-agent-ai-systems/4226593</a></li></ul><h4>What are the properties of Multi-Agent systems/architectures?</h4><ol><li>Single Responsibility Principle/Modularity</li><li>Breakdown of Complex Tasks</li><li>Collaboration</li><li>Scalability</li><li>Fault Tolerance</li><li>Parallelism</li><li>Reusability</li><li>Need to control Costs</li><li>Need to control Latency or design appropriate UX</li></ol><p><strong>💡Interesting to note that there are similarities with Monolithic vs Modular/MicroServices regarding Software Architecture!</strong></p><h3>Multi-Agents in Academia &amp; Industry</h3><h4>MA Architectures/Patterns</h4><p>Below is a categorized list of multi-agent architectures/patterns highlighted in various research papers. They, in some way, try to achieve superiority over single-agent architecture. We will be going through some of them via a deep dive into the paper or an actual experiment.</p><h4>1. Orchestration Patterns</h4><p><strong>a. Route to the most appropriate domain expert agent based on the input</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/820/1*kqAUf6TiAAHJD6rShn2xZA.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2410.07490">https://arxiv.org/abs/2410.07490</a></figcaption></figure><p><strong>b. Decompose the task first and then route to 1 or several appropriate domain agents</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MOZf7eF2Ag6MOYMBN7WbtQ.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2410.02189">https://arxiv.org/abs/2410.02189</a></figcaption></figure><p><strong>c. Chain of Agents</strong></p><p>The full context is distributed among several agents, and generations are sequentially passed on as additional context to the next agent from the previous agent. At the end of the worker agents’ chain, a manager agent synthesizes the information into a coherent final output.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*J86NRBD5jDuR5v_B_TAxhQ.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2406.02818">https://arxiv.org/abs/2406.02818</a></figcaption></figure><p><strong>d. Long Agent</strong></p><p>A leader agent organizes a team to gather information and resolve conflicting information.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vogq9kTOHU1txeqfF26ugA.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2402.11550">https://arxiv.org/abs/2402.11550</a></figcaption></figure><p><strong>e. Dynamically generate Expert Agents based on Evolutionary Algorithms</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/820/1*uwugLmYTrZpu_XhXarsoiA.jpeg" /><figcaption>Source: <a href="https://evo-agent.github.io/">https://evo-agent.github.io/</a></figcaption></figure><h4>2. Multiple Generations followed by Selection/Aggregation Patterns</h4><p><strong>a. Generate multiple candidate codes and verify them using an appropriate method, such as unit testing.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xHYRhtecm3oVtsS2HYpJDQ.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2407.21787">https://arxiv.org/abs/2407.21787</a></figcaption></figure><p><strong>b. Generate answers by multiple Agents and choose best by Majority voting</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/942/1*HBOxuWVhln6eJG-lmw-RPQ.jpeg" /><figcaption>Source:<a href="https://arxiv.org/abs/2402.05120">https://arxiv.org/abs/2402.05120</a></figcaption></figure><p><strong>c. Multiple Agents generate 3 candidate answers each and a final aggregate prediction is made</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*T-swiAGVvGH9WEUVIoKjlg.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2402.19379">https://arxiv.org/abs/2402.19379</a></figcaption></figure><p><strong>d. Mixture of Agents： </strong>Surpasses GPT4o by using much weaker LLMs in a Multi-layer Generate &amp; Aggregate structure</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NQq-RdEWdXWh47eUwWVuDg.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2406.04692">https://arxiv.org/abs/2406.04692</a></figcaption></figure><p><strong>e. Multi-Agent Debate/Discussion</strong></p><p>Multiple agents debate in a certain way and come to a conclusion or consensus about a task.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V9QruHR3t6vnOUHRz7wtnA.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2308.07201">https://arxiv.org/abs/2308.07201</a></figcaption></figure><h4>3. Weak-Strong Agent Combination Patterns</h4><p><strong>a. A Talker-Reasoner Architecture</strong></p><p>Async System 1 thinking(Talker Agent = Fast) + System 2 thinking(Reasoning Agent = Slow) collaborating via shared memory.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rPhe_PbxXgW8ANosB4PZAA.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2410.08328">https://arxiv.org/abs/2410.08328</a></figcaption></figure><p>Another paper of the same pattern: <a href="https://arxiv.org/abs/2305.17390">https://arxiv.org/abs/2305.17390</a></p><p><strong>b. LLM Cascades via Mixture of Thoughts</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1WG6O9vVUfh7vyE6sYEIQg.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2310.03094">https://arxiv.org/abs/2310.03094</a></figcaption></figure><p>c. A stronger agent fills out the important details in a template answer generated by a Weaker Agent (this is just my idea. It is similar to <a href="https://arxiv.org/abs/2403.03870">https://arxiv.org/abs/2403.03870</a>）</p><h4>4. System of Agents</h4><p><strong>a. Internet of Agents</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jLTWQ9WPVmQm20z4Tl-8Ug.jpeg" /><figcaption>Source: <a href="https://github.com/OpenBMB/IoA">https://github.com/OpenBMB/IoA</a></figcaption></figure><p><strong>b. AIOS</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iAxru5aniT2hWJqPlPzRng.jpeg" /><figcaption>Source:<a href="https://github.com/agiresearch/AIOS">https://github.com/agiresearch/AIOS</a></figcaption></figure><h4>5. Theory of Mind Based</h4><p>Multi-Modal Multi-Agent Theory Of Mind</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uscGK-oztjJNAf-Mm-k9bg.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2408.12574">https://arxiv.org/abs/2408.12574</a></figcaption></figure><h3>Applications of MA Architectures/Patterns in specific domains</h3><p>Below, we see some examples of applying the MA architectures in specific domains such as Legal, Social games, Software Engineering, &amp; Machine Learning.</p><p><strong>a. Enhance Legal Reasoning with Insights from Multi-Agent Collaboration</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IW9WCRzmUSKk3K8NAgwWiw.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2410.02507">https://arxiv.org/abs/2410.02507</a></figcaption></figure><p><strong>b. Multi-LLM Based Agent Debugger via Refinement and Generation Guidance</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UkOvsxJAnHsMYi9MC6tocw.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2410.01242">https://arxiv.org/abs/2410.01242</a></figcaption></figure><p><strong>c. A multi-agent LLM framework for full-pipeline AutoML</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IOYsjYMnNVauJC-GuOvFHw.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2410.02958">https://arxiv.org/abs/2410.02958</a></figcaption></figure><p><strong>d. Weak — Strong Agent Collaboration in Minecraft based on TOM（Theory of Mind）</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OjwH1-vRvNQkfd8yzgJvXQ.jpeg" /><figcaption>Source: <a href="https://arxiv.org/html/2411.12977v1">https://arxiv.org/html/2411.12977v1</a></figcaption></figure><h4>MA Applications/Systems</h4><p>Below are some examples of actual MA applications</p><p><strong>a. Social Simulations</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3WBIwtt0IRIHeUPg87qY6A.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2304.03442">https://arxiv.org/abs/2304.03442</a></figcaption></figure><p><strong>b. AI Lab</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ln23tnmBZ89CqJPdTpQ9qg.jpeg" /><figcaption>Source: <a href="https://github.com/SamuelSchmidgall/AgentLaboratory">https://github.com/SamuelSchmidgall/AgentLaboratory</a></figcaption></figure><p><strong>c. Data Analysis Agent inside LinkedIn</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZxXIQCkwHZYktp-WJlJqjw.jpeg" /><figcaption>Source: <a href="https://www.linkedin.com/blog/engineering/ai/practical-text-to-sql-for-data-analytics">https://www.linkedin.com/blog/engineering/ai/practical-text-to-sql-for-data-analytics</a></figcaption></figure><p><strong>d. Interesting Storytelling because Single Agents don’t produce creative stories</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-NFQNKDuTLsbRtpgtpW4_A.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2410.02603">https://arxiv.org/abs/2410.02603</a></figcaption></figure><h4>MA/SA frameworks</h4><p>Below is a short list of code or no-code frameworks for Multi-Agents and generically for Single Agents. The list is endless, and new frameworks are being released daily, so these are the main ones.</p><ol><li>Openai swarm <a href="https://cookbook.openai.com/examples/orchestrating_agents">https://cookbook.openai.com/examples/orchestrating_agents</a></li><li>Langgraph UI <br><a href="https://blog.langchain.dev/langgraph-studio-the-first-agent-ide/">https://blog.langchain.dev/langgraph-studio-the-first-agent-ide/</a></li><li>Crew AI <br><a href="https://github.com/crewAIInc/crewAI">https://github.com/crewAIInc/crewAI</a></li><li>LlamaIndex <br><a href="https://www.llamaindex.ai/blog/introducing-workflows-beta-a-new-way-to-create-complex-ai-applications-with-llamaindex">https://www.llamaindex.ai/blog/introducing-workflows-beta-a-new-way-to-create-complex-ai-applications-with-llamaindex</a></li><li>HF’s SmolAgents <a href="https://huggingface.co/docs/smolagents/examples/multiagents">https://huggingface.co/docs/smolagents/examples/multiagents</a></li><li><a href="https://microsoft.github.io/autogen/stable/">https://microsoft.github.io/autogen/stable/</a></li><li><a href="https://github.com/microsoft/TinyTroupe">https://github.com/microsoft/TinyTroupe</a></li><li><a href="https://github.com/geekan/MetaGPT">https://github.com/geekan/MetaGPT</a></li><li><a href="https://docs.langflow.org/">https://docs.langflow.org/</a></li><li><a href="https://github.com/awslabs/multi-agent-orchestrator?tab=readme-ov-file">https://github.com/awslabs/multi-agent-orchestrator?tab=readme-ov-file</a></li><li><a href="https://github.com/lmnr-ai/flow">https://github.com/lmnr-ai/flow</a></li><li><a href="https://flowiseai.com/">https://flowiseai.com/</a></li><li><a href="https://www.gumloop.com/">Gumloop</a></li><li><a href="https://github.com/n8n-io/n8n">n8n</a></li><li>Interframework communication<br><a href="https://github.com/langchain-ai/agent-protocol">https://github.com/langchain-ai/agent-protocol</a></li></ol><p>Personally recommend to start with <strong>Langgraph</strong>:</p><p><strong>Pros:</strong></p><ol><li>Low entry barrier. Easy to start. UI to debug.</li><li>Full control and customizable.</li><li>Modular features so use what you need. Simple concepts like Workflow of Nodes + State ⇒ Graph</li></ol><h3>Deep Dive Some Papers</h3><p>Let’s look deeply into some papers to get a feel for how the patterns are used to solve actual problems</p><h4>Long Agent</h4><p><a href="https://arxiv.org/abs/2402.11550">https://arxiv.org/abs/2402.11550</a></p><h4>Patterns we will come across in this paper</h4><ol><li>Task Decomposition &amp; Iterative Querying</li><li>Task-based Agent Role Assignment</li><li>Context distribution</li><li>Conflict Resolution cycles</li><li>Aggregation</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7xpEGrVNJ89ejMV66lgb-Q.jpeg" /></figure><h4>There are 2 types of Agents</h4><ol><li>Leader Agent x 1</li><li>Member Agents x n<br>a. Entire Context(Single/Multiple Documents) is broken into n chunks, and each member is assigned 1 chunk.<br>b. According to the nature of the task, the appropriate role for the member is chosen Ex. QA, Summarizer, Math, Retrieve</li></ol><h4>Let’s run through an Example:</h4><p><strong>Query(Multi-Hop query):</strong> Which team does the player named 2015 Diamond Head Classic’s MVP play for?</p><p><strong>Step 1: Leader Decomposes Complex Queries</strong></p><p>The leader breaks down complex queries into simpler sub-questions or sub-tasks</p><p>Example: The leader divides the main question into two sub-questions:</p><p>“Who won the MVP of the 2015 Diamond Head Classic?”<br>“Which team did that player play for?”</p><p><strong>Step 2: Collaborative Reasoning and Information Gathering</strong></p><p>The leader asks the sub-questions to the member agents</p><p>Each member agent responds based on the content of its assigned text chunk. Some members may not have the answer in their text chunk</p><p>Example:</p><p>Member 1 (chunk 1) answers: “Buddy Hield” ⇒ Correct Answer (When the answer exists in a chunk, it is correctly returned)</p><p>Member 2 (chunk 2) answers: “Mark Gibson” ⇒ Hallucination (But when the answer doesn’t exist, sometimes the LLM, instead of saying It doesn’t know, will lie)</p><p>Member 3 (chunk 3) answers: “No mention in my doc.” (Correctly says don’t know)</p><p><strong>Step 3(Key Step): Conflict Resolution</strong></p><p>If a member responds to conflict, the leader initiates a conflict resolution process</p><p><strong>Members with conflicting responses share their text chunks with each other</strong></p><p>Example: The leader detects that members 1 and 2 gave different MVP names and asks them to share their text chunks. This may lead to one or both members correcting their answers.</p><p><strong>Step 4: Iterative Querying</strong></p><p>The leader can pose additional sub-questions or ask for clarifications if the initial responses are insufficient</p><p>This process continues until the leader believes he has enough information to answer the original query</p><p>Example: The leader may have a second sub-question, “Which team does Buddy Hield play for?” after determining that member 1 is correct.</p><p><strong>Step 5: Answer Deduction</strong></p><p>The leader deduces the final answer based on the information gathered through the collaborative process</p><p>The leader provides the final answer to the user</p><p>Example: <strong>The leader determines that Buddy Hield is the MVP and that he plays for the Sacramento Kings. It then provides the final response: “Buddy Hield won the 2015 Diamond Head Classic MVP and plays for the Sacramento Kings.</strong>”</p><h4>Key Points</h4><p><strong>Contextual Understanding:</strong> Each member has a limited view of the text, but by working together, they can extract information from it. The system is designed to accommodate the fact that some members will not have the answers to some questions</p><p><strong>Collaboration:</strong> The leader and member agents work together, with the leader coordinating the process and the members providing the information from their respective chunks of text</p><p><strong>Iterative Process:</strong> The system is iterative; it may ask members multiple questions to clarify and refine answers, and members may exchange chunks to resolve differences. This is not a one-shot-question-answer approach</p><h3>Evaluation</h3><p><strong>Key Evaluation Metric:</strong> <strong>Needle In Haystack Plus</strong> It is an improvement over the traditional Needle In Haystack</p><p><a href="https://github.com/gkamradt/LLMTest_NeedleInAHaystack"><strong>Needle In Haystack</strong></a></p><blockquote><em>In this setup, a fact or statement of interest (the needle) is placed within a lengthy distracting document (the haystack), and the model is tasked with retrieving this hidden key information. Performance is evaluated by varying the position of the needle within the distracting document and the length of the distracting document itself.</em></blockquote><p><a href="https://github.com/zuucan/NeedleInAHaystack-PLUS"><strong>Needle In Haystack Plus</strong></a></p><blockquote><em>To assess the longtext capabilities more comprehensively, we propose Needle-in-a-Haystack PLUS, which shifts the focus from simple fact retrieval to more challenging single-document/multi-document question answering tasks. In Needle-in-a-Haystack PLUS, the needle represents the document(s) containing the answers, while the haystack comprises distracting documents. The model must locate one or more relevant documents scattered within the haystack and reason the correct answers from them.</em></blockquote><p><strong>Evaluation Dataset:</strong></p><blockquote><em>For multi-document QA, questions require information from two or more documents to reason the final answer. We construct test samples based on </em><strong><em>HotpotQA</em></strong><em>, a widely adopted multi-document QA dataset. We select 60 questions from the validation set of HotpotQA that require information from two documents to answer. Each sample contains two needles, and the haystack is still composed of distractor documents. The length of haystack ranges from 1, 000 to 128, 000 tokens with equal intervals, totaling 15 different lengths. The two needles are randomly scattered at the depth of 0%, 33%, 66%, and 100% of the haystack, resulting in 6 combinations: (0%, 33%), (0%, 66%), (0%, 100%), (33%, 66%), (33%, 100%), and (66%, 100%). For specific lengths and needle positions, we randomly select 10 needles to construct 10 test samples for evaluation.</em></blockquote><h4><strong>Evaluation Results:</strong></h4><p>More Red is worse. We can see the loss in the middle phenomenon in GPT4. Using 3.5-turbo for both leader and members gives scores 1.5% better than GPT4.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZVuIPSND0avj4WXAGix1FQ.jpeg" /></figure><h4>Conclusions</h4><p>💡By assigning separate chunks of context to each member, we effectively enhance the system&#39;s attention, surpassing that of GPT4. But that introduces hallucinations from members who don’t have relevant context chunks. This is fixed by orchestrating conflict resolution by sharing context among conflicting members (Authors suggest Debate or Reflexion as alternate conflict resolution strategies)</p><p>The responsibility of the leader to decompose the query is critical, so it is important to design this agent well. Complex tasks can be accomplished.</p><h3>Multi-Agent Debate(LLM as a Jury)</h3><h4>Introduction of ChatEval</h4><p>Introduces a novel framework that enhances text evaluation by employing multiple Large Language Models (LLMs) to debate and assess generated responses.</p><p>Code and Demo: <a href="https://github.com/thunlp/ChatEval?tab=readme-ov-file#-simple-video-demo">https://github.com/thunlp/ChatEval?tab=readme-ov-file#-simple-video-demo</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*he5s8_raBaczVLf7aE1kMg.jpeg" /><figcaption>Source: <a href="https://arxiv.org/abs/2308.07201">https://arxiv.org/abs/2308.07201</a></figcaption></figure><h4>Key Components of ChatEval</h4><ol><li><strong>Debater Agents</strong>: Each LLM functions as an individual agent with a unique role or persona, ensuring diverse perspectives during evaluation.</li><li><strong>Communication Strategies:</strong> The framework employs various strategies to facilitate interaction among agents, such as:</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3-glq2edIIOmRad9xkJF5g.jpeg" /></figure><ul><li><strong>One-By-One:</strong> Agents take turns generating responses in a set order. ⇒ Speaking Order Bias</li><li><strong>Simultaneous-Talk:</strong> Agents generate responses asynchronously within each iteration.</li><li><strong>Simultaneous-Talk-with-Summarizer:</strong> An additional LLM summarizes the discussion at the end of each iteration to inform subsequent responses.</li></ul><p><strong>3. Diverse Role Specification</strong>: Assigning distinct personas to each agent ensures evaluations from multiple perspectives, enhancing the assessment’s depth and reliability.</p><h4>Example Scenario</h4><p><em>Objective:</em> Evaluate two AI-generated responses to the question: “How can I improve my time management skills?”</p><p><em>Responses:</em></p><ul><li><strong>Assistant 1:</strong> “Improving your time management skills involves setting clear goals, prioritizing tasks, and minimizing distractions.”</li><li><strong>Assistant 2:</strong> “Here are some tips to improve your time management: create a schedule, delegate tasks when possible, and take regular breaks to maintain productivity.”</li></ul><p><em>Evaluation Process:</em></p><ol><li><strong>Initialization:</strong> Three LLM agents are assigned distinct roles:</li></ol><ul><li><strong>General Public:</strong> Focuses on the response’s relevance to a general audience.</li><li><strong>Critic:</strong> Assesses the clarity and fluency of the responses.</li><li><strong>News Author:</strong> Evaluate the consistency and informativeness of the responses.</li></ul><p><strong>2. Debate Rounds:</strong></p><p><strong>Round 1:</strong></p><ul><li><strong>General Public:</strong> Analyzes both responses for general applicability and provides feedback.</li><li><strong>Critic:</strong> Reviews the language quality and points out any ambiguities.</li><li><strong>News Author:</strong> Checks for factual accuracy and comprehensiveness.</li></ul><p><strong>Round 2:</strong></p><ul><li>Agents discuss each other’s evaluations, highlighting strengths and weaknesses in the responses.</li></ul><p><strong>Round 3:</strong></p><ul><li>A consensus is reached, with each agent assigning a score to both responses based on the discussion.</li></ul><p><strong>3. Final Assessment:</strong> The scores are aggregated to determine which response better addresses the question, considering all evaluated aspects.</p><h4>Advantages of ChatEval</h4><ul><li><strong>Enhanced Accuracy:</strong> By simulating a human-like evaluation process through multi-agent debates, ChatEval aligns more closely with human assessments.</li><li><strong>Diverse Perspectives:</strong> Incorporating agents with varied roles ensures a comprehensive evaluation from multiple angles.</li><li><strong>Scalability:</strong> Automating the evaluation process reduces the need for extensive human labor, making it more efficient.</li></ul><p>💡But the Simultaneous talk style debate in ChatEval leads to n*n communication problem! Each Agent’s prompt complexity and cost scales with the total number of debaters!</p><h4>Improved Communication Strategy with lower cost!</h4><p><strong>Sparse Communication Topology</strong>: <br>Source: <a href="https://arxiv.org/abs/2406.11776">https://arxiv.org/abs/2406.11776</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/762/1*zxlUYlcPvEL2g9QlByVDOw.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/762/1*bMnuYOCA1yqsLncgAHMGkQ.jpeg" /></figure><p>We can infer from the above evaluation results that only communicating with your neighbors gives the same or better results over the fully connected MAD topology!</p><h4>An example scenario in Sparse MAD</h4><p>Source: <a href="https://microsoft.github.io/autogen/dev//user-guide/core-user-guide/design-patterns/multi-agent-debate.html">https://microsoft.github.io/autogen/dev//user-guide/core-user-guide/design-patterns/multi-agent-debate.html</a></p><p>There are two types of agents in this pattern: solver agents and an aggregator agent. The solver agents are connected in a sparse manner following the technique described in <a href="https://arxiv.org/abs/2406.11776">Improving Multi-Agent Debate with Sparse Communication Topology</a>. The solver agents are responsible for solving math problems and exchanging responses with each other. The aggregator agent is responsible for distributing math problems to the solver agents, waiting for their final responses, and aggregating the responses to get the final answer.</p><p>The pattern works as follows:</p><ol><li>The user sends a math problem to the aggregator agent.</li><li>The aggregator agent distributes the problem to the solver agents.</li><li>Each solver agent processes the problem and publishes a response to its <strong>neighbors</strong>.</li><li>Each solver agent uses the <strong>responses from its neighbors to refine its response and publishes a new response.</strong></li><li>Repeat step 4 for a fixed number of rounds. In the final round, each solver agent publishes a final response.</li><li><strong>The aggregator agent uses majority voting to aggregate the final responses from all solver agents to get a final answer and publishes the answer.</strong></li></ol><h4>More references on MAD</h4><ol><li>Financial Sentiment Analysis:</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8b4UgXbBW7QcJrk1jiiAHw.jpeg" /><figcaption>Source: <a href="https://arxiv.org/html/2401.05799v1">https://arxiv.org/html/2401.05799v1</a></figcaption></figure><p>2. One of the latest papers on MAD<br><a href="https://arxiv.org/abs/2305.19118">https://arxiv.org/abs/2305.19118</a></p><h3>What’s Next?</h3><p>This is part 1 of the 3-part series, and in part 2, I will go through some of the internal experiments we have done involving some of these multi-agent architecture papers!</p><p>More Articles:</p><p><a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a></p><p>About Company:</p><p><a href="https://gaudiy.com/en/">Gaudiy Inc. | Moving forward in a new era with fans</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1b1778345ad9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gaudiy-ai-lab/1b1778345ad9">Considerations on Multi Agents(Part1)- A Comprehensive Survey</a> was originally published in <a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[LLM-as-a-Judge: A Technique for Evaluation Using LLMs]]></title>
            <link>https://medium.com/gaudiy-ai-lab/032a47b0b017?source=rss----096fc41648f8---4</link>
            <guid isPermaLink="false">https://medium.com/p/032a47b0b017</guid>
            <category><![CDATA[llm-as-a-judge]]></category>
            <category><![CDATA[llmops]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[gaudiy]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Gaudiy Lab]]></dc:creator>
            <pubDate>Tue, 04 Mar 2025 12:52:19 GMT</pubDate>
            <atom:updated>2024-10-22T01:58:06.980Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v8M2_a3cNBEhX3hLW4ysuw.jpeg" /></figure><p><em>Posted by </em><a href="https://x.com/sekikazu01"><em>seya</em></a><em>, LLM App Engineer of </em><a href="http://redirect.medium.systems/r-03wSyM2SI4"><em>Gaudiy Inc</em></a><em>.</em></p><p>As the name suggests, LLM-as-a-Judge is a technique that uses LLMs for various types of evaluations. LLMs are excellent at interpreting unstructured data, allowing them to make judgments or assign scores to tasks that are typically difficult to assess with conventional programming.</p><p>In this article, I’ll introduce my approach and tips for creating an LLM-as-a-Judge system to confirm improvements and detect regressions in LLM-based systems.</p><h3>Why Use LLMs for Judging?</h3><p>First, let’s clarify the purpose of why we want to evaluate using LLMs.</p><p>The primary goal is to detect whether an LLM system has improved or regressed. LLM-based features can change due to various factors:</p><ul><li>Modifying prompts</li><li>Changing input methods</li><li>Adjusting LLM API request parameters</li><li>Switching models</li><li>Data changes</li></ul><p>With so many variables, it becomes time-consuming to manually check for improvements or regressions each time we make changes. Therefore, we want an automated system that can inspect and judge, similar to writing unit tests in programming.</p><p>This is the overarching goal we want to achieve.</p><p>*Note: While LLM-as-a-Judge can be used as a feature within a product, this article focuses solely on using it for internal quality improvement.</p><p>While LLMs are not always the best tool for automated evaluation, for certain evaluation aspects that are difficult or impossible to judge with conventional programming, we want to use LLM-as-a-Judge.</p><p>When incorporating LLMs into products, the criteria for evaluating their behavior tend to be much fuzzier compared to testing in traditional system development.</p><p>For example, in our company, when developing an AI that mimics a specific person, we have evaluation criterias such as:</p><ul><li>Showing empathy, encouragement, or a listening attitude</li><li>Responding in casual language, not using formal speech</li></ul><p>These are aspects that are challenging to evaluate with pure programming. This is where LLMs come into play.</p><p>However, as mentioned earlier, “LLMs are not always the best tool.” If an evaluation aspect can be judged with conventional programming, it’s generally better to use that approach. This is because LLM-as-a-Judge is slow and expensive. 💸 (Although recent models like GPT-4o mini and Gemini Flash are becoming less so)</p><p>For instance, if you wanted to confirm whether the response is in the same language as the user’s query (English for English, Japanese for Japanese), you could use LLMs to judge this, but there are also dedicated language detection libraries available.</p><p><a href="https://github.com/LlmKira/fast-langdetect">GitHub - LlmKira/fast-langdetect: ⚡️ 80x faster language detection with Fasttext | Split text by language for TTS</a></p><p>So for such evaluation aspects, we use functions like this to generate scores:</p><pre>from fast_langdetect import detect<br>from langsmith import EvaluationResult<br>from langsmith.schemas import Example, Run<br>def answer_with_language_same_as_query(run: Run, example: Example) -&gt; EvaluationResult:<br> &quot;&quot;&quot;Check if the agent reply is in query language.&quot;&quot;&quot;<br> query = example.inputs[&quot;query&quot;].replace(&quot;\n&quot;, &quot;&quot;)<br> query_language = detect(query)<br>agent_reply = get_output(run, &quot;output&quot;).replace(&quot;\n&quot;, &quot;&quot;)<br> agent_reply_language = detect(agent_reply)<br>score = query_language.get(&quot;lang&quot;) == agent_reply_language.get(&quot;lang&quot;)<br> return EvaluationResult(key=&quot;ANSWER_WITH_SAME_LANGUAGE_AS_QUERY&quot;, score=score)</pre><p>There are also evaluation aspects that are not suitable for either LLMs or programming. Clear examples include subjective elements like “conversation interestingness” or specialized areas requiring practical experience, such as medical or legal consultations. If you try to evaluate these aspects, LLMs might provide plausible reasons and scores, but the correlation with real human evaluations is likely to be relatively low.</p><p>I’ll explain “how to create LLM-as-a-Judge” next, but it takes more than just a quick effort to create this. Therefore, when actually creating it, it’s important to question, “Is LLM really the optimal choice for this evaluation?”</p><p>Now that we’ve aligned on the purpose, let’s look at how to create it!</p><h3>How to Create LLM-as-a-Judge: Start with Annotation</h3><p>When you actually start creating LLM-as-a-Judge, you might be tempted to first consider the evaluation criteria… but I believe it’s better to start by diligently creating data and performing annotations.</p><p>This is because you’ll need datasets for later evaluation anyway, and evaluation criteria often only become clear after you’ve actually done some evaluations. Of course, you should have a rough verbal description based on the functional requirements and hypotheses you have, but at this stage, I think it’s important not to decide too rigidly and to actually try evaluating.</p><p>The overall picture, including LLM-as-a-Judge, will eventually look something like this (of course, we won’t go as far as evaluating the evaluation of the evaluation, as that would be excessive):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M-UUlLNjzpdkfUUvoL7Dkw.png" /></figure><p>So, we start by creating a dataset to run the LLM system we want to evaluate, actually running it, and then annotating the results.</p><p>Our company uses LangSmith as a platform for evaluation and experiment management, so we ultimately input the data there and use the SDK’s evaluation functions. This is convenient for seeing changes over time when scoring evaluations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YhWmyKpvRTxFKSRZi_Ctcg.png" /></figure><p>By the way, we’ve released a library as OSS to perform evaluations in LangSmith, so feel free to use it if you’d like.</p><p><a href="https://github.com/gaudiy/langsmith-evaluation-helper">GitHub - gaudiy/langsmith-evaluation-helper: Helper library for LangSmith that provides an interface to run evaluations by simply writing config files.</a></p><p>As you continue annotating, you’ll likely find yourself able to verbalize evaluation aspects, and these evaluations can later be used to evaluate the evaluation itself.</p><h4>Request Annotations from Domain Experts</h4><p>After talking about just diving into evaluation, I don’t mean to dampen the enthusiasm, but it’s important to ask people who can evaluate more accurately — domain experts for the product you’re trying to build. (For the very first PoC, it’s fine if someone from the development team does it for speed, but…)</p><p>As a personal failure story, I recently started by annotating the responses of an in-house agent myself. However, my evaluations were quite lenient, and when I later compared annotations with internal people (domain experts) who had the domain knowledge that the agent should assume, many of the items I had judged as OK were determined to be incorrect.</p><p>If you looked closely at the LangSmith reference screenshot I just posted and thought, “The score has gone down!”, it’s because I’ve adjusted to a stricter evaluation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YhWmyKpvRTxFKSRZi_Ctcg.png" /></figure><p>If I had created an LLM-as-a-Judge based on my annotations, the scores might have been unfairly high, unable to detect problems and thus becoming useless, or worse, giving an inaccurate sense of security that could have been harmful.</p><p>These evaluation criteria and the resulting scoring are what I consider to be guidelines for how to nurture any LLM product. So I think it’s an unexpectedly important decision.</p><p>If you’re only creating something that’s “just for reference,” it won’t lead to the experience of being able to confidently make modifications with reduced effort. So it’s important to find people who can do high-quality annotations, collaborate with them, and improve over time.</p><h4>Don’t Over-Engineer Evaluation Criteria Too Early</h4><p>As I mentioned when discussing the sense of purpose at the beginning, “similar to writing unit tests in programming” is important. The evaluation we’re doing with LLM-as-a-Judge is ultimately to confirm whether the LLM system is working as expected or better within the specified parameters.</p><p>In other words, even if the accuracy of the evaluation itself is extremely high and you can achieve a high score for the LLM system accordingly, it might still be poorly received by users or contribute nothing to the growth of the product.</p><p>In such situations, you might realize that the feature is no longer necessary, or that a different evaluation aspect is important, potentially rendering the data you’ve been diligently creating internally worthless. So investing too much in this automated evaluation system by deciding on evaluation criteria too early could be a waste of time.</p><p>Therefore, I think it’s better to start with quality standards that won’t change and must be maintained, and then gradually tune the evaluation items and criteria based on user tests and actual usage after release.</p><p>To bring in something I wrote in a different article a while ago, it’s easier to imagine if you think of it as climbing up the categories defined in the Kano model.</p><ul><li>Start with basic quality (the kind of quality protected by guardrails)</li><li>Then one-dimensional quality</li><li>Finally, attractive quality</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*dkmHHHkBmgu7ACXK" /></figure><p>source: <a href="https://operationsinsider.com/the-language-of-lean/kano-model">KANO model</a></p><p>Personally, while I believe that <strong>evaluation</strong> is <strong>most important</strong> in creating LLM products, I think it’s crucial not to become too dogmatic and to gauge the level of investment according to the situation.</p><h3>Evaluating the Evaluation</h3><p>Finally, we create an evaluation dataset for the evaluation prompts and check their accuracy, so I’ll introduce that process.</p><h4>Create Evaluation Prompts</h4><p>First, we need evaluation prompts for the target we’re evaluating. Let’s create them.<br>We’ll do proper tuning after creating the evaluation dataset, so for now, it’s fine as long as it doesn’t look too strange when checked visually.</p><p>As an example, here’s a simple one I’ve written:</p><p>Key points:</p><ul><li>Have it return reasons — This makes it easier to judge when evaluating the evaluation later</li><li>Temperature is 0 — If the evaluation changes between different experiments, it becomes difficult to track what caused the score to go up or down</li></ul><pre>class EvalResult(BaseModel): # noqa: D101<br> score: str = Field(description=&quot;score (0 or 1)&quot;)<br> score_reason_jp: str = Field(description=&quot;reason for the score in japanese&quot;)<br> score_reason_en: str = Field(description=&quot;reason for the score in english&quot;)<br> <br>def judge_correctness_with_llm(query: str, agent_reply: str, ground_truth_agent_reply: str) -&gt; EvalResult: # noqa: D103<br> llm_as_judge = ChatOpenAI(model=&quot;gpt-4o-mini&quot;, temperature=0).with_structured_output(EvalResult, method=&quot;json_mode&quot;)<br> llm_judge_result: EvalResult = llm_as_judge.invoke(<br> f&quot;&quot;&quot;Your task is to give a score(0 or 1) for the below Agent reply based on whether the given agent reply has same information as ground truth agent reply.<br>Query:<br>{query}<br>Ground truth Agent reply:<br>{ground_truth_agent_reply}<br>Agent reply:<br>{agent_reply}<br># Criteria<br>- If the information in the agent reply has nuance that&#39;s opposite to ground truth agent reply, give score 0, else give score 1.<br>- If the agent reply is missing some information from the ground truth agent reply, but the rest of the information is correct, give score 1.<br>- not all the information have to match exactly.<br># Output format<br>You must respond in json format with the following keys:<br>- score_reason_jp: reason for the score in japanese<br>- score_reason_en: reason for the score in english<br>- score: score (0 or 1)&quot;&quot;&quot;<br>return llm_judge_result</pre><p>By the way, there are decision points in “how to assign scores,” such as whether to output 0 or 1, or to use a score with more gradation.</p><p>Personally, I prefer 0 or 1 because it’s easier to understand and judge, but this too depends on the “evaluation aspect.”<br>For example, in a recent case, I wanted to determine “how much of the content that should be included in the Ground Truth answer is present in the agent’s response.” A 0 or 1 evaluation would have been either too strict or too lenient for this evaluation aspect, so I changed to a method with gradation by generating a list of “items that should be included” and determining what percentage of them were present.</p><p>In this way, how to evaluate and assign scores varies depending on the evaluation aspect.</p><p>Also, besides assigning scores, there are various methods like pairwise comparison (preparing two answers and choosing which is better) or taking the average of scores from multiple models.</p><h4>Create an Evaluation Dataset for the Evaluation</h4><p>Next, for creating the evaluation dataset, the annotations we’ve been doing come in handy.</p><p>In my recent workflow, I first run the system to be evaluated in LangSmith, then pick out items that disagree with human evaluation and compile them into a separate dataset.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-M7h8LKvzwui_nSwtl3dCw.png" /></figure><p>This kind of data is optimal for checking false negatives/false positives.<br>When tuning LLM-as-a-Judge, the goal is basically to align with human judgment (annotation).</p><p>What’s useful when aligning is the items that differ from human judgment, and the main focus during tuning is to eliminate “items judged as correct when they’re actually incorrect — false positives” and “items judged as incorrect when they’re actually correct — false negatives.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Xf8r1RyjnQgPtNVCOnitYA.png" /></figure><p>Of course, items that already match in judgment are also useful as data for regression checks.</p><p>By the way, from my experience so far, I tend to focus more on false positives, i.e., detecting cases that are “not good but given a high score.”<br>While having something good judged as bad increases wasted checking time, it’s not a big problem if the number of cases isn’t too high.</p><p>On the other hand, overlooking problems due to false positives has a greater impact on quality degradation, so prioritizing the expansion of datasets to check for these becomes more important.</p><p>Keeping these aspects in mind, we accumulate data. There’s no specific correct amount, but I think about 30–50 cases are sufficient for initial verification.</p><h4>Expanding the Evaluation Dataset Using Synthetic Data</h4><p>Using synthetic data, that is, having LLMs expand the test dataset, is also effective in increasing confidence.</p><p>Above all, the previous method limits the range of inputs to the initially created dataset. Therefore, it’s useful to ensure a range of tests with inputs that are similar in aspect but different in content.</p><p>However, it’s important not to add too many overly simple examples. If you prepare test data that’s the exact opposite of the ideal answer to test for false positives, it might result in data that’s too obvious like “Of course it would judge this as incorrect!” Such data doesn’t make for a good test. In fact, if too much of this data accumulates, it might give a false sense of security like “The accuracy rate is over 90%, so we’re safe.”</p><p>Of course, it’s effective to gradually test difficulties that you want to overcome, like “Surely it should handle this much…” In such cases, it’s good to add tags for difficulty levels so they can be executed individually.</p><p>As a reference, in a recent example of creating synthetic data, I asked GPT-4o to create slightly incorrect data like this. It can create good examples that aren’t obviously wrong at first glance but are incorrect upon careful reading.</p><pre>def generate_slightly_wrong_output(reference_answer: str) -&gt; str:<br> &quot;&quot;&quot;Generate a slightly wrong output.&quot;&quot;&quot;<br>llm = ChatOpenAI(model=&quot;gpt-4o&quot;, temperature=0)<br> output = llm.invoke(<br> f&quot;&quot;&quot;Your task is to generate a wrong answer text based on the reference answer.<br>this output is going to be used to test evaluator&#39;s ability to judge the correctness of the information.<br>reference answer:<br>{reference_answer}<br>background information of the agent reply:<br>{document}<br>&quot;&quot;&quot;<br> )<br> return str(output.content)</pre><h4>Write an Evaluation Program for the Evaluation</h4><p>I’m simply checking if it matches the reference:</p><pre>from langsmith import EvaluationResult<br>from langsmith.schemas import Example, Run<br>def check_actual_score_matches_reference_score(run: Run, example: Example) -&gt; EvaluationResult:<br> &quot;&quot;&quot;Evaluate if the output score of the run matches the reference score.&quot;&quot;&quot;<br> actual_score = run.outputs.get(&quot;output&quot;) if run.outputs else None<br> reference_score = example.outputs.get(&quot;output&quot;) if example.outputs else None<br>if actual_score is None or reference_score is None:<br> raise ValueError(&quot;actual_score or reference_score is None&quot;)<br>score = int(actual_score) == int(reference_score)<br>return EvaluationResult(key=&quot;matches_reference&quot;, score=score)</pre><h4>Tuning!</h4><p>Once all the preparation is done, all that’s left is to keep tweaking the LLM system until the score improves.</p><p>From my experience, recent LLMs are cheap and smart, so this tuning itself doesn’t seem to take much time. Rather, the key point that takes time is “how to effectively verbalize the evaluation criteria.” So it might feel more like criteria tuning than prompt tuning.</p><h3>Conclusion</h3><p>That’s how to create LLM-as-a-Judge.</p><p>It takes a bit of effort, but when it successfully detects regressions, you feel good thinking, “Oh… it’s smart…”</p><p>In practice, LLM systems often improve in one aspect while deteriorating in another as you tweak them, so this approach often comes in handy.</p><p>Let’s make good use of it!</p><h3>References</h3><ul><li><a href="https://eugeneyan.com/writing/llm-evaluators/">Evaluating the Effectiveness of LLM-Evaluators (aka LLM-as-Judge)</a></li><li><a href="https://arxiv.org/abs/2404.12272">Who Validates the Validators? Aligning LLM-Assisted Evaluation of LLM Outputs with Human Preferences</a></li></ul><p>More Articles:</p><p><a href="https://medium.com/gaudiy-web3-and-ai-lab">Gaudiy AI Lab</a></p><p>About Company:</p><p><a href="https://gaudiy.com/en/">Gaudiy Inc. | Moving forward in a new era with fans</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=032a47b0b017" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gaudiy-ai-lab/032a47b0b017">LLM-as-a-Judge: A Technique for Evaluation Using LLMs</a> was originally published in <a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Context Matters: Using Chat History to Retrieve More Relevant Memories for AI Agent]]></title>
            <link>https://medium.com/gaudiy-ai-lab/b628896107aa?source=rss----096fc41648f8---4</link>
            <guid isPermaLink="false">https://medium.com/p/b628896107aa</guid>
            <category><![CDATA[gaudiy]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[langchain]]></category>
            <category><![CDATA[chatgpt]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Gaudiy Lab]]></dc:creator>
            <pubDate>Tue, 04 Mar 2025 12:51:40 GMT</pubDate>
            <atom:updated>2024-08-29T06:37:28.717Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4AtBq9ZxTNeJn-h840e3Rg.png" /></figure><p><em>Posted by </em><a href="https://x.com/ShreyasGenAI"><em>Shreyas Shinde</em></a><em>, AI Engineer of </em><a href="https://gaudiy.com/en/"><em>Gaudiy Inc</em></a><em>.</em></p><h3>Introduction</h3><p>In the rapidly evolving field of artificial intelligence, ensuring that AI agents can provide relevant and accurate responses is crucial. One of the key challenges in AI conversations is handling inappropriate or ambiguous user queries. This blog explores the importance of query contextualization using chat history to improve agents’ memory retrieval and overall conversation quality. It also goes deep into how to systematically tune a prompt to achieve query contextualization.</p><p>At Gaudiy, we provide <strong>“</strong><a href="https://site.gaudiy.com/en/product/fanlink"><strong>Gaudiy Fanlink</strong></a><strong>,”</strong> a community platform for multiple IPs (intellectual properties). It has a Beta feature that allows users to chat with AI avatars of specific characters (such as actual celebrities or anime characters) in collaboration with specific IP holders. We use vector cosine similarity search to retrieve the Character AI’s memories (base information about the character and summaries of chat history with a user) relevant to the user’s message query from a Vector Store (using Vertex AI) and then add them to the LLM’s prompt to initiate a reply generation process. As the user’s message doesn’t always contain enough context to retrieve relevant memories it becomes essential to first contextualize the user’s message using recent chat history and then use it as a query.</p><h3>Problem Example</h3><p>Imagine a conversation between a User and Agent below:</p><pre>User: Have you read any new books recently?<br>Agent: Yes, I recently read ‘Harry Potter’.<br>User: That&#39;s great. Which volume are you reading?<br>Agent: I&#39;ve started reading the first volume. It&#39;s very interesting.<br>User: Nice!</pre><p>When we directly use the user’s last message Nice! as a query for the vector db we would get results which would not be related to what that particular Nice! was about. It would miss out on getting information about key topics and nuance in the current conversation i.e Harry Potter, books, first volume &amp; interesting . If our Agent has memories which are relevant to these topics within the context of Nice! , it would miss out on the chance to utilize them to have a much richer &amp; personalized conversation.</p><h3>Literature Survey</h3><p>We investigated 3 different possible approaches:</p><h4>1.</h4><p>Prompt an LLM to rewrite the user’s message based on the chat history such that it can be understood standalone. This has been documented in <a href="https://python.langchain.com/v0.2/docs/how_to/qa_chat_history_how_to/#tying-it-together">LangChain’s docs</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0cs77xx0cdxkgE9z46P9UA.png" /></figure><p>We quickly found out that although the simple prompt provided by LangChain performs great when used with a powerful but expensive LLM like gpt4o, there were several issues when used with a cost effective LLM like gpt3.5, gemini-1.0, gemini-1.5-flash, claude-sonnet-3 depending upon the model chosen.</p><p>Some Issues faced:</p><ol><li>Replies to the user’s last message instead of creating a contextualized message</li><li>Only partially contextualizes the message</li><li>Adds hallucination in the contextualized message</li></ol><p>The choice for this LLM needed to be a cost effective one for it be used in our product. We found gemini-1.5-flash to be the most promising among the cheaper models.</p><h4>2.</h4><p>To counter the issue of incomplete contextualization mentioned in 1, instead of only 1 generate 3 standalone queries(statement or question) from user message and chat history and retrieve per query.</p><p>But this approach has the additional overhead of post-retrieval reranking and filtering the set of memories retrieved by 3 queries. We could use an approach like <a href="https://github.com/langchain-ai/rag-from-scratch/blob/main/rag_from_scratch_5_to_9.ipynb">reciprocal rank fusion</a> or llm filter to do so. We would also need some prompt engineering to improve the relevance of the 3 queries and to avoid adding hallucinations.</p><h4>3.</h4><p><a href="https://python.langchain.com/v0.2/docs/templates/hyde/">HypotheticalDocumentEmbedding</a>: This approach first generates an Agent reply without using any memories and then uses the reply to retrieve memories and then regenerate the reply. As our reply generation process is multi-step this approach leads to doubling our cost so it is not very cost effective.</p><h4>💡 <strong>Decision</strong>:</h4><p>We chose to go ahead with the first approach above as it was judged that by prompt tuning we would be able to recognize and resolve issues and use it in our product cost effectively.</p><h3>Prompt Tuning for Query Contextualization</h3><p>In order to improve the simple prompt from LangChain in a structured way we took below steps:</p><ol><li>Generate a synthetic dataset of various types of conversations between a user and agent for evaluating the prompt against using chatgpt4. Add ground truth contextualized queries per example. These are the so called ideal queries which would lead to the retrieval of most relevant memories Upload the dataset to LangSmith.</li><li>Write an evaluator function using LangSmith which calculates the cosine similarity distance between the embeddings of the generated contextualized query and the ground truth contextualized query. Shorter distance would mean the prompt is producing a contextualized query for an example that is similar to the ground truth and leads to similar relevant memories retrieval as would with the ground truth query. We used the <a href="https://github.com/gaudiy/langsmith-evaluation-helper">langsmith helper library</a> we recently released as OSS to help us with this evaluation.</li><li>Execute the query contextualiser llm per example and evaluate the generated contextualized query. Calculate average score across categories of examples. Identify troublesome examples and update the prompt in an attempt to resolve them. Rerun the evaluation to confirm if they are resolved. This cycle of evaluation not only gave us a direction for improving the prompt on specific category of examples but as we were also computing an average evaluation score during each iteration it also made sure that the prompt was also performing well on the overall dataset.</li><li>Run it against actual production queries. Add few shot examples and set the LLM’s temperature to 0 for consistent outputs as it reduces the LLM’s creativity.</li></ol><h4>1. Generating Synthetic Dataset using chatgpt4</h4><p>By chatting with chatgpt4 we created a high quality synthetic dataset of 75 conversations by varying the types of chat_history and user_last_message. It is important to consider the example types deeply for better coverage of actual scenarios.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/988/1*YJJXVcp4k4vvb3YBr4-ckA.png" /></figure><p>We also generated ground truth contextualized queries for evaluation.</p><p>Example:</p><pre>&quot;chat_history&quot;: &quot;L: The SPK has valuable intel.<br>        Shreyas: Should we contact them?<br>        L: Yes, but cautiously.<br>        Shreyas: What&#39;s the best approach?<br>        L: A secure line.&quot;,<br>&quot;user_last_message&quot;: &quot;Should I set up the call?&quot;,<br>&quot;ground_truth_standalone_message&quot;: &quot;Should I set up a secure call to contact the SPK for their valuable intel?&quot;,<br>&quot;example_type&quot;: &quot;Multiple turns, question, context from multiple messages&quot;</pre><p>Uploaded to LangSmith:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NhHImyjVv49vWWIb3Q4w-Q.png" /></figure><p><strong><em>What was the reason for Human in the loop style of synthetic dataset generation using chatgpt4?</em></strong></p><p>💡 There were clear merits of doing this task in a chat way rather than one prompt and script. Chat way gave more control over the dataset being generated (I first asked it to generate 10 examples with the given prompt and then verify what mistakes it was doing). As we can see how it is failing to follow the instructions we can immediately ask it to correct it, verify again and then produce more examples. We decide the conditions of how we want to vary the examples (in my case I wanted example types like user_last_message is statement or question, chat_history has single message or multiple messages etc). It also allows us to flexibly make changes to the schema mid-way without having to be perfect about it in the first go in case it is needed to change it. As it has a long context window all the history is being used to keep following all instructions in previous messages. Chatgpt easily outputs files in csv, json formats. CSV file can be directly uploaded from LangSmith’s UI to create a new dataset! It also helped me finally translate the file into Japanese because first I had to verify the data in English. This time we only needed to create 75 examples. In case we need 1000 examples the idea will be that after performing this chat and we can satisfactorily and consistently produce examples we ask it to summarize the conversation and create a prompt. And then use that prompt by script and api to generate 1000 examples.</p><h4>2. Evaluator function using LangSmith</h4><p>We used the LangSmith’s off the shelf <a href="https://docs.smith.langchain.com/how_to_guides/evaluation/use_langchain_off_the_shelf_evaluators#use-string-or-embedding-distance-metrics">embedding_distance evaluator</a></p><pre>from typing import Any<br><br>from langchain_google_vertexai import VertexAIEmbeddings<br>from langsmith.evaluation import LangChainStringEvaluator<br>from langsmith.evaluation.integrations import _langchain<br>from langsmith.schemas import Example, Run<br><br>from gaudiy_python_kit.ai_testing.tests.utils import get_child_run, get_output<br><br><br>def prepare_data_for_similarity_eval_single_contextualized_query_and_ground_truth(  # noqa: D103<br>    run: Run, example: Example | None<br>) -&gt; _langchain.SingleEvaluatorInput:<br>    prompt_run = get_child_run(run, &quot;prompt&quot;)<br>    relevant_memories_run = get_child_run(prompt_run, &quot;get_relevant_memories&quot;)<br>    pre_retrieval_query_translation_run = get_child_run(relevant_memories_run, &quot;_pre_retrieval_query_translation&quot;)<br>    contextualize_query_run = get_child_run(pre_retrieval_query_translation_run, &quot;contextualize_query&quot;)<br>    return {<br>        &quot;prediction&quot;: get_output(contextualize_query_run, &quot;output&quot;)[0],<br>        &quot;reference&quot;: get_output(example, &quot;ground_truth_standalone_message&quot;),<br>        &quot;input&quot;: &quot;&quot;,  # added because mypy complains about missing key<br>    }<br><br><br>evaluators = [<br>    LangChainStringEvaluator(<br>        &quot;embedding_distance&quot;,<br>        config={<br>            &quot;embeddings&quot;: VertexAIEmbeddings(model_name=&quot;textembedding-gecko-multilingual@latest&quot;),<br>            &quot;distance_metric&quot;: &quot;cosine&quot;,<br>        },<br>        prepare_data=prepare_data_for_similarity_eval_single_contextualized_query_and_ground_truth,<br>    ),<br>]<br>summary_evaluators: list[Any] = []</pre><h4>3. Evaluating the query contextualizer’s prompt on the dataset and prompt tuning</h4><ol><li>First of all in order to create a benchmark we ran the evaluator without query contextualization. The score was 0.11 (average of embedding distances between user_last_message and ground_truth_standalone_message).</li><li>Next we ran evaluation on following configuration:<br>a. v1 of the prompt (langchain’s simple prompt)<br>b. model: gemini-1.5-flash<br>The score was 0.08. As the average distance reduced we could conclude that query contextualization makes the user_last_message more cosine similar to the ground_truth query.</li><li>We then checked the type of examples which had largest embedding distance as those were the clear targets for prompt tuning. After 22 such prompt revisions we finally reached a score of 0.04 and a good enough prompt that performed well on all the different types of examples. We made sure to version the prompts in code in order to be able to back track and compare variations effectively. Again chatgpt4 helped here in improving the prompts by just giving it the current prompt and a list of examples where the prompt was performing poorly!</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4GJW2TpQwgKQvkx6p_3LpA.png" /></figure><p>💡 <em>We can see from the above graph that we keep tuning the prompt in a manner that the distance keeps reducing!</em></p><h4>4. Evaluating against actual production chat log</h4><p>Running only against synthetic dataset is not enough to confirm the prompt works well in actual practice. It is because synthetic datasets suffer with the problem of textual diversity even if we have made sure to create a variety of examples. For better confidence we tested the prompt against a few production queries and chat history and quickly found some improvements that needed to be done to the prompt. We also found that adding few shot examples to the prompt and setting the LLM’s temperature to 0 leads to much more consistent results. As gpt4o-mini was recently released we also did comparative evaluation using it and found it performed better so we switched to using it.</p><h3>Results</h3><h4>1. Some actual outputs (in Japanese)</h4><pre>#Chat History:<br>shreyas: 最近何か新しい本を読みましたか？<br>AI: はい、最近「ハリーポッター」を読みました。<br>shreyas: それは素晴らしいですね。どの巻を読んでいますか？<br>AI: 最初の巻を読み始めました。とても面白いです。<br>#Last user message:<br>いいね！<br>#Contextualized query:<br>最近「ハリーポッター」の最初の巻を読み始めたことに対して、いいね！と言います。<br><br>#Chat History:<br>shreyas: ピカチュウって、ほんとにあるのかな？<br>#Last user message:<br>あるよ！<br>#Contextualized query:<br>ピカチュウは実在するキャラクターで、ポケットモンスターの一部として知られていますよ！<br><br>#Chat History:<br>shreyas: 明日、アイドルフェスティバルでライブ配信があるんだ<br>#Last user message:<br>それって何ですか？<br>#Contextualized query:<br>明日行われるアイドルフェスティバルについて詳しく教えてもらえますか？</pre><h4>2. Prompt and model config</h4><p>Model: gpt-4o-mini<br>Temperature: 0<br>Few shots: Yes (ref: <a href="https://python.langchain.com/v0.2/docs/how_to/few_shot_examples_chat/#fixed-examples">using LangChain</a>)<br>Prompt:</p><pre>Examine the provided chat history and the last user message to determine its clarity as a complete query or statement.<br>The chat history is formatted as a continuous string where each message is on a new line and includes the sender&#39;s name, while the last user message is given separately without a username identifier.<br>If the last message is already a clear, complete query or statement that can be understood independently, return it as is.<br>If it lacks sufficient clarity or requires contextual details to be fully understood on its own, carefully examine all previous interactions in chat history to gather every relevant piece of information.<br>If the last message implicitly or explicitly references any aspect discussed earlier in the chat history that is essential for understanding then Rewrite the last message to include all necessary details discussed earlier, ensuring it clearly &amp; completely integrates those pertinent details to stand alone confidently and directly connects the context from chat history to the user&#39;s statement.<br>The rewritten message in japanese should maintain the original message&#39;s intent, either as a question or a statement, without explaining or answering itself.</pre><p><em>Note: update the prompt if you want output in English!</em></p><h3>Conclusion</h3><p>In conclusion, the approach of contextualizing user queries using chat history has proven to be a critical enhancement for improving memory retrieval in AI conversational agents. By systematically tuning prompts and leveraging synthetic datasets for evaluation, we were able to significantly reduce the embedding distance between the original and contextualized queries. This improvement ensures that AI agents can retrieve more relevant memories, leading to richer, more personalized conversations. The iterative process of prompt tuning, coupled with real-world testing, has resulted in a robust system capable of handling diverse conversation scenarios effectively. This method not only enhances the user experience but also optimizes the performance of AI agents in practical applications. The success of this approach underscores the importance of context in AI conversations and demonstrates the value of carefully designed prompt engineering and evaluation strategies in developing more capable AI systems.</p><p>Gaudiy will continue to advance research and development in AI agents.</p><p>More Articles:</p><p><a href="https://medium.com/gaudiy-web3-and-ai-lab">Gaudiy AI Lab</a></p><p>About Company:</p><p><a href="https://gaudiy.com/en/">Gaudiy Inc. | Moving forward in a new era with fans</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b628896107aa" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gaudiy-ai-lab/b628896107aa">Context Matters: Using Chat History to Retrieve More Relevant Memories for AI Agent</a> was originally published in <a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Part 2: Process— How We Used LangSmith to Streamline Evaluation and Experimentation in LLM Product…]]></title>
            <link>https://medium.com/gaudiy-ai-lab/b63535948a88?source=rss----096fc41648f8---4</link>
            <guid isPermaLink="false">https://medium.com/p/b63535948a88</guid>
            <category><![CDATA[llm-app-development]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[gaudiy]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[langsmith]]></category>
            <dc:creator><![CDATA[Gaudiy Lab]]></dc:creator>
            <pubDate>Tue, 04 Mar 2025 12:50:25 GMT</pubDate>
            <atom:updated>2024-07-22T23:31:52.615Z</atom:updated>
            <content:encoded><![CDATA[<h3>Part 2: Process— How We Used LangSmith to Streamline Evaluation and Experimentation in LLM Product Development</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dWm7VHfA7RrwqsgRYFb8Rg.jpeg" /></figure><p><em>Posted by </em><a href="https://x.com/ruwatana"><em>Wataru Namiki</em></a><em>, Software Engineer of </em><a href="http://redirect.medium.systems/r-03wSyM2SI4"><em>Gaudiy Inc</em></a><em>.</em></p><p>Over the past 1–2 years, the excitement in the generative AI / LLM field has accelerated significantly, and many people are focusing on how to utilize it to provide new value.</p><p>Our company, Gaudiy, saw potential in this field relatively early and has been actively taking on challenges. As we develop LLM products, we’re refining our processes to improve daily based on the knowledge accumulated from addressing challenges that arise.</p><p>This time, I’d like to focus on the process of prompt tuning, which is inseparable from development in this field. I’ll introduce how we’re improving efficiency and resolving common issues, along with some use cases.</p><p>Note: This article is the second part of a two-part series, covering the “Process Implementation” section. I hope you’ll also read the first part by seya, which covers the “Tech Selection/OSS” section.</p><p><a href="https://medium.com/gaudiy-web3-and-ai-lab/4825a9872c05">Part 1: Tech Stack Selection — How We Used LangSmith to Streamline Evaluation and Experimentation…</a></p><h3>What we build — Fanlink</h3><p>At Gaudiy, we provide <strong>“Fanlink,”</strong> a community platform for multiple IPs (intellectual properties).</p><p>Although it’s still in beta and we can’t share many details, we’re currently developing and iterating on a feature that allows users to chat with AI avatars of specific characters (such as actual celebrities or anime characters) on Fanlink, in collaboration with specific IP holders. We call this feature “<strong>AI Talk</strong>.”</p><p>For this feature, we use LLM to generate AI messages. LLM is an essential component, and it’s not an exaggeration to say that it greatly influences the quality of the feature.</p><p>Let me briefly introduce the current AI Talk feature:</p><p>Users can send messages to AI avatars (hereafter referred to as character AI) that behave like specific characters on Fanlink. The character AI retains information about its unique characteristics and memories, and is always prompted to behave appropriately in response to user messages and the flow of conversation.</p><p>In addition to text messages, we also offer supplementary experiences such as giving digital item gifts. We’re also considering features that allow multimodal inputs like voice in the future.</p><p>Prompts include not only replies to user messages but also various scenarios such as initiating conversation topics, self-introductions, and ending conversation flows. We use different prompts in our logic depending on various contexts. This allows us to create a mechanism that can behave more naturally from the start to the end of a conversation segment, or seamlessly transition to the next topic.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dDYm94NVDo3FZeac.png" /></figure><p>Next, let me briefly introduce the architecture:</p><p>When a message is sent from a user, we stack a task in the reply generation queue (using Cloud Pub/Sub). When processing begins, we retrieve information from a DB that stores character features based on a unique ID representing which character AI’s information it is.<br><em>Note: The character AI itself is structured so that multiple individuals can be built separately, and we’re currently operating and verifying two character AIs.</em></p><p>Furthermore, we use vector search to extract information close to the user’s message query from pre-stored base character AI memory data and summaries of past conversations between users and character AI in the Vector Store (using Vertex AI) .</p><p>Then, based on context information such as the start of the conversation or how many times the conversation has occurred, we determine which prompt template to use. We extract only the necessary inputs for the prompt from various data obtained above, such as character information and related memories, and generate messages by invoking LLM using the LangChain interface.</p><p>This approach allows us to realize the behavior of multiple character AIs with a single prompt template.</p><p>While very simple tasks are composed of single prompts, there are also patterns where we chain-execute prompts in multiple steps using LangGraph for more complex context recognition and cost reduction (we’ll omit the detailed technical content here).</p><p>The execution of such LLMs can be traced and viewed on the GUI provided by LangSmith from LangChain, making it very easy to debug when hallucinations occur.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*rVQPBc8aE9RLz8C7.png" /></figure><h3>Challenges Faced in Prompt Tuning Tasks</h3><p>As mentioned earlier, multiple prompts are used to realize the character AI talk feature, and the importance of prompt tuning to improve the accuracy of each one was very high.</p><p>However, we hadn’t established an efficient way of prompt tuning, and we were facing various challenges such as ballooning time-costs due to ad-hoc approaches.</p><p>The biggest challenge was that the common language between the engineers in charge of tuning and the stakeholders (our Biz members) was only qualitative results based on the final behavior of the application. This resulted in large costs and losses for engineers to break down and analyze abstract requirements to the implementation level.</p><p>Previously, after a set of features was fully implemented, we operated with a system where our Biz members, who were well-versed in the AI’s characteristics and in close coordination with the IP, essentially became the stakeholders and were asked to check the quality.<br>The result of the quality check was a qualitative list of items from an infinite number of perspectives, such as whether it met the characteristics of the base character AI, including speech patterns.</p><p>While the quality check results were correct, the burden on engineers to interpret each of these qualitative perspectives and implement them into prompts (sometimes prioritizing them) while meeting all of them was very high.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*8cNjJCtELFczAhpY.png" /></figure><p>In this work, where the completion criteria were difficult to see, there were times when the work itself naturally disappeared or development proceeded without satisfactory tuning. In the end, there were even unfounded conclusions such as the quality had decreased because we changed to a cheaper LLM model.</p><p>When doing prompt tuning, problems like “if you stand this up, that falls down” often occur. The communication cost of having to ask questions each time about which requirements ultimately needed to be realized as must-haves was also significant.</p><p>We also used LangSmith’s Playground to check and tune prompt behavior, but in this case, there was no history left, and each trial had to be done manually, which was very costly.</p><p>This approach would also be very costly when comparing between LLM models to determine which is optimal for the prompt in question.</p><h3>Improving Efficiency in Prompt Tuning</h3><h4>Planning Phase</h4><p>Although an AI team had been formed and we were gaining more members familiar with this field, the AI team was mainly focused on R&amp;D and couldn’t approach these messy process issues. As a member of the product development team, I felt there was a big issue here, so I thought it would be good if we could at least achieve the following to resolve this:</p><ul><li>Create a dataset in some kind of document with multiple expected inputs and examples of correct answers as expected values</li><li>Prepare an environment that can automatically execute LLM and generate execution results based on the prepared dataset</li><li>Prepare persistent data that can serve as a common language for tuning personnel (engineers) and stakeholders (Biz) to discuss improvements based on execution results</li><li>Easily switch to any LLM model and compare results</li></ul><h4>Implementation Phase</h4><p>First, we decided to create a dataset of multiple expected inputs and correct cases in a Google Spreadsheet.<br>The reason for this choice was that it matched the characteristics of spreadsheets, which allow multiple people to edit in real-time online and easily reflect changes in common definitions to the entire dataset using sheet/cell references.</p><p>We structured the spreadsheet as follows:</p><ul><li>A common definition sheet that duplicates DB data to make it easier to tune the character AI’s feature information itself</li><li>Sheets (multiple) for each prompt, summarizing the necessary inputs and outputs for each case in one row, created for as many patterns as needed</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gmKg1ZTYT5hX9uRO.png" /></figure><p>We set up select boxes on the sheets for each prompt to set which character AI the tuning case is for, and used formulas in cell values to auto-fill dynamically from the common definition sheet side.</p><p>We asked Biz members to create other necessary user message inputs and expected output values for each case to cover all patterns.</p><p>Next, for the efficiency of LLM execution and extraction of execution results, we had almost no technical knowledge in this area and were about to start digging when the AI team volunteered to collaborate. After re-syncing the sense of issues and the world view we wanted to realize, they proposed a mechanism to batch execute for each dataset and persist execution verification results on LangSmith using LangSmith’s Datasets &amp; Testing feature!</p><p>Since the API for effectively using LangSmith’s Datasets &amp; Testing feature is published by LangChain, we were able to realize our requirements by wrapping these for our use case.</p><p><em>*This Helper is currently also published as an OSS library. For more details, please read the repository mentioned below or </em><a href="https://medium.com/gaudiy-web3-and-ai-lab/4825a9872c05"><em>seya’s article</em></a><em> introduced at the beginning.</em></p><p><a href="https://github.com/gaudiy/langsmith-evaluation-helper">GitHub - gaudiy/langsmith-evaluation-helper: Helper library for LangSmith that provides an interface to run evaluations by simply writing config files.</a></p><p>From here, I’ll briefly illustrate the overall realization flow and show part of the implementation for efficiently conducting prompt Evaluation along with how to use the Helper (please note that it may differ slightly from the current Helper OSS interface).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ZM9x9QLCUloaF2Wj.png" /></figure><p>First, we implement and execute a Python script (dataset.py) to read information from Google Spreadsheet, convert it to the format of LangSmith’s Datasets Example, and register it.</p><p>For reading, we used the Google Sheet API provided within GCP, which Gaudiy usually uses as infrastructure.<br>To use the Google Sheet API from the script, we realized it by granting viewing permissions to the previously created service account in the sharing settings on the spreadsheet side (note that the way of assigning permissions is different from IAM).</p><p>We perform necessary mapping and formatting processing on the read dictionary data, and call the Helper to register/overwrite to any Dataset Example on LangSmith.</p><pre># LangSmith Dataset Name<br>LANGSMITH_DATASET_NAME = &quot;Test Dataset&quot;<br># Google Spreadsheet ID<br>SPREADSHEET_ID = &quot;hogehoge&quot;<br># Google Spreadsheet name<br>SHEET_RANGE = &quot;test_prompt_sheet&quot;<br># path to SA json for Google Sheet API<br>SERVICE_ACCOUNT_KEY_PATH = os.getenv(&quot;SERVICE_ACCOUNT_KEY_PATH&quot;)<br><br>async def dataset() -&gt; None:<br>        # fetch Spreadsheet<br>    values = get_sheet_values(SHEET_RANGE)<br><br>    # map and create input data<br>    header_row = values[0]<br>    header_columns_count = len(header_row)<br>    data_rows = values[1:]<br><br>    dict_list: list[dict[str, Any]] = []<br><br>    for row in data_rows:<br>        name = row_dict.get(&quot;name&quot;, &quot;&quot;)<br>        bio = row_dict.get(&quot;bio&quot;, &quot;&quot;)<br>        attitude = row_dict.get(&quot;attitude&quot;, &quot;&quot;)<br>        last_message = row_dict.get(&quot;last_message&quot;, &quot;&quot;)<br>        output = row_dict.get(&quot;output&quot;, &quot;&quot;)<br><br>        dict_list.append({<br>            **TestPromptInput(<br>                name=name,<br>                bio=bio,<br>                attitude=attitude,<br>                last_message=last_message,<br>            ).model_dump(),<br>            &quot;output&quot;: output,<br>        })<br><br>        # Call helper library<br>    create_examples(dict_list, dataset_name=LANGSMITH_DATASET_NAME)<br><br>def get_sheet_values(sheet_range: str) -&gt; list[Any]:<br>    credentials = service_account.Credentials.from_service_account_file(<br>        SERVICE_ACCOUNT_KEY_PATH,<br>        scopes=[&quot;https://www.googleapis.com/auth/spreadsheets.readonly&quot;],<br>    )<br><br>    service = build(&quot;sheets&quot;, &quot;v4&quot;, credentials=credentials)<br><br>    sheet = service.spreadsheets()<br>    result = sheet.values().get(spreadsheetId=SPREADSHEET_ID, range=sheet_range).execute()<br>    values = result.get(&quot;values&quot;)<br><br>    return values<br><br>if __name__ == &quot;__main__&quot;:<br>    asyncio.run(dataset())</pre><p>By executing this script (python -m dataset.py), we were able to reflect all inputs and outputs defined in the spreadsheet to the Examples of any Dataset.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ZDPUG_6mzVNZVTQR.png" /></figure><p>Next, we describe the specification of Datasets, LLM models, builtin check functions we want to use, etc. in the YAML file (config.yaml) that defines the config necessary for the Experiment using the Helper.</p><pre>description: test prompt<br><br>prompt:<br>  name: prompt.py<br>  type: python<br>  entry_function: prompt<br><br>evaluators_file_path: evaluations.py<br><br>providers:<br>  - id: TURBO<br><br>tests:<br>  type: langsmith_db<br>  dataset_name: Test Dataset<br>  experiment_prefix: test_</pre><p>By the way, if you want to apply custom checks, you can easily run that evaluation by writing evaluation logic in evaluations.py and specifying it in the config.</p><p>For example, we were able to implement and use custom check functions for accuracy judgment of positive and negative for prompts that return scores including positive and negative.</p><pre>def same_sign(run: Run, example: Example) -&gt; dict[str, bool]:<br>    if run.outputs is None or example.outputs is None or run.outputs.get(&quot;output&quot;) is None:<br>        return {&quot;score&quot;: False}<br><br>    example_output = example.outputs.get(&quot;output&quot;)<br>    if example_output is None:<br>        raise ValueError(&quot;example.outputs is None&quot;)<br><br>    example_score = int(example_output)<br>    output_score = int(run.outputs[&quot;output&quot;].score)<br>    if example_score == 0 or output_score == 0:<br>        return {&quot;score&quot;: example_score == output_score}<br><br>    example_sign = example_score // abs(example_score)<br>    output_sign = output_score // abs(output_score)<br>    return {&quot;score&quot;: example_sign == output_sign}</pre><p>This wasn’t in the initial requirements, but we were taught that such things can be used as Experiments functions.</p><p>The score results of the implemented check functions can be visually displayed in Charts on LangSmith’s Experiments, which was also very easy to understand as an evaluation indicator.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*yqToHtPmZYhGSI8s.png" /></figure><p>Finally, we define the implementation of the prompt template to be executed in a separate file (prompt.py), referring to the actual production-side prompt template definition. (It’s also possible to define the prompt text directly on this file)</p><pre>@traceable<br>async def prompt(inputs: dict[str, Any]) -&gt; TestPromptTemplate:<br>    return TestPromptTemplate(<br>        input=TestPromptInput(<br>            name=inputs.get(&quot;name&quot;, &quot;&quot;),<br>            bio=inputs.get(&quot;bio&quot;, &quot;&quot;),<br>            attitude=inputs.get(&quot;attitude&quot;, &quot;&quot;),<br>        )<br>    )</pre><p>Then, when you execute the alias command that refers to the config, LLM execution is performed for the number of registered datasets, and after execution is complete, all execution results are saved in Experiments on LangSmith.</p><pre>make evaluate &lt;path/to/config.yml&gt;</pre><p>Since there are URL links for each of these Experiments results, sharing this link made it easy for tuning personnel and stakeholders to discuss execution results and modification policies 🎉</p><h4>Operation Phase</h4><p>As mentioned earlier, the actual prompt tuning task involved multiple prompts, so we divided the work, including the above implementation, among engineer members (basically, one person was in charge of one prompt each).</p><p>We summarized in Notion, for each prompt, what kind of logic was used to correct phenomena that engineers could tune and were clearly wrong against the expected value, so that we could track the content of tuning chronologically.</p><p>Also, when we got good results to some extent, we shared the Output LangSmith Experiments URL with Biz members for review, continued to make corrections based on their feedback, or completed if there were no problems.</p><h3>Results and Considerations</h3><p>Previously, we had an unfavorable situation where engineers and PdMs familiar with LLM development were stuck on prompt tuning for about <strong>two weeks</strong>, repeatedly reviewing and modifying prompt improvements, or sometimes the tuning itself was neglected.</p><p>With the efficiency improvement of prompt tuning, we were able to achieve results where the data creation work in spreadsheets centered on Biz took a few days, the implementation of Datasets and Evaluation Script by engineers took <strong>0.5 to 1 day</strong>, and the tuning and review implementation took a few days depending on the complexity of the prompt (actually, we were able to complete the tuning implementation period in about a <strong>week</strong>).</p><p>This achievement is expected to be quite good information as a guideline for doing similar work in the future.</p><p>Also, while previously only specific members performed this task, having all engineers in the team share this process had secondary effects such as acquiring context and knowledge of prompt tuning and mutual review.</p><p>Until now, we had Biz members actually touch the completed application, perform qualitative quality evaluation, and list modification perspectives. This time, by having them register the expected output values as a dataset in advance as ground truth, we were able to greatly reduce the psychological burden on the tuning side.</p><p>We gave Git links to the prompt definition files to Biz members in advance, explained and had them understand the actual prompt text itself, how it works, and in what situations the relevant prompt is executed, before conducting tuning. As it became easy to view Chain trace data from LangSmith’s Experiments results, we were able to receive suggestions from Biz members for modification policies, such as “Wouldn’t it be better to change this notation in this prompt like this?” As engineers, it’s really helpful to receive such detailed perspectives.</p><p>Prompt tuning work is directly linked to quality and cannot be completed by engineers alone. We also gained the realization that it’s very important for context to mutually permeate including surrounding members.</p><h3>Future Challenges</h3><p>Our efficiency improvements are still quite rough and ad-hoc improvements at present.</p><p>We expect various challenges to arise as we continue to develop LLM products in the future, but here are some examples of future challenges and the improvements we are considering.</p><p>In particular, when it comes to mass-producing character AIs in the future, the human power cost aspect of Biz members is a very big challenge, so we are considering approaching this next.</p><ul><li>Human power cost of creating feature and memory data for creating character AI<br> — Automatically generate feature and memory data from personality definition data using generative AI</li><li>Human power cost of creating ground truth data as expected Output values<br> — Infer and automatically generate correct data for new AI based on existing datasets and personality definition data using generative AI</li><li>Quality issues and cost issues of parts where prompt accuracy Evaluation and tuning are done by human power<br> — Consider mechanisms for AI to evaluate and tune using LLM as a Judge</li></ul><h3>Conclusion</h3><p>I introduced an approach to improve efficiency using Google Spreadsheet and LangSmith’s Helper for challenges that arise in the rather common task of prompt tuning.<br>I feel we were able to create one case of how to improve efficiency in labor-intensive and time-consuming tasks, and how to apply new technology like generative AI to resolve it.<br>We hope to continue improving and make LLM product development better.</p><p>That’s all, thank you for reading.</p><p>More Articles:</p><p><a href="https://medium.com/gaudiy-web3-and-ai-lab">Gaudiy AI Lab</a></p><p>About Company:</p><p><a href="https://gaudiy.com/en/">Gaudiy Inc. | Moving forward in a new era with fans</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b63535948a88" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gaudiy-ai-lab/b63535948a88">Part 2: Process— How We Used LangSmith to Streamline Evaluation and Experimentation in LLM Product…</a> was originally published in <a href="https://medium.com/gaudiy-ai-lab">Gaudiy AI Lab</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>