與 Claude 3.5 Sonnet 協作開發「相似文章推薦」

發表時間: | 分類: 經驗分享 | 字數:1486 | 閱讀時間:3分鐘

動機

上次【與 Claude 3.5 Sonnet 協作完成開發並部署 RSS Filter 到 Netlify Functions 上】的體驗良好,對於用 AI 來協作開發蠻感興趣的,想藉由這樣的模式做更多事,擴大自己能夠開發的項目範圍。

最近對中文文字處理蠻感興趣的,做了短暫的研究後,發現這幾年自然語言處理在 Transformer 模型出現之後快速的起飛。從五年前的 Bidirectional Encoder Representations from Transformers (BERT) 到最近兩年很紅的的 Generative Pre-Training (GPT),利用預訓練好的語言模型,在大多數的自然語言處理問題就已經有一定的水準。

一直以來,都想在自己的部落格裡做相似文章的推薦,不過之前會卡在不知道怎麼處理中文最適合,所以遲遲沒有下手。在 2024 大型語言模型很流行的今天,感覺直接把整篇文章轉成語意向量(Embedding)就能達到一定的水準,如此簡單直接的方式,也很適合當作一個小練習。

作法

簡單拆解這個任務會有以下步驟:

  1. 算出每篇文章的語意向量
  2. 對於每兩篇文章計算向量的距離,距離越小表示越相似
  3. 對每篇文章選出最相似的 N 筆文章
  4. 在每篇文章的結尾用 Card View 放最相似的文章
  5. 最後設定 Github Actions 在文章更新時自動更新語意向量和最相似的文章

Anthropic 本身沒有出 Embedding API,最後在 OpenAIGemini 選擇文件比較詳細的 OpenAI。

流程上甚至可以直接參考 OpenAI 提供的【Recommendation using embeddings and nearest neighbor search】範例,很接近我們設定的目標,在這個範例還有提供對 Embeddings 做快取的步驟,減少 API 的使用量,減少計算成本。

把現有大概 130 幾篇的文章都用 Text-embedding-3-large 計算語意向量,大概會用到 20 萬的 Token 數,大概花台幣 1 元,單純做這件事的話相當便宜。

過程中有遇到處理 Zola 的 Markdown 的問題,需要額外處理 Front matter,不過只要進一步提供 AI 一個範例和你想取出的部分,就能快速生成堪用的程式碼。

而第二步和第三步的交叉計算與取 K Nearest Neighbor (KNN) 的運算也都是很常見的程式,在遇到效能瓶頸之前生成的代碼基本上都沒問題。

生成上最困難的部分是與 Zola 的整合,一方面因為比較小眾,AI 的答案較容易出現幻覺,給出無法用的代碼;一方面測試上與除錯也不太容易。

之前在做 Google Analytics View Count 的整合時,有發現可以利用 load_data 的方法,去讀取預先算好的檔案。這次也是直接把算好的最相似網頁儲存成一個 Map,用 page.path 當索引鍵,用來取得最相近的網頁的檔案路徑,因為 get_page 需要用檔案路徑當作參數。

1{
2 "/changelog/": "blog/wordpress-to-zola/index.md:blog/comment-system/index.md:blog/fix-open-graph-in-twitter-card/index.md",
3 "/about/": "blog/wordpress-to-zola/index.md:wisdom/articles/15-years-in-programming.md:changelog.md",
4 "...": "...",
5}

當有多個檔案路徑時,我目前用冒號做分隔。

最後就可以在 template 中讀取檔案、分割字串、用 get_page 得到 page 的物件:

1{% set top_3_nn = load_data(path="static/data/top_3_nn.json") %}
2{% if top_3_nn[page.path] %}
3{% set top_3_nn_paths = top_3_nn[page.path] | split(pat=":") %}
4{% for path in top_3_nn_paths %}
5{% set page = get_page(path=path) %}
6{{ macro::card(page=page) }}
7{% endfor %}
8{% endif %}

接著是整合進網頁,這邊就是 AI 比我擅長的部分,方法是提供一個類似樣式的圖片,要求 AI 生成 HTML 和 CSS 樣式,而最難的部分也還是 zola 的整合,這次的做法是寫一個 macro,能利用上一步傳入的 page 物件組合出 card view 需要的資訊。

1{% macro card(page) %}
2{% if page.extra.image %}
3{% set image = config.base_url ~ page.path ~ page.extra.image %}
4{% else %}
5{% set image = get_url(path="images/default-og-image.webp") %}
6{% endif %}
7{% set ancestors = page.ancestors %}
8{% set breadcrumb = "" %}
9{% for ancestor in ancestors %}
10{% set section = get_section(path=ancestor) %}
11{% if loop.first %}
12{% set_global breadcrumb = '<a href="' ~ section.permalink ~ '">' ~ section.title ~ '</a>' %}
13{% else %}
14{% set_global breadcrumb = breadcrumb ~ ' > ' ~ '<a href="' ~ section.permalink ~ '">' ~ section.title ~ '</a>' %}
15{% endif %}
16{% endfor %}
17<div class="recommended-article">
18 <div class="article-image">
19 <img src="{{ image }}">
20 </div>
21 <div class="article-info">
22 <div class="article-meta">
23 <span class="article-category">{{ breadcrumb | safe }}</span>
24 <span class="article-date">{{ page.date }}</span>
25 </div>
26 <h3 class="article-title">
27 <a href="{{ page.permalink }}">{{ page.title }}</a>
28 </h3>
29 </div>
30</div>
31{% endmacro card %}

過程中有踩到一個雷是在 Tera 的迴圈裡,如果用 set 去改變變數的值,變動的影響範圍只有在迴圈內(如上圖 highlight 處)。要影響到外面的變數,要使用 set_global,詳見 Assignments

最後生成 Github Actions 的 Template 也是很常見的任務,只是有可能會用到比較舊版的 Action 或者是不會自動幫你設定程式語言的版本或是對 Dependency 或結果做快取,這些細節還是需要根據需求額外看文件做調整。

心得

之前阻止自己去開發的原因,主要有三:

  • 缺乏先備知識
  • 程式開發的初始摩擦力
  • 與美觀和使用者介面相關的調整,如 HTML 和 CSS

與生成式 AI 協作可以用很短的時間突破後兩項。在研究先備知識上,只要是夠普遍的知識,跟 AI 做討論出現幻覺的機率也會比較低。在生成式 AI 時代,多嘗試創造擴大能力邊界真的是蠻重要的事。

#generative-ai

提及本篇的文章

相關文章推薦