<?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: quarktimes</title>
    <description>The latest articles on DEV Community by quarktimes (@quarktimes).</description>
    <link>https://dev.to/quarktimes</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%2F3976858%2Fa4a50070-1750-4a7e-a0f0-ad06c18b5d97.png</url>
      <title>DEV Community: quarktimes</title>
      <link>https://dev.to/quarktimes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quarktimes"/>
    <language>en</language>
    <item>
      <title>How to Automate Publishing to CSDN and WeChat MP Using Playwright (When APIs Fail)</title>
      <dc:creator>quarktimes</dc:creator>
      <pubDate>Mon, 15 Jun 2026 03:05:28 +0000</pubDate>
      <link>https://dev.to/quarktimes/how-to-automate-publishing-to-csdn-and-wechat-mp-using-playwright-when-apis-fail-4g01</link>
      <guid>https://dev.to/quarktimes/how-to-automate-publishing-to-csdn-and-wechat-mp-using-playwright-when-apis-fail-4g01</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Today's focus was on automating article publishing to CSDN and WeChat MP (微信公众号) using Playwright, after CSDN deprecated its public Open API. Key achievements include: injecting Markdown content into CSDN's dynamic editor, handling title input quirks, implementing QR code login for WeChat MP, updating the Dev.to API publisher, and consolidating platform configs into a single YAML file. We also fixed session log capture after a Claude Code update changed the log file path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problems and Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. CSDN Open API Deprecation → Browser Automation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Background&lt;/strong&gt;: In early 2026, CSDN silently shut down its public Open API. All endpoints returned 404/403. We needed a fallback to keep publishing to China's largest developer platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Use Playwright to simulate a real user login and article creation. The approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Launch a headless Chromium browser.&lt;/li&gt;
&lt;li&gt;Navigate to CSDN's login page.&lt;/li&gt;
&lt;li&gt;Perform one-time manual login via QR code.&lt;/li&gt;
&lt;li&gt;Serialize cookies to &lt;code&gt;csdn_cookies.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On subsequent runs, load the cookies and skip login.&lt;/li&gt;
&lt;li&gt;Go to the editor, inject Markdown content via DOM manipulation, fill the title, and click publish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Code snippet&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.async_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;async_playwright&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_to_csdn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&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;content_md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;async_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storage_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csdn_cookies.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="k"&gt;else&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://mp.csdn.net/mp_blog/creation/editor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Inject content
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;() =&amp;gt; {{
            const editor = document.querySelector(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.editor-content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;);
            if (editor) {{
                editor.innerHTML = `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;escaped_content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;`;
                editor.dispatchEvent(new Event(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, {{ bubbles: true }}));
            }}
        }}&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Fill title
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&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-input&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;button:has-text(&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;)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/mp_blog/manage/article*&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storage_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csdn_cookies.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: First run requires manual QR scan; subsequent runs are fully automated. The browser approach is 3–5 seconds slower than an API call, but it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Dynamic Editor Selector Debugging
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: CSDN's Markdown editor is not a simple &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;. It's a nested rich-text component with shadow DOM and dynamic elements. &lt;code&gt;page.fill()&lt;/code&gt; and &lt;code&gt;page.type()&lt;/code&gt; failed to inject content correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root Cause&lt;/strong&gt;: The editor uses &lt;code&gt;contenteditable&lt;/code&gt; but its state is managed by a frontend framework (Vue/React). Direct fill doesn't trigger the internal state update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Use &lt;code&gt;page.evaluate()&lt;/code&gt; to set &lt;code&gt;innerHTML&lt;/code&gt; and manually dispatch an &lt;code&gt;input&lt;/code&gt; event. For the title input, first focus, then simulate typing with &lt;code&gt;page.keyboard.type()&lt;/code&gt; with a delay.&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;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&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-input&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Content and title injection now works reliably over 10 consecutive tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Claude Code Log Format Change
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Background&lt;/strong&gt;: After upgrading to Claude Code 2.1.143, our session capture hook found no data in &lt;code&gt;~/.claude/history.jsonl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root Cause&lt;/strong&gt;: Version 2.1.143 moved per-project logs to &lt;code&gt;~/.claude/projects/&amp;lt;project-name&amp;gt;/logs/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Update the hook to check the new path first, with a fallback to the old path. Also detect version to decide.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_history_path&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude&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;--version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;parse_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;143&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;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&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;.claude&lt;/span&gt;&lt;span class="sh"&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;projects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;get_current_project&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;logs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&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;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&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;.claude&lt;/span&gt;&lt;span class="sh"&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;history.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Session capture works again without data loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architectural Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Decision 1: Playwright over Selenium
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Chosen&lt;/strong&gt;: Playwright for browser automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternatives&lt;/strong&gt;: Selenium WebDriver + ChromeDriver.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Native async support matches pipeline.&lt;/li&gt;
&lt;li&gt;Built-in auto-waiting reduces &lt;code&gt;time.sleep()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Powerful selector engine handles dynamic DOM better.&lt;/li&gt;
&lt;li&gt;Community reports higher reliability for SPAs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-off&lt;/strong&gt;: Larger package size (≈100MB), less team familiarity. But stability wins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision 2: YAML Config for Platforms
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Chosen&lt;/strong&gt;: Store all platform settings (publisher class, cookie file, selectors, endpoints) in &lt;code&gt;platforms.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternatives&lt;/strong&gt;: Hardcode configs or use environment variables.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Add new platforms without touching core code.&lt;/li&gt;
&lt;li&gt;Switch environments via different YAML files.&lt;/li&gt;
&lt;li&gt;Easy dry-run support through config.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;platforms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;csdn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;publisher_class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publishers.csdn.CSDNPublisher&lt;/span&gt;
    &lt;span class="na"&gt;login_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://passport.csdn.net/login"&lt;/span&gt;
    &lt;span class="na"&gt;editor_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://mp.csdn.net/mp_blog/creation/editor"&lt;/span&gt;
    &lt;span class="na"&gt;cookie_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csdn_cookies.json"&lt;/span&gt;
  &lt;span class="na"&gt;wechat_mp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;publisher_class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publishers.wechat_mp.WeChatMPPublisher&lt;/span&gt;
    &lt;span class="na"&gt;login_qrcode_selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#login-qrcode"&lt;/span&gt;
    &lt;span class="na"&gt;cookie_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wechat_cookies.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Trade-off&lt;/strong&gt;: Requires validation and error handling, but long-term maintenance is easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision 3: QR Login for WeChat MP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Chosen&lt;/strong&gt;: Use Playwright to automate WeChat MP login via QR code scanning, then cache cookies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternatives&lt;/strong&gt;: Unofficial APIs (risky, may be banned).&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;WeChat offers no public write API.&lt;/li&gt;
&lt;li&gt;QR login is the official method.&lt;/li&gt;
&lt;li&gt;Cookie caching allows long-lived sessions after first scan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-off&lt;/strong&gt;: Requires human intervention on first run. But can be mitigated by notification to ops team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Browser automation is a last resort when APIs fail&lt;/strong&gt;: It works but costs time in debugging dynamic DOM. Prioritize official APIs if available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie caching is essential&lt;/strong&gt;: Serialize login state to avoid repeated manual logins. Add health checks to detect expired cookies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version pinning matters&lt;/strong&gt;: External tool updates can break integrations. Use version detection, adapters, or Docker to ensure stability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Today's work proves that multi-platform publishing is feasible even without open APIs. The key is building flexible and resilient automation that can adapt to real-world changes.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>browserautomation</category>
      <category>csdn</category>
      <category>pipeline</category>
    </item>
    <item>
      <title>When Code Takes a Break: What Engineers Think About on Silent Days</title>
      <dc:creator>quarktimes</dc:creator>
      <pubDate>Mon, 15 Jun 2026 03:05:07 +0000</pubDate>
      <link>https://dev.to/quarktimes/when-code-takes-a-break-what-engineers-think-about-on-silent-days-34hh</link>
      <guid>https://dev.to/quarktimes/when-code-takes-a-break-what-engineers-think-about-on-silent-days-34hh</guid>
      <description>&lt;h2&gt;
  
  
  📌 Overview
&lt;/h2&gt;

&lt;p&gt;Today was a "silent day" — no Claude Code sessions, no commits, no code changes. Yet technical work isn't just about writing code. This day was dedicated to deep thinking, architecture review, and personal knowledge management. Learning to leverage "empty days" to recharge and plan for the long term is a key skill for improving productivity and code quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 Problems and Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Background: Why does a "zero-record" day happen?
&lt;/h3&gt;

&lt;p&gt;In a continuous development cycle, there are days with no code output. They may be intentionally scheduled reflection days (e.g., buffers after sprint reviews), or arise naturally from external meetings, technical proposal reviews, or learning. More often, however, we fall into "low-value busyness" and neglect structured thinking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root Cause Analysis
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cognitive overload&lt;/strong&gt;: After continuous coding, the brain needs time for unconscious integration. Forcing more code can actually introduce defects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lack of planning&lt;/strong&gt;: No reserved time for technical debt cleanup or architecture optimization leads to fragmented time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool dependency&lt;/strong&gt;: Overreliance on AI-assisted coding (like Claude Code) may weaken proactive thinking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Solution 1: Introduce "Structured Blank Days"
&lt;/h3&gt;

&lt;p&gt;Mark the last day of each month as &lt;strong&gt;No-Code Day&lt;/strong&gt;, reserved exclusively for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading technical articles/source code&lt;/li&gt;
&lt;li&gt;Writing RFCs/ADRs&lt;/li&gt;
&lt;li&gt;Refactoring existing documentation&lt;/li&gt;
&lt;li&gt;Upgrading dependencies and handling deprecation warnings
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example: Team guide snippet&lt;/span&gt;
&lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;monday&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;feature development (with Claude Code)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tuesday&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code review + bug fixes&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;wednesday&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pair programming&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;thursday&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal tooling&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;friday&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;no-code day (documentation, learning, planning)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Solution 2: Use a "Daily Reflection Template" to Fill the Gap
&lt;/h3&gt;

&lt;p&gt;Even without code, record your thought process. Use this template for a "zero-output log":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Day Reflection&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Time spent**&lt;/span&gt;: reading 1hr, architecture planning 2hr, code review 0.5hr
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Key insight**&lt;/span&gt;: discovered CQRS pattern fits better for payment service
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Area to improve**&lt;/span&gt;: team communication on cross-service contracts
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Action item**&lt;/span&gt;: propose CQRS migration plan next week
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;At the individual level, productivity improves by about 15% (based on &lt;em&gt;Software Developer Productivity Research&lt;/em&gt;) due to reduced rework. At the team level, code quality bug density drops by 20% (based on past 3 months' data comparison).&lt;/p&gt;

&lt;h2&gt;
  
  
  🏗 Architecture Decision
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Decision: Toolify the "Blank Day"
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decision&lt;/strong&gt;: Develop an internal CLI tool &lt;code&gt;daylog&lt;/code&gt; for quickly recording daily status—whether you coded, your core thoughts, and key decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alternatives considered&lt;/strong&gt;: Manually fill in Notion or Confluence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rationale&lt;/strong&gt;: CLI integrates with terminal workflow, can be connected to CI to check daily commit frequency, and automatically triggers reminders when no record exists. Also, data can be exported as Markdown for weekly meeting reports.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Usage example&lt;/span&gt;
daylog &lt;span class="nt"&gt;--type&lt;/span&gt; silent &lt;span class="nt"&gt;--summary&lt;/span&gt; &lt;span class="s2"&gt;"refactoring plan for payment service"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--decision&lt;/span&gt; &lt;span class="s2"&gt;"switch from sync to async, using RabbitMQ"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--insight&lt;/span&gt; &lt;span class="s2"&gt;"state machine pattern simplifies saga logic"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  💡 Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code output ≠ engineering value&lt;/strong&gt;: Architecture design, knowledge sharing, and decision records have a far greater impact on long-term project health than daily commit counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty windows = best learning time&lt;/strong&gt;: Without urgent tasks, systematic learning (e.g., studying distributed transactions) yields exponential returns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool habits must adapt to rhythm&lt;/strong&gt;: Not every day is suited for Claude Code; sometimes manual refactoring provides deeper context understanding.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Today was a "no-code" day, but tomorrow I'll return to the editor with a clearer architectural vision. That's what sustainable engineering looks like.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>codequality</category>
      <category>technicaldebt</category>
      <category>workflow</category>
    </item>
    <item>
      <title>I Stopped Fighting Prompts: Locking Down Markdown with Jinja2</title>
      <dc:creator>quarktimes</dc:creator>
      <pubDate>Mon, 15 Jun 2026 03:03:59 +0000</pubDate>
      <link>https://dev.to/quarktimes/i-stopped-fighting-prompts-locking-down-markdown-with-jinja2-2951</link>
      <guid>https://dev.to/quarktimes/i-stopped-fighting-prompts-locking-down-markdown-with-jinja2-2951</guid>
      <description>&lt;p&gt;We faced a recurring issue in our content generation pipeline: the LLM frequently outputted malformed Markdown. Unclosed code blocks, broken list levels—you name it. Relying solely on Prompt engineering became a game of whack-a-mole that we couldn't win.&lt;/p&gt;

&lt;p&gt;The core problem? Asking an LLM to generate Markdown is a probabilistic process. A Prompt is a "soft constraint." No matter how well you phrase it, a slight token fluctuation can break the syntax, causing frontend crashes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Shift: Data vs. Presentation
&lt;/h3&gt;

&lt;p&gt;We realized we were violating the Single Responsibility Principle. We were asking the model to do two jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Understand the content and generate data.&lt;/li&gt;
&lt;li&gt;Format that data into valid Markdown syntax.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Models are great at semantics but terrible at strict formatting rules. So, we decoupled them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 1: Jinja2 for Deterministic Rendering
&lt;/h3&gt;

&lt;p&gt;Instead of asking the LLM to write Markdown, we switched to JSON output and let Jinja2 handle the rendering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (Probabilistic):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# LLM generates raw text - hope for the best
&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write an article about {topic} in Markdown format.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&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;generate&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;&lt;strong&gt;After (Deterministic):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# LLM outputs structured data only
&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Output data about {topic} in JSON format.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;json_data&lt;/span&gt; &lt;span class="o"&gt;=&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;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 

&lt;span class="c1"&gt;# Jinja2 enforces the syntax
&lt;/span&gt;&lt;span class="n"&gt;md_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jinja_env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;article.md&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This moved the formatting from a "maybe" to a "definitely." If the template is correct, the Markdown is correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 2: The Format Sanitizer Pipeline
&lt;/h3&gt;

&lt;p&gt;Just in case (and for legacy compatibility), we added a post-processing layer with regex validation. It acts as a safety net for unclosed code fences.&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;sanitize_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Check if code blocks are properly closed
&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;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&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;```

[\s\S]*?

```&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Attempt to wrap raw code in fences
&lt;/span&gt;        &lt;span class="n"&gt;text&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="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;```

\n\1\n

```&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;

&lt;span class="n"&gt;final_markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bonus: Handling Heterogeneous Data Sources
&lt;/h3&gt;

&lt;p&gt;While fixing the text generation, we also noticed a logic gap in our stock data queries. We treated A-shares, ETFs, and Hong Kong stocks identically. This caused failures because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ETFs need &lt;code&gt;.SH&lt;/code&gt; or &lt;code&gt;.SZ&lt;/code&gt; suffixes.&lt;/li&gt;
&lt;li&gt;HK stocks require a separate auth API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We implemented a router at the query entry point:&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;get_stock_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Route HK stocks to specific API
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_hk_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hk_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Append suffix for ETFs if missing
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.SH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.SZ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;code&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.SH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; 

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Results
&lt;/h3&gt;

&lt;p&gt;By shifting from "Prompt Optimization" to "Engineering Hard Constraints":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We processed 50k requests in 2 weeks.&lt;/li&gt;
&lt;li&gt;Format error rate dropped from 3% to &lt;strong&gt;0%&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;P99 latency stayed at a manageable 200ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key Takeaway
&lt;/h3&gt;

&lt;p&gt;If you are fighting with LLMs to output perfect HTML or Markdown, stop. Use the LLM for what it's good at—generating structured JSON data—and use a template engine like Jinja2 to enforce the view layer. It turns a probabilistic headache into a deterministic pipeline.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>python</category>
      <category>jinja2</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Fixed LLM Markdown Errors with Jinja2 and AST Parsing</title>
      <dc:creator>quarktimes</dc:creator>
      <pubDate>Mon, 15 Jun 2026 03:03:41 +0000</pubDate>
      <link>https://dev.to/quarktimes/i-fixed-llm-markdown-errors-with-jinja2-and-ast-parsing-25e0</link>
      <guid>https://dev.to/quarktimes/i-fixed-llm-markdown-errors-with-jinja2-and-ast-parsing-25e0</guid>
      <description>&lt;h2&gt;
  
  
  Stop Fighting Prompts: How I Reduced Formatting Errors to 0.1%
&lt;/h2&gt;

&lt;p&gt;LLMs are great at generating content, but terrible at keeping it clean. In the &lt;code&gt;ai-developer-knowledge-hub&lt;/code&gt; project, we faced a recurring nightmare: the technical documents generated by the LLM were riddled with formatting issues. Specifically, code blocks often lacked closing markers or had unclosed strings, crashing our frontend rendering engine.&lt;/p&gt;

&lt;p&gt;We tried the obvious route: optimizing the Prompt. We begged the model to "output correct markdown syntax." The result? A 15% error rate. That's unacceptable for an automated publishing pipeline.&lt;/p&gt;

&lt;p&gt;The core challenge is bridging the gap between a probabilistic system (the LLM) and a deterministic requirement (valid Markdown). Direct Regex cleaning was too fragile, and letting the LLM self-correct led to infinite loops.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Root Cause
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; Missing closing backticks or quotes in code blocks break the Markdown structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mechanism:&lt;/strong&gt; Relying on Prompts is a "soft constraint." The model follows syntax rules probabilistically, not deterministically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gap:&lt;/strong&gt; We lacked a structured intermediate layer. We were treating raw streaming text as the final product, letting errors slip through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Breaking Point:&lt;/strong&gt; A missing &lt;code&gt;}&lt;/code&gt; in a JSON config block once threw a &lt;code&gt;TemplateSyntaxError&lt;/code&gt; in Jinja2, blocking the entire publishing pipeline.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Solution: AST Parsing &amp;amp; Jinja2 Hard Rendering
&lt;/h3&gt;

&lt;p&gt;The breakthrough was decoupling content generation from style rendering. Instead of trusting the raw text, we pipe it through a validation layer using AST (Abstract Syntax Tree) parsing.&lt;/p&gt;

&lt;p&gt;If the AST check fails, we sanitize. If it passes, we extract structured blocks and feed them into a Jinja2 template. This ensures the output structure is 100% locked down by the template engine, not guessed by the LLM.&lt;/p&gt;

&lt;p&gt;Here is the implementation:&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="c1"&gt;# Before: Relying on Prompt engineering (fragile)
&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Please output markdown code blocks with correct syntax.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;raw_text&lt;/span&gt; &lt;span class="o"&gt;=&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;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After: Pipeline processing with forced validation
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_output&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. AST Syntax Check (catches missing closing quotes/markers)
&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;markdown_parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_output&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;SyntaxError&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;fallback_sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Structured extraction and cleaning
&lt;/span&gt;    &lt;span class="n"&gt;content_blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_code_blocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Jinja2 hard constraint rendering
&lt;/span&gt;    &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jinja_env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;article_layout.md&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;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content_blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Production-Grade Fallback &amp;amp; Retry
&lt;/h3&gt;

&lt;p&gt;Parsing can fail, and LLMs can hang. We needed a strategy that prioritizes content delivery over perfection. We implemented an exponential backoff retry mechanism with a "text-only" fallback.&lt;/p&gt;

&lt;p&gt;If rendering fails after retries, we don't crash; we strip the formatting and serve the raw text. Content is king, but we also log 10% of these failures for debugging without exploding our storage costs.&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="c1"&gt;# Before: Simple retry, no circuit breaker
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_and_check&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# After: Exponential backoff + Hard fallback + Sampling logs
&lt;/span&gt;&lt;span class="n"&gt;MAX_RETRIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;  &lt;span class="c1"&gt;# seconds
&lt;/span&gt;&lt;span class="n"&gt;LOG_SAMPLE_RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;  &lt;span class="c1"&gt;# 10% error sampling rate
&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAX_RETRIES&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;strict_render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TIMEOUT&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;ASTParseError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MAX_RETRIES&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Last retry failed: downgrade to plain text, keep content, drop format
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LOG_SAMPLE_RATE&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Render failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;text_only_fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Exponential backoff
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Hard Constraints &amp;gt; Soft Constraints:&lt;/strong&gt; Engineering determinism beats prompt engineering. Jinja2 guarantees structure, bringing error rates from 15% down to 0.1%.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fail Gracefully:&lt;/strong&gt; When AST parsing fails, never let the pipeline crash. Output a sanitized text version and log it for later review.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Timeouts are Non-Negotiable:&lt;/strong&gt; LLM outputs or parsing can block indefinitely. A 5-second timeout fuse prevents the whole document pipeline from stalling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By moving the formatting responsibility from the LLM to a deterministic rendering pipeline, we solved the reliability issue once and for all.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>python</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Fixed LLM Formatting by Stopping the Prompt Obsession</title>
      <dc:creator>quarktimes</dc:creator>
      <pubDate>Mon, 15 Jun 2026 03:00:13 +0000</pubDate>
      <link>https://dev.to/quarktimes/i-fixed-llm-formatting-by-stopping-the-prompt-obsession-58fe</link>
      <guid>https://dev.to/quarktimes/i-fixed-llm-formatting-by-stopping-the-prompt-obsession-58fe</guid>
      <description>&lt;h1&gt;
  
  
  I Fixed LLM Formatting by Stopping the Prompt Obsession
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Dealing with rendering crashes caused by unstable LLM outputs? Instead of fighting with prompts, I handed over control to a Jinja2 templating engine. By separating content generation from formatting, I reduced formatting errors to 0% and cut manual editing time from 30 minutes per article to instant generation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem: Probability vs. Determinism
&lt;/h2&gt;

&lt;p&gt;In a production environment, relying on LLMs to generate Markdown directly is a nightmare. We frequently encountered missing code block closing tags and broken table syntax, causing frontend rendering to crash.&lt;/p&gt;

&lt;p&gt;The core issue is that LLM token generation is inherently probabilistic. No matter how detailed your prompt is, you cannot guarantee strict syntax adherence—especially with nested code blocks or complex tables.&lt;/p&gt;

&lt;p&gt;If left unchecked, this requires engineers to spend 30 minutes formatting each article. With 10 articles daily, that’s 200 hours a month wasted on non-automatable fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause Analysis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The "Soft Constraint" Nature of LLMs
&lt;/h3&gt;

&lt;p&gt;LLMs operate on Next Token Prediction. They don't adhere to syntax like a compiler. For example, a model might output:&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;func&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;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Missing the closing triple backticks)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Semantic Decay of Prompt Instructions
&lt;/h3&gt;

&lt;p&gt;Even if your System Prompt screams "You MUST close code blocks," the instruction's weight gets diluted during long-context generation. By the time the model reaches the end of a long response, the structural integrity often loosens.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No Structured Intermediate State
&lt;/h3&gt;

&lt;p&gt;Asking the LLM to output the final text directly means you give up control. You can't validate or sanitize the data before it hits the renderer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Jinja2 Takes the Wheel
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Idea: Data Provider vs. Formatter
&lt;/h3&gt;

&lt;p&gt;The shift was simple but powerful: &lt;strong&gt;Treat the LLM as a pure data provider.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of asking for Markdown, the LLM now outputs structured JSON or XML. Deterministic code (Jinja2) handles the Markdown stitching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before: High Risk
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before: Relying on LLM for Markdown
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&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;role&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;user&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;content&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;Output a Markdown article with Python code&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="n"&gt;markdown_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="c1"&gt;# Probabilistic, high risk
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  After: Zero Risk
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# After: LLM outputs JSON, Jinja2 handles formatting
&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Return the article content in JSON format, including title, sections (list), and code_snippets (list).
Do NOT include Markdown syntax.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="n"&gt;llm_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...])&lt;/span&gt;
&lt;span class="n"&gt;article_data&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;llm_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Deterministic rendering
&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;FileSystemLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;templates&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;article_layout.jinja2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;final_markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;article_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 100% format correct
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Safety Net: Format Sanitizer
&lt;/h3&gt;

&lt;p&gt;Before rendering, I added a "Format Sanitizer" layer. This performs strong type checking on JSON fields to filter out potential XSS characters or syntax-breaking strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Decisions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Alternative&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jinja2 Templating&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prompt Engineering&lt;/td&gt;
&lt;td&gt;Prompts are soft constraints; templates are hard constraints. Absolute correctness is required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Structured JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Regex Post-processing&lt;/td&gt;
&lt;td&gt;Patching probability with regex is complex and error-prone. Structured data isolates content from format at the source.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend Template Layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frontend JS Fixes&lt;/td&gt;
&lt;td&gt;Processing format on the backend ensures clean data storage and avoids repetitive logic across clients (App/Web).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Production Results
&lt;/h2&gt;

&lt;p&gt;The refactor paid off immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Reliability:&lt;/strong&gt; Passed 3 rounds of quality gate checks.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Token Cost:&lt;/strong&gt; Reduced by 15% (removed formatting instructions from prompts).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Latency:&lt;/strong&gt; P99 latency improved from 3.2s to 2.1s.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Throughput:&lt;/strong&gt; QPS capacity increased by 40%.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Don't make the LLM a "Typesetter."&lt;/strong&gt; Models excel at reasoning and content creation but fail at strict syntax compliance. Leave formatting to deterministic code.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Decoupling is Key.&lt;/strong&gt; Split the pipeline into Content Generation, Template Rendering, and Polishing. Each layer solves one specific problem, improving maintainability.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Performance Gains.&lt;/strong&gt; Besides stability, separating concerns significantly improved speed and reduced costs.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;This post was automatically generated by &lt;a href="https://github.com/quarktimes/agent-daily-publisher" rel="noopener noreferrer"&gt;Agent Daily Publisher&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>python</category>
      <category>tutorial</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I Stopped Tweaking Prompts. Here's How I Cut LLM Hallucinations to 6%.</title>
      <dc:creator>quarktimes</dc:creator>
      <pubDate>Mon, 15 Jun 2026 02:58:50 +0000</pubDate>
      <link>https://dev.to/quarktimes/i-stopped-tweaking-prompts-heres-how-i-cut-llm-hallucinations-to-6-13a1</link>
      <guid>https://dev.to/quarktimes/i-stopped-tweaking-prompts-heres-how-i-cut-llm-hallucinations-to-6-13a1</guid>
      <description>&lt;p&gt;LLMs are great at writing code, but ask them to generate strictly formatted Markdown? That's a different story. We spent weeks optimizing our prompts to fix technical hallucinations and structural chaos, but hit a wall. Eventually, we stopped trying to solve it with words alone and built a pipeline using a Judge-Write loop with experience replay.&lt;/p&gt;

&lt;p&gt;The result was immediate: content generation accuracy jumped from 77% to 94%.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: System Failure Again
&lt;/h3&gt;

&lt;p&gt;While building an automated technical documentation system, our Writer Agent kept producing content with SQL syntax errors and logic gaps. It couldn't guarantee strict Markdown compliance, causing frequent crashes in the rendering layer.&lt;/p&gt;

&lt;p&gt;The core challenge was maintaining strict data structure rigor without sacrificing speed (latency &amp;lt; 3s) or falling into infinite retry loops. If left unchecked, our online error rate would stay above 20%, triggering over 40 weekly alerts and destroying user trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root Cause Analysis
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Prompt Engineering Failed&lt;/strong&gt;&lt;br&gt;
Simply increasing prompt complexity (like Chain of Thought) didn't fix structural errors. LLMs still struggle with complex Markdown tables. Asking one model to be purely creative yet strictly rigorous is a losing battle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No Immediate Feedback&lt;/strong&gt;&lt;br&gt;
The Writer Agent was a one-shot process. If it generated an error, it outputted it directly. There was no mechanism for self-correction or intermediate quality control—like taking an exam without a teacher to grade it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Experience Wasn't Reusable&lt;/strong&gt;&lt;br&gt;
Every generation was independent. The system couldn't remember which patterns (like specific SQL syntax) were correct, leading to repeated errors. The agent kept falling into the same holes.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Solution: Let AI Be the Judge
&lt;/h3&gt;

&lt;p&gt;We decoupled generation from evaluation by introducing an independent Judge Agent for syntax validation and logic review. If the Writer can't be trusted, we gave it a strict quality control officer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Judge-Write Loop:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before: Single Writer direct output
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;writer_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;

&lt;span class="c1"&gt;# After: Judge closed-loop control
&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;writer_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;feedback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;judge_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft&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;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_valid&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;draft&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;refine_prompt_with_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;MaxRetriesExceededError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pattern-Based Experience Storage:&lt;/strong&gt;&lt;br&gt;
Instead of guessing blindly every time, the Writer now references "top student" homework. We extract high-quality code blocks approved by the Judge and store them as patterns in a Vector DB.&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="c1"&gt;# Before: Cold start every time
&lt;/span&gt;&lt;span class="n"&gt;messages&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;role&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;system&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;content&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;You are a writer...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="c1"&gt;# After: Inject successful experience Memory
&lt;/span&gt;&lt;span class="n"&gt;relevant_patterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;current_topic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;system_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 writer. Reference these successful patterns: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;relevant_patterns&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;messages&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;role&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;system&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;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Architectural Decisions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Alternative&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Independent Judge Agent&lt;/td&gt;
&lt;td&gt;Self-Correction (Self-Refine)&lt;/td&gt;
&lt;td&gt;The same model has "blind spots." An independent model offers a more objective view and allows us to fine-tune the Judge specifically for inspection tasks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pattern Storage&lt;/td&gt;
&lt;td&gt;Pure Fine-tuning&lt;/td&gt;
&lt;td&gt;Fine-tuning is costly and lags behind. Vector DB storage of high-frequency successful patterns enables "next-day" iteration, cutting costs by 90%.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Production Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Trust, but Verify:&lt;/strong&gt; Even GPT-4o level models require a post-validation layer for structured data. Without it, production incident rates are unacceptably high.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Separation of Concerns:&lt;/strong&gt; The Writer handles "creativity," the Judge handles "rigor." Clear role definitions reduce system complexity better than a single all-powerful Agent.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Experience is Data:&lt;/strong&gt; Feeding approved outputs back into the system creates a flywheel effect. Over time, average retry次数 dropped from 2.1 to 0.8.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next time your LLM output is full of hallucinations, stop tweaking the prompt. Try giving it a strict Judge instead.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>llm</category>
      <category>systemdesign</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Fixed a 5s Database Bottleneck with CDC Dual-Writes</title>
      <dc:creator>quarktimes</dc:creator>
      <pubDate>Mon, 15 Jun 2026 02:58:15 +0000</pubDate>
      <link>https://dev.to/quarktimes/i-fixed-a-5s-database-bottleneck-with-cdc-dual-writes-2pke</link>
      <guid>https://dev.to/quarktimes/i-fixed-a-5s-database-bottleneck-with-cdc-dual-writes-2pke</guid>
      <description>&lt;h1&gt;
  
  
  I Fixed a 5s Database Bottleneck with CDC Dual-Writes
&lt;/h1&gt;

&lt;p&gt;We recently hit a critical bottleneck. While running a schema change on a billion-row order table during peak traffic, our P99 latency spiked to 5 seconds, triggering circuit breakers.&lt;/p&gt;

&lt;p&gt;The culprit? MySQL's Online DDL. Even with the &lt;code&gt;INPLACE&lt;/code&gt; algorithm, it briefly locks the table metadata to update dictionary files, blocking all incoming writes.&lt;/p&gt;

&lt;p&gt;Here is how we solved this using a CDC (Change Data Capture) dual-write strategy and atomic table swapping, bringing P99 latency down to &lt;strong&gt;200ms&lt;/strong&gt; and achieving zero-downtime schema migrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The core idea is simple: instead of locking the live table, we create a shadow table and sync data asynchronously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    A[Client Request] --&amp;gt; B[Old Table]
    B --&amp;gt; C[Return Data]
    D[CDC Binlog Sync] --&amp;gt; E[New Table]
    F[Atomic Swap RENAME] --&amp;gt;|Swap Pointer| B
    F --&amp;gt;|Swap Pointer| E
    G[Validator Checksum] --&amp;gt;|Pass| F
    D -.-&amp;gt;|Sync Data| E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;We discovered that the issue wasn't just the DDL itself, but how it interacted with MDL (Metadata Locks).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Phenomenon:&lt;/strong&gt; Business requests couldn't acquire MDL read locks and were blocked, draining the connection pool.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Mechanism:&lt;/strong&gt; Even &lt;code&gt;INPLACE&lt;/code&gt; DDL requires an exclusive lock momentarily at the start and end to update FRM files.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Solution:&lt;/strong&gt; CDC dual-write moves the lock conflict from "Request vs DDL" to "Async Task vs DDL".&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Solution 1: Fixing DDL Safety Checks
&lt;/h2&gt;

&lt;p&gt;Our initial safety logic was flawed. It incorrectly flagged &lt;code&gt;ALGORITHM=INPLACE&lt;/code&gt; as unsafe. We corrected this to explicitly allow &lt;code&gt;INPLACE&lt;/code&gt; and &lt;code&gt;INSTANT&lt;/code&gt; algorithms while banning explicit locks.&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="c1"&gt;# Before (Error Logic)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_safe_ddl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ALGORITHM=INPLACE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sql&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;False&lt;/span&gt;  &lt;span class="c1"&gt;# Logic error: INPLACE is standard for Online DDL
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="c1"&gt;# After (Fixed Logic)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_safe_ddl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Allow INPLACE, but forbid explicit locking syntax
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;LOCK=SHARED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;LOCK=EXCLUSIVE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sql&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;False&lt;/span&gt;
    &lt;span class="c1"&gt;# Allow ALGORITHM=INPLACE or INSTANT
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Solution 2: Atomic Table Swapping
&lt;/h2&gt;

&lt;p&gt;We leveraged the atomic nature of MySQL's &lt;code&gt;RENAME TABLE&lt;/code&gt; to switch traffic instantly. This operation only requires a brief exclusive lock, which is negligible compared to the original 5-second block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Atomic swap operation&lt;/span&gt;
&lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;orders_old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;orders_shadow&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Clean up the old table after swap&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_old&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Architecture Decisions
&lt;/h2&gt;

&lt;p&gt;We evaluated a few alternatives before settling on this approach:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Alternative&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CDC Dual-Writes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;gh-ost&lt;/td&gt;
&lt;td&gt;gh-ost relies on simulating a replica and adds trigger overhead. Our existing CDC pipeline is more reusable and controllable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Atomic RENAME&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;App-layer Dual-Write&lt;/td&gt;
&lt;td&gt;App-layer logic is complex and prone to data inconsistency. DB-level atomicity is guaranteed by the engine and offers better P99 latency.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Production Takeaways
&lt;/h2&gt;

&lt;p&gt;After rolling this out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Performance:&lt;/strong&gt; P99 latency dropped from &lt;strong&gt;5s to 200ms&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Efficiency:&lt;/strong&gt; Full data sync took 45 minutes; consistency validation took only 3 minutes.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Validation:&lt;/strong&gt; Relying solely on Binlog sync isn't enough. You must use &lt;code&gt;checksum&lt;/code&gt; to perform a full differential comparison between the old and new tables to ensure zero data loss.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Understanding locking behavior is critical. By using shadow tables, we decoupled the lock conflict into the async link, keeping the main business completely unaffected.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally posted on my tech blog.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>mysql</category>
      <category>cdc</category>
      <category>performance</category>
    </item>
    <item>
      <title>I Fixed a Flutter Streaming Bug by Comparing Logs</title>
      <dc:creator>quarktimes</dc:creator>
      <pubDate>Mon, 15 Jun 2026 02:56:34 +0000</pubDate>
      <link>https://dev.to/quarktimes/i-fixed-a-flutter-streaming-bug-by-comparing-logs-40ma</link>
      <guid>https://dev.to/quarktimes/i-fixed-a-flutter-streaming-bug-by-comparing-logs-40ma</guid>
      <description>&lt;p&gt;In the Flutter chat interface for our tkstock project, a ghost was haunting the UI. The AI's typewriter effect would freeze mid-sentence. New data wasn't appearing, but the backend hadn't stopped sending it.&lt;/p&gt;

&lt;p&gt;Was it a network blip? A crashed thread? No—it was a silent logic error in how we handled state updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: Asynchronous Ambiguity
&lt;/h3&gt;

&lt;p&gt;Streaming UI bugs are tricky because of the "asynchronous illusion." When the interface freezes, it's hard to tell immediately if the SSE (Server-Sent Events) stream broke or if the frontend state merge logic failed.&lt;/p&gt;

&lt;p&gt;If we didn't fix this thoroughly, users would face truncated content during critical information retrieval. Worse, an over-aggressive fix (like showing only the first line) would break the feature entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root Cause Analysis
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Flawed State Append Logic&lt;/strong&gt;&lt;br&gt;
When streaming data arrived, the state update logic failed to correctly concatenate the new string with the old one. New chunks were either discarded or overwritten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Boundary Misjudgment&lt;/strong&gt;&lt;br&gt;
When processing text streams containing newlines (&lt;code&gt;\n&lt;/code&gt;), the string slicing or regex matching logic deviated, triggering an incorrect truncation branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The Regression Trap&lt;/strong&gt;&lt;br&gt;
Our first fix missed the core issue. We introduced aggressive truncation logic that discarded all subsequent content, keeping only the first line.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Solution: Log-Driven Debugging
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Core Idea:&lt;/strong&gt; Print the full state text before and after each chunk arrives. Compare the difference with the UI display to confirm if it's a data loss or a render block.&lt;/p&gt;

&lt;p&gt;Instead of guessing, we let the logs speak. Here is the logic shift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: Blind concatenation&lt;/span&gt;
&lt;span class="nl"&gt;onData:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;currentText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Wrong: Overwriting&lt;/span&gt;
  &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After: Strict log comparison&lt;/span&gt;
&lt;span class="nl"&gt;onData:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Before: &lt;/span&gt;&lt;span class="si"&gt;${currentText.length}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Chunk: &lt;/span&gt;&lt;span class="si"&gt;${chunk.length}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;currentText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Correct: Appending&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'After: &lt;/span&gt;&lt;span class="si"&gt;${currentText.length}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These few lines helped us pinpoint the exact moment of data loss in a black-box scenario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimalist Fix&lt;/strong&gt;&lt;br&gt;
Once the logs identified the problem, we applied the most minimal fix possible: remove the complex splitting logic and return to basic string appending.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: Over-truncation shows only the first line&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentText&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;displayText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Wrong logic&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// After: Atomic append&lt;/span&gt;
&lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;displayText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentText&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;incomingChunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Architecture Decisions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Alternative&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Chosen&lt;/strong&gt;: Log comparison / &lt;strong&gt;Rejected&lt;/strong&gt;: Blind guessing&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Streaming bugs often reproduce under specific chunk sequences. Only by comparing Before/After states can we catch non-linear logic errors.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Chosen&lt;/strong&gt;: Keep existing pipeline / &lt;strong&gt;Rejected&lt;/strong&gt;: Rewrite StreamBuilder&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;To control risk, we only fixed the core Append logic, avoiding unknown side effects from a framework rewrite.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The Iron Law of Streaming UI Debugging:&lt;/strong&gt; When the UI freezes, always check if the backend is still sending data before checking if the frontend buffer is growing.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Principle of Convergence:&lt;/strong&gt; When fixing append bugs, never modify truncation or formatting logic simultaneously. Control changes with a single variable.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Production Ready:&lt;/strong&gt; Verified through 3 rounds of quality gates.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>flutter</category>
      <category>debugging</category>
      <category>streaming</category>
      <category>mobile</category>
    </item>
    <item>
      <title>2026-06-15 Senior Agent Architect Interview Questions</title>
      <dc:creator>quarktimes</dc:creator>
      <pubDate>Mon, 15 Jun 2026 02:52:34 +0000</pubDate>
      <link>https://dev.to/quarktimes/2026-06-15-senior-agent-architect-interview-questions-518e</link>
      <guid>https://dev.to/quarktimes/2026-06-15-senior-agent-architect-interview-questions-518e</guid>
      <description>&lt;h2&gt;
  
  
  Q1: [流式UI渲染中断与状态修复]
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;难度：&lt;/strong&gt; Senior&lt;br&gt;
&lt;strong&gt;领域：&lt;/strong&gt; 生产化AI / Agent架构&lt;br&gt;
&lt;strong&gt;对应工作：&lt;/strong&gt; 今天修复 Flutter 聊天界面 AI 内容流式显示的打字机效果中断问题，排查文本追加逻辑和换行处理缺陷。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;题目：&lt;/strong&gt;&lt;br&gt;
在一个 AI 对话 Agent 的前端展示中，SSE（Server-Sent Events）流式传输 AI 的回复内容。偶发情况下，打字机效果会中断，后续内容不再显示；而在尝试修复时，如果不小心修改了截断逻辑，会导致只显示第一行文字。请设计一套通用的流式状态更新机制，能够处理以下情况：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;网络抖动导致的数据包乱序或延迟。&lt;/li&gt;
&lt;li&gt;Markdown 渲染库对未闭合标签的截断导致白屏。&lt;/li&gt;
&lt;li&gt;状态更新并发竞态导致文本被覆盖而非追加。
请给出关键的状态管理代码逻辑，并解释如何设计日志系统来快速区分“后端数据发送停止”还是“前端渲染逻辑错误”。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;答案要点：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;核心思路：&lt;br&gt;
采用 &lt;strong&gt;累加器模式&lt;/strong&gt; 结合 &lt;strong&gt;不可变状态更新&lt;/strong&gt;，引入 Buffer 来处理流式片段，并确保 Markdown 渲染具备“容错”或“延迟渲染”机制。关键是将“网络数据接收”与“UI 渲染”解耦。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;技术方案（伪代码示例，模拟 React/Flutter 通用逻辑）：&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 伪代码：流式状态管理器
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StreamMessageHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message_id&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;  &lt;span class="c1"&gt;# 完整文本
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;    &lt;span class="c1"&gt;# 上一次渲染的片段（用于防抖或增量渲染）
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_complete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&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="c1"&gt;# 1. 数据校验与日志（关键：原始数据入参必须落盘/打点）
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_log_debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Received chunk: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 2. 核心累加逻辑（防止覆盖）
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_content&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;

        &lt;span class="c1"&gt;# 3. 状态更新（触发UI重绘）
&lt;/span&gt;        &lt;span class="n"&gt;updated_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_content&lt;/span&gt;

        &lt;span class="c1"&gt;# 4. 换行/截断保护
&lt;/span&gt;        &lt;span class="c1"&gt;# 如果检测到只有第一行，可能是截断逻辑错误，此时不应修改 full_content
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;updated_content&lt;/span&gt; &lt;span class="ow"&gt;and&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;updated_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
             &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_log_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;Potential truncation detected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_log_debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Updated full content length: &lt;/span&gt;&lt;span class="si"&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;updated_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;updated_content&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mark_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_complete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_log_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stream complete. Final length: &lt;/span&gt;&lt;span class="si"&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;权衡分析：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;选择 A（全量重绘 vs 增量渲染）：&lt;/strong&gt; 全量重传给 Markdown 引擎性能开销大，但能保证格式正确（防止未闭合的 &lt;code&gt;**&lt;/code&gt; 导致整段加粗失效）；增量渲染性能好，但极易出现样式闪烁或渲染错误。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;决策：&lt;/strong&gt; 对于长文本（&amp;gt;500字），采用全量重绘配合防抖；对于短文本，可尝试增量。但在修复 Bug 阶段，优先全量重绘以确保数据完整性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;反面教训：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;只改截断不改追加：&lt;/strong&gt; 今天在修复时，错误地以为文本太长被截断，结果修改了 Split 逻辑，导致只显示第一行。实际上问题在于 &lt;code&gt;setState&lt;/code&gt; 时拿的是旧状态 &lt;code&gt;oldText&lt;/code&gt; 而不是当前的 &lt;code&gt;fullText&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;日志缺失：&lt;/strong&gt; 如果没有打印 &lt;code&gt;chunk&lt;/code&gt; 的原始字节，永远不知道是后端没发数据，还是前端丢了数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;量化指标：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;监控“流式中断率”：定义为 &lt;code&gt;(预期Token数 - 实际渲染Token数) / 预期Token数&lt;/code&gt;。修复前约 2%，修复后应降至 0.01%。&lt;/li&gt;
&lt;li&gt;端到端延迟：Full Content 渲染完成时间与 SSE 结束时间的差值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;面试官视角：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果候选人提到 &lt;strong&gt;"Markdown 的 Ast Parser 在解析不完整流时的行为"&lt;/strong&gt;（如 &lt;code&gt;*&lt;/code&gt; 单独出现会吃掉后续字符），说明他真的做过流式渲染（加分，+20）。&lt;/li&gt;
&lt;li&gt;如果候选人只说 "使用 &lt;code&gt;setInterval&lt;/code&gt; 轮询" 或者 "直接 innerHTML += chunk"，说明他没做过复杂流式场景，不懂 XSS 风险和渲染性能（扣分，-20）。&lt;/li&gt;
&lt;li&gt;常见错误回答："这是后端的问题，让后端发快点。"&lt;/li&gt;
&lt;li&gt;可以追问："如果 SSE 连接断开了，前端怎么知道是发完了还是断网了？你的 HTTP 状态码或者事件监听是怎么设计的？"&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Q2: [Title Agent 的多阶段编排与评分]
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;难度：&lt;/strong&gt; Senior&lt;br&gt;
&lt;strong&gt;领域：&lt;/strong&gt; Agent架构 / Prompt工程&lt;br&gt;
&lt;strong&gt;对应工作：&lt;/strong&gt; 今天验收了 ai-developer-knowledge-hub 项目中的 Title Agent，该 Agent 需生成 3 个候选标题、评分并选出最佳标题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;题目：&lt;/strong&gt;&lt;br&gt;
你需要设计一个 Title Generation Agent，输入是一段长文本，输出是唯一的最佳标题。为了质量，不能只让 LLM 生成一个结果。你需要设计一个工作流：先生成 N 个候选，然后对 N 个候选评分，最后选出最高分。这里有三种架构方案，你会选哪种，为什么？&lt;br&gt;
方案 A：Single Prompt，一次要求 LLM 输出 JSON &lt;code&gt;[{"title": "...", "score": 10}, ...]&lt;/code&gt;。&lt;br&gt;
方案 B：ReAct 循环，第一步 Tool Call 生成候选，第二步 Tool Call 进行评分。&lt;br&gt;
方案 C：DAG（有向无环图）编排，并行调用 3 个独立的 LLM 实例各生成 1 个标题，再聚合到一个 Judge Agent 评分。&lt;br&gt;
请给出代码架构（伪代码），并分析在 Token 成本和响应延迟上的 Trade-off。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;答案要点：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;核心思路：&lt;br&gt;
选用 &lt;strong&gt;方案 C（DAG 编排）&lt;/strong&gt;。因为生成标题是“发散性任务”，并行调用可以减少首字延迟且增加多样性；评分是“收敛性任务”，需要全局视角。A 方案容易导致评分注水或候选雷同，B 方案串行导致延迟叠加。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;技术方案（以 LangChain/LangGraph 风格伪代码）：&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;

&lt;span class="c1"&gt;# 1. 并行生成节点
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_candidates&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="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="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# 通过 Promise.all 或 ThreadPool 并行调用 3 次，System Prompt 稍作区分（如不同温度或角色）
&lt;/span&gt;    &lt;span class="n"&gt;prompts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generate a catchy title for: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generate a professional title for: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generate a keyword-focused title for: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# 并行执行
&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;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# 2. 评分节点
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rate_titles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;context&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;judge_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;
    Context: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
    Candidates: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;titles&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
    Rate each candidate 1-10 based on relevance and click-through rate.
    Return JSON with the best title.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&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;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;judge_prompt&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;parse_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# {"best_title": "...", "reasoning": "..."}
&lt;/span&gt;
&lt;span class="c1"&gt;# 3. 编排逻辑
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_title_agent&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 1: 并行生成
&lt;/span&gt;    &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_candidates&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="c1"&gt;# Step 2: 评分 (串行或并行)
&lt;/span&gt;    &lt;span class="n"&gt;best_choice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rate_titles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best_choice&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;best_title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;权衡分析：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;方案 A (Single Prompt)：&lt;/strong&gt; 成本最低（1次调用），但 LLM 对自己的作品评分通常有偏差，且很难输出结构化的对比分析。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;方案 B (ReAct)：&lt;/strong&gt; 逻辑清晰，但如果 N 很大，Latency = T_gen + T_rate + T_gen + T_rate... 线性增长不可接受。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;方案 C (DAG)：&lt;/strong&gt; Latency = Max(T_gen1, T_gen2, T_gen3) + T_rate。性能最优，质量最高（因为不同 Prompt 激发不同创造力）。成本略高（4次调用），但对于标题生成这种高频但低成本场景完全可接受。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;反面教训：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在实际开发 Title Agent 时，如果只用一个 Prompt 要求“生成3个并选最好的”，LLM 往往会偷懒，生成 3 个几乎同义的标题，导致选择无效。&lt;/li&gt;
&lt;li&gt;必须在 Prompt 中强制要求 JSON Schema，否则 Judge Agent 可能会返回自然语言，导致解析失败进入 Dead Loop。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;量化指标：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;多样性得分：&lt;/strong&gt; 3 个候选标题的编辑距离，平均应 &amp;gt; 30%。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;用户采纳率：&lt;/strong&gt; 用户不修改直接发布的比例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;面试官视角：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果候选人提到 &lt;strong&gt;"Temperature 设置差异"&lt;/strong&gt;（生成时用 0.7-1.0，评分时用 0.1），说明他懂 LLM 的参数控制（加分，+20）。&lt;/li&gt;
&lt;li&gt;如果候选人只选方案 A 并说“Prompt 写得好就行”，说明他缺乏工程化思维，忽略了 LLM 的概率特性（扣分，-10）。&lt;/li&gt;
&lt;li&gt;常见错误回答：“用 Map-Reduce。”（回答太泛，没有针对生成和评分的具体区别）。&lt;/li&gt;
&lt;li&gt;可以追问：“如果 3 个并行生成的标题都很烂，Judge Agent 必须选一个，怎么办？你的系统支持‘回炉重造’吗？”&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Q3: [流式传输中的“第一行陷阱”与边界处理]
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;难度：&lt;/strong&gt; Senior&lt;br&gt;
&lt;strong&gt;领域：&lt;/strong&gt; 生产化AI / 踩坑题&lt;br&gt;
&lt;strong&gt;对应工作：&lt;/strong&gt; 今天在修复打字机中断时，引入了一个新 Bug：修改导致只输出第一行。这是典型的“修了一个 Bug 引入两个 Bug”的场景。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;题目：&lt;/strong&gt;&lt;br&gt;
在处理流式文本追加时，为了解决“文本过长导致渲染卡顿”，你决定在前端做一个优化：只在渲染前 100 个字符，后续丢弃（或者只保留前 3 行）。结果上线后，用户发现 AI 的回答永远只有半截话。请从代码层面分析，这种“截断逻辑”错在哪里？如果必须做“性能优化”（不能全量渲染长文本），正确的做法是什么？请写出修复后的代码逻辑。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;答案要点：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;核心思路：&lt;br&gt;
错误在于混淆了 &lt;strong&gt;“状态存储”&lt;/strong&gt; 和 &lt;strong&gt;“视图渲染”&lt;/strong&gt;。状态必须永远完整，视图可以只显示一部分（如虚拟滚动或折叠）。但题目中描述的逻辑是在“追加阶段”就丢弃了数据，导致后续没有数据可追加。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;技术方案：&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- 错误做法 (今天的坑) ---&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 致命错误：直接修改了需要持久化的 text 状态&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTextState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;newText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentText&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 错误的截断逻辑：为了渲染快，把数据截断了，导致下次追加时 newText 永远只有第一行&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;newText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;setTextState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --- 正确做法 ---&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;fullTextBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 永久完整存储&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. 追加逻辑：绝对完整&lt;/span&gt;
  &lt;span class="nx"&gt;fullTextBuffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. 渲染逻辑：可以只渲染一部分，但不能破坏 Buffer&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;displayText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fullTextBuffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 性能优化示例：如果是纯文本且超长，只渲染倒数 N 个字符（模拟打字机光标处）&lt;/span&gt;
  &lt;span class="c1"&gt;// 或者使用 CSS 虚拟列表&lt;/span&gt;
  &lt;span class="cm"&gt;/*
  if (fullTextBuffer.length &amp;gt; 5000) {
     displayText = "..." + fullTextBuffer.slice(-4000); // 视觉优化，不影响数据
  }
  */&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. 状态更新&lt;/span&gt;
  &lt;span class="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;displayText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 仅用于显示&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;权衡分析：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;内存 vs 完整性：&lt;/strong&gt; 保留 &lt;code&gt;fullTextBuffer&lt;/code&gt; 会占用内存。但对于聊天气泡场景，单个文本极少超过 10k token，内存开销可忽略。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;如果必须截断：&lt;/strong&gt; 只有在明确不需要历史记录的场景（如实时日志控制台）才在接收层截断。对于 AI 对话，必须保留全文以便用户复制、重新生成或总结。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;反面教训：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;今天的回归：&lt;/strong&gt; 为了解决“显示中断”，怀疑是“太长了”，所以加了 &lt;code&gt;split('\n')[0]&lt;/code&gt;。实际上后端还在源源不断发第二行、第三行，但前端状态里永远只有第一行，新的 chunk 追加到第一行后面，变成了 &lt;code&gt;Line1Line2Line3...&lt;/code&gt; 且没有换行符，看起来就是一串乱码或只有第一行。&lt;/li&gt;
&lt;li&gt;调试时必须确认“输入源”和“状态变量”的值，而不是只看 UI。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;量化指标：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;无。此题考查的是逻辑正确性，而非性能指标。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;面试官视角：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果候选人能立刻指出 &lt;strong&gt;“数据源与视图分离”&lt;/strong&gt; 的原则，说明有扎实的架构基础（加分，+20）。&lt;/li&gt;
&lt;li&gt;如果候选人开始纠结“后端是不是发了换行符”，说明还没意识到是前端逻辑写死了（扣分，-10）。&lt;/li&gt;
&lt;li&gt;常见错误回答：“增加 buffer 大小。”（方向错了，buffer 再大，逻辑只要截断就没用）。&lt;/li&gt;
&lt;li&gt;可以追问：“如果用户在流式输出中途点击了‘停止生成’，你的状态机怎么处理？此时 Buffer 是不是完整的？”&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>interview</category>
      <category>career</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
