〈Stanford CS329A Self-Improving AI Agents〉-EP-Part 7 | Self-Improvement and Deep Research Agents

https://www.youtube.com/watch?v=Uni9dqyuuDM

模型的輸出裡本來就躺著解答,難的是把它挑出來。

Stanford CS329A 第七講把整堂課押在「搜尋」這兩個字上。講者開場先把前提講白:我們大致知道解答存在於模型輸出的搜尋空間裡,這堂課要處理的是怎麼從那個空間裡把答案挑出來。她示範了兩種很不一樣的搜尋。一種是把 sampling 開到極限,再靠 filtering 和 clustering 把候選收斂到交得出去的數量,這是 AlphaCode 與 AlphaCode 2 的路線。另一種是在推理過程中一邊發現知識缺口、一邊發查詢,把檢索到的文件讀懂之後抄成筆記放回思路,這是 Search-O1。課程安排上,作業二用的是 HumanEval 這類單函數題目,作業三才會真正走到 agentic search 這一套。

一百萬個解答,只交十個出去

AlphaCode 要解的不是補完一行程式,而是完整的競賽題。題目給你一段描述、一組輸入輸出,等於一份契約:餵進這組輸入,就該吐出那組輸出。寫 code completion 的 agent 是幫你把當下那行接完;這裡要的是從題目讀出意圖、想出演算法、生出整個區塊的程式,而且連「先看懂 docstring 才知道該用哪種解法」都算在內。講者說,模型一次吐一行建議,開發者接得很順,成功率也高;換成端到端解掉一道題,就是另一個量級的問題。

這篇論文發表在 2022 年,講者說這是將近四年前的工作。當時他們讓模型直接在真實比賽裡跑,賽中生成、賽中篩選、賽中提交,總共打了十場,每場大約五千名參賽者。平均成績是排進前 54.3%,前提是每題只交十個候選。講者補了一句,這個水準大致和過去半年在 Codeforces 上出賽的參賽者裡約 28% 的那群人相當。這也是第一次有人示範 AI 可以跨出狹窄任務、把一道題從頭解到尾。成績表上列著每場比賽的最好、估計與最差排名,而排名是把提交出去的解放到其他參賽者的解旁邊,比正確性,也比效率。

這條 pipeline 的前半段是自己養一顆模型。他們用 encoder-decoder 架構,先在 GitHub 上大約 700GB 的程式碼做預訓練,目標是 next token prediction,loss 走 masked language model 那一套。到了微調階段,他們加了幾個技巧,其中一個叫 GOLD:對高機率的 token 給比較高的權重,對低機率的 token 給比較低的權重,等於在 next token prediction 的 loss 上再疊一層跟 token 可能性有關的加權,目的是把精確度拉起來,讓機率集中在比較有意義的模式上。他們另外做了 value conditioning 與 prediction。

接著才是這條 pipeline 真正的主角:大規模 sampling。每一道題生成一百萬個程式,一半 Python、一半 C++;prompt 裡的題目標籤與 rating 會被打亂,sampling 溫度拉高,讓解法盡量分散。

一百萬個答案不可能全部交上去,所以中間多了一道手續:先篩選,再 clustering。篩選的條件很樸素,只留下能通過題目範例測資的程式。clustering 處理的是「寫法不同、語意等價」這件事,他們另外訓練了一個測試輸入生成模型,讓它看著題目描述去預測新的測試輸入,用這種方式把語意等價的解法歸在一起。做完這兩步,才有辦法從一百萬個樣本裡挑出一小批候選,送進 Codeforces 平台。講者說,如果一題只能交十個,你不會想交十個長得差不多的解法。

評估有兩條軌道。一條是 Codeforces 的正式比賽,交出去就跟人類參賽者一起排名;另一條是 CodeContests 這個訓練資料集裡留出來的測試集,離線就能量到訊號,不必每次都在真實賽事上跑。

課堂上有人指著成績表問,為什麼同一套系統在某些比賽排得很前面,換一場就掉好幾個百分點。講者沒有直接回答,先把問題丟回去,再自己收:一種可能是那些題目離訓練分布比較遠,模型本來就沒見過;另一種可能是選答案這一步在某些比賽特別不靈光。搜尋空間裡有正確解,不代表你有能力把它挑出來,選得不好,一樣拿不到分。

pass@k 量覆蓋率,10@k 量你有沒有本事挑

要讀懂 AlphaCode 的成績,得先把兩個指標分開。pass@k 問的是:讓模型生成 k 個樣本、全部送去隱藏測資評測,有幾成題目真的被解掉。它量的是搜尋空間的覆蓋率,也就是取樣這件事本身有多會撈。10@k 的那個十,是實際能交出去的提交數;你就算生了一百萬個樣本,也沒辦法把一百萬個都拿去對隱藏測資,中間一定要有篩選與評分的動作。所以 10@k 量的是 selection 這一端做得好不好,兩個指標擺在一起,瓶頸在哪裡就很清楚。

實驗把模型分成 9B、41B,以及 41B 加上 clustering 三組,在驗證集與測試集上從 10@1k 一路做到 10@10k、10@100k、10@1M。結果有三件事很穩定:模型越大分數越好,樣本越多分數越好,同樣的模型再加上 clustering,分數再往上墊一層。clustering 之所以有效,理由不是它讓模型更聰明,而是它讓你能在十個名額裡塞進十種不一樣的思路。競賽題如果交十份本質相同的解,等於只交了一份。

把取樣預算放到橫軸、解題率放到縱軸,會看到一條對數線性的關係:樣本每多一個數量級,解題率往上加一段;底模越強,斜率越漂亮。41B 那條曲線比底下那顆三億參數的模型陡,同樣的趨勢在 pass@k 上也看得到。講者特別點出一件事:如果只准交十份,曲線會提早壓平。pass@k 是不限提交次數的假想情境,可以爬到 40% 以上;換成 10@k,只剩 30% 左右。中間蒸發掉的那一段,就是選答案的能力還不夠,你被自己的 filtering 與 clustering 卡住。

這條對數線性是可以推導的,Azalia 在前面的課講過那篇推導;講者要強調的是,就算中間插進 selection 這一步,這條線還是維持對數線性。課堂上有人問,如果趨勢繼續,樣本數再往上加一個數量級,分數會不會直接翻倍,甚至值得當成專案題目。講者的回答很直接:把模型本身變強,比一直堆樣本容易。AlphaCode 2 就是從這條路走出來的,同樣一百萬個樣本,它可以拿到更好的成績,或者用一百個樣本就打平舊版本。她也補了一個容易被跳過的假設:往上加樣本的前提,是每一個新樣本都帶來新的多樣性;如果多生十倍,解法卻都長得一樣,分數不會動。還有一個更根本的限制,底模的能力本身有天花板,模型不夠強的時候,樣本再多也救不回來。課堂上也提到 saturation:模型越小時,要追到同樣的 pass@k,樣本數得開得越大,而加樣本的效益遲早會平掉。

loss 降下去,不代表解題率會上來

AlphaCode 這條路線的收穫,講者收在覆蓋率上:大規模 sampling 加上 filtering、clustering,確實可以把「正確解在不在樣本裡」這件事推到很高的水準。他們還回頭查了一件事,確認模型不是在抄訓練資料裡的解法。搜過之後,生成出來的程式帶有某種新意,所以這算是一種分布外的泛化,模型是真的在推理。

但訓練時盯著的 loss,對解題率來說是很差的代理指標,這顆模型在動態規劃與構造類演算法上就明顯偏弱。第二個代價是規模:每題一百萬個樣本,放在哪裡都不便宜。如果場景有時間限制,你需要的其實是一邊生成、一邊把可接受的解排出來,而不是等全部生完再挑。還有一件事跟難度有關:越難的題目需要越多樣本,而且這套一次到底的做法在難題上並不強;真正該走的方向是多步解法,讓難題拆成好幾步走得完的過程。

AlphaCode 2 換掉底模,也多了一顆會打分的模型

AlphaCode 2 的起手式是一個假設:為什麼要自己預訓練一顆模型,直接拿一顆現成的 LLM 來微調不行嗎。這篇在 Google 的工作挑了 Gemini Pro,做法是微調,而不只是在 prompt 裡把它帶進競賽題的世界。

取樣那邊的改動是把多樣性做到極致。他們養的不是一顆 AlphaCode 2,而是一家族模型:把 CodeContest V2 切成不同片段,配上不同的超參數、難度與題目標籤,各自微調出一個版本,再讓這些版本一起貢獻樣本。語言也收斂到只剩 C++,不像上一代是一半 Python、一半 C++;生成時把溫度與 metadata 都打亂,繼續逼多樣性。資料也換了,CodeContest V2 是開源的版本,問題與解的品質都更高。

真正的差別在選擇階段。第一代靠 clustering 這類啟發式規則,判斷哪些解法語意等價、哪些該留;第二代訓練了一顆 scoring model,講者直接把它叫作 reward model,讓它替每個候選估一個 0 到 1 之間的正確機率。這顆模型吃的不是拿來生成的那份資料,而是另一個品質更高、由人工標註與評分的資料集,學的其實是排序:同一道題,兩個解法擺在一起,哪一個該排在前面。

流程攤開就是:一百萬個樣本先全部拿去跑測試輸入,編譯不過或答案錯的直接丟掉,這一步砍掉大約 95%,剩下來的大約五萬個;接著聚成 cluster,只留下最大的十群;再用 scoring model 重新排序,每一群挑出分數最好的那一個代表。這十個代表,就是送出去的答案。

兩代擺在一起比:舊版要一百萬個樣本才有 25% 的解題率,新版在同樣一百萬個樣本上是 43%,接近兩倍;而且新版只要一百個樣本,就追平舊版一百萬個樣本的分數。對人類參賽者來說,整體落在第 85 百分位,大約是 Expert 到 Master 這個區間;如果只看它最好的兩個候選,可以勝過 99.5% 的參賽者。講者說,隔一年再推一次,只是換了一套系統,成績就走到這裡。

這裡真正留下來的一課是「用更少的樣本換到更好的成績」:底模更強、樣本更多樣、選擇更準,三個一起改才會有這個結果。代價也還在,實驗本身很貴,方法高度綁定程式這個領域,而且大部分算力花在生成那些根本編譯不過的樣本上。

九成五的樣本,死在編譯與基本測試那一關

95% 這個數字讓課堂上有人忍不住問,這樣不是貴到不合理嗎,有沒有更好的取樣方式,例如把 prompt 寫得更可靠,或改用別的做法。講者把問題丟回去,學生接了:作業一裡做過的 self-refinement 也許可以搬過來,先拿到一點回饋,再據此修正樣本,不必盲目生一百萬個。講者把兩條路分清楚:AlphaCode 這種做法比較像平行搜尋,生一大堆、靠 clustering 收斂;refinement 是在已經生出來的解上接回饋、把同一個解改好。問題是,在這個場景裡,回饋要從哪裡來。另一個可以砍成本的方向在訓練端:如果 RL 迴圈能讓模型本身更會解題,test-time 需要燒掉的算力就會少一些。

同一個問題也把討論帶到「這套系統到底要怎麼蓋」。講者把視角拉高一層:日常在用的 thinking model,背後其實也只有幾十個樣本;真正該問的是,你要怎麼蓋出那個能生出這幾十個樣本、還能把它們排隊評分的系統。用這個角度看,AlphaCode 2 已經很接近一個多模型系統:一邊是負責生成的家族,另一邊是負責評分的模型,中間靠 filtering、clustering 與評分把候選收斂到十個。如果把這樣一整套系統裡學到的東西蒸餾回單一模型,那顆模型就會變成大家熟悉的大型推理模型。講者把這個典範收成一句話:在 test-time compute 的世界裡,正確解應該存在於模型輸出的空間裡,所以你得有本事把它搜出來。自己重蓋一套系統的好處是,你手上的招比「當代模型剛好會什麼」多很多。

討論裡也順便解釋了為什麼 scoring model 不吃跟生成模型同一份資料。原因是汙染:評分模型不該看過完全一樣的題目,但它又必須看過同一個分布裡的東西,否則學不到怎麼排序。用不同階段、不同品質的資料集分頭微調,還得留意另一件事,後一個階段如果沒有把前一階段的資料混進來,模型會把前面學到的東西忘掉。

簡單題不必開到一百萬次取樣

講者丟了兩個問題給課堂:取樣策略該怎麼隨題目難度調整,以及推理要怎麼直接嵌進模型裡。

第一個問題的共識很清楚,難題才需要大規模取樣。有學生提議讓一個模型先把題目分成簡單、中等、困難,資料也帶著這些標籤,簡單題就從比較容易的池子裡多抓一點。講者把結論收在 sampling 這一側:如果題目簡單,模型本來就比較容易在搜尋空間裡撈到解,樣本數可以少一個量級,甚至靠幾輪迭代修正就能收斂,不必每次都開到一百萬。她也提醒,平行生大量樣本的目的,是逼出不同的解法路線,很少是在修同一個答案。

第二個問題把大家帶回訓練資料。有學生說,可以在資料裡加上提示,告訴模型這題用的是哪一類演算法;講者把它一般化成大家已經看過的東西。如果能把「寫程式之前先想一遍」的那段思考也放進訓練資料,那就是 chain of thought;如果反過來,先給模型答案,再要它把推理補出來,那是 STaR 那一類做法。遇到更複雜的題目,可以把它拆成子問題,為每個子問題加上提示,而不是要求模型一次解完。

有人追問,拆解之後如果第一步就錯了,後面不就跟著爛掉。講者的回答是,這正是為什麼最後一定要把迴圈關起來、而且要多步,最好還能像樹狀搜尋那樣,第一步先想幾個候選、確認大致正確,再往第二步走,走錯了還能回頭。這個方向還在早期。她也補了一個前提:任務拆解之所以行得通,是因為多數題目背後有共同的抽象模式;如果題目本身是分布外的,可能就得讓人留在迴圈裡,也就是前幾堂課談過的 AI scientist 那一類做法。

RAG 只在開頭檢索一次,複雜題目撐不住

課程後半段換到 deep research agent。Search-O1 的底層是大顆的推理模型,而推理模型有它天生的缺口:知識有 cutoff,昨天發生的事不在權重裡。講者說,還有一個更細微的症狀。推理模型遇到知識缺口的時候,不一定會停下來,而是會在推理鏈裡留下不確定性的字眼,例如 perhaps、alternatively,或是 wait。拿 GPQA 的推理鏈出來看,這種字出現的頻率很高。缺口不會自己消失,它會沿著整條推理鏈往下傳,把不確定性帶進最後的答案。

最直覺的補法是 RAG:把問題變成查詢,丟給搜尋工具,拿回一份文件塞進 prompt,再讓模型照著回答。講者用一個對比說明它為什麼不夠。如果問題只是問天氣,查一次就夠了;如果問題有好幾個部分,推理走到某一步才會發現需要新資訊,而你在一開始就把該查的查完,中間沒有再查的機會。RAG 相對直接推理當然有進步,但一進到 multi-step 的題目就會明顯吃虧。

讀懂文件再抄成筆記,才是 Search-O1 的重點

中間還有一種做法叫 agentic RAG。模型在推理過程裡遇到自己在意的實體,就插入特殊 token 觸發搜尋,檢索工具回來的文件直接插進推理鏈。這已經比開頭查一次好,但文件可以很長、又充滿雜訊,模型對長 context 的理解與推理就決定了上限;十份、二十份文件塞進去,它未必處理得來。

Search-O1 多做的是兩件事。第一個是讓模型一邊推理、一邊產生查詢,偵測到知識缺口就觸發一次搜尋,同一段推理裡可以來回好幾輪。第二個更要緊:拿到文件之後,它先讀過、判斷哪些段落跟當下的問題有關,抽出精華,再把這些筆記接回推理鏈。

講者用一題化學題走過三種做法。題目要算 product 3 的碳原子數,而 product 3 是一連串化學反應的產物。用一般推理模型,它看到不確定的名詞,就在腦內猜一個結構,然後照著猜出來的東西把答案算完,猜錯的話後面全部跟著錯。改成單純的 tool call 補文件,模型查完把文件塞進 prompt,一次塞進十份文件,資訊量超過它能好好推理的範圍,答案還是錯的,十個碳答成十四個。Search-O1 的做法是查完之後在文件裡推理,把真正相關的片段抓出來,再把整理過的資訊接回下一輪推理,答案就對了。

講者把這件事比成人類讀參考資料。你不需要把所有參考文獻都攤在桌上,你會做筆記,用自己的話把重點記下來,之後是這份筆記在幫你理解與綜合。Search-O1 裡負責讀文件的那個模組做的就是這件事:逐一檢查每份文件跟當前查詢、以及前面推理的關聯,抽出片段後附加到 prompt 上。它其實是在檢索工具內部又跑了另一個多步流程。

有人接著問,模型怎麼知道該搜什麼。講者把難點直接點出來:這才是整件事最難的地方。實務上你得先拆出問題裡的關鍵實體,針對那些實體去查,而不是把整段問題原封不動丟給搜尋工具;凡是你不打算信任權重裡那份知識的地方,就該交給 tool call。

課堂上有人問,如果前一段推理需要修正,這套機制怎麼處理。講者說這裡有個沒有寫得很明白的前提:系統裡得有一份狀態或記憶緩衝,記著目前要回答的查詢是什麼、前面推理走到哪裡。也有人問,能不能不改架構,直接在 prompt 裡叫模型把查到的東西做個總結再繼續推理。講者說這是好問題,建議在作業三直接試;她的保留意見是,這種做法等於假設模型對長 context 的理解與推理都很好,而這正是目前真正的限制,有一篇新工作叫 agentic context engineering 就在處理這件事。理想上當然希望模型能把長 context 吃得很乾淨,但那還不是現在。

文件越多,RAG 越差,Search-O1 反而越好

Search-O1 的成績攤開來看,橫軸是檢索回來的文件數量,縱軸是 pass@1,分成物理、化學、生物與整體四個面板。直接推理與 RAG 兩條線,文件變多,分數不會變好;Search-O1 的線會往上走,因為它會從每一份文件裡提煉相關資訊,文件越多,能抓到的線索越多。講者把這形容成另一條軸線:你可以靠迭代把同一個答案改好,也可以平行地多撈一些資料,再把資料削成塞得進 context 的大小。

在 GPQA 上,這條線畫出的是跟人類專家的距離。物理、化學、生物三欄都有專家的分數可以比;Search-O1 在物理與生物上已經很有競爭力,某些情況下甚至比人類專家高,化學明顯落後。講者提醒要看對角線,物理學家的分數要跟模型解物理題比,不能拿生物專家的分數去對。她也不忘踩剎車:這是「在這個等級的題目上有競爭力」,不是「已經超越人類專家」,化學還有很多空間。

有人提出一個假設:化學特別弱,會不會是因為摘要時很難保住化學意義,分子結構只要改一個地方,性質就整個變了。講者沒有下定論,只建議直接去 GPQA 上驗,並且補了另一個可能,每個科目的表現差異,也可能來自訓練資料本身不夠強,或題目落在分布外。

Multi-hop 問答是另一個戰場。這類題目要跨好幾份文件才能拼出答案,標準 RAG 做到某個水準就飽和,加上 agentic 的做法也會撞到天花板;在 HotpotQA 這類題庫上,表上被標成粗體的最好成績大多來自 Search-O1。講者也把進步歸給兩件事:不確定性下降,以及文件的品質與被提煉過的程度上升。多輪下來,模型能認出自己哪裡還不知道,再針對那裡補查,推理品質就上來了。她還給了一個可觀察的指標:推理鏈裡那些表達不確定的字,perhaps、wait、likely,數量在 Search-O1 的做法下大幅下降。

課堂上有人問檢索本身的 precision 與 recall。講者把責任歸屬講清楚:有可能真的沒檢索到對的文件,但這篇論文的主張是另一件事。即使找回來的只是好幾份「大致相關」的文件,一次全部塞進 context,等於要求模型同時對這麼多文件做推理,這才是它撐不住的地方。

最後一個沒解完的:模型不知道自己不知道

課程結束前,講者提到一篇沒時間講的 Search-R1,把它跟 Search-O1 放在一起對照:Search-O1 是用 prompting 的方式把迴圈關起來,Search-R1 則是用 RL 迴圈直接教模型怎麼去搜尋。差別在方法,不在目標。她請大家自己去找那篇論文看。

最後一個提問回到語言模型最根本的地方:模型輸出答案的時候,理論上帶著機率,這些機率跟答案對不對有沒有關係。講者先處理這個問題。把輸出 token 的 log probability 拿出來聚合,不能只是加總,起碼要做平均或幾何平均之類的處理;做出來會看到模型普遍過度自信。校正過去看,如果它只有五成把握是對的,它還是會表現得像有八成把握。這也解釋了為什麼你試著糾正模型,它有時候會很強硬地說是自己對,不太肯改。

沿著這條線,研究上有人在試的是讓模型用第二次 pass 估自己的信心,或直接用 RL、RLHF 把校正調好,讓它在沒把握的時候不要硬吐答案。不同模型的行為不一樣,外界看不到實驗室裡怎麼調,最後只剩下「這個模型在幻覺」這種籠統的說法。至於機率與正確性的相關性,講者的觀察是,答案是對的時候,模型大致會答對;答案不對的時候,它反而可能很篤定自己知道。幻覺與「模型知不知道自己在說什麼」,在她口中是一個還很活躍的研究領域,也是一個很適合拿來當專題的題目。

這堂課停在這裡:搜尋空間裡有解,選解的系統可以蓋得比模型本身更聰明,但模型對自己的無知,仍然沒有可靠的感覺。