Table of Contents

This post walks through the Engineer News tech stack: why each tool was chosen, how they fit together, and which choices were the result of tradeoffs.

The Overall Architecture

graph LR
  subgraph "內容生產"
    YT["YouTube"] --> Crawl["crawl.ts\nllama-3.1-70b"]
    Conv["對話/筆記"] --> Ingest["ingest.ts\nllama-3.1-8b"]
  end

  subgraph "Cloudflare"
    Pages["Pages\n靜態 CDN"]
    Worker["Workers\nSSR + API"]
    D1["D1\nSQLite"]
    Vec["Vectorize\n384-dim cosine"]
    R2["R2\nOG + TTS"]
    WAI["Workers AI\nbge-m3 / qwen-14b"]
  end

  Crawl --> Git["git push"]
  Ingest --> Git
  Git --> GHA["GitHub Actions"]
  GHA --> Pages
  GHA --> Sync["sync-to-d1.ts"]
  Sync --> D1
  Sync --> WAI
  WAI --> Vec
  Worker --> D1
  Worker --> Vec
  Worker --> R2
  Worker --> WAI

From a YouTube video or a conversation, all the way to a reader seeing the article in their browser and being able to run a semantic search — the entire flow runs inside the Cloudflare ecosystem.

Frontend: Astro

Astro is a frontend framework designed around a “content-first” philosophy. By default it outputs pure static HTML, injecting JavaScript only into the components that actually need interactivity (Island Architecture). It’s especially well suited to article and documentation sites.

Astro isn’t React, and it isn’t Vue — it’s a framework designed with “content” at its core.

For a blog this design makes a lot of sense: most pages are pure reading and don’t need any JS bundle at all. Articles are managed as Markdown (Content Collections), with a Zod schema validating frontmatter at build time, so a wrong field fails locally instead of waiting for CI.

i18n routing is also built into Astro: zh-TW is the default language with no URL prefix, while the English version lives under /en/*.

Compared to Next.js: Next has a more mature ecosystem, but for a pure content site both the bundle size and configuration complexity are higher. Astro with the Cloudflare adapter’s output: 'server' mode routes the dynamic parts (API routes, SSR) through Workers and the static parts through the Pages CDN — a natural division of labor.

Deployment: Cloudflare Pages + Workers

Cloudflare Pages is a CDN hosting service for static assets that automatically deploys and produces a Preview URL on every git push. Cloudflare Workers is an edge compute platform running on V8 isolates, handling dynamic requests (API routes, SSR). Used together, static and dynamic each do their own job.

Pages handles CDN distribution of static assets (HTML, CSS, JS, images), and Workers handles dynamic requests (APIs, SSR pages). Both are managed in the same wrangler.jsonc and deployed with the same token:

git push main
  → GitHub Actions
      → pnpm build
      → wrangler pages deploy dist

Every push to a non-main branch automatically produces a Preview URL, making it easy to confirm the result before merging.

Database: D1 (SQLite on the edge)

D1 is Cloudflare’s SQLite-compatible edge database. Inside a Worker you query directly with env.DB.prepare().all() — no connection pool, no TCP overhead, no cross-service IAM setup.

The current table breakdown:

TablePurpose
postsArticle metadata (title, date, tag, language)
doc_chunksArticle text chunks (used for RAG)
page_viewsView count per article
search_logsSearch keyword records
settingsSite-wide key-value settings

Migration files live in the migrations/ directory, managed with a version-number prefix:

-- migrations/0001_init.sql
CREATE TABLE IF NOT EXISTS posts (
  id TEXT PRIMARY KEY,
  title TEXT NOT NULL,
  date TEXT NOT NULL,
  category TEXT NOT NULL,
  lang TEXT NOT NULL DEFAULT 'zh-TW',
  tags TEXT,
  description TEXT,
  tldr TEXT
);

CREATE TABLE IF NOT EXISTS doc_chunks (
  id TEXT PRIMARY KEY,
  post_id TEXT NOT NULL,
  chunk_index INTEGER NOT NULL,
  content TEXT NOT NULL
);
# Run migration locally
wrangler d1 execute my-site-db --local --file=migrations/0001_init.sql

# Run migration remotely
wrangler d1 execute my-site-db --remote --file=migrations/0001_init.sql

D1’s limits: a 25MB cap per query and a 10GB cap on database size. More than enough for a blog. Large binaries (audio, images) go to R2 instead.

Why not PlanetScale / Supabase? An external database means extra connection management, IAM, cost, and cross-service latency. D1 is a local call inside Workers, so latency is practically negligible.

Vector Search: Vectorize + Workers AI

This is the most interesting part of the whole stack.

After an article is deployed, sync-to-d1.ts splits each article into chunks, generates a 384-dimensional embedding with Workers AI’s bge-m3 model, and stores it in Vectorize. When a user searches:

sequenceDiagram
  participant "瀏覽器" as Browser
  participant "Worker" as W
  participant "WorkersAI" as AI
  participant "Vectorize" as V
  participant "Database" as D1
  Browser->>W: POST /api/search {query}
  W->>AI: embed(query) via bge-m3
  AI-->>W: query_vector[384]
  W->>V: similaritySearch(top_k=5)
  V-->>W: [{chunk_id, score}...]
  W->>D1: SELECT chunks WHERE id IN (...)
  D1-->>W: chunks[]
  W->>AI: query-14b stream(query + chunks)
  AI-->>W: 回答
  W->>Browser: 回答
  note right of AI
    回答

Core snippet from sync-to-d1.ts:

// 呼叫 bge-m3 生成 embedding
const res = await fetch(
  `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/ai/run/@cf/baai/bge-m3`,
  {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_TOKEN}` },
    body: JSON.stringify({ text: chunkContent }),
  }
);
const { result } = await res.json();
const vector = result?.data?.[0]; // number[384]

// 寫入 Vectorize(NDJSON 格式批次 insert)
// wrangler vectorize insert engineer-news-index --file=vectors.ndjson

This entire RAG flow runs inside Workers — no external API calls, no OpenAI costs. qwen-14b is good enough for Traditional Chinese technical Q&A.

Why 384 dimensions instead of 1536? Vectorize’s cost scales with dimensionality. The 384 dimensions of bge-m3 are already sufficient for semantic search over Chinese technical articles; there’s no need to inflate cost just to “look higher-dimensional.”

Object Storage: R2

R2 is Cloudflare’s object storage service. It’s S3-API-compatible but has no bandwidth charges (egress free).

R2 stores two kinds of things:

OG images: an API route generates them dynamically (satori + a Chinese font), caches the result to R2 after the first generation, and afterward returns it directly without re-running satori. This gives social media shares a correct preview image without recomputing on every request.

const { OG_IMAGES } = locals.runtime.env;

// 先查 R2 cache
const cached = OG_IMAGES ? await OG_IMAGES.get(cacheKey) : null;
if (cached) {
  return new Response(await cached.arrayBuffer(), {
    headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=31536000' },
  });
}

// miss:跑 satori 生成,再寫回 R2
const png = await renderToPng(createShareCardNode({ post }), fontData);
await OG_IMAGES.put(cacheKey, png);
return new Response(png, { headers: { 'Content-Type': 'image/png' } });

TTS audio: each article has a corresponding .wav, batch-generated and uploaded by tts-all.ts, with the audio_url recorded in the frontmatter for the frontend to play directly.

R2 is S3-API-compatible but has no bandwidth charges (it only bills for storage and operations), which is very friendly for large files like audio.

AI Models: Workers AI

Workers AI is Cloudflare’s inference platform, offering serverless access to a number of open-source models, invoked directly from Workers with env.AI.run().

ModelPurpose
bge-m3Article embedding (384 dim, Chinese-friendly)
qwen-14bRAG search answers (streaming)
llama-3.1-8bMetadata extraction at ingest time (frontmatter)
llama-3.1-70bzh-TW summary generation at crawl time

An example of calling it from a Worker / API route:

const { AI } = locals.runtime.env;

// embedding(用於向量搜尋)
const { data } = await AI.run('@cf/baai/bge-m3', { text: [query] });
const queryVector = data[0]; // number[384]

// RAG 回答(串流)
const stream = await AI.run('@cf/qwen/qwen1.5-14b-chat-awq', {
  stream: true,
  messages: [
    { role: 'system', content: systemPrompt },
    { role: 'user', content: userQuery },
  ],
});
return new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } });

Everything runs through Workers AI, so there’s no API key rotation to manage and no external-service latency hops. For Chinese articles, qwen-14b’s comprehension and generation quality is far better than English-leaning models of comparable size.

Why not OpenAI? Workers AI is good enough for this use case, and the entire AI pipeline shares the same set of credentials as the infrastructure — no separate OpenAI billing or rate limits to manage.

Content Automation: crawl + ingest

crawl.ts: runs daily at UTC 02:00 (10 AM Taiwan time) via GitHub Actions, crawling 9 YouTube channels, generating Traditional Chinese summaries with llama-3.1-70b, and automatically committing + pushing — no manual intervention.

ingest.ts: feed it a conversation or notes file, and it automatically detects and masks sensitive information (tokens, keys, internal URLs), then uses llama-3.1-8b to generate the title, tags, tldr, and description, outputting a complete Markdown article.

These two scripts, combined with Claude Code’s post skill, let each day’s engineering decisions turn into articles with very little friction.

Full-Text Search: Pagefind

Beyond RAG (vector semantic search), the site also uses Pagefind for static full-text indexing. After pnpm build finishes, Pagefind scans the dist/ directory to build an index, so exact keyword search needs no backend and runs entirely in the browser.

The division of labor between RAG and Pagefind: RAG answers open-ended questions, Pagefind finds exact terms.

Development Tooling

TypeScript: all scripts and API routes are written in TypeScript, paired with a strict content schema to catch problems early at build time.

pnpm: faster than npm, and its shared node_modules mechanism saves space — well suited to a setup with multiple scripts like this.

GitHub Actions: three workflows:

  • deploy.yml: auto-deploys on push to main
  • crawl.yml: runs the scheduled YouTube crawl daily
  • fix-mermaid.yml: manually triggered to repair broken Mermaid diagrams in articles

References

Ask this article

Answers come from this article only. Click any prompt below or open the chat at the bottom right.

🇺🇸 English

So what is this blog actually running on? That's the question I want to answer today — every tool in the stack, why I picked it, and where I had to make a tradeoff. And here's the punchline up front: from a YouTube video getting crawled, all the way to a reader typing a search query in their browser, the entire pipeline lives inside one ecosystem — Cloudflare. Let me walk you through how the pieces fit.

Let's start at the front, with the framework: Astro.

Astro is built around a content-first philosophy. By default, it ships pure static HTML and only sprinkles in JavaScript for the components that genuinely need to be interactive. They call that Island Architecture. And for a blog, that's exactly right — most pages are just text you read. There's no reason to ship a JavaScript bundle for that. Astro isn't React, it isn't Vue; it's a framework with content at the center.

Articles are managed as Markdown, with a schema validating the frontmatter at build time. So if I typo a field name, it breaks on my machine, locally, instead of silently failing in CI later. Multilingual routing is baked in too — Traditional Chinese is the default with no URL prefix, and the English version lives under slash-en.

Now, why Astro over Next.js? Next has the more mature ecosystem, no argument there. But for a pure content site, Next means a bigger bundle and more configuration complexity. With Astro running in server mode on the Cloudflare adapter, the dynamic stuff — API routes, server rendering — goes through Workers, and the static stuff is served straight off the Pages CDN. A clean division of labor.

Which brings us to deployment: Cloudflare Pages and Workers, working as a pair. Pages is the CDN — it hosts all the static assets and, on every git push, automatically deploys and hands you a preview URL. Workers is the edge compute side, running on V8 isolates, handling the dynamic requests. The flow is simple: I push to main, GitHub Actions kicks in, runs the build, and deploys. And here's a detail I love — every push to a non-main branch spins up its own preview URL. So before I merge anything, I can actually look at the result live.

Next layer down: the database, D1. This is Cloudflare's SQLite-compatible database running at the edge. And the magic here is that inside a Worker, you just query it directly. No connection pool. No TCP overhead. No cross-service permissions to wire up. It's essentially a local call.

The tables are about what you'd expect: one for article metadata, one for the text chunks used in search, one for view counts per article, one logging search keywords, and one for site-wide settings. Schema changes are handled through migration files, prefixed with version numbers so they apply in order.

Does D1 have limits? Sure — there's a cap of twenty-five megabytes per query and ten gigabytes on the whole database. For a blog, that's enormous headroom. And the big binary stuff — audio, images — never goes in the database anyway. That goes to object storage.

So why not reach for PlanetScale or Supabase? Because an external database drags in connection management, permissions, extra cost, and cross-service latency. D1, being a local call inside Workers, makes that latency basically disappear.

Okay — now the part I find most interesting in the whole stack: vector search. This is Vectorize plus Workers AI.

Here's how it works. After an article deploys, a sync script chops it into chunks, and for each chunk it generates an embedding — a three-hundred-eighty-four-dimensional vector — using a model called bge-m3. Those vectors get stored in Vectorize.

Then a user searches. Their query gets turned into a vector with the same model. We ask Vectorize for the five most similar chunks. We pull the actual text of those chunks out of the database. And then we hand the query plus that context to a language model — qwen-14b — which streams back an answer. That's retrieval-augmented generation, and the beautiful thing is the entire loop runs inside Workers. No external API calls. No OpenAI bill. And qwen-14b is genuinely good enough for Traditional Chinese technical Q&A.

One choice worth explaining: why three-hundred-eighty-four dimensions instead of the fifteen-thirty-six you see elsewhere? Because Vectorize costs scale with dimensionality. And three-eighty-four is already plenty for semantic search over Chinese technical writing. There's no reason to inflate your costs just to look like you're working in higher dimensions.

Next up: object storage, R2. It's Cloudflare's S3-compatible storage, but with one killer difference — there are no bandwidth charges. Egress is free. It stores two things for this blog.

First, the social-share preview images. An API route generates them on demand using a rendering library and a Chinese font, then caches the result to R2. So the first request does the work; every request after that just returns the cached image. Your shared links get a proper preview without recomputing anything.

Second, the text-to-speech audio. Every article has a corresponding audio file, generated in a batch and uploaded, with the audio URL recorded in the article's frontmatter so the frontend can just play it. And because R2 doesn't charge for bandwidth — only storage and operations — it's incredibly friendly for big files like audio.

Now let's talk about the AI models themselves, all served through Workers AI. This is Cloudflare's inference platform, giving you serverless access to a bunch of open-source models, called directly from a Worker. There are four in play here. Bge-m3 does the embeddings. Qwen-14b writes the search answers, streaming. A smaller llama model, the eight-billion one, extracts metadata when I ingest a new article. And a larger llama, the seventy-billion one, generates the Chinese summaries when the crawler runs.

The win is the same theme as everywhere else in this stack: everything runs through Workers AI, so there are no API keys to rotate, no external latency hops. And for Chinese content, qwen-14b's comprehension and generation quality beats comparably-sized English-leaning models by a wide margin. Why not OpenAI? Because Workers AI is good enough here, and the whole AI pipeline shares the same credentials as the infrastructure. No separate billing, no separate rate limits.

That covers serving. Let me close out with how content actually gets made — the automation. There are two scripts. The crawler runs daily, ten in the morning Taiwan time, sweeping nine YouTube channels, generating Traditional Chinese summaries with that big llama model, and committing and pushing entirely on its own. No hands. The other script is the ingester: I feed it a conversation or a notes file, it automatically detects and masks sensitive stuff — tokens, keys, internal URLs — then generates the title, tags, summary, and description, and spits out a finished Markdown article.

There's also a second kind of search worth mentioning. Beyond the RAG vector search, the site uses Pagefind for full-text indexing. After the build finishes, Pagefind scans the output directory and builds an index, so exact keyword search needs no backend at all — it runs entirely in the browser. The way I think about the division: RAG answers open-ended questions, Pagefind finds exact terms.

And underneath it all, the dev tooling is boring in the best way — TypeScript everywhere with a strict schema to catch problems at build time, pnpm for speed and disk savings, and three GitHub Actions workflows handling deploy, the scheduled crawl, and the occasional diagram repair.

So if you take three things away from this, here they are. One: the whole point of this stack is that everything lives in one ecosystem — database, vectors, storage, AI inference — which means almost no cross-service latency and one set of credentials instead of five. Two: pick tools that match the actual workload. A content site doesn't need a heavy JavaScript framework, and a blog's search doesn't need fifteen-hundred-dimensional vectors — matching the tool to the job is where the real savings come from. And three: automation closes the loop. From a YouTube video or a quick note, all the way to a published, searchable, narrated article — the friction is low enough that a day's engineering decisions can just become writing. And that, really, is the whole idea.

🇹🇼 中文

先講結論:這個部落格叫 Engineer News,整套技術堆疊有個很明確的設計原則——能跑在 Cloudflare 生態系內的,就不往外接。從 YouTube 影片爬取、對話筆記轉文章,一路到使用者在瀏覽器裡做語意搜尋,沒有任何一個環節跳出 Cloudflare。這集就來拆解,為什麼是這些工具、它們怎麼接在一起,以及哪些選擇其實是取捨的結果。

先看整體的流動。內容的源頭有兩個:一個是 YouTube,由爬蟲腳本配 llama-3.1-70b 生成中文摘要;另一個是對話跟筆記,由 ingest 腳本配比較小的 llama-3.1-8b 抽取 metadata。這兩條路最後都匯流到 git push,觸發 GitHub Actions,一邊把網站部署到 Cloudflare Pages,一邊跑同步腳本,把文章切塊、生成向量、寫進 D1 跟 Vectorize。使用者端的 Worker 再去讀這些資料庫、讀 R2、呼叫 Workers AI。整張圖你只要記得一件事:所有東西都在同一個圍牆內。

前端用的是 Astro。它跟 React、Vue 不一樣,是「內容優先」的框架,預設就吐純靜態 HTML,只在真的需要互動的元件才注入 JavaScript,這叫 Island Architecture。對部落格來說這個哲學很合理,因為大多數頁面就是純閱讀,根本不需要 JS bundle。文章用 Markdown 管理,frontmatter 有 Zod schema 在 build 階段驗證,欄位寫錯本地就報錯,不用等 CI。多語系路由也是內建的,中文是預設、沒有 URL 前綴,英文走 /en 路徑。

那為什麼不用 Next.js?Next 生態確實更成熟,但對純內容站來說,bundle size 跟設定複雜度都偏高。Astro 搭 Cloudflare adapter 的 server 模式,剛好讓動態的部分走 Workers、靜態的部分走 Pages CDN,分工很自然。

講到部署,就是 Pages 加 Workers 這對組合。Pages 負責靜態資源的 CDN 分發,Workers 是跑在 V8 isolates 上的邊緣運算,負責 API 跟 SSR。兩者在同一份設定檔裡管理、用同一個 token 部署。流程很乾淨:push 到 main,GitHub Actions 跑 build,再用 wrangler 部署。而且只要你推到非 main 的分支,它會自動生一個 Preview URL,合併前先確認效果,很方便。

資料庫用 D1,就是 Cloudflare 跑在邊緣的 SQLite。它最大的好處是,在 Worker 裡直接查就好,沒有連線池、沒有 TCP 開銷、沒有跨服務的權限設定。目前幾張表分工很清楚:posts 放文章的元資料,doc_chunks 放切塊文字給 RAG 用,page_views 記瀏覽次數,search_logs 記搜尋關鍵字,settings 放全站設定。Migration 檔案用版本號前綴管理,本地跟遠端各跑一次指令就好。D1 的限制是單次查詢 25MB、資料庫總量 10GB,對部落格綽綽有餘,真的有大型二進位檔案,像音訊圖片,就丟去 R2。

那為什麼不用 PlanetScale 或 Supabase?關鍵在延遲跟管理成本。外部資料庫意味著額外的連線管理、權限、費用,還有跨服務的延遲跳點。D1 在 Worker 裡是本地呼叫,延遲幾乎可以忽略。

接下來是我覺得整套堆疊裡最有趣的部分:向量搜尋,用 Vectorize 加 Workers AI。文章部署後,同步腳本把每篇切成 chunks,用 bge-m3 模型生成 384 維的 embedding 存進 Vectorize。使用者搜尋的時候,流程是這樣:瀏覽器把 query 送到 Worker,Worker 先呼叫 bge-m3 把問題也轉成向量,拿這個向量去 Vectorize 做相似度搜尋、取最接近的五個 chunk,再回 D1 把這些 chunk 的原文撈出來,最後連同問題一起餵給 qwen-14b 串流生成答案,邊生成邊回傳給瀏覽器。整條 RAG 流程全在 Worker 裡,沒有外部 API 呼叫,也沒有 OpenAI 帳單。

這裡有個值得講的決策:為什麼選 384 維,而不是看起來更高級的 1536 維?因為 Vectorize 的費用跟維度正相關,而 bge-m3 的 384 維對中文技術文章的語意搜尋,效果已經夠了。沒必要為了「數字看起來更高」去多付成本。

物件儲存用 R2。它 API 相容 S3,但最大的賣點是 egress free,流量不收費,只收儲存跟操作費用。R2 在這裡存兩種東西。第一是 OG 分享圖,由 API route 動態生成,用 satori 配中文字體渲染,第一次算完就快取進 R2,之後直接回傳,不重複跑 satori,既能讓社群分享有正確預覽圖,又不用每次都重算。第二是 TTS 音訊,每篇文章都有對應的 wav 檔,批次生成後上傳,frontmatter 裡記下音訊網址,前端直接播。音訊這種大檔,搭配沒有流量費的 R2,特別划算。

AI 模型全部走 Workers AI,這是 Cloudflare 的 serverless 推論平台,在 Worker 裡一行 env.AI.run 就能呼叫。四個模型各司其職:bge-m3 負責 embedding,qwen-14b 負責 RAG 回答串流,llama-3.1-8b 在 ingest 時抽 metadata,llama-3.1-70b 在爬蟲時生成中文摘要。好處是不用管 API key 輪替,也沒有外部服務的延遲跳點,整個 AI pipeline 跟基礎設施共用同一組憑證。對中文內容來說,qwen-14b 的理解跟生成品質,比同量級偏英文的模型好不少。不用 OpenAI 的理由也一樣:夠用,而且不用另外管帳單跟 rate limit。

內容自動化靠兩個腳本。crawl 每天台灣時間早上十點透過 GitHub Actions 跑,爬九個 YouTube 頻道、生成中文摘要、自動 commit 推上去,完全不用手動。ingest 則是把對話或筆記丟進去,自動偵測並遮蔽 token、key、內部網址這些敏感資訊,再生成標題、標籤、摘要,輸出完整文章。這兩個腳本加上 Claude Code 的 post skill,讓每天的工程決策都能很低摩擦地變成文章。

全文搜尋這塊還有 Pagefind 補位。它在 build 完之後掃 dist 目錄產生靜態索引,讓精確的關鍵字搜尋不需要後端,直接在瀏覽器跑。所以站上其實是兩套搜尋分工:RAG 處理開放性的問題,Pagefind 負責找精確的詞彙。開發工具的部分就比較常規,全用 TypeScript 配嚴格 schema、用 pnpm 管套件、三個 GitHub Actions workflow 分別負責部署、爬取跟修 Mermaid 圖。

最後收個尾,三個核心要點。第一,整套架構的主軸是「單一生態系」,前端、部署、資料庫、向量搜尋、AI 推論、物件儲存全在 Cloudflare 內,省掉的不只是費用,更是跨服務的延遲跟管理複雜度。第二,工具選擇都是明確的取捨,選 Astro 是因為內容站不需要重前端框架,選 D1 是因為本地呼叫沒延遲,選 384 維是為了控制成本,每個決定背後都有理由,而不是追新。第三,內容生產這條 pipeline 是高度自動化的,從爬取到 ingest 到部署到搜尋,人要做的事被壓到最低,這才是讓一個個人部落格能持續產出的真正關鍵。

Tags

Related Articles

a920604a Labs: A Dual-Cloud Full-Stack Playground Integrating Four Tools with a pnpm Monorepo

A pnpm monorepo integrating four tool SPAs: a to-do list, a habit tracker, an ebook reader, and a resignation stamp collector. apps/root is the sole build entry point; the four modules are library-only workspace packages bundled together by root's Vite, sharing @a920604a/auth and @a920604a/ui, deployed across Firebase + Cloudflare dual cloud.

RAG's Five Stages: From Pipeline to Reasoning Retrieval, and the Naive RAG on My Own Site

Over the past two years RAG evolved from a 'linear pipeline' to 'loop-based reasoning'. It maps cleanly to five stages: Naive, Advanced, Modular, Graph, Agentic. The real inflection point is control moving from pipeline to agent — a System 1 → System 2 shift. Looking back at engineer-news's own RAG stack, it's stuck at the Naive edge — so this post also lays out what to fix next.