與AI結(jié)對:一位高級開發(fā)人員構(gòu)建插件的歷程
作者分享了他使用 ChatGPT 學(xué)習(xí) Go、瀏覽 Kolide API 以及構(gòu)建一個復(fù)雜的 Steampipe 插件的經(jīng)驗。
譯自Pairing With AI: A Senior Developer's Journey Building a Plugin,作者 Jon Udell。
雖然改進(jìn)開發(fā)人員文檔始終有幫助,但許多人(包括我自己)更喜歡在實踐中學(xué)習(xí)。這是我在七項指導(dǎo)原則中提出的第七項,也是最重要的一項:因為你在面向任務(wù)的可教授時刻獲取知識,所以學(xué)習(xí)不是前瞻性的,而是直接且切實的。
當(dāng)經(jīng)驗豐富的開發(fā)人員與 LLM 合作時,其機(jī)器智能會支持并增強(qiáng)你的智力。
好處對我來說很明顯。在 LLM 時代編寫Steampipe 的 ODBC 插件比我在沒有此類幫助的情況下編寫插件的體驗容易得多。但那公認(rèn)是一個主觀評估,所以我一直在尋找一個機(jī)會與另一位插件開發(fā)人員比較筆記,當(dāng)James Ramirez在我們的社區(qū) Slack 中出現(xiàn)并宣布一個新插件用于Kolide API時。我邀請他告訴我他構(gòu)建它的經(jīng)歷,他親切地帶我進(jìn)行了一次與 ChatGPT 的長時間對話,在此對話中,他熟悉了三個對他來說都是新知識的技術(shù)領(lǐng)域:Kolide API、Go 語言和 Steampipe 插件架構(gòu)。
作為一個額外的挑戰(zhàn):雖然插件開發(fā)人員通常為其插件針對的 API 找到合適的 Go SDK,但這里并非如此。因此,有必要為 Kolide API 創(chuàng)建一個 Go 封裝,然后將其集成到插件中。
測試 ChatGPT 的 Go 能力
James 從一些熱身練習(xí)開始。首先,為了測試 ChatGPT 的 Go 能力,他提供了一對 Go 函數(shù),他編寫了這些函數(shù)來調(diào)用相關(guān)的 API/devices/和/devices/ID,并要求對它們之間的共享邏輯進(jìn)行慣用重構(gòu)。
接下來,他使用簡單的可變參數(shù)而不是更復(fù)雜的函數(shù)選項模式探索了函數(shù)的可選參數(shù),并確定了簡單的方法——使用Search結(jié)構(gòu)的切片來封裝 Kolide 的查詢參數(shù)的字段/運(yùn)算符/值樣式——就足夠了。他要求一個函數(shù)將Search結(jié)構(gòu)的切片序列化為一個 REST URL,然后優(yōu)化 ChatGPT 提出的版本以創(chuàng)建最終的serializeSearches,該版本增加了對將友好名稱映射到參數(shù)的支持并使用字符串構(gòu)建器。
AI 處理吹毛求疵,并經(jīng)常提供可提交的建議。
其中一些優(yōu)化,包括使用字符串構(gòu)建器,是由 AI 驅(qū)動的機(jī)器人CodeRabbit提出的,它提供了有用的代碼審查。他說,這是幫助你和你的團(tuán)隊專注于全局的反饋,因為它處理吹毛求疵,并經(jīng)常(但并非總是)提供可提交的建議。它還采取更廣泛的視角來總結(jié)拉取請求并評估已關(guān)閉的 PR 是否解決了其關(guān)聯(lián)問題中陳述的目標(biāo)。
映射運(yùn)算符
他繼續(xù)探索將 Steampipe 運(yùn)算符(如QualOperatorEqual)映射到 Kolide 運(yùn)算符(如Equals)的方法。同樣,ChatGPT 提出的方法也變成了一個一次性方案,朝著一個干凈簡單的方案前進(jìn)。但正如 James 在我們的采訪中證實的那樣,由于你無論如何都會迭代一次性版本,所以能夠生成合理的迭代而不是通過手工更繁瑣地對它們進(jìn)行編碼是有幫助的。在此過程中,他正在學(xué)習(xí)基本的 Go 慣用語。
James:
Go 中有 do-while 循環(huán)嗎?
ChatGPT
沒有,但是……
James:
Go 中有三元運(yùn)算符嗎?
ChatGPT
沒有,但是……
James:
我如何追加到map[string]string?
ChatGPT
像這樣……
使用反射增強(qiáng)的訪問者模式
在理解了基礎(chǔ)知識并為 Kolide API 開發(fā)了一個 Go 客戶端后,James 準(zhǔn)備解決插件開發(fā)的實際工作:定義從 API 封裝返回的 Go 類型映射到控制針對這些表的 SQL 查詢的 Steampipe 架構(gòu)的表。
與所有插件開發(fā)者一樣,他從一個可以列出資源集的表開始,然后通過過濾和分頁對其進(jìn)行增強(qiáng)。在添加第二個表后,是時候考慮如何抽象出公共模式和行為。最終結(jié)果是訪問者模式的優(yōu)雅實現(xiàn)。以下是與表kolide_device和kolide_issue相對應(yīng)的 SteampipeList函數(shù)。
func listDevices(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
  var visitor ListPredicate = func(client *kolide.Client, cursor string, limit int32, searches ...kolide.Search) (interface{}, error) {
    return client.GetDevices(cursor, limit, searches...)
  }
  return listAnything(ctx, d, h, "kolide_device.listDevices", visitor, "Devices")
}
func listAdminUsers(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
  var visitor ListPredicate = func(client *kolide.Client, cursor string, limit int32, searches ...kolide.Search) (interface{}, error) {
    return client.GetAdminUsers(cursor, limit, searches...)
  }
  return listAnything(ctx, d, h, "kolide_admin_user.listAdminUsers", visitor, "AdminUsers")
}以下是所有插件表通用的列表清單函數(shù)。
func listAnything(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData, callee string, visitor ListPredicate, target string) (interface{}, error) {
  // Create a slice to hold search queries
  searches, err := query(ctx, d)
  if err != nil {
    plugin.Logger(ctx).Error(callee, "qualifier_operator_error", err)
    return nil, err
  }
  // Establish connection to Kolide client
  client, err := connect(ctx, d)
  if err != nil {
    plugin.Logger(ctx).Error(callee, "connection_error", err)
    return nil, err
  }
  // Iterate through pagination cursors, with smallest number of pages
  var maxLimit int32 = kolide.MaxPaging
  if d.QueryContext.Limit != nil {
    limit := int32(*d.QueryContext.Limit)
    if limit < maxLimit {
      maxLimit = limit
    }
  }
  cursor := ""
  for {
    // Respect rate limiting
    d.WaitForListRateLimit(ctx)
    res, err := visitor(client, cursor, maxLimit, searches...)
    if err != nil {
      plugin.Logger(ctx).Error(callee, err)
      return nil, err
    }
    // Stream retrieved results
    collection := reflect.ValueOf(res).Elem().FieldByName(target)
    if collection.IsValid() {
      for i := 0; i < collection.Len(); i++ {
        d.StreamListItem(ctx, collection.Index(i).Interface())
        // Context can be cancelled due to manual cancellation or the limit has been hit
        if d.RowsRemaining(ctx) == 0 {
          return nil, nil
        }
      }
    }
    next := reflect.ValueOf(res).Elem().FieldByName("Pagination").FieldByName("NextCursor")
    if next.IsValid() {
      cursor = next.Interface().(string)
    }
    if cursor == "" {
      break
    }
  }
  return nil, nil
}有了此設(shè)置,向插件添加新表幾乎完全是聲明式的:你僅需定義架構(gòu)及 KeyColumns,以及在 SQL 查詢中的 where(或 join)子句與 API 級過濾器之間形成橋梁的相關(guān)運(yùn)算符。然后編寫一個微小的 List 函數(shù),該函數(shù)定義一個訪問器,并將該函數(shù)傳遞到 common listAnything 函數(shù)中,該函數(shù)封裝查詢參數(shù)編組、連接至 API 客戶端、調(diào)用 API、將響應(yīng)解壓縮到一個集合中以及迭代集合以將數(shù)據(jù)項傳輸?shù)?Steampipe 的外部數(shù)據(jù)包裝器的功能。
James 使用 ChatGPT 來啟動訪問者模式在 Go 中的慣用實現(xiàn)。這包括學(xué)習(xí)如何為訪問者函數(shù)定義一個類型,然后聲明一個函數(shù)來滿足類型。每個表的訪問者都封裝對 API 客戶端的調(diào)用,并返回一個接口。所有這些都相當(dāng)通用,但訪問者的響應(yīng)特定于包裝的 API 響應(yīng)的 Go 類型,這意味著需要為每個表編寫一個不同的 List 函數(shù)。如何避免這種情況?James 問道:“res 變量上的字段引用需要是可變類型,在執(zhí)行時指定。你能建議一種方法嗎?”
ChatGPT 的建議(他采納了)是使用反射,以便調(diào)用 listAnything(如 listAnything(ctx, d, h, “kolide_device.listDevices”, visitor, “Devices”))可以傳遞一個名稱("Devices"),使 listAnything 能夠以與類型無關(guān)的方式訪問響應(yīng)結(jié)構(gòu)的字段,例如,此處的 Devices 字段。
type DeviceListResponse struct {
      Devices    []Device   `json:"data"`
      Pagination Pagination `json:"pagination"`
    }正因如此,listAnything 終于如其名,成為一個通用的 Steampipe List 函數(shù)。該解決方案很少使用反射,并在 API 層和 Steampipe 層中都保留了 Go 的強(qiáng)類型檢查。
LLM 協(xié)助真正意味著什么?
它肯定不意味著 LLM 根據(jù)以下提示編寫了一個體現(xiàn)復(fù)雜設(shè)計模式的插件:“我需要一個用于 Kolide API 的 Steampipe 插件,請創(chuàng)建它?!睂ξ襾碚f,以及對 James 來說,它的含義更有趣:“讓我們討論為 Kolide API 編寫插件的過程?!边@就像與一只橡皮鴨交談,以便大聲思考需求和策略。但 LLM 是一只會說話的橡皮鴨。有時響應(yīng)直接適用,有時不適用,但無論哪種方式,它們通??梢詭椭惬@得清晰度。
作為一名經(jīng)驗豐富的軟件工程師,James 本可以想出辦法——但這需要更長的時間。
James 說:“對話要求我對所問的問題非常具體。”雖然他從頭開始使用 Go,但他帶來了豐富的經(jīng)驗,使他能夠快速定位并找出哪些是需要問的正確問題。作為一名經(jīng)驗豐富的軟件工程師,James 本可以自己想出所有這些。但這需要更長的時間,而且他將花費(fèi)大量時間預(yù)先閱讀文章和文檔,而不是通過實踐學(xué)習(xí)。而且可能沒有時間!正如我現(xiàn)在從許多其他人那里聽到的那樣,LLM 提供的加速通常決定了擁有一個想法和能夠執(zhí)行它之間的區(qū)別。
James 還提到了我未考慮過的開源角度。在 LLM 之前,他不會以完全公開的方式完成這項工作?!拔視恢北C?,直到我更有信心,”他說,“但這從一開始就在那里,我很高興它在那里?!边@使得與 Turbot 團(tuán)隊的接觸盡早成為可能。
這不是一個自動化故事,而是一個增強(qiáng)故事。當(dāng)像 James Ramirez 這樣的經(jīng)驗豐富的開發(fā)人員與 LLM 合作時,其機(jī)器智能支持并放大了他的智力。兩者協(xié)同工作——不僅編寫代碼,更重要的是,思考架構(gòu)和設(shè)計。















 
 
 







 
 
 
 