從網頁對話到 coding agent第 2 篇,共 9 篇

LLM 與 agent 的運作原理

Agent 為何會遺忘與編造,以及如何檢查它的結論。

發佈於

Agent 的回答並非總是可信,它會忘記先前交代過的事,也會編造不存在的函數、文獻或數字。多數人遇到這些情形只能反覆重試,分不清問題出在何處。

本篇說明 LLM 與 agent 各自如何運作,遺忘與編造從何而來,以及如何檢查 agent 的結論。

基本原理

LLM(Large Language Model,大型語言模型)所做的事只有一件,即讀進一段文字,預測接下來最可能出現的一小段,將其接在後面,再以加長後的文字預測下一段,如此反覆,直到回答結束。這一小段稱為 token,是 LLM 處理文字的單位,大小介於字母與單字之間,常見的英文單字是一個 token,較長或少見的字會被拆成數個,中文則大致一字一個或更多。可以將其理解為 LLM 眼中的字數。用量也以 token 計算,LLM 讀進的與生成的 token 都計入,口語上說的消耗 token 就是這件事。

也就是說,LLM 是一個以大量文字訓練出來的預測模型,輸出的是在訓練資料裡最常接在這段文字之後的內容。它並不查閱資料庫,也不在內部執行計算,回答之所以多半正確,是因為正確的說法在訓練資料中出現得最多。這就像一個背了大量考古題的考生,看到題目都是那麼解,即使不理解底層原理,考試時仍然可以照著寫對;考生遇到考古題裡沒有的題目,也會照舊題的樣子寫滿,看似完整卻未必正確。

如果某個問題在訓練資料中少見,或答案取決於它沒見過的東西,例如使用者電腦上的某份資料,它仍會照樣輸出一段讀來通順的文字,但通順與否和正確與否無關;「不知道」在多數情境下並不是最可能的接續,因此它會給出一個形式上合理的答案。這就是編造的由來,在資料分析裡常見的情形如下:

  • 套件裡不存在的函數或參數,名稱取得很像真的。
  • 不存在的文獻,作者、年份、期刊俱全。
  • 沒有實際執行就報出的數字,或執行過但轉述時寫錯的數字。
  • 對資料的描述與實際不符,例如聲稱某欄沒有缺失值,而它其實並未檢查。

預測時帶有隨機性,因此同一個問題問兩次,回答的措辭乃至結論都可能不同。

Context window

LLM 每次預測所能讀進的文字有上限,這個上限稱為 context window,同樣以 token 計。它限制的是單一對話一次讀得進多少;訂閱方案限制的則是一段時間內合計的用量,兩者是兩件事。可以將其想成一塊大小固定的白板,對話至今的每一句話、讀過的每一個檔案、執行指令後的每一段輸出,全都要寫在板上,LLM 才看得到;板上沒有的東西,對它而言便不存在。Claude 桌面 app 的模型選單旁有一個用量環,點開可以看到目前對話已用去多少 context window。

所謂記憶,也只是這塊白板上的內容。同一個對話裡 LLM 之所以記得先前說過什麼,是因為每一次送出訊息時,整段對話都會從頭再送一遍,重新寫在板上;開啟新的對話,即是換上一塊全新的白板,上一個對話的內容一概不知,LLM 本身沒有跨對話的記憶。

現今的網頁對話卻常像是記得其他對話裡提過的事,是因為服務商在背後將過往的對話提煉成摘要,每開一個新對話,便先將摘要寫在板上,LLM 讀到了,看來便像是記得。因此新的對話並不是真的從空白開始,一開始便已用去一部分 context window,只是這一部分並不顯示在對話裡,使用者看不到。

Coding agent 也有同類的機制,agent 將它判斷日後用得上的事寫成檔案,新對話開始時載入;寫不寫、寫什麼都由它決定。

對話內容即將超出 context window 時,早期的做法是直接截去最舊的部分,開頭說過的事便整段消失;現今幾乎所有 agent 都改為自動整理,做法類似磁碟快滿時清理空間。以 Claude 為例,先刪去較舊的工具輸出,即 agent 讀檔與執行指令時傳回的內容,仍不足時再將先前的對話濃縮成摘要,只留下它判斷之後仍用得上的部分,然後繼續工作。哪些不重要由它判斷,並不保證判斷正確,正如清理磁碟時也可能刪去日後才發現要用的檔案;Claude 的官方文件寫明,使用者的要求與重要的 code 片段會保留,但對話早期交代的細節,例如「缺失值一律不補」或「圖一律用灰階」這類規則,可能在此過程中遺失。

這就是 agent 遺忘的由來。對策是將須長期遵守的規則寫進檔案,讓 agent 每次對話都會讀到,這就是「規範檔的作用與位置」一節的規範檔;還沒有規範檔時,如果發現 agent 開始違反先前講定的事,與其一再糾正,不如開一個新對話,重新交代一次。

Agent 的工作流程

LLM 只讀進文字、輸出文字,自己不能開啟檔案,也不能執行 code。「初次使用:讀取既有資料夾」一節看到的讀檔與執行,是由包在 LLM 外面的那一層完成的,這一層稱為 agent 框架(英文為 harness),以下簡稱框架。以人來比喻,LLM 是大腦,只負責判斷下一步該做什麼;框架是手腳與感官,照大腦的決定去開啟檔案、執行指令,再將看到的結果傳回大腦。兩者合起來才是一個能辦事的人,即「網頁對話與 coding agent 之別」一節所說的代理人,也就是 agent。

流程是一個循環。使用者送出指示後,LLM 輸出的如果不是回答,而是一個請求,例如「開啟某個檔案」或「執行這行指令」,框架便代為執行,並將檔案的內容或指令的輸出以文字送回 LLM;LLM 讀到之後決定下一步,可能再讀一個檔案,可能修改 code 後再執行一次,直到它判斷任務完成,才輸出給使用者的回答。

step 0/8loops 0
UserLLMHarnessTools

現今的網頁對話也有框架,同樣是 agent,其中的搜尋即是一例,讀者多半已見過。以下列出其中幾次來回,實際的請求與傳回都是特定格式的文字,此處改用白話:

使用者:lme4 套件最近一次更新改了什麼
LLM :請求,搜尋「lme4 CRAN NEWS」
框架:傳回,搜尋結果的標題、網址與摘要
LLM :請求,開啟 CRAN 上 lme4 的頁面
框架:傳回,頁面的文字
LLM :請求,開啟該頁連結的 NEWS
框架:傳回,更新紀錄的全文
……
LLM :回答,最近一版的主要改動,並附上來源連結

每一行「請求」都是 LLM 輸出的文字,每一行「傳回」都是框架送回的文字;這使得 LLM 可以擁有作為純文字模型本不具備的能力。Research 一類的功能也是同一個循環,只是來回的次數多得多。

「網頁對話與 coding agent 之別」一節提過,網頁對話與 coding agent 背後是同一個 LLM;兩者也都是 agent,流程與上例相同,差別在於框架不同,給的工具也就不同。網頁對話的工具是搜尋、開啟網頁,以及在服務商的電腦上執行 code;coding agent 的工具則是在使用者的電腦上讀檔、改檔與執行指令,並負責在動手前徵求同意,一句指示背後,往往是十數次這樣的來回。

每一次來回,框架都將至今的全部內容連同新的工具輸出重新送給 LLM,讀進的檔案與執行的輸出愈多,context window 與用量便消耗得愈快。

本篇其後單稱 agent 時,指的都是 coding agent。

由此可知,同一件工作交給不同的 coding agent,結果不會相同。LLM 本身就不同,好比各人掌握的知識不同,同樣的資訊擺在面前,有的看得出問題所在、提得出更有用的做法,有的則不能,模型的選擇見「模型規模與 effort」一節。

框架也不同,同一個 LLM 配上不同的框架,交到手上的工具與資訊便不相同,缺了工具有些事做不了,而資訊如果是不足,再強的大腦也難以做出有效的判斷。

這些資訊有一大段是框架在每次對話開始前便已先寫上的:其內建的指示,稱為 system prompt,交代 agent 該如何行事;各項工具的說明,LLM 之所以知道自己可以讀檔、可以執行指令,全靠這段文字;以及專案裡的規範檔,與已安裝的 skill 的簡介,前者是使用者寫給 agent 的規矩,後者是事先寫好、用到時才載入的做法說明。這些都在使用者開口之前便已佔去一部分 context window,同一個 LLM 在不同的 app 裡表現不同,多半也是這個原因。

32%compacted 0×

SystemRulesChatFilesToolsSummary

此外還有指示,即 prompt。LLM 學過的知識遠多於它會主動用上的,未被要求的事,即使知道該怎麼做,通常也不做。Agent 的 prompt 並不只是使用者送出的那句話,上述框架預先寫上的內容也在其中,使用者能改的是規範檔、skill 與每次送出的話,前兩者於規範檔與 skill一篇說明,後者見「Prompt 技巧與協作的做法」一節。

結果須自行驗證

前述幾種編造裡,coding agent 對不存在的函數與參數有天然的抵抗力,因為 code 會被實際執行,當場報錯,agent 讀到錯誤訊息便會修正。其餘的則沒有,文獻不會報錯,寫進回答裡的數字與描述是 LLM 生成的文字,並非直接取自輸出,轉述的過程便有可能出錯。

因此 agent 說「已完成,結果顯著」時,這句話同樣是 LLM 生成的文字,是否確實完成,仍須親自檢查。

做法之一是要它拿出圖與數字,即要求 agent 將結論所依據的圖與表格由腳本輸出成檔案,並指明每個數字出自哪一個腳本的哪一行,然後自己開啟檔案查看。

把這個結論所依據的圖存成 PNG、係數表存成 CSV,放在 output 資料夾。
逐一列出回答裡提到的每個數字,註明是哪個腳本的哪一行算出來的。
沒有實際執行就寫下的數字,請標明。

圖與表格既是腳本的輸出,重新執行腳本便能重現,與 agent 的轉述無關;如果回答裡的數字與檔案裡的不一致,以檔案為準。

另一個做法是開一個新對話審查。在同一個對話裡請 agent 檢查自己的成果,效果有限,因為先前的推理仍留在 context window 裡,它會傾向於沿著原來的思路再走一遍;新的對話沒有這些內容,較容易看出問題。