iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
AI Engineering

AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄系列 第 9

Day 9|第一份 Skill:怎麼延續前次審查?能不能直接接上,在發佈那一刻就決定了

  • 分享至 

  • xImage
  •  

簡短回顧

前面幾天把這個 Code Review Skill 的核心流程與架構都闡述出來了。今天補的是架構外的東西:它要怎麼透過 GitLab API 發佈報告,以及下一輪怎麼接回來。

這裡有一個很容易被「POST 成功」掩蓋的問題:報告出現在 MR 上,不代表發佈流程就完整了。

如果發佈當下沒有保存後續所需的識別資訊,這一輪看起來完全成功;等作者回覆、下一輪審查要接續時,才會發現系統找不回上一份報告。

這個問題最後落在兩個值上:根留言的 created_at,以及 discussion_id。它們各自負責什麼,要先從 GitLab 的 Note 與 Discussion 資料模型說起。

報告發出去之後,你怎麼找回作者的回覆?

今天所有的端點,其實都是在回答這一題。

Day 4 講過,第二次以後的審查要把「前次報告發佈之後,MR 上又出現了哪些討論」撈回來當輔助資訊,作者的回覆也在其中。目前這份 Skill 會保存根留言的 created_at 作為 cutoff 時間 T;下一輪逐頁取回 MR 的所有 discussions,只保留 T 之後新增的 notes。

但時間只能回答「發佈之後發生了什麼」,不能直接指出「我上次貼的是哪一則報告」。所以發佈紀錄還要保存 discussion_id:目前重新審查的批次撈取不靠它限縮範圍,但直接查閱那一串、回覆,以及追蹤報告時都會用到。這兩種留言在 UI 上長得很像,在資料模型裡不是同一個東西,我一開始沒分清楚。所以資料模型先講,端點放後面。

Note 是原子,Discussion 是容器

Note = 一則留言。MR 留言區裡你看到的每一段文字都是一個 note,包括系統自動產生的(「added 1 commit」那種 system note)。

Discussion = 裝 note 的串。在 GitLab 的資料模型裡,每一個 note 其實都住在某個 discussion 裡,沒有例外。差別只在容器的型態:

  • Individual note discussion:建立時只有一個 note,也就是 UI 上按「Comment」發出的獨立留言。有人回覆後,它可以升格成 thread
  • Thread(可回覆討論串):容器裡可以有 N 個 note(根留言+回覆們)。這是 UI 上按「Start thread」開的串:可回覆、可 resolve(還有一種掛了 position 的變體,錨定在 diff 特定行上,就是 Code Review 常見的行內評論)

所以在這份 Skill 的正常路徑裡,主報告固定走 Discussions API。關鍵不是能不能被回覆,而是 POST .../discussions 會在發佈成功時回傳建立完成的 discussion object;其中頂層的 id,就是後續 API 路徑使用的 discussion ID。目前重新審查時,Skill 以時間 T 撈取整個 MR 的新 discussion activity;discussion ID 則保留給直接查閱、回覆與報告追蹤。

真正的差別不是「能不能回覆」

我第一版把主報告使用 Discussions API 的理由寫成:「individual note 不能被串狀回覆,也不能 resolve。」

這個理由是錯的。GitLab 官方文件寫得很清楚:回覆一則 individual note,正是把它升格成 thread 的動作;升格之後,它也可以被 resolve。

這個修正不是 API 冷知識而已。如果選擇 Discussions API 的理由只有「individual note 不能回覆」,那麼一旦知道它其實可以回覆,整個選擇就失去依據,下一個維護者很可能把流程改回 Notes API。

真正的需求不是「這則留言以後能不能補救」,而是:發佈完成的當下,系統有沒有拿到足以接續下一輪的狀態。

POST .../discussions 會回傳建立完成的 discussion object。這份 Skill 會把頂層的 id 保存為 discussion_id,並把 notes[0].created_at 保存為根留言的 created_at,一起寫進發佈紀錄:

  • created_at 用來切出發佈後新增的 discussion activity。
  • discussion_id 用來直接查閱、回覆與追蹤這一份報告。

技術上,只要另外保存 note id,individual note 仍然可以從 discussions 清單反查。但那是一條需要重新搜尋與關聯狀態的 recovery flow,不是目前這份 Skill 的正常路徑。

正常路徑既然能在發佈當下取得後續必需的識別資訊,就沒有理由先把它丟掉,等下一輪再猜自己上次發了什麼。

那我們要用到哪些端點?

我們內部使用的是 GitLab,因此本文會以 GitLab 情境為主。

同樣的設計問題也存在於 GitHub,但資料模型與 API 並非一對一;若要套用,仍須重新確認對應的留言、討論串與回覆機制。

我會刻意把 GitLab URL {domain} 做成模板形式,後續方便大家置換。

baseUrl 就會是:https://{domain}/api/v4

真正驅動整條流程的只有三個:發佈報告走 POST .../discussions,下一輪用 GET .../discussions 逐頁取回所有 discussion items,再保留 時間 T 之後新增的 activity;要回話則是 POST .../discussions/:discussion_id/notes。其餘六個是配套:確認身分、取 MR、下載附件、零星留言。完整的九條盤點放在本文最後。

GET .../discussions 是分頁端點,預設每頁 20 筆。只讀第一頁不等於取得整個 MR;實作必須跟著 pagination 逐頁取完。

底下四件事值得單獨講,因為只看官方文件,不容易直接理解這些端點在這套 Code Review 流程裡該怎麼使用。

一、URL 裡的 :id:merge_request_iid 指向不同層級。 我發起審查時會提供 MR URL,LLM 要 parse 出專案路徑與 MR 編號。API 路徑中的 projects/:id 是專案識別碼,可以使用數字 project ID,也可以使用 URL-encoded 的完整專案路徑;:merge_request_iid 則是該專案內的 MR 編號,也就是 MR URL 最後面的數字。

取 MR 那個端點會回這幾個後面用得到的欄位:titledescriptionsource_branchtarget_branchweb_url(組報告直連 URL 用)、project_id。另外它還會回一個 diff_refs(裡面有 base_sha / head_sha),這套流程目前沒有用到它:為什麼會需要它,後面真的跑一場就知道了。

二、附件是驗證需求覆蓋的關鍵素材。 MR description 裡的附件([名稱](/uploads/{secret}/{filename}) 格式)通常是需求規格或畫面截圖。逐個下載、按副檔名分流處理:.md.txt 對照 diff 查需求覆蓋,圖片用視覺工具看。單一附件若下載失敗就報告註明、繼續審,不中止。

三、發佈報告會留下兩種後續資訊。 GitLab 回傳的 discussion object 頂層 id 會保存為 discussion_id,用於直接查閱、回覆與追蹤這一則報告;notes[0].created_at 則保存為根留言的 created_at,作為 cutoff 時間 T。目前再次審查時,Skill 會逐頁取回整個 MR 的 discussions,再以 T 保留發佈後新增的 notes,沒有用 discussion_id 把範圍限縮在單一討論串。兩個值仍會一起回寫進報告 JSON 的「發佈紀錄」,但用途不同。

四、個別留言不是給主報告用的。 它是為了零星的單則留言場景而存在。GitLab 官方也明確區分兩者:Notes API 不會回傳 discussion thread 裡的 DiscussionNote,要取得串內回覆仍得走 Discussions API。目前這份 Skill 與 proxy 都沒有開放 GET/POST .../notes;附錄保留它們,是為了把資料模型與能力邊界交代完整。在這份 Skill 的正常路徑裡,主報告固定走 Discussions API,理由就是前面那一節。

最重的一條規則:發佈之前要有人點頭

九個端點裡有三個會把東西寫出去:發報告、發個別留言、回覆討論串。這三個掛著整份文件最重的一條硬規則:對外、不可逆的動作要 human in the loop,AI 不得自動呼叫。

要發報告,先把渲染好的內容在對話裡給使用者看,點頭才發。要回覆作者,一樣先給草稿。理由不是不信任模型,是這個動作的後果不在我這台機器上:它會出現在同事的 MR 底下、永久留存,而且沒有任何一個 API 可以讓我把「已經被讀到」收回來。

老實講,這條規則現在只是一句寫在 skill 文件裡的話。寫在文件裡的規則,擋不擋得住一個已經拿到能力的 agent?這個系列後面會用一次真的越界來回答。更難看的是,那次規則甚至已經寫在模型外面了。

這裡要先分清楚:proxy 強制的是「agent 能呼叫哪些端點」,不是「使用者是否已經點頭」。發報告與回覆仍在白名單內,human-in-the-loop 目前仍由 Skill 的流程規則約束。

Proxy 的確切實作會在後面跟大家分享

真正擋得住它的東西不會寫在 skill 裡,它得放在 skill 構不到的地方。上面那九個操作最後只有七個穿得過去,而被擋掉的那兩個,正好就是本文前半段講錯過理由的那對個別留言端點。擋下它們的那個東西還順手做了兩件事:它替 AI 在每一個請求上蓋章,卻不把印章交到 AI 手上;它也會數次數。

這條路要等到講環境的那幾天才鋪得完。到時候會看到一句話:知道有這個端點,跟決定放行這個端點,是兩件事。

附錄:九個端點的完整盤點

用途 方法與路徑 什麼時候用
確認 token 有效、知道「我是誰」 GET /user 發 comment 前的身分前置確認
取回 MR 詳情 GET /projects/:id/merge_requests/:iid 每一場審查的第一步
下載 MR 附件 GET /projects/:id/uploads/:secret/:filename 說明裡有規格或截圖時
發佈報告 POST /projects/:id/merge_requests/:iid/discussions 審查完、使用者點頭之後
列出所有討論串 GET /projects/:id/merge_requests/:iid/discussions 再次審查時逐頁取得所有 discussion items,再保留時間 T 之後的新 activity
取單一討論串(含回覆) GET …/discussions/:discussion_id 已知 discussion_id 時直接取得該討論串
取得個別留言(不含 discussion thread 裡的回覆) GET /projects/:id/merge_requests/:iid/notes API 盤點;目前 Skill 與 proxy 未開放
發一則個別留言 POST /projects/:id/merge_requests/:iid/notes API 盤點;目前 Skill 與 proxy 未開放,主報告也不走這條
回覆討論串 POST …/discussions/:discussion_id/notes 回作者的話

官方文件在 docs.gitlab.com/19.2/api 底下,照路徑找得到,我就不逐條貼連結了。連結指的是較新的版本。除了附件下載那條(GitLab 17.4 才有)之外,其餘在更舊的 GitLab 上都已經存在。

本日小結

今天把 GitLab API 這條路徑鋪完了:從驗證身分、取 MR、下載附件,到發佈報告與取回討論串。

但真正要記住的不是那張表,是為什麼要先問資料模型再看端點。在這份 Skill 的正常路徑裡,主報告固定走 Discussions API,因為它會在發佈成功時回傳完整的 discussion object。這份 Skill 會把頂層 id 保存為 discussion_id,把 notes[0].created_at 保存為根留言的 created_at。再次審查時,後者作為時間 T,用來取得整個 MR 在報告發佈後新增的 discussion activity;前者則用於直接查閱、回覆與追蹤那一則報告。對 AI workflow 來說,重點是每一次外部操作完成時,都留下後續步驟真正會使用的狀態。

明天處理報告發出去之後的事:作者反駁怎麼辦,以及版本號怎麼訂。


上一篇
Day 8|第一份 Skill:審查報告怎麼交付?結論由程式決定,不由 AI 語氣決定
系列文
AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言