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 後再執行一次,直到它判斷任務完成,才輸出給使用者的回答。
現今的網頁對話也有框架,同樣是 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 ×
SystemRulesChatFilesToolsSummary
此外還有指示,即 prompt。LLM 學過的知識遠多於它會主動用上的,未被要求的事,即使知道該怎麼做,通常也不做。Agent 的 prompt 並不只是使用者送出的那句話,上述框架預先寫上的內容也在其中,使用者能改的是規範檔、skill 與每次送出的話,前兩者於規範檔與 skill一篇說明,後者見「Prompt 技巧與協作的做法」一節。
結果須自行驗證
前述幾種編造裡,coding agent 對不存在的函數與參數有天然的抵抗力,因為 code 會被實際執行,當場報錯,agent 讀到錯誤訊息便會修正。其餘的則沒有,文獻不會報錯,寫進回答裡的數字與描述是 LLM 生成的文字,並非直接取自輸出,轉述的過程便有可能出錯。
因此 agent 說「已完成,結果顯著」時,這句話同樣是 LLM 生成的文字,是否確實完成,仍須親自檢查。
做法之一是要它拿出圖與數字,即要求 agent 將結論所依據的圖與表格由腳本輸出成檔案,並指明每個數字出自哪一個腳本的哪一行,然後自己開啟檔案查看。
把這個結論所依據的圖存成 PNG、係數表存成 CSV,放在 output 資料夾。逐一列出回答裡提到的每個數字,註明是哪個腳本的哪一行算出來的。沒有實際執行就寫下的數字,請標明。圖與表格既是腳本的輸出,重新執行腳本便能重現,與 agent 的轉述無關;如果回答裡的數字與檔案裡的不一致,以檔案為準。
另一個做法是開一個新對話審查。在同一個對話裡請 agent 檢查自己的成果,效果有限,因為先前的推理仍留在 context window 裡,它會傾向於沿著原來的思路再走一遍;新的對話沒有這些內容,較容易看出問題。