<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-06-18T07:09:08+00:00</updated><id>/feed.xml</id><title type="html">Site for developer algorithms</title><subtitle>Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.</subtitle><entry><title type="html">TimesFM 2.5: Khi Google dùng GPT để dự báo du lịch Đà Nẵng</title><link href="/2026/04/04/timesfm-du-bao-du-lich-da-nang.html" rel="alternate" type="text/html" title="TimesFM 2.5: Khi Google dùng GPT để dự báo du lịch Đà Nẵng" /><published>2026-04-04T00:00:00+00:00</published><updated>2026-04-04T00:00:00+00:00</updated><id>/2026/04/04/timesfm-du-bao-du-lich-da-nang</id><content type="html" xml:base="/2026/04/04/timesfm-du-bao-du-lich-da-nang.html"><![CDATA[<h1 id="timesfm-25-khi-google-dùng-gpt-để-dự-báo-du-lịch-đà-nẵng">TimesFM 2.5: Khi Google dùng GPT để dự báo du lịch Đà Nẵng</h1>

<blockquote>
  <p>Bạn biết GPT dự đoán từ tiếp theo trong câu. Nhưng nếu thay “từ” bằng “con số”, bạn sẽ có <strong>TimesFM</strong> — foundation model cho dự báo chuỗi thời gian từ Google Research. Bài viết này phân tích thuật toán, benchmark với 6 model truyền thống, và xây MVP dự báo du lịch Đà Nẵng.</p>
</blockquote>

<hr />

<h2 id="mục-lục">Mục lục</h2>

<ol>
  <li><a href="#1-timesfm-là-gì">TimesFM là gì?</a></li>
  <li><a href="#2-thuật-toán--8-bước-chi-tiết">Thuật toán — 8 bước chi tiết</a></li>
  <li><a href="#3-data-training--100-tỷ-timepoints">Data training — 100 tỷ timepoints</a></li>
  <li><a href="#4-mvp-dự-báo-du-lịch-đà-nẵng">MVP: Dự báo du lịch Đà Nẵng</a></li>
  <li><a href="#5-benchmark-timesfm-vs-6-model-truyền-thống">Benchmark: TimesFM vs 6 model truyền thống</a></li>
  <li><a href="#6-kết-luận">Kết luận</a></li>
</ol>

<hr />

<h2 id="1-timesfm-là-gì">1. TimesFM là gì?</h2>

<p><strong>TimesFM</strong> (Time Series Foundation Model) là mô hình pretrained do Google Research phát triển, công bố tại <strong>ICML 2024</strong>. Ý tưởng cốt lõi: áp dụng kiến trúc GPT (decoder-only transformer) cho bài toán dự báo chuỗi thời gian.</p>

<p><strong>Specs:</strong></p>
<ul>
  <li><strong>200M parameters</strong> (nhỏ hơn GPT-3 gần 1000 lần)</li>
  <li><strong>16K context length</strong> — nhìn được tới 16,000 điểm dữ liệu quá khứ</li>
  <li><strong>Zero-shot</strong> — không cần train lại, chạy thẳng trên data mới</li>
  <li><strong>Quantile forecast</strong> — confidence intervals 10%-90%</li>
</ul>

<p>Paper: <a href="https://arxiv.org/abs/2310.10688">A decoder-only foundation model for time-series forecasting</a></p>

<p>Repo: <a href="https://github.com/google-research/timesfm">github.com/google-research/timesfm</a></p>

<hr />

<h2 id="2-thuật-toán--8-bước-chi-tiết">2. Thuật toán — 8 bước chi tiết</h2>

<h3 id="tổng-quan-gpt-cho-time-series">Tổng quan: “GPT cho Time Series”</h3>

<p>Cách train TimesFM <strong>gần như giống hệt</strong> cách train GPT/LLM, chỉ thay <strong>token text → patch số liệu</strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GPT:     "Tôi" → "đi" → "học" → [dự đoán: "bài"]
TimesFM: [32 giá] → [32 giá] → [32 giá] → [dự đoán: 128 giá tiếp]
</code></pre></div></div>

<h3 id="bước-1-patching--tokenize-chuỗi-số">Bước 1: Patching — Tokenize chuỗi số</h3>

<p>Thay vì tokenize text thành words, TimesFM <strong>chia chuỗi thời gian thành patches</strong> (tương tự ViT chia ảnh):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Input: [y₁, y₂, ... y₅₁₂]  (512 ngày)
                ↓
Patch 1: [y₁ ... y₃₂]      ← 32 điểm = 1 "token"
Patch 2: [y₃₃ ... y₆₄]
...
Patch 16: [y₄₈₁ ... y₅₁₂]
</code></pre></div></div>

<p>Tại sao?</p>
<ul>
  <li><strong>Giảm sequence length 32×</strong> → transformer nhanh hơn nhiều</li>
  <li>Mỗi patch chứa đủ thông tin local (trend ngắn, mức noise)</li>
  <li>Output patch = 128 điểm (4× input) — “nhìn ít, đoán nhiều”</li>
</ul>

<h3 id="bước-2-input-encoding">Bước 2: Input Encoding</h3>

<p>Mỗi patch [32 số] + mask [32 bit] → concat thành vector 64 chiều → qua <strong>Residual Block</strong> (Linear→SiLU→Linear + skip connection) → vector 1280 chiều → cộng <strong>Rotary Position Embedding (RoPE)</strong>.</p>

<p>Mask đánh dấu vị trí padding — model biết chỗ nào là data thật, chỗ nào không.</p>

<h3 id="bước-3-transformer-decoder-core">Bước 3: Transformer Decoder (Core)</h3>

<p><strong>20 layers</strong>, mỗi layer gồm:</p>

<ol>
  <li><strong>RMSNorm</strong> (thay LayerNorm — nhẹ hơn)</li>
  <li><strong>Multi-Head Causal Attention</strong>: 16 heads × 80d, Fused QKV, Per-dim scaling</li>
  <li><strong>Residual connection</strong></li>
  <li><strong>FFN</strong> với SiLU activation + Residual</li>
</ol>

<p><strong>Causal Attention</strong> = điểm mấu chốt — mỗi token chỉ nhìn được các token <strong>trước</strong> nó (tương lai bị che). Giống hệt GPT.</p>

<h3 id="bước-4-output-heads">Bước 4: Output Heads</h3>

<p>Mỗi output embedding → 2 heads:</p>

<table>
  <thead>
    <tr>
      <th>Head</th>
      <th>Output</th>
      <th>Mục đích</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Point Head</strong></td>
      <td>128 values</td>
      <td>Dự đoán trung bình</td>
    </tr>
    <tr>
      <td><strong>Quantile Head</strong></td>
      <td>1024 × 10</td>
      <td>9 quantiles (10th→90th) + mean</td>
    </tr>
  </tbody>
</table>

<h3 id="bước-5-loss-function--mse">Bước 5: Loss Function — MSE</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Loss = mean(MSE₁, MSE₂, ..., MSEₙ)
</code></pre></div></div>

<p>Tính <strong>song song</strong> cho tất cả positions trong 1 forward pass. Decoder-only architecture cho phép parallelize hoàn toàn khi training.</p>

<h3 id="bước-6-random-front-masking">Bước 6: Random Front Masking</h3>

<p>Vấn đề: nếu train với patches đều đặn, model chỉ giỏi khi context = bội số 32.</p>

<p>Giải pháp: mỗi sample, random mask <code class="language-plaintext highlighter-rouge">r ∈ [0, 31]</code> điểm đầu của patch 1:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>r=4:  Patch 1 = [MASK×4, y₅...y₃₂] → thấy 28 điểm
r=20: Patch 1 = [MASK×20, y₂₁...y₃₂] → thấy 12 điểm
</code></pre></div></div>

<p>Lặp qua tất cả r → model học được <strong>mọi context length</strong>.</p>

<h3 id="bước-7-revin-reversible-instance-normalization">Bước 7: RevIN (Reversible Instance Normalization)</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Forward: normalize
</span><span class="n">normalized</span> <span class="o">=</span> <span class="p">(</span><span class="n">patch</span> <span class="o">-</span> <span class="n">μ</span><span class="p">)</span> <span class="o">/</span> <span class="n">σ</span>  <span class="c1"># running stats per patch
</span>
<span class="c1"># Transformer xử lý
</span><span class="n">output</span> <span class="o">=</span> <span class="n">Transformer</span><span class="p">(</span><span class="n">normalized</span><span class="p">)</span>

<span class="c1"># Reverse: denormalize
</span><span class="n">forecast</span> <span class="o">=</span> <span class="n">output</span> <span class="err">×</span> <span class="n">σ</span> <span class="o">+</span> <span class="n">μ</span>
</code></pre></div></div>

<p>Đảm bảo <code class="language-plaintext highlighter-rouge">f(aX + b) = a·f(X) + b</code> — model xử lý được mọi scale (giá 1,684 hay giá 27 đều OK).</p>

<h3 id="bước-8-autoregressive-decoding-inference">Bước 8: Autoregressive Decoding (Inference)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Prefill 16 patches → KV-Cache → Output 128 ngày
Nếu cần thêm: lấy output → chia patches mới → decode tiếp (dùng cache)
</code></pre></div></div>

<p>Giống LLM inference: KV-Cache tránh tính lại attention cho tokens cũ.</p>

<h3 id="so-sánh-với-gpt">So sánh với GPT</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>GPT</th>
      <th>TimesFM</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Input</td>
      <td>Text tokens</td>
      <td>Patches 32 số</td>
    </tr>
    <tr>
      <td>Output</td>
      <td>1 token/step</td>
      <td><strong>128 values/step</strong></td>
    </tr>
    <tr>
      <td>Attention</td>
      <td>Causal</td>
      <td>Causal</td>
    </tr>
    <tr>
      <td>Position</td>
      <td>RoPE</td>
      <td>RoPE</td>
    </tr>
    <tr>
      <td>Norm</td>
      <td>LayerNorm</td>
      <td><strong>RMSNorm + RevIN</strong></td>
    </tr>
    <tr>
      <td>Loss</td>
      <td>Cross-entropy</td>
      <td><strong>MSE</strong></td>
    </tr>
    <tr>
      <td>Params</td>
      <td>175B</td>
      <td><strong>200M</strong></td>
    </tr>
    <tr>
      <td>Training</td>
      <td>Text internet</td>
      <td><strong>Time series 100B pts</strong></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="3-data-training--100-tỷ-timepoints">3. Data training — 100 tỷ timepoints</h2>

<p>TimesFM được pretrain trên ~100 tỷ timepoints từ 3 nguồn chính:</p>

<h3 id="google-trends-05b-timepoints">Google Trends (~0.5B timepoints)</h3>
<ul>
  <li>~22K search queries phổ biến nhất</li>
  <li>Hourly, daily, weekly, monthly (2007-2022)</li>
  <li>Pattern: seasonal spikes, trends, mean-reversion</li>
</ul>

<h3 id="wikipedia-pageviews-300b-timepoints--nguồn-lớn-nhất">Wikipedia Pageviews (~300B timepoints) — Nguồn lớn nhất</h3>
<ul>
  <li>Lượt xem tất cả trang Wikimedia</li>
  <li>Jan 2012 – Nov 2023</li>
  <li>Pattern: daily/weekly seasonality + event-driven spikes</li>
</ul>

<h3 id="synthetic-data-3-triệu-chuỗi--2048-points">Synthetic Data (3 triệu chuỗi × 2048 points)</h3>
<ul>
  <li>ARMA processes</li>
  <li>Seasonal (tổ hợp sin/cos)</li>
  <li>Trends (linear, exponential + change points)</li>
  <li>Step functions</li>
</ul>

<h3 id="ý-nghĩa-cho-ứng-dụng">Ý nghĩa cho ứng dụng</h3>

<p>TimesFM <strong>không</strong> được train trên financial data hay dữ liệu du lịch cụ thể. Nhưng vì Google Trends data là nguồn training chính, nên khi chúng ta dùng Google Trends làm input → <strong>đúng domain</strong> → accuracy cao.</p>

<hr />

<h2 id="4-mvp-dự-báo-du-lịch-đà-nẵng">4. MVP: Dự báo du lịch Đà Nẵng</h2>

<h3 id="bài-toán">Bài toán</h3>

<p>Đà Nẵng đón <strong>12.8 triệu lượt khách</strong> trong 8 tháng đầu 2025. Chủ khách sạn, nhà hàng, tour operator cần biết: <strong>tuần/tháng tới khách đông hay vắng?</strong></p>

<h3 id="giải-pháp">Giải pháp</h3>

<p>Kết hợp 4 nguồn dữ liệu miễn phí + TimesFM 2.5:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Google Trends (daily/weekly) ─┐
Open-Meteo Weather ───────────┤──&gt; TimesFM 2.5 ──&gt; Forecast
Event Calendar ───────────────┤                        │
Cục Thống kê ĐN (monthly) ───┘                   Dashboard
</code></pre></div></div>

<h3 id="google-trends--leading-indicator">Google Trends — Leading Indicator</h3>

<p>Tôi track 11 search queries liên quan đến du lịch Đà Nẵng:</p>

<p><strong>Tiếng Việt (nội địa):</strong></p>
<ul>
  <li>“khách sạn đà nẵng”, “du lịch đà nẵng”, “vé máy bay đà nẵng”</li>
  <li>“bà nà hills”, “biển mỹ khê”</li>
</ul>

<p><strong>Tiếng Anh (quốc tế):</strong></p>
<ul>
  <li>“da nang hotel”, “da nang travel”, “flight to danang”</li>
  <li>“danang”, “danang hotel”, “vietnam beach”</li>
</ul>

<h3 id="kết-quả-forecast-12-tuần-tới">Kết quả forecast (12 tuần tới)</h3>

<p><img src="/assets/images/timesfm/forecast_da_nang_hotel.png" alt="Forecast khách sạn Đà Nẵng" />
<em>“Khách sạn đà nẵng” — Search interest tăng 80.5% trong 12 tuần tới. Seasonal pattern rõ ràng: peak mùa hè.</em></p>

<p><img src="/assets/images/timesfm/forecast_visitors.png" alt="Forecast lượng khách" />
<em>Lượng khách hàng tháng — Từ peak 1,970K (T8/2025) dự báo giảm về ~1,296K (mùa mưa). Đúng seasonal pattern.</em></p>

<h3 id="nhận-định-từ-model">Nhận định từ model</h3>

<table>
  <thead>
    <tr>
      <th>Query</th>
      <th>Xu hướng</th>
      <th>Insight</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>🟢 khách sạn đà nẵng</td>
      <td><strong>+80.5%</strong></td>
      <td>Nội địa tăng rất mạnh</td>
    </tr>
    <tr>
      <td>🟢 vé máy bay đà nẵng</td>
      <td>+32.3%</td>
      <td>Nhu cầu đi lại tăng</td>
    </tr>
    <tr>
      <td>🟢 bà nà hills</td>
      <td>+31.9%</td>
      <td>Điểm đến hot</td>
    </tr>
    <tr>
      <td>🟢 du lịch đà nẵng</td>
      <td>+28.0%</td>
      <td>Trend chung tăng</td>
    </tr>
    <tr>
      <td>🔴 da nang travel (EN)</td>
      <td>-29.8%</td>
      <td>Khách quốc tế giảm nhẹ</td>
    </tr>
    <tr>
      <td>🔴 vietnam beach</td>
      <td>-21.3%</td>
      <td>Có thể do căng thẳng địa chính trị</td>
    </tr>
  </tbody>
</table>

<p><strong>Insight:</strong> Khách nội địa đang dẫn dắt tăng trưởng. Khách quốc tế giảm nhẹ — có thể do bất ổn địa chính trị toàn cầu (căng thẳng Iran). Khuyến nghị: focus marketing nội địa, tăng giá phòng mùa hè, tuyển thêm seasonal staff.</p>

<p><img src="/assets/images/timesfm/dashboard.png" alt="Dashboard" />
<em>Dashboard tổng hợp: 11 queries + lượng khách + thời tiết Đà Nẵng</em></p>

<hr />

<h2 id="5-benchmark-timesfm-vs-6-model-truyền-thống">5. Benchmark: TimesFM vs 6 model truyền thống</h2>

<p>Để đánh giá công bằng, tôi chạy <strong>walk-forward backtest</strong> (3 splits, horizon 6 tháng) trên data lượng khách hàng tháng:</p>

<p><img src="/assets/images/timesfm/benchmark_visitors.png" alt="Benchmark" /></p>

<h3 id="kết-quả">Kết quả</h3>

<table>
  <thead>
    <tr>
      <th>Rank</th>
      <th>Model</th>
      <th>MAE</th>
      <th>MAPE</th>
      <th>Dir Acc</th>
      <th>Thời gian</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>🥇</td>
      <td><strong>Holt-Winters</strong></td>
      <td>113.35</td>
      <td>12.05%</td>
      <td>80%</td>
      <td>123ms</td>
    </tr>
    <tr>
      <td>🥈</td>
      <td><strong>Seasonal Naive</strong></td>
      <td>116.67</td>
      <td>11.60%</td>
      <td>80%</td>
      <td>0ms</td>
    </tr>
    <tr>
      <td>🥉</td>
      <td>Moving Average</td>
      <td>141.67</td>
      <td>13.38%</td>
      <td>20%</td>
      <td>0ms</td>
    </tr>
    <tr>
      <td>4</td>
      <td><strong>TimesFM 2.5</strong></td>
      <td>162.11</td>
      <td>14.53%</td>
      <td><strong>80%</strong></td>
      <td>229ms</td>
    </tr>
    <tr>
      <td>5</td>
      <td>Auto-ARIMA</td>
      <td>188.83</td>
      <td>16.99%</td>
      <td>40%</td>
      <td>905ms</td>
    </tr>
    <tr>
      <td>6</td>
      <td>Naive</td>
      <td>225.00</td>
      <td>20.14%</td>
      <td>20%</td>
      <td>0ms</td>
    </tr>
    <tr>
      <td>7</td>
      <td>Prophet</td>
      <td>359.43</td>
      <td>37.64%</td>
      <td>40%</td>
      <td>544ms</td>
    </tr>
  </tbody>
</table>

<h3 id="phân-tích">Phân tích</h3>

<p><strong>Holt-Winters thắng</strong> — hợp lý vì:</p>
<ul>
  <li>Data có seasonal pattern <strong>cực kỳ rõ</strong> (mùa hè vs mùa mưa)</li>
  <li>Chỉ 32 datapoints (monthly) — ít → model truyền thống chuyên seasonality chiếm ưu thế</li>
  <li>Holt-Winters được <strong>thiết kế đúng</strong> cho bài toán trend + seasonality</li>
</ul>

<p><strong>TimesFM 2.5 xếp thứ 4</strong> nhưng đáng chú ý:</p>
<ul>
  <li><strong>Directional Accuracy = 80%</strong> — ngang top, đoán đúng hướng tăng/giảm</li>
  <li><strong>Zero-shot hoàn toàn</strong> — không cần config, tuning, hay chọn hyperparameters</li>
  <li>Đánh bại <strong>Auto-ARIMA</strong> (cần grid search) và <strong>Prophet</strong> (Meta)</li>
  <li>Gap 43% MAE vs best — chấp nhận được cho model không train trên domain</li>
</ul>

<p><strong>Prophet tệ nhất</strong> — 32 datapoints quá ít cho Prophet hoạt động tốt.</p>

<h3 id="khi-nào-dùng-gì">Khi nào dùng gì?</h3>

<table>
  <thead>
    <tr>
      <th>Điều kiện</th>
      <th>Model tốt nhất</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Monthly, seasonal rõ, ít điểm</td>
      <td><strong>Holt-Winters</strong></td>
    </tr>
    <tr>
      <td>Daily/weekly, nhiều điểm (&gt;200)</td>
      <td><strong>TimesFM 2.5</strong></td>
    </tr>
    <tr>
      <td>Không biết gì về data, cần nhanh</td>
      <td><strong>TimesFM 2.5</strong> (zero-shot)</td>
    </tr>
    <tr>
      <td>Cần interpretability</td>
      <td><strong>ARIMA</strong></td>
    </tr>
    <tr>
      <td>Multi-seasonality + holidays</td>
      <td><strong>Prophet</strong> (cần &gt;2 năm data)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="6-kết-luận">6. Kết luận</h2>

<h3 id="timesfm-25--đánh-giá">TimesFM 2.5 — Đánh giá</h3>

<p><strong>Điểm mạnh:</strong></p>
<ul>
  <li>🟢 Zero-shot: chạy ngay, không cần train hay tuning</li>
  <li>🟢 Đa năng: 1 model cho mọi loại time series</li>
  <li>🟢 Quantile forecast: confidence intervals miễn phí</li>
  <li>🟢 Fast: ~2s cho 11 queries trên MacBook Air M1</li>
</ul>

<p><strong>Điểm yếu:</strong></p>
<ul>
  <li>🔴 Thua model truyền thống trên data ít điểm + seasonal rõ</li>
  <li>🔴 Không train trên financial data → stock forecast yếu</li>
  <li>🔴 Cần PyTorch + HuggingFace (nặng hơn statsmodels)</li>
</ul>

<h3 id="mvp-du-lịch-đà-nẵng--lessons-learned">MVP Du lịch Đà Nẵng — Lessons Learned</h3>

<ol>
  <li><strong>Google Trends = leading indicator tuyệt vời</strong> — khách search trước khi đến</li>
  <li><strong>TimesFM + Google Trends = match hoàn hảo</strong> — đúng domain training</li>
  <li><strong>Luôn benchmark</strong> — đừng assume model mới = tốt hơn</li>
  <li><strong>Kết hợp</strong> — dùng TimesFM cho daily trends (mạnh), Holt-Winters cho monthly visitors (mạnh hơn)</li>
</ol>

<h3 id="source-code">Source Code</h3>

<ul>
  <li><strong>MVP:</strong> <a href="https://github.com/phanngoc/danang-tourism-forecast">github.com/phanngoc/danang-tourism-forecast</a></li>
  <li><strong>TimesFM:</strong> <a href="https://github.com/google-research/timesfm">github.com/google-research/timesfm</a></li>
  <li><strong>Paper:</strong> <a href="https://arxiv.org/abs/2310.10688">arxiv.org/abs/2310.10688</a></li>
</ul>

<hr />

<p><em>Viết bởi Ngoc Phan — Đà Nẵng, 04/2026</em></p>]]></content><author><name></name></author><category term="TimesFM" /><category term="Google Research" /><category term="Time Series" /><category term="Forecasting" /><category term="Da Nang" /><category term="Tourism" /><category term="AI" /><summary type="html"><![CDATA[Phân tích thuật toán TimesFM 2.5 — foundation model cho time series forecasting từ Google Research, và ứng dụng thực tế dự báo du lịch Đà Nẵng.]]></summary></entry><entry><title type="html">Mổ xẻ DeerFlow 2.0 — Super Agent Harness #1 GitHub Trending của ByteDance</title><link href="/2026/03/26/deerflow-architecture-analysis.html" rel="alternate" type="text/html" title="Mổ xẻ DeerFlow 2.0 — Super Agent Harness #1 GitHub Trending của ByteDance" /><published>2026-03-26T01:00:00+00:00</published><updated>2026-03-26T01:00:00+00:00</updated><id>/2026/03/26/deerflow-architecture-analysis</id><content type="html" xml:base="/2026/03/26/deerflow-architecture-analysis.html"><![CDATA[<blockquote>
  <p><strong>TL;DR:</strong> DeerFlow 2.0 là rewrite hoàn toàn của ByteDance, đạt #1 GitHub Trending ngày 28/2/2026 với 46k ⭐. Kiến trúc xoay quanh <strong>middleware pipeline composable</strong>, <strong>memory có schema rõ ràng</strong>, và <strong>subagent executor với ThreadPool + timeout</strong>. Bài này mổ xẻ từng phần từ source code thực tế, sau đó so sánh với OpenClaw để hiểu khi nào nên dùng cái nào.</p>
</blockquote>

<hr />

<h2 id="mục-lục">Mục lục</h2>

<ol>
  <li><a href="#tổng-quan">Tổng quan</a></li>
  <li><a href="#phần-1-middleware-pipeline">Middleware Pipeline</a></li>
  <li><a href="#phần-2-memory-system">Memory System</a></li>
  <li><a href="#phần-3-subagent-executor">Subagent Executor</a></li>
  <li><a href="#phần-4-threadstate">ThreadState — Custom LangGraph State</a></li>
  <li><a href="#phần-5-deerflow-vs-openclaw">DeerFlow vs OpenClaw</a></li>
  <li><a href="#lessons-learned">Lessons Learned</a></li>
  <li><a href="#nguồn--stack">Nguồn &amp; Stack</a></li>
</ol>

<hr />

<h2 id="tổng-quan">Tổng quan</h2>

<p>DeerFlow v1 là một Deep Research framework đơn giản. V2 là rewrite hoàn toàn — không share một dòng code nào — thành một <strong>Super Agent Harness</strong> có thể:</p>

<ul>
  <li>Orchestrate multiple <strong>sub-agents</strong> chạy song song</li>
  <li>Maintain <strong>long-term memory</strong> với schema structured</li>
  <li>Execute code trong <strong>isolated sandbox</strong></li>
  <li>Nhận lệnh qua <strong>Telegram, Slack, Feishu</strong></li>
  <li>Load <strong>extensible skills</strong> (tương tự plugin)</li>
</ul>

<p>Stack: Python 3.12 + LangChain + LangGraph + FastAPI. Frontend: Next.js.</p>

<hr />

<h2 id="phần-1-middleware-pipeline">Phần 1: Middleware Pipeline</h2>

<p>Thay vì hardcode mọi logic vào agent, DeerFlow dùng <strong>middleware chain</strong> có thứ tự rõ ràng:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Từ lead_agent/agent.py — thứ tự này không phải ngẫu nhiên
</span><span class="n">middlewares</span> <span class="o">=</span> <span class="p">[</span>
    <span class="n">ThreadDataMiddleware</span><span class="p">(),</span>        <span class="c1"># 1. inject workspace paths
</span>    <span class="n">UploadsMiddleware</span><span class="p">(),</span>           <span class="c1"># 2. handle file uploads (cần thread_id từ #1)
</span>    <span class="n">DanglingToolCallMiddleware</span><span class="p">(),</span>  <span class="c1"># 3. patch orphan ToolMessages
</span>    <span class="n">SummarizationMiddleware</span><span class="p">(),</span>     <span class="c1"># 4. compress context SỚM để giảm tokens
</span>    <span class="n">TodoMiddleware</span><span class="p">(),</span>              <span class="c1"># 5. plan mode (track tasks)
</span>    <span class="n">TokenUsageMiddleware</span><span class="p">(),</span>        <span class="c1"># 6. track costs
</span>    <span class="n">TitleMiddleware</span><span class="p">(),</span>             <span class="c1"># 7. auto-generate thread title
</span>    <span class="n">MemoryMiddleware</span><span class="p">(),</span>            <span class="c1"># 8. async memory update (sau TitleMiddleware)
</span>    <span class="n">ViewImageMiddleware</span><span class="p">(),</span>         <span class="c1"># 9. vision (nếu model support)
</span>    <span class="n">DeferredToolFilterMiddleware</span><span class="p">(),</span><span class="c1"># 10. tool search
</span>    <span class="n">SubagentLimitMiddleware</span><span class="p">(),</span>     <span class="c1"># 11. cap parallel subagents
</span>    <span class="n">LoopDetectionMiddleware</span><span class="p">(),</span>     <span class="c1"># 12. phá loop
</span>    <span class="n">ClarificationMiddleware</span><span class="p">(),</span>     <span class="c1"># 13. CUỐI CÙNG: hỏi user
</span><span class="p">]</span>
</code></pre></div></div>

<p>Mỗi middleware implement hook <code class="language-plaintext highlighter-rouge">after_model()</code> hoặc <code class="language-plaintext highlighter-rouge">after_agent()</code>. Cực kỳ dễ thêm/bỏ feature mà không đụng vào core agent logic.</p>

<h3 id="loopdetectionmiddleware--p0-safety">LoopDetectionMiddleware — P0 Safety</h3>

<p>Đây là middleware ấn tượng nhất. Agent AI hay bị stuck gọi cùng một tool liên tục — middleware này phá vòng lặp:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_hash_tool_calls</span><span class="p">(</span><span class="n">tool_calls</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">dict</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="s">"""MD5 của (tool_name + args), order-independent."""</span>
    <span class="n">normalized</span> <span class="o">=</span> <span class="p">[{</span><span class="s">"name"</span><span class="p">:</span> <span class="n">tc</span><span class="p">[</span><span class="s">"name"</span><span class="p">],</span> <span class="s">"args"</span><span class="p">:</span> <span class="n">tc</span><span class="p">[</span><span class="s">"args"</span><span class="p">]}</span> <span class="k">for</span> <span class="n">tc</span> <span class="ow">in</span> <span class="n">tool_calls</span><span class="p">]</span>
    <span class="n">normalized</span><span class="p">.</span><span class="n">sort</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">tc</span><span class="p">:</span> <span class="p">(</span><span class="n">tc</span><span class="p">[</span><span class="s">"name"</span><span class="p">],</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">tc</span><span class="p">[</span><span class="s">"args"</span><span class="p">],</span> <span class="n">sort_keys</span><span class="o">=</span><span class="bp">True</span><span class="p">)))</span>
    <span class="n">blob</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">normalized</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">hashlib</span><span class="p">.</span><span class="n">md5</span><span class="p">(</span><span class="n">blob</span><span class="p">.</span><span class="n">encode</span><span class="p">()).</span><span class="n">hexdigest</span><span class="p">()[:</span><span class="mi">12</span><span class="p">]</span>
</code></pre></div></div>

<p>Logic 3 mức:</p>
<ol>
  <li><strong>Bình thường</strong> — pass qua</li>
  <li><strong><code class="language-plaintext highlighter-rouge">&gt;= warn_threshold</code> (mặc định 3)</strong> → inject cảnh báo vào <code class="language-plaintext highlighter-rouge">HumanMessage</code></li>
  <li><strong><code class="language-plaintext highlighter-rouge">&gt;= hard_limit</code> (mặc định 5)</strong> → strip toàn bộ <code class="language-plaintext highlighter-rouge">tool_calls</code>, force text answer</li>
</ol>

<blockquote>
  <p><strong>Tại sao dùng <code class="language-plaintext highlighter-rouge">HumanMessage</code> thay vì <code class="language-plaintext highlighter-rouge">SystemMessage</code>?</strong> Anthropic không cho phép SystemMessage ở giữa conversation. Dùng HumanMessage work với mọi provider.</p>
</blockquote>

<p>Sliding window 20 calls, LRU evict 100 threads — production-ready.</p>

<hr />

<h2 id="phần-2-memory-system">Phần 2: Memory System</h2>

<p>Memory không phải flat string mà có <strong>schema JSON structured</strong>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"user"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"workContext"</span><span class="p">:</span><span class="w">     </span><span class="p">{</span><span class="w"> </span><span class="nl">"summary"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"updatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"personalContext"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"summary"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"updatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"topOfMind"</span><span class="p">:</span><span class="w">       </span><span class="p">{</span><span class="w"> </span><span class="nl">"summary"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"updatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"history"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"recentMonths"</span><span class="p">:</span><span class="w">       </span><span class="p">{</span><span class="w"> </span><span class="nl">"summary"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"updatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"earlierContext"</span><span class="p">:</span><span class="w">     </span><span class="p">{</span><span class="w"> </span><span class="nl">"summary"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"updatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"longTermBackground"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"summary"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"updatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"facts"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="flow-memory-update">Flow memory update</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Agent hoàn thành turn
    ↓
MemoryMiddleware.after_agent()
    ↓
_filter_messages_for_memory()
  ✅ giữ: HumanMessage, AIMessage (không có tool_calls)
  ❌ bỏ: ToolMessage, AIMessage với tool_calls, &lt;uploaded_files&gt; blocks
    ↓
MemoryUpdateQueue.add(thread_id, filtered_messages)
    ↓ debounce timer
    ↓ batch nhiều threads update gần nhau
    ↓
MemoryUpdater.update_memory()  ← LLM summarize async
    ↓
Write JSON file (global hoặc per-agent)
</code></pre></div></div>

<p>Điểm tinh tế: <strong><code class="language-plaintext highlighter-rouge">&lt;uploaded_files&gt;</code> blocks bị strip</strong> trước khi vào memory vì file paths là session-scoped. Nhưng user’s actual question vẫn được giữ lại.</p>

<h3 id="debounce-queue">Debounce Queue</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MemoryUpdateQueue</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">thread_id</span><span class="p">,</span> <span class="n">messages</span><span class="p">,</span> <span class="n">agent_name</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
        <span class="c1"># Nếu thread đã có pending update → replace bằng cái mới nhất
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">_queue</span> <span class="o">=</span> <span class="p">[</span><span class="n">c</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">_queue</span> <span class="k">if</span> <span class="n">c</span><span class="p">.</span><span class="n">thread_id</span> <span class="o">!=</span> <span class="n">thread_id</span><span class="p">]</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">_queue</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">_reset_timer</span><span class="p">()</span>  <span class="c1"># reset debounce
</span></code></pre></div></div>

<p>Elegant — nhiều conversations trong cùng debounce window được batch, và luôn dùng state mới nhất của mỗi thread.</p>

<hr />

<h2 id="phần-3-subagent-executor">Phần 3: Subagent Executor</h2>

<h3 id="architecture">Architecture</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Lead Agent
  └─ gọi tool "task" → spawn subagent
        ↓
SubagentLimitMiddleware  ← hard cap 2-4 concurrent
        ↓
SubagentExecutor
  ├─ filter tools (allowlist/denylist per config)
  ├─ resolve model ("inherit" từ parent hoặc model riêng)
  └─ create minimal agent (thinking=False để tiết kiệm token)
        ↓
2 execution modes:
  ├─ execute()       → synchronous
  └─ execute_async() → background
       _scheduler_pool (3 workers): lifecycle + timeout
       _execution_pool (3 workers): actual execution
</code></pre></div></div>

<h3 id="async-execution-với-real-time-streaming">Async execution với real-time streaming</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">_aexecute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">task</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">result_holder</span><span class="p">):</span>
    <span class="n">agent</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_create_agent</span><span class="p">()</span>
    <span class="c1"># stream thay vì invoke → capture từng AIMessage real-time
</span>    <span class="k">async</span> <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">agent</span><span class="p">.</span><span class="n">astream</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">stream_mode</span><span class="o">=</span><span class="s">"values"</span><span class="p">):</span>
        <span class="n">last_msg</span> <span class="o">=</span> <span class="n">chunk</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"messages"</span><span class="p">,</span> <span class="p">[])[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
        <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">last_msg</span><span class="p">,</span> <span class="n">AIMessage</span><span class="p">):</span>
            <span class="k">if</span> <span class="ow">not</span> <span class="nb">any</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"id"</span><span class="p">)</span> <span class="o">==</span> <span class="n">last_msg</span><span class="p">.</span><span class="nb">id</span> <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">result</span><span class="p">.</span><span class="n">ai_messages</span><span class="p">):</span>
                <span class="n">result</span><span class="p">.</span><span class="n">ai_messages</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">last_msg</span><span class="p">.</span><span class="n">model_dump</span><span class="p">())</span>
</code></pre></div></div>

<h3 id="timeout-handling">Timeout handling</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">execution_future</span> <span class="o">=</span> <span class="n">_execution_pool</span><span class="p">.</span><span class="n">submit</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">execute</span><span class="p">,</span> <span class="n">task</span><span class="p">,</span> <span class="n">result_holder</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
    <span class="n">exec_result</span> <span class="o">=</span> <span class="n">execution_future</span><span class="p">.</span><span class="n">result</span><span class="p">(</span><span class="n">timeout</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">config</span><span class="p">.</span><span class="n">timeout_seconds</span><span class="p">)</span>
<span class="k">except</span> <span class="n">FuturesTimeoutError</span><span class="p">:</span>
    <span class="n">result</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="n">SubagentStatus</span><span class="p">.</span><span class="n">TIMED_OUT</span>
    <span class="n">execution_future</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span>  <span class="c1"># best-effort
</span></code></pre></div></div>

<h3 id="tránh-đệ-quy-vô-hạn">Tránh đệ quy vô hạn</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">SubagentConfig</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"bash"</span><span class="p">,</span>
    <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="s">"bash"</span><span class="p">,</span> <span class="s">"ls"</span><span class="p">,</span> <span class="s">"read_file"</span><span class="p">,</span> <span class="s">"write_file"</span><span class="p">,</span> <span class="s">"str_replace"</span><span class="p">],</span>
    <span class="n">disallowed_tools</span><span class="o">=</span><span class="p">[</span><span class="s">"task"</span><span class="p">,</span> <span class="s">"ask_clarification"</span><span class="p">],</span>  <span class="c1"># subagent không spawn subagent
</span>    <span class="n">model</span><span class="o">=</span><span class="s">"inherit"</span><span class="p">,</span>
    <span class="n">max_turns</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Một dòng <code class="language-plaintext highlighter-rouge">disallowed_tools=["task"]</code> ngăn infinite recursion. Trace ID được parent truyền xuống subagent, giúp follow log chain mà không cần distributed tracing phức tạp.</p>

<hr />

<h2 id="phần-4-threadstate">Phần 4: ThreadState</h2>

<p>Custom LangGraph State với reducers thông minh:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ThreadState</span><span class="p">(</span><span class="n">AgentState</span><span class="p">):</span>
    <span class="n">sandbox</span><span class="p">:</span> <span class="n">NotRequired</span><span class="p">[</span><span class="n">SandboxState</span> <span class="o">|</span> <span class="bp">None</span><span class="p">]</span>
    <span class="n">thread_data</span><span class="p">:</span> <span class="n">NotRequired</span><span class="p">[</span><span class="n">ThreadDataState</span> <span class="o">|</span> <span class="bp">None</span><span class="p">]</span>
    <span class="n">title</span><span class="p">:</span> <span class="n">NotRequired</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span><span class="p">]</span>

    <span class="c1"># Custom reducer: deduplicate artifacts
</span>    <span class="n">artifacts</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">merge_artifacts</span><span class="p">]</span>

    <span class="c1"># Custom reducer: empty dict = CLEAR (không phải merge!)
</span>    <span class="n">viewed_images</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">ViewedImageData</span><span class="p">],</span> <span class="n">merge_viewed_images</span><span class="p">]</span>

    <span class="n">todos</span><span class="p">:</span> <span class="n">NotRequired</span><span class="p">[</span><span class="nb">list</span> <span class="o">|</span> <span class="bp">None</span><span class="p">]</span>
    <span class="n">uploaded_files</span><span class="p">:</span> <span class="n">NotRequired</span><span class="p">[</span><span class="nb">list</span><span class="p">[</span><span class="nb">dict</span><span class="p">]</span> <span class="o">|</span> <span class="bp">None</span><span class="p">]</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">viewed_images</code> dùng convention <code class="language-plaintext highlighter-rouge">{}</code> để signal “clear all” — ViewImageMiddleware inject ảnh vào state, xử lý xong thì clear bằng <code class="language-plaintext highlighter-rouge">return {"viewed_images": {}}</code>.</p>

<hr />

<h2 id="phần-5-deerflow-vs-openclaw">Phần 5: DeerFlow vs OpenClaw</h2>

<p>Sau khi mổ xẻ DeerFlow, mình thấy nó có nhiều điểm tương đồng với <strong>OpenClaw</strong> — cả hai đều là agent harness với skills/channels. Nhưng triết lý thiết kế khác nhau rõ ràng.</p>

<h3 id="bảng-so-sánh">Bảng so sánh</h3>

<table>
  <thead>
    <tr>
      <th>Khía cạnh</th>
      <th>DeerFlow 2.0</th>
      <th>OpenClaw</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Memory</strong></td>
      <td>JSON schema 6 fields + LLM summarize async</td>
      <td>MEMORY.md markdown free-form</td>
    </tr>
    <tr>
      <td><strong>Memory update</strong></td>
      <td>Debounce queue + batch</td>
      <td>Manual / heartbeat periodic</td>
    </tr>
    <tr>
      <td><strong>Subagent spawn</strong></td>
      <td>Tool <code class="language-plaintext highlighter-rouge">task</code> → ThreadPool</td>
      <td><code class="language-plaintext highlighter-rouge">sessions_spawn</code> → ACP runtime</td>
    </tr>
    <tr>
      <td><strong>Subagent limit</strong></td>
      <td>Hard cap 2-4 concurrent</td>
      <td>Unlimited</td>
    </tr>
    <tr>
      <td><strong>Loop detection</strong></td>
      <td>MD5 hash + sliding window ✅</td>
      <td>❌ không có</td>
    </tr>
    <tr>
      <td><strong>Trace ID</strong></td>
      <td>Parent → child propagation</td>
      <td>Session ID per run</td>
    </tr>
    <tr>
      <td><strong>Channels</strong></td>
      <td>Telegram, Slack, Feishu</td>
      <td>Telegram, Discord, Signal, WhatsApp</td>
    </tr>
    <tr>
      <td><strong>Scheduling</strong></td>
      <td>❌ không có</td>
      <td>Cron + heartbeat 24/7</td>
    </tr>
    <tr>
      <td><strong>Skills</strong></td>
      <td>Extensible (clawhub-style)</td>
      <td>clawhub.com marketplace</td>
    </tr>
    <tr>
      <td><strong>Core framework</strong></td>
      <td>LangChain + LangGraph</td>
      <td>Custom (Node.js)</td>
    </tr>
    <tr>
      <td><strong>Frontend</strong></td>
      <td>Next.js UI ✅</td>
      <td>CLI / Telegram UI</td>
    </tr>
    <tr>
      <td><strong>Multi-user</strong></td>
      <td>✅ designed for teams</td>
      <td>❌ single-user personal</td>
    </tr>
    <tr>
      <td><strong>Coding agent</strong></td>
      <td>Sandbox execution</td>
      <td>Spawn Codex/Claude Code in thread</td>
    </tr>
  </tbody>
</table>

<h3 id="triết-lý-khác-nhau">Triết lý khác nhau</h3>

<p><strong>DeerFlow</strong> là <strong>“task-centric”</strong> — bạn đưa task, nó plan → execute → deliver. Tối ưu cho research, coding, report generation. Think: <em>“Do this complex thing for me.”</em></p>

<p><strong>OpenClaw</strong> là <strong>“ambient-centric”</strong> — agent chạy nền, chủ động monitor, proactive interrupt khi cần. Tối ưu cho personal productivity. Think: <em>“Be my always-on assistant.”</em></p>

<h3 id="use-case-thực-tế">Use case thực tế</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bạn cần...                          → Dùng
──────────────────────────────────────────────────────
Research report dài (1-2 giờ)       → DeerFlow
Deploy code với CI/CD               → DeerFlow
Multi-user workspace cho team       → DeerFlow
Nhắn Telegram lúc 7am hỏi tin       → OpenClaw
Nhắc meeting sau 30 phút            → OpenClaw
Monitor PR và ping khi merge        → OpenClaw
Chạy cron job kiểm tra server       → OpenClaw
</code></pre></div></div>

<h3 id="điều-deerflow-làm-tốt-hơn">Điều DeerFlow làm tốt hơn</h3>

<p><strong>LoopDetectionMiddleware</strong> là thứ OpenClaw không có nhưng nên có. Khi agent gọi tool lặp đi lặp lại, DeerFlow tự detect và force-stop sau 5 lần — critical cho production.</p>

<p><strong>Memory schema</strong> granular hơn — phân biệt <code class="language-plaintext highlighter-rouge">workContext</code> vs <code class="language-plaintext highlighter-rouge">personalContext</code> vs <code class="language-plaintext highlighter-rouge">topOfMind</code> giúp LLM recall đúng loại thông tin.</p>

<h3 id="điều-openclaw-làm-tốt-hơn">Điều OpenClaw làm tốt hơn</h3>

<p><strong>Heartbeat system</strong> — OpenClaw tự động check email, calendar, mentions và proactive notify. DeerFlow hoàn toàn reactive.</p>

<p><strong>Channel diversity</strong> — Signal, WhatsApp, Discord ngoài Telegram.</p>

<p><strong>clawhub.com</strong> — Skills marketplace cho phép install features không cần code.</p>

<h3 id="verdict">Verdict</h3>

<blockquote>
  <p>Nếu build <strong>SaaS có AI feature</strong> → DeerFlow SDK.<br />
Nếu cần <strong>personal assistant 24/7</strong> → OpenClaw.<br />
Nếu có budget → Cả hai: OpenClaw làm “daily driver”, DeerFlow làm “heavy worker”.</p>
</blockquote>

<hr />

<h2 id="lessons-learned">Lessons Learned</h2>

<p><strong>1. Middleware pattern &gt; monolithic agent</strong><br />
Dễ add/remove behavior, dễ test từng middleware riêng lẻ, dễ debug. Pattern này giống Express.js middleware — ai đã làm Node.js sẽ thấy quen ngay.</p>

<p><strong>2. Memory cần schema, không phải free-form</strong><br />
<code class="language-plaintext highlighter-rouge">workContext</code>, <code class="language-plaintext highlighter-rouge">personalContext</code>, <code class="language-plaintext highlighter-rouge">topOfMind</code> tách biệt → LLM summarize đúng chỗ → recall chính xác hơn. Free-form markdown linh hoạt hơn nhưng recall kém hơn.</p>

<p><strong>3. Loop detection là must-have cho production</strong><br />
Hash tool calls + sliding window là approach đơn giản mà hiệu quả. Không có cái này, agent có thể bị stuck vô hạn gây chi phí không kiểm soát.</p>

<p><strong>4. Debounce queue cho memory update</strong><br />
Không cần update memory sau mỗi message. Batch + debounce giảm API calls đáng kể, đồng thời luôn giữ state mới nhất.</p>

<p><strong>5. Subagent không được spawn subagent</strong><br />
<code class="language-plaintext highlighter-rouge">disallowed_tools=["task"]</code> — một dòng config ngăn infinite recursion. Đơn giản nhưng dễ bỏ sót khi tự build.</p>

<hr />

<h2 id="nguồn--stack">Nguồn &amp; Stack</h2>

<p><strong>Source code:</strong> <code class="language-plaintext highlighter-rouge">git clone https://github.com/bytedance/deer-flow</code> (phân tích commit tháng 3/2026)</p>

<p><strong>Stack DeerFlow:</strong></p>
<ul>
  <li>Python 3.12 · LangChain · LangGraph · FastAPI · Next.js</li>
  <li>Tested với: Claude Sonnet 4.6 via <a href="https://github.com/router-for-me/CLIProxyAPI">CLIProxyAPI</a> OAuth (Max subscription, không cần API key trực tiếp)</li>
</ul>]]></content><author><name></name></author><category term="ai-agent" /><category term="langchain" /><category term="langgraph" /><category term="architecture" /><category term="bytedance" /><category term="python" /><category term="memory" /><category term="subagent" /><summary type="html"><![CDATA[TL;DR: DeerFlow 2.0 là rewrite hoàn toàn của ByteDance, đạt #1 GitHub Trending ngày 28/2/2026 với 46k ⭐. Kiến trúc xoay quanh middleware pipeline composable, memory có schema rõ ràng, và subagent executor với ThreadPool + timeout. Bài này mổ xẻ từng phần từ source code thực tế, sau đó so sánh với OpenClaw để hiểu khi nào nên dùng cái nào.]]></summary></entry><entry><title type="html">claude-mem: Xây dựng Pipeline Trích Xuất Bộ Nhớ Chính Xác cho AI Agent</title><link href="/2026/03/21/claude-mem-memory-extraction-pipeline.html" rel="alternate" type="text/html" title="claude-mem: Xây dựng Pipeline Trích Xuất Bộ Nhớ Chính Xác cho AI Agent" /><published>2026-03-21T00:00:00+00:00</published><updated>2026-03-21T00:00:00+00:00</updated><id>/2026/03/21/claude-mem-memory-extraction-pipeline</id><content type="html" xml:base="/2026/03/21/claude-mem-memory-extraction-pipeline.html"><![CDATA[<h1 id="claude-mem-xây-dựng-pipeline-trích-xuất-bộ-nhớ-chính-xác-cho-ai-agent">claude-mem: Xây dựng Pipeline Trích Xuất Bộ Nhớ Chính Xác cho AI Agent</h1>

<blockquote>
  <p>Bài viết phân tích cách <strong>claude-mem</strong> giải quyết bài toán cốt lõi của AI agent: làm thế nào để “nhớ” chính xác những gì quan trọng qua nhiều session, trong khi vẫn giữ context nhỏ gọn và tìm kiếm hiệu quả. Đây là bài học thực tế từ việc reverse-engineer và benchmark toàn bộ pipeline.</p>
</blockquote>

<hr />

<h2 id="mục-lục">Mục lục</h2>

<ol>
  <li><a href="#1-vấn-đề-cần-giải-quyết">Vấn đề cần giải quyết</a></li>
  <li><a href="#2-kiến-trúc-tổng-quan">Kiến trúc tổng quan — 7 phases</a></li>
  <li><a href="#3-phase-1-hook">Phase 1: Hook vào tool activity</a></li>
  <li><a href="#4-phase-2-3-xml">Phase 2-3: Observer XML generation</a></li>
  <li><a href="#5-phase-4-sqlite">Phase 4: Atomic SQLite storage + FTS5</a></li>
  <li><a href="#6-phase-5-6-chroma">Phase 5-6: ChromaDB semantic sync</a></li>
  <li><a href="#7-phase-7-search">Phase 7: Hybrid search + context injection</a></li>
  <li><a href="#8-schema">Schema SQLite — 7 migrations</a></li>
  <li><a href="#9-benchmark">Benchmark thực tế</a></li>
  <li><a href="#10-key-decisions">Key design decisions</a></li>
</ol>

<hr />

<h2 id="1-vấn-đề-cần-giải-quyết">1. Vấn đề cần giải quyết</h2>

<p>Claude Code làm việc trong sessions ngắn. Mỗi session restart, toàn bộ context biến mất. Các giải pháp naive:</p>

<ul>
  <li><strong>Replay toàn bộ transcript</strong>: 50 tool calls × 300 tokens = 15,000 tokens. Tốn kém, nhiễu.</li>
  <li><strong>Tóm tắt thủ công</strong>: Phụ thuộc vào user, không đáng tin.</li>
  <li><strong>Lưu raw logs</strong>: Fast nhưng không searchable, không hierarchical.</li>
</ul>

<p><strong>claude-mem</strong> giải quyết bằng cách ép một <em>observer agent</em> <strong>chọn lọc và extract</strong> những gì thực sự quan trọng thành schema cố định — và inject context đó vào session tiếp theo với <strong>39x ít tokens hơn</strong>.</p>

<hr />

<h2 id="2-kiến-trúc-tổng-quan">2. Kiến trúc tổng quan</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Tool Call (raw, noisy)
  ↓ PostToolUse hook
HTTP POST /api/sessions/observations
  ↓ SessionQueueProcessor
buildObservationPrompt()  →  XML prompt
  ↓ @anthropic-ai/claude-agent-sdk
Observer Claude (no tools, read-only)
  ↓ selectivity filter
&lt;observation&gt; XML  |  "no observation needed"
  ↓ parseObservations()
ParsedObservation struct
  ↓ storeObservation() — atomic, dedup
SQLite (observations table)  →  FTS5 auto-trigger
  ↓ async fire-and-forget
ChromaDB (vector embeddings)
  ↓
GET /api/context/inject
  ↓ HybridSearch (FTS5 + Chroma)
Top-N context  →  Next session
</code></pre></div></div>

<p>7 phases, mỗi phase giải quyết 1 vấn đề cụ thể.</p>

<hr />

<h2 id="3-phase-1-hook-vào-tool-activity">3. Phase 1: Hook vào tool activity</h2>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// PostToolUse hook trong Claude Code</span>
<span class="nx">HTTP</span> <span class="nx">POST</span> <span class="o">/</span><span class="nx">api</span><span class="o">/</span><span class="nx">sessions</span><span class="o">/</span><span class="nx">observations</span>
<span class="p">{</span>
  <span class="nl">contentSessionId</span><span class="p">:</span> <span class="dl">"</span><span class="s2">user-abc123</span><span class="dl">"</span><span class="p">,</span>   <span class="c1">// user's session</span>
  <span class="nx">tool_name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Write</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">tool_input</span><span class="p">:</span> <span class="p">{</span> <span class="nl">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">server/db.js</span><span class="dl">"</span> <span class="p">},</span>
  <span class="nx">tool_response</span><span class="p">:</span> <span class="dl">"</span><span class="s2">File written (728 insertions)</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">cwd</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/projects/kanban-mvp</span><span class="dl">"</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Key insight:</strong> Dùng 2 loại session ID riêng biệt:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">contentSessionId</code> → gắn với user’s work (stable)</li>
  <li><code class="language-plaintext highlighter-rouge">memorySessionId</code> → gắn với observer agent (thay đổi khi restart)</li>
</ul>

<p>Observer crash và restart không mất data vì tất cả observations đều gắn với <code class="language-plaintext highlighter-rouge">contentSessionId</code>.</p>

<hr />

<h2 id="4-phase-2-3-observer-xml-generation">4. Phase 2-3: Observer XML generation</h2>

<p>Observer Claude nhận prompt được format từ tool call:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;observed_from_primary_session&gt;</span>
  <span class="nt">&lt;what_happened&gt;</span>Write<span class="nt">&lt;/what_happened&gt;</span>
  <span class="nt">&lt;parameters&gt;</span>{"path": "server/db.js"}<span class="nt">&lt;/parameters&gt;</span>
  <span class="nt">&lt;outcome&gt;</span>"File written (728 insertions, 10 files changed)"<span class="nt">&lt;/outcome&gt;</span>
  <span class="nt">&lt;working_directory&gt;</span>/projects/kanban-mvp<span class="nt">&lt;/working_directory&gt;</span>
<span class="nt">&lt;/observed_from_primary_session&gt;</span>
</code></pre></div></div>

<p>Observer được config <strong>không có tools</strong> (observer-only) và respond bằng XML có schema cố định:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;observation&gt;</span>
  <span class="nt">&lt;type&gt;</span>feature<span class="nt">&lt;/type&gt;</span>
  <span class="nt">&lt;title&gt;</span>Issue Version History Database Schema<span class="nt">&lt;/title&gt;</span>
  <span class="nt">&lt;subtitle&gt;</span>Added SQLite table and snapshot function<span class="nt">&lt;/subtitle&gt;</span>
  <span class="nt">&lt;facts&gt;</span>
    <span class="nt">&lt;fact&gt;</span>issue_history table với composite PRIMARY KEY (repo_key, number, version)<span class="nt">&lt;/fact&gt;</span>
    <span class="nt">&lt;fact&gt;</span>snapshotIssue() function capture state trước mỗi edit<span class="nt">&lt;/fact&gt;</span>
    <span class="nt">&lt;fact&gt;</span>Version numbers auto-increment per issue<span class="nt">&lt;/fact&gt;</span>
    <span class="nt">&lt;fact&gt;</span>728 insertions across 10 files<span class="nt">&lt;/fact&gt;</span>
  <span class="nt">&lt;/facts&gt;</span>
  <span class="nt">&lt;narrative&gt;</span>Implemented foundational infrastructure for version history tracking.<span class="nt">&lt;/narrative&gt;</span>
  <span class="nt">&lt;concepts&gt;</span>
    <span class="nt">&lt;concept&gt;</span>architecture<span class="nt">&lt;/concept&gt;</span>
    <span class="nt">&lt;concept&gt;</span>how-it-works<span class="nt">&lt;/concept&gt;</span>
  <span class="nt">&lt;/concepts&gt;</span>
  <span class="nt">&lt;files_modified&gt;</span>
    <span class="nt">&lt;file&gt;</span>server/db.js<span class="nt">&lt;/file&gt;</span>
  <span class="nt">&lt;/files_modified&gt;</span>
<span class="nt">&lt;/observation&gt;</span>
</code></pre></div></div>

<p><strong>Selectivity quan trọng:</strong> Observer tự quyết định skip noise:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">git log --oneline -3</code> → <em>“routine git status, no observation needed”</em></li>
  <li><code class="language-plaintext highlighter-rouge">Write server/db.js (728 insertions)</code> → <strong>captured</strong> với 4 facts cụ thể</li>
  <li><code class="language-plaintext highlighter-rouge">Write tests/e2e/06-history.spec.cjs</code> → <strong>captured</strong> với 21 tests, full coverage info</li>
</ul>

<hr />

<h2 id="5-phase-4-atomic-sqlite-storage--fts5">5. Phase 4: Atomic SQLite storage + FTS5</h2>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// observations/store.ts</span>
<span class="kd">function</span> <span class="nx">storeObservation</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="nx">memorySessionId</span><span class="p">,</span> <span class="nx">project</span><span class="p">,</span> <span class="nx">observation</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// 1. Content-hash deduplication (30s window)</span>
  <span class="kd">const</span> <span class="nx">hash</span> <span class="o">=</span> <span class="nx">sha256</span><span class="p">(</span><span class="nx">memorySessionId</span> <span class="o">+</span> <span class="nx">title</span> <span class="o">+</span> <span class="nx">narrative</span><span class="p">).</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span>
  <span class="kd">const</span> <span class="nx">existing</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span>
    <span class="dl">'</span><span class="s1">SELECT id FROM observations WHERE content_hash = ? AND created_at_epoch &gt; ?</span><span class="dl">'</span>
  <span class="p">).</span><span class="kd">get</span><span class="p">(</span><span class="nx">hash</span><span class="p">,</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="mi">30</span><span class="nx">_000</span><span class="p">)</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">existing</span><span class="p">)</span> <span class="k">return</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="nx">existing</span><span class="p">.</span><span class="nx">id</span> <span class="p">}</span>  <span class="c1">// skip duplicate</span>

  <span class="c1">// 2. Atomic INSERT</span>
  <span class="nx">db</span><span class="p">.</span><span class="nx">prepare</span><span class="p">(</span><span class="s2">`
    INSERT INTO observations
    (memory_session_id, project, type, title, subtitle,
     facts, narrative, concepts, files_modified,
     discovery_tokens, content_hash, created_at_epoch)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `</span><span class="p">).</span><span class="nx">run</span><span class="p">(</span>
    <span class="nx">memorySessionId</span><span class="p">,</span> <span class="nx">project</span><span class="p">,</span> <span class="kd">type</span><span class="p">,</span> <span class="nx">title</span><span class="p">,</span> <span class="nx">subtitle</span><span class="p">,</span>
    <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">facts</span><span class="p">),</span> <span class="nx">narrative</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">concepts</span><span class="p">),</span>
    <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">files_modified</span><span class="p">),</span>
    <span class="nx">discoveryTokens</span><span class="p">,</span> <span class="nx">hash</span><span class="p">,</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span>
  <span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Ngay sau INSERT, <strong>FTS5 trigger tự động fire</strong>:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Auto-generated trigger từ migration006</span>
<span class="k">CREATE</span> <span class="k">TRIGGER</span> <span class="n">observations_ai</span> <span class="k">AFTER</span> <span class="k">INSERT</span> <span class="k">ON</span> <span class="n">observations</span> <span class="k">BEGIN</span>
  <span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">observations_fts</span><span class="p">(</span><span class="n">rowid</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">subtitle</span><span class="p">,</span> <span class="n">narrative</span><span class="p">,</span> <span class="nb">text</span><span class="p">,</span> <span class="n">facts</span><span class="p">,</span> <span class="n">concepts</span><span class="p">)</span>
  <span class="k">VALUES</span> <span class="p">(</span><span class="k">new</span><span class="p">.</span><span class="n">id</span><span class="p">,</span> <span class="k">new</span><span class="p">.</span><span class="n">title</span><span class="p">,</span> <span class="k">new</span><span class="p">.</span><span class="n">subtitle</span><span class="p">,</span> <span class="k">new</span><span class="p">.</span><span class="n">narrative</span><span class="p">,</span> <span class="k">new</span><span class="p">.</span><span class="nb">text</span><span class="p">,</span> <span class="k">new</span><span class="p">.</span><span class="n">facts</span><span class="p">,</span> <span class="k">new</span><span class="p">.</span><span class="n">concepts</span><span class="p">);</span>
<span class="k">END</span><span class="p">;</span>
</code></pre></div></div>

<p>Kết quả: SQLite query &lt;1ms ở 10,000 observations, WAL mode, zero config.</p>

<hr />

<h2 id="6-phase-5-6-chromadb-semantic-sync">6. Phase 5-6: ChromaDB semantic sync</h2>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Async, fire-and-forget — không block storage</span>
<span class="nx">ChromaSync</span><span class="p">.</span><span class="nx">syncObservation</span><span class="p">(</span><span class="nx">obsId</span><span class="p">)</span>

<span class="c1">// Build document text (concatenate fields for embedding)</span>
<span class="kd">const</span> <span class="nx">doc</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">title</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">subtitle</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">facts</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="p">)}</span><span class="s2"> </span><span class="p">${</span><span class="nx">narrative</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">concepts</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="p">)}</span><span class="s2">`</span>

<span class="c1">// Gọi chroma-mcp subprocess (Python 3.11)</span>
<span class="k">await</span> <span class="nx">chromaMcp</span><span class="p">.</span><span class="nx">callTool</span><span class="p">(</span><span class="dl">"</span><span class="s2">chroma_add_documents</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">collection_name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">kanban-mvp_observations</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">documents</span><span class="p">:</span> <span class="p">[</span><span class="nx">doc</span><span class="p">],</span>
  <span class="na">ids</span><span class="p">:</span> <span class="p">[</span><span class="s2">`obs_</span><span class="p">${</span><span class="nx">obsId</span><span class="p">}</span><span class="s2">`</span><span class="p">],</span>
  <span class="na">metadatas</span><span class="p">:</span> <span class="p">[{</span> <span class="kd">type</span><span class="p">,</span> <span class="nx">project</span><span class="p">,</span> <span class="nx">created_at_epoch</span> <span class="p">}]</span>
<span class="p">})</span>
<span class="c1">// → onnxruntime embeds: [0.023, -0.145, ...] (384 dims, all-MiniLM-L6-v2)</span>
<span class="c1">// → stored in ~/.claude-mem/chroma/</span>
</code></pre></div></div>

<p><strong>Python version gotcha:</strong> ChromaDB chỉ tương thích Python 3.11. Python 3.13/3.14 fail do onnxruntime dependency conflict. Fix:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">~/.claude-mem/settings.json</span><span class="w">
</span><span class="p">{</span><span class="w"> </span><span class="nl">"CLAUDE_MEM_PYTHON_VERSION"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3.11"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="7-phase-7-hybrid-search--context-injection">7. Phase 7: Hybrid search + context injection</h2>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// HybridSearchStrategy</span>
<span class="k">async</span> <span class="nx">search</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="nx">project</span><span class="p">,</span> <span class="nx">limit</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// 1. SQLite FTS5 — keyword match, &lt;1ms</span>
  <span class="kd">const</span> <span class="nx">ftsIds</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="s2">`
    SELECT rowid FROM observations_fts
    WHERE observations_fts MATCH ?
    ORDER BY rank  -- BM25
  `</span><span class="p">).</span><span class="nx">all</span><span class="p">(</span><span class="nx">query</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">r</span> <span class="o">=&gt;</span> <span class="nx">r</span><span class="p">.</span><span class="nx">rowid</span><span class="p">)</span>

  <span class="c1">// 2. ChromaDB — semantic similarity, ~100-500ms</span>
  <span class="kd">const</span> <span class="nx">chromaResults</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">chroma</span><span class="p">.</span><span class="nx">query</span><span class="p">({</span>
    <span class="na">collection</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">project</span><span class="p">}</span><span class="s2">_observations`</span><span class="p">,</span>
    <span class="na">query_texts</span><span class="p">:</span> <span class="p">[</span><span class="nx">query</span><span class="p">],</span>
    <span class="na">n_results</span><span class="p">:</span> <span class="nx">limit</span> <span class="o">*</span> <span class="mi">2</span>
  <span class="p">})</span>

  <span class="c1">// 3. Intersect + order by Chroma (semantic) rank</span>
  <span class="kd">const</span> <span class="nx">rankedIds</span> <span class="o">=</span> <span class="nx">chromaResults</span><span class="p">.</span><span class="nx">ids</span>
    <span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">id</span> <span class="o">=&gt;</span> <span class="nx">ftsIds</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nb">parseInt</span><span class="p">(</span><span class="nx">id</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">obs_</span><span class="dl">'</span><span class="p">,</span> <span class="dl">''</span><span class="p">))))</span>

  <span class="c1">// 4. Hydrate full rows từ SQLite</span>
  <span class="k">return</span> <span class="nx">rankedIds</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">id</span> <span class="o">=&gt;</span> <span class="nx">db</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="dl">'</span><span class="s1">SELECT * FROM observations WHERE id = ?</span><span class="dl">'</span><span class="p">).</span><span class="kd">get</span><span class="p">(</span><span class="nx">id</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Context injection output (format compact, token-efficient):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># $CMEM kanban-mvp 2026-03-21 10:19am GMT+7

Stats: 2 obs (491t read) | 705t work | 30% savings

1 10:06a 🟣 Issue Version History Database Schema
2 10:19a 🟣 E2E test suite for issue version history

Access 1k tokens of past work via get_observations([IDs]) or mem-search skill.
</code></pre></div></div>

<hr />

<h2 id="8-schema-sqlite--7-migrations">8. Schema SQLite — 7 migrations</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>migrations/
├── 001 — Core tables: sessions, memories, overviews, diagnostics, transcript_events
├── 002 — Add hierarchical fields to memories (title, subtitle, facts, concepts)
├── 003 — streaming_sessions (v2 arch — deprecated)
├── 004 — SDK agent arch: sdk_sessions, observation_queue, observations, session_summaries
├── 005 — DROP orphaned tables (streaming_sessions, observation_queue)
├── 006 — FTS5 virtual tables + auto-sync triggers
└── 007 — discovery_tokens column (ROI tracking)
</code></pre></div></div>

<p><strong>Core schema hiện tại (post-migration 007):</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sdk_sessions</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="err">────</span> <span class="p">(</span><span class="n">N</span><span class="p">)</span> <span class="n">observations</span>
<span class="n">sdk_sessions</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="err">────</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="n">session_summaries</span>
<span class="n">observations</span>   <span class="err">────</span> <span class="n">auto</span><span class="o">-</span><span class="k">trigger</span> <span class="err">────</span> <span class="n">observations_fts</span> <span class="p">(</span><span class="n">FTS5</span><span class="p">)</span>
<span class="n">observations</span>   <span class="err">────</span> <span class="n">async</span> <span class="err">────</span> <span class="n">ChromaDB</span> <span class="p">(</span><span class="n">vectors</span><span class="p">)</span>

<span class="n">observations</span><span class="p">:</span>
  <span class="k">type</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">subtitle</span><span class="p">,</span> <span class="n">facts</span> <span class="n">JSON</span><span class="p">,</span> <span class="n">narrative</span><span class="p">,</span> <span class="n">concepts</span> <span class="n">JSON</span>
  <span class="n">files_read</span> <span class="n">JSON</span><span class="p">,</span> <span class="n">files_modified</span> <span class="n">JSON</span>
  <span class="n">discovery_tokens</span><span class="p">,</span> <span class="n">content_hash</span>  <span class="err">←</span> <span class="n">dedup</span> <span class="o">+</span> <span class="n">ROI</span>
</code></pre></div></div>

<hr />

<h2 id="9-benchmark-thực-tế">9. Benchmark thực tế</h2>

<p>Đo trên macOS 14.6 M1, Bun 1.3.11, 46 test cases:</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Kết quả</th>
      <th>Target</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SQLite read p95 (10k rows)</td>
      <td><strong>1ms</strong></td>
      <td>&lt;50ms</td>
    </tr>
    <tr>
      <td>Context inject p95</td>
      <td><strong>15ms</strong></td>
      <td>&lt;500ms</td>
    </tr>
    <tr>
      <td>Observer recall</td>
      <td><strong>91%</strong></td>
      <td>&gt;80%</td>
    </tr>
    <tr>
      <td>Context relevance</td>
      <td><strong>90%</strong></td>
      <td>&gt;70%</td>
    </tr>
    <tr>
      <td>Token savings (50 calls)</td>
      <td><strong>39x</strong></td>
      <td>&gt;5x</td>
    </tr>
    <tr>
      <td>ChromaDB semantic search</td>
      <td>100–500ms</td>
      <td>functional</td>
    </tr>
  </tbody>
</table>

<p><strong>Compression reality check:</strong></p>

<table>
  <thead>
    <tr>
      <th>Session size</th>
      <th>Tokens raw</th>
      <th>Tokens injected</th>
      <th>Ratio</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1 tool call</td>
      <td>41</td>
      <td>89</td>
      <td>0.46x (overhead &gt; content)</td>
    </tr>
    <tr>
      <td>50 tool calls</td>
      <td>~15,600</td>
      <td>~400</td>
      <td><strong>39x</strong></td>
    </tr>
  </tbody>
</table>

<p>Short sessions don’t compress — by design. At scale (50+ calls với file reads), savings rất lớn.</p>

<hr />

<h2 id="10-key-design-decisions">10. Key design decisions</h2>

<h3 id="1-facts-array-thay-vì-narrative-blob">1. Facts array thay vì narrative blob</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❌ "Implemented authentication using express-session with SQLite backend 
    where tokens are stored server-side and never exposed via httpOnly cookies"

✅ facts: [
    "express-session + connect-sqlite3",
    "token không bao giờ gửi xuống browser",
    "httpOnly cookie trong production",
    "7-day session expiry (604800s)"
  ]
</code></pre></div></div>

<p>Mỗi fact độc lập, verifiable, và match được riêng biệt → recall cao hơn nhiều.</p>

<h3 id="2-dual-session-ids--tách-concerns">2. Dual session IDs — tách concerns</h3>

<p>Observer crash không mất data vì observations luôn gắn với <code class="language-plaintext highlighter-rouge">contentSessionId</code> (user’s work), không phải <code class="language-plaintext highlighter-rouge">memorySessionId</code> (observer state).</p>

<h3 id="3-chroma-là-optional-fts5-là-mandatory">3. Chroma là optional, FTS5 là mandatory</h3>

<p>Nếu Chroma down, SQLite FTS5 vẫn serve keyword search. <code class="language-plaintext highlighter-rouge">discovery_tokens</code> không phụ thuộc vào Chroma.</p>

<h3 id="4-discovery_tokens--feedback-loop">4. <code class="language-plaintext highlighter-rouge">discovery_tokens</code> — feedback loop</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>487 tokens → 4 facts = efficient
2000 tokens → 2 facts = prompt cần tune
</code></pre></div></div>

<p>Đây là signal để cải thiện observer prompt theo thời gian.</p>

<h3 id="5-observer-không-có-tools">5. Observer không có tools</h3>

<p>Strict: observer chỉ có thể read và write observations. Không thể Bash, Write, Edit. Loại bỏ hoàn toàn infinite loop risks.</p>

<hr />

<h2 id="kết-luận">Kết luận</h2>

<p>claude-mem không phải là “lưu chat history vào DB”. Đây là một <strong>structured knowledge extraction pipeline</strong> với:</p>

<ul>
  <li>Observer agent thông minh, chọn lọc</li>
  <li>Schema atomic facts (không phải narrative blob)</li>
  <li>Hybrid search (FTS5 keyword + ChromaDB semantic)</li>
  <li>Deduplication, ROI tracking, crash-safe dual IDs</li>
</ul>

<p><strong>39x token savings không phải là con số marketing</strong> — đó là kết quả đo được khi pipeline đủ mature để phân biệt signal khỏi noise.</p>

<p>Code: <a href="https://github.com/thedotmack/claude-mem">thedotmack/claude-mem</a></p>

<hr />

<p><em>Tags: SQLite, AI Memory, ChromaDB, Claude Code, RAG, Bun</em></p>]]></content><author><name></name></author><category term="SQLite" /><category term="AI" /><category term="Memory" /><category term="ChromaDB" /><category term="Claude" /><summary type="html"><![CDATA[claude-mem: Xây dựng Pipeline Trích Xuất Bộ Nhớ Chính Xác cho AI Agent]]></summary></entry><entry><title type="html">Bypass Anti-Bot Bằng Chính Trình Duyệt Thật: Kiến Trúc Chrome Extension + Local Proxy Crawl Shopee</title><link href="/2026/03/19/shopee-proxy-chrome-extension-crawler.html" rel="alternate" type="text/html" title="Bypass Anti-Bot Bằng Chính Trình Duyệt Thật: Kiến Trúc Chrome Extension + Local Proxy Crawl Shopee" /><published>2026-03-19T03:00:00+00:00</published><updated>2026-03-19T03:00:00+00:00</updated><id>/2026/03/19/shopee-proxy-chrome-extension-crawler</id><content type="html" xml:base="/2026/03/19/shopee-proxy-chrome-extension-crawler.html"><![CDATA[<blockquote>
  <p><strong>Disclaimer:</strong> Bài viết này hoàn toàn mang tính nghiên cứu kỹ thuật. Hãy tôn trọng Terms of Service của các nền tảng bạn tương tác.</p>
</blockquote>

<hr />

<p>Ở <a href="/2026/03/17/shopee-anti-bot-analysis.html">bài trước</a>, tôi đã mổ xẻ toàn bộ hệ thống anti-bot của Shopee — 6 lớp phòng thủ từ TLS fingerprint đến behavioral analysis — và kết luận rằng <strong>không có giải pháp free nào bypass được ở scale</strong>.</p>

<p>Nhưng bài đó bỏ sót một insight quan trọng: <strong>điều gì xảy ra nếu chúng ta không cần bypass bất kỳ lớp nào?</strong></p>

<p>Thay vì giả mạo browser, giả mạo hành vi người dùng, rotate proxy đắt tiền — tại sao không dùng luôn <strong>browser thật của bạn</strong>? Mọi cookie, mọi fingerprint, mọi behavioral signal đều 100% authentic. Chúng ta chỉ cần một cách để “nghe lén” các API call mà browser đang thực hiện.</p>

<p>Đó là ý tưởng đằng sau <code class="language-plaintext highlighter-rouge">shopee-proxy</code> — một hệ thống <strong>zero-bypass crawling</strong> dùng Chrome Extension làm network sniffer, local Python server làm data sink, và SQLite làm storage.</p>

<hr />

<h2 id="vấn-đề-gốc-rễ-với-mọi-crawler-truyền-thống">Vấn Đề Gốc Rễ Với Mọi Crawler Truyền Thống</h2>

<p>Trước khi vào architecture, hãy hiểu tại sao approach thông thường thất bại.</p>

<p>Khi bạn dùng <code class="language-plaintext highlighter-rouge">requests</code>, Playwright, hay Selenium để crawl Shopee, bạn đang cố gắng <strong>impersonate</strong> một browser. Shopee detect impersonation thông qua:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Your bot]
    ├── TLS fingerprint ≠ real Chrome → BLOCK
    ├── Canvas hash = known headless hash → BLOCK  
    ├── Mouse patterns = too uniform → CAPTCHA
    ├── IP = datacenter/proxy → rate limit
    └── Session tokens = stale/stolen → REVOKE
</code></pre></div></div>

<p>Mỗi lớp detection là một signal độc lập. Bypass 5/6 vẫn bị block.</p>

<p><strong>Insight:</strong> Thay vì impersonate browser, hãy <em>là</em> browser. Chỉ cần thêm một lớp ở giữa để capture data mà browser đang fetch.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Real Chrome, thật 100%]
    ├── TLS fingerprint = real Chrome ✅
    ├── Canvas hash = real GPU ✅
    ├── Mouse patterns = bạn đang dùng chuột thật ✅
    ├── IP = IP nhà bạn ✅
    └── Session tokens = thật, fresh ✅
    │
    └── [Chrome Extension] ← chúng ta chỉ thêm cái này
            │ intercept API responses
            ▼
    [Local Python Server]
            │ parse + store
            ▼
    [SQLite Database]
</code></pre></div></div>

<hr />

<h2 id="kiến-trúc-tổng-thể">Kiến Trúc Tổng Thể</h2>

<p>Hệ thống gồm 2 phần chạy song song:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────────────┐
│                    Chrome Browser                            │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                 Web Page (MAIN world)                │    │
│  │                                                      │    │
│  │  window.fetch()    ←── intercepted by injector.js   │    │
│  │  XMLHttpRequest()  ←── intercepted by injector.js   │    │
│  │                                                      │    │
│  │  Khi có API response → window.postMessage(payload)  │    │
│  └──────────────────────────┬──────────────────────────┘    │
│                             │ postMessage                    │
│  ┌──────────────────────────▼──────────────────────────┐    │
│  │            Content Script (ISOLATED world)           │    │
│  │                      bridge.js                       │    │
│  │                                                      │    │
│  │  Nhận postMessage → chrome.runtime.sendMessage()    │    │
│  └──────────────────────────┬──────────────────────────┘    │
│                             │ chrome.runtime message         │
│  ┌──────────────────────────▼──────────────────────────┐    │
│  │                  background.js                       │    │
│  │              (Service Worker, MV3)                   │    │
│  │                                                      │    │
│  │  Nhận message → đính kèm cookies → POST /capture    │    │
│  └──────────────────────────┬──────────────────────────┘    │
└───────────────────────────────────────────────────────────   │
                              │ HTTP POST localhost:9234
┌─────────────────────────────▼───────────────────────────┐
│                     proxy.py                             │
│                 (Python HTTP Server)                     │
│                                                          │
│  Nhận payload → parse recommend_v2 / search_items       │
│  → INSERT OR IGNORE INTO products                        │
│                    │                                     │
│                    ▼                                     │
│              products.db (SQLite)                        │
└──────────────────────────────────────────────────────────┘
</code></pre></div></div>

<p>4 components, mỗi cái một trách nhiệm rõ ràng. Hãy đi sâu vào từng phần.</p>

<hr />

<h2 id="component-1-injectorjs--monkey-patch-fetchxhr">Component 1: injector.js — Monkey-Patch Fetch/XHR</h2>

<p>Đây là “trái tim” của extension. File này được inject vào <strong>MAIN world</strong> của trang web — nghĩa là nó chạy trong cùng execution context với JavaScript của Shopee, có thể override các global APIs.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// injector.js — chạy trong MAIN world của trang</span>
<span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">__shopeeCapture</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// chống inject lại</span>
  <span class="nb">window</span><span class="p">.</span><span class="nx">__shopeeCapture</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>

  <span class="c1">// Intercept fetch</span>
  <span class="kd">const</span> <span class="nx">_fetch</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">fetch</span><span class="p">;</span>  <span class="c1">// lưu bản gốc</span>
  <span class="nb">window</span><span class="p">.</span><span class="nx">fetch</span> <span class="o">=</span> <span class="k">async</span> <span class="kd">function</span> <span class="p">(...</span><span class="nx">args</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">[</span><span class="nx">input</span><span class="p">,</span> <span class="nx">init</span><span class="p">]</span> <span class="o">=</span> <span class="nx">args</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="k">typeof</span> <span class="nx">input</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">?</span> <span class="nx">input</span> <span class="p">:</span> <span class="nx">input</span><span class="p">?.</span><span class="nx">url</span> <span class="o">||</span> <span class="dl">""</span><span class="p">;</span>

    <span class="c1">// Chỉ capture các API call</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isApi</span><span class="p">(</span><span class="nx">url</span><span class="p">))</span> <span class="k">return</span> <span class="nx">_fetch</span><span class="p">.</span><span class="nx">apply</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nx">args</span><span class="p">);</span>

    <span class="c1">// Gọi fetch thật</span>
    <span class="kd">const</span> <span class="nx">resp</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">_fetch</span><span class="p">.</span><span class="nx">apply</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nx">args</span><span class="p">);</span>
    
    <span class="c1">// Clone response để đọc body (response body chỉ đọc được 1 lần)</span>
    <span class="kd">const</span> <span class="nx">clone</span> <span class="o">=</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">clone</span><span class="p">();</span>
    <span class="nx">clone</span><span class="p">.</span><span class="nx">text</span><span class="p">().</span><span class="nx">then</span><span class="p">((</span><span class="nx">body</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">emit</span><span class="p">({</span>
        <span class="nx">method</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span>
        <span class="na">timestamp</span><span class="p">:</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">(),</span>
        <span class="na">requestBody</span><span class="p">:</span> <span class="nx">reqBody</span><span class="p">,</span>
        <span class="na">responseStatus</span><span class="p">:</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">status</span><span class="p">,</span>
        <span class="na">responseBody</span><span class="p">:</span> <span class="nx">body</span><span class="p">,</span>  <span class="c1">// full response body</span>
      <span class="p">});</span>
    <span class="p">});</span>

    <span class="k">return</span> <span class="nx">resp</span><span class="p">;</span>  <span class="c1">// trả về response gốc cho Shopee</span>
  <span class="p">};</span>
<span class="p">})();</span>
</code></pre></div></div>

<p><strong>Tại sao clone response?</strong> <code class="language-plaintext highlighter-rouge">Response.body</code> là ReadableStream — chỉ có thể đọc một lần. Nếu chúng ta đọc nó trước, Shopee code sẽ nhận về stream rỗng. <code class="language-plaintext highlighter-rouge">resp.clone()</code> tạo bản sao để chúng ta đọc riêng mà không ảnh hưởng đến luồng chính.</p>

<p><strong>Tương tự với XHR:</strong></p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Patch XMLHttpRequest</span>
<span class="nx">XMLHttpRequest</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">open</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">method</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span> <span class="p">...</span><span class="nx">rest</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">_cap</span> <span class="o">=</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="nx">method</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">(),</span> <span class="nx">url</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{}</span> <span class="p">};</span>
  <span class="k">return</span> <span class="nx">_open</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nx">method</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span> <span class="p">...</span><span class="nx">rest</span><span class="p">);</span>
<span class="p">};</span>

<span class="nx">XMLHttpRequest</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">send</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">body</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_cap</span> <span class="o">&amp;&amp;</span> <span class="nx">isApi</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_cap</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">load</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
      <span class="nx">emit</span><span class="p">({</span>
        <span class="na">url</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_cap</span><span class="p">.</span><span class="nx">url</span><span class="p">,</span>
        <span class="na">responseBody</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">responseText</span><span class="p">,</span>  <span class="c1">// XHR thì dễ hơn</span>
        <span class="c1">// ...</span>
      <span class="p">});</span>
    <span class="p">});</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">_send</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nx">body</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>

<hr />

<h2 id="component-2-chrome-extension-world-isolation">Component 2: Chrome Extension World Isolation</h2>

<p>Chrome Extension MV3 có một security model quan trọng cần hiểu: <strong>world isolation</strong>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────┐
│             Chrome Tab                   │
│                                          │
│  ┌─────────────────────────────────┐    │
│  │    MAIN world                   │    │
│  │    - Shopee's JavaScript        │    │
│  │    - window.fetch (original)    │    │
│  │    - injector.js ← inject vào đây│   │
│  └──────────────┬──────────────────┘    │
│                 │ window.postMessage     │
│  ┌──────────────▼──────────────────┐    │
│  │    ISOLATED world               │    │
│  │    - Content scripts (bridge.js)│    │
│  │    - Có chrome.* API            │    │
│  │    - Không có window.fetch thật │    │
│  └──────────────┬──────────────────┘    │
│                 │ chrome.runtime.sendMessage
└─────────────────│───────────────────────┘
                  │
┌─────────────────▼───────────────────────┐
│  Service Worker (background.js)          │
│  - Chạy ngoài tab                        │
│  - Có chrome.cookies API                 │
│  - Có fetch() để call localhost          │
└──────────────────────────────────────────┘
</code></pre></div></div>

<p><strong>Tại sao cần bridge.js?</strong></p>

<p>Content scripts chạy trong ISOLATED world — họ không thể đọc/ghi <code class="language-plaintext highlighter-rouge">window</code> của trang web. Nhưng cả MAIN world lẫn ISOLATED world đều nhận <code class="language-plaintext highlighter-rouge">window.postMessage</code> từ cùng origin.</p>

<p>Đây là cách data flow:</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">injector.js</code> (MAIN) gọi <code class="language-plaintext highlighter-rouge">window.postMessage({ type: "__SHOPEE_CAPTURE__", payload })</code></li>
  <li><code class="language-plaintext highlighter-rouge">bridge.js</code> (ISOLATED) lắng nghe <code class="language-plaintext highlighter-rouge">window.addEventListener("message", ...)</code></li>
  <li><code class="language-plaintext highlighter-rouge">bridge.js</code> forward qua <code class="language-plaintext highlighter-rouge">chrome.runtime.sendMessage()</code></li>
  <li><code class="language-plaintext highlighter-rouge">background.js</code> (Service Worker) nhận và POST lên localhost</li>
</ol>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// bridge.js — đơn giản nhưng critical</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">message</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">source</span> <span class="o">!==</span> <span class="nb">window</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// chỉ nhận từ cùng tab</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">data</span><span class="p">?.</span><span class="nx">type</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">__SHOPEE_CAPTURE__</span><span class="dl">"</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// filter đúng type</span>
  
  <span class="k">try</span> <span class="p">{</span>
    <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">sendMessage</span><span class="p">({</span> <span class="na">action</span><span class="p">:</span> <span class="dl">"</span><span class="s2">capture</span><span class="dl">"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="nx">event</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">payload</span> <span class="p">});</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Extension context có thể bị invalidate khi reload extension</span>
    <span class="c1">// Ignore để tránh crash</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<hr />

<h2 id="component-3-backgroundjs--cookie-enrichment--http-forward">Component 3: background.js — Cookie Enrichment + HTTP Forward</h2>

<p>Background script có quyền truy cập <code class="language-plaintext highlighter-rouge">chrome.cookies</code> API — thứ mà content script không có. Trước khi POST lên local server, nó đính kèm toàn bộ cookies của <code class="language-plaintext highlighter-rouge">.shopee.vn</code>:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// background.js</span>
<span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">onMessage</span><span class="p">.</span><span class="nx">addListener</span><span class="p">((</span><span class="nx">message</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">action</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">capture</span><span class="dl">"</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

  <span class="c1">// Lấy toàn bộ cookies shopee.vn</span>
  <span class="nx">chrome</span><span class="p">.</span><span class="nx">cookies</span><span class="p">.</span><span class="nx">getAll</span><span class="p">({</span> <span class="na">domain</span><span class="p">:</span> <span class="dl">"</span><span class="s2">.shopee.vn</span><span class="dl">"</span> <span class="p">},</span> <span class="p">(</span><span class="nx">cookies</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">cookieStr</span> <span class="o">=</span> <span class="nx">cookies</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">c</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="s2">`</span><span class="p">${</span><span class="nx">c</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">=</span><span class="p">${</span><span class="nx">c</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">`</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">; </span><span class="dl">"</span><span class="p">);</span>
    
    <span class="kd">const</span> <span class="nx">enriched</span> <span class="o">=</span> <span class="p">{</span> 
      <span class="p">...</span><span class="nx">message</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>    <span class="c1">// original payload (url, method, body, headers)</span>
      <span class="na">cookies</span><span class="p">:</span> <span class="nx">cookieStr</span>  <span class="c1">// thêm cookies vào</span>
    <span class="p">};</span>

    <span class="c1">// POST lên local proxy server</span>
    <span class="nx">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://localhost:9234/capture</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span> <span class="p">},</span>
      <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">enriched</span><span class="p">),</span>
    <span class="p">});</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<p><strong>Một điểm thú vị về SPA navigation:</strong> Shopee là Single Page Application dùng React. Khi bạn navigate giữa các trang, URL thay đổi nhưng không có full page reload. injector.js có thể bị mất sau navigation.</p>

<p>background.js handle điều này bằng cách lắng nghe <code class="language-plaintext highlighter-rouge">chrome.webNavigation.onHistoryStateUpdated</code> — event này fire mỗi khi SPA thay đổi URL qua History API:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Re-inject khi SPA navigate</span>
<span class="nx">chrome</span><span class="p">.</span><span class="nx">webNavigation</span><span class="p">?.</span><span class="nx">onHistoryStateUpdated</span><span class="p">?.</span><span class="nx">addListener</span><span class="p">(</span>
  <span class="p">(</span><span class="nx">details</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">details</span><span class="p">.</span><span class="nx">frameId</span> <span class="o">!==</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
    <span class="nx">chrome</span><span class="p">.</span><span class="nx">scripting</span><span class="p">.</span><span class="nx">executeScript</span><span class="p">({</span>
      <span class="na">target</span><span class="p">:</span> <span class="p">{</span> <span class="na">tabId</span><span class="p">:</span> <span class="nx">details</span><span class="p">.</span><span class="nx">tabId</span> <span class="p">},</span>
      <span class="na">files</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">injector.js</span><span class="dl">"</span><span class="p">],</span>
      <span class="na">world</span><span class="p">:</span> <span class="dl">"</span><span class="s2">MAIN</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">});</span>
  <span class="p">},</span>
  <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="p">[{</span> <span class="na">hostContains</span><span class="p">:</span> <span class="dl">"</span><span class="s2">shopee.vn</span><span class="dl">"</span> <span class="p">}]</span> <span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="component-4-proxypy--parse-deduplicate-store">Component 4: proxy.py — Parse, Deduplicate, Store</h2>

<p>Python HTTP server nhận POST requests từ extension và parse data ra 2 API format khác nhau.</p>

<h3 id="shopee-có-2-api-trả-về-sản-phẩm">Shopee có 2 API trả về sản phẩm:</h3>

<p><strong>1. <code class="language-plaintext highlighter-rouge">recommend/recommend_v2</code></strong> — dùng cho category pages, có structure phức tạp hơn:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">parse_recommend_product</span><span class="p">(</span><span class="n">unit</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="n">item</span> <span class="o">=</span> <span class="n">unit</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"item"</span><span class="p">,</span> <span class="p">{})</span>
    <span class="n">asset</span> <span class="o">=</span> <span class="n">item</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"item_card_displayed_asset"</span><span class="p">,</span> <span class="p">{})</span>  <span class="c1"># display data
</span>    <span class="n">item_data</span> <span class="o">=</span> <span class="n">item</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"item_data"</span><span class="p">,</span> <span class="p">{})</span>  <span class="c1"># raw data
</span>
    <span class="c1"># Lấy item_id từ tracking_card_id
</span>    <span class="c1"># Format: "TYPE::SHOPID::ITEMID"
</span>    <span class="n">tid</span> <span class="o">=</span> <span class="n">unit</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"tracking_card_id"</span><span class="p">,</span> <span class="s">""</span><span class="p">)</span>
    <span class="n">item_id</span> <span class="o">=</span> <span class="n">tid</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"::"</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="k">if</span> <span class="s">"::"</span> <span class="ow">in</span> <span class="n">tid</span> <span class="k">else</span> <span class="s">""</span>

    <span class="c1"># Price được store dưới dạng integer * 100000
</span>    <span class="n">price_raw</span> <span class="o">=</span> <span class="n">asset</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"display_price"</span><span class="p">,</span> <span class="p">{}).</span><span class="n">get</span><span class="p">(</span><span class="s">"price"</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
    <span class="n">price</span> <span class="o">=</span> <span class="n">price_raw</span> <span class="o">/</span> <span class="mi">100000</span>  <span class="c1"># convert về VND thật
</span>
    <span class="k">return</span> <span class="p">{</span>
        <span class="s">"item_id"</span><span class="p">:</span> <span class="n">item_id</span><span class="p">,</span>
        <span class="s">"name"</span><span class="p">:</span> <span class="n">asset</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"name"</span><span class="p">,</span> <span class="s">""</span><span class="p">),</span>
        <span class="s">"price"</span><span class="p">:</span> <span class="n">price</span><span class="p">,</span>
        <span class="c1"># ...
</span>    <span class="p">}</span>
</code></pre></div></div>

<p><strong>2. <code class="language-plaintext highlighter-rouge">search/search_items</code></strong> — dùng cho search results, structure đơn giản hơn:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">parse_search_product</span><span class="p">(</span><span class="n">item</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="n">item_basic</span> <span class="o">=</span> <span class="n">item</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"item_basic"</span><span class="p">)</span> <span class="ow">or</span> <span class="n">item</span>
    <span class="n">item_id</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">item_basic</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"itemid"</span><span class="p">,</span> <span class="s">""</span><span class="p">))</span>

    <span class="n">price</span> <span class="o">=</span> <span class="n">item_basic</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"price"</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">/</span> <span class="mi">100000</span>
    
    <span class="c1"># Sold count formatting
</span>    <span class="n">historical_sold</span> <span class="o">=</span> <span class="n">item_basic</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"historical_sold"</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">historical_sold</span> <span class="o">&gt;=</span> <span class="mi">1000</span><span class="p">:</span>
        <span class="n">sold_text</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"Đã bán </span><span class="si">{</span><span class="n">historical_sold</span> <span class="o">//</span> <span class="mi">1000</span><span class="si">}</span><span class="s">k+"</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">sold_text</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"Đã bán </span><span class="si">{</span><span class="n">historical_sold</span><span class="si">}</span><span class="s">"</span> <span class="k">if</span> <span class="n">historical_sold</span> <span class="k">else</span> <span class="s">""</span>

    <span class="c1"># Seller type
</span>    <span class="k">if</span> <span class="n">item_basic</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"is_official_shop"</span><span class="p">):</span>
        <span class="n">seller_type</span> <span class="o">=</span> <span class="s">"MALL"</span>
    <span class="k">elif</span> <span class="n">item_basic</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"shopee_verified"</span><span class="p">):</span>
        <span class="n">seller_type</span> <span class="o">=</span> <span class="s">"PREFERRED"</span>

    <span class="k">return</span> <span class="p">{</span> <span class="s">"item_id"</span><span class="p">:</span> <span class="n">item_id</span><span class="p">,</span> <span class="s">"price"</span><span class="p">:</span> <span class="n">price</span><span class="p">,</span> <span class="s">"sold"</span><span class="p">:</span> <span class="n">sold_text</span><span class="p">,</span> <span class="p">...</span> <span class="p">}</span>
</code></pre></div></div>

<h3 id="sqlite-với-insert-or-ignore--deduplication-tự-động">SQLite với <code class="language-plaintext highlighter-rouge">INSERT OR IGNORE</code> — deduplication tự động:</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">init_db</span><span class="p">(</span><span class="n">db_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">sqlite3</span><span class="p">.</span><span class="n">Connection</span><span class="p">:</span>
    <span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">db_path</span><span class="p">)</span>
    <span class="n">conn</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="s">"""
        CREATE TABLE IF NOT EXISTS products (
            item_id TEXT PRIMARY KEY,  -- PRIMARY KEY đảm bảo unique
            shop_id TEXT,
            name TEXT,
            price REAL,
            original_price REAL,
            discount TEXT,
            sold TEXT,
            seller_type TEXT,
            image TEXT,
            images TEXT,       -- JSON array
            url TEXT,
            category_id TEXT,
            captured_at TEXT
        )
    """</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">conn</span>

<span class="k">def</span> <span class="nf">insert_products</span><span class="p">(</span><span class="n">products</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">dict</span><span class="p">],</span> <span class="n">cat_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">products</span><span class="p">:</span>
        <span class="n">conn</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span>
            <span class="s">"""INSERT OR IGNORE INTO products (...)  -- bỏ qua nếu item_id đã tồn tại
               VALUES (?, ?, ?, ...)"""</span><span class="p">,</span>
            <span class="p">(...),</span>
        <span class="p">)</span>
    <span class="n">conn</span><span class="p">.</span><span class="n">commit</span><span class="p">()</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">INSERT OR IGNORE</code> kết hợp với <code class="language-plaintext highlighter-rouge">PRIMARY KEY</code> trên <code class="language-plaintext highlighter-rouge">item_id</code> tự động handle deduplication — cùng một sản phẩm xuất hiện ở nhiều pages/categories chỉ được lưu một lần.</p>

<hr />

<h2 id="manifestjson--minimum-permissions">manifest.json — Minimum Permissions</h2>

<p>Extension request đúng permissions cần thiết, không hơn:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"manifest_version"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shopee Network Capture"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.2"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"permissions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"scripting"</span><span class="p">,</span><span class="w">      </span><span class="err">//</span><span class="w"> </span><span class="err">để</span><span class="w"> </span><span class="err">inject</span><span class="w"> </span><span class="err">injector.js</span><span class="w"> </span><span class="err">vào</span><span class="w"> </span><span class="err">MAIN</span><span class="w"> </span><span class="err">world</span><span class="w">
    </span><span class="s2">"tabs"</span><span class="p">,</span><span class="w">           </span><span class="err">//</span><span class="w"> </span><span class="err">để</span><span class="w"> </span><span class="err">detect</span><span class="w"> </span><span class="err">tab</span><span class="w"> </span><span class="err">navigation</span><span class="w">
    </span><span class="s2">"webNavigation"</span><span class="p">,</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">để</span><span class="w"> </span><span class="err">detect</span><span class="w"> </span><span class="err">SPA</span><span class="w"> </span><span class="err">history</span><span class="w"> </span><span class="err">changes</span><span class="w">
    </span><span class="s2">"cookies"</span><span class="w">         </span><span class="err">//</span><span class="w"> </span><span class="err">để</span><span class="w"> </span><span class="err">đọc</span><span class="w"> </span><span class="err">shopee.vn</span><span class="w"> </span><span class="err">cookies</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"host_permissions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"*://*.shopee.vn/*"</span><span class="p">],</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">chỉ</span><span class="w"> </span><span class="err">trên</span><span class="w"> </span><span class="err">shopee.vn</span><span class="w">
  </span><span class="nl">"background"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"service_worker"</span><span class="p">:</span><span class="w"> </span><span class="s2">"background.js"</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">MV</span><span class="mi">3</span><span class="err">:</span><span class="w"> </span><span class="err">service</span><span class="w"> </span><span class="err">worker</span><span class="w"> </span><span class="err">thay</span><span class="w"> </span><span class="err">cho</span><span class="w"> </span><span class="err">background</span><span class="w"> </span><span class="err">page</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"content_scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w">
    </span><span class="nl">"matches"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"*://*.shopee.vn/*"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"js"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"bridge.js"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"run_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"document_start"</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">inject</span><span class="w"> </span><span class="err">sớm</span><span class="w"> </span><span class="err">nhất</span><span class="w"> </span><span class="err">có</span><span class="w"> </span><span class="err">thể</span><span class="w">
  </span><span class="p">}]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="workflow-thực-tế">Workflow Thực Tế</h2>

<h3 id="1-khởi-động-server">1. Khởi động server</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>rich
python3 proxy.py <span class="nt">--db</span> products.db <span class="nt">--port</span> 9234
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╭─────────────────────────────────────────╮
│          Shopee Product Proxy           │
│ Server:   http://localhost:9234         │
│ Database: /path/products.db (0 products)│
│ Captures: recommend_v2 + search_items  │
╰─────────────────────────────────────────╯

Listening... Browse Shopee category pages.
</code></pre></div></div>

<h3 id="2-load-extension">2. Load Extension</h3>

<p><code class="language-plaintext highlighter-rouge">chrome://extensions</code> → Developer mode → Load unpacked → chọn thư mục <code class="language-plaintext highlighter-rouge">extension/</code></p>

<h3 id="3-browse-shopee-bình-thường">3. Browse Shopee bình thường</h3>

<p>Mở bất kỳ category page nào, scroll:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  # 1  GET 200 /api/v4/recommend/recommend_v2
       ＋48 new (0 dupes, 48 parsed, 48 in DB)
  # 2  GET 200 /api/v4/recommend/recommend_v2
       ＋0 new (48 dupes, 48 parsed, 48 in DB)
  # 3  POST 200 /api/v4/recommend/recommend_v2
       ＋52 new (3 dupes, 55 parsed, 100 in DB)
  # 4  GET 200 /api/v4/search/search_items
       ＋24 new (0 dupes, 24 parsed, 124 in DB)
</code></pre></div></div>

<h3 id="4-query-data">4. Query data</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite3 products.db <span class="s2">"
  SELECT name, printf('%.0f', price) as price, discount, sold
  FROM products
  WHERE seller_type = 'MALL'
  ORDER BY price DESC
  LIMIT 10;
"</span>
</code></pre></div></div>

<p>Hoặc trong Python:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sqlite3</span><span class="p">,</span> <span class="n">json</span>

<span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="s">"products.db"</span><span class="p">)</span>

<span class="c1"># Top sản phẩm discount cao nhất
</span><span class="n">results</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="s">"""
    SELECT name, price, original_price, discount
    FROM products
    WHERE original_price IS NOT NULL
    ORDER BY (original_price - price) DESC
    LIMIT 20
"""</span><span class="p">).</span><span class="n">fetchall</span><span class="p">()</span>

<span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">price</span><span class="p">,</span> <span class="n">orig</span><span class="p">,</span> <span class="n">disc</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">name</span><span class="p">[</span><span class="si">:</span><span class="mi">40</span><span class="p">]</span><span class="si">}</span><span class="s"> | </span><span class="si">{</span><span class="n">price</span><span class="si">:</span><span class="p">,.</span><span class="mi">0</span><span class="n">f</span><span class="si">}</span><span class="s">đ | </span><span class="si">{</span><span class="n">disc</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="xử-lý-edge-cases">Xử Lý Edge Cases</h2>

<h3 id="anti-bot-response-error-code-90309999">Anti-bot Response (error code 90309999)</h3>

<p>Shopee đôi khi trả về response với error code này khi phát hiện bất thường. proxy.py check và skip:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="s">"90309999"</span> <span class="ow">in</span> <span class="n">resp_body</span><span class="p">:</span>
    <span class="n">console</span><span class="p">.</span><span class="k">print</span><span class="p">(</span><span class="s">"[red]BLOCKED (anti-bot)[/]"</span><span class="p">)</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span>
</code></pre></div></div>

<p>Trong thực tế với approach này (browser thật), bạn rất hiếm gặp error này vì mọi request đều đến từ session hợp lệ của bạn.</p>

<h3 id="spa-navigation--re-inject">SPA Navigation — Re-inject</h3>

<p>Shopee SPA thay đổi URL mà không reload page. background.js inject lại injector.js mỗi khi detect history state change:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">chrome</span><span class="p">.</span><span class="nx">webNavigation</span><span class="p">?.</span><span class="nx">onHistoryStateUpdated</span><span class="p">?.</span><span class="nx">addListener</span><span class="p">(</span>
  <span class="p">(</span><span class="nx">details</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">chrome</span><span class="p">.</span><span class="nx">scripting</span><span class="p">.</span><span class="nx">executeScript</span><span class="p">({</span>
      <span class="na">target</span><span class="p">:</span> <span class="p">{</span> <span class="na">tabId</span><span class="p">:</span> <span class="nx">details</span><span class="p">.</span><span class="nx">tabId</span> <span class="p">},</span>
      <span class="na">files</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">injector.js</span><span class="dl">"</span><span class="p">],</span>
      <span class="na">world</span><span class="p">:</span> <span class="dl">"</span><span class="s2">MAIN</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">});</span>
  <span class="p">},</span>
  <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="p">[{</span> <span class="na">hostContains</span><span class="p">:</span> <span class="dl">"</span><span class="s2">shopee.vn</span><span class="dl">"</span> <span class="p">}]</span> <span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Guard <code class="language-plaintext highlighter-rouge">if (window.__shopeeCapture) return;</code> trong injector.js đảm bảo nếu đã inject rồi thì không patch lại (tránh double-patch fetch/XHR).</p>

<h3 id="extension-context-invalidated">Extension Context Invalidated</h3>

<p>Khi bạn reload extension trong <code class="language-plaintext highlighter-rouge">chrome://extensions</code>, Service Worker bị restart nhưng content scripts trong các tab đang mở thì không. <code class="language-plaintext highlighter-rouge">chrome.runtime.sendMessage()</code> trong bridge.js lúc này sẽ throw error.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span> <span class="p">{</span>
  <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">sendMessage</span><span class="p">({</span> <span class="na">action</span><span class="p">:</span> <span class="dl">"</span><span class="s2">capture</span><span class="dl">"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="nx">event</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">payload</span> <span class="p">});</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Extension context invalidated — bỏ qua, không crash page</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="crawlpy--standalone-extractor-từ-json-log">Crawl.py — Standalone Extractor Từ JSON Log</h2>

<p>Ngoài real-time capture, có thể export network log từ Chrome DevTools và parse offline:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># crawl.py
</span><span class="k">def</span> <span class="nf">extract_from_log</span><span class="p">(</span><span class="n">log_file</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">dict</span><span class="p">]:</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">log_file</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">data</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>

    <span class="n">all_products</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">seen_ids</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>

    <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">data</span><span class="p">:</span>
        <span class="n">url</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"url"</span><span class="p">,</span> <span class="s">""</span><span class="p">)</span>
        <span class="k">if</span> <span class="s">"recommend/recommend_v2"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">url</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="n">resp_body</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"response_body"</span><span class="p">,</span> <span class="s">""</span><span class="p">)</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">resp_body</span> <span class="ow">or</span> <span class="s">"90309999"</span> <span class="ow">in</span> <span class="n">resp_body</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="n">resp</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">resp_body</span><span class="p">)</span>
        <span class="n">units</span> <span class="o">=</span> <span class="n">resp</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"data"</span><span class="p">,</span> <span class="p">{}).</span><span class="n">get</span><span class="p">(</span><span class="s">"units"</span><span class="p">,</span> <span class="p">[])</span>
        
        <span class="k">for</span> <span class="n">u</span> <span class="ow">in</span> <span class="n">units</span><span class="p">:</span>
            <span class="n">product</span> <span class="o">=</span> <span class="n">parse_product</span><span class="p">(</span><span class="n">u</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">product</span> <span class="ow">and</span> <span class="n">product</span><span class="p">[</span><span class="s">"item_id"</span><span class="p">]</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">seen_ids</span><span class="p">:</span>
                <span class="n">seen_ids</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">product</span><span class="p">[</span><span class="s">"item_id"</span><span class="p">])</span>
                <span class="n">all_products</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">product</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">all_products</span>
</code></pre></div></div>

<p>Dùng khi bạn đã có sẵn <code class="language-plaintext highlighter-rouge">network_log.json</code> (export từ DevTools → Network → Save all as HAR):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 crawl.py <span class="nt">--log</span> network_log.json <span class="nt">--output</span> products.json
</code></pre></div></div>

<hr />

<h2 id="so-sánh-với-các-approach-khác">So Sánh Với Các Approach Khác</h2>

<table>
  <thead>
    <tr>
      <th>Approach</th>
      <th>Setup</th>
      <th>Anti-bot Risk</th>
      <th>Scale</th>
      <th>Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>requests/httpx</strong></td>
      <td>Đơn giản</td>
      <td>Rất cao</td>
      <td>Thấp</td>
      <td>Free</td>
    </tr>
    <tr>
      <td><strong>Playwright headless</strong></td>
      <td>Trung bình</td>
      <td>Cao</td>
      <td>Trung bình</td>
      <td>Free</td>
    </tr>
    <tr>
      <td><strong>Playwright + residential proxy</strong></td>
      <td>Phức tạp</td>
      <td>Thấp</td>
      <td>Cao</td>
      <td>$$$</td>
    </tr>
    <tr>
      <td><strong>Extension + local proxy (approach này)</strong></td>
      <td>Trung bình</td>
      <td>Gần bằng 0</td>
      <td>Trung bình*</td>
      <td>Free</td>
    </tr>
  </tbody>
</table>

<p><strong>*Scale limitation:</strong> Approach này yêu cầu bạn browse thủ công hoặc có browser đang chạy thật. Không tự động hoàn toàn như headless crawler. Phù hợp với data collection có giám sát, không phải autonomous scraping 24/7.</p>

<hr />

<h2 id="giới-hạn-và-khi-nào-nên-dùng">Giới Hạn Và Khi Nào Nên Dùng</h2>

<p><strong>Phù hợp khi:</strong></p>
<ul>
  <li>Cần data chất lượng cao, không lo bị block</li>
  <li>Dataset size vừa phải (vài ngàn đến vài chục ngàn sản phẩm)</li>
  <li>Có thể để browser chạy trong vài giờ</li>
  <li>Muốn lấy data từ nhiều categories khác nhau</li>
</ul>

<p><strong>Không phù hợp khi:</strong></p>
<ul>
  <li>Cần crawl 1 triệu+ sản phẩm một cách autonomous</li>
  <li>Cần schedule tự động không cần người dùng</li>
  <li>Production pipeline không có human supervision</li>
</ul>

<p><strong>Extension vào approach tốt hơn:</strong></p>
<ul>
  <li>Thêm auto-pagination: inject JavaScript để tự click “next page” sau mỗi N giây</li>
  <li>Kết hợp với category list để system hướng dẫn bạn browse đúng trang</li>
  <li>Add price tracking: so sánh price giữa các lần capture để detect price changes</li>
</ul>

<hr />

<h2 id="kết-luận">Kết Luận</h2>

<p>Đôi khi giải pháp tốt nhất không phải là đánh trực tiếp vào vấn đề — mà là <strong>đi vòng quanh nó</strong>.</p>

<p>Thay vì chiến đấu với 6 lớp anti-bot của Shopee bằng proxy đắt tiền, fingerprint spoofing, và behavior simulation phức tạp, hệ thống này tận dụng một sự thật đơn giản: <strong>browser thật của bạn đã vượt qua tất cả các lớp đó rồi</strong>. Chúng ta chỉ cần một cái “tai nghe” gắn vào.</p>

<p>Kiến trúc này dạy chúng ta một bài học quan trọng về system design: <strong>luôn hỏi “có gì sẵn có mà ta có thể tận dụng không?” trước khi bắt đầu build từ đầu</strong>.</p>

<p>Data pipeline đơn giản nhất thường là data pipeline tốt nhất.</p>

<hr />

<p><strong>Source code:</strong> <a href="https://github.com/phanngoc/shopee-proxy">github.com/phanngoc/shopee-proxy</a></p>

<p><em>Xem thêm: <a href="/2026/03/17/shopee-anti-bot-analysis.html">Giải Mã Hệ Thống Anti-Bot Của Shopee</a> — phân tích 6 lớp phòng thủ và 10 lần thất bại.</em></p>]]></content><author><name></name></author><category term="web-scraping" /><category term="chrome-extension" /><category term="shopee" /><category term="python" /><category term="sqlite" /><category term="browser-automation" /><category term="anti-bot" /><summary type="html"><![CDATA[Disclaimer: Bài viết này hoàn toàn mang tính nghiên cứu kỹ thuật. Hãy tôn trọng Terms of Service của các nền tảng bạn tương tác.]]></summary></entry><entry><title type="html">Phân tích 16,800 bài báo Tuổi Trẻ: Topic chính trị nào nổi bật từng tuần 2025-2026?</title><link href="/2026/03/18/tuoitre-political-analysis.html" rel="alternate" type="text/html" title="Phân tích 16,800 bài báo Tuổi Trẻ: Topic chính trị nào nổi bật từng tuần 2025-2026?" /><published>2026-03-18T07:00:00+00:00</published><updated>2026-03-18T07:00:00+00:00</updated><id>/2026/03/18/tuoitre-political-analysis</id><content type="html" xml:base="/2026/03/18/tuoitre-political-analysis.html"><![CDATA[<blockquote>
  <p>Tôi crawl 16,825 bài báo từ Tuổi Trẻ Online trong vòng <strong>24 phút</strong> bằng Go + Chrome DevTools Protocol, sau đó dùng TF-IDF clustering để vẽ bản đồ các chủ đề chính trị theo tuần. Kết quả khá thú vị — đặc biệt là cú shock của chiến sự Iran bắt đầu ngày 28/2/2026.</p>
</blockquote>

<hr />

<h2 id="tại-sao-làm-việc-này">Tại sao làm việc này?</h2>

<p>Tôi muốn có một cách để <strong>nhanh chóng nắm bắt xu hướng chính trị</strong> qua các tuần mà không cần đọc từng bài. Câu hỏi đặt ra: <em>Tuần này Tuổi Trẻ đang viết nhiều nhất về gì? Topic nào vừa bùng nổ?</em></p>

<p>Thay vì đọc thủ công, tôi xây pipeline:</p>
<ol>
  <li>Crawl toàn bộ bài báo → SQLite</li>
  <li>TF-IDF vectorize → K-Means cluster thành 20 topic</li>
  <li>Vẽ heatmap, timeline, entity tracking</li>
</ol>

<hr />

<h2 id="architecture-bypass-ip-block-bằng-chromedp">Architecture: Bypass IP Block bằng chromedp</h2>

<p>Vấn đề đầu tiên: Tuổi Trẻ block comment API với server IP (cookie <code class="language-plaintext highlighter-rouge">__stat="BLOCK"</code>). Giải pháp: dùng <strong>Chrome DevTools Protocol</strong> để chạy <code class="language-plaintext highlighter-rouge">fetch()</code> <em>bên trong browser thật</em> — browser dùng cookies thật của mình, bypass hoàn toàn.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// chromedp: chạy JS fetch() trong browser session thật</span>
<span class="n">js</span> <span class="o">:=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">`
  (async () =&gt; {
    const resp = await fetch(
      "https://id.tuoitre.vn/api/getlist-comment.api?appKey=%s&amp;objectId=%s&amp;...",
      { credentials: "include" }  // dùng cookies của browser
    );
    return JSON.stringify(await resp.json());
  })()
`</span><span class="p">,</span> <span class="n">appKey</span><span class="p">,</span> <span class="n">objectID</span><span class="p">)</span>

<span class="k">var</span> <span class="n">result</span> <span class="k">interface</span><span class="p">{}</span>
<span class="n">chromedp</span><span class="o">.</span><span class="n">Run</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">chromedp</span><span class="o">.</span><span class="n">Evaluate</span><span class="p">(</span><span class="n">js</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">result</span><span class="p">,</span> <span class="n">chromedp</span><span class="o">.</span><span class="n">EvalAsValue</span><span class="p">))</span>
</code></pre></div></div>

<p><strong>Trick quan trọng:</strong> <code class="language-plaintext highlighter-rouge">chromedp.Evaluate</code> với <code class="language-plaintext highlighter-rouge">interface{}</code> thay vì <code class="language-plaintext highlighter-rouge">string</code> để handle cả khi API trả về JSON object trực tiếp thay vì string-wrapped.</p>

<h3 id="tốc-độ-http-parallel--browser-chỉ-cho-comments">Tốc độ: HTTP parallel + Browser chỉ cho comments</h3>

<p>Kiến trúc cuối cùng:</p>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>Tech</th>
      <th>Tốc độ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Article listing</td>
      <td><code class="language-plaintext highlighter-rouge">net/http</code> parallel</td>
      <td>~100ms/page</td>
    </tr>
    <tr>
      <td>Article content</td>
      <td><code class="language-plaintext highlighter-rouge">net/http</code> + custom HTML parser</td>
      <td>~200ms/article</td>
    </tr>
    <tr>
      <td>Comments</td>
      <td><code class="language-plaintext highlighter-rouge">chromedp.Evaluate</code> + <code class="language-plaintext highlighter-rouge">fetch()</code></td>
      <td>~2s/article</td>
    </tr>
    <tr>
      <td>Workers</td>
      <td>10 goroutines đồng thời</td>
      <td>—</td>
    </tr>
  </tbody>
</table>

<p>Kết quả: <strong>37,100 articles/giờ</strong> (so với ~140/giờ khi dùng Chrome navigate cho tất cả).</p>

<h3 id="pagination-api">Pagination API</h3>

<p>Tuổi Trẻ dùng format ít tài liệu hóa:</p>
<ul>
  <li>Page 1: <code class="language-plaintext highlighter-rouge">https://tuoitre.vn/{section}.htm</code></li>
  <li>Page 2+: <code class="language-plaintext highlighter-rouge">https://tuoitre.vn/timeline/{cateId}/trang-{page}.htm</code></li>
</ul>

<p>Tìm được bằng cách intercept network requests qua CDP khi scroll trang.</p>

<hr />

<h2 id="dataset-16825-bài-11-chuyên-mục">Dataset: 16,825 bài, 11 chuyên mục</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>the-gioi:    1,995 bài  | giai-tri:   1,975 bài
thoi-su:     1,937 bài  | van-hoa:    1,936 bài
phap-luat:   1,899 bài  | nhip-song-tre: 1,890 bài
suc-khoe:    1,774 bài  | giao-duc:   1,768 bài
kinh-doanh:  1,571 bài  | chinh-tri:     78 bài
</code></pre></div></div>

<p>Phạm vi: <strong>2025-01-01 đến 2026-03-18</strong></p>

<hr />

<h2 id="tf-idf--k-means-20-topic-clusters">TF-IDF + K-Means: 20 Topic Clusters</h2>

<p>Dùng <code class="language-plaintext highlighter-rouge">scikit-learn</code> với bigrams:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">vectorizer</span> <span class="o">=</span> <span class="n">TfidfVectorizer</span><span class="p">(</span>
    <span class="n">max_features</span><span class="o">=</span><span class="mi">5000</span><span class="p">,</span>
    <span class="n">ngram_range</span><span class="o">=</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span>      <span class="c1"># unigrams + bigrams
</span>    <span class="n">min_df</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">max_df</span><span class="o">=</span><span class="mf">0.7</span><span class="p">,</span>    <span class="c1"># lọc quá hiếm và quá phổ biến
</span>    <span class="n">stop_words</span><span class="o">=</span><span class="n">STOPWORDS_VI</span><span class="p">,</span>
    <span class="n">sublinear_tf</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>        <span class="c1"># log normalization
</span><span class="p">)</span>
<span class="n">X</span> <span class="o">=</span> <span class="n">vectorizer</span><span class="p">.</span><span class="n">fit_transform</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s">'title'</span><span class="p">]</span> <span class="o">+</span> <span class="s">' '</span> <span class="o">+</span> <span class="n">df</span><span class="p">[</span><span class="s">'sapo'</span><span class="p">])</span>

<span class="n">km</span> <span class="o">=</span> <span class="n">MiniBatchKMeans</span><span class="p">(</span><span class="n">n_clusters</span><span class="o">=</span><span class="mi">20</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">,</span> <span class="n">n_init</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
<span class="n">km</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>20 clusters auto-detected:</strong></p>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Topic</th>
      <th>Bài</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>💼 Kinh doanh / Doanh nghiệp</td>
      <td>962</td>
    </tr>
    <tr>
      <td>1</td>
      <td>⚔️ Chiến sự Trung Đông (Mỹ–Iran–Israel)</td>
      <td>985</td>
    </tr>
    <tr>
      <td>4</td>
      <td>🏗️ Dự án / Đầu tư công</td>
      <td>1,370</td>
    </tr>
    <tr>
      <td>5</td>
      <td>🌡️ Đời sống / Xã hội</td>
      <td>3,394</td>
    </tr>
    <tr>
      <td>6</td>
      <td>🇻🇳 Việt Nam / Chính trị nội địa</td>
      <td>1,145</td>
    </tr>
    <tr>
      <td>12</td>
      <td>🎋 Tết Bính Ngọ 2026</td>
      <td>757</td>
    </tr>
    <tr>
      <td>14</td>
      <td>🏛️ Lãnh đạo / Chính sách</td>
      <td>463</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="kết-quả-timeline-theo-tuần">Kết quả: Timeline theo tuần</h2>

<h3 id="entity-mentions--chiến-sự-bùng-nổ-tuần-2026-02-23">Entity mentions — Chiến sự bùng nổ tuần 2026-02-23</h3>

<p>Theo dõi 6 thực thể chính theo tuần:</p>

<table>
  <thead>
    <tr>
      <th>Tuần</th>
      <th>Iran</th>
      <th>Israel</th>
      <th>Mỹ/Trump</th>
      <th>Nga</th>
      <th>Trung Quốc</th>
      <th>VN Chính trị</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2026-01-05</td>
      <td>16</td>
      <td>4</td>
      <td>62</td>
      <td>62</td>
      <td>42</td>
      <td>94</td>
    </tr>
    <tr>
      <td>2026-02-02</td>
      <td>19</td>
      <td>1</td>
      <td>42</td>
      <td>76</td>
      <td>41</td>
      <td>131</td>
    </tr>
    <tr>
      <td><strong>2026-02-23</strong></td>
      <td><strong>80</strong></td>
      <td><strong>38</strong></td>
      <td>48</td>
      <td>75</td>
      <td>39</td>
      <td>150</td>
    </tr>
    <tr>
      <td><strong>2026-03-02</strong></td>
      <td><strong>144</strong></td>
      <td><strong>50</strong></td>
      <td>49</td>
      <td>46</td>
      <td>15</td>
      <td>171</td>
    </tr>
    <tr>
      <td>2026-03-09</td>
      <td>123</td>
      <td>38</td>
      <td>40</td>
      <td>56</td>
      <td>22</td>
      <td>165</td>
    </tr>
  </tbody>
</table>

<p>Cú nhảy từ 19 → <strong>144</strong> mentions/tuần của Iran sau ngày 28/2 rất rõ.</p>

<h3 id="topic-shift-trước-vs-sau-chiến-sự">Topic shift: Trước vs Sau chiến sự</h3>

<p>So sánh cơ cấu bài viết <strong>7,002 bài trước 28/2</strong> vs <strong>2,689 bài sau</strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TĂNG MẠNH:
  +5.9%  🇻🇳 VN Chính trị (Quốc hội khóa XVI)
  +3.8%  ⚔️ Chiến sự Trung Đông
  +1.9%  📈 Tài chính/Chứng khoán
  +1.2%  🚗 Giao thông

GIẢM MẠNH:
  -0.8%  👮 Công an/Pháp luật  
  -2.6%  🌡️ Đời sống/Xã hội
  -9.5%  🎋 Tết Bính Ngọ (đương nhiên!)
</code></pre></div></div>

<h3 id="weekly-digest--mỗi-tuần-có-gì">Weekly digest — Mỗi tuần có gì?</h3>

<p>Một vài tuần đáng chú ý:</p>

<p><strong>📅 Tuần 2026-02-09</strong> (trước chiến sự):</p>
<ul>
  <li>🎋 Tết Bính Ngọ (196 bài) — tuần cao điểm Tết</li>
  <li>🌡️ Đời sống (146 bài)</li>
</ul>

<p><strong>📅 Tuần 2026-02-23</strong> (chiến sự bắt đầu):</p>
<ul>
  <li>🌡️ Đời sống (177 bài)</li>
  <li>⚔️ Chiến sự: <em>“Dân Iran được kêu gọi ‘rời thủ đô’, dân Israel ‘bình thản’”</em></li>
  <li>👮 Pháp luật: <em>“Cựu Bộ trưởng Nguyễn Thị Kim Tiến được đưa 20,4 tỉ…”</em></li>
</ul>

<p><strong>📅 Tuần 2026-03-02</strong> (chiến sự leo thang):</p>
<ul>
  <li>🌡️ Đời sống (171 bài)</li>
  <li>🇻🇳 VN Chính trị (126 bài) — <em>“Chủ tịch Quốc hội: Kỳ họp thứ nhất Quốc hội khóa XVI…”</em></li>
  <li>⚔️ Chiến sự (122 bài): <em>“Sáng nay Israel không kích mạnh vào Tehran…”</em></li>
</ul>

<hr />

<h2 id="interactive-charts">Interactive Charts</h2>

<p>Tất cả charts interactive (Plotly) ở đây:</p>

<ul>
  <li>🗺️ <a href="/assets/tuoitre-politics/heatmap_topics.html">Heatmap: Topic × Tuần 2025-2026</a></li>
  <li>🌍 <a href="/assets/tuoitre-politics/entities_timeline.html">Entity Timeline: Iran/Israel/Mỹ/Nga theo tuần</a></li>
  <li>📊 <a href="/assets/tuoitre-politics/topic_shift.html">Topic Shift: Trước/Sau chiến sự 28/2</a></li>
  <li>📅 <a href="/assets/tuoitre-politics/weekly_digest.html">Weekly Digest HTML</a></li>
</ul>

<hr />

<h2 id="code">Code</h2>

<p>GitHub: <a href="https://github.com/phanngoc/phanngoc.github.io">tuoitre-crawler</a> (trong repo này)</p>

<p>Stack:</p>
<ul>
  <li><strong>Crawler:</strong> Go + chromedp (CDP) + net/http</li>
  <li><strong>Analysis:</strong> Python + scikit-learn + plotly</li>
  <li><strong>Storage:</strong> SQLite</li>
</ul>

<hr />

<h2 id="limitations">Limitations</h2>

<ol>
  <li><strong>Comments = 0</strong> — server IP bị block, chỉ lấy được article metadata</li>
  <li><strong>TF-IDF không hiểu ngữ nghĩa</strong> — một số cluster bị mix. Nên thử <code class="language-plaintext highlighter-rouge">PhoBERT</code> hoặc multilingual sentence-transformers cho kết quả tốt hơn</li>
  <li><strong>Chỉ title + sapo</strong> — content đầy đủ có nhưng chưa dùng để cluster (quá nhiều noise)</li>
</ol>

<hr />

<table>
  <tbody>
    <tr>
      <td>*Crawled &amp; analyzed: 2026-03-18</td>
      <td>Tools: Go 1.18, Python 3.10, chromedp v0.9.5*</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="phần-2-gensim-lda--probabilistic-topic-modeling">Phần 2: Gensim LDA — Probabilistic Topic Modeling</h2>

<p>Sau khi có kết quả từ TF-IDF K-Means, mình apply thêm <strong>Latent Dirichlet Allocation (LDA)</strong> từ Gensim để có model probabilistic — mỗi document không bị gán cứng vào 1 cluster mà có phân phối xác suất trên tất cả topics.</p>

<h3 id="tại-sao-lda-tốt-hơn-k-means-cho-text">Tại sao LDA tốt hơn K-Means cho text?</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>K-Means + TF-IDF</th>
      <th>Gensim LDA</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Assignment</td>
      <td>Hard (1 cluster)</td>
      <td>Soft (phân phối xác suất)</td>
    </tr>
    <tr>
      <td>Interpretability</td>
      <td>Centroid keywords</td>
      <td>Top words per topic</td>
    </tr>
    <tr>
      <td>Handles polysemy</td>
      <td>❌</td>
      <td>✅ (từ đa nghĩa)</td>
    </tr>
    <tr>
      <td>Coherence metric</td>
      <td>❌</td>
      <td>✅ C_V coherence</td>
    </tr>
  </tbody>
</table>

<h3 id="chọn-số-topics-bằng-coherence-score">Chọn số topics bằng Coherence Score</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">gensim.models</span> <span class="kn">import</span> <span class="n">LdaMulticore</span>
<span class="kn">from</span> <span class="nn">gensim.models.coherencemodel</span> <span class="kn">import</span> <span class="n">CoherenceModel</span>

<span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">15</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="mi">25</span><span class="p">]:</span>
    <span class="n">lda</span> <span class="o">=</span> <span class="n">LdaMulticore</span><span class="p">(</span><span class="n">corpus</span><span class="o">=</span><span class="n">bow_corpus</span><span class="p">,</span> <span class="n">id2word</span><span class="o">=</span><span class="n">dictionary</span><span class="p">,</span> 
                       <span class="n">num_topics</span><span class="o">=</span><span class="n">k</span><span class="p">,</span> <span class="n">workers</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">passes</span><span class="o">=</span><span class="mi">15</span><span class="p">)</span>
    <span class="n">cm</span> <span class="o">=</span> <span class="n">CoherenceModel</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="n">lda</span><span class="p">,</span> <span class="n">texts</span><span class="o">=</span><span class="n">docs</span><span class="p">,</span> <span class="n">coherence</span><span class="o">=</span><span class="s">'c_v'</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"k=</span><span class="si">{</span><span class="n">k</span><span class="si">}</span><span class="s">: coherence=</span><span class="si">{</span><span class="n">cm</span><span class="p">.</span><span class="n">get_coherence</span><span class="p">()</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k=10: coherence=0.4817  ← best
k=15: coherence=0.4726
k=20: coherence=0.4758
k=25: coherence=0.4674
</code></pre></div></div>

<p><strong>k=10</strong> tối ưu — thêm topics không cải thiện coherence.</p>

<h3 id="10-lda-topics-tìm-được">10 LDA Topics tìm được</h3>

<table>
  <thead>
    <tr>
      <th>Topic</th>
      <th>Label</th>
      <th>Bài</th>
      <th>Top words</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>🏛️ Chính trị VN (bầu cử, đối ngoại)</td>
      <td>1,672</td>
      <td>quốc, hội, tổng, mỹ, cử</td>
    </tr>
    <tr>
      <td>1</td>
      <td>⚔️ Chiến sự Iran-Israel-Mỹ</td>
      <td>1,054</td>
      <td>iran, mỹ, quân, bay, tàu, chiến</td>
    </tr>
    <tr>
      <td>3</td>
      <td>🎓 Giáo dục (TP.HCM, đại học)</td>
      <td>2,612</td>
      <td>học, sinh, trường, đại, giáo</td>
    </tr>
    <tr>
      <td>4</td>
      <td>👮 Pháp luật / Công an</td>
      <td>2,172</td>
      <td>công an, tỉnh, tra, vụ, án</td>
    </tr>
    <tr>
      <td>5</td>
      <td>🏥 Sức khỏe / Y tế</td>
      <td>1,387</td>
      <td>bệnh, viện, bác, khoa</td>
    </tr>
    <tr>
      <td>6</td>
      <td>📈 Kinh tế / BĐS / Doanh nghiệp</td>
      <td>2,037</td>
      <td>giá, doanh, đồng, đầu, sản</td>
    </tr>
    <tr>
      <td>7</td>
      <td>🎭 Văn hóa / Nghệ sĩ</td>
      <td>2,612</td>
      <td>nghệ, việt, nam, diễn, văn</td>
    </tr>
    <tr>
      <td>9</td>
      <td>🚗 Giao thông / Tết</td>
      <td>1,399</td>
      <td>tết, đường, xe, khách, hcm</td>
    </tr>
  </tbody>
</table>

<h3 id="lda-insights--chiến-sự-iran-rõ-hơn">LDA Insights — Chiến sự Iran rõ hơn</h3>

<p>Với LDA, topic ⚔️ Chiến sự được phân biệt rõ với 🏛️ Chính trị VN — điều mà K-Means gộp chung do cùng xuất hiện từ “Mỹ”, “tổng thống”. LDA nhận ra:</p>
<ul>
  <li>Topic 0 → <code class="language-plaintext highlighter-rouge">quốc hội, cử, biểu</code> = bầu cử trong nước</li>
  <li>Topic 1 → <code class="language-plaintext highlighter-rouge">iran, quân, bay, tàu, chiến</code> = chiến sự quân sự</li>
</ul>

<p><strong>Topic confidence</strong> (avg probability) của ⚔️ Chiến sự tăng vọt từ tuần 2026-02-23, trong khi confidence của 🚗 Giao thông/Tết giảm — người đọc chuyển sang đọc tin chiến sự, tòa soạn follow accordingly.</p>

<h3 id="interactive-lda-charts">Interactive LDA Charts</h3>

<ul>
  <li>🧠 <a href="/assets/tuoitre-politics/lda_heatmap.html">LDA Heatmap: Topic × Tuần</a></li>
  <li>🔑 <a href="/assets/tuoitre-politics/lda_keywords.html">Top Keywords mỗi Topic</a></li>
  <li>📈 <a href="/assets/tuoitre-politics/lda_timeline.html">LDA Timeline 2026</a></li>
  <li>🌡️ <a href="/assets/tuoitre-politics/lda_confidence.html">Topic Confidence theo tuần</a></li>
  <li>📊 <a href="/assets/tuoitre-politics/lda_shift.html">LDA Topic Shift: Trước/Sau chiến sự</a></li>
</ul>

<hr />

<p><em>Updated 2026-03-18 với Gensim LDA (k=10, coherence=0.4817)</em></p>

<hr />

<h2 id="phần-3-bertopic--neural-topic-modeling-với-multilingual-bert">Phần 3: BERTopic — Neural Topic Modeling với Multilingual BERT</h2>

<p>Sau TF-IDF K-Means và Gensim LDA, mình apply <strong>BERTopic</strong> — phương pháp dùng sentence embeddings thay vì bag-of-words, kết hợp UMAP + HDBSCAN để cluster.</p>

<h3 id="architecture-bertopic">Architecture BERTopic</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>texts → SentenceTransformer → embeddings (384-dim)
                                    ↓
                               UMAP (5-dim)
                                    ↓
                            HDBSCAN clustering
                                    ↓
                   c-TF-IDF per cluster → topic keywords
                                    ↓
                    MaximalMarginalRelevance (diversity=0.3)
</code></pre></div></div>

<p>Model dùng: <code class="language-plaintext highlighter-rouge">paraphrase-multilingual-MiniLM-L12-v2</code> — hỗ trợ 50+ ngôn ngữ kể cả tiếng Việt.</p>

<h3 id="so-sánh-3-methods">So sánh 3 methods</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>TF-IDF K-Means</th>
      <th>Gensim LDA</th>
      <th><strong>BERTopic</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Topics found</td>
      <td>20 (fixed)</td>
      <td>10 (fixed)</td>
      <td><strong>37 auto</strong></td>
    </tr>
    <tr>
      <td>Coherence (c_v)</td>
      <td>N/A</td>
      <td>0.505</td>
      <td>N/A</td>
    </tr>
    <tr>
      <td>Outliers</td>
      <td>0%</td>
      <td>0%</td>
      <td><strong>34.9%</strong></td>
    </tr>
    <tr>
      <td>Iran topic</td>
      <td><code class="language-plaintext highlighter-rouge">iran \| mỹ \| tấn công</code></td>
      <td><code class="language-plaintext highlighter-rouge">mỹ \| tổng \| thống</code></td>
      <td><strong><code class="language-plaintext highlighter-rouge">iran_trump_tổng thống_ukraine</code></strong></td>
    </tr>
    <tr>
      <td>Speed</td>
      <td>1.6s</td>
      <td>18.7s</td>
      <td><strong>107s</strong></td>
    </tr>
    <tr>
      <td>Granularity</td>
      <td>Coarse</td>
      <td>Medium</td>
      <td><strong>Fine</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>Outlier 34.9%</strong>: BERTopic dùng HDBSCAN nên những bài không thuộc cluster nào bị đánh dấu -1. Đây là trade-off của clustering density-based — chỉ giữ lại những pattern rõ ràng.</p>

<h3 id="37-topics-cụ-thể-từ-bertopic-2026">37 Topics cụ thể từ BERTopic (2026)</h3>

<p>Top 20 topics tìm được (với keywords tự động):</p>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Keywords</th>
      <th>Bài</th>
      <th>Peak week</th>
      <th>Ví dụ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>bầu, bí thư, bầu cử, quốc hội</td>
      <td>2,308</td>
      <td>2026-03-09</td>
      <td>Quốc hội khóa XVI</td>
    </tr>
    <tr>
      <td>1</td>
      <td><strong>iran, trump, tổng thống, ukraine</strong></td>
      <td><strong>719</strong></td>
      <td>2026-01-05</td>
      <td>Chiến sự Trung Đông</td>
    </tr>
    <tr>
      <td>2</td>
      <td>học, đại học, tuyển sinh</td>
      <td>439</td>
      <td>2026-01-19</td>
      <td>Tuyển sinh 2026</td>
    </tr>
    <tr>
      <td>3</td>
      <td>lừa, lừa đảo, bắt, tiền</td>
      <td>436</td>
      <td>2026-01-12</td>
      <td>Tội phạm kinh tế</td>
    </tr>
    <tr>
      <td>4</td>
      <td>giao thông, tài xế, tai nạn</td>
      <td>249</td>
      <td>2026-03-09</td>
      <td>An toàn giao thông</td>
    </tr>
    <tr>
      <td>6</td>
      <td>dầu, xăng, giá xăng</td>
      <td>189</td>
      <td>2026-03-09</td>
      <td>Giá xăng dầu tăng</td>
    </tr>
    <tr>
      <td>7</td>
      <td>oscar, thời trang, hollywood</td>
      <td>154</td>
      <td>2026-02-02</td>
      <td>Oscar 2026</td>
    </tr>
    <tr>
      <td>11</td>
      <td><strong>trí tuệ nhân tạo, AI, lao động</strong></td>
      <td><strong>107</strong></td>
      <td>2026-01-26</td>
      <td>AI thay thế việc làm</td>
    </tr>
    <tr>
      <td>17</td>
      <td>thuế quan, mức thuế, tòa án</td>
      <td>61</td>
      <td>2026-02-23</td>
      <td>Trump áp thuế</td>
    </tr>
    <tr>
      <td>19</td>
      <td>anime, jujutsu kaisen, manga</td>
      <td>49</td>
      <td>2026-01-19</td>
      <td>Văn hóa Nhật</td>
    </tr>
  </tbody>
</table>

<h3 id="điểm-nổi-bật-của-bertopic-so-với-lda">Điểm nổi bật của BERTopic so với LDA</h3>

<ol>
  <li>
    <table>
      <tbody>
        <tr>
          <td><strong>Topic 1 “iran_trump_ukraine”</strong> — LDA gộp chung thành “mỹ</td>
          <td>tổng</td>
          <td>thống”, BERTopic tách riêng nhờ semantic embeddings phân biệt ngữ cảnh chiến sự vs. bầu cử trong nước</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li><strong>Topic 11 AI/LLM</strong> — LDA không tìm được topic này (too niche), BERTopic tìm được 107 bài về AI</li>
  <li><strong>Topic 19 Anime</strong> — 49 bài cực kỳ specific về Jujutsu Kaisen — không phương pháp nào khác detect được</li>
  <li><strong>Topic 6 xăng dầu</strong> — Tách riêng khỏi kinh tế chung, peak tuần 03-09 khi chiến sự ảnh hưởng giá dầu</li>
</ol>

<h3 id="interactive-bertopic-charts">Interactive BERTopic Charts</h3>

<ul>
  <li>🧠 <a href="/assets/tuoitre-politics/bertopic_heatmap.html">BERTopic Heatmap: Topic × Tuần</a></li>
  <li>🗺️ <a href="/assets/tuoitre-politics/bertopic_umap.html">BERTopic UMAP 2D Scatter</a></li>
  <li>📊 <a href="/assets/tuoitre-politics/method_comparison.html">Method Comparison: 3 methods side-by-side</a></li>
  <li>⚔️ <a href="/assets/tuoitre-politics/bertopic_timeline.html">Iran/Chiến sự topic theo 3 methods</a></li>
</ul>

<hr />

<h2 id="kết-luận-nên-dùng-method-nào">Kết luận: Nên dùng method nào?</h2>

<ul>
  <li><strong>TF-IDF K-Means</strong>: Nhanh, đơn giản, tốt cho prototyping. Dùng khi cần tổng quan nhanh.</li>
  <li><strong>Gensim LDA</strong>: Probabilistic, có coherence score để tune. Tốt khi muốn soft assignment và interpretable.</li>
  <li><strong>BERTopic</strong>: Tốt nhất cho <strong>tiếng Việt</strong> vì dùng multilingual embeddings — hiểu ngữ nghĩa thay vì chỉ đếm từ. Trade-off: chậm hơn và có nhiều outliers.</li>
</ul>

<p>Với 9,691 bài Tuổi Trẻ 2026, <strong>BERTopic tìm ra 37 topics granular</strong> so với 10-20 topics thô của các method truyền thống.</p>

<p><em>Pipeline: Go + chromedp → SQLite → Python (scikit-learn + gensim + bertopic) → Plotly</em></p>]]></content><author><name></name></author><category term="nlp" /><category term="clustering" /><category term="tfidf" /><category term="politics" /><category term="vietnam" /><category term="data-analysis" /><category term="chromedp" /><category term="golang" /><summary type="html"><![CDATA[Tôi crawl 16,825 bài báo từ Tuổi Trẻ Online trong vòng 24 phút bằng Go + Chrome DevTools Protocol, sau đó dùng TF-IDF clustering để vẽ bản đồ các chủ đề chính trị theo tuần. Kết quả khá thú vị — đặc biệt là cú shock của chiến sự Iran bắt đầu ngày 28/2/2026.]]></summary></entry><entry><title type="html">Giải Mã Hệ Thống Anti-Bot Của Shopee: Từ Góc Nhìn Của Một Developer Vừa Bị Chặn</title><link href="/2026/03/17/shopee-anti-bot-analysis.html" rel="alternate" type="text/html" title="Giải Mã Hệ Thống Anti-Bot Của Shopee: Từ Góc Nhìn Của Một Developer Vừa Bị Chặn" /><published>2026-03-17T07:00:00+00:00</published><updated>2026-03-17T07:00:00+00:00</updated><id>/2026/03/17/shopee-anti-bot-analysis</id><content type="html" xml:base="/2026/03/17/shopee-anti-bot-analysis.html"><![CDATA[<blockquote>
  <p><strong>Disclaimer:</strong> Bài viết này được viết với mục đích nghiên cứu kỹ thuật. Tất cả các thử nghiệm được thực hiện trên dữ liệu public. Hãy tôn trọng ToS của các nền tảng.</p>
</blockquote>

<hr />

<p>Tôi vừa mất 3 giờ chiến đấu với hệ thống anti-bot của Shopee để crawl danh sách sản phẩm điện thoại. Kết quả: chỉ crawl được ~250 sản phẩm trước khi bị chặn hoàn toàn. Nhưng trong quá trình đó, tôi học được rất nhiều thứ thú vị về cách Shopee bảo vệ platform của họ.</p>

<p>Đây là bài phân tích chi tiết — dành cho những developer muốn hiểu <strong>anti-bot hoạt động như thế nào ở cấp độ thực tế</strong>.</p>

<hr />

<h2 id="bức-tranh-tổng-thể-shopee-dùng-gì">Bức Tranh Tổng Thể: Shopee Dùng Gì?</h2>

<p>Qua quan sát trực tiếp network requests, response patterns và hàng chục lần bị chặn, đây là kiến trúc bảo vệ đầy đủ mà Shopee triển khai:</p>

<p><img src="/assets/shopee-antibot/01-defense-in-depth.png" alt="Defense-in-Depth Architecture" /></p>

<p>Một request từ bot phải vượt qua <strong>6 lớp kiểm tra độc lập</strong>, mỗi lớp bắt một loại attacker khác nhau:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Bot Request]
      │
      ▼
[L1] TLS Fingerprint ──── JA3/JA4 hash mismatch → BLOCK
      │ pass
      ▼
[L2] IP Reputation ──────  Datacenter/TOR/proxy AS → BLOCK
      │ pass
      ▼
[L3] Browser Fingerprint ─ Canvas hash, WebGL → BLOCK
      │ pass
      ▼
[L4] Behavioral Analysis ─ Mouse entropy, scroll timing → CAPTCHA
      │ pass
      ▼
[L5] Session Token ──────  SPC_EC server-side revoke → BLOCK
      │ pass
      ▼
[L6] Correlation ID ─────  Honeypot tracking_id → PERMANENT BAN
      │ pass
      ▼
[Response] ✓
</code></pre></div></div>

<p>Không có layer nào là “magic bullet” — chính sự <strong>kết hợp 6 signals đồng thời</strong> mới tạo nên độ khó thực sự. Bypass một layer không đủ nếu các layer còn lại vẫn fire.</p>

<h3 id="vì-sao-khó-bypass-đến-vậy">Vì sao khó bypass đến vậy?</h3>

<p>Mỗi layer được thiết kế để <strong>fail independently</strong> — tức là nếu layer 2 (IP) pass nhưng layer 4 (behavior) fail, request vẫn bị block. Không có “shortcut” nào bypass toàn bộ stack chỉ bằng một trick đơn lẻ.</p>

<hr />

<h2 id="layer-1-tls-fingerprinting-ja3ja4">Layer 1: TLS Fingerprinting (JA3/JA4)</h2>

<p><img src="/assets/shopee-antibot/02-tls-fingerprint.png" alt="TLS Fingerprint Comparison" /></p>

<p>Đây là thứ đầu tiên tôi gặp phải và không nhận ra ngay.</p>

<p>Khi một browser connect tới HTTPS server, quá trình TLS handshake tạo ra một <strong>fingerprint độc nhất</strong> dựa trên:</p>
<ul>
  <li>Cipher suites được đề xuất (và thứ tự)</li>
  <li>TLS extensions</li>
  <li>Elliptic curves và point formats</li>
</ul>

<p>Tools như <strong>curl</strong>, <strong>requests</strong> (Python), hay Playwright Chromium có JA3 hash khác với real Chrome. Shopee detect điều này ở connection layer — <strong>trước khi nhìn vào HTTP headers</strong>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># requests library JA3: 771,4866-4867-4865-...,0-23-65281-...
# real Chrome JA3:      771,4866-4867-4865-...,0-23-65281-... (khác order)
</span></code></pre></div></div>

<p><strong>Cách Playwright Firefox thoát được:</strong> Firefox có TLS profile khác hoàn toàn với Chromium và giống với real Firefox traffic. Đó là lý do <code class="language-plaintext highlighter-rouge">p.firefox.launch()</code> hoạt động còn <code class="language-plaintext highlighter-rouge">p.chromium.launch()</code> bị block ngay.</p>

<hr />

<h2 id="layer-2-ip-reputation--rate-limiting">Layer 2: IP Reputation &amp; Rate Limiting</h2>

<p><img src="/assets/shopee-antibot/05-ip-reputation.png" alt="IP Reputation Hierarchy" /></p>

<p>Đây là layer chặn tôi nhiều nhất — và đây là những gì tôi quan sát được:</p>

<p><strong>Signal 1: AS Number</strong>
Data center IPs (AWS, GCP, DigitalOcean) bị rate-limit cực gắt. Shopee trace AS number của IP và assign “trust score” thấp hơn cho cloud IPs.</p>

<p><strong>Signal 2: Request velocity</strong>
Phân tích response thực tế cho thấy pattern:</p>
<ul>
  <li>Request 1-2: OK ✅</li>
  <li>Request 3: <code class="language-plaintext highlighter-rouge">verify/captcha</code> ❌</li>
</ul>

<p>Đây không phải threshold đơn giản — Shopee tracking <strong>velocity per IP per time window</strong> với sliding window, không phải fixed counter.</p>

<p><strong>Signal 3: Shared IP history</strong>
Nếu IP đó từng được dùng để spam/scrape trước đây, nó sẽ bị “pre-flagged”. Public proxy lists thường toàn IPs đã bị blacklist.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Thực tế từ crawl session của tôi:
Page 1: OK (60 products)
Page 2: verify/captcha
↳ Restart browser + re-inject cookies: OK
Page 3: verify/captcha (permanent)
↳ Browser restart: vẫn block
</code></pre></div></div>

<p>Pattern này cho thấy IP bị “cool down” sau N requests — không phải session-based.</p>

<hr />

<h2 id="layer-3-behavioral-fingerprinting">Layer 3: Behavioral Fingerprinting</h2>

<p>Layer này tinh vi nhất và khó bypass nhất. Shopee collect các signals sau <strong>trong JavaScript</strong>:</p>

<p><img src="/assets/shopee-antibot/03-mouse-entropy.png" alt="Mouse Movement Entropy" /></p>

<h3 id="mouse-movement-entropy">Mouse Movement Entropy</h3>

<p>Real users có mouse movement với <strong>Gaussian noise</strong> — cursor không bao giờ di chuyển thẳng tuyến tính. Automation tools tạo ra movement patterns quá “đều”:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Bot: perfect straight line</span>
<span class="nx">mousemove</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span><span class="mi">100</span><span class="p">)</span> <span class="err">→</span> <span class="nx">mousemove</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span><span class="mi">200</span><span class="p">)</span> <span class="err">→</span> <span class="nx">mousemove</span><span class="p">(</span><span class="mi">300</span><span class="p">,</span><span class="mi">300</span><span class="p">)</span>

<span class="c1">// Human: có micro-jitter, acceleration/deceleration</span>
<span class="nx">mousemove</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span><span class="mi">100</span><span class="p">)</span> <span class="err">→</span> <span class="nx">mousemove</span><span class="p">(</span><span class="mi">143</span><span class="p">,</span><span class="mi">152</span><span class="p">)</span> <span class="err">→</span> <span class="nx">mousemove</span><span class="p">(</span><span class="mi">198</span><span class="p">,</span><span class="mi">201</span><span class="p">)</span> <span class="err">→</span> <span class="p">...</span>
</code></pre></div></div>

<p>Tôi đã implement <code class="language-plaintext highlighter-rouge">human_mouse_wiggle()</code> để simulate điều này, nhưng chưa đủ — cần thêm <strong>Bezier curves</strong> và velocity modeling.</p>

<h3 id="scroll-pattern">Scroll Pattern</h3>

<p>Shopee detect scroll bằng cách measure:</p>
<ul>
  <li>Time between scroll events</li>
  <li>Delta per event (humans scroll in non-uniform chunks)</li>
  <li>Pause patterns (humans dừng lại để đọc)</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Human-like scroll implementation của tôi:
</span><span class="k">async</span> <span class="k">def</span> <span class="nf">human_scroll</span><span class="p">(</span><span class="n">page</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">scroll_pct</span> <span class="ow">in</span> <span class="p">[</span><span class="mi">20</span><span class="p">,</span> <span class="mi">40</span><span class="p">,</span> <span class="mi">60</span><span class="p">,</span> <span class="mi">80</span><span class="p">,</span> <span class="mi">100</span><span class="p">]:</span>
        <span class="k">await</span> <span class="n">page</span><span class="p">.</span><span class="n">evaluate</span><span class="p">(</span><span class="sa">f</span><span class="s">"window.scrollTo()"</span><span class="p">)</span>
        <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">random</span><span class="p">.</span><span class="n">uniform</span><span class="p">(</span><span class="mf">0.15</span><span class="p">,</span> <span class="mf">0.45</span><span class="p">))</span>
        <span class="k">if</span> <span class="n">random</span><span class="p">.</span><span class="n">random</span><span class="p">()</span> <span class="o">&lt;</span> <span class="mf">0.15</span><span class="p">:</span>  <span class="c1"># đọc content
</span>            <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">random</span><span class="p">.</span><span class="n">uniform</span><span class="p">(</span><span class="mf">0.5</span><span class="p">,</span> <span class="mf">1.5</span><span class="p">))</span>
</code></pre></div></div>

<p>Điều thú vị: <code class="language-plaintext highlighter-rouge">behavior: 'smooth'</code> trong JS thực ra <strong>giúp</strong> bypass detection vì nó tạo ra nhiều intermediate scroll events như human.</p>

<h3 id="event-timing-distribution">Event Timing Distribution</h3>

<p>Browser automation thường trigger events với timing quá đều. Real users có <strong>timing jitter</strong> theo phân phối log-normal:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bot timing:    [100ms, 100ms, 100ms, 100ms]  ← quá đều
Human timing:  [87ms, 134ms, 92ms, 201ms]    ← log-normal distribution
</code></pre></div></div>

<hr />

<h2 id="layer-4-session-token-architecture">Layer 4: Session Token Architecture</h2>

<p><img src="/assets/shopee-antibot/04-session-token.png" alt="Session Token Lifecycle" /></p>

<p>Đây là discovery thú vị nhất. Shopee dùng 2 loại tokens:</p>

<p><strong><code class="language-plaintext highlighter-rouge">SPC_F</code></strong> — Device fingerprint token, tồn tại lâu dài
<strong><code class="language-plaintext highlighter-rouge">SPC_EC</code></strong> — Session encryption token, expire nhanh</p>

<p>Khi tôi inspect cookies sau khi bị captcha:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SPC_EC: [encrypted blob, ~256 chars]  ← bị invalidate server-side
SPC_F:  [shorter token]               ← vẫn valid nhưng bị flag
</code></pre></div></div>

<p>Vấn đề: ngay cả khi re-inject cookies từ real Chrome profile, server đã <strong>revoke <code class="language-plaintext highlighter-rouge">SPC_EC</code></strong> sau khi detect automation. Re-inject cookies cũ không giúp được gì vì server-side state đã thay đổi.</p>

<p>Đây là kiến trúc <strong>stateful session validation</strong> — token validity được check ở server, không chỉ ở client.</p>

<hr />

<h2 id="layer-5-canvas--webgl-fingerprinting">Layer 5: Canvas &amp; WebGL Fingerprinting</h2>

<p>Shopee run JavaScript để lấy canvas fingerprint:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Shopee's fingerprinting code (deobfuscated pattern)</span>
<span class="kd">var</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">canvas</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">'</span><span class="s1">2d</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">fillText</span><span class="p">(</span><span class="dl">'</span><span class="s1">Shopee fingerprint</span><span class="dl">'</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">hash</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">toDataURL</span><span class="p">();</span> <span class="c1">// unique per GPU/driver/OS</span>
</code></pre></div></div>

<p>Playwright Chromium headless có một <strong>known canvas fingerprint</strong> mà Shopee đã blacklist. Firefox có fingerprint khác — đó là lý do Firefox hoạt động ban đầu.</p>

<p>Tuy nhiên, sau vài requests, <strong>behavioral signals</strong> (mouse, scroll, timing) override cả canvas fingerprint.</p>

<hr />

<h2 id="layer-6-the-honeypot--verifytrafficerror">Layer 6: The Honeypot — <code class="language-plaintext highlighter-rouge">verify/traffic/error</code></h2>

<p>URL này rất thú vị:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://shopee.vn/verify/traffic/error?
  home_url=https://shopee.vn
  &amp;is_logged_in=false
  &amp;tracking_id=676960ddb3-5467-4771-a88e-230f1195c66d
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">tracking_id</code> là một <strong>correlation ID</strong> — Shopee dùng nó để link request này với toàn bộ session history của IP/fingerprint combination. Một khi bị assign tracking_id này, <strong>mọi request tiếp theo từ IP đó</strong> đều bị route qua verification flow, bất kể browser hay cookies.</p>

<hr />

<h2 id="cái-gì-thực-sự-hoạt-động">Cái Gì Thực Sự Hoạt Động</h2>

<p>Từ quá trình thử nghiệm, đây là ranking của các techniques:</p>

<table>
  <thead>
    <tr>
      <th>Technique</th>
      <th>Bypass rate</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Firefox (vs Chromium)</td>
      <td>⭐⭐⭐⭐</td>
      <td>TLS + canvas fingerprint khác</td>
    </tr>
    <tr>
      <td>Real Chrome cookies</td>
      <td>⭐⭐⭐</td>
      <td>Bypass login check, không bypass IP</td>
    </tr>
    <tr>
      <td>Human scroll/mouse</td>
      <td>⭐⭐</td>
      <td>Delay detection, không prevent</td>
    </tr>
    <tr>
      <td>Browser restart</td>
      <td>⭐⭐</td>
      <td>New fingerprint, same IP → still flagged</td>
    </tr>
    <tr>
      <td>Free proxy lists</td>
      <td>⭐</td>
      <td>Đều bị pre-blacklist</td>
    </tr>
    <tr>
      <td>Residential proxy</td>
      <td>⭐⭐⭐⭐⭐</td>
      <td>Real ISP IP → bypass IP reputation</td>
    </tr>
    <tr>
      <td>TOR</td>
      <td>⭐⭐</td>
      <td>Exit nodes thường bị blacklist</td>
    </tr>
  </tbody>
</table>

<p><strong>Pattern thực tế:</strong> Shopee cho phép ~1-3 pages per IP per time window trước khi captcha. Với residential proxy rotation (mỗi request/IP khác nhau), bạn bypass được layer IP hoàn toàn.</p>

<hr />

<h2 id="architecture-của-một-crawler-thực-sự-bypass-được">Architecture Của Một Crawler Thực Sự Bypass Được</h2>

<p>Nếu phải thiết kế lại từ đầu, đây là architecture tôi sẽ dùng:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────┐
│                  Crawler Core                    │
│                                                  │
│  Playwright Firefox (TLS fingerprint ✅)         │
│       + Real Chrome Cookies (session ✅)         │
│       + Human Behavior Engine (behavioral ✅)   │
│                    │                             │
│                    ▼                             │
│         Residential Proxy Rotator               │
│    (new IP per page, ISP-grade trust ✅)        │
│                    │                             │
│                    ▼                             │
│              Shopee.vn ✅                        │
└─────────────────────────────────────────────────┘
</code></pre></div></div>

<p><strong>Human Behavior Engine</strong> cần implement:</p>
<ol>
  <li><strong>Bezier curve mouse movement</strong> — không phải random wiggle</li>
  <li><strong>Log-normal timing distribution</strong> cho keyboard/click events</li>
  <li><strong>Realistic scroll with reading pauses</strong> — dừng lại ở product cards</li>
  <li><strong>Random viewport interactions</strong> — hover qua filters, không chỉ scroll thẳng</li>
</ol>

<hr />

<h2 id="lesson-learned-shopee-không-chỉ-detect-bot-họ-detect-patterns">Lesson Learned: Shopee Không Chỉ Detect Bot, Họ Detect Patterns</h2>

<p>Điều tôi nhận ra sau cùng: Shopee không nhìn vào từng request riêng lẻ. Họ build <strong>behavioral profile</strong> theo thời gian:</p>

<ul>
  <li>Session mở lúc nào?</li>
  <li>Có xem trang chủ trước không?</li>
  <li>Click vào category hay vào thẳng URL?</li>
  <li>Dừng lại bao lâu ở mỗi trang?</li>
  <li>Có interact với search/filter không?</li>
</ul>

<p>Real users không mở thẳng <code class="language-plaintext highlighter-rouge">?page=5&amp;sortBy=pop</code>. Họ vào trang chủ, search, filter, rồi mới pagination. Một crawler “hoàn hảo” phải simulate <strong>toàn bộ user journey</strong>, không chỉ từng request.</p>

<hr />

<h2 id="hành-trình-thất-bại-hoàn-toàn-ghi-chép-thực-tế">Hành Trình Thất Bại Hoàn Toàn (Ghi Chép Thực Tế)</h2>

<p>Đây là phần tôi muốn viết rõ nhất — vì hầu hết blog tech chỉ show success story. Đây là timeline thất bại theo thứ tự:</p>

<h3 id="attempt-1-go--go-rod-chromium">Attempt 1: Go + go-rod (Chromium)</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Result: Redirect ngay → /verify/traffic/error
Time to fail: ~3 giây
Reason: Chromium fingerprint bị blacklist
</code></pre></div></div>

<h3 id="attempt-2-go--go-rod--stealth-chromium">Attempt 2: Go + go-rod + stealth (Chromium)</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Result: Vẫn bị /verify/traffic/error
Time to fail: ~3 giây  
Reason: Stealth mode không đủ che TLS fingerprint của Chromium
</code></pre></div></div>

<h3 id="attempt-3-chrome-profile-thật-copy-cookies">Attempt 3: Chrome profile thật (copy cookies)</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Result: login page — cookies không decrypt được
Time to fail: ~5 giây
Reason: Chrome encrypt cookies bằng OS keyring, copy raw file ra không dùng được
</code></pre></div></div>

<h3 id="attempt-4-playwright-chromium--stealth">Attempt 4: Playwright Chromium + stealth</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Result: /verify/captcha
Time to fail: ~8 giây
Reason: Chromium headless fingerprint đã bị blacklist
</code></pre></div></div>

<h3 id="attempt-5-playwright-firefox--chrome-cookies-pycookiecheat--breakthrough">Attempt 5: Playwright Firefox + Chrome cookies (pycookiecheat) ← <strong>BREAKTHROUGH</strong></h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Result: ✅ 60 items page 1!
Pages crawled: 1 → captcha
Reason: Firefox TLS profile khác, pycookiecheat decrypt được cookies
</code></pre></div></div>

<h3 id="attempt-6-human-behavior-simulation">Attempt 6: Human behavior simulation</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Tôi implement:
</span><span class="k">async</span> <span class="k">def</span> <span class="nf">human_scroll</span><span class="p">(</span><span class="n">page</span><span class="p">):</span>          <span class="c1"># smooth scroll với random pauses
</span><span class="k">async</span> <span class="k">def</span> <span class="nf">human_mouse_wiggle</span><span class="p">(</span><span class="n">page</span><span class="p">):</span>    <span class="c1"># random mouse movement
# + delay 20-40s giữa các trang
# + quay lại homepage giữa các page
</span>
<span class="n">Result</span><span class="p">:</span> <span class="n">Vẫn</span> <span class="n">captcha</span> <span class="n">sau</span> <span class="mi">1</span><span class="o">-</span><span class="mi">2</span> <span class="n">trang</span>
<span class="n">Pages</span> <span class="n">crawled</span><span class="p">:</span> <span class="mi">2</span> <span class="err">→</span> <span class="n">captcha</span>
<span class="n">Reason</span><span class="p">:</span> <span class="n">Behavior</span> <span class="n">delay</span> <span class="n">tốt</span> <span class="n">nhưng</span> <span class="n">IP</span> <span class="n">reputation</span> <span class="n">vẫn</span> <span class="n">bị</span> <span class="n">flag</span>
</code></pre></div></div>

<h3 id="attempt-7-browser-restart--re-inject-cookies">Attempt 7: Browser restart + re-inject cookies</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Result: 1 lần recover, lần 2 permanent block
Reason: Server đã revoke SPC_EC token, inject lại vô nghĩa
</code></pre></div></div>

<h3 id="attempt-8-scraperapi-free-plan">Attempt 8: ScraperAPI (free plan)</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Result: "requires Ultra Premium proxies" — paid plan only
Cost: Free plan không work với Shopee
</code></pre></div></div>

<h3 id="attempt-9-free-proxy-lists-220-proxies">Attempt 9: Free proxy lists (220 proxies)</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Result: 3/220 pass basic test, 0/3 pass Shopee
Reason: Tất cả public proxy IPs đã bị Shopee pre-blacklist
</code></pre></div></div>

<h3 id="attempt-10-tor-5-different-exit-nodes">Attempt 10: TOR (5 different exit nodes)</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IPs tried:
- 38.135.24.72   → /verify/traffic ❌
- 185.62.56.249  → /verify/captcha ❌  
- 192.42.116.46  → /verify/captcha ❌
- 192.42.116.93  → /verify/captcha ❌
- 192.159.99.27  → /verify/traffic ❌

Result: 5/5 bị block
Reason: Shopee maintain danh sách TOR exit nodes và blacklist toàn bộ
</code></pre></div></div>

<p><strong>Tổng kết sau ~5 giờ chiến đấu:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Attempts:  10
Success:   1 (Firefox + cookies, ~1 trang)
Products:  251 / ~6,000 (4%)
Proxies:   220+ tested, 0 work
Cost:      $0 (free tier only)
</code></pre></div></div>

<hr />

<h2 id="điều-thực-sự-cần-để-bypass">Điều Thực Sự Cần Để Bypass</h2>

<p>Sau tất cả những thử nghiệm trên, đây là <strong>điều kiện cần</strong> (không phải đủ) để crawl Shopee ở scale:</p>

<p><strong>1. Residential proxy</strong> — không phải datacenter, không phải TOR, không phải public proxy. Phải là IP từ ISP thật của người dùng (Viettel, VNPT, FPT…). Chi phí: ~$15-50/tháng cho rotating residential.</p>

<p><strong>2. Real browser fingerprint</strong> — Firefox hoặc Chrome built bằng Playwright với đầy đủ GPU/canvas rendering (không phải headless hoặc virtual display). Chạy trên máy thật hoặc cloud VM có GPU.</p>

<p><strong>3. Session warming</strong> — không jump thẳng vào category page. Phải simulate user journey: trang chủ → search → filter → browse → pagination.</p>

<p><strong>4. Cookie refresh</strong> — SPC_EC token expire nhanh. Cần mechanism tự động login lại và lấy token mới thay vì reuse token cũ.</p>

<p>Nói thẳng: <strong>không có giải pháp free nào bypass Shopee ở thời điểm 2026</strong>. Shopee đã đầu tư đủ để làm cho ROI của scraper nhỏ lẻ trở nên âm.</p>

<hr />

<h2 id="kết-luận">Kết Luận</h2>

<p>Shopee đã đầu tư nghiêm túc vào anti-bot — đây là hệ thống multi-layer với từng layer bắt một loại attacker khác nhau:</p>

<ul>
  <li><strong>Script kiddies</strong> → bị chặn bởi TLS fingerprint</li>
  <li><strong>Basic Selenium/Playwright</strong> → bị chặn bởi canvas fingerprint + IP reputation</li>
  <li><strong>Sophisticated bots</strong> → bị chặn bởi behavioral analysis</li>
  <li><strong>TOR / free proxies</strong> → bị chặn bởi IP reputation blacklist (pre-emptive)</li>
  <li><strong>Residential proxies</strong> → bypass được IP layer, nhưng còn behavioral + session analysis</li>
</ul>

<p>Hệ thống này không “unhackable” — nhưng chi phí để bypass nó đã vượt qua ngưỡng mà hầu hết side projects có thể chịu được. Đó là mục tiêu thực sự: không cần block 100%, chỉ cần làm cho cost &gt; benefit.</p>

<p>Từ góc nhìn defensive, đây là một case study xuất sắc về <strong>economic security</strong> — bảo mật không phải bằng tường lửa tuyệt đối mà bằng cách tăng chi phí tấn công lên đủ cao. Từ góc nhìn offensive… thất bại cũng là một loại học 🙂</p>

<hr />

<p><strong>Source code</strong> của crawler (Python + Playwright + proxy rotation): <a href="https://github.com/phanngoc/shopee-crawler">github.com/phanngoc/shopee-crawler</a></p>

<p><em>Dataset 251 sản phẩm crawl được: <a href="https://www.kaggle.com/ngocphansun">kaggle.com/ngocphansun</a></em></p>]]></content><author><name></name></author><category term="web-scraping" /><category term="anti-bot" /><category term="playwright" /><category term="shopee" /><category term="security" /><category term="browser-automation" /><summary type="html"><![CDATA[Disclaimer: Bài viết này được viết với mục đích nghiên cứu kỹ thuật. Tất cả các thử nghiệm được thực hiện trên dữ liệu public. Hãy tôn trọng ToS của các nền tảng.]]></summary></entry><entry><title type="html">Phân Tích Thị Trường Bất Động Sản Đà Nẵng 2026: Insights Từ 5,207 Listings Thực Tế</title><link href="/2026/03/17/phan-tich-thi-truong-bds-da-nang-2026.html" rel="alternate" type="text/html" title="Phân Tích Thị Trường Bất Động Sản Đà Nẵng 2026: Insights Từ 5,207 Listings Thực Tế" /><published>2026-03-17T02:00:00+00:00</published><updated>2026-03-17T02:00:00+00:00</updated><id>/2026/03/17/phan-tich-thi-truong-bds-da-nang-2026</id><content type="html" xml:base="/2026/03/17/phan-tich-thi-truong-bds-da-nang-2026.html"><![CDATA[<blockquote>
  <p><strong>TL;DR:</strong> Tôi crawl 5,207 listings bất động sản Đà Nẵng từ batdongsan.com.vn, chạy EDA toàn diện và rút ra một số insights thú vị: Sơn Trà đắt nhất tính theo m², Ngũ Hành Sơn sôi động nhất về khối lượng giao dịch, và phần lớn thị trường tập trung ở phân khúc 5–15 tỷ. Dataset và notebook đầy đủ trên Kaggle.</p>
</blockquote>

<hr />

<h2 id="bối-cảnh--phương-pháp">Bối Cảnh &amp; Phương Pháp</h2>

<p>Đà Nẵng luôn là một trong những thị trường BĐS được theo dõi sát nhất Việt Nam — vừa là thành phố du lịch quốc tế, vừa là hub công nghệ đang phát triển mạnh. Nhưng đọc tin tức thị trường thì được mấy phần thực chất?</p>

<p>Tôi quyết định lấy thẳng từ nguồn: crawl toàn bộ listings đất nền Đà Nẵng trên <strong>batdongsan.com.vn</strong> bằng Go + go-rod (browser automation với stealth mode), thu về <strong>5,207 listings</strong> tính đến ngày 17/03/2026.</p>

<p><strong>Stack sử dụng:</strong></p>
<ul>
  <li>Crawler: Go 1.21 + go-rod + stealth</li>
  <li>Storage: SQLite</li>
  <li>Analysis: Python + Pandas + Seaborn + Matplotlib</li>
  <li>Publish: Kaggle Datasets + Notebook</li>
</ul>

<p>📊 <strong>Dataset &amp; Notebook:</strong> <a href="https://www.kaggle.com/datasets/ngocphansun/da-nang-real-estate-listings">kaggle.com/ngocphansun/da-nang-real-estate-listings</a></p>

<hr />

<h2 id="1-bức-tranh-tổng-quan">1. Bức Tranh Tổng Quan</h2>

<table>
  <thead>
    <tr>
      <th>Chỉ số</th>
      <th>Giá trị</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tổng listings</td>
      <td><strong>5,207</strong></td>
    </tr>
    <tr>
      <td>Có thông tin giá</td>
      <td><strong>4,868 (93.5%)</strong></td>
    </tr>
    <tr>
      <td>Giá trung vị</td>
      <td><strong>9.0 tỷ VND</strong></td>
    </tr>
    <tr>
      <td>Giá trung bình</td>
      <td><strong>19.4 tỷ VND</strong></td>
    </tr>
    <tr>
      <td>Giá thấp nhất</td>
      <td><strong>280 triệu</strong></td>
    </tr>
    <tr>
      <td>Giá cao nhất</td>
      <td><strong>582 tỷ</strong></td>
    </tr>
    <tr>
      <td>Diện tích trung vị</td>
      <td><strong>110 m²</strong></td>
    </tr>
    <tr>
      <td>Đăng trong 1 tuần</td>
      <td><strong>94.2%</strong></td>
    </tr>
  </tbody>
</table>

<p>Con số đáng chú ý đầu tiên: <strong>khoảng cách lớn giữa trung vị (9 tỷ) và trung bình (19.4 tỷ)</strong> — dấu hiệu rõ của phân phối lệch phải. Một số lô đất siêu cao (100–582 tỷ) kéo giá trung bình lên đáng kể, trong khi đại đa số giao dịch vẫn nằm ở mức 5–20 tỷ.</p>

<hr />

<h2 id="2-quận-nào-sôi-động-nhất">2. Quận Nào Sôi Động Nhất?</h2>

<p><img src="/assets/bds-danang-2026/district_listings.png" alt="Listings by District" /></p>

<table>
  <thead>
    <tr>
      <th>Quận</th>
      <th>Listings</th>
      <th>% thị phần</th>
      <th>Giá trung vị</th>
      <th>Giá/m² trung vị</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Ngũ Hành Sơn</strong></td>
      <td>1,537</td>
      <td>29.5%</td>
      <td>10.2 tỷ</td>
      <td>81.6 tr/m²</td>
    </tr>
    <tr>
      <td><strong>Sơn Trà</strong></td>
      <td>1,302</td>
      <td>25.0%</td>
      <td>16.5 tỷ</td>
      <td>143.8 tr/m²</td>
    </tr>
    <tr>
      <td><strong>Cẩm Lệ</strong></td>
      <td>715</td>
      <td>13.7%</td>
      <td>7.2 tỷ</td>
      <td>64.4 tr/m²</td>
    </tr>
    <tr>
      <td><strong>Liên Chiểu</strong></td>
      <td>680</td>
      <td>13.1%</td>
      <td>6.15 tỷ</td>
      <td>58.7 tr/m²</td>
    </tr>
    <tr>
      <td><strong>Hòa Vang</strong></td>
      <td>384</td>
      <td>7.4%</td>
      <td>2.78 tỷ</td>
      <td>19.5 tr/m²</td>
    </tr>
    <tr>
      <td><strong>Hải Châu</strong></td>
      <td>343</td>
      <td>6.6%</td>
      <td>11.5 tỷ</td>
      <td>118.3 tr/m²</td>
    </tr>
    <tr>
      <td><strong>Thanh Khê</strong></td>
      <td>245</td>
      <td>4.7%</td>
      <td>6.15 tỷ</td>
      <td>87.5 tr/m²</td>
    </tr>
  </tbody>
</table>

<p><strong>Ngũ Hành Sơn chiếm gần 30% tổng listings</strong> — con số này phản ánh làn sóng phát triển ở khu vực ven biển phía Nam thành phố, đặc biệt quanh Bán đảo Sơn Trà và Non Nước. Khu vực này đang hút rất nhiều nhà đầu tư dài hạn với quỹ đất còn tương đối rộng so với trung tâm.</p>

<hr />

<h2 id="3-phân-bổ-giá-thị-trường-tập-trung-ở-đâu">3. Phân Bổ Giá: Thị Trường Tập Trung Ở Đâu?</h2>

<p><img src="/assets/bds-danang-2026/price_distribution.png" alt="Price Distribution" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Phân khúc giá       Số lượng    Tỷ lệ
─────────────────────────────────────
&lt; 3 tỷ                  283      5.8%
3 – 5 tỷ                697     14.3%
5 – 8 tỷ              1,174     24.1%  ← đông nhất
8 – 10 tỷ               433      8.9%
10 – 15 tỷ              731     15.0%
15 – 20 tỷ              420      8.6%
20 – 30 tỷ              410      8.4%
30 – 50 tỷ              393      8.1%
50 – 100 tỷ             208      4.3%
&gt; 100 tỷ                119      2.4%
</code></pre></div></div>

<p><strong>Phân khúc 5–8 tỷ là “sweet spot” của thị trường</strong> với 24% tổng số listings. Kết hợp với phân khúc 3–5 tỷ, có thể thấy gần 40% listings nằm trong range 3–8 tỷ — đây là phân khúc tầm trung đang được giới đầu tư lướt sóng và ở thực nhắm đến nhiều nhất.</p>

<p>Đáng chú ý, <strong>gần 15% listings có giá từ 30 tỷ trở lên</strong> — cho thấy phân khúc cao cấp vẫn rất sôi động ở Đà Nẵng, chủ yếu tập trung ở Sơn Trà và Hải Châu.</p>

<hr />

<h2 id="4-giá-theo-m-sơn-trà-đắt-gấp-7-lần-hòa-vang">4. Giá Theo m²: Sơn Trà Đắt Gấp 7 Lần Hòa Vang</h2>

<p><img src="/assets/bds-danang-2026/price_per_m2.png" alt="Price per m2" /></p>

<p>Đây là góc nhìn công bằng hơn để so sánh giá trị thực giữa các quận:</p>

<table>
  <thead>
    <tr>
      <th>Quận</th>
      <th>Giá/m² trung vị</th>
      <th>Giá/m² trung bình</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>🥇 <strong>Sơn Trà</strong></td>
      <td>142 tr/m²</td>
      <td>154 tr/m²</td>
    </tr>
    <tr>
      <td>🥈 <strong>Hải Châu</strong></td>
      <td>118 tr/m²</td>
      <td>131 tr/m²</td>
    </tr>
    <tr>
      <td>🥉 <strong>Thanh Khê</strong></td>
      <td>87.5 tr/m²</td>
      <td>90.7 tr/m²</td>
    </tr>
    <tr>
      <td><strong>Ngũ Hành Sơn</strong></td>
      <td>81.1 tr/m²</td>
      <td>111.9 tr/m²</td>
    </tr>
    <tr>
      <td><strong>Cẩm Lệ</strong></td>
      <td>64.3 tr/m²</td>
      <td>68.0 tr/m²</td>
    </tr>
    <tr>
      <td><strong>Liên Chiểu</strong></td>
      <td>58.6 tr/m²</td>
      <td>61.4 tr/m²</td>
    </tr>
    <tr>
      <td><strong>Hòa Vang</strong></td>
      <td>17.9 tr/m²</td>
      <td>23.4 tr/m²</td>
    </tr>
  </tbody>
</table>

<p><strong>Sơn Trà đắt nhất</strong> với trung vị 142 tr/m² — đây là hệ quả của vị trí ven biển, mật độ resort/khách sạn cao, và quỹ đất hạn chế do địa hình bán đảo. <strong>Hòa Vang rẻ nhất</strong> (17.9 tr/m²), nhưng đây là huyện ngoại thành với đất nông nghiệp và đất ở nông thôn, không thể so sánh trực tiếp.</p>

<p>Điều thú vị: <strong>Ngũ Hành Sơn có giá trung bình (112 tr/m²) cao hơn nhiều so với trung vị (81 tr/m²)</strong> — nghĩa là có một số lô siêu cao giá đang kéo trung bình lên. Phân phối ở đây rất rộng, phản ánh sự đa dạng từ đất nền khu dân cư đến đất mặt tiền biển.</p>

<hr />

<h2 id="5-diện-tích-100m-là-standard">5. Diện Tích: 100m² Là “Standard”</h2>

<p><img src="/assets/bds-danang-2026/area_distribution.png" alt="Area Distribution" /></p>

<p>Phân tích diện tích cho thấy thị trường Đà Nẵng khá chuẩn hóa:</p>

<ul>
  <li><strong>100 m²</strong> là size phổ biến nhất (785 listings, ~15%)</li>
  <li><strong>90 m²</strong> đứng thứ hai (245 listings)</li>
  <li><strong>Hơn 60%</strong> listings nằm trong range 80–200 m²</li>
</ul>

<p>Điều này phản ánh quy hoạch đô thị khá đồng nhất — hầu hết các dự án đất nền ở Đà Nẵng được chia lô theo tiêu chuẩn 5×20m (100m²) hoặc 5×18m (90m²).</p>

<hr />

<h2 id="6-độ-tươi-của-data--vip-tier">6. Độ Tươi Của Data &amp; VIP Tier</h2>

<p><img src="/assets/bds-danang-2026/freshness_vip.png" alt="Freshness &amp; VIP" /></p>

<p><strong>94.2% listings được đăng trong 1 tuần trước</strong> — đây là điểm mạnh của dữ liệu, phản ánh “nhiệt độ” thực của thị trường tại thời điểm crawl chứ không phải tồn đọng cũ.</p>

<p>Về <strong>VIP tier</strong> (mức độ quảng cáo trả phí):</p>
<ul>
  <li>Normal (miễn phí): 5,080 listings (97.6%)</li>
  <li>Silver: 96 (1.8%)</li>
  <li>Gold: 21 (0.4%)</li>
  <li>Diamond: 10 (0.2%)</li>
</ul>

<p>Chỉ ~2.4% listings trả phí để boost — điều này có nghĩa là <strong>thị trường BĐS Đà Nẵng vẫn chủ yếu do môi giới/cá nhân đăng tự do</strong>, chưa có nhiều chủ đầu tư lớn đổ ngân sách marketing vào nền tảng này.</p>

<hr />

<h2 id="7-tương-quan-giá--diện-tích">7. Tương Quan Giá &amp; Diện Tích</h2>

<p><img src="/assets/bds-danang-2026/price_vs_area.png" alt="Price vs Area" /></p>

<p>Scatter plot và correlation matrix cho thấy:</p>

<ul>
  <li><strong>Tương quan giá × diện tích: ~0.35</strong> — tương quan dương nhưng không mạnh</li>
  <li><strong>Tương quan giá × giá/m²: ~0.72</strong> — cao hơn nhiều</li>
</ul>

<p>Kết luận: <strong>Vị trí (quận, đường, view) giải thích giá tốt hơn diện tích</strong>. Một lô 100m² ở Sơn Trà có thể đắt hơn 5–7 lần so với lô cùng size ở Hòa Vang. Diện tích lớn hơn không nhất thiết tương quan với giá cao hơn theo cách tuyến tính.</p>

<hr />

<h2 id="8-key-takeaways-cho-nhà-đầu-tư">8. Key Takeaways Cho Nhà Đầu Tư</h2>

<h3 id="-phân-khúc-hot-515-tỷ">🔴 Phân khúc Hot: 5–15 tỷ</h3>
<p>Gần 40% tổng listings, liquidity tốt nhất thị trường. Phù hợp cho cả ở thực lẫn đầu tư lướt sóng.</p>

<h3 id="-cẩn-trọng-sơn-trà--hải-châu">🟡 Cẩn Trọng: Sơn Trà &amp; Hải Châu</h3>
<p>Giá/m² cao nhất (&gt;100 tr/m²), nhưng room tăng tiếp có thể hạn chế. Phù hợp cho dài hạn, nắm giữ.</p>

<h3 id="-tiềm-năng-ngũ-hành-sơn--liên-chiểu">🟢 Tiềm Năng: Ngũ Hành Sơn &amp; Liên Chiểu</h3>
<ul>
  <li><strong>Ngũ Hành Sơn</strong>: Thị phần lớn nhất, infrastructure đang phát triển, giá/m² còn chấp nhận được</li>
  <li><strong>Liên Chiểu</strong>: Giá thấp nhất trong nội thành (~59 tr/m²), hưởng lợi từ quy hoạch cảng Liên Chiểu</li>
</ul>

<h3 id="-cơ-hội-niche-hòa-vang">🔵 Cơ Hội Niche: Hòa Vang</h3>
<p>Giá đất siêu rẻ (trung vị 17.9 tr/m²), thích hợp cho đầu tư đất nông nghiệp chuyển đổi dài hạn hoặc resort/nghỉ dưỡng.</p>

<hr />

<h2 id="hạn-chế--caveats">Hạn Chế &amp; Caveats</h2>

<p>Một số điều cần lưu ý khi đọc phân tích này:</p>

<ol>
  <li><strong>Giá listing ≠ giá thành công</strong>: Đây là giá chào, không phải giá giao dịch thực tế</li>
  <li><strong>Bias từ nền tảng</strong>: batdongsan.com.vn có thể không đại diện cho toàn bộ thị trường (bỏ sót giao dịch qua Facebook, Zalo, môi giới trực tiếp)</li>
  <li><strong>Snapshot 1 tuần</strong>: Dữ liệu phản ánh thời điểm tháng 3/2026, không track được trend theo thời gian</li>
  <li><strong>Geocoding thô</strong>: Phân loại quận dựa trên text, có thể có sai sót nhỏ</li>
</ol>

<hr />

<h2 id="kết-luận">Kết Luận</h2>

<p>Với 5,207 listings trong tay, bức tranh thị trường BĐS Đà Nẵng đầu 2026 khá rõ nét: <strong>thị trường đang sôi động</strong> (94% listings mới trong tuần), <strong>tập trung ở phân khúc 5–15 tỷ</strong>, và <strong>Sơn Trà + Hải Châu dẫn đầu về giá trị/m²</strong> trong khi <strong>Ngũ Hành Sơn dẫn đầu về khối lượng</strong>.</p>

<p>Dataset đầy đủ và notebook EDA interactive trên Kaggle — mọi người có thể tự khám phá thêm:</p>

<table>
  <tbody>
    <tr>
      <td>👉 <a href="https://www.kaggle.com/datasets/ngocphansun/da-nang-real-estate-listings">Dataset</a></td>
      <td><a href="https://www.kaggle.com/code/ngocphansun/ph-n-t-ch-c-h-i-u-t-b-s-n-ng">EDA Notebook</a></td>
    </tr>
  </tbody>
</table>

<p>Source code crawler (Go): <a href="https://github.com/phanngoc/bds-pipeline">github.com/phanngoc/bds-pipeline</a></p>]]></content><author><name></name></author><category term="data-analysis" /><category term="real-estate" /><category term="da-nang" /><category term="eda" /><category term="python" /><category term="kaggle" /><summary type="html"><![CDATA[TL;DR: Tôi crawl 5,207 listings bất động sản Đà Nẵng từ batdongsan.com.vn, chạy EDA toàn diện và rút ra một số insights thú vị: Sơn Trà đắt nhất tính theo m², Ngũ Hành Sơn sôi động nhất về khối lượng giao dịch, và phần lớn thị trường tập trung ở phân khúc 5–15 tỷ. Dataset và notebook đầy đủ trên Kaggle.]]></summary></entry><entry><title type="html">Cuộc Đua AI Assistant Cá Nhân: Từ OpenClaw 430K Dòng Code Đến PicoClaw Chạy Trên $10 Hardware</title><link href="/2026/02/11/so-sanh-kien-truc-ai-assistant-openclaw-nanobot-picoclaw.html" rel="alternate" type="text/html" title="Cuộc Đua AI Assistant Cá Nhân: Từ OpenClaw 430K Dòng Code Đến PicoClaw Chạy Trên $10 Hardware" /><published>2026-02-11T03:30:00+00:00</published><updated>2026-02-11T03:30:00+00:00</updated><id>/2026/02/11/so-sanh-kien-truc-ai-assistant-openclaw-nanobot-picoclaw</id><content type="html" xml:base="/2026/02/11/so-sanh-kien-truc-ai-assistant-openclaw-nanobot-picoclaw.html"><![CDATA[<h2 id="mở-đầu-tại-sao-cần-ai-assistant-cá-nhân">Mở đầu: Tại Sao Cần AI Assistant Cá Nhân?</h2>

<p>Bạn có bao giờ cảm thấy mệt mỏi khi phải chuyển đổi giữa ChatGPT, Claude, các ứng dụng messaging, email, và vô số công cụ khác? Hay lo ngại về việc dữ liệu cá nhân bị gửi đến các server cloud mà bạn không kiểm soát được?</p>

<p>Đó chính là lý do ra đời của <strong>Personal AI Assistant</strong> — một hệ thống AI chạy trên thiết bị của bạn, kết nối với tất cả các kênh giao tiếp bạn sử dụng hàng ngày.</p>

<p>Trong bài viết này, tôi sẽ phân tích sâu ba dự án open-source đang làm mưa làm gió trong cộng đồng: <strong>OpenClaw</strong>, <strong>nanobot</strong>, và <strong>PicoClaw</strong>. Ba dự án này đại diện cho ba triết lý thiết kế hoàn toàn khác nhau — từ “feature-rich” đến “ultra-minimal”.</p>

<hr />

<h2 id="-openclaw-con-tôm-hùm-full-stack">🦞 OpenClaw: Con Tôm Hùm Full-Stack</h2>

<blockquote>
  <p>“EXFOLIATE! EXFOLIATE!” — Slogan của OpenClaw</p>
</blockquote>

<h3 id="tổng-quan">Tổng Quan</h3>

<p><a href="https://github.com/openclaw/openclaw">OpenClaw</a> là dự án “heavyweight” nhất trong ba — một <strong>full-featured personal AI assistant</strong> với hơn 430,000 dòng code TypeScript. Đây không chỉ là chatbot, mà là một <strong>platform hoàn chỉnh</strong>.</p>

<h3 id="kiến-trúc">Kiến Trúc</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WhatsApp / Telegram / Slack / Discord / Signal / iMessage / ...
                     │
                     ▼
          ┌─────────────────────┐
          │      Gateway        │
          │   (Control Plane)   │
          │  ws://127.0.0.1     │
          └─────────┬───────────┘
                    │
          ┌─────────┼─────────┐
          │         │         │
          ▼         ▼         ▼
       Pi Agent   CLI    macOS/iOS App
       (RPC)            (Canvas, Voice)
</code></pre></div></div>

<p><strong>Core Components:</strong></p>

<ol>
  <li><strong>Gateway WebSocket Server</strong>: Control plane duy nhất quản lý tất cả sessions, channels, tools và events</li>
  <li><strong>Multi-Channel Support</strong>: WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage (BlueBubbles), Microsoft Teams, Matrix, Zalo, WebChat…</li>
  <li><strong>Companion Apps</strong>: Native apps cho macOS, iOS, Android</li>
  <li><strong>Voice Wake + Talk Mode</strong>: Always-on speech với ElevenLabs TTS</li>
  <li><strong>Live Canvas</strong>: Agent-driven visual workspace</li>
  <li><strong>Browser Control</strong>: Tích hợp Playwright/CDP để điều khiển browser</li>
</ol>

<h3 id="điểm-mạnh">Điểm Mạnh</h3>

<ul>
  <li>✅ <strong>Feature Completeness</strong>: Hầu như mọi thứ bạn cần đều có sẵn</li>
  <li>✅ <strong>Enterprise-Ready</strong>: Security policies, DM pairing, allowlists</li>
  <li>✅ <strong>Rich Ecosystem</strong>: Skills platform, browser automation, cron jobs</li>
  <li>✅ <strong>Multi-Agent Routing</strong>: Có thể route requests đến các agents khác nhau</li>
</ul>

<h3 id="trade-offs">Trade-offs</h3>

<ul>
  <li>⚠️ <strong>Footprint Lớn</strong>: &gt;1GB RAM, startup time dài</li>
  <li>⚠️ <strong>Complexity</strong>: 430K+ lines code → học curve cao</li>
  <li>⚠️ <strong>Hardware Requirements</strong>: Cần Mac Mini hoặc máy Linux tương đối mạnh</li>
</ul>

<hr />

<h2 id="-nanobot-chú-mèo-nhỏ-nhưng-có-võ">🐈 nanobot: Chú Mèo Nhỏ Nhưng Có Võ</h2>

<blockquote>
  <p>“Ultra-lightweight: 99% smaller than Clawdbot”</p>
</blockquote>

<h3 id="tổng-quan-1">Tổng Quan</h3>

<p><a href="https://github.com/HKUDS/nanobot">nanobot</a> ra đời từ câu hỏi: <em>“Liệu chúng ta có thể giữ lại 80% functionality với 1% code?”</em></p>

<p>Được phát triển bởi HKUDS (HKU Data Science), nanobot chỉ có <strong>~4,000 dòng code Python</strong> — đúng là 99% nhỏ hơn OpenClaw!</p>

<h3 id="kiến-trúc-1">Kiến Trúc</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────┐
│               nanobot                    │
├─────────────────────────────────────────┤
│  Providers   │  Channels   │   Tools    │
│  ──────────  │  ─────────  │  ───────   │
│  OpenRouter  │  Telegram   │  Web Search│
│  Anthropic   │  Discord    │  Code Exec │
│  OpenAI      │  WhatsApp   │  Memory    │
│  DeepSeek    │  Feishu     │  Schedule  │
│  Groq        │  Slack      │            │
│  vLLM        │  Email      │            │
│  Gemini      │  QQ         │            │
│  Zhipu       │  DingTalk   │            │
└─────────────────────────────────────────┘
</code></pre></div></div>

<h3 id="design-philosophy">Design Philosophy</h3>

<p>nanobot áp dụng <strong>Provider Registry Pattern</strong> — thêm một LLM provider mới chỉ cần 2 bước:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Step 1: Add to registry
</span><span class="n">ProviderSpec</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"myprovider"</span><span class="p">,</span>
    <span class="n">keywords</span><span class="o">=</span><span class="p">(</span><span class="s">"myprovider"</span><span class="p">,</span> <span class="s">"mymodel"</span><span class="p">),</span>
    <span class="n">env_key</span><span class="o">=</span><span class="s">"MYPROVIDER_API_KEY"</span><span class="p">,</span>
    <span class="n">litellm_prefix</span><span class="o">=</span><span class="s">"myprovider"</span><span class="p">,</span>
<span class="p">)</span>

<span class="c1"># Step 2: Add to config schema
</span><span class="k">class</span> <span class="nc">ProvidersConfig</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">myprovider</span><span class="p">:</span> <span class="n">ProviderConfig</span> <span class="o">=</span> <span class="n">ProviderConfig</span><span class="p">()</span>
</code></pre></div></div>

<p>Không còn if-elif chains rối rắm!</p>

<h3 id="điểm-mạnh-1">Điểm Mạnh</h3>

<ul>
  <li>✅ <strong>Research-Ready</strong>: Code sạch, dễ đọc, dễ modify</li>
  <li>✅ <strong>Fast Iteration</strong>: Startup nhanh, modify nhanh, test nhanh</li>
  <li>✅ <strong>Local LLM Support</strong>: vLLM integration cho self-hosted models</li>
  <li>✅ <strong>China-Friendly</strong>: Support Zhipu, Moonshot, Qwen, Feishu, DingTalk, QQ</li>
</ul>

<h3 id="trade-offs-1">Trade-offs</h3>

<ul>
  <li>⚠️ <strong>Python Dependencies</strong>: Vẫn cần pip/uv, có thể conflict</li>
  <li>⚠️ <strong>Memory</strong>: &gt;100MB RAM</li>
  <li>⚠️ <strong>Fewer Features</strong>: Không có Voice Wake, Canvas, Browser Control</li>
</ul>

<hr />

<h2 id="-picoclaw-con-tôm-tích-chạy-trên-10-hardware">🦐 PicoClaw: Con Tôm Tích Chạy Trên $10 Hardware</h2>

<blockquote>
  <p>“皮皮虾，我们走！” (Let’s go, mantis shrimp!)</p>
</blockquote>

<h3 id="tổng-quan-2">Tổng Quan</h3>

<p><a href="https://github.com/sipeed/picoclaw">PicoClaw</a> đẩy giới hạn đến mức cực đoan: chạy AI assistant trên hardware chỉ <strong>$10</strong> với <strong>&lt;10MB RAM</strong>!</p>

<p>Được viết bằng Go và phát triển qua quá trình <strong>AI-bootstrapping</strong> — 95% code được generate bởi chính AI agent!</p>

<h3 id="so-sánh-đáng-kinh-ngạc">So Sánh Đáng Kinh Ngạc</h3>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>OpenClaw</th>
      <th>nanobot</th>
      <th>PicoClaw</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Language</td>
      <td>TypeScript</td>
      <td>Python</td>
      <td><strong>Go</strong></td>
    </tr>
    <tr>
      <td>RAM</td>
      <td>&gt;1GB</td>
      <td>&gt;100MB</td>
      <td><strong>&lt;10MB</strong></td>
    </tr>
    <tr>
      <td>Startup (0.8GHz)</td>
      <td>&gt;500s</td>
      <td>&gt;30s</td>
      <td><strong>&lt;1s</strong></td>
    </tr>
    <tr>
      <td>Min Hardware Cost</td>
      <td>$599 (Mac)</td>
      <td>~$50 (SBC)</td>
      <td><strong>$10</strong></td>
    </tr>
    <tr>
      <td>LOC</td>
      <td>430K+</td>
      <td>~4K</td>
      <td>~2K</td>
    </tr>
  </tbody>
</table>

<h3 id="kiến-trúc-2">Kiến Trúc</h3>

<p>PicoClaw tận dụng đặc tính của Go:</p>
<ul>
  <li><strong>Single Binary</strong>: Compile một lần, chạy everywhere (RISC-V, ARM, x86)</li>
  <li><strong>No Runtime Dependencies</strong>: Không cần Python, Node.js, hay bất cứ gì khác</li>
  <li><strong>Minimal Memory</strong>: Go’s efficient garbage collector</li>
</ul>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Một binary duy nhất, cross-compile cho mọi platform</span>
<span class="nb">make</span> <span class="n">build</span><span class="o">-</span><span class="n">all</span>  <span class="c">// → picoclaw-linux-amd64</span>
                <span class="c">//    picoclaw-linux-arm64</span>
                <span class="c">//    picoclaw-linux-riscv64</span>
</code></pre></div></div>

<h3 id="use-cases-độc-đáo">Use Cases Độc Đáo</h3>

<p>PicoClaw mở ra những khả năng mà trước đây không thể:</p>

<ol>
  <li><strong>$9.9 LicheeRV-Nano</strong>: Biến board RISC-V $10 thành Home Assistant</li>
  <li><strong>$30-100 NanoKVM</strong>: Automated Server Maintenance</li>
  <li><strong>$50-100 MaixCAM</strong>: Smart Monitoring với AI Vision</li>
</ol>

<h3 id="điểm-mạnh-2">Điểm Mạnh</h3>

<ul>
  <li>✅ <strong>True Portability</strong>: Một binary chạy mọi nơi</li>
  <li>✅ <strong>Instant Startup</strong>: 1 giây boot, không chờ đợi</li>
  <li>✅ <strong>Edge Deployment</strong>: Chạy trên IoT devices</li>
  <li>✅ <strong>AI-Generated</strong>: 95% code do AI viết, dễ maintain</li>
</ul>

<h3 id="trade-offs-2">Trade-offs</h3>

<ul>
  <li>⚠️ <strong>Limited Channels</strong>: Chỉ Telegram và Discord (currently)</li>
  <li>⚠️ <strong>Basic Features</strong>: Không có Voice, Canvas, Browser Control</li>
  <li>⚠️ <strong>Go Ecosystem</strong>: Ít libraries hơn Python/Node</li>
</ul>

<hr />

<h2 id="-deep-comparison-chọn-cái-nào">📊 Deep Comparison: Chọn Cái Nào?</h2>

<h3 id="bảng-so-sánh-toàn-diện">Bảng So Sánh Toàn Diện</h3>

<table>
  <thead>
    <tr>
      <th>Feature</th>
      <th>OpenClaw 🦞</th>
      <th>nanobot 🐈</th>
      <th>PicoClaw 🦐</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Core</strong></td>
      <td> </td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>Lines of Code</td>
      <td>430K+</td>
      <td>~4K</td>
      <td>~2K</td>
    </tr>
    <tr>
      <td>Language</td>
      <td>TypeScript</td>
      <td>Python</td>
      <td>Go</td>
    </tr>
    <tr>
      <td>Memory</td>
      <td>&gt;1GB</td>
      <td>&gt;100MB</td>
      <td>&lt;10MB</td>
    </tr>
    <tr>
      <td>Startup</td>
      <td>&gt;500s</td>
      <td>&gt;30s</td>
      <td>&lt;1s</td>
    </tr>
    <tr>
      <td><strong>Channels</strong></td>
      <td> </td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>WhatsApp</td>
      <td>✅</td>
      <td>✅</td>
      <td>🚧</td>
    </tr>
    <tr>
      <td>Telegram</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Discord</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Slack</td>
      <td>✅</td>
      <td>✅</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>iMessage</td>
      <td>✅</td>
      <td>❌</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>Signal</td>
      <td>✅</td>
      <td>❌</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>Email</td>
      <td>❌</td>
      <td>✅</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>China Apps</td>
      <td>Zalo</td>
      <td>Feishu, QQ, DingTalk</td>
      <td>❌</td>
    </tr>
    <tr>
      <td><strong>Features</strong></td>
      <td> </td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>Voice Wake</td>
      <td>✅</td>
      <td>❌</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>Browser Control</td>
      <td>✅</td>
      <td>❌</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>Live Canvas</td>
      <td>✅</td>
      <td>❌</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>Local LLM</td>
      <td>❌</td>
      <td>✅ (vLLM)</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>Multi-Agent</td>
      <td>✅</td>
      <td>❌</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>Web Search</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td><strong>Deployment</strong></td>
      <td> </td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>Min Cost</td>
      <td>$599</td>
      <td>$50</td>
      <td>$10</td>
    </tr>
    <tr>
      <td>Docker</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>IoT/Edge</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
  </tbody>
</table>

<h3 id="khi-nào-chọn-gì">Khi Nào Chọn Gì?</h3>

<h4 id="chọn-openclaw-khi">Chọn <strong>OpenClaw</strong> khi:</h4>
<ul>
  <li>Bạn cần <strong>full ecosystem</strong> với voice, canvas, browser control</li>
  <li>Bạn muốn <strong>native apps</strong> cho macOS/iOS/Android</li>
  <li>Bạn cần <strong>enterprise-grade security</strong> (DM policies, pairing)</li>
  <li>Bạn có <strong>hardware mạnh</strong> (Mac Mini, Linux server)</li>
  <li>Bạn cần support <strong>nhiều channels</strong> (đặc biệt iMessage, Signal)</li>
</ul>

<h4 id="chọn-nanobot-khi">Chọn <strong>nanobot</strong> khi:</h4>
<ul>
  <li>Bạn đang <strong>research/experiment</strong> và cần code dễ đọc</li>
  <li>Bạn muốn <strong>modify/extend</strong> core functionality</li>
  <li>Bạn cần <strong>local LLM</strong> support (vLLM)</li>
  <li>Bạn ở <strong>China</strong> và cần Feishu, QQ, DingTalk</li>
  <li>Bạn muốn <strong>fast iteration</strong> cycle</li>
</ul>

<h4 id="chọn-picoclaw-khi">Chọn <strong>PicoClaw</strong> khi:</h4>
<ul>
  <li>Bạn cần chạy trên <strong>IoT/edge devices</strong> ($10-50)</li>
  <li>Bạn cần <strong>instant startup</strong> (&lt;1s)</li>
  <li>Bạn muốn <strong>single binary</strong> deployment</li>
  <li>Memory là <strong>constraint</strong> (&lt;10MB available)</li>
  <li>Bạn thích <strong>Go</strong> và muốn contribute</li>
</ul>

<hr />

<h2 id="-the-evolution-tree">🧬 The Evolution Tree</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>             OpenClaw (2024)
             ───────────────
             Full-featured, 430K LOC
             TypeScript, &gt;1GB RAM
                    │
                    │ "Can we do 80% with 1%?"
                    ▼
             nanobot (2026)
             ───────────────
             Ultra-light, 4K LOC
             Python, &gt;100MB RAM
                    │
                    │ "Can we run on $10 hardware?"
                    ▼
             PicoClaw (2026)
             ───────────────
             Edge-ready, 2K LOC
             Go, &lt;10MB RAM
</code></pre></div></div>

<p>Đây là một ví dụ tuyệt vời về <strong>evolutionary design</strong>: mỗi thế hệ học từ thế hệ trước, tập trung vào một góc cạnh khác nhau của vấn đề.</p>

<hr />

<h2 id="-tương-lai">🔮 Tương Lai</h2>

<h3 id="convergence-hay-divergence">Convergence hay Divergence?</h3>

<p>Tôi dự đoán ba dự án này sẽ <strong>diverge</strong> thay vì converge:</p>

<ol>
  <li><strong>OpenClaw</strong> sẽ tiếp tục thêm features, trở thành “macOS of AI assistants”</li>
  <li><strong>nanobot</strong> sẽ focus vào research community và local LLM integration</li>
  <li><strong>PicoClaw</strong> sẽ mở rộng vào embedded systems và IoT</li>
</ol>

<h3 id="whats-next">What’s Next?</h3>

<ul>
  <li><strong>Federated Learning</strong>: Học từ nhiều devices mà không share raw data</li>
  <li><strong>On-Device LLMs</strong>: Khi Llama 4 1B đủ mạnh để chạy on-edge</li>
  <li><strong>Hardware Acceleration</strong>: RISC-V AI extensions cho PicoClaw</li>
  <li><strong>Protocol Standardization</strong>: Có thể sẽ có chuẩn chung cho personal AI assistants</li>
</ul>

<hr />

<h2 id="-kết-luận">💡 Kết Luận</h2>

<p>Cả ba dự án đều có chỗ đứng riêng trong ecosystem personal AI:</p>

<table>
  <thead>
    <tr>
      <th>Dự án</th>
      <th>Một Câu Mô Tả</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>OpenClaw</strong></td>
      <td>“I want everything, and I have the hardware”</td>
    </tr>
    <tr>
      <td><strong>nanobot</strong></td>
      <td>“I want to understand and modify”</td>
    </tr>
    <tr>
      <td><strong>PicoClaw</strong></td>
      <td>“I want to run it anywhere”</td>
    </tr>
  </tbody>
</table>

<p>Không có “best” choice — chỉ có <strong>right choice cho use case của bạn</strong>.</p>

<p>Nếu bạn đang bắt đầu và có Mac, hãy thử OpenClaw. Nếu bạn muốn hack và learn, hãy đọc nanobot code. Nếu bạn có một con Raspberry Pi Zero hoặc ESP32 nằm không, hãy thử PicoClaw.</p>

<hr />

<h2 id="-references">📚 References</h2>

<ul>
  <li><a href="https://github.com/openclaw/openclaw">OpenClaw GitHub</a></li>
  <li><a href="https://docs.openclaw.ai">OpenClaw Docs</a></li>
  <li><a href="https://github.com/HKUDS/nanobot">nanobot GitHub</a></li>
  <li><a href="https://github.com/sipeed/picoclaw">PicoClaw GitHub</a></li>
  <li><a href="https://www.aliexpress.com/item/1005006519668532.html">LicheeRV-Nano - $9.9 RISC-V Board</a></li>
</ul>

<hr />

<p><em>Bạn đang dùng AI assistant nào? Hãy chia sẻ experience của bạn trong comments!</em> 🚀</p>]]></content><author><name></name></author><category term="ai-assistant" /><category term="personal-ai" /><category term="architecture" /><category term="open-source" /><summary type="html"><![CDATA[Mở đầu: Tại Sao Cần AI Assistant Cá Nhân?]]></summary></entry><entry><title type="html">Hybrid Memory Search Architecture Openclaw</title><link href="/2026/02/08/hybrid-memory-search-architecture-openclaw.html" rel="alternate" type="text/html" title="Hybrid Memory Search Architecture Openclaw" /><published>2026-02-08T00:00:00+00:00</published><updated>2026-02-08T00:00:00+00:00</updated><id>/2026/02/08/hybrid-memory-search-architecture-openclaw</id><content type="html" xml:base="/2026/02/08/hybrid-memory-search-architecture-openclaw.html"><![CDATA[<h1 id="deep-dive-hybrid-memory-search-architecture-trong-openclaw">Deep Dive: Hybrid Memory Search Architecture trong OpenClaw</h1>

<blockquote>
  <p>Bài viết phân tích chuyên sâu kiến trúc Memory Search của OpenClaw – hệ thống kết hợp Vector Search (semantic) và Full-Text Search (BM25) để tạo ra một bộ nhớ dài hạn thông minh cho AI agent. Dành cho developer muốn hiểu cách xây dựng Retrieval-Augmented Generation (RAG) system trong thực tế production.</p>
</blockquote>

<hr />

<h2 id="mục-lục">Mục lục</h2>

<ol>
  <li><a href="#1-vấn-đề-cần-giải-quyết">Vấn đề cần giải quyết</a></li>
  <li><a href="#2-kiến-trúc-tổng-quan">Kiến trúc tổng quan</a></li>
  <li><a href="#3-giai-đoạn-0-ingestion-pipeline">Giai đoạn 0: Ingestion Pipeline</a></li>
  <li><a href="#4-giai-đoạn-1-dual-search----vector--fts">Giai đoạn 1: Dual Search – Vector + FTS</a></li>
  <li><a href="#5-giai-đoạn-2-score-fusion----weighted-sum">Giai đoạn 2: Score Fusion – Weighted Sum</a></li>
  <li><a href="#6-giai-đoạn-3-post-processing">Giai đoạn 3: Post-Processing</a></li>
  <li><a href="#7-sqlite-schema----ba-bảng-hai-virtual-table">SQLite Schema – Ba bảng, hai virtual table</a></li>
  <li><a href="#8-embedding-provider-system">Embedding Provider System</a></li>
  <li><a href="#9-sync-lifecycle--atomic-reindex">Sync Lifecycle &amp; Atomic Reindex</a></li>
  <li><a href="#10-session-memory--pre-compaction-flush">Session Memory &amp; Pre-Compaction Flush</a></li>
  <li><a href="#11-fallback-architecture">Fallback Architecture</a></li>
  <li><a href="#12-sơ-đồ-kiến-trúc-hoàn-chỉnh">Sơ đồ kiến trúc hoàn chỉnh</a></li>
  <li><a href="#13-lessons-learned--trade-offs">Lessons Learned &amp; Trade-offs</a></li>
</ol>

<hr />

<h2 id="1-vấn-đề-cần-giải-quyết">1. Vấn đề cần giải quyết</h2>

<p>AI agent xử lý conversation trong context window có giới hạn. Khi conversation kéo dài, các thông tin quan trọng bị mất do compaction. Memory system giải quyết bài toán này: <strong>duy trì kiến thức dài hạn bằng cách lưu thông tin ra file Markdown, rồi tìm kiếm lại khi cần</strong>.</p>

<p>Nhưng “tìm kiếm” không đơn giản. Có hai kiểu tìm kiếm bổ sung cho nhau:</p>

<ul>
  <li><strong>Semantic search</strong> (vector): “Tìm nội dung liên quan đến thanh toán” – hiểu <em>nghĩa</em>, không cần match chính xác từ.</li>
  <li><strong>Keyword search</strong> (FTS): “payment_processor” – match chính xác tên biến, tên hàm, thuật ngữ kỹ thuật.</li>
</ul>

<p>Mỗi kiểu có điểm mạnh riêng. Hybrid search kết hợp cả hai.</p>

<hr />

<h2 id="2-kiến-trúc-tổng-quan">2. Kiến trúc tổng quan</h2>

<p>Toàn bộ hệ thống Memory nằm trong <code class="language-plaintext highlighter-rouge">src/memory/</code> với khoảng 40 file TypeScript. Entry point là <code class="language-plaintext highlighter-rouge">MemoryIndexManager</code> – một singleton per-agent, cached theo key <code class="language-plaintext highlighter-rouge">agentId:workspaceDir:settingsHash</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                    ┌──────────────────────────────┐
                    │     MemoryIndexManager        │
                    │  (singleton per agent config) │
                    └──────────┬───────────────────┘
                               │
              ┌────────────────┼────────────────┐
              │                │                │
    ┌─────────▼──────┐  ┌─────▼──────┐  ┌──────▼───────┐
    │  Ingestion      │  │   Search    │  │  Lifecycle   │
    │  Pipeline       │  │   Engine    │  │  Management  │
    │                 │  │             │  │              │
    │ - File watcher  │  │ - Vector    │  │ - Sync       │
    │ - Chunking      │  │ - FTS/BM25  │  │ - Reindex    │
    │ - Embedding     │  │ - Fusion    │  │ - Flush      │
    │ - Caching       │  │ - Filtering │  │ - Cleanup    │
    └─────────────────┘  └────────────┘  └──────────────┘
              │                │                │
              └────────────────┼────────────────┘
                               │
                    ┌──────────▼───────────────┐
                    │     SQLite Database       │
                    │                           │
                    │ tables: meta, files,      │
                    │         chunks,           │
                    │         embedding_cache   │
                    │ virtual: chunks_fts (FTS5)│
                    │          chunks_vec (vec0)│
                    └───────────────────────────┘
</code></pre></div></div>

<hr />

<h2 id="3-giai-đoạn-0-ingestion-pipeline">3. Giai đoạn 0: Ingestion Pipeline</h2>

<p>Trước khi search được, dữ liệu phải được <strong>ingest</strong> vào database. Đây là pipeline đầy đủ:</p>

<h3 id="31-thu-thập-file-nguồn">3.1 Thu thập file nguồn</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>listMemoryFiles(workspaceDir)
    │
    ├── MEMORY.md          (workspace root)
    ├── memory.md          (alternative)
    ├── memory/**/*.md     (recursive walk)
    └── extraPaths (config) (absolute or relative)

    Filter: chỉ .md, bỏ qua symlinks
    Dedup: theo realpath
</code></pre></div></div>

<p>Hệ thống cũng index <strong>session transcripts</strong> – file <code class="language-plaintext highlighter-rouge">.jsonl</code> chứa lịch sử conversation, nằm tại <code class="language-plaintext highlighter-rouge">~/.openclaw/agents/&lt;agentId&gt;/sessions/</code>.</p>

<h3 id="32-chunking-chia-văn-bản-thành-đoạn">3.2 Chunking: Chia văn bản thành đoạn</h3>

<p>File <code class="language-plaintext highlighter-rouge">internal.ts</code> – hàm <code class="language-plaintext highlighter-rouge">chunkMarkdown()</code>:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Default config</span>
<span class="nx">chunking</span><span class="p">:</span> <span class="p">{</span>
  <span class="nl">tokens</span><span class="p">:</span> <span class="mi">400</span><span class="p">,</span>    <span class="c1">// target chunk size</span>
  <span class="nx">overlap</span><span class="p">:</span> <span class="mi">80</span>     <span class="c1">// overlap giữa 2 chunk liên tiếp</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Thuật toán:</strong></p>

<ol>
  <li>Quy đổi token → char: <code class="language-plaintext highlighter-rouge">maxChars = tokens * 4 = 1600 chars</code>, <code class="language-plaintext highlighter-rouge">overlapChars = overlap * 4 = 320 chars</code></li>
  <li>Duyệt từng dòng, tích lũy vào buffer hiện tại</li>
  <li>Khi buffer vượt <code class="language-plaintext highlighter-rouge">maxChars</code> → flush thành 1 chunk</li>
  <li>Sau flush, giữ lại các dòng cuối (tổng ≤ <code class="language-plaintext highlighter-rouge">overlapChars</code>) làm overlap cho chunk tiếp theo</li>
  <li>Dòng quá dài → cắt thành segments <code class="language-plaintext highlighter-rouge">maxChars</code> ký tự</li>
</ol>

<p>Mỗi chunk lưu: <code class="language-plaintext highlighter-rouge">startLine</code>, <code class="language-plaintext highlighter-rouge">endLine</code>, <code class="language-plaintext highlighter-rouge">text</code>, <code class="language-plaintext highlighter-rouge">hash</code> (SHA-256 của text).</p>

<p><strong>Tại sao overlap?</strong> Để tránh mất ngữ cảnh ở biên chunk. Nếu một ý nằm trải qua ranh giới giữa 2 chunk, overlap đảm bảo nó xuất hiện đầy đủ ở ít nhất 1 chunk.</p>

<h3 id="33-embedding-chuyển-text--vector">3.3 Embedding: Chuyển text → vector</h3>

<p>Mỗi chunk text được gửi qua embedding provider để tạo vector số thực:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"Lỗi xử lý thanh toán khi amount = 0"
        │
        ▼
  Embedding API (OpenAI / Gemini / Voyage / Local)
        │
        ▼
  [0.012, -0.034, 0.089, ..., 0.001]  ← N chiều (thay đổi theo model)
        │
        ▼
  sanitizeAndNormalizeEmbedding()
    - NaN/Infinity → 0
    - L2 normalize to unit length
</code></pre></div></div>

<p><strong>Embedding cache</strong>: Trước khi gọi API, hệ thống kiểm tra bảng <code class="language-plaintext highlighter-rouge">embedding_cache</code> theo key <code class="language-plaintext highlighter-rouge">(provider, model, provider_key, hash)</code>. Nếu text không đổi (cùng SHA-256 hash) và cùng provider config → dùng lại embedding cũ, tiết kiệm API call.</p>

<h3 id="34-lưu-trữ">3.4 Lưu trữ</h3>

<p>Mỗi chunk được lưu vào 3 nơi đồng thời:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. chunks table     ← text + JSON embedding + metadata
2. chunks_vec       ← Float32Array blob (cho sqlite-vec vector search)
3. chunks_fts       ← text (cho FTS5 keyword search)
</code></pre></div></div>

<hr />

<h2 id="4-giai-đoạn-1-dual-search--vector--fts">4. Giai đoạn 1: Dual Search – Vector + FTS</h2>

<p>Khi user query đến, hai search engine chạy <strong>song song</strong>:</p>

<h3 id="41-vector-search-semantic">4.1 Vector Search (Semantic)</h3>

<p>File <code class="language-plaintext highlighter-rouge">manager-search.ts</code> – hàm <code class="language-plaintext highlighter-rouge">searchVector()</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User Query: "Lỗi logic code xử lý thanh toán"
    │
    ▼
embedQuery(text)  ← cùng embedding provider đã dùng lúc index
    │
    ▼
queryVec: [0.023, -0.045, ...]
    │
    ▼
┌─ Path A: sqlite-vec available ──────────────────────────┐
│  SQL:                                                     │
│  SELECT c.*, vec_distance_cosine(v.embedding, ?) AS dist │
│  FROM chunks_vec v JOIN chunks c ON c.id = v.id          │
│  WHERE c.model = ?                                        │
│  ORDER BY dist ASC  LIMIT ?                               │
│                                                           │
│  score = 1 - dist  (cosine distance → similarity)        │
└──────────────────────────────────────────────────────────┘
    │
    │ (fallback nếu sqlite-vec không load được)
    ▼
┌─ Path B: JS fallback ───────────────────────────────────┐
│  Load ALL chunks from DB                                  │
│  Parse JSON embeddings                                    │
│  Compute cosineSimilarity() in JavaScript                │
│  Sort + take top-N                                        │
│  (chậm nhưng luôn hoạt động)                             │
└──────────────────────────────────────────────────────────┘
</code></pre></div></div>

<p><strong>Cosine Similarity</strong> đo sự tương đồng về ngữ nghĩa giữa hai vector:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// internal.ts</span>
<span class="kd">function</span> <span class="nx">cosineSimilarity</span><span class="p">(</span><span class="nx">a</span><span class="p">:</span> <span class="kr">number</span><span class="p">[],</span> <span class="nx">b</span><span class="p">:</span> <span class="kr">number</span><span class="p">[]):</span> <span class="kr">number</span> <span class="p">{</span>
  <span class="c1">// dot(a,b) / (||a|| * ||b||)</span>
  <span class="c1">// Range: -1 (opposite) to 1 (identical)</span>
  <span class="c1">// Do vectors đã L2-normalized nên dot product = cosine similarity</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="42-keyword-search-ftsbm25">4.2 Keyword Search (FTS/BM25)</h3>

<p>File <code class="language-plaintext highlighter-rouge">hybrid.ts</code> – hàm <code class="language-plaintext highlighter-rouge">buildFtsQuery()</code> + file <code class="language-plaintext highlighter-rouge">manager-search.ts</code> – hàm <code class="language-plaintext highlighter-rouge">searchKeyword()</code>:</p>

<p><strong>Bước 1: Tokenization (đơn giản)</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// hybrid.ts</span>
<span class="kd">function</span> <span class="nx">buildFtsQuery</span><span class="p">(</span><span class="nx">raw</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="kr">string</span> <span class="o">|</span> <span class="kc">null</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">tokens</span> <span class="o">=</span> <span class="nx">raw</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/</span><span class="se">[</span><span class="sr">A-Za-z0-9_</span><span class="se">]</span><span class="sr">+/g</span><span class="p">);</span>  <span class="c1">// regex, không phải NLP tokenizer</span>
  <span class="kd">const</span> <span class="nx">quoted</span> <span class="o">=</span> <span class="nx">tokens</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">t</span> <span class="o">=&gt;</span> <span class="s2">`"</span><span class="p">${</span><span class="nx">t</span><span class="p">}</span><span class="s2">"`</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">quoted</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2"> AND </span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// Input:  "Lỗi logic code xử lý thanh toán"</span>
<span class="c1">// Output: "logic" AND "code" AND "thanh" AND "to" AND "n"</span>
<span class="c1">// (Lưu ý: regex chỉ lấy [A-Za-z0-9_], Unicode chars bị loại)</span>
</code></pre></div></div>

<p><strong>Bước 2: FTS5 BM25 query</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">id</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="k">source</span><span class="p">,</span> <span class="n">start_line</span><span class="p">,</span> <span class="n">end_line</span><span class="p">,</span> <span class="nb">text</span><span class="p">,</span>
       <span class="n">bm25</span><span class="p">(</span><span class="n">chunks_fts</span><span class="p">)</span> <span class="k">AS</span> <span class="n">rank</span>
  <span class="k">FROM</span> <span class="n">chunks_fts</span>
 <span class="k">WHERE</span> <span class="n">chunks_fts</span> <span class="k">MATCH</span> <span class="s1">'"logic" AND "code"'</span>
   <span class="k">AND</span> <span class="n">model</span> <span class="o">=</span> <span class="o">?</span>
 <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">rank</span> <span class="k">ASC</span>    <span class="c1">-- BM25: lower = better match</span>
 <span class="k">LIMIT</span> <span class="o">?</span>
</code></pre></div></div>

<p><strong>Bước 3: Rank → Score normalization</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// hybrid.ts</span>
<span class="kd">function</span> <span class="nx">bm25RankToScore</span><span class="p">(</span><span class="nx">rank</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">number</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">normalized</span> <span class="o">=</span> <span class="nb">Number</span><span class="p">.</span><span class="nb">isFinite</span><span class="p">(</span><span class="nx">rank</span><span class="p">)</span> <span class="p">?</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">rank</span><span class="p">)</span> <span class="p">:</span> <span class="mi">999</span><span class="p">;</span>
  <span class="k">return</span> <span class="mi">1</span> <span class="o">/</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="nx">normalized</span><span class="p">);</span>
  <span class="c1">// rank=0 → score=1.0 (best)</span>
  <span class="c1">// rank=1 → score=0.5</span>
  <span class="c1">// rank=9 → score=0.1</span>
  <span class="c1">// Asymptotically approaches 0</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="43-candidate-pool">4.3 Candidate Pool</h3>

<p>Cả hai search đều fetch nhiều hơn số kết quả cần thiết:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>candidateMultiplier = 4  (default)
maxResults = 6           (default)

→ Mỗi search engine fetch: min(200, max(1, 6 * 4)) = 24 candidates
</code></pre></div></div>

<p>Mục đích: tạo pool đủ lớn để fusion algorithm có “nguyên liệu” chất lượng.</p>

<hr />

<h2 id="5-giai-đoạn-2-score-fusion--weighted-sum">5. Giai đoạn 2: Score Fusion – Weighted Sum</h2>

<p>File <code class="language-plaintext highlighter-rouge">hybrid.ts</code> – hàm <code class="language-plaintext highlighter-rouge">mergeHybridResults()</code>:</p>

<p><strong>Quan trọng: Đây là Weighted Sum, KHÔNG PHẢI Reciprocal Rank Fusion (RRF).</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Vector Results:  [{id: "c1", vectorScore: 0.92}, {id: "c2", vectorScore: 0.85}, ...]
Keyword Results: [{id: "c2", textScore: 0.78}, {id: "c5", textScore: 0.65}, ...]
                          │
                          ▼
              ┌───────────────────────┐
              │   Merge by chunk ID   │
              └───────────┬───────────┘
                          │
        ┌─────────────────┼─────────────────┐
        │                 │                 │
  id="c1"           id="c2"           id="c5"
  vec=0.92          vec=0.85          vec=0.00  ← chỉ có keyword
  text=0.00         text=0.78         text=0.65
  (chỉ có vector)   (có cả hai)
        │                 │                 │
        ▼                 ▼                 ▼
  score = 0.7*0.92   0.7*0.85          0.7*0.00
        + 0.3*0.00   + 0.3*0.78        + 0.3*0.65
        = 0.644      = 0.829           = 0.195
</code></pre></div></div>

<p><strong>Thuật toán chi tiết:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">mergeHybridResults</span><span class="p">({</span> <span class="nx">vector</span><span class="p">,</span> <span class="nx">keyword</span><span class="p">,</span> <span class="nx">vectorWeight</span><span class="p">,</span> <span class="nx">textWeight</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">byId</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">();</span>

  <span class="c1">// 1. Insert tất cả vector results, textScore = 0</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">r</span> <span class="k">of</span> <span class="nx">vector</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">byId</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span><span class="nx">r</span><span class="p">,</span> <span class="na">vectorScore</span><span class="p">:</span> <span class="nx">r</span><span class="p">.</span><span class="nx">vectorScore</span><span class="p">,</span> <span class="na">textScore</span><span class="p">:</span> <span class="mi">0</span> <span class="p">});</span>
  <span class="p">}</span>

  <span class="c1">// 2. Merge keyword results</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">r</span> <span class="k">of</span> <span class="nx">keyword</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">existing</span> <span class="o">=</span> <span class="nx">byId</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">existing</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">existing</span><span class="p">.</span><span class="nx">textScore</span> <span class="o">=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">textScore</span><span class="p">;</span>  <span class="c1">// ← chunk có cả 2 signals</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="nx">byId</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span><span class="nx">r</span><span class="p">,</span> <span class="na">vectorScore</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="na">textScore</span><span class="p">:</span> <span class="nx">r</span><span class="p">.</span><span class="nx">textScore</span> <span class="p">});</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="c1">// 3. Compute final score</span>
  <span class="k">return</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">byId</span><span class="p">.</span><span class="nx">values</span><span class="p">())</span>
    <span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">entry</span> <span class="o">=&gt;</span> <span class="p">({</span>
      <span class="p">...</span><span class="nx">entry</span><span class="p">,</span>
      <span class="na">score</span><span class="p">:</span> <span class="nx">vectorWeight</span> <span class="o">*</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">vectorScore</span> <span class="o">+</span> <span class="nx">textWeight</span> <span class="o">*</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">textScore</span>
    <span class="p">}))</span>
    <span class="p">.</span><span class="nx">toSorted</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">b</span><span class="p">.</span><span class="nx">score</span> <span class="o">-</span> <span class="nx">a</span><span class="p">.</span><span class="nx">score</span><span class="p">);</span>  <span class="c1">// descending</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Tại sao Weighted Sum mà không phải RRF?</strong></p>

<p>Project docs thừa nhận: <em>“This is not IR-theory perfect. RRF or score normalization would be potential future improvements.”</em> Weighted Sum đơn giản, nhanh, dễ debug, và đủ tốt cho use case memory recall. Trade-off có ý thức.</p>

<h3 id="phân-tích-ưunhược">Phân tích ưu/nhược</h3>

<table>
  <thead>
    <tr>
      <th>Weighted Sum</th>
      <th>RRF</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Đơn giản, O(n)</td>
      <td>Phức tạp hơn, cần rank position</td>
    </tr>
    <tr>
      <td>Phụ thuộc vào scale của score</td>
      <td>Bất biến với score scale</td>
    </tr>
    <tr>
      <td>Cần score calibrated tốt</td>
      <td>Chỉ cần rank order</td>
    </tr>
    <tr>
      <td>Fast, no overhead</td>
      <td>Thêm 1 pass sort mỗi list</td>
    </tr>
  </tbody>
</table>

<p>Trong context này, cả vectorScore (cosine similarity, range 0-1) và textScore (BM25 normalized, range 0-1) đã được normalize về cùng range, nên Weighted Sum hoạt động ổn.</p>

<hr />

<h2 id="6-giai-đoạn-3-post-processing">6. Giai đoạn 3: Post-Processing</h2>

<p>Sau fusion, kết quả đi qua 2 bộ lọc:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Merged results (sorted by score descending)
    │
    ▼
Filter: score &gt;= minScore (default: 0.35)
    │
    ▼
Limit: top maxResults (default: 6)
    │
    ▼
Truncate snippet: max 700 chars (SNIPPET_MAX_CHARS)
    │
    ▼
Return MemorySearchResult[] {
  path, startLine, endLine,
  score (0-1), snippet, source,
  citation? ("path#L5-L20")
}
</code></pre></div></div>

<hr />

<h2 id="7-sqlite-schema--ba-bảng-hai-virtual-table">7. SQLite Schema – Ba bảng, hai virtual table</h2>

<p>File <code class="language-plaintext highlighter-rouge">memory-schema.ts</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────────────────────────────────────────────────┐
│                     SQLite Database                       │
│              (~/.openclaw/memory/{agentId}.sqlite)        │
│                                                           │
│  ┌──────────────────────────────────────────────────┐    │
│  │ meta (key-value store)                           │    │
│  │   key: "memory_index_meta_v1"                    │    │
│  │   value: {model, provider, vectorDims, ...}      │    │
│  └──────────────────────────────────────────────────┘    │
│                                                           │
│  ┌──────────────────────────────────────────────────┐    │
│  │ files (tracks indexed source files)              │    │
│  │   path TEXT PK, source, hash, mtime, size        │    │
│  └──────────────────────────────────────────────────┘    │
│                                                           │
│  ┌──────────────────────────────────────────────────┐    │
│  │ chunks (text + embedding data)                   │    │
│  │   id TEXT PK, path, source, start_line,          │    │
│  │   end_line, hash, model, text,                   │    │
│  │   embedding TEXT (JSON number[]),                 │    │
│  │   updated_at                                      │    │
│  │   INDEX: idx_chunks_path, idx_chunks_source      │    │
│  └──────────────────────────────────────────────────┘    │
│                                                           │
│  ┌──────────────────────────────────────────────────┐    │
│  │ embedding_cache (avoid re-embedding)             │    │
│  │   PK: (provider, model, provider_key, hash)      │    │
│  │   embedding TEXT, dims INT, updated_at            │    │
│  └──────────────────────────────────────────────────┘    │
│                                                           │
│  ┌──────────────────────────────────────────────────┐    │
│  │ chunks_fts (FTS5 virtual table)                  │    │
│  │   text (indexed), id/path/source/model/          │    │
│  │   start_line/end_line (UNINDEXED)                │    │
│  │   ⚠️ Created conditionally, graceful degradation │    │
│  └──────────────────────────────────────────────────┘    │
│                                                           │
│  ┌──────────────────────────────────────────────────┐    │
│  │ chunks_vec (vec0 via sqlite-vec extension)       │    │
│  │   id TEXT PK, embedding FLOAT[N]                 │    │
│  │   N = dims from embedding provider               │    │
│  │   ⚠️ Created lazily when dims known              │    │
│  │   ⚠️ Dropped &amp; recreated if dims change          │    │
│  └──────────────────────────────────────────────────┘    │
│                                                           │
└──────────────────────────────────────────────────────────┘
</code></pre></div></div>

<p><strong>Key insight:</strong> Embedding lưu ở <strong>hai nơi</strong>: <code class="language-plaintext highlighter-rouge">chunks.embedding</code> là JSON string (cho JS fallback), <code class="language-plaintext highlighter-rouge">chunks_vec.embedding</code> là Float32Array blob (cho sqlite-vec accelerated search). Redundancy có chủ đích.</p>

<hr />

<h2 id="8-embedding-provider-system">8. Embedding Provider System</h2>

<p>File <code class="language-plaintext highlighter-rouge">embeddings.ts</code> – Hệ thống multi-provider với auto-selection:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Provider Selection (provider = "auto")
    │
    ├── 1. Local (node-llama-cpp + GGUF model)
    │     ├── Kiểm tra: modelPath configured + file exists on disk?
    │     ├── Default model: embeddinggemma-300M-Q8_0.gguf (~0.6GB)
    │     └── Ưu điểm: không cần API key, chạy offline
    │
    ├── 2. OpenAI
    │     ├── Model: text-embedding-3-small
    │     ├── Endpoint: /v1/embeddings
    │     └── Cần: OPENAI_API_KEY
    │
    ├── 3. Gemini
    │     ├── Model: gemini-embedding-001
    │     ├── Phân biệt: taskType "RETRIEVAL_QUERY" vs "RETRIEVAL_DOCUMENT"
    │     └── Cần: GEMINI_API_KEY
    │
    └── 4. Voyage
          ├── Model: voyage-4-large
          ├── Phân biệt: input_type "query" vs "document"
          └── Cần: VOYAGE_API_KEY

Nếu tất cả fail → throw aggregated error
</code></pre></div></div>

<p><strong>Fallback mechanism:</strong> Nếu primary provider fail, config <code class="language-plaintext highlighter-rouge">fallback</code> (e.g., <code class="language-plaintext highlighter-rouge">"openai"</code>) sẽ tự động kick in. Ngoài ra có runtime fallback – nếu embedding errors xảy ra giữa sync, hệ thống tự thử provider khác.</p>

<p><strong>Batch embedding APIs:</strong> Cho indexing lớn, hệ thống hỗ trợ async batch processing qua OpenAI/Gemini/Voyage Batch API (max 50,000 requests/batch). Có failure tracking: sau 2 batch failures → disable batch, fall back to inline embedding.</p>

<hr />

<h2 id="9-sync-lifecycle--atomic-reindex">9. Sync Lifecycle &amp; Atomic Reindex</h2>

<h3 id="91-sync-triggers">9.1 Sync triggers</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>File watcher (chokidar, debounce 1.5s)
          │
Session transcript event (debounce 5s)
          │
Search request (if dirty + sync.onSearch = true)
          │
Session start (if sync.onSessionStart = true)
          │
Interval timer (configurable)
          │
          ▼
     runSync() ← coalesced (chỉ 1 sync chạy tại 1 thời điểm)
</code></pre></div></div>

<h3 id="92-incremental-vs-full-reindex">9.2 Incremental vs Full Reindex</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>runSync()
    │
    ├── Check meta: model/provider/chunking params changed?
    │     │
    │     ├── YES → runSafeReindex() (atomic full reindex)
    │     │
    │     └── NO → Incremental sync
    │           ├── Compare file hashes: chỉ re-index files đã thay đổi
    │           ├── Delete chunks của files đã xóa
    │           └── Embed new/changed chunks (with cache lookup)
    │
    ▼
  Done
</code></pre></div></div>

<h3 id="93-atomic-reindex--crash-safe">9.3 Atomic Reindex – Crash-safe</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>runSafeReindex()
    │
    ├── 1. Tạo temp SQLite DB (temp file)
    ├── 2. Index toàn bộ vào temp DB
    ├── 3. Seed embedding cache từ old DB (tránh re-embed)
    ├── 4. Đổi tên: current → backup, temp → current
    ├── 5. Mở DB mới, verify
    ├── 6. Xóa backup
    │
    └── Nếu fail ở bước 4-5:
          └── Rollback: backup → current
</code></pre></div></div>

<p>Đây là pattern chuẩn trong database: <strong>write-ahead with swap</strong>. Tránh tình trạng DB corrupt nếu process crash giữa chừng.</p>

<hr />

<h2 id="10-session-memory--pre-compaction-flush">10. Session Memory &amp; Pre-Compaction Flush</h2>

<h3 id="101-session-transcript-indexing">10.1 Session Transcript Indexing</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Session .jsonl file
    │
    ▼
Parse each line → filter type="message"
    │
    ▼
Extract user/assistant text content
    │
    ▼
Normalize whitespace + redact sensitive data
    │
    ▼
Format: "User: ..." / "Assistant: ..."
    │
    ▼
chunkMarkdown() → embed → store (source="sessions")
</code></pre></div></div>

<p><strong>Delta-based sync:</strong> Không reindex toàn bộ session file mỗi lần. Theo dõi <code class="language-plaintext highlighter-rouge">lastSize</code>, <code class="language-plaintext highlighter-rouge">pendingBytes</code>, <code class="language-plaintext highlighter-rouge">pendingMessages</code>. Chỉ sync khi vượt ngưỡng:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">deltaBytes &gt;= 100KB</code> HOẶC</li>
  <li><code class="language-plaintext highlighter-rouge">deltaMessages &gt;= 50 messages</code></li>
</ul>

<h3 id="102-pre-compaction-memory-flush">10.2 Pre-Compaction Memory Flush</h3>

<p>Khi conversation gần hết context window:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>totalTokens &gt;= contextWindow - reserveTokensFloor(20K) - softThresholdTokens(4K)
    │
    ▼
Inject prompt: "Hãy lưu thông tin quan trọng vào memory file"
    │
    ▼
Agent writes to MEMORY.md / memory/YYYY-MM-DD.md
    │
    ▼
File watcher triggers sync → index new content
    │
    ▼
Context compaction proceeds (old messages trimmed)
    │
    ▼
Agent có thể recall thông tin đã flush qua memory_search tool
</code></pre></div></div>

<p>Đây là cơ chế <strong>durable memory</strong> – biến context ephemeral thành persistent storage.</p>

<hr />

<h2 id="11-fallback-architecture">11. Fallback Architecture</h2>

<p>OpenClaw có fallback ở <strong>3 tầng</strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Tầng 1: Backend Fallback
┌───────────────────────────────┐
│  QMD Backend (external tool)  │
│  BM25 + Vector + Reranking    │
└──────────┬────────────────────┘
           │ fail
           ▼
┌───────────────────────────────┐
│  Builtin MemoryIndexManager   │
│  (luôn available)             │
└───────────────────────────────┘

Tầng 2: Embedding Provider Fallback
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  OpenAI  │ →  │  Gemini  │ →  │  Voyage  │ →  │  Local   │
└──────────┘    └──────────┘    └──────────┘    └──────────┘

Tầng 3: Vector Search Fallback
┌──────────────────────┐
│ sqlite-vec extension │
│ vec_distance_cosine()│
└──────────┬───────────┘
           │ extension not loaded
           ▼
┌──────────────────────┐
│ JS cosineSimilarity()│
│ (load ALL chunks,    │
│  compute in-memory)  │
└──────────────────────┘
</code></pre></div></div>

<p>Thiết kế này đảm bảo memory search <strong>luôn hoạt động</strong> dù environment thiếu dependencies.</p>

<hr />

<h2 id="12-sơ-đồ-kiến-trúc-hoàn-chỉnh">12. Sơ đồ kiến trúc hoàn chỉnh</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╔═══════════════════════════════════════════════════════════════════════════════╗
║                        HYBRID MEMORY SEARCH PIPELINE                           ║
╚═══════════════════════════════════════════════════════════════════════════════╝

┌─────────────────────────────────────────────────────────────────────────────────┐
│ GIAI ĐOẠN 0: INGESTION PIPELINE                                                 │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                 │
│   Memory Files (.md)              Session Transcripts (.jsonl)                  │
│   MEMORY.md, memory/*.md          ~/.openclaw/agents/*/sessions/                │
│         │                                    │                                  │
│         ▼                                    ▼                                  │
│   ┌─────────────┐                    ┌──────────────────┐                       │
│   │ File Watcher│                    │ Session Event    │                       │
│   │ debounce 1.5s│                   │ debounce 5s      │                       │
│   └──────┬──────┘                    └────────┬─────────┘                       │
│          │                                    │                                  │
│          │                           ┌────────▼─────────┐                       │
│          │                           │ Delta Check      │                       │
│          │                           │ ≥100KB or ≥50msg?│                       │
│          │                           └────────┬─────────┘                       │
│          │                                    │ Yes                             │
│          └────────────────┬───────────────────┘                                 │
│                           ▼                                                     │
│                  ┌─────────────────┐                                            │
│                  │ chunkMarkdown() │                                            │
│                  │ tokens=400      │                                            │
│                  │ overlap=80      │                                            │
│                  └────────┬────────┘                                            │
│                           ▼                                                     │
│                  ┌─────────────────┐      ┌─────────────────┐                   │
│                  │ Embedding Cache │──Hit─▶│ chunks table    │                   │
│                  │ Lookup          │      │ (text + JSON)   │                   │
│                  └────────┬────────┘      └────────┬────────┘                   │
│                           │ Miss                   │                            │
│                           ▼                        ▼                            │
│                  ┌─────────────────┐      ┌─────────────────┐                   │
│                  │ Embed Provider  │      │ chunks_vec      │                   │
│                  │ local→openai→   │      │ (Float32Array)  │                   │
│                  │ gemini→voyage   │      └─────────────────┘                   │
│                  └────────┬────────┘              │                             │
│                           │                       ▼                             │
│                           ▼              ┌─────────────────┐                    │
│                  ┌─────────────────┐     │ chunks_fts      │                    │
│                  │ L2 Normalize    │     │ (FTS5 indexed)  │                    │
│                  └────────┬────────┘     └─────────────────┘                    │
│                           │                                                     │
│                           ▼                                                     │
│                  ┌─────────────────┐                                            │
│                  │ embedding_cache │                                            │
│                  └─────────────────┘                                            │
└─────────────────────────────────────────────────────────────────────────────────┘

                                    │
                    ════════════════╪════════════════
                                    │
                                    ▼

        ╔═══════════════════════════════════════════════════╗
        ║              USER QUERY                            ║
        ║  "Lỗi logic code xử lý thanh toán"                ║
        ╚═══════════════════════════════════════════════════╝
                           │
           ┌───────────────┴───────────────┐
           ▼                               ▼

┌─────────────────────────────────────────────────────────────────────────────────┐
│ GIAI ĐOẠN 1: DUAL SEARCH (Song song)                                            │
├──────────────────────────────────┬──────────────────────────────────────────────┤
│                                  │                                              │
│  VECTOR SEARCH (Semantic)        │  KEYWORD SEARCH (FTS/BM25)                   │
│  ─────────────────────────       │  ──────────────────────────                  │
│                                  │                                              │
│  embedQuery(text)                │  buildFtsQuery()                             │
│         │                        │  Regex: /[A-Za-z0-9_]+/g                     │
│         ▼                        │         │                                    │
│  Query Vector                    │         ▼                                    │
│  [0.023, -0.045, ...]            │  "logic" AND "code" AND ...                  │
│         │                        │         │                                    │
│    ┌────┴────┐                   │         ▼                                    │
│    ▼         ▼                   │  bm25(chunks_fts)                            │
│ sqlite-vec  JS Fallback          │  MATCH + ORDER BY rank                       │
│ (native)    (all chunks)         │         │                                    │
│    │         │                   │         ▼                                    │
│    └────┬────┘                   │  bm25RankToScore()                           │
│         ▼                        │  score = 1/(1+rank)                          │
│  vectorScore                     │         │                                    │
│  = 1 - distance                  │         ▼                                    │
│  (range 0-1)                     │  textScore (range 0-1)                       │
│                                  │                                              │
└──────────────────────────────────┴──────────────────────────────────────────────┘
           │                               │
           └───────────────┬───────────────┘
                           ▼

┌─────────────────────────────────────────────────────────────────────────────────┐
│ GIAI ĐOẠN 2: SCORE FUSION (Weighted Sum)                                        │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                 │
│   Candidate Pool (24 per engine)                                                │
│            │                                                                    │
│            ▼                                                                    │
│   Merge by chunk ID ◄─── Weights: vectorWeight=0.7, textWeight=0.3              │
│            │                                                                    │
│            ▼                                                                    │
│   score = 0.7 × vectorScore + 0.3 × textScore                                   │
│            │                                                                    │
│            ▼                                                                    │
│   Sort descending by score                                                      │
│                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────┘
                           │
                           ▼

┌─────────────────────────────────────────────────────────────────────────────────┐
│ GIAI ĐOẠN 3: POST-PROCESSING                                                    │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                 │
│   Filter: score &gt;= 0.35  ──▶  Limit: top 6  ──▶  Truncate: 700 chars            │
│                                      │                                          │
│                                      ▼                                          │
│                          Add citation: path#L{start}-L{end}                     │
│                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────┘
                           │
                           ▼

        ╔═══════════════════════════════════════════════════╗
        ║              MemorySearchResult[]                  ║
        ║  {path, startLine, endLine, score, snippet,       ║
        ║   source, citation}                                ║
        ╚═══════════════════════════════════════════════════╝
</code></pre></div></div>

<hr />

<h2 id="13-lessons-learned--trade-offs">13. Lessons Learned &amp; Trade-offs</h2>

<h3 id="trade-off-1-weighted-sum-vs-rrf">Trade-off 1: Weighted Sum vs RRF</h3>

<p>Weighted Sum cần scores được calibrate về cùng range. Trong hệ thống này, cosine similarity tự nhiên ở range 0-1, và BM25 rank được transform qua <code class="language-plaintext highlighter-rouge">1/(1+rank)</code> cũng về 0-1. Đủ tốt cho use case recall, không cần phức tạp hóa.</p>

<h3 id="trade-off-2-dual-storage-cho-embedding">Trade-off 2: Dual storage cho embedding</h3>

<p>Embedding lưu cả JSON string (trong <code class="language-plaintext highlighter-rouge">chunks</code>) lẫn Float32Array (trong <code class="language-plaintext highlighter-rouge">chunks_vec</code>). Tốn storage gấp đôi, nhưng đảm bảo JS fallback luôn có data khi sqlite-vec không available.</p>

<h3 id="trade-off-3-regex-tokenizer-thay-vì-nlp-tokenizer">Trade-off 3: Regex tokenizer thay vì NLP tokenizer</h3>

<p><code class="language-plaintext highlighter-rouge">/[A-Za-z0-9_]+/g</code> không xử lý được Unicode (tiếng Việt, tiếng Trung…), không stemming, không stop-word removal. Nhưng: zero dependency, fast, predictable. Với code-heavy memory (tên biến, tên hàm), regex tokenizer thực ra khá hiệu quả.</p>

<h3 id="trade-off-4-token-based-chunk-size-estimation">Trade-off 4: Token-based chunk size estimation</h3>

<p><code class="language-plaintext highlighter-rouge">maxChars = tokens * 4</code> là heuristic thô. Thực tế character/token ratio thay đổi theo ngôn ngữ. Nhưng tránh phải load tokenizer model chỉ để ước chunk size.</p>

<h3 id="trade-off-5-single-sqlite-file-per-agent">Trade-off 5: Single SQLite file per agent</h3>

<p>Đơn giản, không cần external database. Nhưng giới hạn concurrency (SQLite single-writer lock). Với use case single-agent-per-session, đây không phải vấn đề.</p>

<hr />

<h2 id="key-takeaways">Key Takeaways</h2>

<ol>
  <li><strong>Hybrid search không khó</strong>: Chỉ cần 2 search engine + 1 hàm merge ~70 dòng code.</li>
  <li><strong>Fallback everywhere</strong>: Production system cần degrade gracefully, không crash.</li>
  <li><strong>Cache embeddings aggressively</strong>: API calls tốn tiền và chậm. SHA-256 hash + provider fingerprint = cache key hoàn hảo.</li>
  <li><strong>Atomic operations cho persistence</strong>: Temp file → swap pattern bảo vệ data integrity.</li>
  <li><strong>Simple &gt; Perfect</strong>: Weighted Sum đủ tốt. Regex tokenizer đủ tốt. Ship it, iterate later.</li>
</ol>

<hr />

<p><em>Code references: <code class="language-plaintext highlighter-rouge">src/memory/hybrid.ts</code>, <code class="language-plaintext highlighter-rouge">src/memory/manager-search.ts</code>, <code class="language-plaintext highlighter-rouge">src/memory/manager.ts</code>, <code class="language-plaintext highlighter-rouge">src/memory/memory-schema.ts</code>, <code class="language-plaintext highlighter-rouge">src/memory/embeddings.ts</code>, <code class="language-plaintext highlighter-rouge">src/memory/internal.ts</code></em></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Deep Dive: Hybrid Memory Search Architecture trong OpenClaw]]></summary></entry><entry><title type="html">Hướng dẫn xây lại cốt lõi vững – không màu mè, không đạo lý, không chữa lành sáo rỗng.</title><link href="/2025/12/25/hng-dn-xy-li-ct-li-vng-khng-mu-m-khng-o-l-khng-cha-lnh-so-rng.html" rel="alternate" type="text/html" title="Hướng dẫn xây lại cốt lõi vững – không màu mè, không đạo lý, không chữa lành sáo rỗng." /><published>2025-12-25T02:22:41+00:00</published><updated>2025-12-25T02:22:41+00:00</updated><id>/2025/12/25/hng-dn-xy-li-ct-li-vng-khng-mu-m-khng-o-l-khng-cha-lnh-so-rng</id><content type="html" xml:base="/2025/12/25/hng-dn-xy-li-ct-li-vng-khng-mu-m-khng-o-l-khng-cha-lnh-so-rng.html"><![CDATA[<p>Được. Phần này mình sẽ <strong>đi đúng vào trọng tâm bạn yêu cầu</strong>:</p>

<blockquote>
  <p><strong>Hướng dẫn xây lại cốt lõi vững – không màu mè, không đạo lý, không chữa lành sáo rỗng.</strong>
Chỉ là <strong>cách một con người học cách đứng vững bên trong</strong>, sau khi đã sống quá lâu bằng phòng vệ.</p>
</blockquote>

<hr />

<h1 id="i-hiểu-đúng-trước-khi-làm-rất-quan-trọng">I. Hiểu đúng trước khi làm (rất quan trọng)</h1>

<h3 id="1-xây-lại-cốt-lõi-không-phải">1. “Xây lại cốt lõi” <strong>không phải</strong></h3>

<ul>
  <li>Tự tin hơn</li>
  <li>Nghĩ tích cực</li>
  <li>Buông bỏ hết</li>
  <li>Trở nên hiền lành</li>
</ul>

<p>👉 Những thứ đó là <strong>hệ quả</strong>, không phải gốc</p>

<hr />

<h3 id="2-xây-lại-cốt-lõi-thực-chất-là">2. “Xây lại cốt lõi” thực chất là</h3>

<blockquote>
  <p><strong>Tái lập cảm giác an toàn nội tại</strong>,
để bạn <strong>không cần phản ứng để tự vệ</strong> nữa.</p>
</blockquote>

<p>Cốt lõi vững =
<strong>Tôi có thể ở đây, như tôi đang là, và tôi không bị đe dọa.</strong></p>

<hr />

<h1 id="ii-bản-đồ-cốt-lõi-để-bạn-không-làm-mù-mờ">II. Bản đồ cốt lõi (để bạn không làm mù mờ)</h1>

<p>Cốt lõi vững gồm <strong>4 tầng</strong>, làm <strong>theo thứ tự</strong>, không nhảy cóc:</p>

<ol>
  <li><strong>Nhận diện phản xạ sinh tồn</strong></li>
  <li><strong>Ở lại với cảm giác (không sửa)</strong></li>
  <li><strong>Xây giá trị không điều kiện</strong></li>
  <li><strong>Tập hành động từ giá trị, không từ sợ</strong></li>
</ol>

<p>Nếu bạn bỏ qua tầng nào → cốt lõi sẽ <strong>giả vững</strong></p>

<hr />

<h1 id="iii-tầng-1--nhận-diện-phản-xạ-sinh-tồn-không-phán-xét">III. TẦNG 1 – Nhận diện phản xạ sinh tồn (không phán xét)</h1>

<h3 id="việc-cần-làm-hằng-ngày-rất-thực-tế">Việc cần làm (hằng ngày, rất thực tế)</h3>

<p>Khi có chuyện xảy ra, <strong>đừng hỏi “tôi nên làm gì”</strong>, hãy hỏi:</p>

<blockquote>
  <p>❓ <em>“Tôi đang bảo vệ điều gì?”</em></p>
</blockquote>

<hr />

<h3 id="4-phản-xạ-sinh-tồn-phổ-biến-hãy-nhận-diện-mình">4 phản xạ sinh tồn phổ biến (hãy nhận diện mình)</h3>

<ol>
  <li>
    <p><strong>Kiểm soát</strong></p>

    <ul>
      <li>Muốn nắm hết</li>
      <li>Khó tin ai</li>
      <li>Sợ mất quyền chủ động
→ Gốc: <em>“Nếu tôi không kiểm soát, tôi sẽ bị tổn thương”</em></li>
    </ul>
  </li>
  <li>
    <p><strong>Làm hài lòng</strong></p>

    <ul>
      <li>Nhường</li>
      <li>Chịu</li>
      <li>Sợ người khác buồn
→ Gốc: <em>“Nếu họ không hài lòng, tôi sẽ bị bỏ”</em></li>
    </ul>
  </li>
  <li>
    <p><strong>Rút lui</strong></p>

    <ul>
      <li>Ít nói</li>
      <li>Ít cần</li>
      <li>Không đòi hỏi
→ Gốc: <em>“Ở gần là nguy hiểm”</em></li>
    </ul>
  </li>
  <li>
    <p><strong>Tấn công / hơn thua</strong></p>

    <ul>
      <li>Nhanh phản bác</li>
      <li>Không chịu thua
→ Gốc: <em>“Nếu tôi yếu, tôi sẽ bị đè”</em></li>
    </ul>
  </li>
</ol>

<p>👉 <strong>Chỉ cần nhận ra</strong>, chưa cần sửa.</p>

<blockquote>
  <p>Nhận ra = bắt đầu có cốt lõi
Chưa nhận ra = cốt lõi đang bị lái</p>
</blockquote>

<hr />

<h1 id="iv-tầng-2--ở-lại-với-cảm-giác-bước-khó-nhất">IV. TẦNG 2 – Ở lại với cảm giác (bước khó nhất)</h1>

<p>Đây là chỗ <strong>99% người bỏ cuộc</strong>.</p>

<h3 id="nguyên-tắc-sắt">Nguyên tắc sắt</h3>

<blockquote>
  <p><strong>Đừng sửa cảm giác trước khi nó được cảm.</strong></p>
</blockquote>

<hr />

<h3 id="thực-hành-3-phút-đủ-dùng">Thực hành 3 phút (đủ dùng)</h3>

<p>Khi bị kích hoạt:</p>

<ol>
  <li><strong>Dừng hành động</strong></li>
  <li>Nhắm mắt (hoặc nhìn cố định)</li>
  <li>
    <p>Hỏi thầm:</p>

    <ul>
      <li>“Cảm giác này nằm ở đâu trong cơ thể?”</li>
      <li>Nặng / nóng / co / trống?</li>
    </ul>
  </li>
</ol>

<p>❌ Không phân tích
❌ Không hợp lý hóa
❌ Không tự trấn an</p>

<p>👉 Chỉ <strong>ở lại</strong></p>

<hr />

<h3 id="điều-sẽ-xảy-ra-đừng-sợ">Điều sẽ xảy ra (đừng sợ)</h3>

<ul>
  <li>Khó chịu tăng lên</li>
  <li>Muốn trốn</li>
  <li>Muốn làm gì đó cho xong</li>
</ul>

<p>👉 Đây là <strong>hệ thần kinh đang học rằng:
‘Tôi có thể ở lại mà không chết’</strong></p>

<p>Đó là <strong>xây cốt lõi</strong>, không phải thiền cho vui.</p>

<hr />

<h1 id="v-tầng-3--xây-giá-trị-không-điều-kiện-cốt-lõi-thật-bắt-đầu-ở-đây">V. TẦNG 3 – Xây giá trị không điều kiện (cốt lõi thật bắt đầu ở đây)</h1>

<h3 id="sự-thật-quan-trọng">Sự thật quan trọng</h3>

<blockquote>
  <p>Cốt lõi yếu vì <strong>giá trị bản thân có điều kiện</strong></p>
</blockquote>

<p>Ví dụ:</p>

<ul>
  <li>Tôi có giá trị <strong>khi</strong> giỏi</li>
  <li>Tôi xứng đáng <strong>khi</strong> hữu ích</li>
  <li>Tôi ổn <strong>khi</strong> không sai</li>
</ul>

<hr />

<h3 id="bài-tập-gốc-rất-thẳng">Bài tập gốc (rất thẳng)</h3>

<p>Viết ra và hoàn thành câu này:</p>

<blockquote>
  <p><strong>“Ngay cả khi tôi không ______, tôi vẫn xứng đáng được tồn tại.”</strong></p>
</blockquote>

<p>Điền vào:</p>

<ul>
  <li>Giỏi</li>
  <li>Mạnh</li>
  <li>Được yêu</li>
  <li>Có ích</li>
  <li>Đúng</li>
</ul>

<p>👉 Nếu bạn <strong>không viết nổi</strong>, đó chính là chỗ cốt lõi đang rách.</p>

<hr />

<h3 id="lặp-lại-bằng-hành-vi-quan-trọng-hơn-suy-nghĩ">Lặp lại bằng hành vi (quan trọng hơn suy nghĩ)</h3>

<ul>
  <li>Cho phép mình mệt</li>
  <li>Cho phép mình không tối ưu</li>
  <li>Cho phép mình không chứng minh</li>
</ul>

<p>👉 <strong>Không sửa – không cải thiện – không justify</strong></p>

<hr />

<h1 id="vi-tầng-4--hành-động-từ-giá-trị-không-từ-sợ">VI. TẦNG 4 – Hành động từ giá trị (không từ sợ)</h1>

<h3 id="câu-hỏi-chuyển-trục-rất-mạnh">Câu hỏi chuyển trục (rất mạnh)</h3>

<p>Thay vì:</p>

<blockquote>
  <p>“Làm vậy có an toàn không?”</p>
</blockquote>

<p>Hãy hỏi:</p>

<blockquote>
  <p><strong>“Nếu tôi không sợ, tôi sẽ chọn gì?”</strong></p>
</blockquote>

<hr />

<h3 id="ví-dụ-rất-đời">Ví dụ rất đời</h3>

<ul>
  <li>Nói thật dù có thể mất lòng</li>
  <li>Dừng một mối quan hệ dù còn tiếc</li>
  <li>Không giải thích thêm khi đã đủ</li>
  <li>Nghỉ khi mệt, không xin phép</li>
</ul>

<p>👉 Lúc này bạn <strong>chưa hết sợ</strong>
👉 Nhưng <strong>sợ không còn lái tay lái</strong></p>

<hr />

<h1 id="vii-dấu-hiệu-bạn-đang-xây-đúng-không-dễ-chịu-đâu">VII. Dấu hiệu bạn đang xây đúng (không dễ chịu đâu)</h1>

<ul>
  <li>Bạn chậm hơn trước khi phản ứng</li>
  <li>Bạn thấy rõ mình đang phòng vệ</li>
  <li>Bạn ít ghét bản thân hơn khi yếu</li>
  <li>Bạn không cần thắng để thấy mình đúng</li>
  <li>Bạn dám mất để giữ giá trị</li>
</ul>

<p>👉 Nếu thấy <strong>khó nhưng thật</strong> → bạn đi đúng</p>

<hr />

<h1 id="viii-sai-lầm-phổ-biến-đừng-mắc">VIII. Sai lầm phổ biến (đừng mắc)</h1>

<p>❌ Muốn chữa nhanh
❌ Muốn “xong”
❌ Muốn thành phiên bản tốt hơn để được yêu</p>

<p>👉 Cốt lõi vững <strong>không làm bạn đáng yêu hơn</strong>
👉 Nó làm bạn <strong>không cần phải đáng yêu</strong></p>

<hr />

<h1 id="ix-một-câu-kết-rất-quan-trọng">IX. Một câu kết (rất quan trọng)</h1>

<blockquote>
  <p><strong>Cốt lõi vững không khiến bạn không còn đau.
Nó khiến bạn không còn phải đánh mất mình để né đau.</strong></p>
</blockquote>]]></content><author><name></name></author><category term="ki-nang-mem" /><category term="soft-skill" /><summary type="html"><![CDATA[Được. Phần này mình sẽ đi đúng vào trọng tâm bạn yêu cầu:]]></summary></entry></feed>