<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: tzedaka</title>
    <description>The latest articles on DEV Community by tzedaka (@jose_torrealba_5175019345).</description>
    <link>https://dev.to/jose_torrealba_5175019345</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3288199%2F88e85782-3091-46da-b5d1-a36e01182267.png</url>
      <title>DEV Community: tzedaka</title>
      <link>https://dev.to/jose_torrealba_5175019345</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jose_torrealba_5175019345"/>
    <language>en</language>
    <item>
      <title>How We Pass Listing Context to an LLM...</title>
      <dc:creator>tzedaka</dc:creator>
      <pubDate>Thu, 09 Apr 2026 10:09:01 +0000</pubDate>
      <link>https://dev.to/jose_torrealba_5175019345/how-we-pass-listing-context-to-an-llm-lfh</link>
      <guid>https://dev.to/jose_torrealba_5175019345/how-we-pass-listing-context-to-an-llm-lfh</guid>
      <description>&lt;p&gt;We built &lt;a href="https://shopao.io" rel="noopener noreferrer"&gt;Shopao&lt;/a&gt;, an AI agent that automatically answers buyer questions on MercadoLibre listings. When a buyer posts a question, the system intercepts the webhook, assembles context from the listing, the seller's business profile, and their catalog — then either sends a reply directly to MeLi or holds it in the seller's portal for one-click approval.&lt;/p&gt;

&lt;p&gt;This post covers the specific decisions we made in wiring real marketplace data into an LLM reasoning loop, including one bug that took us an embarrassing amount of time to debug.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Spring Boot 3 (REST API + webhook handler)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent service&lt;/strong&gt;: Spring Boot 3 + LangChain4j (stateless reasoning loop)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM&lt;/strong&gt;: swappable via config — currently OpenAI &lt;code&gt;gpt-4o-mini&lt;/code&gt;, with Gemini and Ollama also supported&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketplace&lt;/strong&gt;: MercadoLibre (Argentina/LATAM)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent service is fully stateless — it receives a request, runs the LangChain4j reasoning loop, and returns a suggested reply. No session, no DB access inside the loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. How the Prompt Is Constructed
&lt;/h2&gt;

&lt;p&gt;The most important architectural decision: &lt;strong&gt;the LLM only sees the buyer's question and the item ID in the user message&lt;/strong&gt;. Everything else arrives through tool calls during the reasoning loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LangChain4j @AiService interface&lt;/span&gt;
&lt;span class="nd"&gt;@SystemMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fromResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/prompts/system-message.txt"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@UserMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fromResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/prompts/user-message.txt"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@V&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"questionText"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;questionText&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;@V&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"itemId"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user message template is minimal — question text, item ID, and a workflow instruction. The LLM then calls three tools in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;QuestionContextTool&lt;/code&gt;&lt;/strong&gt; — hits MeLi's public API, formats listing data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SellerProfileTool&lt;/code&gt;&lt;/strong&gt; — returns seller policies (warranty, shipping, returns) from a &lt;code&gt;ThreadLocal&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RelatedProductsTool&lt;/code&gt;&lt;/strong&gt; — returns catalog context for upsell/cross-sell, called only when relevant&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We intentionally don't pre-inject listing data as a JSON blob into the user message. The tool-calling pattern lets the LLM skip the related products tool on purely technical questions, saving tokens and ~200ms per request.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Handling Incomplete Listing Data
&lt;/h2&gt;

&lt;p&gt;MercadoLibre listings vary wildly in data quality. Some have 40 structured attributes; some have a title and nothing else. A naive implementation passes the raw API response to the LLM and lets it deal with nulls — we don't do that.&lt;/p&gt;

&lt;p&gt;Three-layer defense:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — Description fallback.&lt;/strong&gt; The description endpoint returns 404 for many listings. Feign throws on 4xx by default, so we catch it and return the literal string &lt;code&gt;"(no description)"&lt;/code&gt;. An empty string would be worse — the model might try to fabricate one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — Attribute filtering at render time.&lt;/strong&gt; MeLi's API frequently returns attribute objects where &lt;code&gt;name&lt;/code&gt; is populated but &lt;code&gt;value_name&lt;/code&gt; is null. We drop those silently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Attribute&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValueName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValueName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"- "&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;": "&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValueName&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\n"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sending &lt;code&gt;- Brand: null&lt;/code&gt; to the LLM is worse than omitting it — it takes up tokens and can confuse the model into treating "null" as a real value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 — Tool-level fallback.&lt;/strong&gt; If the entire tool call fails (network timeout, MeLi outage), the tool returns &lt;code&gt;"Product context not available for item {id}."&lt;/code&gt; instead of throwing. The agent falls back to whatever it can infer from the seller profile alone — imperfect, but it keeps the pipeline alive.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Passing Per-Request State Without Polluting the LLM Context
&lt;/h2&gt;

&lt;p&gt;The seller's OAuth token, business profile, and email address all need to be reachable during the reasoning loop — but none of them should appear in any LLM message.&lt;/p&gt;

&lt;p&gt;We use a set of &lt;code&gt;ThreadLocal&lt;/code&gt; values, loaded before the LLM call and cleared in a &lt;code&gt;finally&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AgentServiceImpl.generateResponse():
  → set ThreadLocals: accessToken, sellerProfile, itemProfile, sellerEmail, questionId
  → call aiService.answer(questionText, itemId)
  → check escalation flag
  → finally: clear all ThreadLocals
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Feign interceptor for MeLi's API reads the OAuth token from the same &lt;code&gt;ThreadLocal&lt;/code&gt; and injects it as an &lt;code&gt;Authorization&lt;/code&gt; header — no token ever touches the LLM conversation, no database lookup needed mid-loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Latency Breakdown
&lt;/h2&gt;

&lt;p&gt;The webhook returns &lt;code&gt;200 OK&lt;/code&gt; immediately. Everything after is asynchronous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Webhook received → 200 OK                              &amp;lt; 5ms
Async thread starts:
  Token validation (fast path: DB read)               ~20–50ms
  Fetch question from MeLi API                        ~100–200ms
  DB write (PROCESSING status)                        ~5ms
  Fetch seller + item profiles from DB                ~10ms
  Build related products context (DB query)           ~10ms
  Agent call (dominant cost):
    QuestionContextTool → getItem()                   ~100–250ms
    QuestionContextTool → getItemDescription()        ~80–200ms
    LLM inference (gpt-4o-mini, ~400–800 tokens in)  ~600ms–2.5s
  Post answer to MeLi (if auto-send)                  ~100–200ms
  DB update (final status)                            ~5ms
─────────────────────────────────────────────────────
Total: 1.2s – 5s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two MeLi API calls inside &lt;code&gt;QuestionContextTool&lt;/code&gt; run sequentially — they could be parallelized. We haven't done it yet because it requires moving parallelism into the assembler layer explicitly, and current P95 latency is acceptable. It's the clearest optimization still on the table.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The Auto-Send Decision
&lt;/h2&gt;

&lt;p&gt;The most sensitive gate in the pipeline: send the LLM's answer directly to MeLi, or hold it in the &lt;a href="https://shopao.io" rel="noopener noreferrer"&gt;Shopao seller portal&lt;/a&gt; for review.&lt;/p&gt;

&lt;p&gt;Three independent conditions must all be true to auto-send:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;autoRespondEnabled&lt;/code&gt;&lt;/strong&gt; — a per-seller flag (defaults to &lt;code&gt;true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work schedule&lt;/strong&gt; — timezone-aware blackout windows the seller configures as JSON (&lt;code&gt;[{"days":["MON",...], "startHour":9, "endHour":18}]&lt;/code&gt;), with support for overnight ranges like 22:00–06:00&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No escalation sentinel&lt;/strong&gt; — if the LLM returned &lt;code&gt;__ESCALATED__&lt;/code&gt;, the question is always held blank regardless of the other settings&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any condition fails, the question lands in &lt;code&gt;READY_FOR_REVIEW&lt;/code&gt;. The seller approves or edits with one click.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Escalation System — and What Broke
&lt;/h2&gt;

&lt;p&gt;This was the most interesting problem to debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Early design: custom exception.&lt;/strong&gt; The escalation tool threw a &lt;code&gt;RuntimeException&lt;/code&gt; with &lt;code&gt;super(null)&lt;/code&gt; — the idea was to signal "stop, don't answer" by interrupting the LangChain4j loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Early version — this broke&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;escalate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EscalationSignal&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// RuntimeException with null message&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LangChain4j 1.2.0 catches exceptions thrown from tool methods and wraps them in a &lt;code&gt;ToolExecutionResultMessage&lt;/code&gt;. Internally it calls &lt;code&gt;e.getMessage()&lt;/code&gt;, which returned &lt;code&gt;null&lt;/code&gt; for our exception. That null hit &lt;code&gt;ToolExecutionResultMessage.from(id, null)&lt;/code&gt;, which threw &lt;code&gt;IllegalArgumentException&lt;/code&gt;, which surfaced as a 500 to the caller. Every escalation killed the request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current design: sentinel string + ThreadLocal flag.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Tool returns a sentinel string and sets a flag&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;escalate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="no"&gt;TRIGGERED&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// ThreadLocal flag&lt;/span&gt;
    &lt;span class="n"&gt;sendEscalationEmail&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// notify seller immediately&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"__ESCALATED__"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// LangChain4j sees this as a normal tool result&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Caller checks the flag after the LLM returns&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aiService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;questionText&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EscalationTool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wasTriggered&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;ESCALATED_SENTINEL&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// discard whatever text the LLM generated post-escalation&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We check the flag &lt;em&gt;after&lt;/em&gt; &lt;code&gt;answer()&lt;/code&gt; returns because the LLM sometimes echoes the sentinel string back as part of its response text. Checking the flag lets us discard that and return cleanly.&lt;/p&gt;

&lt;p&gt;The escalation tool also sends a transactional email to the seller with a direct link to the question in the portal. If the email fails, it logs a warning and continues — the escalation happens regardless.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Preventing Over-Escalation
&lt;/h2&gt;

&lt;p&gt;We learned the hard way that LLMs escalate on borderline questions whenever you give them an opening. The tool description is now deliberately over-specified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ONLY call this tool when ALL of these conditions are met:
(1) You already called the product context tool AND the seller profile tool.
(2) The answer is genuinely impossible — not just uncertain, but impossible.
(3) The question matches one of these specific categories:
    price negotiation; post-sale issue; request for private contact info;
    product the seller doesn't carry; real-time ERP data unavailable in the listing.

When in doubt: answer. Do not escalate.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key distinction is &lt;strong&gt;"genuinely impossible — not just uncertain"&lt;/strong&gt;. The system prompt backs this up with concrete examples: if the listing says "compatible with iPhone 15" and the buyer asks about the 15 Pro, the agent should answer — the Pro is part of the same family. Reasonable inference is expected. Fabrication is not.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Multi-SKU Variants and the Stock Tradeoff
&lt;/h2&gt;

&lt;p&gt;MercadoLibre listings have a &lt;code&gt;variations&lt;/code&gt; array with &lt;code&gt;attribute_combinations&lt;/code&gt; per variant. A phone listing might have 12 variations: 4 colors × 3 storage tiers. We pass these as a flat list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;AVAILABLE VARIANTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Color: Negro Almacenamiento&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128GB&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Color: Negro Almacenamiento&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256GB&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Color: Blanco Almacenamiento&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128GB&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Color: Blanco Almacenamiento&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256GB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We deliberately don't include per-variant stock levels or per-variant prices. That would require a separate API call per variation ID — too slow for the real-time pipeline. The LLM knows which combinations exist but not whether each is currently in stock. For the vast majority of questions this doesn't matter; for "do you have the white 256GB?" the model answers affirmatively if the variant exists and hedges if it can't confirm stock.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. OAuth Token Safety Under Concurrent Webhooks
&lt;/h2&gt;

&lt;p&gt;MercadoLibre issues &lt;strong&gt;single-use refresh tokens&lt;/strong&gt; — use one to refresh, and the old token is immediately invalidated. Two concurrent webhook threads hitting a near-expired token simultaneously would cause the second refresh to fail with &lt;code&gt;invalid_token&lt;/code&gt;, requiring a full seller re-auth.&lt;/p&gt;

&lt;p&gt;The fix: per-account &lt;code&gt;synchronized&lt;/code&gt; block with double-checked locking. The first thread acquires the lock, refreshes, and persists the new token before releasing. Any thread that was waiting re-reads from the DB inside the lock, finds the token already fresh, and returns early without touching the refresh endpoint.&lt;/p&gt;

&lt;p&gt;We also apply a 5-minute buffer — we refresh before the token actually expires to avoid edge-case 401s mid-request. If the refresh itself fails (seller revoked access), we mark the account &lt;code&gt;needsReconnect=true&lt;/code&gt; and surface a 401 so the seller re-authenticates through the &lt;a href="https://shopao.io" rel="noopener noreferrer"&gt;Shopao&lt;/a&gt; OAuth flow.&lt;/p&gt;




&lt;p&gt;Full context on why we built this: &lt;a href="https://shopao.io/es/blog/chatbot-vs-ia-mercadolibre-cual-entiende-tus-preguntas" rel="noopener noreferrer"&gt;chatbot vs. AI agent for MercadoLibre sellers&lt;/a&gt;&lt;/p&gt;

</description>
      <category>langchain4j</category>
      <category>mercadolibre</category>
      <category>springboot</category>
      <category>automation</category>
    </item>
    <item>
      <title>Keyword bot vs. LLM agent for e-commerce Q&amp;A: a technical breakdown</title>
      <dc:creator>tzedaka</dc:creator>
      <pubDate>Thu, 09 Apr 2026 09:28:41 +0000</pubDate>
      <link>https://dev.to/jose_torrealba_5175019345/keyword-bot-vs-llm-agent-for-e-commerce-qa-a-technical-breakdown-3fja</link>
      <guid>https://dev.to/jose_torrealba_5175019345/keyword-bot-vs-llm-agent-for-e-commerce-qa-a-technical-breakdown-3fja</guid>
      <description>&lt;p&gt;Most automation tools for MercadoLibre sellers fall into one of two categories: keyword-based chatbots (MercadoBot, Yobot, JaimeBot) or LLM-powered agents. From the outside, they look similar — buyer asks a question, system sends a reply. Under the hood, they're completely different architectures with very different failure modes.&lt;/p&gt;

&lt;p&gt;This post breaks down how each works technically, where each fails, and why the difference matters at scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  How a keyword bot works
&lt;/h2&gt;

&lt;p&gt;The core logic is a rule engine. At setup, the seller configures a list of keyword → response pairs. At runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;keyword_bot_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;question_lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;question_lower&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;kw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keywords&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# no match → fallback or no reply
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's essentially it. The system checks if the incoming text contains a preconfigured string. If it does, it returns the associated template. If it doesn't, the question goes unanswered or gets a generic fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The structural problem:&lt;/strong&gt; the bot has no access to the product listing. It responds with text the seller wrote at configuration time — which may be outdated, incomplete, or wrong for a specific variant. The seller has to manually anticipate every phrasing a buyer might use.&lt;/p&gt;

&lt;p&gt;At small scale with simple catalogs (10 products, predictable questions), this works fine. At scale or with technical products, it breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  How an LLM agent works
&lt;/h2&gt;

&lt;p&gt;An LLM agent doesn't pattern-match — it reasons. The key architectural difference is &lt;strong&gt;context injection&lt;/strong&gt;: before generating a reply, the agent retrieves real data from the product listing and injects it into the prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;agent_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listing_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Fetch live listing context from MeLi API
&lt;/span&gt;    &lt;span class="n"&gt;listing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mercadolibre_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_listing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listing_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attributes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attributes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;       &lt;span class="c1"&gt;# voltage, dimensions, compatibility...
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;variations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;variations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;       &lt;span class="c1"&gt;# sizes, colors, models
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seller_profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_seller_profile&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;    &lt;span class="c1"&gt;# warranty, shipping, return policy
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Build prompt with context
&lt;/span&gt;    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    You are a sales assistant for a MercadoLibre seller.

    Product context:
    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

    Buyer question: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

    Answer the question using only the product context provided.
    If the information is not available in the context, say so clearly.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Call LLM
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can answer questions about voltage, compatibility, variants, and warranty without any preconfigured rules — because it reads the actual listing before generating the reply.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure modes by question type
&lt;/h2&gt;

&lt;p&gt;This is where the architectural difference becomes practical:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Keyword bot&lt;/th&gt;
&lt;th&gt;LLM agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact keyword match&lt;/td&gt;
&lt;td&gt;"¿tiene garantía?"&lt;/td&gt;
&lt;td&gt;✅ works&lt;/td&gt;
&lt;td&gt;✅ works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synonym / paraphrase&lt;/td&gt;
&lt;td&gt;"¿qué cobertura tiene en fallas?"&lt;/td&gt;
&lt;td&gt;❌ no match&lt;/td&gt;
&lt;td&gt;✅ reads warranty from profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technical spec&lt;/td&gt;
&lt;td&gt;"¿sirve para 220V?"&lt;/td&gt;
&lt;td&gt;❌ unless pre-configured&lt;/td&gt;
&lt;td&gt;✅ reads voltage attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variant combination&lt;/td&gt;
&lt;td&gt;"¿el azul también viene en XL?"&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ reads variations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compatibility&lt;/td&gt;
&lt;td&gt;"¿funciona con mi HP 14s?"&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ reads compatibility attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out-of-scope&lt;/td&gt;
&lt;td&gt;"¿hacen instalación a domicilio?"&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ returns "not available" cleanly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For catalogs with simple, predictable questions, keyword bots cover 60–70% of cases. For technical catalogs (electronics, tools, auto parts), the unmatched rate is typically 30–50%.&lt;/p&gt;




&lt;h2&gt;
  
  
  The context window problem
&lt;/h2&gt;

&lt;p&gt;LLM agents have their own failure mode: &lt;strong&gt;context quality&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The agent is only as good as the data injected into the prompt. If the listing has incomplete attributes — no voltage listed, missing dimensions, vague description — the agent has nothing to work with and will either hallucinate or return a non-answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Bad&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;
&lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BRAND&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Samsung&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Galaxy Tab&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt; &lt;span class="n"&gt;can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t answer: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;¿funciona con 220V?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; — voltage not in context

// Good listing attributes  
attributes = [
    {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BRAND&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Samsung&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;},
    {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Galaxy Tab&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;},
    {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VOLTAGE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;110V/220V&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;},
    {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONNECTIVITY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WiFi, Bluetooth 5.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;},
    {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPATIBLE_DEVICES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Android, Windows, Mac&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;},
]
// Agent answers voltage, compatibility questions correctly
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the quality of automated replies is directly tied to how complete the listing data is — which is actually a useful forcing function for sellers to maintain better catalog hygiene.&lt;/p&gt;




&lt;h2&gt;
  
  
  MercadoLibre's native AI: a hybrid case
&lt;/h2&gt;

&lt;p&gt;MeLi introduced an AI suggestion feature in 2024–2025. It reads the listing and generates a suggested reply — which is genuinely good quality for standard questions.&lt;/p&gt;

&lt;p&gt;The catch: &lt;strong&gt;it doesn't auto-send&lt;/strong&gt;. The seller has to approve each suggestion manually. Architecturally, it's an LLM agent without the final delivery step. For sellers with off-hours volume (60% of questions arrive outside business hours per Ventiapp 2025 data), this doesn't solve the automation problem.&lt;/p&gt;

&lt;p&gt;The delta between MeLi's native AI and external agents like &lt;a href="https://shopao.io" rel="noopener noreferrer"&gt;Shopao&lt;/a&gt; is one step in the pipeline: auto-send vs. manual approval.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to use each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keyword bot:&lt;/strong&gt; small catalog, simple questions, budget-constrained, seller willing to invest setup time per product category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM agent:&lt;/strong&gt; technical catalog, high off-hours volume, multi-variant products, seller wants zero configuration per question type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hybrid (keyword first, LLM fallback):&lt;/strong&gt; high volume scenarios where you want to minimize LLM API calls for trivially answerable questions. The keyword layer handles FAQ-type questions cheaply; the LLM handles everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hybrid_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listing_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Try cheap keyword match first
&lt;/span&gt;    &lt;span class="n"&gt;quick_reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;keyword_bot_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;quick_reply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;quick_reply&lt;/span&gt;

    &lt;span class="c1"&gt;# Fall back to LLM for complex/unmatched questions
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;agent_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listing_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The choice isn't really "chatbot vs. AI" — it's about whether your reply system has access to real product context at inference time. A keyword bot configured with correct answers can outperform a poorly-prompted LLM agent. But a keyword bot fundamentally cannot answer questions it wasn't configured for, while an LLM agent with good listing data can handle the full distribution of buyer questions without any manual rule configuration.&lt;/p&gt;

&lt;p&gt;For technical catalogs on MercadoLibre, the unmatched question rate with keyword bots is high enough that the difference in conversion is measurable.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full comparison including pricing and real case studies: &lt;a href="https://shopao.io/es/blog/chatbot-vs-ia-mercadolibre-cual-entiende-tus-preguntas" rel="noopener noreferrer"&gt;shopao.io/blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;#ai&lt;/code&gt; &lt;code&gt;#machinelearning&lt;/code&gt; &lt;code&gt;#ecommerce&lt;/code&gt; &lt;code&gt;#python&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>ecommerce</category>
      <category>python</category>
    </item>
  </channel>
</rss>
