这套东西没有需要"安装"的部分。它不是框架也不是库,就是一条 prompt 构造规则加一个字典合并函数。只需要一个能跑 chat completion 的模型。
export BIGMODEL_API_KEY=你的key
这次用智谱 BigModel 的 GLM 系列(glm-4.5-flash),因为 OpenRouter 上调 Gemini 被 provider ToS 403 拦了。
GLM 系列默认开推理,content 字段会是空字符串——全部内容跑到 reasoning_content 里去了。请求体里必须显式关掉,否则动作解析全部拿到空值:
{"thinking": {"type": "disabled"}}
免费档并发一高就 429,而失败调用如果返回空串,会被打分函数判成"答错"——最后拿到的是一张漂亮但完全错误的准确率表。第一次跑的时候我就吃了这个亏。处理方式:
_throttle = threading.Semaphore(2) # 全局限并发
_last = [0.0]
def _pace(min_gap=1.2): # 全局最小请求间隔
with _lock:
wait = _last[0] + min_gap - time.time()
if wait > 0: time.sleep(wait)
_last[0] = time.time()
# retries=12,指数退避;硬失败次数单独统计打印
跑完必须看日志末尾这一行,"0 hard failures" 不为 0,整组数据作废:
wrote results.jsonl (354 LLM calls, 0 hard failures)
对比的公平性全靠这一点:四个 runtime 拿到的 skill.instructions 完全一样,事件序列由同一个 seed 生成,唯一的自变量就是上下文管理方式。以下模板逐字来自论文 Appendix A(论文明确说 "exactly as they were dynamically constructed")。
Instructions:
{skill.instructions}
History:
Observation: {history[0].observation}
Reasoning & Action: {history[0].response}
[... 追加所有历史 observation 和 action ...]
Latest Observation: {observation}
Generate your next reasoning and action (format 'Action: <cmd>'):
把上面的 History: 换成 Summarized History: + Recent History:(只保留最近 3 轮)。
注意它同时有结构化 state 和完整历史,这是它和 SKILL.state 唯一的区别,也是论文要证的点:
Instructions:
{skill.instructions}
Current State:
{json.dumps(state, indent=2)}
History:
[... 依然追加所有历史 ...]
Latest Observation: {observation}
Update the state if necessary, provide reasoning, and output 'Action: <cmd>'.
To update state, use the format: StateUpdate: {"key": "value"}
Instructions:
{skill.instructions}
Skill Execution State:
```json
{json.dumps(state, separators=(',', ':'))}
```
Latest Observation: {observation}
Provide your response with:
1. Step-by-step reasoning (will be discarded after execution)
2. A JSON block fenced with ```json ... ``` containing both your State Patch and your Action.
The JSON block MUST have exactly these two keys:
{ "state_patch": { <dict: your state updates, set keys to null to delete> },
"action": "<string: the exact command you want to execute>" }
三个细节值得注意:
separators=(',', ':') 序列化,不缩进——省的就是这点空格。state_patch。state 更新按 Σ_t+1 = Σ_t ⊕ ΔΣ_t 进行,⊕ 是带 null 删除语义的字典合并:
def merge_patch(state, patch):
for k, v in (patch or {}).items():
if v is None:
state.pop(k, None)
elif isinstance(v, dict) and isinstance(state.get(k), dict):
merge_patch(state[k], v)
else:
state[k] = v
return state
Warehouse Management(论文 Appendix B.1 Environment 1):500 个货架,每架最多一件货。动作空间 Store / Ship / Move / Wait。事件有三类:
| 事件 | 正确动作 |
|---|---|
Shipment arrived containing item_N. | Store 到一个空货架 |
Customer ordered item_N. | 从当初放它的那个货架 Ship 出去 |
Maintenance required on shelf_M. | 把那架上的货 Move 到别的空架 |
打分 = 成功动作数 / 可动作事件数。往占用的货架上 Store、从错误的货架 Ship、Move 不存在的货,都算失败。
这个环境的巧妙之处在于:它逼着 agent 维护 500 个互不重叠的状态变量,而早期的 observation 会随着 horizon 增长滑出上下文窗口。不记住 item_12 放在 shelf_42,后面接到订单就必然出错。
模型 glm-4.5-flash,temperature 0.0 / top_p 1.0,seed 42,横轴是执行步数 T:
| T | Runtime | Score | 平均 prompt(字符) | 最大 prompt | prompt tokens | 总 tokens |
|---|---|---|---|---|---|---|
| 10 | Prompt (ReAct) | 0.70 | 3,555 | 6,147 | 8,841 | 10,016 |
| 10 | Memory (Summary) | 0.70 | 2,584 | 3,199 | 6,598 | 7,813 |
| 10 | Stateful (LangGraph) | 0.70 | 4,415 | 8,241 | 11,614 | 13,505 |
| 10 | SKILL.state | 0.70 | 1,582 | 1,639 | 3,780 | 7,566 |
| 25 | Prompt (ReAct) | 0.72 | 8,532 | 16,847 | 56,880 | 60,420 |
| 25 | Memory (Summary) | 0.68 | 2,953 | 4,346 | 19,700 | 23,140 |
| 25 | Stateful (LangGraph) | 0.72 | 9,071 | 16,971 | 65,869 | 70,110 |
| 25 | SKILL.state | 0.72 | 1,620 | 1,669 | 9,864 | 18,991 |
| 50 | Prompt (ReAct) | 0.60 | 20,000 | 42,836 | 264,466 | 274,112 |
| 50 | Memory (Summary) | 0.56 | 5,492 | 8,877 | 78,763 | 98,978 |
| 50 | Stateful (LangGraph) | 0.60 | 16,561 | 31,812 | 243,184 | 250,902 |
| 50 | SKILL.state | 0.60 | 1,620 | 1,657 | 19,699 | 38,377 |
SKILL.state 的平均 prompt 是 1,582 → 1,620 → 1,620 字符,最大值 1,639 → 1,669 → 1,657。步数翻了五倍,prompt 一动不动。同期 ReAct 的最大 prompt 从 6,147 涨到 42,836,Stateful 从 8,241 涨到 31,812。这不是"压缩得比较狠",是根本不积累。
| Horizon | vs ReAct | vs Memory | vs Stateful |
|---|---|---|---|
| T=10 | 1.32× | 1.03× | 1.78× |
| T=25 | 3.18× | 1.22× | 3.69× |
| T=50 | 7.14× | 2.58× | 6.54× |
T=10 上它几乎不省钱(对 Memory 只有 1.03×)。这是最容易被论文标题里的 16.2× 数字掩盖掉的一点:短任务上这套架构不值得引入。收益要到几十步以上才开始明显。
| T=50 | prompt tokens | completion tokens |
|---|---|---|
| Prompt (ReAct) | 264,466 | 9,646 |
| SKILL.state | 19,699 | 18,678 |
SKILL.state 的输出 token 是 ReAct 的近两倍——因为它每一步都要额外生成一份 state_patch JSON。而多数模型的 output 定价是 input 的 3–5 倍。按 4× 折算成"输入等价 token":ReAct ≈ 303k,SKILL.state ≈ 94k,仍然领先 3.2×,但不是表面上的 7.1×。报价的时候别只看 total tokens。
三个 horizon 上 SKILL.state 的得分都等于当轮最好的 baseline(0.70 / 0.72 / 0.60),一次都没超过。论文声称的是 "improves task accuracy",这一轮只复现出"不降低"。倾向于认为这是模型能力的差异:论文用 Gemini-3-Flash 在 T=100 拿到 0.94,而 glm-4.5-flash 在 T=50 已经掉到 0.60。基础能力不足以稳定执行时,上下文管理方式带来的边际收益会被噪声淹没。
记录了每一步 Σ_t 里 inventory 的条目数,整个 T=50 过程中它在 1 到 5 之间波动——因为 Ship 事件会把货从架上拿走,库存不是单调增长的。SKILL.state 的 O(1) 成立的前提是 state 本身有界。如果任务里状态只增不减(比如爬虫累积 URL、调查类 agent 累积证据),|Σ_t| 会随 t 增长,prompt 也就跟着涨,只是常数比全历史小而已。
论文 §5.7 给了开源模型的错误分布:Gemma-4-31B 在 T=100 上,68% 的错误是"更新 state 时误删了已有 key"。这轮 glm-4.5-flash 在 T=10/25/50 三个 horizon 上的非法 patch 数都是 0——85 步、每步一个 JSON 块,全部解析成功且结构合法。说明论文这条结论对模型代际比较敏感,但确定性校验层还是要写,代价接近零。
| 指标 | 论文(Gemini-3-Flash) | 本次(glm-4.5-flash) | 是否复现 |
|---|---|---|---|
| ReAct 平均 prompt @T=10 | 3,249 字符 | 3,555 字符 | ✓ 同量级 |
| SKILL.state prompt 保持恒定 | 1,775→1,811(T=10→200) | 1,582→1,620(T=10→50) | ✓ |
| 累计 token 削减 | 16.2×(T=100, vs Stateful) | 6.5×(T=50, vs Stateful) | ✓ 趋势一致,倍数随 horizon 增长 |
| 准确率优于 baseline | 0.94 vs 0.91 @T=100 | 持平,未超越 | ✗ |
| 小模型 68% 错误是 state 覆盖 | Gemma-4-31B | 0 次非法 patch | ✗ |
论文里没有复现的部分:噪声鲁棒性实验(Experiment 2)、状态恢复实验(Experiment 3)、公开 benchmark(InterCode CTF / τ-Bench)、以及 budget-matched 对照组(Truncated / LLMLingua)。其中 budget-matched 那组最值得单独验证——论文说把 ReAct 压到同样 1,800 token 预算时,滑动窗口掉到 0.18、LLMLingua 掉到 0.22,而 SKILL.state 是 0.94。如果这条成立,说明省 token 的功劳来自结构化而不仅仅是变短。
论文 §7 Limitations 给了三个明确的失效场景,这是全文最该先读的一节:
加上实测得到的两条:
反过来,最适合的场景是:长流程、状态可 schema 化、动作有明确成败反馈的执行类任务——论文选的仓库管理、Git 仓库操作、CTF、客服工单都是这个模子。
不需要换框架,四步:
不要按任务写。问自己:"下一步决策需要知道什么?"只留这些字段。论文的 CTF schema 只有 5 个字段。
删掉 History:,换成 Skill Execution State: + Latest Observation:。这是唯一必须改的地方。
每步输出 {"state_patch": ..., "action": ...},并在 prompt 里明说推理会被丢弃。这句话是有效的——模型知道推理不留存,才会主动往 state 里写。
校验不通过就丢弃补丁、重试当前步,绝不把畸形输出写进持久状态。schema 的所有权在代码这一侧,不在模型那一侧——这是整个架构安全性的来源。
| 现象 | 原因 | 修法 |
|---|---|---|
GLM 返回的 content 为空 | 默认开着 thinking,内容全跑到 reasoning_content | 请求体加 thinking: {"type": "disabled"} |
| 并发一高就 429,准确率表却"正常"跑完 | 失败调用返回空串被打分函数判成"答错",静默污染数据 | 全局限流 + 指数退避 + 单独统计硬失败次数,非 0 就整组数据作废 |
| 以为"已经有 state 了"就等于用上省 token | Stateful (LangGraph 式) 同时保留完整历史,T=50 实测烧了 250,902 token | 省钱的关键动作是删历史,不是加 state;对照组必须包含 Stateful,不能只对比裸 ReAct |
| 拿 16.2× 当自己能拿到的数字 | 那是论文 T=100、特定模型、特定环境下对特定 baseline 的比值 | 倍数是 horizon 的函数,T=10 上可能只有 1.3×,自己的任务要重新测 |
| 总 token 降了但账单没降多少 | SKILL.state 的输出 token 会翻倍,output 定价通常是 input 的 3–5 倍 | 报价时把 completion tokens 单独折算,别只看 total tokens |
| 跑长任务发现 prompt 还是涨了 | 状态本身单调增长(如累积证据列表),O(1) 假设不成立 | 先确认 |Σ_t| 有界,state 只增不减的任务这套架构帮不上忙 |