<?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: Abdullrahman Yazouri</title>
    <description>The latest articles on DEV Community by Abdullrahman Yazouri (@abd_yazouri).</description>
    <link>https://dev.to/abd_yazouri</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1062400%2F53ac4b0b-eabb-4b97-b870-5a2949342a82.jpg</url>
      <title>DEV Community: Abdullrahman Yazouri</title>
      <link>https://dev.to/abd_yazouri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abd_yazouri"/>
    <language>en</language>
    <item>
      <title>Why my Arabic debate-scoring engine refuses to give one overall score</title>
      <dc:creator>Abdullrahman Yazouri</dc:creator>
      <pubDate>Mon, 09 Nov 2026 12:19:58 +0000</pubDate>
      <link>https://dev.to/abd_yazouri/why-my-arabic-debate-scoring-engine-refuses-to-give-one-overall-score-p1m</link>
      <guid>https://dev.to/abd_yazouri/why-my-arabic-debate-scoring-engine-refuses-to-give-one-overall-score-p1m</guid>
      <description>&lt;p&gt;In debate, two judges can score the same speech differently. Students get a number with no reasons, and new judges have no standard to check themselves against. I'm building Saql, an Arabic debate-analysis engine that breaks a debate into arguments and objections, scores each on a written scale, and keeps the reasons for every score.&lt;/p&gt;

&lt;p&gt;This post covers the methods it is built on, two design decisions I think matter most, the numbers that actually exist, and what I haven't proven.&lt;/p&gt;

&lt;h2&gt;
  
  
  The methods behind it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Toulmin's model&lt;/strong&gt; (claim, grounds, warrant, backing, qualifier, rebuttal) as the basis for scoring how an argument is constructed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walton's argumentation schemes&lt;/strong&gt;, with critical questions and burden of proof, as the basis for scoring reasoning quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pragma-dialectics&lt;/strong&gt; as the basis for scoring how well a reply handles an objection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-criteria decision analysis (MCDA)&lt;/strong&gt;, to organize the criteria and indicators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behaviorally anchored rating scales (BARS)&lt;/strong&gt;: every level from 0 to 4 has a written description of what that score looks like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test-retest reliability&lt;/strong&gt;: in "precise mode" an evaluation runs three times, and scores that don't repeat are flagged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop adjudication&lt;/strong&gt;: Saql never issues a final verdict. A human decides.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision 1: written anchors, not labels
&lt;/h2&gt;

&lt;p&gt;Until 12 October 2026, the rubric levels in Saql were only labels ("absent", "weak", "partial", "competent", "strong") with empty descriptors, and the per-indicator anchors were recorded as unresolved. Only one indicator, RQ1, had written anchors. In commit &lt;code&gt;935fb96&lt;/code&gt; I loaded the approved anchors for every indicator (methodology v2). The model's instructions moved into a standalone methodology document, and the prompt went from v1 to v3.&lt;/p&gt;

&lt;p&gt;Here is what an anchor looks like. The indicator is RR2, "Response Relevance", which measures whether a reply is aimed at the objection that was actually raised:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level 1&lt;/th&gt;
&lt;th&gt;Level 3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same general topic, but it does not touch the objection.&lt;/td&gt;
&lt;td&gt;Directed at the objection, with a minor digression.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(The anchors are written in Arabic; the English above is my translation.)&lt;/p&gt;

&lt;p&gt;The gap between the levels is what makes a disagreement interpretable. If one judge gives 1 and another gives 3, the question is precise: did the reply touch the objection? It is no longer "who is better?". When a human and the model use the same written anchors, you can ask whether the judge applied a description loosely, or whether the description itself needs tuning.&lt;/p&gt;

&lt;p&gt;Some anchors also carry guard rules against style tricks. For indicator AC5, scope of the claim, cautious language alone does not raise the score, while an unqualified claim that is fully supported gets 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: no overall score
&lt;/h2&gt;

&lt;p&gt;Saql scores along five criteria:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;What it covers&lt;/th&gt;
&lt;th&gt;Indicators&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AC&lt;/td&gt;
&lt;td&gt;Argument Construction&lt;/td&gt;
&lt;td&gt;AC1 to AC6, numeric 0 to 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RQ&lt;/td&gt;
&lt;td&gt;Reasoning Quality&lt;/td&gt;
&lt;td&gt;RQ1 to RQ5, numeric 0 to 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RR&lt;/td&gt;
&lt;td&gt;Rebuttal Response&lt;/td&gt;
&lt;td&gt;RR1 to RR5, numeric 0 to 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BF&lt;/td&gt;
&lt;td&gt;Burden Fulfillment&lt;/td&gt;
&lt;td&gt;BF1 to BF5, qualitative states, no number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CR&lt;/td&gt;
&lt;td&gt;Claim Relevance&lt;/td&gt;
&lt;td&gt;scored at the criterion level&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6z8qoodddauhr50a28ye.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6z8qoodddauhr50a28ye.webp" alt="Saql's anatomy of one argument: claim, evidence, inferential link, objection and response, each tagged with the criterion that measures it" width="800" height="803"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How Saql sees one argument. The claim rests on evidence through an inferential link (AC, RQ). An objection can target the claim, its evidence or its link, and the reply is measured only if the speaker had a chance to respond (RR). The claim's relevance to the case is measured separately (CR). Burden of proof (BF) is tracked per team as five obligations with qualitative states (satisfied, partial, unsatisfied, unspecified), never as a number, and is not derived from argument scores. The unit of analysis differs by criterion: the argument for AC, RQ and CR; the objection-and-reply exchange for RR; the team for BF. (Labels translated from Arabic.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is 16 numeric indicators, each with written anchors. The most important decision is that Saql &lt;strong&gt;does not combine them into one number&lt;/strong&gt;. The code has no approved aggregation rule from indicators to criteria yet, so it doesn't invent one. An overall score would hide exactly what a student or a judge needs to see: whether an argument was clear but badly supported, or well supported but off target.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the code does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Each score is stored per indicator, together with its provenance: provider, model, prompt version, token count and latency.&lt;/li&gt;
&lt;li&gt;Each score links to its evidence references.&lt;/li&gt;
&lt;li&gt;Every change to a score is written to an audit trail.&lt;/li&gt;
&lt;li&gt;A human adjudicator always makes the final call.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The numbers that exist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Number&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scale levels&lt;/td&gt;
&lt;td&gt;5 (0 to 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Criteria&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numeric indicators&lt;/td&gt;
&lt;td&gt;16 (AC1 to AC6, RQ1 to RQ5, RR1 to RR5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passes in precise mode&lt;/td&gt;
&lt;td&gt;3 per evaluation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt versions&lt;/td&gt;
&lt;td&gt;v1 to v3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RQ1 pilot&lt;/td&gt;
&lt;td&gt;5 debates, 28 scoring units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completed score rows in the pilot&lt;/td&gt;
&lt;td&gt;40, of which 39 have a numeric score (qwen2.5:7b: 28 rows, gemini-2.5-flash: 12 rows)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pilot covered one indicator (RQ1), used the first version of the prompt, and ran on two different models. It was a pipeline test, not a measurement of quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I haven't proven
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No calibration yet.&lt;/strong&gt; I have not compared Saql's scores with a panel of human judges at scale. When I do, I'll report exact agreement, agreement within one point, and quadratic weighted kappa, with the sample size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The anchors have not been validated&lt;/strong&gt; against debate coaches. They are a careful first draft of what each score should mean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No aggregation rule.&lt;/strong&gt; I deliberately left it open until there is evidence for one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No adoption yet.&lt;/strong&gt; Some debate organizations have shown early interest, but no organization has adopted it or run a pilot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Saql is meant to be used
&lt;/h2&gt;

&lt;p&gt;Saql is built for institutional ownership, not as a public app. The design assumes that a debate organization, a league or an academic program owns the instance, controls its data, and decides how its judges and coaches use it. That is why the scoring is transparent (written anchors, a record for every score, a human who always decides), and why I'm not promoting a public demo. A tool that scores people's reasoning should sit with an institution that is accountable for how it is used.&lt;/p&gt;

&lt;p&gt;If you work with or advise a debate organization, a league or a university program and would like to talk about how this could work for your judges, leave a comment below or find me on LinkedIn: &lt;a href="https://www.linkedin.com/in/iv7l" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/iv7l&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm a developer in Gaza building Fasil.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>evaluation</category>
    </item>
    <item>
      <title>I Asked an AI to Answer in Arabic. It Answered in Chinese.</title>
      <dc:creator>Abdullrahman Yazouri</dc:creator>
      <pubDate>Sun, 12 Jul 2026 13:22:14 +0000</pubDate>
      <link>https://dev.to/abd_yazouri/i-asked-an-ai-to-answer-in-arabic-it-answered-in-chinese-13md</link>
      <guid>https://dev.to/abd_yazouri/i-asked-an-ai-to-answer-in-arabic-it-answered-in-chinese-13md</guid>
      <description>&lt;h2&gt;
  
  
  The problem nobody optimizes for
&lt;/h2&gt;

&lt;p&gt;Arabic is spoken by 400M+ people and represents &lt;strong&gt;under 1% of internet training data&lt;/strong&gt;. Every LLM you have used is, effectively, an English model with Arabic bolted on.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Fasil&lt;/strong&gt; — a platform where AI evaluates structured debates in formal Arabic (فصحى) — and that 1% turned out to be the hardest engineering constraint I faced. Harder than the war. Harder than the blackouts. Harder than having zero API budget.&lt;/p&gt;

&lt;p&gt;This post is what actually broke, what I measured, and — importantly — &lt;strong&gt;which of my assumptions turned out to be wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fci271ie7mxueyopi1wio.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fci271ie7mxueyopi1wio.JPG" alt="Screenshot of a live Arabic debate on the Fasil interface" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;The Fasil interface showing a structured debate in formal Arabic (فصحى).
  &lt;p&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The tokenizer tax is 2.2x, and I measured it
&lt;/h2&gt;

&lt;p&gt;Arabic tokenizes badly. Here is the same sentence, same meaning, through Qwen2.5's tokenizer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;
&lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen2.5-7B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;en&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The strongest argument wins.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;ar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;الحجة الأقوى هي التي تنتصر.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;en&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# -&amp;gt; 5 tokens
&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ar&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# -&amp;gt; 11 tokens
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5 tokens in English. 11 in Arabic. Identical meaning, 2.2x the cost.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The cause is morphological. Arabic is highly inflected — a single word can encode subject, tense, object, and possession. A tokenizer trained overwhelmingly on English has no efficient merges for those forms, so it shatters them into subword fragments that carry no meaning on their own.&lt;/p&gt;

&lt;p&gt;That 2.2x multiplier compounds through everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Effective context window shrinks by more than half&lt;/strong&gt; — the same debate transcript eats 2.2x the budget&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inference cost more than doubles&lt;/strong&gt; per unit of meaning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small models degrade faster&lt;/strong&gt; than any English benchmark would predict&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mitigation
&lt;/h3&gt;

&lt;p&gt;Aggressive prompt compression, and — more importantly — &lt;strong&gt;moving structural logic out of the prompt and into application code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Debate phases, turn-taking, scoring rules, and state transitions all live in Django. The model is never asked to &lt;em&gt;remember&lt;/em&gt; rules. It is asked only to &lt;em&gt;judge&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is the first rule of low-resource-language engineering: &lt;strong&gt;every token spent on instructions is a token stolen from content.&lt;/strong&gt; In English you can afford that waste. At 2.2x, you cannot.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Small models collapse on فصحى, not on dialect
&lt;/h2&gt;

&lt;p&gt;A counter-intuitive finding, and the one that cost me the most time.&lt;/p&gt;

&lt;p&gt;Small models handled &lt;strong&gt;colloquial&lt;/strong&gt; Arabic noticeably better than &lt;strong&gt;formal&lt;/strong&gt; Arabic. Obvious in hindsight: colloquial Arabic is what appears in scraped social data. Formal Arabic (فصحى) is underrepresented &lt;em&gt;even within&lt;/em&gt; the already-tiny Arabic slice of the corpus.&lt;/p&gt;

&lt;p&gt;For a debate platform, formal Arabic is non-negotiable. The naive approach — &lt;em&gt;"just use a small model, Arabic is Arabic"&lt;/em&gt; — failed on contact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mitigation: task decomposition
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Narrow verification (binary, structured)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma2:2b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cheap, sufficient for &lt;em&gt;checking&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argument evaluation &amp;amp; reasoning&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:7b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required for فصحى reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Small models are fine at &lt;strong&gt;checking&lt;/strong&gt;. They are unreliable at &lt;strong&gt;reasoning in فصحى&lt;/strong&gt;. Match model size to task — and never assume a benchmark score transfers across registers of the same language.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Structured output: I was wrong, then I measured
&lt;/h2&gt;

&lt;p&gt;This is the section where I have to correct myself in public, so let me do it properly.&lt;/p&gt;

&lt;p&gt;My first instinct was: &lt;em&gt;"Arabic breaks JSON output."&lt;/em&gt; I ran the prompt 5 times against &lt;code&gt;gemma2:2b&lt;/code&gt;, got &lt;strong&gt;5/5 malformed&lt;/strong&gt;, and nearly published that number.&lt;/p&gt;

&lt;p&gt;Then I did what I should have done first: &lt;strong&gt;I ran the same prompt in English.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It failed 5/5 too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two failure classes, and conflating them is the mistake
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Class A — the wrapper problem.&lt;/strong&gt; The model wraps its JSON in prose or &lt;code&gt;json&lt;/code&gt; fences instead of returning raw JSON.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;English&lt;/th&gt;
&lt;th&gt;Arabic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma2:2b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20/20 (100%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20/20 (100%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:7b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0/20 (0%)&lt;/td&gt;
&lt;td&gt;0/20 (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is &lt;strong&gt;not an Arabic problem.&lt;/strong&gt; It is a small-model problem, identical in both languages. It is a real reason to fail closed — but it is not evidence of anything linguistic, and selling it as such would have been dishonest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Class B — the content problem.&lt;/strong&gt; Strip the wrapper, &lt;em&gt;then&lt;/em&gt; check: does the JSON parse? Did control characters corrupt it? Did the model silently switch languages inside the values?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;English&lt;/th&gt;
&lt;th&gt;Arabic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma2:2b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0/20 (0%)&lt;/td&gt;
&lt;td&gt;3/20 (15%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:7b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/20 (0%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1/20 (5%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pooled&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/40 (0%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4/40 (10%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the &lt;code&gt;qwen2.5:7b&lt;/code&gt; row carefully. That model produced &lt;strong&gt;perfectly valid JSON in English, 20 times out of 20.&lt;/strong&gt; No wrapper. No corruption. Flawless.&lt;/p&gt;

&lt;p&gt;Same prompt. Same schema. Same temperature. Same server. Switch the language, and it breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero failures in 40 English runs. Four in 40 Arabic runs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The failure is not model capability. It is language.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the failures actually look like — and this is the interesting part
&lt;/h3&gt;

&lt;p&gt;I expected the models to fall back to &lt;strong&gt;English&lt;/strong&gt;. That is not what happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;qwen2.5:7b&lt;/code&gt; fell back to Chinese.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the raw response. The model was writing Arabic, mid-sentence decided it was done, announced &lt;em&gt;in Chinese&lt;/em&gt; that it would answer in Chinese instead, and re-answered the entire task:&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claim&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;الحجة الأقوى هي التي تنتصر في المناظرة المنظمة، لا الصوت الأعلى.&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;verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&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;يعتبر هذا التصريح صحيحًا لأنه يلغي فكرة الهيمنة على الحوار من خلال الصوت而是使用中文回答：}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;← [the model emits a literal markdown fence here, INSIDE the JSON string]&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claim&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;最强的论据在有组织的辩论中会获胜，而不是声音最大的论据。&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;verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&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;这个陈述是正确的，因为它否定了通过声音大小来主导对话的观点。&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;sources&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;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look closely at where it breaks. Mid-word, no space, no delimiter:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;...من خلال الصوت&lt;/code&gt; &lt;strong&gt;而是使用中文回答：&lt;/strong&gt; ← &lt;em&gt;"instead, I will answer in Chinese"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then it opens a &lt;strong&gt;nested markdown fence inside a JSON string value&lt;/strong&gt; and produces a complete, well-formed Chinese JSON object. It is perfectly coherent — in the wrong language, inside a broken structure.&lt;/p&gt;

&lt;p&gt;Qwen is trained by Alibaba. Its dominant language is Chinese.&lt;/p&gt;

&lt;p&gt;Now &lt;code&gt;gemma2:2b&lt;/code&gt;, a Google model, on the same task:&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&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;إنّ الحجة الأقوى هي aquela التي تتميز بالدقة والوضوح...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;aquela&lt;/code&gt; is &lt;strong&gt;Portuguese&lt;/strong&gt;. Not English. Portuguese.&lt;/p&gt;

&lt;p&gt;And another run, structural collapse — note the doubled closing bracket:&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claim&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;...&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;reason&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;...&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;sources&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;مفهوم المناظرة&lt;/span&gt;&lt;span class="sh"&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;unmatched&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The actual finding
&lt;/h3&gt;

&lt;p&gt;The models did not degrade &lt;em&gt;toward English&lt;/em&gt;. Each one degraded &lt;strong&gt;toward its own dominant training language&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Qwen fell to Chinese. Gemma leaked Romance-language fragments. Arabic was not close enough to either model's center of gravity to hold the generation together — so when the model lost its footing, it fell home.&lt;/p&gt;

&lt;p&gt;I have not seen this described clearly anywhere, and it changes how I think about low-resource-language deployment: &lt;strong&gt;your failure mode is not "the model gets worse." Your failure mode is "the model goes home."&lt;/strong&gt; And where home is depends on who trained it.&lt;/p&gt;

&lt;p&gt;If you are building in a low-resource language, this means your choice of base model carries a hidden risk that no benchmark reports: not &lt;em&gt;how well&lt;/em&gt; it fails, but &lt;em&gt;where&lt;/em&gt; it lands when it does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mitigation: fail closed
&lt;/h3&gt;

&lt;p&gt;Every AI output passes through a verification layer. If the structure cannot be parsed, or a claim cannot be confirmed against a retrieved source, it &lt;strong&gt;does not pass&lt;/strong&gt; as verified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better to return nothing than confidently-wrong Arabic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a debate platform, a hallucinated citation is not a bug. It is a betrayal of the entire premise.&lt;/p&gt;

&lt;p&gt;Here is the actual verifier from production. It checks numeric claims in a debate argument against retrieved facts, and flags any number it cannot support:&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="n"&gt;FENCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;          &lt;span class="c1"&gt;# the markdown fence, factored out so this post
&lt;/span&gt;                             &lt;span class="c1"&gt;# does not break its own code block (yes, really)
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_verify_with_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cited_facts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    LLM verifier for orphan numbers. Returns set of numbers confirmed unsupported.
    Fails closed: on any parse error, all orphans are treated as unsupported.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_VERIFIER_SYSTEM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;AgentLLMError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;LLM error: %s - all orphans treated as unsupported&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;# &amp;lt;-- fail closed
&lt;/span&gt;
    &lt;span class="c1"&gt;# The scar tissue: strip the markdown fence the model was told not to emit
&lt;/span&gt;    &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FENCE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&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="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FENCE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[a-zA-Z]*\n?&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FENCE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdicts&lt;/span&gt; &lt;span class="o"&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;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verdicts&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# &amp;lt;-- fail closed
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;verdicts&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&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="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;verdict&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unsupported&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;# &amp;lt;-- fail closed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate &lt;code&gt;return set(orphans)&lt;/code&gt; branches. Every uncertainty path — LLM error, wrong type, unparseable JSON — resolves to &lt;em&gt;"assume nothing is supported."&lt;/em&gt; The claim gets stripped from the argument rather than published with a citation nobody verified.&lt;/p&gt;

&lt;p&gt;And note the fence-stripping block in the middle. I wrote that &lt;strong&gt;months before I ran the measurements in this post.&lt;/strong&gt; It is scar tissue — I added it because &lt;code&gt;gemma2:2b&lt;/code&gt; kept wrapping its JSON, long before I could put a number on how often. Turns out the number is 100%, in both languages.&lt;/p&gt;

&lt;p&gt;Read your own defensive code sometime. It is a map of every failure you have already survived.&lt;/p&gt;

&lt;h3&gt;
  
  
  An honest caveat
&lt;/h3&gt;

&lt;p&gt;N=20 per cell, 80 calls total, temperature 1.0. That is a small sample, and I am not going to pretend a 5% rate is precisely measured.&lt;/p&gt;

&lt;p&gt;But the English baseline is &lt;strong&gt;zero across 40 runs on two different models&lt;/strong&gt;, and the Arabic failures appear on &lt;strong&gt;both&lt;/strong&gt; models independently. The direction is not ambiguous, even if the exact rate is.&lt;/p&gt;

&lt;p&gt;If someone runs this at N=500 across more model families, I would genuinely like to see the numbers — especially whether the "falls back to its training language" pattern holds.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Self-hosting wasn't ideology — it was the only door
&lt;/h2&gt;

&lt;p&gt;Payment gateways do not work where I am. Commercial APIs were never an option — not a preference I made, a door that was closed. So inference runs on self-hosted &lt;strong&gt;Ollama&lt;/strong&gt; on a private VPC.&lt;/p&gt;

&lt;p&gt;The unexpected upside: &lt;strong&gt;complete control over the Arabic pipeline.&lt;/strong&gt; I can swap models per task, tune prompts per register, and inspect every failure — none of which you get from behind an API you could not afford anyway.&lt;/p&gt;

&lt;p&gt;The constraint did not limit the architecture. It &lt;em&gt;forced&lt;/em&gt; it — and the forced version is better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Django + PostgreSQL&lt;/li&gt;
&lt;li&gt;Self-hosted Ollama on a separate instance (private VPC), task-split across model sizes&lt;/li&gt;
&lt;li&gt;Daemon threads instead of Celery — a full task queue was too heavy for the infra&lt;/li&gt;
&lt;li&gt;SHA-256 hashing to make debate outcomes tamper-evident &lt;em&gt;(on-chain anchoring is roadmap, not production — I am being precise, because overclaiming is how trust dies)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Running in production&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why this matters beyond my project
&lt;/h2&gt;

&lt;p&gt;The 1% problem is not a complaint. It is an engineering reality with a &lt;strong&gt;predictable shape&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Worse tokenization&lt;/strong&gt; → shrinking effective context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weaker formal-register performance&lt;/strong&gt; → small models fail exactly where precision matters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language-dependent structured-output corruption&lt;/strong&gt; → verification cannot be optional&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you know the shape, you can design around it. Most of my architecture is simply the 1% problem, answered.&lt;/p&gt;

&lt;p&gt;If you build for a low-resource language, you are not "doing the same thing in another language." You are doing a &lt;strong&gt;harder thing with fewer tools&lt;/strong&gt;, and you will push logic out of the model and into code far more aggressively than any English-first tutorial will tell you.&lt;/p&gt;

&lt;p&gt;That applies to Swahili, Bengali, Tagalog, and every other language the benchmarks quietly ignore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And test your assumptions against an English control.&lt;/strong&gt; My most useful finding this week was discovering that my first hypothesis was wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Context, briefly
&lt;/h2&gt;

&lt;p&gt;I am a developer in Gaza. Much of this was written between blackouts, with electricity measured in hours per day and an internet connection that came and went.&lt;/p&gt;

&lt;p&gt;I mention it only because the constraints shaped the architecture — and, honestly, made it better. I did not wait for perfect conditions. They may never come.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's next:&lt;/strong&gt; an AI-agent debate engine (two agents arguing in فصحى, argument and counter-argument), and a citation pipeline grounding every claim in a retrievable source.&lt;/p&gt;

&lt;p&gt;I would value your thoughts — &lt;strong&gt;especially on the verification layer, which I would rewrite differently today.&lt;/strong&gt; And if you have built for a low-resource language, I want to hear what broke for you.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://fasil.quotefather.com" rel="noopener noreferrer"&gt;fasil.quotefather.com&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  On keeping the lights on
&lt;/h3&gt;

&lt;p&gt;One practical note, and then I will get out of your way.&lt;/p&gt;

&lt;p&gt;Everything in this post runs on self-hosted inference because commercial APIs were never available to me — payment gateways do not operate where I live. That constraint produced better architecture, but it did not make the servers free. GPU time, storage, and bandwidth are the real, boring costs of keeping an Arabic-language AI platform running, and right now I cover them myself.&lt;/p&gt;

&lt;p&gt;If this work seems worth continuing, there is a &lt;br&gt;
&lt;a href="https://fasil.quotefather.com/support" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Support the development of Fasil&lt;/a&gt;
.&lt;/p&gt;

&lt;p&gt;Contributions go directly to infrastructure and development, and the roadmap they fund is public — you can see exactly which milestone your contribution moves.&lt;/p&gt;

&lt;p&gt;And if not, that is completely fine. The measurements above are yours to reuse, the scripts are reproducible, and I would rather you take the findings and build something with them than send me anything at all. That was the point of writing this.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nlp</category>
      <category>selfhosted</category>
      <category>django</category>
    </item>
  </channel>
</rss>
