用語言模型寫代碼、查數(shù)據(jù)庫、跑自動化流程這些事情大家早就習(xí)以為常了。Vibe Coding 到今年二月剛好滿一年,絕大多數(shù)人或多或少都在用它搞定代碼庫、寫文檔、處理各種雜活。但有一個問題始終是避免不了的:任務(wù)一多Agent 就開始丟三落四甚至開始一本正經(jīng)地胡說八道。
MCP 讓外部工具的接入變得很方便,Playwright、Supabase、Slack 這些都能掛上去,但代價是Context Rot [1]。簡單說就是輸入 Token 一多模型性能就會塌方式下降。
我們先看看上下文窗口里到底裝了些什么。
Claude的內(nèi)存結(jié)構(gòu)拆解
![]()
拿 Claude 舉例,它的上下文窗口大致是這么分配的:系統(tǒng)提示詞占 1.4%系統(tǒng)工具(包括 MCP 工具)占 8.3%,Agent 上下文(技能、工具描述、對話歷史)吃掉約 70%,用戶實際能用的提示詞空間反而很小。
Anthropic 的研究數(shù)據(jù)表示:真正用來放系統(tǒng)級指令的部分只有大約 10%,剩下全被對話歷史、工具輸出和各種中間結(jié)果給填滿了。一旦膨脹到 200K Token 的量級模型根本分不清什么才是重點。
模型健忘、幻覺頻發(fā)的根本原因就是 Context Rot。一個緩解思路是 Ralph Wiggum Loop,可以更合理地利用上下文窗口。
CodeAct
CodeAct[2] 是 2024 年的一篇論文,核心思想非常簡單:既然語言模型天然擅長寫代碼為什么不直接讓它用可執(zhí)行代碼來和外部世界交互?說白了就是用代碼當(dāng)動作空間。這個想法對任何寫過程序的人來說都不陌生。
舉個最簡單的例子,假設(shè)要做一個學(xué)生數(shù)據(jù)庫管理系統(tǒng),現(xiàn)在需要查 2025 年入學(xué)的所有學(xué)生。如果讓語言模型自己在上下文里逐條掃描記錄來"推理",那既慢又不靠譜。但換個思路直接寫一條 SQL:
SELECT *
FROM students
WHERE enrollment_year = 2025;
跑一下就完事了。把檢索的工作交給數(shù)據(jù)庫引擎,這才是正常開發(fā)者的做法。
CodeAct 的邏輯完全一樣,與其把海量數(shù)據(jù)塞進(jìn)上下文讓模型去"理解"(順便制造 Context Rot),不如讓模型寫一段代碼、執(zhí)行它、拿到結(jié)果。
![]()
![]()
Image from [2] — CodeAct in multi-turn interaction framework
回到剛才的學(xué)生數(shù)據(jù)庫場景,CodeAct 的工作流是這樣的:先接收用戶的自然語言查詢——
Find me the record of students who have been enrolled in the year 2025
然后通過一次 LM 調(diào)用理解意圖,生成 Python 代碼,在編譯器里跑一遍,檢查輸出。結(jié)果滿意就直接返回,不滿意就繼續(xù)迭代修正,直到拿到正確答案。
1、原子工具使用:CodeAct 匹配或超越 JSON/Text
第一組實驗測的是最基礎(chǔ)的場景:單個 API 調(diào)用。在 API-Bank 基準(zhǔn)上作者對比了文本格式調(diào)用、JSON 格式調(diào)用和 CodeAct(Python 函數(shù)調(diào)用)三種方案。
即便在這種完全用不上控制流優(yōu)勢的簡單場景下,CodeAct 在多數(shù)模型上的正確率都持平甚至更高。GPT-4、Claude 這些閉源模型在三種格式上都表現(xiàn)穩(wěn)定,但開源模型從 CodeAct 中獲益明顯更大。合理的解釋是:預(yù)訓(xùn)練階段見過大量代碼的模型,用代碼表達(dá)動作比用 JSON 更自然、更順手。
![]()
Image from [2] — Atomic API call correctness comparison.
2、復(fù)雜多工具任務(wù):CodeAct 實現(xiàn)更高成功率
真正拉開差距的是多工具組合場景。M3ToolEval 基準(zhǔn)包含 82 個人工精選的多工具任務(wù),CodeAct 在這里的優(yōu)勢就很明顯了——模型可以在一個代碼塊里組合多個工具、用循環(huán)和條件語句控制流程、存儲中間變量、跨步驟復(fù)用輸出。
數(shù)據(jù)上看,最佳模型的成功率絕對提升了 20.7%,交互輪次平均減少 2.1 輪。有意思的是,模型越強,從結(jié)構(gòu)化動作空間里獲得的收益就越大。
![]()
Image from [2] — Success rate comparison
![]()
Image from [2] — Full M3ToolEval results
3、多輪自調(diào)試
CodeAct 帶來了一個很有意思的能力:自調(diào)試。因為動作本身就是代碼,執(zhí)行出錯會產(chǎn)生 traceback,模型直接拿到結(jié)構(gòu)化的錯誤反饋,下一輪就可以針對性地修復(fù)。
論文里展示了一個典型案例:CodeActAgent 先用 Pandas 下載數(shù)據(jù),然后訓(xùn)練回歸模型、可視化系數(shù),中間碰到 matplotlib 報錯就自己修,發(fā)現(xiàn)缺失值就自己處理。整個過程不是簡單的工具調(diào)用,而是基于執(zhí)行反饋的迭代推理。
![]()
Image from [2] — Multi-turn interaction example.
4、微調(diào)后的 CodeActAgent 進(jìn)一步提升性能
作者構(gòu)建了 CodeActInstruct 數(shù)據(jù)集(約 7k 條多輪軌跡),在此基礎(chǔ)上微調(diào)出了 CodeActAgent。相比基礎(chǔ)的 LLaMA-2 和 Mistral 有大幅提升,在 MINT 任務(wù)上表現(xiàn)突出,跟更大規(guī)模的模型相比也有競爭力。
比如 CodeActAgent(Mistral-7B),在同等規(guī)模的開源模型里排在前列,通用 Agent 任務(wù)得分明顯提高,同時在 MMLU、GSM8K、HumanEval 等通用能力評測上也沒有退化。
![]()
Image from [2] — CodeActAgent evaluation.
從實驗數(shù)據(jù)整體來看,CodeAct 做到的不只是格式上的改進(jìn)。它實質(zhì)上重構(gòu)了 Agent 的動作空間——模型獲得了控制流、數(shù)據(jù)流、可復(fù)用變量和自動反饋循環(huán)。工具使用不再是一個接一個地調(diào) API,而是變成了可編程的推理過程。交互步驟更少,任務(wù)成功率更高,特別是在需要組合多個工具的場景下。
實現(xiàn)
我先試了 langchain-codeact[3] 這個包,但坑不少,而且只兼容 Anthropic 的模型,所以干脆自己擼了一個小原型。
實驗環(huán)境用的 Google Colab + OpenAI API。生產(chǎn)環(huán)境建議用隔離沙箱。
導(dǎo)入依賴
import os
import re
import io
import contextlib
from openai import OpenAI
from google.colab import userdata
os.environ["OPENAI_API_KEY"] = userdata.get("OPENAI_API_KEY")
client = OpenAI()
兩個工具函數(shù):一個從 LM 輸出里提取 Python 代碼塊,另一個用內(nèi)置的 exec() 執(zhí)行它。
def extract_python_code(text: str):
pattern = r"```python(.*?)```"
match = re.search(pattern, text, re.S | re.I)
return match.group(1).strip() if match else None
def run_python(code: str):
buf = io.StringIO()
try:
with contextlib.redirect_stdout(buf):
exec(code, {})
return buf.getvalue()
except Exception as e:
return f"Execution error: {e}"
CodeAct 循環(huán),這里用 GPT-5-mini 作為底座模型,因為需要推理能力。
SYSTEM_PROMPT = """
You are a CodeAct agent.
Always solve using Python code.
Return ONLY a python code block.
Do not explain in text.
"""
def codeact_run(user_problem, max_iters=3):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_problem}
]
for step in range(max_iters):
resp = client.chat.completions.create(
model="gpt-5-mini",
messages=messages
)
text = resp.choices[0].message.content
print("\n===== MODEL OUTPUT =====\n")
print(text)
code = extract_python_code(text)
if not code:
print("\n No code block — retrying\n")
messages.append({
"role": "user",
"content": "Return python code block only."
})
continue
print("\n EXECUTING PYTHON:\n")
print(code)
output = run_python(code)
print("\n PYTHON OUTPUT:\n")
print(output)
return output
print("Failed to obtain executable code.")
return None
整個流程就是:讓 LM 理解問題、寫 Python 代碼,extract_python_code() 提取代碼,run_python() 執(zhí)行,拿到結(jié)果。
problem = """
A batter hits a baseball at 45.847 m/s at 23.474 degrees.
Outfielder throws it back at 24.12 m/s at 39.12 degrees.
Find final distance from batter.
Assume no air resistance and g = 9.8.
"""
codeact_run(problem)
輸出:
===== MODEL OUTPUT =====
```python
import math
# Given values
v1 = 45.847 # m/s, initial hit speed
theta1_deg = 23.474 # degrees
v2 = 24.12 # m/s, outfielder throw speed
theta2_deg = 39.12 # degrees
g = 9.8 # m/s^2
# Convert to radians
theta1 = math.radians(theta1_deg)
theta2 = math.radians(theta2_deg)
# Range of the initial hit (from batter, to the right is positive)
R1 = (v1**2) * math.sin(2 * theta1) / g
# Outfielder throws back toward the batter: horizontal velocity is negative
vx2 = -v2 * math.cos(theta2)
vy2 = v2 * math.sin(theta2)
# Time of flight for the throw (starting and ending at ground level)
t2 = 2 * vy2 / g
# Horizontal displacement of the throw relative to outfielder's position
dx2 = vx2 * t2
# Final x-position relative to batter (batter at x=0)
x_final = R1 + dx2
# Distance from batter (absolute)
distance_from_batter = abs(x_final)
print(f"Initial landing distance from batter (R1): {R1:.3f} m")
print(f"Horizontal displacement from outfielder's throw (dx2): {dx2:.3f} m")
print(f"Final position relative to batter (x_final): {x_final:.3f} m")
print(f"Final distance from batter: {distance_from_batter:.3f} m")
```
EXECUTING PYTHON:
import math
# Given values
v1 = 45.847 # m/s, initial hit speed
theta1_deg = 23.474 # degrees
v2 = 24.12 # m/s, outfielder throw speed
theta2_deg = 39.12 # degrees
g = 9.8 # m/s^2
# Convert to radians
theta1 = math.radians(theta1_deg)
theta2 = math.radians(theta2_deg)
# Range of the initial hit (from batter, to the right is positive)
R1 = (v1**2) * math.sin(2 * theta1) / g
# Outfielder throws back toward the batter: horizontal velocity is negative
vx2 = -v2 * math.cos(theta2)
vy2 = v2 * math.sin(theta2)
# Time of flight for the throw (starting and ending at ground level)
t2 = 2 * vy2 / g
# Horizontal displacement of the throw relative to outfielder's position
dx2 = vx2 * t2
# Final x-position relative to batter (batter at x=0)
x_final = R1 + dx2
# Distance from batter (absolute)
distance_from_batter = abs(x_final)
print(f"Initial landing distance from batter (R1): {R1:.3f} m")
print(f"Horizontal displacement from outfielder's throw (dx2): {dx2:.3f} m")
print(f"Final position relative to batter (x_final): {x_final:.3f} m")
print(f"Final distance from batter: {distance_from_batter:.3f} m")
? PYTHON OUTPUT:
Initial landing distance from batter (R1): 156.731 m
Horizontal displacement from outfielder's throw (dx2): -58.119 m
Final position relative to batter (x_final): 98.612 m
Final distance from batter: 98.612 m
'Initial landing distance from batter (R1): 156.731 m\nHorizontal displacement from outfielder\'s throw (dx2): -58.119 m\nFinal position relative to batter (x_final): 98.612 m\nFinal distance from batter: 98.612 m\n'
到這里可以看到 CodeAct 是怎么讓模型動手干活的——寫代碼、執(zhí)行、拿結(jié)果,LLM 有了"編程的手",不再只是被動回答問題。
但還有一個問題沒解決。
模型能寫代碼了,可如果輸入本身就極其龐大呢?幾百頁的報告、幾個 G 的日志、整個代碼倉庫——單次前向傳播根本消化不了這么多信息。
那如果代碼不只是用來調(diào) API、查數(shù)據(jù)庫,而是用來組織模型自身的推理過程呢?
這就是遞歸語言模型(Recursive Language Models)要解決的事情。模型不再寫代碼去調(diào)用外部工具,而是寫代碼來調(diào)用自己——把大任務(wù)拆成小任務(wù),分別處理,最后把結(jié)果拼起來。
CodeAct 是代碼作為動作接口,RLM 是代碼作為推理控制器。
RLM 遞歸語言模型
![]()
RLM [4] 由 Alex Zhang 和 Omar Khattab 在 2025 年 10 月提出。他們在論文中明確表示受到了 CodeAct 的啟發(fā),但認(rèn)為 CodeAct 在面對超長上下文的推理任務(wù)時力不從心。
用偽代碼描述 RLM 的工作方式:
huge document
→ split into sections
→ model analyzes each section
→ model summarizes
→ model calls itself on summaries
→ final answer
論文給出的正式定義是:"一種通用推理策略,將長提示詞視為外部環(huán)境的一部分,允許 LLM 以編程方式檢查、分解并遞歸地在提示詞片段上調(diào)用自身。"
![]()
Image from [4]
上圖來自 RLM 論文 [4],展示的是如何處理一整本書這樣的超大上下文。RLM 不會把完整文本硬塞進(jìn)模型(塞不進(jìn)去),而是把提示詞當(dāng)作外部環(huán)境來對待。具體操作是:先把提示詞作為變量加載到 REPL 環(huán)境里,然后用代碼把它拆成可管理的小塊。根語言模型(depth = 0)通過代碼執(zhí)行檢查文本的不同部分,挑出跟任務(wù)相關(guān)的塊,對這些塊發(fā)起遞歸子調(diào)用(depth = 1)。每個子調(diào)用只看自己那一小段上下文,返回中間結(jié)果,最后由根模型把這些結(jié)果聚合起來生成最終響應(yīng)。這套機(jī)制繞過了上下文窗口的硬限制,讓模型可以通過結(jié)構(gòu)化分解和受控遞歸處理任意長度的輸入。
不同上下文長度下的性能表現(xiàn)
RLM 的核心主張是能把推理能力擴(kuò)展到標(biāo)準(zhǔn) LLM 固定上下文窗口之外。實驗數(shù)據(jù)很支持這個論斷。
在 S-NIAH(恒定復(fù)雜度)、OOLONG(線性復(fù)雜度)、OOLONG-Pairs(二次復(fù)雜度)三個基準(zhǔn)上,基礎(chǔ)語言模型的表現(xiàn)都是隨輸入長度增加快速崩塌的,任務(wù)復(fù)雜度越高崩得越厲害。RLM 則一路穩(wěn)住了——哪怕輸入規(guī)模到了百萬 Token 量級,遠(yuǎn)遠(yuǎn)超過底層模型的原生窗口大小,表現(xiàn)依然穩(wěn)健。
![]()
Image from [4] — the log-scale performance vs input length graph.
信息密集型推理任務(wù)的性能
差距最大的地方在信息密集型任務(wù)上。OOLONG 和 OOLONG-Pairs 要求模型聚合輸入的幾乎每個部分,做語義變換,根據(jù)成對關(guān)系構(gòu)建輸出——簡單說就是不能跳過任何信息。
在 OOLONG-Pairs 上,基礎(chǔ)模型的 F1 分?jǐn)?shù)接近零,根本處理不了長上下文下的密集關(guān)系推理。RLM 卻通過遞歸分解展示了涌現(xiàn)能力,GPT-5 的配置下拿到了 58% 的 F1。
這說明 RLM 做的不只是"看更多 Token"這么簡單。它改變的是推理本身的執(zhí)行方式。
![]()
Image from [4] — performance comparison table.
效率和成本分析
遞歸推理聽起來開銷應(yīng)該很大,但實際數(shù)據(jù)卻不是這樣:RLM 的平均 API 成本跟基礎(chǔ)模型差不多,有時候反而更低。
原因在于 RLM 不會獲得整個上下文。它通過代碼執(zhí)行和針對性的子調(diào)用只探測相關(guān)部分,避免了全量上下文的輸入,減少了無效 Token 的處理,計算資源只分配到真正需要的地方。雖然由于執(zhí)行軌跡的不同,RLM 的成本方差會大一些,但中位成本通常跟摘要壓縮之類的基線策略持平甚至更低。
好的推理性能靠的不是暴力堆算力,而是結(jié)構(gòu)化分解和選擇性上下文交互。
![]()
Image from [4]-cost distribution quartiles.
RLM 的核心優(yōu)勢不在于訪問了更多 Token,而在于徹底改變了推理的計算結(jié)構(gòu)。把提示詞外化到環(huán)境中、允許遞歸子調(diào)用,推理就從被動地消耗 Token 變成了主動的信息檢索和受控分解。Context Rot 被削弱了,推理時的信噪比上去了,任意長度的輸入都可以通過可擴(kuò)展的聚合來處理。本質(zhì)上RLM 把長上下文推理從內(nèi)存瓶頸問題轉(zhuǎn)化成了一個編程式的搜索問題。
實現(xiàn)
我這里用一本 Arthur Conan Doyle 的《福爾摩斯探案集》(txt 格式)[5] 當(dāng)輸入。Grammerly 統(tǒng)計下來單詞量超過 1M,字符數(shù)約 6.5M。
![]()
using Grammerly for word count
按 OpenAI[6] 的換算規(guī)則,常見英文文本中 1 個 Token 大約對應(yīng) 4 個字符,也就是約 0.75 個單詞。6.5M 字符對應(yīng)大約 1.625M Token。
![]()
Image from [7]
OpenAI 當(dāng)前最強模型的上下文窗口是 400,000 Token,我實驗用的 GPT-4.1-mini 是 1,047,576 Token。但實際推理時用戶拿不到全部窗口,系統(tǒng)提示詞、工具描述之類的要占掉一大塊,輸入輸出 Token 還得共享這個空間。就算假設(shè)能用滿 1,047,576 的窗口,1,625,000 Token 的輸入也放不進(jìn)去。
所以問題很明確:這么大的上下文,怎么用 RLM 來處理?
實驗任務(wù)是讓模型"提取前 20 個最頻繁出現(xiàn)的大寫實體,并總結(jié) 3 個主要主題"。
安裝依賴,用 pip 裝 rlms[8] 包:
!pip install -qU rlms
導(dǎo)入包,加載環(huán)境變量(Google Colab 的寫法略有不同):
import os
from rlm import RLM
from rlm.logger import RLMLogger
from google.colab import userdata #google colab
api_key = userdata.get('OPENAI_API_KEY')
加載文檔,創(chuàng)建 RLMLogger 存日志:
with open("big.txt", "r", encoding="utf-8") as f:
large_document = f.read()
print(f"Loaded document with {len(large_document)} characters.")
logger = RLMLogger(log_dir="./logs")
初始化 RLM environment 設(shè)成 local, Python 內(nèi)置的 exec() 來跑代碼。也可以換成 docker、prime-sandbox、modal、daytona 之類的外部服務(wù)。
max-depth 設(shè)成 1,跟論文里一致,調(diào)用結(jié)構(gòu)是這樣的:
Root LM
├── Sub LM call
├── Sub LM call
└── Sub LM call
如果 max-depth = 2 就多一層嵌套:
Root LM
├── Sub LM
│ ├── Sub-Sub LM
│ └── Sub-Sub LM
└── Sub LM
└── Sub-Sub LM
以此類推。
rlm = RLM(
backend="openai",
backend_kwargs={
"model_name": "gpt-4.1-mini", # You can upgrade to stronger model
"api_key": api_key,
},
environment="local",
environment_kwargs={},
max_depth=1, # Depth-1 recursion like in paper
logger=logger,
verbose=True,
)
prompt = f"""
You are analyzing a large enterprise document.
Return ONLY valid JSON.
TASK:
Extract the top 20 most frequent capitalized entities
and summarize 3 major themes.
Document:
{large_document}
"""
result = rlm.completion(prompt)
print("\n RLM Analysis Result:\n")
print(result)
結(jié)果出來了:
Loaded document with 6488666 characters.
╭─ ◆ RLM ━ Recursive Language Model ──────────────────────────────────────────────────────────────────────────────╮
│ │
│ Backend openai Environment local │
│ Model gpt-4.1-mini Max Iterations 30 │
│ Max Depth 1 │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
────────────────────────────────────────────────── Iteration 1 ──────────────────────────────────────────────────
Iterations 25
Total Time 397.59s
Input Tokens 2,026,303
Output Tokens 98,396
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
25 輪迭代,總耗時約 400 秒,處理了 2M 輸入 Token(含子 LM 調(diào)用)輸出約 98,000 Token。給了一個塞不進(jìn)任何上下文窗口的巨大輸入,模型還是給出了接近正確的結(jié)果。
![]()
Actual count vs RLM count
跟確定性正則匹配的計數(shù)比,RLM 的輸出有輕微偏差。這可以理解因為RLM 做的是跨 chunk 的遞歸語義實體提取,不是嚴(yán)格的詞法計數(shù)。聚合過程中會引入累計漂移,高頻實體和變體形式尤其容易受影響。但關(guān)鍵是模型準(zhǔn)確識別了語料庫的主要敘事主題。RLM 追求的是結(jié)構(gòu)化的語義理解而非 Token 粒度的精確計數(shù)。
DSPy 也把 RLM 集成進(jìn)了自己的包里,可以直接用 dspy.RLM [10]。
總結(jié):動作 vs 推理——選擇正確的范式
大語言模型已經(jīng)不只是文本生成器了。它們正在變成可編程的系統(tǒng)。CodeAct 和 RLM 是這條進(jìn)化路徑上兩個方向不同但可以組合的范式。這兩種方法都讓 LLM 的推理過程變得透明可觀察:中間步驟、執(zhí)行軌跡、分解結(jié)構(gòu)都暴露出來了,開發(fā)者不用再對著一個黑箱去猜模型在想什么。
CodeAct 把 LLM 變成了執(zhí)行引擎。模型不再只是給你一個文本答案,而是寫代碼、跑代碼、看結(jié)果,不滿意就再來一輪。適合的場景包括工具調(diào)用與 API 編排、數(shù)據(jù)庫查詢與數(shù)據(jù)處理、流程自動化,以及需要通過執(zhí)行來驗證正確性的結(jié)構(gòu)化問題求解。一句話概括:CodeAct 適合需要"動手做事"的任務(wù)。
RLM 走的是另一個方向。它不是讓模型去操作外部世界,而是讓模型用代碼來組織自己的推理過程——遞歸分解大輸入、通過受控子調(diào)用聚合結(jié)果。適用于超長文檔處理、多文檔推理、信息密集型的分析任務(wù)、跨大規(guī)模語料庫的結(jié)構(gòu)化聚合。RLM 解決的是推理規(guī)模的瓶頸。
CodeAct 是代碼作為動作接口,RLM 是代碼作為推理控制器。
兩者不是互斥的。在生產(chǎn)系統(tǒng)里完全可以組合使用——RLM 負(fù)責(zé)在海量上下文中完成推理,CodeAct 負(fù)責(zé)把決策執(zhí)行出去、跟外部系統(tǒng)交互。
這里真正的范式轉(zhuǎn)移是:與其一味地擴(kuò)大上下文窗口,不如去重構(gòu)計算本身。無論是 CodeAct 的執(zhí)行循環(huán)還是 RLM 的遞歸分解,LLM 系統(tǒng)的未來不在于能吃下多少 Token,而在于如何更聰明地控制推理和動作。
引用
https://avoid.overfit.cn/post/021ca9c0ed414fac82ab09532992b7df
by Shreyansh Jain
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.