James Tsang

James Tsang

A developer.
github
twitter
tg_channel

ARTS 打卡第 5 天

A:268. 丟失的數字#

給定一個包含 [0, n] 中 n 個數的數組 nums ,找出 [0, n] 這個範圍內沒有出現在數組中的那個數。
示例 1:
輸入:nums = [3,0,1]
輸出:2
解釋:n = 3,因為有 3 個數字,所以所有的數字都在範圍 [0,3] 內。2 是丟失的數字,因為它沒有出現在 nums 中。
示例 2:
輸入:nums = [0,1]
輸出:2
解釋:n = 2,因為有 2 個數字,所以所有的數字都在範圍 [0,2] 內。2 是丟失的數字,因為它沒有出現在 nums 中。
示例 3:
輸入:nums = [9,6,4,2,3,5,7,0,1]
輸出:8
解釋:n = 9,因為有 9 個數字,所以所有的數字都在範圍 [0,9] 內。8 是丟失的數字,因為它沒有出現在 nums 中。
示例 4:
輸入:nums = [0]
輸出:1
解釋:n = 1,因為有 1 個數字,所以所有的數字都在範圍 [0,1] 內。1 是丟失的數字,因為它沒有出現在 nums 中。

打卡時間不多了,寫得很匆忙性能不太好。

function missingNumber(nums: number[]): number {
  const maxLength = nums.length
  const allNums: number[] = new Array(maxLength).fill(0).map((_, index) => index)
  allNums.push(maxLength)
  const missNums = allNums.filter(num => !nums.includes(num))
  if (missNums.length === 1) {
    return missNums[0]
  }
  throw new Error('no result')
};

提交結果為:

122/122 cases passed (700 ms)
Your runtime beats 5.49 % of typescript submissions
Your memory usage beats 21.98 % of typescript submissions (45.8 MB)

官方的解法更加高效,還使用了哈希集合,因為哈希集合添加和查找元素的時間複雜度都是 O(1),因此更加高效:

function missingNumber(nums: number[]): number {
  const set = new Set()
  const n: number = nums.length;
  for (let i = 0; i < n; i++) {
    set.add(nums[i])
  }
  let missing: number = -1
  for (let i = 0; i <= n; i++) {
    if (!set.has(i)) {
      missing = i
      break
    }
  }
  return missing
}

先將每一個元素記錄到 Set 中,之後按數值順序與 Set 中已有的值進行比較,缺失的就直接被找到了。感受到了哈希集合的好處。

R:探索生成式 AI#

由於大語言模型(LLM)最近的火熱,作者對大語言模型也產生了好奇,想知道這對工作會造成什麼影響,怎麼在軟件交付的實踐中使用 LLM。這是她經過一番探索實踐後所做的分享記錄。

工具鏈#

對於一個仍在進行模式演進的技術,需要建立一個心智模型來理解它是怎麼工作的,這有助於處理湧來的海量信息。它用來解決哪種類型的問題?解決問題需要拼湊出哪些部分?它們怎麼組合在一起的?

工具怎麼分類的:
在心智模型中對工具做以下分類來支持編程工作:

  • 任務類型:在上下文中更快地查找信息;生成代碼;關於代碼的推理(解釋或者找出問題);代碼轉換(例如得到文檔或圖表)
  • 交互模式:聊天窗口;行內助手(比如 Github Copilot);命令行
  • Prompt 組成:用戶從零輸入;結合用戶輸入和上下文
  • 模型屬性:模型是否用於代碼生成的任務?能生成什麼語言的代碼?它是什麼時候訓練的,信息有多新;模型的參數量;模型上下文長度限制;模型內容過濾是怎麼樣的,由誰進行 serving
  • Hosting 方式:是否是商業公司 host 的產品;哪些開源工具可以連接到大語言模型服務上;自建的工具,連接到大語言模型服務上;自建工具進行微調,自建大語言模型 API

作者按照這些維度對 Github Copilot, Github Copilot Chat, ChatGPT 等進行了分析:

image

中位數函數 - 三個函數的故事#

這是一個關於生成中位數函數的故事,可以用來表明 LLM 輔助變成的用處以及局限性。

通常作者都是搜索 "中位數的 JS 函數" 來實現,這次她嘗試使用 Github Copilot 來輔助。Copilot 首先生成了正確的函數簽名,然後給了三種不同的實現。

第一種實現是:先 sort 排序,然後後取中間位置的數,如果是偶數序列則取中間兩個數的均值。目標是實現了的,問題在於 sort 方法不是 immutable 的,會改變原數組的排序順序,這可能引入一些很難排查的 bug。

第二種實現是:原數組 slice 後再用第一種實現,這就不會造成原數組變更了,因此沒什麼問題。

第三種實現是:原數組 slice 後直接取 Math.floor(sorted.length / 2) 的數,這在數組長度為偶數時會有問題,沒有取中間兩個數的均值。

從這三個函數的表現來看,理解我們寫的函數在做什麼,然後寫一些充分的測試用例來測生成的代碼還是很重要的。

使用 Copilot 生成代碼和搜索然後複製粘貼不一樣嗎?搜索然後複製粘貼我們能知道這段代碼的來源,從而通過一些比如 Stackoverflow 上的投票數還有評論來幫助判斷粘貼的代碼是否可靠,但 Copilot 生成的代碼我們缺少判斷依據。

那我們是生成測試用例、還是代碼還是都生成呢?作者用 Copilot 生成了這個中位數函數的測試用例,效果確實不錯,對於中位數函數這個複雜度的任務,她願意用 Copilot 同時生成用例和代碼。但對於更複雜的函數,她還是寧願自己寫測試用例以確保質量,同時更好地組織測試用例的結構,避免即使是把一部分內容給到 Copilot 去生成而引起的遺漏。

Copilot 能幫我修復生成代碼中的錯誤嗎?作者要求 Copilot 進行重構後,它的確給出了一些合理的建議,包括給 ChatGPT,ChatGPT 都能直接指出其中的錯誤,不過這一切的前提都是我知道這段代碼仍需要改進和修正。

結論:

  • 你必須清楚地知道自己在做什麼,才能判斷生成的代碼如何。就像上面例子中必須知道中位數是什麼要考慮什麼邊界情況,才能得到合理的測試用例和代碼
  • Copilot 自身是能夠再次改進有問題的代碼的,這是否意味著在使用 AI 工具時我們需要循環和 AI 進行對話
  • 即使對生成的測試用例和代碼質量有所懷疑,但可以選擇不采用它的代碼,僅僅是用它生成的代碼來幫自己覆蓋測試用例遺漏的場景或者幫助推斷自己寫的代碼

代碼行內助手在什麼時候更有用?#

對於行內代碼助手,說有用說沒有用的人都有,這取決於具體的上下文還有對它的預期。

“有用” 具體指什麼?在這裡有用是指用了它以後能以相當的質量幫我更快地解決問題,這不只是寫代碼的時候,也包括後續的人工 review 還有後續的返工處理、質量問題。

影響生成內容有用性的因素#

以下因素我只記錄了相對客觀的部分,但原文中的每一點作者都還闡述了相對主觀的見解,感興趣請閱讀原文

更流行的技術棧

使用的技術棧越流行,模型中關於這個技術棧的數據集越豐富,這意味著 Java 和 JS 的數據比 Lua 更多。但也有同事在 Rust 這種數據不那麼多的語言上也能取得不錯的測試結果。

簡單和常見的問題

簡單常見的問題一般有下面幾種例子:問題本身很簡單;在上下文中比較常見的解決模式;模板化的代碼;重複的模式。

這對經常手寫重複代碼的場景是有幫助的,但對熟悉了 IDE 的高級特性、快捷鍵、多光標操作的人來說,Copilot 幫他們減少的重複性工作意義就沒那麼大了,而且還可能減少大家重構的動力。

更小的問題

規模越小的問題越好 review 生成的代碼,規模大了以後問題和代碼都會更難理解,往往還要分成多步,這就增加測試覆蓋不足的風險而且可能引入我們不需要的內容。

更有經驗的開發者

經驗豐富的開發者能更好地判斷生成代碼的質量然後高效使用它們。

更大的錯誤容忍度

判斷生成代碼的質量和正確性是非常重要的,但還有一個問題就是我們的場景裡對質量和正確問題的容忍程度。對於錯誤容忍度高的場景,可以更高接受度地採納它的建議,但對於比如安全策略的低容忍度場景,比如 Content-Security-Policy HTTP 頭這樣的內容,我們還是更難去輕易採納 Copilot 的建議。

結論:
用行內代碼助手是有適合用的地方的,但影響有用性的因素也有很多,使用它的技巧也不是通過一個訓練課程或博客文章就能說清楚的。只有多用,甚至探索到有用的邊界之外,才能更好地使用這個工具。

行內代碼助手在什麼時候會成為阻礙因素?#

上面說了 Copilot 有用的情況,那麼自然就有沒用的情況。比如:

擴大不好的或過時的實踐

由於 Copilot 會參考關聯上下文中的任何內容來做參考,比如打開的相同語言的文件,找一些相似的代碼片段,因此它也可能把壞的代碼示例搬過來。

還有比如我們想對代碼庫做一些重構,但 Copilot 還總是會採用老的模式,因為代碼庫裡老模式的代碼還是廣泛存在的,作者把這種情況叫作 “中毒上下文”,這種情況目前還沒有好的解決辦法。

結論:
AI 希望通過代碼庫的代碼來改善 Prompt 上下文既有好處也有壞處,這是開發者不再相信 Copilot 的因素之一。

Review 生成代碼的疲勞

用 Copilot 意味著一遍又一遍地 review 生成的小塊代碼內容。通常在編程時的心流是持續不斷第書寫實現我們腦中的解決方案。用 Copilot 後中間就需要持續閱讀、review 生成的代碼,這是不同的認知方式,並且這種方式比持續地产出代碼更缺少樂趣,這會引起 review 疲勞,感覺心流被打斷。如果我們不處理這種 review 疲勞,我們可能就會對生成代碼進行判斷開始打馬虎眼。

此外還可能會有一些影響:

  • 自動化偏見:一旦我們在生成式 AI 獲得了良好的體驗,我們就會過度信任它們
  • 沉沒成本:一旦我們花時間用 Copilot 生成了一些局部有問題的代碼,後續我們可能也更傾向於花 20 分鐘用 Copilot 讓這塊代碼工作,而不是自己花 5 分鐘重寫
  • 錨定效應:Copilot 給出的建議很可能會錨定我們的思維,讓我們後續的思考都受它的影響。因此擺脫這種思維影響,不要被它錨定住也很重要

結論:
使用 Copilot 時不要被它限制思維很重要,也需要跳脫出它的限制,否則我們就會成為導航把我們導向湖裡,我們就把車開進湖裡的人。

代碼助手無法取代結對編程#

雖然行內代碼助手還有聊天機器人可以很大程度上像人一樣和開發者進行互動,但作者還是不認為這種實踐能取代結對編程。

認為編程助手和機器人能取代結對編程的看法可能是產生了一些概念性的偏差,以下是結對編程的優點:

image

編程助手能夠產生明顯作用的是上圖中的第一個區域:“1+1>2”。它可以幫我們擺脫困難,更快啟動和得到可工作的成果,讓我們更加專注在設計整體解決方案上,同時也能共享給我們更多知識。

但結對編程不只是分享代碼中的顯示知識,還有這個代碼庫的演進歷史等隱性知識,這在大語言模型中是獲取不到的。此外結對編程還可以改進團隊的工作流、避免時間浪費、讓持續集成更加容易。它還幫助我們鍛鍊了溝通、同理心、給予和接受反饋等寫作技能。它還在遠程工作中給團隊提供了寶貴的產生連接的機會。

結論:
編程助手只能够 cover 結對編程中的一小部分目標和好處,因為結對編程不只是幫助個人,還是幫助團隊做整體性提升的。結對編程可能提高整個團隊的溝通協作水平,改善工作流,增強代碼的 owner 意識。此外結對編程還沒有使用編程助手時遇到的上述提及的壞處。

使用 Github Copilot 的 TDD#

使用 AI 編程助手後,我們就不再需要測試了嗎?TDD 是否過時了。為了回答這個問題,我們用 TDD 在軟件開發中帶來的兩個幫助來測試:1. 提供良好的反饋;2. 解決問題時能採用分而治之的思想

提供良好反饋

良好的返回需要是快速和精確的。無論是手動測試、文檔、code review 都沒有單元測試能給的反饋快。因此無論是人工手寫的代碼還是 AI 生成的代碼,都需要快速反饋來驗證正確性和質量。

分而治之

分而治之是解決大問題的快速思路。這還讓持續集成、基於主幹的開發、持續交付都得以實施。

即使在 AI 生成代碼的情況下,也還是採用迭代開發的模式,更重要的是,LLM 也有類似 CoT Prompt 可以提升模型輸出的質量這種說法,這也正是 TDD 中所推崇的思想,這正是很好契合的。

使用 Github Copilot 做 TDD 的技巧#

  1. 開始

從一個空的測試文件開始不代表從空的上下文一開,一般都還會有相關的用戶故事筆記以及和結對編程夥伴之間的討論。

這些都是 Copilot “看不見” 的,它只能處理拼寫、語法之類的錯誤。因此我們需要給他提供這些上下文:

  • 提供 mock
  • 寫下通過驗收的標準
  • 假設性指導:比如不需要 GUI、使用面向對象編程或者函數式編程

此外 Copilot 使用打開的文件作為上下文,因此我們要保持測試文件和實現文件都打開。

  1. Red

一開始先寫一個描述性的測試用例名稱,名稱越具有描述性,Copilot 生成的測試代碼表現就越好。

Given-When-Then 的結構能夠從三個方面幫助我們:首先,它提醒我們要給出業務上下文;其次,它給了 Copilot 生成有表現力的用例命名的機會;最後,這能夠讓我們看出 Copilot 對這個問題的理解。

比如在寫測試用例命名時,Copilot 幫我們不全的是 “假設用戶... 點擊購買按鈕”,這意味著 Copilot 沒有足夠理解我們的目的,這就可以在文件頂部的上下文描述中加上 “假設不需要 GUI”、“這是一個 Python Flask 應用的 API 測試套件”。

  1. Green

現在可以開始實現代碼了,一個已有的、富有表達性和可讀性的測試用例可以最大程度地發揮 Copilot 的潛能。這時候 Copilot 有了更多可以發揮的輸入,就不需要像嬰兒一樣自己走路了。

回填測試用例:Copilot 在這時很可能不會再 “小步走”,而是會生成大塊的代碼,這些代碼很可能沒有完整的測試用例。這種情況可以回去補充測試用例,雖然不是 TDD 的標準流程,但目前看出現太大的問題。

刪掉重新生成:對於要重新實現的代碼,讓 Copilot 有效果最好的方式是刪除實現並讓它重寫,因為有測試用例在,即使重寫也相對安全。如果這失敗了,刪除內容並分步寫出註釋可能會有幫助。如果還是不行,可能就需要關閉 Copilot 自己來寫了。

  1. Refactor

在 TDD 中重構意味著用增量改變去改善代碼的可維護性和可擴展性,同時保持一切行為的一致。

對於這一點,Copilot 可能會有一些能力局限,考慮下面兩種場景:

  1. “我知道想做什麼重構”:通過 IDE 的快捷鍵和特性,比如多光標、函數提取、重命名等做重構可能比用 Copilot 重構更快;
  2. “我不知道從哪裡開始重構”:對於小的、局部範圍的重構 Copilot 是能夠提供建議的,但大規模的重構建議 Copilot 還不能做到很好。

在一些我們知道要做什麼,只是想不起語法和 API 的場景 Copilot 可以很好地幫助我們,寫一點代碼註釋就可以自動幫我們完成本來要搜索才能搞定的事情。

結論:
俗話說 "garbage in, garbage out",這對數據工程師和生成式 AI LLM 來說都是一樣的。在作者的實踐中,TDD 保障了代碼庫的高質量,這些高質量的輸入讓 Copilot 取得了更好的表現,因此建議在使用 Copilot 時使用 TDD。

T:umami 網站訪問統計分析#

一個網站訪問統計分析的框架,可以自己部署使用。目前用的 xLog 博客就支持配置 umami,本站用了自己部署的服務。

S:閱讀《李笑來:我的讀書經驗》筆記#

閱讀兩個最基本的價值觀:
・需要認清現實並思考未來;
・更喜歡有繁殖能力的知識。


Reference:

  1. ARTS 打卡活動
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。