# 卓琪的开发笔记
> AI 应用开发工程师刘卓琪的技术博客。记录 Agent 产品开发、后端工程、上下文工程、RAG、工具调用与 AI 安全的实战与调研。
---
title: "买中转 API 之前先验货——六个问题判断这家卖的是不是真模型"
description: "便宜的 Claude、GPT、Seedance 中转站越来越多,分组名写着「官转」「官 key」「原生」,但分组名不是证据。真正能把这家卖的东西和官方对上号的,是响应 id 的前缀、和官方计数接口逐个对得上的 token、故意发错参数时的报错、视频文件里的 C2PA 签名和签名证书,以及它自己日志里的实扣。这篇按六个问题讲清每一项怎么查、要不要花钱、看到什么算有问题,没有官方 key 时怎么办,最后给出一个把整套流程交给 Agent 跑完、自动出对照报告的开源 skill。"
url: "https://zhuoqidev.com/posts/verify-llm-relay-api/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-10-24"
updated: "2026-10-24"
tags: ["API 中转","new-api","Claude","OpenAI","LLM","C2PA","Skills","实测"]
translation: "https://zhuoqidev.com/en/posts/verify-llm-relay-api/index.md"
---
# 买中转 API 之前先验货——六个问题判断这家卖的是不是真模型
> **TL;DR** 在过去一个多月测过的 30 多家中转里,最常见的问题不是「假模型」,而是模型是真的,但它不是你以为的那条渠道、多收了你看不见的 token、吞掉了你的参数。这些都能在付款前查出来:大部分检查不花钱,我做过的文本线路检测,官方加中转两边合计实花大多不到 1 美元,最多的一次约 1.3 美元。分组名不是证据,token 才是。
> **同一句 OK,多收了 508 个 token。**
>
> 让模型回一个词,先用官方计数接口数一遍输入,再看中转实际按多少计费:
> - 官方计数:18
> - 中转计费:526
> - 隐藏提示:+508
>
> *Claude 5 系列(如 Sonnet 5.5)· 官方计数 2026-10-14 · 订阅号池型中转 2026-10-19 实测(供应商匿名)*
*这篇写给准备买中转 API 的个人开发者和小团队,以及要给平台挑上游的人。案例来自我 2026 年 9—10 月在工作中测过的 30 多家中转,**一律匿名**:不点名、不放域名,只讲现象和怎么查。官方行为在 2026-10-23 对照官方文档核对过;中转的做法变得很快,具体数字以你测的那天为准。*
---
## 先搞清楚你买的是什么
中转站(relay,也叫代理、分销)做的事很简单:你用它发的 key 调一个和官方长得一样的接口,它再把请求转给某个「上游」,按自己的价目扣你的余额。市面上大部分中转跑的是开源的 [new-api](https://github.com/QuantumNous/new-api) / one-api,或者把订阅账号转成 API 的 [sub2api](https://github.com/Wei-Shaw/sub2api) 这一类程序,所以它们的行为有很多共性。(想自己搭一个转发网关、算算放在哪朵云上划算,可以看[上线一个应用要花多少钱](https://zhuoqidev.com/posts/model-gateway-cloudflare-vs-china-cloud/),那篇拿模型网关当完整例子。)
问题出在「上游」可能是好几种东西,而你只看得到中转给你的那一层:
```mermaid
flowchart LR
B[中转站
new-api / sub2api]
subgraph OK[正常生意]
C[官方 API
Anthropic / OpenAI]
D[云平台托管
Bedrock / Vertex / Azure]
end
subgraph RISK[要警惕]
E[订阅号池
Claude Max / ChatGPT]
F[自建开源权重
冒充官方]
end
B --> C
B --> D
B -.-> E
B -.-> F
```
「正常生意」是官方直连,或者模型厂商授权给云平台托管的同一个模型。「要警惕」的两种:订阅号池会给请求塞一段你看不见的系统提示并照收钱——我测过两家,一家每次多收 508 个 token,另一家没塞,所以每家都得实测;转卖消费者订阅还**通常**违反上游条款,号随时可能被封;自建开源权重干脆不是你买的那个模型。
先认几个词:
- **分组**:中转里同一个模型的不同货源,各有各的价,名字由中转随便起。
- **倍率**:分组价是官方挂牌价的几倍,new-api 里叫 `group_ratio`。
- **官方基准**:用官方 key、按官方文档拿到的同一请求的结果,所有对比都和它比。
- **计数接口**:官方「数一下这段输入有多少 token」的接口。Claude 的 `/v1/messages/count_tokens` 官方写明免费(有每分钟限流);OpenAI 的 `/v1/responses/input_tokens` 文档没写是否计费,我用余额为 0 的官方 key 照样能调,实测不扣费。
- **C2PA**:写在图片 / 视频文件里的内容凭证,带签名证书和数字签名。
> [!TIP]
> 整篇只有一个原则:**拿同一个请求,在官方和中转上各跑一遍,逐项对。** 下面六个问题都是这个原则的不同切面。
### 六项检查速查
按这个顺序做,前两项最便宜,也最能说明问题:
| # | 检查 | 要官方 key 吗 | 花不花钱 | 有问题的信号 |
|---|---|---|---|---|
| 1 | 看响应 id 前缀 | 不要 | 中转一次最便宜的请求 | 前缀和分组名宣称的渠道对不上 |
| 2 | 输入 token 对账 | 要(或用下面的公开基准数) | 官方计数免费;中转几厘钱 | 每次固定多出几百上千 |
| 3 | 逼出真实型号 | 不要 | 官方免费;经中转可能被预扣 | 报错里的型号和回包写的不一样 |
| 4 | 同一批错误参数两边发 | 要 | 经中转可能照常扣费,先单发 | 官方 400、中转 200 |
| 5 | 视频看签名证书 | 要(取官方原片的指纹) | 每档一单 | 证书指纹对不上官方 |
| 6 | 读它自己的日志 | 不要 | 不花钱 | 用量接口和日志对不上 |
> [!NOTE]
> **没有官方 key 怎么办。** 第 1、3、6 项本来就不需要。第 2 项可以用公开的基准数:同一句「Reply with the single word OK.」,Claude Sonnet 5.5 / Opus 5.5 在官方计数接口上是 18(2026-10-14 实测,换模型或隔太久要复测)。第 5 项,火山方舟 Seedance 官方原片的签名证书指纹,已收在下文 skill 的已知清单里。
---
## 问题一:后端到底是谁?
**看响应 id 的前缀。** 第一个成功的请求就能看出来:
| 后端 | 消息 id 前缀 | 报错里常见的痕迹 |
|---|---|---|
| Anthropic 一方 API | `msg_011C…`(请求 id `req_011C…`) | JSON 外壳带 `request_id` |
| AWS Bedrock | `msg_bdrk_…` | `ValidationException` |
| Google Vertex | `msg_vrtx_…` | Google 风格的错误外壳 |
| OpenAI Chat / Responses | `chatcmpl-…` / `resp_…` | `invalid_request_error` 等错误码 |
一个反直觉的事实:**分组名完全不能当证据。** 我见过名字暗示「官方直连 / 参数透传」的分组,实际全是 Bedrock;也见过名字里带 AWS 的分组,回来的却是一方 id。Bedrock 和一方都是真 Claude,但部分功能和计费细节不同(有些 beta 功能只在一方),你得知道自己买的是哪个。
> [!NOTE]
> id 前缀是观察到的规律,不是官方承诺的格式,厂商随时可能改。看到陌生前缀时,先用官方 key 发一单核对当前长什么样。想顺便发个错误参数看报错原文的话,经中转先单发一次、看日志有没有扣钱(见问题四)。
## 问题二:有没有偷塞系统提示?
**用官方计数接口对账。** 拿一句固定的话,比如「Reply with the single word OK.」,先发到官方的计数接口得到输入 token 数,再发到中转,看它回包 `usage` 里实际计费的输入是多少。Claude 要把 `input_tokens`、`cache_creation_input_tokens`、`cache_read_input_tokens` 三项加起来,因为有的中转会悄悄加缓存断点。
| | |
| --- | --- |
| 官方计数接口(2026-10-14) | 18 |
| 正常中转(一方或 Bedrock,多家实测) | 18 |
| 订阅号池中转(伪装成 Claude Code,2026-10-19) | 526 |
*同一句「Reply with the single word OK.」的输入 token,Claude 5 系列。供应商匿名。*
多出来的数如果**每次都一样**,就是被注入了固定的系统提示。上面那家是把 Claude 订阅账号转成 API 卖,为了冒充 Claude Code 客户端,它给每个请求加了一段 Claude Code 的系统提示,508 个 token 按你的价照收。OpenAI 这边也有同类:走 ChatGPT / Codex 订阅后端的中转,我测到每个请求带着约 4,380 个 token 的 Codex 指令,照样计费。
两个容易踩的坑:
- **别拿中转自己的计数接口比。** 订阅号池的计数接口不注入,数出来和官方一模一样,真实请求才多出来。只比计数接口会误判它是干净的。
- **差几个 token 不一定是注入。** Anthropic 文档本身说计数接口是估算、可能有小偏差;固定多 4 个左右、又念不出任何隐藏内容,多半是聊天模板差异。差几百上千、而且固定,才是注入。要确认,就让最便宜的模型「把你前面的 system 文本原样放进代码块」,往往能直接念出来。
## 问题三:是不是换了便宜的模型?
**逼上游自己报出型号名。** 只看 token 数,同一代的几个型号数出来一样,分不出是谁。对 Claude 有个好用的办法:发一个离谱的 `max_tokens: 9999999`,上游会回 400,报错原文里写着这个模型最多允许多少,**模型名就在报错里**:
> `max_tokens: 9999999 > 128000, which is the maximum allowed number of output tokens for` **`claude-xxxx`**
>
> (报错格式来自实测,型号已脱敏。)
我见过一个分组,回包里 `model` 字段照写你请求的贵模型,报错里露出来的却是另一个型号,价格按贵的收。这种情况下 token 数也许对得上,只有这个报错说了实话。
> [!WARNING]
> 这招**只在直连官方 Claude API 时免费**(400 不扣费)。经过中转时,有的会按 `max_tokens × 单价` 预扣、上游报错也不退,有的会把 400 换个渠道重试。先发一次,去它的日志里看扣没扣钱,再决定要不要对每个型号都发。Kimi、Gemini 和 OpenAI 的 Responses 接口对超大 `max_tokens` 不报错,会真的生成并扣费,这招对它们无效。
## 问题四:你的参数还在吗?
**两边发同一批错误输入。** 很多中转不是更宽容,而是把它不认识或不想处理的字段直接丢了。以 Claude 为例:
| 请求里写的 | 官方 | 我见过的中转 |
|---|---|---|
| `temperature: 9`(Claude 合法范围 0–1) | 400,说明范围 | 200,值被丢 |
| `inference_geo: "us"`(只在美国境内推理,合法) | 照做,回包显示美国 | 字段被剥掉,回包仍是全球推理 |
| `temperature: 0.5`(合法) | 照常 | 照常 |
第二行最危险:你以为的合规要求根本没生效,而且没有任何报错。把非法参数、缺必填头、错 key、不存在的模型、不存在的任务各发一次,状态码和报错原文两边并排放,和官方不一样的地方按「影响用户 / 影响接入 / 不影响使用」分三档。
> [!CAUTION]
> 错误参数探针经中转**不一定免费**:见过后端把非法值照单全收、正常生成并扣费,也见过官方拒绝后中转把请求改投别的池子、回 200 照收。先单发一次,看日志,再批量发。
## 问题五:视频是不是官方原片?
视频线路多一层问题:成片可能是官方生成后转存的,也可能是用开源权重自己跑出来的。按证据硬度一步步往下查:
```mermaid
flowchart TB
%% layout: tree
S(["拿到一条成片"])
S --> Q1["① 有 C2PA,签名有效、哈希匹配?"]
Q1 --> N1["否:不是原样的官方文件
(wan3 官方原片本来就没有 C2PA,直接看 ③)"]
Q1 --> Q2["是 → ② 签名证书指纹 = 你从官方 API 拿到的原片?"]
Q2 --> N2["否:签名可能是自签的,不能当原片证据"]
Q2 --> Q3["是 → ③ 编码器、元数据和官方原片一致?"]
Q3 --> N3["否:被重新封装或重新编码过"]
Q3 --> Q4["是 → ④ 成片链接、任务号、用量是官方的?"]
Q4 --> N4["否:原片,但被转存或封装
没法按官方方式对账和接入"]
Q4 --> OK["是:官方原片,原样透传"]
classDef q fill:#fff3bf,stroke:#f08c00
classDef bad fill:#ffe3e3,stroke:#e03131
classDef pick fill:#d3f9d8,stroke:#2f9e44
class S,Q1,Q2,Q3,Q4 q
class N1,N2,N3,N4 bad
class OK pick
```
最容易漏的是第二步:**签名有效不等于是官方签的。** 任何人都能自签一张证书,把签发方写成「Byteplus Pte. Ltd.」,签名照样有效。要把签名证书的指纹和你自己从官方 API 拿到的原片比,一致才算官方原片。第三步看编码器版本、码流里的编码器信息,以及有没有 AIGC 标签——这要**和官方原片一致**:阿里 wan3 原片带 AIGC 标签,Seedance 原片没有,Seedance 成片多出这个标签反而说明被重新封装过。自建管线还常在元数据里留下整份 ComfyUI 工作流,模型、量化、采样步数都写在里面。
## 问题六:实际扣了多少?
**看它自己的日志,不看用量接口。** new-api 系中转的 `/v1/dashboard/billing/usage` 用量接口经常不准(我见过一单 0.29 美元读成 2 美元);每次请求的真实扣费在它的日志接口里(`/api/log/token`,key 放在 `Authorization: Bearer` 头里,别用 `?key=` 的写法,否则 key 会进 URL 和各层日志)。
视频还要**每个档位都实测,不线性外推**。我见过低档位只要官方价的零头、1080P 却接近官方价,按低档推算会低估好几倍;也见过价目表写着支持 2K,提交时先扣了钱,过了一会儿才回「只支持 768P 以下」再退款。每个付费请求之前先算「单价 × 条数」,从最便宜的档位一档一档往上测。
---
## 结果怎么读
| 你看到的 | 说明什么 | 怎么办 |
|---|---|---|
| id 前缀和分组名对不上 | 渠道不是它宣称的那条 | 按实际渠道比价,问清是否混用 |
| 输入 token 固定多几百上千 | 注入了系统提示(多半是订阅号池) | 按实际成本算;别用于要稳定的生产 |
| 报错里的型号和回包写的不一样 | 换了模型 | 不接 |
| 非法参数回 200 | 字段被吞 | 列进要对方修的清单;查合法字段是否也被丢 |
| Seedance 签名证书对不上官方 | 不是官方原片,或转存后重编码 | 按我的标准不接(偏严,转存可接受的业务可以放宽) |
| 用量接口和日志对不上 | 计费口径不可靠 | 只认日志,每档实测 |
稳定性和容量不在验货里回答:几十次请求测不出来,接入之后看线上监控和 30 天成功率更可靠。
---
## 让 Agent 把这套跑完
上面每一步都不难,难的是每家都要一项不漏地做一遍、记下原始请求和响应、最后整理成别人看得懂的报告。我把自己在工作里用的检测规范脱敏后开源成了一个 Agent skill:**[llm-relay-audit](https://github.com/YouToco/llm-relay-audit)**。把中转的地址和 key 交给 Claude Code、Codex、Cursor 这类 Agent,它按固定流程先跑免费探针,**任何可能花钱的请求都先报价、等你点头**,最后输出两份报告。
```bash
# 一行安装
npx skills add YouToco/llm-relay-audit
```
外发证据版只写「这家 vs 官方」两方,第一页是一句话结论和四张结论卡,可以直接发给供应商让它修:
。")
内部决策卡一页纸,给拍板的人看接不接、和现有线路比贵还是便宜:
。")
它在流程之外还做了几件人容易做错的事:所有请求和响应落盘时 key 自动打码;可能已经送达的付费请求**绝不自动重发**(避免一张视频买三遍);外发报告里出现内部线路名或像 key 的字符串,生成器直接拒绝输出;视频的 C2PA 检查比对签名证书指纹,而不是只看签名有效。项目的设计和工程细节在[作品页](https://zhuoqidev.com/projects/llm-relay-audit/)。
---
## 局限与来源
- **实测**:508(两家订阅号池里注入的那家)、约 4,380、0.29 美元读成 2 美元、每次检测的花费、OpenAI 计数接口不扣费(余额 0 的 key 可调)、各种吞参数和换模型的现象,都来自我 2026 年 9—10 月对 30 多家中转的接入测试,供应商不公开。
- **观察到的规律,不是官方承诺**:id 前缀(尤其 `msg_011C`)、订阅号池注入的具体 token 数、报错文案格式,厂商和中转都可能改。
- **推断**:转卖消费者订阅通常违反上游条款,是按各家条款的一般规定推断的,具体以你所用服务的条款为准。
- **官方文档**(2026-10-23 查):[Anthropic token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting) · [OpenAI input token counts](https://developers.openai.com/api/reference/resources/responses/subresources/input_tokens) · [C2PA 规范](https://c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html)
- **工具**:[new-api](https://github.com/QuantumNous/new-api) · [sub2api](https://github.com/Wei-Shaw/sub2api) · [llm-relay-audit](https://github.com/YouToco/llm-relay-audit)
---
**今天就能做的最小三步:先看 id 前缀,再对 token,再用报错逼出型号。**
**分组名不是证据,token 才是。先对账,再付款。**
附录:三条最常用的检查命令(第 1 条免费,第 2 条几厘钱,第 3 条经中转先单发看日志)
```bash
# 1. 官方基准:用官方 key 数输入 token(免费;Sonnet 5.5 对这句话是 18,2026-10-14)
curl https://api.anthropic.com/v1/messages/count_tokens \
-H "x-api-key: $OFFICIAL_KEY" -H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"claude-sonnet-5-5","messages":[{"role":"user","content":"Reply with the single word OK."}]}'
```
```bash
# 2. 中转实际计费:同一句话发到中转,原始响应存下来,看 usage 和 id 前缀
curl $RELAY_BASE/v1/messages \
-H "x-api-key: $RELAY_KEY" -H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"claude-sonnet-5-5","max_tokens":16,"messages":[{"role":"user","content":"Reply with the single word OK."}]}' \
| tee relay-ok.json
```
```bash
# 3. 逼出真实型号:官方上 400 不扣费;经中转先发一次,再去它的日志看有没有扣钱
curl $RELAY_BASE/v1/messages \
-H "x-api-key: $RELAY_KEY" -H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"claude-sonnet-5-5","max_tokens":9999999,"messages":[{"role":"user","content":"hi"}]}' \
| tee relay-max-tokens.json
```
模型名按你要测的型号换。有的中转只认 `Authorization: Bearer`,不认 `x-api-key`,换一下即可。
---
title: "Mac 到底适不适合跑大模型?——苹果和英伟达的宣传页上,都没写的那几个数"
description: "苹果说 Mac mini「快速、私密的本机推理」、提示处理快 13.5 倍;英伟达说笔记本 RTX 5090 有 1,824 AI TOPS。小字里的对比对象、测试配置,和真正决定你用起来快不快的三个数——内存带宽、读提示词的算力、GPU 实际能用的内存——却都没摆在显眼处。这篇用 llama.cpp 社区几十台机器的统一跑分、两台手头机器的核验,再按苹果脚注的原测法在 16 GB 基础款上复现一遍,再摆出它在写代码、编译、跑测试上实测赢下的部分,分场景回答 Mac mini、Mac Studio 和各档 N 卡到底适合干什么。"
url: "https://zhuoqidev.com/posts/mac-for-local-llms/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-10-18"
updated: "2026-10-18"
tags: ["本地大模型","硬件选型","Apple Silicon","NVIDIA","llama.cpp","内存带宽","实测"]
translation: "https://zhuoqidev.com/en/posts/mac-for-local-llms/index.md"
---
# Mac 到底适不适合跑大模型?——苹果和英伟达的宣传页上,都没写的那几个数
> **TL;DR** 能跑,和跑得好用,中间隔着几个宣传页上不写的数。出字速度看内存带宽,读长文看算力,能跑多大看 GPU 实际能用的内存;而两家的宣传,一个拿新 Mac 和五年前的 M1 比,一个让台式和笔记本共用「5090」这个名字。结论要分开说:Mac mini 拿来写代码、编译、跑测试、给云端模型的 Agent 当工作机,实测比同时期的游戏本快一倍多,很合适;拿来当本地大模型主力就不合适;Mac Studio 是「装得下大模型」最省事的办法,但读长文慢;N 卡在显存装得下的范围内最快,装不下就一落千丈。
> **能跑,不等于跑得好。**
>
> 一台 16 GB 的 Mac mini,按苹果自己脚注里的测法跑 14B 模型:
> - 8K 提示 · 首字:15 秒
> - 16K 提示 · 首字:35 秒
> - 32K 提示:崩溃
>
> *Qwen3-14B 4 bit · mlx-lm 0.32 · 2026-10-18 实测*
*这篇写给想买一台机器在本地跑大模型、正在看宣传页做决定的人。数据主要来自 llama.cpp 社区的统一跑分,我用手上的 Mac mini(M6,16 GB)和 RTX 3060 笔记本跑同一条命令核对过,又按苹果脚注的原测法在 16 GB 基础款上复现了一遍。怎么跑的、完整数字在文末[附录](#附录跑法和完整数字)。*
---
## 宣传页上写了什么,小字里又写了什么
先把两家的宣传和脚注并排放:
| 宣传页上写的 | 小字里的条件 | 没摆在显眼处的 |
|---|---|---|
| Mac mini:「快速、私密的本机推理」,M6 的 LLM 提示处理「最高快 13.5 倍」 | 对比的是 **2020 年的 M1**;M6 测试机配 **32 GB** 内存;测的是 14B 模型、8K 提示的首字时间 | 没和任何显卡比;16 GB 起售款的带宽是 153 GB/s,页面写的是「最高 170」 |
| Mac Studio:「在本机运行前沿级模型,不需要云端 token」 | M5 Ultra 最多 512 GB,只有顶配能选 | 读长提示词的速度只有同档 N 卡的三分之一 |
| 笔记本 RTX 5090:1,824 AI TOPS,「在本地运行最新 AI 模型」 | 显存 24 GB、带宽 896 GB/s | 台式 RTX 5090 是 32 GB、1,792 GB/s,同一个名字,带宽差一倍 |
| 台式 RTX 5090:3,352 AI TOPS | — | 产品页上没写带宽;32 GB 装不下的模型,再多 TOPS 也跑不了 |
两家都没有说假话,但都把「算力」放在最显眼的位置。问题是,**跑大模型时你最常等的那件事——一个字一个字往外吐——几乎不看算力**。
---
## 跑模型,其实在比三件事
### 一、出字快不快:看内存带宽
模型生成文字是一个 token 一个 token 往外吐的。每吐一个,GPU 都要把**全部权重**从内存读一遍,和刚算出的一点状态做乘法。乘法很快,读很慢。所以单人出字速度有个很粗但好用的上限:
> **出字速度 ≈ 内存带宽 ÷ 模型占的字节数**
llama.cpp 社区有两个统一跑分帖([苹果芯片](https://github.com/ggml-org/llama.cpp/discussions/4167)、[N 卡](https://github.com/ggml-org/llama.cpp/discussions/15013)),大家用同一个模型、同一条命令贴结果。我先用手上两台跑同一条命令核对:RTX 3060 笔记本 75.2 对社区 3060 台式机 76.9,Mac mini M6 35.0 对社区 Mac mini M6 35.7,只差 2–4%,数据可信。把各台机器按带宽排开:
| 内存带宽(GB/s) | N 卡 | 苹果芯片 |
| --- | --- | --- |
| 153 | | 35 |
| 170 | | 36 |
| 288 | 64 | |
| 307 | | 66 |
| 336 | 75 | |
| 360 | 77 | |
| 614 | | 120 |
| 896 | 158 | |
| 936 | 162 | |
| 1,008 | 189 | |
| 1,228 | | 179 |
| 1,792 | 300 | |
- Mac mini M6
- M5 Max
- M5 Ultra
- 笔记本 5090
- 4090
- 台式 5090
*Llama 2 7B 4 bit,llama.cpp 单人出字速度。数据取自 llama.cpp 社区跑分帖,Mac mini M6(16 GB)和 3060 笔记本两点为我 2026-10-17 的核验实测。带宽为厂商标称值(M5 Ultra 苹果只写「1.2 TB/s」,按 M5 Max 的两倍取 1,228)。社区各条软件版本不同,只看量级。*
几乎是一条直线。每台机器都只用上了标称带宽的 56–88%,但排名完全由带宽决定:M5 Ultra 的带宽比 4090 高两成,出字差不多;台式 5090 带宽是笔记本 5090 的两倍,出字也快了将近一倍。**同样叫 5090,两条线上差了一大截。**
### 二、读长文快不快:看算力
模型读你给它的那一大段文字(预填充),是几千个 token 一起算,这时瓶颈才轮到算力。差距一下子拉开:
| | |
| --- | --- |
| 台式 RTX 5090(32 GB) | 14,970 |
| RTX 4090(24 GB) | 14,771 |
| 笔记本 RTX 5090(24 GB) | 7,642 |
| M5 Ultra(最多 512 GB) | 4,945 |
| M5 Max(最多 128 GB) | 3,220 |
| RTX 3060 笔记本(6 GB) | 2,311 |
| Mac mini M6(16 GB) | 1,016 |
*读提示词速度(token/秒),Llama 2 7B 4 bit、512 token 提示。社区跑分帖;3060 笔记本与 Mac mini 为我的核验实测。*
出字差不多的 4090 和 M5 Ultra,读提示词差了三倍。短对话感觉不到;可一旦你要它读一个代码仓库、一份几十页的合同,或者让它当 Agent 一轮轮调工具——每一轮都要把越来越长的上下文重读一遍——这三倍就是三倍的等待。
### 三、能跑多大:看 GPU 实际能用的内存
带宽决定快慢,**容量决定有没有**。模型装不进去,再快也是零。这一项两家的宣传都写了总数,没写「实际能用多少」:
- **Mac 的统一内存,GPU 用不满。** 我那台 16 GB 的 Mac mini,llama.cpp 启动时读到系统给 GPU 的上限是 12.7 GB,约 79%,而且还要和系统、浏览器、编辑器共用。
- **N 卡的显存是硬上限。** 超出的部分只能放回 CPU 内存,速度会掉一个数量级(下面有实测)。消费级最大的台式 5090 是 32 GB。
> [!NOTE]
> 一个粗算法:4 bit 量化的模型,每 10 亿参数约占 0.6 GB,再给上下文(KV 缓存)和运行时留两三成。照这个算,GPU 能用 12 GB 左右只够 14B 级,24–32 GB 养得下 30B 级,128 GB 养得下 100–150B 级,512 GB 才碰得到 600B 级的 MoE 大模型。这是估算,具体以模型和量化格式为准。
---
## 按苹果脚注的测法,在基础款上跑一遍
苹果的「快 13.5 倍」用的是:14B 模型、4 bit 量化、8K token 提示,测首字时间,测试机 32 GB。我在 16 GB 的 Mac mini 上用同样的条件跑了一遍(MLX,mlx-lm),另外加测 16K 和 32K——Agent 干活时,上下文到几万 token 很常见。同一个模型也拿到 6 GB 显存的 3060 笔记本上试了试。
| | |
| --- | --- |
| 2K 提示(Mac mini) | 3.4 秒 |
| 8K 提示(Mac mini(苹果脚注的条件)) | 15.4 秒 |
| 16K 提示(Mac mini) | 34.6 秒 |
| 8K 提示(3060 笔记本,一半层放 GPU) | 21.0 秒 |
*Qwen3-14B 4 bit,从发出提示到第一个字的等待时间(秒)。Mac mini 用 mlx-lm 0.32,3060 笔记本用 llama.cpp b11696。2026-10-18 实测,每组跑两轮取平均。32K 提示在 Mac mini 上跑不起来,没有画。*
- **8K 提示,第一个字要等 15 秒**,之后每秒 15 个 token。日常问答还能接受,但这是苹果自己选的测试条件。
- **16K 要等 35 秒**,出字降到每秒 13 个;GPU 内存用到 11.5 GB,离系统给的 12.7 GB 上限只差一点。
- **32K 直接崩溃**:Metal 报「GPU 内存不足」,程序退出。14B 模型加 32K 上下文,在 16 GB 基础款上根本跑不起来——而 Agent 读代码仓库,上下文到这个量级很平常。
- **3060 笔记本连模型都放不进显存**:14B 要 8.2 GB,6 GB 显存只能放一半,剩下的留在 CPU 上算,出字掉到每秒 4.8 个;全放 CPU 只有 2.5 个。同一张卡跑放得下的 7B,出字是 75。**N 卡的墙不是慢,是一过线就塌。**
> [!TIP]
> 还有一件宣传页不会告诉你的事:**MLX 的版本很要紧**。我第一次用系统自带的 Python 3.9 装 mlx-lm,只装上了旧版 MLX(0.29),同样 8K 提示首字要等 55 秒——慢了 3.6 倍,因为旧版用不上 M6 GPU 里的神经网络加速器。换 Python 3.13 装上 0.32 才是上面的数。用 LM Studio、Ollama 也一样,先确认它带的 MLX 够新。
---
## Mac mini 不擅长跑模型,但很擅长另一件事
说完短板,也得公平地说它的长处。同一台 16 GB 的 Mac mini,和那台 RTX 3060 笔记本(CPU 是 i7-12700H,2022 年的 14 核游戏本)跑同一份脚本,测的是我每天真会干的活。真实项目用的是一个 TypeScript 网关仓库,5,139 个测试:
| | |
| --- | --- |
| 跑全量测试(65 秒 对 136 秒) | 2.1× |
| TypeScript 类型检查(3.3 秒 对 7.9 秒) | 2.4× |
| 前端构建(31 秒 对 73 秒) | 2.4× |
| 单核 Python 混合负载(0.05 秒 对 0.16 秒) | 3.1× |
| CPU 内存拷贝带宽(114 对 16 GB/s) | 7.2× |
| numpy 单精度矩阵乘(1,988 对 434 GFLOPS) | 4.6× |
| 起子进程(每秒 717 对 2,119 个) | 3.0× |
| 两万个小文件增删(1.63 秒 对 0.74 秒) | 2.2× |
| git grep 整个仓库(428 对 175 毫秒) | 2.4× |
| 训练小 CNN(两边各用最快框架) | 2.7× |
| 4B 模型单人出字(56 对 96 token/秒) | 1.7× |
- 打平: 1.0×
*快多少倍(快的一方 ÷ 慢的一方)。2026-10-15 至 10-17 实测,每项多轮取稳定值。完整数字见附录。*
- **写代码、跑测试、编译,Mac mini 快一倍多。** 单核快约 3 倍,CPU 搬内存快 7 倍;SQLite 逐条提交更是快了 16 倍(0.15 秒对 2.4 秒)。
- **它慢的是系统调用多的小动作**:起子进程、增删小文件、`git grep` 慢两到三倍。但都是毫秒级——起一个子进程 1.4 毫秒对 0.5 毫秒,人感觉不到。连续满载 120 秒后,它会掉约 5% 的速度,笔记本不掉。
- **所以苹果(英文版)宣传页上说可以借助 Claude Code、Codex 这类 Agent 提升效率,这一句是成立的**:这类 Agent 的模型跑在云端,本机干的正是编译、跑测试、读写文件、调 git,这些 Mac mini 做得又快又稳。办公、写文档就更不在话下。
**还有一个宣传页上没吹、但实测很硬的长处:省电,也安静。** 我用 macOS 自带的功耗遥测(不用 root,每秒采样,取累加平均)测了整机功耗,又用 `nvidia-smi` 读了 3060 笔记本显卡的功耗:
| | |
| --- | --- |
| Mac mini 空闲(苹果官方数据) | 4 W |
| Mac mini CPU 12 核跑满(实测 60 秒平均) | 21 W |
| Mac mini 跑 14B 模型(实测,出字 14.6 token/秒) | 30 W |
| Mac mini 官方上限(苹果官方数据) | 70 W |
| 3060 显卡空闲(实测 30–37 W) | 33 W |
| 3060 显卡跑 14B 模型(实测,一半放显卡,出字 4.5 token/秒) | 86 W |
*功耗(瓦)。Mac mini 为系统功耗遥测(PowerTelemetryData,板上功耗加电源损耗,不是插座电表),空闲与上限取苹果官方插座实测数据;3060 只读得到显卡本身,不含 CPU 和屏幕。2026-10-18 实测。*
Mac mini 把 14B 整个放进内存跑,整机不到 30 W;3060 笔记本光是显卡空闲就有 30 W 出头,跑同一个模型要 86 W,还因为装不下只出得了三分之一的字。跑满的时候 Mac mini 也几乎听不到风扇——这是我的主观感受,没有测分贝。一台 24 小时开着、给 Agent 当工作机的电脑,这一点很实在。
问题只出在「把模型也搬到这台机器上跑」。
## 苹果没写在显眼处的
1. **带宽跟着配置走,宣传写「最高」。** Mac mini M6 页面写「最高可达 170 GB/s」,16 GB 起售款是 153;Mac Studio 的 M5 Max 页面写 614 GB/s,那是 40 核 GPU 版,32 核起步版只有 460。按上面那条直线,带宽少一成,出字就慢一成。
2. **GPU 用不满统一内存。** 16 GB 实际给 GPU 约 12.7 GB,再扣掉你正在用的程序。
3. **读长文是短板。** 苹果所有「快 X 倍」都是和老款 Mac 比的;和同价位的 N 卡比,读提示词慢两到三倍。16 GB 基础款跑 14B,8K 提示要等 15 秒,32K 直接跑不起来——而 Agent、读代码仓库、长文档,正是「本地 AI」最常见的用法。
4. **软件版本决定能不能用上新硬件。** 旧版 MLX 用不上 M6 的神经网络加速器,读提示词慢 3.6 倍。
5. **它真正的长处是容量。** Mac Studio 最多 128 GB / 512 GB 统一内存,单台就能装下 N 卡要好几张才装得下的模型。这一点宣传没有夸大。
## 英伟达没写在显眼处的
1. **显存是硬上限。** 宣传页的 AI TOPS 再高,模型装不进显存,速度会断崖式下跌。
2. **笔记本和台式同名不同命。** 笔记本 5090:24 GB、896 GB/s;台式 5090:32 GB、1,792 GB/s。3060 也一样:笔记本版 6 GB,台式版 12 GB。买笔记本时要看的是显存和带宽,不是型号。
3. **AI TOPS 管的是读提示词那一下。** 出字速度看带宽,台式 5090 的产品页上恰恰没写带宽。
4. **功耗和噪音。** 台式 5090 一张卡 575 W,要配 1,000 W 电源;Mac Studio 整机满载也远低于这个数。
---
## 所以,Mac 到底适不适合跑大模型
| 你想做什么 | Mac 合不合适 | 更合适的选择 |
|---|---|---|
| 用 Claude Code、Codex 这类 Agent 写代码(模型在云端)、编译、跑测试、办公 | **很合适**:实测编译、跑测试比 i7-12700H 游戏本快一倍多 | — |
| 用苹果智能、偶尔跑 4B–8B 小模型 | **合适**,Mac mini 就够 | — |
| 本地跑 14B 级模型当日常助手,或接 Agent 读代码仓库 | **不太合适**:16 GB 基础款 8K 提示等 15 秒、32K 跑不起来;32 GB 版能装下,但读长文仍慢 | 16 GB 以上显存的台式 N 卡 |
| 30B 级模型,自己用,要快 | **一般**:要 Mac Studio 才装得下,出字和 4090 同档,读长文慢 | 24–32 GB 的台式 N 卡(4090、5090) |
| 100B 级以上,能装下优先,慢一点可以 | **最合适**:Mac Studio 128 GB / 512 GB 单台就能装下,N 卡要好几张 | — |
| 多人同时用、要读得快、长上下文 | **不合适** | 多张专业卡,或者直接买 API |
国行参考价(2026-10-18 apple.com.cn):Mac mini M6 16 GB 6,999 元起;Mac Studio M5 Max 19,999 元起;M5 Ultra 46,999 元起。
一句话:**Mac mini 是一台很好用的开发机和 Agent 工作机,不是一台好用的本地大模型机。** 宣传页把这两件事写在了一起——「创造你自己的 AI 智能体」和在本机跑大语言模型——前一件实测成立,后一件在 16 GB 基础款上只能算「能跑,体验一般」。想在本地装下真正的大模型,要看的是 Mac Studio 那一档的大内存;想跑得快,还是 N 卡。
---
## 再往上:电和国境
如果你还想继续往上加——一台机房里的 8 卡服务器、一张最新的数据中心卡——会撞上两个和性能无关的天花板:
- **电**:英伟达 DGX Rubin NVL8 一台约 24 kW,要直触芯片的液冷;家里普通插座是 10 A(约 2.2 kW),接空调的 16 A 插座也只有约 3.5 kW。
- **国境**:Blackwell、Rubin 两代数据中心卡对华禁运,截至 2026-10 没有放行的消息;完整版 RTX PRO 6000(96 GB)也在管制线以上,中国卖的是特供的 RTX PRO 6000D(据拆解 84 GB、核心少约 17%)。
再往上的,就只能租卡或者买 token 了。
---
## 局限
- 社区跑分只测 7B 模型、单人、512 token 短提示,只覆盖 llama.cpp;各条结果软件版本不同,新版本本身会快一到三成,只看量级。
- 日常开发那组对比的是 2022 年的 i7-12700H 游戏本,不是同价位的新 PC;测试时笔记本上开着更多程序(快速重跑过一次,单核多核结果几乎一样)。npm 安装依赖的耗时受网络和缓存影响,没有计入。
- 功耗:Mac mini 用的是系统自带的功耗遥测,不是插座电表;3060 笔记本只读得到显卡,CPU 功耗要 root 才能读,没测。
- 我的复现只有 Mac mini 16 GB 一台,Mac 上只用了 mlx-lm 一个引擎,苹果原测用的是 LM Studio;32 GB 版、Mac Studio 没有实测。14B 的数字用的是 Qwen3-14B,和苹果脚注里的模型未必相同。
- 价格、出口管制都是 2026-10-18 查到的状态,变化很快;6000D 的规格来自拆解和跑分,不是官方数据。
---
## 来源
- 宣传页与脚注:苹果 [Mac mini](https://www.apple.com/mac-mini/)([中文版](https://www.apple.com.cn/mac-mini/),脚注 6 为「13.5 倍」的测试条件)、[Mac Studio](https://www.apple.com/mac-studio/);英伟达 [RTX 5090](https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/)、[RTX 50 系笔记本](https://www.nvidia.com/en-us/geforce/laptops/50-series/)。均为 2026-10-18 读取。
- 跑分:llama.cpp 社区 [苹果芯片帖 #4167](https://github.com/ggml-org/llama.cpp/discussions/4167)、[N 卡帖 #15013](https://github.com/ggml-org/llama.cpp/discussions/15013)(2026-10-17 抓取原文)。
- 规格:[英伟达显卡对比页](https://www.nvidia.com/en-us/geforce/graphics-cards/compare/)、[3060 笔记本版(Notebookcheck)](https://www.notebookcheck.net/NVIDIA-GeForce-RTX-3060-Mobile-GPU-Benchmarks-and-Specs.497453.0.html);苹果 [Mac mini 规格](https://www.apple.com/mac-mini/specs/)、[Mac Studio 规格](https://www.apple.com/mac-studio/specs/);国行价格取自 [apple.com.cn](https://www.apple.com.cn/shop/buy-mac/mac-studio)。
- 功耗:苹果 [Mac mini 功耗与散热数据](https://support.apple.com/103253)(M6:空闲 4 W、最高 70 W,插座端测量);[DGX Rubin NVL8](https://www.nvidia.com/en-us/data-center/dgx-rubin-nvl8/);国标 [GB 1002-2024](https://www.chinesestandard.net/PDF/English.aspx/GB1002-2024)。
- 出口管制:[Tom's Hardware 关于 6000D](https://www.tomshardware.com/pc-components/gpus/nvidia-rtx-pro-6000d-b40-blackwell-gpus-reportedly-set-to-supersede-banned-h20-accelerators-in-china)、[PCGH 拆解](https://www.pcgameshardware.de/Grafikkarten-Grafikkarte-97980/News/Nvidia-RTX-6000D-mit-84-GiByte-GDDR7-1487562/)。
---
## 附录:跑法和完整数字
核验与复现的跑法
```bash
# 社区统一跑分的同一条命令(llama.cpp b11696 官方预编译包)
llama-bench -m llama-2-7b.Q4_0.gguf -ngl 99 -fa 0,1 -p 512 -n 128
# 苹果脚注 6 的条件:14B、4 bit、8K 提示,测首字时间(另测 16K、32K)
mlx_lm.benchmark --model mlx-community/Qwen3-14B-4bit -p 8192 -g 128 -n 2
```
两台核验机器的完整数字
| 项目 | RTX 3060 笔记本(6 GB) | Mac mini M6(16 GB) |
|---|---|---|
| Llama 2 7B 4 bit 出字 / 读提示词(token/秒) | 75.2 / 2,311 | 35.0 / 1,016 |
| 跑全量测试(5,139 个,4 分片) | 136 秒 | 65 秒 |
| TypeScript 类型检查 / 前端构建 | 7.9 秒 / 72.5 秒 | 3.3 秒 / 30.8 秒 |
| 单核 Python 混合负载 / 全核 | 0.157 秒 / 2.67 秒 | 0.051 秒 / 0.44 秒 |
| CPU 内存拷贝 / numpy 单精度矩阵乘 | 15.9 GB/s / 434 GFLOPS | 114 GB/s / 1,988 GFLOPS |
| SQLite 1,000 次逐条提交 | 2.4 秒 | 0.15 秒 |
| 起子进程 / 两万小文件增删 / git grep | 2,119 个/秒 / 0.74 秒 / 175 毫秒 | 717 个/秒 / 1.63 秒 / 428 毫秒 |
| 120 秒满载末段 ÷ 首段 | 1.005 | 0.954 |
| 功耗:空闲 / CPU 跑满 / 跑 14B 模型 | 显卡 30–37 W / 没测 / 显卡 86 W(最高 90 W) | 4 W(官方)/ 21 W / 30 W(整机遥测) |
| Qwen3-4B 4 bit 单人出字(最快引擎) | 96 | 56(MLX) |
| Qwen3-4B 4 bit 8 路同时(引擎直测) | 296 | 256(llama.cpp) |
| GPU 能用的内存 | 6 GB 显存 | 约 12.7 GB(系统上限) |
| Qwen3-14B 4 bit,8K 提示:首字 / 出字 | 21.0 秒 / 4.8 token/秒(只放得下一半,20 / 40 层) | 15.4 秒 / 14.8 token/秒(mlx-lm 0.32) |
| Qwen3-14B 4 bit,16K / 32K 提示首字 | 没测 | 34.6 秒 / GPU 内存不足,崩溃 |
---
title: "上线一个应用,每月要花多少钱?——服务器、Serverless、边缘函数怎么选,先学会拆三笔账"
description: "写给想把第一个应用放上线、却不知道该买服务器还是用 Serverless 的人。先讲清云厂商的货架上卖的几样东西,再教你把自己的应用拆成三笔账(时间花在等还是算、每个请求搬多少字节、要不要一笔一笔记账),给个人网站、小程序后端、AI 应用、图片处理工具四种常见应用对号入座,查清各家免费额度和新人价;最后拿一个模型网关从零搭一遍,用 aliyun CLI、tccli、各家公开价格接口和官方价目页实查 2026-10-09 七家云的价格,把每月 1,000 万、1 亿、10 亿次请求的账单算到每一项。"
url: "https://zhuoqidev.com/posts/model-gateway-cloudflare-vs-china-cloud/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-10-09"
updated: "2026-10-09"
tags: ["云服务选型","Serverless","云服务器","Cloudflare Workers","阿里云","腾讯云","成本测算","模型网关","架构选型"]
translation: "https://zhuoqidev.com/en/posts/model-gateway-cloudflare-vs-china-cloud/index.md"
---
# 上线一个应用,每月要花多少钱?——服务器、Serverless、边缘函数怎么选,先学会拆三笔账
> **TL;DR** 第一次上线,别急着买服务器。先问三件事:你的应用时间花在等还是算,每个请求搬多少字节,要不要一笔一笔记账。起步阶段,大多数应用靠免费额度或者一年一两百块就够了;真正花钱是在量大以后,而且钱花在哪一笔,就看你怎么回答这三个问题。
*这篇写给想把第一个应用放上线的人。前半篇讲怎么选、怎么估钱;后半篇拿一个模型网关从零搭一遍,把七家云的账单算到每一项。价格是 2026-10-09 查到的,怎么查的、完整的数字表都在文末[附录](#附录怎么查的和完整数字)。[^scope]*
---
## 先说结论(给赶时间的人)
1. **起步先用免费额度。** 个人网站放 Cloudflare Pages 或 GitHub Pages 不花钱;Cloudflare Workers 每天 10 万次请求以内免费;国内要一台服务器加一个数据库的话,阿里云各有 99 元一年的活动。多数应用起步时,每月几块到几十块就够了。
2. **别为「等」付钱。** 如果你的应用大部分时间在等别人(调大模型、调第三方接口),别用按运行时长计费的 Serverless,等多久它就收多久。
3. **量大以后,看你搬多少字节、记几笔账。** 这时候 CPU 反而是最便宜的一项。
4. **用户在中国大陆,就用国内云,并且先做 ICP 备案。**
后面每一句,都会讲清楚为什么。
---
## 这篇写给谁
- 你手上有一个想放上线的应用:个人网站、小程序或 App 的后端、一个调大模型的 AI 应用,或者一个处理图片视频的小工具。你没买过服务器,也说不太清 Serverless 和服务器有什么区别。
- 读完前半篇,你能看懂云厂商卖的几样东西,把自己的应用拆成三笔账,挑一个起步方案,估出每月大概多少钱,并且知道什么时候该换。
- 后半篇写给想看细节的人:拿一个模型网关做完整例子,假设你知道 LLM API 怎么调用、流式输出是一段一段推回来的(SSE)。看不懂可以先跳过,不影响前半篇的结论。
- 本文**不讨论**模型推理本身(GPU、推理引擎),合规只在影响选型的地方点到为止。
### 术语表
| 术语 | 一句话解释 |
|---|---|
| 请求 | 用户打开一个页面、点一下按钮,浏览器或 App 向你的服务发出的一次「问话」 |
| 云服务器 | 云厂商租给你的一台一直开着的电脑(阿里云叫 ECS,腾讯云叫 CVM,AWS 叫 EC2) |
| Serverless | 你只上传代码,有请求时平台临时替你开机跑一下,没请求不收机器钱(函数计算、云函数、Lambda、Cloud Run) |
| 边缘函数 | 代码放在全球很多机房里就近运行,Cloudflare Workers 最典型 |
| 挂钟时间 | 一个请求从进来到结束经过的真实时间,包括等别人的时间 |
| CPU 时间 | 程序真正在算的时间,等网络、等数据库的时间不算 |
| 出流量 | 从云里发出去的数据,比如网页、图片、接口的回复;大多数云按 GB 收钱 |
| 记账 / 状态 | 需要记下来、下次还要读的数据,比如用户余额、调用次数、登录状态 |
| ICP 备案 | 网站用中国大陆的服务器或节点对外服务前,必须完成的工信部登记 |
---
## 云厂商的货架上卖什么
第一次打开云厂商的官网,几十个产品看得人头晕。其实放代码的地方只有三种,外加一类替你管好的零件:
| 货架上的东西 | 是什么 | 像什么 | 怎么收钱 |
|---|---|---|---|
| 云服务器 | 租一台一直开着的电脑,系统、软件、安全更新都自己管 | 整租一套房 | 按月或按年付,空着也付 |
| 按时长计费的 Serverless | 只传代码,有请求时平台替你开机跑 | 住酒店,按晚付 | 从请求进来到结束,每一秒都算钱,等别人的时间也算 |
| 边缘函数(Cloudflare Workers) | 代码放在全球机房,就近运行 | 只按「真正干活的时间」计费的钟点工 | 按代码真正在算的毫秒收钱,等待不收 |
| 托管服务 | 数据库、文件存储、CDN 这些别人替你管好的零件 | 拎包入住 | 按规格或按用量收钱 |
这篇文章里有五个角色会反复出场,先认识一下:
```comic cols=2
> 一个普通的请求
[用户] 帮我问问大模型:明天要带伞吗?
[你的服务] 收到,我这就转给大模型。
---
> 二十秒后
[大模型] 明天……有……小雨……
[你的服务] 我只忙了 10 毫秒,剩下 20 秒都在等它一个字一个字说完。
---
[云厂商!] 那么问题来了:我该按什么收你的钱?
[你] 这正是这篇文章要算清楚的。
```
漫画里的「你的服务」,就是你放在云上的那段程序。它真正干活只要 10 毫秒,可每个请求都要等大模型说上 20 秒。云厂商收它的钱有三种办法,账单能差出好几倍:
```comic
> 按 CPU 时间收(Cloudflare Workers)
[云厂商] 你的服务真正干活的那 10 毫秒我才收钱,等的时候不算。
---
> 按运行时长收(Lambda、函数计算)
[云厂商] 从请求进来到结束,20 秒,一秒不落。
[你的服务] 可我大部分时间在发呆啊!
---
> 按机器收(云服务器)
[云厂商] 你租了几台机器,就付几台的钱,闲着也照付。
```
除了放代码,还有两笔钱别漏了:**出流量**(东西从云里发出去,大多数云按 GB 收钱,Cloudflare 不收)和**记账**(数据库、缓存,有的按台收,有的按读写次数收)。
---
## 学会拆:把你的应用拆成三笔账
```comic cols=2
> 第一次上线
[你] 我想上线一个小程序,该买服务器还是用 Serverless?
[云厂商] 先别急着掏钱。你的应用整天都在干嘛?
---
[云厂商] 在等别人、在自己算,还是在搬东西?要不要一笔一笔记账?
[你!] 原来得先把它拆开看。
```
拆账只需要回答三个问题,外加一个约束。每个问题都有办法自己查出答案:
**1. 时间花在等,还是花在算?**
看你的接口在干嘛:
- 要调大模型、调第三方接口(支付、地图、短信),或者查很慢的数据库,一次要等好几秒 → **等待型**。
- 要处理图片和视频、渲染 PDF、压缩、跑本地模型 → **计算型**。
- 读写一下数据库就返回,几十毫秒完事 → **轻量型**,等和算都不多。
**2. 每个请求搬多少字节?**
用浏览器打开你的应用,按 F12 打开开发者工具,切到「网络(Network)」面板,点一个请求就能看到它的大小。图片和视频看文件大小;AI 应用看 prompt 和回答有多长,一个汉字大约 3 个字节。几 KB 算轻,几百 KB 以上算重。
**3. 要不要一笔一笔记账?**
每个请求要不要读写数据库?要不要实时扣余额、数次数、限流?要的话,记账就是你的一笔固定开销,而各家收这笔钱的方式差别很大。
**外加一个约束:用户在哪?** 主要在中国大陆,就得用国内云,而且网站对外服务前要先做 ICP 备案;Cloudflare 这类境外平台在大陆没有节点,访问会慢。主要在海外,选择就多了。
拆完以后,再估一下量:**每月请求数 ≈ 日活用户 × 每人每天的请求数 × 30**。比如 1,000 个日活,每人每天点 50 下,就是每月 150 万次请求。
有了这几个数,月费大致是四项相加:
| | 云服务器 | 按时长计费的 Serverless | 边缘函数(Cloudflare) |
|---|---|---|---|
| 保底费(没人用也要付) | **高**:机器、数据库一直开着 | 低 | 低(Workers 付费版每月 $5) |
| 等待和计算 | 几乎不另收钱,但机器要按高峰买够 | **按挂钟时间收**,等待也算 | 只收计算的毫秒 |
| 出流量 | 按 GB 收(有的套餐不限流量) | 按 GB 收 | 不收 |
| 记账 | 自己的数据库,按台收 | 外接数据库,按台或按量收 | **按读写次数收** |
哪一列便宜,就看你的应用在哪一行花得多。
---
## 对号入座:四种常见应用怎么起步
先看四种应用怎么起步:
| 应用 | 拆出来的三笔账 | 用户主要在国内 | 用户主要在海外 |
|---|---|---|---|
| 个人网站、博客、作品集 | 几乎不算;行李看图片多少;不记账 | 对象存储加 CDN,按流量付钱,要备案 | Cloudflare Pages 或 GitHub Pages,免费 |
| 小程序、App 后端 | 轻量;行李轻;要记账(用户数据) | 一台小服务器加一个数据库;微信小程序也可以用腾讯云开发 | Cloudflare Workers 加 D1,免费额度起步 |
| 调大模型的 AI 应用 | 等;行李中等;要记账(用量、余额) | 一台常驻的小服务器,别用按时长计费的函数计算 | Cloudflare Workers,免费额度起步 |
| 图片、视频处理工具 | 算;行李重;不记账 | 偶发用函数计算,持续跑满用服务器;文件放对象存储 | 偶发用 Lambda 或 Cloud Run,持续用服务器;文件放 R2(出流量免费) |
起步阶段最该先用的,是各家的免费额度和新人价:[^free]
| 平台 | 免费额度或起步价 | 要注意 |
|---|---|---|
| Cloudflare Pages | 免费,静态请求不限量,每月 500 次构建 | |
| GitHub Pages | 免费,站点 1 GB 以内,每月流量软上限 100 GB | 不能用来做网上生意、电商或 SaaS |
| Vercel Hobby | 免费,每月 100 GB 流量 | 只限非商业的个人使用 |
| Cloudflare Workers | 每天 10 万次请求免费;付费版每月 $5,含 1,000 万次请求 | 免费版每次只给 10 毫秒 CPU |
| Cloudflare D1 / KV / R2 | D1 每天读 500 万行、写 10 万行;KV 每天读 10 万次、写 1,000 次;R2 每月 10 GB | R2 出流量免费 |
| 阿里云 ECS 99 | 2 核 2G、3M 带宽不限流量,99 元/年 | 每人同时只能有 1 台;续费同价,活动到 2029-03-31 |
| 阿里云 RDS 99 | MySQL 基础版,99 元/年 | 能和 ECS 99 同时买;活动到 2027-03-31;基础版是单节点 |
| 阿里云轻量服务器 | 官网写「低至 68 元 1 年」 | 页面没写规格和条件;活动机续费按正常价 |
| 阿里云函数计算 | 新用户每月 15 万 CU,连续 3 个月 | 没有长期免费额度 |
| 腾讯云轻量服务器 | 4 核 4G 3M,新用户首年 109 元 | 同一实名只能买一次,活动到 2026-12-30,续费按刊例价 |
| 腾讯云开发(小程序) | 免费体验版每月 3,000 资源点;个人版 19.9 元/月(限时) | 小程序一发布,免费环境 15 天后到期 |
| AWS | 新账户先给 $100,用指定服务最多再给 $100;Lambda 每月 100 万次请求长期免费 | 免费计划最长 6 个月,到期或额度用完就关闭账户 |
| Google Cloud | 新客户 $300,90 天内用完;Cloud Run 每月 200 万次请求免费;一台 e2-micro 长期免费 | e2-micro 只限美国三个地域 |
| Oracle Cloud | 长期免费:ARM 机每月约 2 核 12 GB、2 台小 AMD 机、每月 10 TB 出流量 | 闲置 7 天可能被回收 |
什么时候该换?看三笔账里哪一笔先涨上来:请求多了、等得久了,按时长计费的该换成常驻服务器或边缘函数;流量大了,看出流量单价(Cloudflare、Oracle 便宜);记账多了,Cloudflare 按次收费会越来越贵,自己的数据库按台收钱。后半篇的模型网关例子,就是把这三笔账从每月 1,000 万次一路算到 10 亿次。
[^free]: 都是 2026-10-09 在官方页面核对的:[Cloudflare Workers](https://developers.cloudflare.com/workers/platform/pricing/)、[Pages](https://developers.cloudflare.com/pages/platform/limits/)、[D1](https://developers.cloudflare.com/d1/platform/pricing/)、[R2](https://developers.cloudflare.com/r2/pricing/)、[GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/github-pages-limits)、[Vercel Hobby](https://vercel.com/docs/plans/hobby)、[阿里云 ECS 99](https://www.aliyun.com/daily-act/ecs/99program)、[RDS 99](https://www.aliyun.com/activity/database/bestoffers)、[阿里云轻量](https://www.aliyun.com/product/swas)、[函数计算试用](https://help.aliyun.com/zh/functioncompute/fc/product-overview/trial-quota-1)、[腾讯云轻量](https://cloud.tencent.com/act/pro/lighthouse)、[腾讯云开发](https://tcb.cloud.tencent.com/pricing)、[AWS Free Tier](https://aws.amazon.com/free/)、[Lambda](https://aws.amazon.com/lambda/pricing/)、[Google Cloud](https://docs.cloud.google.com/free/docs/free-cloud-features)、[Oracle Always Free](https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm)。活动价随时会变,阿里云、腾讯云轻量服务器的规格和续费价以购买页为准;RDS 99 的规格取自组合购页的规则说明。
还有几样常见的服务也顺手归一下类:
| 服务 | 时间花在 | 每个请求的字节 | 记账 | 适合放哪 |
|---|---|---|---|---|
| 个人网站、博客 | 几乎不算 | 看图片多少 | 不用 | 静态托管加 CDN |
| 小程序、App 后端 | 轻量 | 轻 | 要(用户数据) | 一台小服务器加数据库,或者托管后端 |
| 调大模型的 AI 应用 | 等 | 中 | 要(用量、余额) | 边缘函数或常驻进程,别用按时长计费的 Serverless |
| 实时推送、聊天(SSE、WebSocket) | 等(长连接) | 轻 | 在线状态、房间 | Workers + DO(WebSocket 休眠期间不收时长费),或常驻进程 + Redis |
| 慢接口代理、BFF、Webhook 中转 | 等 | 轻 | 没有或很少 | 边缘函数最省;量大以后常驻进程也划算 |
| 图片、PDF、视频处理 | 算 | 重 | 不用 | 偶发用按时长计费的 Serverless,持续用机器 |
| 文件和媒体下载 | 几乎不算 | 很重 | 不用 | 对象存储加 CDN;海外用户可以用出流量免费的 Cloudflare R2 |
| 高频短请求(鉴权、计数、限流服务) | 很少 | 轻 | 每个请求都要读,要求毫秒级 | 常驻进程加内存缓存(Unkey 从 Workers 迁走就是这个原因,见后文) |
*表:按三个问题给常见服务归类(本文的判断,不是官方建议)。*
**算得多的应用,比的是每 CPU 小时多少钱:**
| | |
| --- | --- |
| 腾讯云 CVM(SA9 包月) | ¥0.10 |
| Oracle(E6.Flex 按需) | ¥0.13 |
| 阿里云 ECS(c9i 包月) | ¥0.14 |
| Google Cloud(c4 按需) | ¥0.29 |
| AWS EC2(c8i 按需) | ¥0.32 |
| Azure(F4als_v7 按需) | ¥0.41 |
| Cloudflare Workers(按 CPU 毫秒) | ¥0.48 |
| AWS Lambda(Arm,1,769 MB ≈ 1 vCPU) | ¥0.57 |
| AWS Lambda(x86) | ¥0.71 |
*每 CPU 小时的价(人民币,2026-10-09)。机器按 4 vCPU 的月价折算,是 100% 用满时的价;平均只用三成,实际单价要乘以三左右。Lambda 按 1,769 MB 内存约等于 1 个 vCPU 折算。*
Workers 的 CPU 单价是阿里云包月机器的约 3.6 倍、AWS 按需机器的约 1.5 倍。[^cpu-price] 听着很贵?可机器得按高峰买,平时大半在闲着:和阿里云比,只要机器平均利用率低于约三成,按毫秒收反而便宜。按时长计费的 Lambda 单价最高,但对计算型应用一点不冤,因为它挂着的每一秒都在算。
所以**同一种 Serverless,对等待型是个坑,对偶发的计算活却是好东西**。持续跑满的计算,还是放机器最便宜。另外别忘了硬限制:Workers 一次最多 5 分钟 CPU、128 MB 内存,大图处理和长视频转码装不下。[^limits]
---
## 完整例子:从零搭一个模型网关
前半篇讲的是方法,这一节拿一个具体的应用,把三笔账算到每一项。挑模型网关,是因为它三笔账几乎占全了:等得多、算得少,每个请求拎着中到重的行李,还要一笔一笔记账。
先把这个例子的结论摆出来,三个规模下谁花多少(点上面的标签切换规模):
| | 每月 1,000 万次请求 | 每月 1 亿次 | 每月 10 亿次 |
| --- | --- | --- | --- |
| Cloudflare(常规写法) | ¥140 | ¥2,756 | ¥32,574 |
| Cloudflare(优化写法) | ¥67 | ¥2,115 | ¥25,139 |
| 阿里云 · 深圳 | ¥1,625 | ¥5,854 | ¥45,696 |
| 腾讯云 · 广州 | ¥1,422 | ¥5,359 | ¥45,179 |
| AWS · 美东 | ¥2,467 | ¥6,646 | ¥47,179 |
| Google Cloud · 美东 | ¥3,056 | ¥7,387 | ¥43,798 |
| Azure · 美东 | ¥4,794 | ¥9,125 | ¥49,848 |
| Oracle · 美东 | ¥1,973 | ¥2,388 | ¥8,832 |
*每月账单,人民币目录价(2026-10-09)。自建 = 负载均衡 + 两个可用区的机器 + Redis + MySQL + 日志 + 出流量。香港、新加坡等地域,以及托管网关、Serverless 的完整数字在文末附录。*
1. **量小,选 Cloudflare。** 每月 1,000 万次请求,它一百多元,最便宜的自建也要一千四百元左右。云厂商收的是保底费:两台机器、一个高可用数据库、一个 Redis,没人用也得付。
2. **量大,看请求有多胖。** 到每月 10 亿次,阿里云、腾讯云和三大海外云的自建只比 Cloudflare 贵三成到一倍多。请求越小,自建越划算;请求越大(比如编码 Agent),Cloudflare 越便宜。
3. **Oracle 不按常理出牌。** 它每月前 10 TB 出流量免费,量大时美东自建比 Cloudflare 还便宜约三倍,代价是机器得自己管。
> [!NOTE]
> 这个例子的账单建立在一组示意假设上:每个请求挂 20 秒,网关只算 10 毫秒,来回搬 50 KB,机器规格没有压测。最敏感的是最后一个数,[第三步那张图](#第三步搬行李)能看出你的业务落在哪一边。
好,动手。假设你要上线一个模型网关:客户拿着 API key 来,你替他把请求转给大模型,按用量收他的钱。我们一个零件一个零件往上装,每装一个,看账单从哪冒出来。
拆到最小,网关只做六件事:
```mermaid
flowchart TD
C(["客户端"]) --> A["① 查 API key
是谁、能用哪些模型"]
A --> B["② 占并发位、预扣余额
超了直接 429"]
B --> R["③ 选上游线路"]
R --> U["④ 把请求体转发给上游"]
U --> S["⑤ 把 SSE 流原样转回
约 20 秒,几乎不用 CPU"]
S --> F["⑥ 读出 usage,释放并发位
按实际用量结算、记一条日志"]
classDef wait fill:#d0ebff,stroke:#1971c2
classDef work fill:#fff3bf,stroke:#f08c00
class S wait
class A,B,R,U,F work
```
*图:一个模型网关请求的最小因果链(示意,不是某个产品的实现)。黄色几步要算,但每步只有几毫秒;蓝色那一步要等,一等就是十几秒到几分钟。*
这六步让模型网关和普通 API 网关有三点不同:
| 特征 | 本文取值(示意) | 为什么重要 |
|---|---|---|
| 挂钟时间很长 | 20 秒 | 大部分时间在等上游生成 token;推理模型会更长 |
| CPU 时间很短 | 10 毫秒 | 鉴权、选线、逐块转发 SSE、找 usage[^cpu-ms] |
| 出流量大 | 50 KB / 请求 | 30 KB 是把客户的 prompt 转给上游,20 KB 是把 SSE 回给客户端 |
10 毫秒除以 20 秒是 0.05%。网关 99.95% 的时间,都在等。
### 第一步:接住请求,然后等
先装最核心的零件:收请求,转给上游,把回答原样传回去。这一步的账,取决于你站在哪种平台上:
- **Workers**:只收那 10 毫秒的 CPU,等的 20 秒一分不收。[^cf-wall] 20 秒的流和 0.2 秒的流一个价。
- **按时长计费的 Lambda、函数计算、云函数**:20 秒全收,一秒不落,而且常常一个实例只能伺候一个请求。[^one-per-instance]
- **自建机器**:等待只占一个连接、不占 CPU;但机器要按峰值并发买,闲着也付钱。
差距有多大?到每月 10 亿次,按时长计费的几家,账单是 Cloudflare 的两倍到十倍:
| | |
| --- | --- |
| Cloudflare Workers(按 CPU 时间计费) | ¥32,574 |
| Google Cloud Run(单实例并发 250) | ¥59,634 |
| 阿里云 AI 网关(托管) | ¥60,518 |
| 阿里云函数计算(单实例并发 100) | ¥68,877 |
| 腾讯云云函数(开请求多并发,估算) | ¥75,903 |
| Azure Container Apps(每副本 100 条流) | ¥79,376 |
| Google Cloud Run(单实例并发 80) | ¥107,676 |
| OCI Functions(Oracle) | ¥264,894 |
| AWS Lambda(128 MB Arm) | ¥278,460 |
| 腾讯云云函数(默认一个实例一条流) | ¥321,865 |
- 阿里云深圳自建: ¥45,696
*每月 10 亿次请求的月费(人民币,含 Redis、数据库、日志和出流量)。虚线是阿里云深圳自建。按挂钟时间计费的方案要为等上游的 20 秒付钱;Cloudflare Workers 只按 CPU 时间计费。*
图里那几家比较便宜的按时长计费方案,都开了「一个实例同时接很多请求」。它们在量小的时候甚至比自建还便宜一点,因为省掉了常驻机器和负载均衡;可量一上去,就反过来了。
想看这一步落到代码上长什么样,下面是 Workers 版的伪代码,注释里标了哪行在等、哪行在算(示意,不是哪个平台的真实源码):
```ts
// 伪代码(示意):Workers 版模型网关的一次请求
export default {
async fetch(req, env, ctx) {
const key = await env.KEYS.get(apiKeyOf(req), "json"); // ① KV 读:等网络,不算 CPU
if (!key) return new Response("invalid key", { status: 401 });
const limiter = env.LIMITER.get(env.LIMITER.idFromName(key.id)); // 每个 API key 一个 DO
const slot = await limiter.acquire(estimateCost(req)); // ② 占并发位 + 预扣余额(写 1 行)
if (!slot.ok) return new Response("too many requests", { status: 429 });
const upstream = await fetch(pickLine(key), forward(req)); // ③④ 等上游首包,可能好几秒,不算 CPU
const { readable, writable } = new TransformStream();
ctx.waitUntil(pipeAndFindUsage(upstream.body, writable, async (usage) => {
await limiter.release(slot.id, usage); // ⑥ 释放并发位 + 结算(写 1 行)
await env.USAGE.send({ key: key.id, usage }); // ⑥ 用量消息,消费者按分钟聚合写 D1
}));
return new Response(readable, upstream); // ⑤ 20 秒的流,CPU 只花在逐块拷贝上
},
};
```
同样六步换到阿里云或腾讯云,就是一个常驻进程(Higress 插件或者自己写的 Go 服务)加一个 Redis:查 key 走进程内存缓存,没命中再查 Redis;占并发位和预扣余额用 Redis 的 `INCR` / `DECR` 加一段 Lua 脚本;用量在进程里攒一批,再批量写数据库或者发到消息队列。
**架构是一样的,差的是每一步怎么收费。**
### 第二步:记账
网关要按用量收客户的钱,就得记账:每个请求进门时占一个并发位、预扣一点余额,出门时按实际用量结算,再留一条用量记录。
```comic cols=2
> 每个请求都要记两笔账
[用户] 我先付了钱,用多少扣多少。
[你的服务] 那我进门先预扣一点,答完再按实际用量结账。
---
> 在 Cloudflare
[云厂商!] 每记一笔,收一笔钱。十亿个请求,就是二十亿笔。
---
> 在自己的 Redis
[云厂商] Redis 按台收钱。你记一笔还是记一亿笔,一个价。
```
这一笔,两边的收法正好反过来。
**Cloudflare 每记一笔,收一笔钱。** 最大的一项是 DO 写行,每月 10 亿次时约 1.3 万元。并发位和余额必须写进 DO 自带的 SQLite,不能只放内存,因为 DO 闲下来就可能睡着,醒来时内存里的计数已经没了,而一条流要挂 20 秒。[^do] 改成「用量先在 DO 里攒着、每分钟落一次库,再去掉 Queues」的优化写法,月费能降两成多。还有三件事要提前想好:
- 一个 DO 是单线程的,每秒大约扛一千次请求。所以每个 API key 一个 DO,大客户的 key 再拆分片,别用一个 DO 做全局限速。
- KV 是最终一致的,改动要一分钟以上才传遍全球,吊销 key、改余额不能只靠它。[^kv]
- D1 单库最大 10 GB,而且不能提。按分钟聚合的用量表,一个多月就写满,得定期汇总、按月分库,冷数据挪去 R2。[^d1]
**自建的 Redis 按台收钱。** 一个 1 GB 双副本的 Redis 每月 77 元,标称每秒 10 万次操作,每月 10 亿次时峰值每秒只要约 4,600 次。[^redis] 查 key、计数、余额全压在它上面,**请求数翻十倍,它的价格纹丝不动**。
把每月 10 亿次时的账单拆成三块,记账这一笔压在谁身上,一眼就看出来了:
| | 状态读写(查 key、计数、余额、用量库) | 出流量 | 其余(机器、负载均衡、日志) | 合计 |
| --- | --- | --- | --- | --- |
| Cloudflare(常规写法) | ¥27,462 | ¥0 | ¥5,112 | ¥32,574 |
| Cloudflare(优化写法) | ¥20,027 | ¥0 | ¥5,112 | ¥25,139 |
| 阿里云 · 深圳 | ¥1,730 | ¥37,997 | ¥5,969 | ¥45,696 |
| 腾讯云 · 广州 | ¥2,008 | ¥40,000 | ¥3,171 | ¥45,179 |
| AWS · 美东 | ¥4,626 | ¥28,827 | ¥13,726 | ¥47,179 |
| Oracle · 美东 | ¥2,954 | ¥2,270 | ¥3,608 | ¥8,832 |
*每月 10 亿次请求时,月费拆成三块(人民币)。Cloudflare 的「状态」按次收费,自建方案的 Redis 和 MySQL 按台收费。*
**Cloudflare 的账单八成多是记账;阿里云、腾讯云、AWS 的账单六到九成是出流量;Oracle 三块差不多大。** 出流量,是下一步的事。
### 第三步:搬行李
网关不只是传话,它还拎着行李。每个请求平均 50 KB:30 KB 是客户的 prompt,网关得替他送到上游;20 KB 是模型的回答,要送回客户端。**对网关来说,替客户转发 prompt 也算出流量。**
```comic cols=2
> 你的服务不只是传话
[用户] 我的问题有 30 KB,回答还有 20 KB。
[你的服务] 我得把问题送到大模型,再把回答搬回给你。
---
> 在国内云和三大海外云
[云厂商] 每个字节出门都得买票。
[你的服务] 连我替用户转给大模型的问题也算?
[云厂商!] 也算。
---
> 在 Cloudflare 和 Oracle
[云厂商] 出门不要票。Oracle 是每月前 10 TB 不要。
```
Cloudflare 出门不要票,Worker 发出的子请求也不收钱,条款里也没有带宽上限。[^tos] 国内云每 GB 五毛到八毛,三大海外云差不多。Oracle 每月前 10 TB 免费,超出以后北美每 GB 不到一美分。
所以,每个请求搬多少字节,几乎决定了谁便宜:
| 每个请求的出流量(KB) | 阿里云 · 深圳自建 | AWS · 美东自建 | Oracle · 美东自建 | Cloudflare 常规写法 | Cloudflare 优化写法 |
| --- | --- | --- | --- | --- | --- |
| 0 | | | | ¥32,574 | ¥25,139 |
| 5 | ¥9,478 | ¥18,896 | ¥6,562 | | |
| 10 | ¥13,723 | ¥22,186 | ¥6,562 | | |
| 15 | ¥17,731 | ¥25,320 | ¥6,834 | | |
| 20 | ¥21,726 | ¥28,443 | ¥7,119 | | |
| 25 | ¥25,721 | ¥31,566 | ¥7,405 | | |
| 30 | ¥29,716 | ¥34,688 | ¥7,690 | | |
| 35 | ¥33,711 | ¥37,811 | ¥7,975 | | |
| 40 | ¥37,706 | ¥40,933 | ¥8,261 | | |
| 50 | ¥45,696 | ¥47,179 | ¥8,832 | | |
| 60 | ¥53,247 | ¥52,548 | ¥9,402 | | |
| 70 | ¥60,737 | ¥57,785 | ¥9,973 | | |
| 80 | ¥68,227 | ¥63,023 | ¥10,544 | | |
| 90 | ¥75,717 | ¥68,261 | ¥11,115 | | |
| 100 | ¥83,207 | ¥73,499 | ¥11,686 | ¥32,574 | ¥25,139 |
- ≈ 34 KB 时打平
- 本文假设 50 KB
*Cloudflare 不收出流量费,所以它的两条线一动不动。500 KB 的编码 Agent 请求在图外:阿里云自建 ¥365,488,AWS ¥236,506,Oracle ¥34,518。*
- **行李轻,自建便宜。** 每个请求的出流量低于约 34 KB 时,阿里云自建比 Cloudflare 便宜(对 Cloudflare 的优化写法是约 24 KB)。短问答只有 5 KB 左右,自建只要 Cloudflare 的三分之一。
- **行李重,Cloudflare 便宜。** 编码 Agent 的请求体动辄几百 KB,这时 Cloudflare 便宜好几倍。AWS 的打平点比阿里云还低。
- **Oracle 在整个范围里都最便宜**,要到三四百 KB 才和 Cloudflare 打平。
- **量越小,打平点越低。** 每月 1,000 万次时,云厂商的保底费已经比 Cloudflare 的全部账单还高,怎么都打不平。
所以「Cloudflare 便宜」这句话,**只在量小或者行李重的时候成立**。
国内云还有一个独门杠杆:**上游如果就在同一家云的内网里**(比如在阿里云上调百炼),转发 prompt 那部分可以不走公网。百炼的私网连接目前只开了北京和香港;网关和百炼同地域时,每月 10 亿次能把出流量这一项从约 3.8 万元降到约 1.9 万元(已算上私网连接费)。网关在深圳、跨地域连北京,就基本省不下来。
### 第四步:把量放大一百倍
零件装齐了。现在把流量从每月 1,000 万次拧到 10 亿次,也就是这一节开头那张图的三个标签。
- **量小的时候,比的是保底费。** Cloudflare 没有保底,云厂商有。所以 1,000 万次时它便宜一个数量级。
- **量大以后,比的是记账和行李。** Cloudflare 的钱花在记账上,其他云的钱花在出流量上,两边只差三成到一倍多。Oracle 的出流量几乎不要钱,所以它跑到了最前面。
- **CPU 几乎不影响账单。** 把网关的 CPU 时间从 5 毫秒翻到 20 毫秒,Cloudflare 的月费只变 6%;自建的话 4 台 8 核机器也装得下。按 CPU 收费时,等上游的 20 秒几乎不花钱。
再说一个让人松口气的事实:**网关的基础设施费,只是模型费的零头。** 摊到每个请求,Cloudflare 约 0.00003 元,阿里云自建约 0.000045 元;而一次 7,500 token 输入、300 token 输出的调用,按 DeepSeek V4.1-Flash 的高峰价约 0.017 元,网关只占 0.2–0.3%,换成便宜得多的通义 qwen-flash 也只占 2–3%。[^model]
所以选平台不能只盯着账单。延迟、可达性和稳定性,至少同样重要。
### 第五步:挪到用户身边
最后一个零件:入口放在哪,离用户多远。
| | |
| --- | --- |
| 阿里云 · 深圳 | 7 ms |
| 腾讯云 · 广州 | 8 ms |
| 阿里云 · 香港 | 13 ms |
| 腾讯云 · 香港 | 13 ms |
| 阿里云 · 新加坡 | 57 ms |
| 腾讯云 · 新加坡 | 89 ms |
| Cloudflare(任播,落在洛杉矶) | 162 ms |
| AWS · 新加坡 | 203 ms |
| AWS · 美东 | 224 ms |
| Oracle · 新加坡 | 230 ms |
| 阿里云 · 弗吉尼亚 | 231 ms |
| Oracle · 美东 | 236 ms |
*深圳电信家宽 ping 各家入口,2026-10-09,每个 20 次。国内地域和 Cloudflare 取 10:49 的中位数,境外地域取 11:55 的平均值。Azure 和 Google Cloud 的入口是全球任播地址,测不出某个地域,没列。*
从深圳电信访问国内云的深圳、广州、香港入口,都在 10 毫秒上下;Cloudflare 的包在电信骨干上一路出境,最后落在洛杉矶,162 毫秒。[^latency] 新加坡也不一定近:同样在新加坡,阿里云 57 毫秒,AWS 和 Oracle 要 200 多毫秒,和美东差不多。国内云的境外地域和国内运营商的互联,通常好得多。
这对首 token 意味着什么?新建 HTTPS 连接至少要 3 个往返(TCP、TLS 1.3、发出请求),162 毫秒一个往返,首 token 前就多了约 0.45 秒;复用连接时多约 0.15 秒。聊天能感觉到,Agent 的长任务基本无所谓。
还有三件事:
- **China Network 救不了这套架构。** 不开 China Network 时,大陆用户连的是境外机房。China Network 要 Enterprise 套餐、单独订阅、每个主域名的 ICP 备案或许可证,还要过京东云的内容审核;它的可用产品清单里有 Workers、KV、R2,**没有 DO、D1、Queues**(官方没说这几项在京东云节点上是不能用还是绕回境外)。`*.workers.dev` 在大陆被封,必须绑自己的域名。[^workers-dev]
- **海外模型的跨境那一跳躲不掉。** OpenAI 和 Anthropic 的 API 支持地区名单里,既没有中国大陆,也没有香港,所以网关调这两家,出口要放在新加坡、日本、美国这类支持地区,香港只能做中转。[^regions] 从大陆用户的角度看,长距离那一跳要么在「用户 → Cloudflare 洛杉矶」,要么在「境外网关 → 美国上游」,所以调海外模型时,Cloudflare 的延迟劣势小得多,差别主要在线路质量。
- **两边都出过大故障,形状不同。** Cloudflare 2025-06-12 因为 KV 依赖的第三方云宕机,AI Gateway 错误率峰值到 97%;2025-11-18 核心代理故障,KV 也大量报 5xx。每个请求都同步读 KV 或 DO 的网关,会跟着全球一起挂,没有「换个可用区」的退路。阿里云香港可用区 C 在 2022-12-18 因制冷故障中断十多个小时,但故障集中在一个可用区,跨可用区部署就有退路。
---
## 上线前要绕开的坑
账单之外,每朵云都埋了几个能让账单翻倍、或者让请求半路断掉的坑。
### Cloudflare 的三个坑
- **让 DO 陪着整条流一起醒着。** 比如让流经过 DO 转发,或者在 DO 里挂一个 `setTimeout` 做租约超时,DO 就会按挂钟时长计费,每月 10 亿次时最多多出约 21.5 万元。租约超时用 `setAlarm`。2026-10-01 起,DO 里没完成的出站调用和 `waitUntil` 也会让它保持清醒(最多 15 分钟),更容易踩到。
- **AI Gateway 默认把完整的 prompt 和回答记进日志。** 2026-09-24 以后新开 AI Gateway 的账号,日志按 Workers Logs 的价格计费,每月 10 亿次时约 9.4 万元。关掉 payload 记录,或者干脆关日志。另外,用 Cloudflare 统一付费(Unified Billing)时,每个 gateway 每分钟只允许 200 次请求,每月 1,000 万次的平均速率就超了,所以转售场景只能用自己的上游 key(BYOK)。
- **默认的日志配置。** 新建的 Worker 默认开日志,每次调用自动写一条,DO 的每次 RPC 按文档推断也写一条,一个请求就是约 4 条。按官方给的平均每条 4.84 KB 粗算,日志费会涨到原来的二十倍。给网关和 DO 所在的 Worker 关掉调用日志,或者采样。[^logs]
### 国内云的四个坑
- **境外节点的阶梯价要手动开。** 阿里云香港、新加坡、东京的流量单价比深圳低,所以每月 10 亿次时,香港自建反而比深圳便宜,虽然香港的机器贵近一倍。但 2024-12-12 起,ECS、EIP **不会自动按阶梯价**(阿里云叫 CDT)计费,要手动「升级至 CDT 计费」,免费,开了不能关;香港不升级,每月 10 亿次时要多付约 2.1 万元。新加坡、东京反过来:不升级时的原价比阶梯首档还低,流量在一定区间内不升级更省。
- **上游请求别走 NAT。** 阿里云 NAT 网关从 2025-09-26 起按处理的流量收费,上游请求走 NAT,会在流量费之外再加约 23%。给每台 ECS 直接挂按流量计费的公网 IP 更省。
- **长连接不贵,但会被超时掐断。** 阿里云 ALB 的容量费实际由处理的字节数决定,2.3 万条长连接只折合不到 8 个容量单位;腾讯云 CLB 共享型干脆不按容量收费。但 **ALB 的请求超时默认 60 秒**,推理模型的长流要先调大,否则会 504。
- **托管 AI 网关不便宜。** 阿里云的 AI 网关(内核是开源的 Higress)多模型路由、Fallback、消费者 API key、Token 限流都有,但最小规格每月约 4,000 元(文档价);余额和用量库还得自己配 Redis 和数据库,所以量小时它比整套自建贵两倍多。2026-09-01 起收费的 Serverless 版没有动辄几千元的起步规格费(企业版每月约 176 元实例费),量小时和自建差不多,但公网流量进出都按 0.8 元/GB 收、不走阶梯价,量大以后更贵。
### 海外四家:三大云和国内云是同一本账,Oracle 是例外
三大云的流量价和国内云在同一量级,首档每 GB 约合六毛到八毛人民币,量大以后降到五毛多;机器、数据库、日志反而更贵,日志每 GB $0.50,是阿里云日志服务的 8 倍多。两边一抵,量大时三大云和国内云的账单差不多。量小时 AWS 美东比阿里云深圳贵一半,Azure 美东贵到近三倍。
如果客户端在中国大陆,Google Cloud 回给客户端的那部分流量要按价目表里「到中国内地」那一档计,每 GiB $0.20–0.23(官方没写按什么判定目的地);全部出流量都按这个价算的话(上限),每月 10 亿次时美东会从约 4.4 万元涨到约 8.1 万元。
Oracle 的出流量几乎免费:每月前 10 TB 不要钱,超出后北美 $0.0085/GB、亚太 $0.025/GB;同地域内的流量、负载均衡转发的数据都不另收费,日志写入也免费。所以每月 10 亿次时,美东 50 TB 出流量只要约 2,300 元,阿里云深圳同样的流量要约 3.8 万元。代价藏在别处:
- 10 TB 免费额度按「来源大区」各算一份,美东和新加坡各 10 TB,同一大区的多个地域共用一份。[^oci-10tb]
- 新加坡只有一个可用域(Oracle 对可用区的叫法),做不到跨可用区,只能跨故障域。
- 按量付费账户默认每个可用域只有 6 个 OCPU、只能开一个地域,量大以后要先申请提额。
- NAT 网关对同一个目的地址和端口的并发连接有上限,美东这种有 3 个可用域的地域每个可用域约 2 万条,新加坡约 6.5 万条;2.3 万条流压在一个可用域、打同一个上游会撞上,所以机器最好挂公网 IP 直接出去(预留公网 IP 官方写明不收费)。
- 最小的 MySQL 高可用规格按 3 个实例收费,每月 $170,是量小时账单的大头。
**负载均衡的默认超时,是上线前最容易踩的坑:**
| | 默认 | 对 SSE 的影响 |
|---|---|---|
| 阿里云 ALB | 请求超时 60 秒,按 ALB 和后端多久没有数据往来算(WebSocket 文档的说法,SSE 没写) | 推理模型首 token 前思考超过 60 秒会 504,要先调大 |
| AWS ALB | 空闲超时 60 秒,可调到 4,000 秒 | 推理模型思考超过 60 秒还没吐首个 token 就会断;调大或发 SSE 心跳 |
| Google Cloud 外部应用负载均衡 | 后端服务超时 30 秒,**算的是整个响应的总时长** | 不是空闲超时,超过 30 秒的流直接被截断;全局负载均衡最多可调到 86,400 秒,上线前要调大 |
| Azure Application Gateway v2 | 请求超时 20 秒,按多久没收到数据算 | 官方排错文档写超时后会把请求再发给另一台后端,没说 POST 例外;如果 POST 也重试,首 token 超过 20 秒的请求**可能被转发两次、上游扣两次费**[^appgw];跑 SSE 还要关掉默认开启的响应缓冲 |
| Oracle 灵活负载均衡 | 空闲超时 60 秒,可调到 7,200 秒 | 发送数据不会重置接收计时 |
### Serverless 和边缘函数:量大以后都别指望
各家按时长计费的产品,各有各的脾气:
- **阿里云函数计算**:规格费按配置收、不按实际用量收,等上游的 20 秒 vCPU 全额计费,也进不了 vCPU 免费的「浅休眠」。唯一的省钱办法是单实例多并发:并发为 1 时的计算费,是并发 100 时的约 95 倍。
- **腾讯云云函数**:默认一个实例同一时刻只处理一条 SSE 连接,量大时峰值还超出每个地域默认的并发配额。
- **AWS Lambda**:默认并发 1,000,量大要申请提额。
- **Google Cloud Run**:一个实例最多同时处理 1,000 个请求,是几家里最适合长连接的,但仍按实例的挂钟时间计费;它还会自动写请求日志、每 GiB 收 $0.50(可以用排除过滤器关掉),图里没算。
- **Azure**:Functions Flex 默认单实例并发 16,每月 10 亿次时要 1,447 个实例,超过 1,000 个的上限;Container Apps 把并发调到 100 后能跑。
- **OCI Functions**:一个实例一次只处理一个请求,同步调用最长 300 秒,而且要等函数执行完才返回结果,做不了流式。
边缘函数更干脆,**国内的边缘函数做不了通用模型网关**,卡在限制上,不在价格上:
| | Cloudflare Workers | 阿里云 ESA 函数 | 腾讯云 EdgeOne 边缘函数 |
|---|---|---|---|
| 怎么计费 | 请求 + CPU 时间 | 按次,¥5/百万次 | 请求 ¥1.7/百万次 + CPU ¥0.11/百万毫秒 |
| 单次总时长 | 不限(客户端连着就行) | **120 秒**(等待也算) | 文档没写上限;`fetch` 超时最大可配 300 秒 |
| 首包 | 文档没列限制 | **10 秒内不出数据就回 504** | 文档没列限制 |
| 请求体 | 100 MB 起(看域名套餐) | 函数文档没写;站点上传上限默认 300 MB | **1 MB** |
| 单次 CPU | 默认 30 秒,可配到 5 分钟 | 文档没写数值 | **200 毫秒** |
| 子请求 | 默认 1 万次 | **4 个**(中文文档写每次 4 个,英文写同时 4 个、可申请提额) | 64 次 |
| 强一致状态 | Durable Objects | 没有(KV 最终一致,最长 300 秒) | KV 内测,只对企业版,每个命名空间每天 100 万次读 |
| 中国大陆节点 | 只有 China Network(Enterprise + ICP) | 有,要 ICP 备案 | 有,要 ICP 备案 |
推理模型的首 token 常常超过 10 秒,长输出常常超过 120 秒,Agent 的长上下文请求也很容易超过 1 MB;两家也都没有 DO 这样的强一致状态,限速和余额还得回源到中心机房。海外的 CloudFront Functions 和 Lambda@Edge 一样转发不了 20 秒的 SSE,Azure Front Door 的文档写明不支持 SSE。**边缘函数可以站在网关前面做缓存、鉴权预检,替代不了网关。**
至于海外的托管 AI 网关,三大云有、Oracle 没有,而且都不是为「转售」设计的:
- **Azure API Management**:v2 的三个层级都能透传 SSE,有 `llm-token-limit` 这类按 token 限流的策略,量小时 Basic v2 每月约 $148(微软把它定位为开发测试用)。卡脖子的是每个上游主机最多 2,048 条并发后端连接,SSE 一条流占一条,每月 1 亿次的峰值就超了;经典层写明按单元算,v2 只写了 2,048,按单元算的话每月 10 亿次要 12 个单元、约 $1.8 万/月,按字面理解则要拆成 12 个实例再加一层分流。上游走 HTTP/2 在 v2 里还是预览。
- **AWS**:Bedrock AgentCore Gateway(2026-07 起支持推理目标)能代理 OpenAI、Anthropic 和兼容端点,SSE 原样透传,2026-08 起能按用户身份限 RPM、TPM 和并发;但内置鉴权只有 IAM、JWT 或不鉴权,自定义校验要另挂拦截器,而文档写拦截器暂不支持流式,也没有「每个客户一个 API key」、按客户的用量账单和预付费余额。API Gateway 的 REST API 2025-11 起支持流式响应,但读不到流里的用量。
- **Google**:Apigee 能透传 SSE,但按 token 限流的策略只能用在最贵的代理类型上,每月 10 亿次时光调用费就要约 $73,000。
- **Oracle**:没有通用的 LLM 网关,生成式 AI 服务只能调自己目录里的模型。
---
## 谁真的把模型网关放在 Workers 上
只采信一手证据:官方博客、官方文档、仓库里的部署配置、创始人署名文章。
| 平台 | 用了哪些 Cloudflare 组件 | 证据 | 强度 |
|---|---|---|---|
| **Cloudflare AI Gateway** | Workers;日志先放 D1,后挪 R2,再改成按「账户 + gateway」分片的 DO | [官方博客 2024-10-24](https://blog.cloudflare.com/billions-and-billions-of-logs-scaling-ai-gateway-with-the-cloudflare/) | 证实 |
| **Helicone**(托管代理和托管 AI Gateway) | Workers + DO(限速器,加每个组织一个钱包:开始按最坏成本冻结,结束按实际用量结算)+ KV + Queues;超过 20 MiB 的请求体转给 Containers;后端在 AWS | [`worker/wrangler.toml`](https://github.com/Helicone/helicone/blob/main/worker/wrangler.toml)、[钱包 DO](https://github.com/Helicone/helicone/blob/main/worker/src/lib/durable-objects/Wallet.ts)、[官方文档](https://docs.helicone.ai/references/availability) | 证实(边缘请求路径) |
| **OpenRouter** | 边缘层用 Workers,在边缘缓存用户和 API key 数据;余额很低时会多查数据库,延迟上升 | [官方文档](https://openrouter.ai/docs/guides/best-practices/latency-and-performance) | 证实(只到边缘层) |
| **Portkey** | 开源网关仓库自带生产环境的 `wrangler.toml`,官方文档说托管版跑在全球「edge workers」上 | [wrangler.toml](https://github.com/Portkey-AI/gateway/blob/main/wrangler.toml) | 间接证据 |
| **Unkey**(迁走了) | 曾用 Workers + DO 做 API key 校验和限速,2025-08 迁到 AWS 上的有状态 Go 服务 | [联合创始人文章 2025-08-01](https://www.unkey.com/blog/serverless-exit) | 证实 |
| **Braintrust**(迁走了) | 2023 年 AI Proxy 跑在 Workers 上;现在托管入口是 AWS 上的 Gateway,原因没公开 | [2023 年博客](https://www.braintrust.dev/blog/ai-proxy)、[现行文档](https://www.braintrust.dev/docs/deploy/gateway) | 证实「迁了」 |
从这张表能看出三件事:
1. **除了 Cloudflare 自己,没有一家是「全 Cloudflare」。** 最接近的是 Helicone:请求路径上的 Workers、DO、KV、Queues 全在 Cloudflare,分析库和账务后端在 AWS。本文 Cloudflare 一侧连账务都放在 D1 里,比现实中的做法更激进。
2. **大家踩的坑都在记账这一层,不在计算层。** AI Gateway 的日志存储、Helicone 的钱包预扣、OpenRouter 的低余额回源、Pydantic 网关的 KV 软额度,都是。
3. **Unkey 的离开有它的场景。** 它的理由是:缓存读要走网络,p99 超过 30 毫秒;为了绕开无状态,堆了 DO、Queues、Workflows 一串产品;数据导出麻烦;客户没法自托管。迁走后延迟降到原来的 1/6。但 Unkey 每个请求都要在 10 毫秒内完成 key 校验,而模型网关的一个请求本来就要挂 20 秒,几十毫秒的状态读取占比小得多,这个结论不能原样搬过来。
国内这边的主流做法是**常驻网关进程加 Redis**,不是边缘函数:
- **Higress**:阿里开源的网关,2026-03-15 进入 CNCF Sandbox,约 9,500 星。项目方说它支撑通义千问 APP、百炼大模型 API 和 PAI,但没有公开流量数字。它的 Token 限流用 Redis 做全局计数,和 Cloudflare 一侧用 DO 计数是同一个设计点。
- **阿里云 AI 网关**:官方客户案例里,国泰产险所有访问大模型的流量都走 AI 网关,日均近亿 token。
- **国内边缘函数**:没有找到点名的模型网关客户。
---
## 怎么选
**先按你的应用。** 拆完三笔账,大致这么走:
1. **用户主要在中国大陆?** 用国内云,网站对外服务前先做 ICP 备案。
2. **只有静态页面**(个人网站、博客、作品集)?用静态托管加 CDN,基本不花钱。
3. **等待型**(调大模型、调第三方接口)?海外用户用边缘函数,国内用户用一台常驻的小服务器;别用按时长计费的 Serverless。
4. **计算型**(图片、视频处理)?偶发的用按时长计费的 Serverless,持续跑满的用机器。
5. **轻量型**(普通的后端)?先用免费额度或者最便宜的服务器套餐跑起来,量大以后再按三笔账重算一遍。
**下面的决策树和清单,是模型网关这一类的。**
```mermaid
flowchart LR
%% layout: tree
Q(["付费用户主要在哪?"])
Q --> CN["中国大陆"] --> CN1["国内模型为主
→ 阿里云 / 腾讯云内地:常驻网关(Higress 或自写)+ Redis
别用函数计算 / 云函数,边缘函数只挡在前面"]
CN --> CN2["也要海外模型
→ 内地入口 + 境外节点调海外模型
出口放在 OpenAI / Anthropic 支持的地区(如新加坡、东京)
香港节点记得升级 CDT,新加坡、东京先算一下"]
Q --> OS["海外"] --> OS1["量小、不想运维
→ Cloudflare Workers + 每个 key 一个 DO
账务按月分库或外接数据库,日志关掉 payload"]
OS --> OS2["量大、流量重
→ Oracle 自建(出流量每月前 10 TB 免费)
已在 AWS / GCP / Azure 上的,流量账和国内云差不多"]
Q --> BOTH["两边都有"] --> BOTH1["→ 两个入口各自就近
账务只放一处,别双写"]
classDef q fill:#fff3bf,stroke:#f08c00
classDef pick fill:#d3f9d8,stroke:#2f9e44
class Q q
class CN1,CN2,OS1,OS2,BOTH1 pick
```
*图:按用户所在地选平台的决策树(本文的判断,不是官方建议)。*
**什么时候选 Cloudflare:**
- 用户主要在海外,或者是调用海外模型的开发者;
- 规模还小(每月千万次以内),想要几乎为零的保底费、不想运维机器,六家云厂商的自建都比不过;
- 请求体大(Agent、长上下文、多模态),流量费在云厂商那边会是账单大头;
- 团队能接受按 key 拆 DO、D1 定期归档这类「为记账而设计」的工作。
**什么时候别用 Cloudflare:**
- 用户主要在中国大陆,对首 token 延迟敏感(聊天产品);
- 规模到每月 10 亿次量级、请求又很短(每个请求出流量低于约 30 KB),国内云或三大云自建会更便宜;
- 用户在海外、规模大,又愿意自己运维机器:Oracle 自建便宜约三倍;
- 需要强一致的全局状态(全局并发、跨 key 的额度池),又不想自己做分片;
- 需要在中国大陆有节点,又买不起 Enterprise,或者用了 DO、D1、Queues。
---
## 结语
回到漫画里的「你的服务」。它一天里真正干活的时间,加起来可能不到一分钟;剩下的时间,它在等大模型说完、在替用户搬数据、在一笔一笔记账。云厂商没有一家只按「干活」收它的钱,它们各自挑了等待、搬运和记账里的一两样来收。
所以第一次上线,先拆账,再用免费额度把应用跑起来;等量真的大了,你已经知道该盯哪一笔。换一类应用,三个问题的答案变了,结论也跟着变:模型网关是**等待密集、搬运密集、状态密集**型的,拿 CPU 价格去比两朵云,比的是它账单里最小的那一项。
> **铁律:先问三件事——谁为等待收钱,谁为搬运收钱,谁为状态收钱。再看你的用户离哪边近。**
---
## 附录:怎么查的和完整数字
正文里的数字都出自这里。表头的「1,000 万次 / 1 亿次 / 10 亿次」都是每月的请求数。
### 怎么查的
- **国内两家用官方命令行工具的询价接口。** 阿里云用 aliyun CLI 3.5.1(`DescribePrice`、`GetPayAsYouGoPrice`),腾讯云用 tccli 3.1.180.1(`InquiryPrice*`、`DescribeDBPrice`)。所有调用都是只读的询价接口,没有创建任何资源。
- **Cloudflare 全部来自官方价目页和限制页。** 它没有价格接口,wrangler 4.148.0 和 Cloudflare API 只能查自己账号的用量。
- **海外四家用不用登录的公开价格接口**(AWS Price List、Azure Retail Prices API、Oracle 价目 API);Google Cloud 的价格接口要 API key,用的是官方价目页。查美东和新加坡两个地域,一律按需价、月按 720 小时。预留实例、Savings Plans 能再省三到五成,但只省机器,不省流量。
- **一律用目录价。** 阿里云的询价接口会同时返回账号折扣(比如我的账号负载均衡和 NAT 有 85 折),这类折扣不计入。查不到接口的用官方文档价并标出来,比如阿里云 AI 网关在计费系统里没有计价模块。
- **同一天做过两轮核实**:前两轮标为「未核实」的事项逐项查了官方原文,查实的已改进正文,仍查不实的(多半要账号、压测或真实账单)照旧标出。
几条代表性的询价命令(完整命令和原始输出都存了下来)
```bash
# 阿里云:ECS 包月价(含 40 GB ESSD PL0 系统盘,不含带宽)
aliyun ecs DescribePrice --RegionId cn-shenzhen --ResourceType instance \
--InstanceType ecs.c9i.xlarge --PriceUnit Month --Period 1 \
--SystemDisk.Category cloud_essd --SystemDisk.PerformanceLevel PL0 --SystemDisk.Size 40
# 阿里云:50,000 GB 公网出流量走 CDT 阶梯的总价
aliyun bssopenapi GetPayAsYouGoPrice --region cn-hangzhou --ProductCode cdt \
--ProductType cdt_DataTransfer_public_cn --SubscriptionType PayAsYouGo --Region cn-shenzhen \
--ModuleList.1.ModuleCode internet_traffic --ModuleList.1.PriceType Usage \
--ModuleList.1.Config 'Region:cn-shenzhen,internet_traffic:50000,charge_type:PayByTraffic,isp:BGP'
# 阿里云:Redis(Tair)、RDS 包月价
aliyun r-kvstore DescribePrice --RegionId cn-shenzhen --ZoneId cn-shenzhen-c \
--InstanceClass redis.shard.large.ce --OrderType BUY --ChargeType PrePaid --Period 1 --NodeType MASTER_SLAVE
aliyun rds DescribePrice --RegionId cn-shenzhen --ZoneId cn-shenzhen-c --Engine MySQL --EngineVersion 8.0 \
--DBInstanceClass mysql.n4.large.2c --DBInstanceStorage 100 --DBInstanceStorageType cloud_essd \
--PayType Prepaid --UsedTime 1 --TimeType Month --Quantity 1 --OrderType BUY --CommodityCode rds
# 腾讯云:CVM 包月价(含 50 GB 通用型 SSD,带宽设 0)
TENCENTCLOUD_REGION=ap-guangzhou tccli cvm InquiryPriceRunInstances --cli-unfold-argument \
--Placement.Zone ap-guangzhou-6 --ImageId img-mmytdhbn --InstanceType SA9.LARGE8 \
--InstanceChargeType PREPAID --InstanceChargePrepaid.Period 1 \
--SystemDisk.DiskType CLOUD_BSSD --SystemDisk.DiskSize 50 \
--InternetAccessible.InternetChargeType TRAFFIC_POSTPAID_BY_HOUR \
--InternetAccessible.InternetMaxBandwidthOut 0 --InstanceCount 1
```
```bash
# AWS:us-east-1 到互联网的出流量阶梯(Price List 公开文件,AWSDataTransfer 服务)
curl -s https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AWSDataTransfer/current/us-east-1/index.json
# Azure:eastus 的出流量阶梯(Retail Prices API)
curl -s "https://prices.azure.com/api/retail/prices?\$filter=serviceName%20eq%20'Bandwidth'%20and%20armRegionName%20eq%20'eastus'%20and%20meterName%20eq%20'Standard%20Data%20Transfer%20Out'"
# Oracle:北美出发的出流量(价目 API,B88327 = Outbound Data Transfer - Originating in North America, Europe, and UK)
curl -s "https://apexapps.oracle.com/pls/apex/cetools/api/v1/products/?currencyCode=USD&partNumber=B88327"
```
### 关键单价
Cloudflare、阿里云(深圳)、腾讯云(广州)
| | Cloudflare(美元) | 阿里云(深圳,人民币) | 腾讯云(广州,人民币) |
|---|---|---|---|
| 计算 | Workers $5/月,含 1,000 万请求和 3,000 万 CPU 毫秒;超出 $0.30/百万请求、$0.02/百万 CPU 毫秒;**挂钟时长不收费** | ECS c9i 包月:2c4g ¥205.91,4c8g ¥391.82,8c16g ¥763.64(含系统盘) | CVM SA9 包月:2c4g ¥156.2,4c8g ¥287.4,8c16g ¥549.8(含系统盘) |
| 负载均衡 | 不需要 | ALB 实例 ¥0.049/时 + 容量单位(LCU)¥0.049/个·时;LCU 按新建连接、并发连接、处理字节、规则次数四个维度取最大值 | CLB 共享型 ¥0.2/时,**不按容量收费** |
| 出流量 | **不收费** | CDT 阶梯:10 TB 内 ¥0.80/GB,10–50 TB ¥0.75,50–150 TB ¥0.70,再往上 ¥0.65;每月免费 20 GB | ¥0.80/GB,单一价,没有阶梯也没有免费额度 |
| 计数 / 余额 | DO:请求 $0.15/百万,SQLite 写 $1.00/百万行(含 5,000 万行) | Tair 双副本 1 GB ¥76.98/月,4 GB ¥360/月 | Redis 主从 1 GB ¥76/月,4 GB ¥304/月 |
| 查 key | KV 读 $0.50/百万(含 1,000 万) | 同上 Redis | 同上 Redis |
| 用量 / 账务库 | Queues $0.40/百万次操作(一条消息 3 次);D1 写 $1.00/百万行,**单库最大 10 GB** | RDS MySQL 高可用:2c4g ¥660/月,4c16g ¥1,370/月 | MySQL 双节点:2c4g ¥480/月,4c16g ¥1,704/月 |
| 日志 | Workers Logs,**2026-12-01 起**按 $0.25/GB 写入 + $0.10/GB·月存储 | SLS ¥0.4/GB(含索引和 30 天存储) | CLS 按功能计费,每 GB 约 ¥0.83 |
AWS、Google Cloud、Azure、Oracle(美东,美元)
| | AWS | Google Cloud | Azure | Oracle |
|---|---|---|---|---|
| 出流量 | 每月免费 100 GB;10 TB 内 $0.09/GB,10–50 TB $0.085(新加坡首档 $0.12) | Premium 层到北美:1 TiB 内 $0.12/GiB,10 TiB 内 $0.11,再往上 $0.08(新加坡到亚洲首档 $0.12) | 每月免费 100 GB;10 TB 内 $0.087/GB,10–50 TB $0.083(新加坡首档 $0.12) | **每月前 10 TB 免费**,超出 $0.0085/GB(新加坡 $0.025) |
| 4 vCPU 8 GiB 机器(每月) | c8i.xlarge $134.94 | c4-highcpu-4 $122.47 | F4als_v7 $174.24 | E6.Flex 2 OCPU $54.72 |
| 负载均衡 | ALB $16.20/月 + LCU(处理字节约 $0.008/GB) | 转发规则 $18/月 + $0.008/GiB | Application Gateway v2 $0.20/时 + 容量单位 $0.008 | 灵活负载均衡每月 $9–41,转发的数据不另收费 |
| 约 1 GB 高可用 Redis(每月) | Valkey 主从 $36.86 | Memorystore 标准层 $46.08 | Azure Managed Redis B1 $46.08 | OCI Cache $55.87 |
| 约 2 核 MySQL 高可用(每月) | RDS Multi-AZ $115.88 | Cloud SQL HA $192.80 | Flexible Server 区域冗余 HA(最小 2 vCore 8 GiB)$272.84 | HeatWave HA $170.11 |
| 日志 | CloudWatch Logs 写入 $0.50/GB | Cloud Logging $0.50/GiB,每项目每月 50 GiB 免费 | Basic Logs $0.50/GB | 写入免费,存储 $0.05/GB·月 |
### 完整月费
自建网关和 Cloudflare(含香港、新加坡)
机器规模:国内两家每月 1,000 万次用 2 台 2 核 4G,1 亿次用 2 台 4 核 8G,10 亿次用 4 台 8 核 16G,双可用区,没有压测;数据库和 Redis 规格随规模放大。人民币目录价:
| 方案 | 1,000 万次 | 1 亿次 | 10 亿次 |
|---|---|---|---|
| Cloudflare 常规写法(Workers + KV + Durable Objects + Queues + D1) | 140 | 2,756 | 32,574 |
| Cloudflare 优化写法(用量在 Durable Object 里累加,去掉 Queues) | 67 | 2,115 | 25,139 |
| 阿里云 · 深圳 | 1,625 | 5,854 | 45,696 |
| 阿里云 · 香港(境外节点参考) | 1,981 | 6,114 | 40,426 |
| 腾讯云 · 广州 | 1,422 | 5,359 | 45,179 |
| AWS · 美东 / 新加坡 | 2,467 / 3,227 | 6,646 / 8,572 | 47,179 / 53,327 |
| Google Cloud · 美东 / 新加坡 | 3,056 / 4,031 | 7,387 / 8,583 | 43,798 / 48,564 |
| Azure · 美东 / 新加坡 | 4,794 / 6,215 | 9,125 / 11,934 | 49,848 / 57,957 |
| Oracle · 美东 / 新加坡 | 1,973 / 1,973 | 2,388 / 2,388 | 8,832 / 13,237 |
托管网关和 Serverless(同样含 Redis、数据库、日志和出流量)
| 方案 | 1,000 万次 | 1 亿次 | 10 亿次 |
|---|---|---|---|
| 阿里云 AI 网关(托管) | 5,428 | 10,195 | 60,518 |
| 阿里云函数计算(单实例并发 100) | 1,419 | 7,703 | 68,877 |
| 腾讯云云函数(默认单并发 / 开请求多并发*) | 3,756 / 1,296 | 32,543 / 7,947 | 321,865 / 75,903 |
| AWS Lambda(128 MB Arm,美东,含平台日志) | 4,142 | 28,874 | 278,460 |
| Google Cloud Run(单实例并发 80 / 250,美东,不含自动请求日志**) | 2,672 / 2,405 | 12,556 / 7,752 | 107,676 / 59,634 |
| Azure Container Apps(每副本 100 条流,美东) | 2,916 | 9,596 | 79,376 |
| OCI Functions(Oracle,美东) | 4,071 | 27,445 | 264,894 |
\* 腾讯云的 SSE 文档写「一个实例同一时刻只处理一条 SSE 连接」,多并发文档又把长连接列为主要用途,只举了 WebSocket 的例子;开多并发那组数按每实例 100 并发、装填率 70% 估算,没实测。\*\* Cloud Run 会自动写请求日志、按 $0.50/GiB 计费,可以用排除过滤器关掉;按每条 1 KB 算,10 亿次时两种并发分别多约 ¥3,100。
一个请求的账、每个规模下最大的几项
按每月 10 亿次时的超量单价,算每百万请求的边际成本:
| Cloudflare 每百万请求 | 美元 | 阿里云深圳每百万请求 | 人民币 |
|---|---|---|---|
| Workers 请求 | 0.30 | 出流量 50 GB × ¥0.75 | 37.5 |
| Workers CPU(10 ms) | 0.20 | ALB 容量单位(处理 50 GB) | 2.45 |
| KV 读 1 次 | 0.50 | SLS 日志 1 GB | 0.40 |
| DO 请求 2 次 | 0.30 | 机器 + Redis + RDS 摊到每百万请求 | 约 4.8 |
| **DO 写 2 行** | **2.00** | | |
| Queues 3 次操作 | 1.20 | | |
| 日志 1 KB | 约 0.27 | | |
| **合计** | **约 4.8(≈ ¥32)** | **合计** | **约 ¥45** |
海外几家按 10 亿次的总价摊到每个请求:AWS 美东约 ¥0.000047,Oracle 美东约 ¥0.000009。
| | 1,000 万次 | 1 亿次 | 10 亿次 |
|---|---|---|---|
| Cloudflare | Queues 55%、Workers 订阅 24% | DO 写行 37%、Queues 29%、KV 读 11% | DO 写行 40%、Queues 25%、KV 读 10% |
| 阿里云自建 · 深圳 | RDS 41%、ECS 25%、流量 24% | **流量 68%**、ECS 13% | **流量 83%**、ECS 7%、ALB 5% |
| 腾讯云自建 · 广州 | MySQL 34%、流量 28%、CVM 22% | **流量 75%** | **流量 89%** |
| AWS 自建 · 美东 | EC2 37%、RDS 32%、流量 10% | **流量 45%**、EC2 27%、RDS 12% | **流量 61%**、EC2 15%、RDS 7%、日志 7% |
| Oracle 自建 · 美东 | MySQL 58%、Redis 19%、机器 19% | MySQL 48%、机器 31% | 机器 33%、**流量 26%**、MySQL 25% |
每个请求的出流量:阿里云深圳的明细和各家的打平点
| 每请求出流量 | 1,000 万次 | 1 亿次 | 10 亿次 | 对照:Cloudflare 10 亿次(常规 / 优化) |
|---|---|---|---|---|
| 5 KB(短问答) | ¥1,243 | ¥2,033 | **¥9,478** | ¥32,574 / ¥25,139 |
| 20 KB | ¥1,371 | ¥3,307 | ¥21,726 | 同上 |
| 50 KB(本文口径) | ¥1,625 | ¥5,854 | ¥45,696 | 同上 |
| 200 KB(带长上下文的 Agent) | ¥2,899 | ¥18,102 | ¥155,788 | 同上 |
| 500 KB(编码 Agent 常见) | ¥5,446 | ¥42,072 | **¥365,488** | 同上 |
和 Cloudflare 打平的点(每请求出流量,对优化写法到常规写法):
- **10 亿次**:深圳约 24–34 KB,香港约 24–37 KB;AWS 美东 15–27 KB,Google Cloud 美东 16–30 KB,Azure 美东 10–22 KB;新加坡出流量更贵,打平点更低。Oracle 美东 336–466 KB,它的账单从 5 KB 到 500 KB 只是 ¥6,562 到 ¥34,518,同样区间 AWS 美东是 ¥18,896 到 ¥236,506。
- **1 亿次**:深圳约 6–14 KB。
- **1,000 万次**:云厂商的保底费已经比 Cloudflare 的全部账单还高,没有打平点。
CPU 时间:Cloudflare 从 5 毫秒到 20 毫秒,10 亿次时的月费从 ¥31,902 变到 ¥33,917。日志按官方平均每条 4.84 KB 算,10 亿次时 Cloudflare 常规写法是 ¥39,622、优化写法 ¥32,188。
延迟原始数据
| 2026-10-09 10:49(北京时间),深圳电信家宽,ICMP 往返 20 次 | 中位数 | P90 |
|---|---|---|
| 阿里云 深圳(ECS API 入口) | 7 ms | 19 ms |
| 阿里云 香港 | 13 ms | 16 ms |
| 腾讯云 广州(CVM API 入口) | 8 ms | 9 ms |
| 腾讯云 香港 | 13 ms | 20 ms |
| Cloudflare 任播地址(博客的 Pages / Functions 入口) | 162–164 ms | 166–186 ms |
同一条线路,11:55 左右再测各家的境外地域(ICMP 20 次,平均值):
| 新加坡 | 平均 | 美东 | 平均 |
|---|---|---|---|
| 阿里云 新加坡 | 57 ms | 阿里云 弗吉尼亚 | 231 ms |
| 腾讯云 新加坡 | 89 ms | AWS us-east-1(S3 入口) | 224 ms |
| AWS ap-southeast-1(S3 入口) | 203 ms | Oracle us-ashburn-1 | 236 ms |
| Oracle ap-singapore-1 | 230 ms | | |
---
## 来源与核对日期
全部在 2026-10-09 核对。
- Cloudflare 价格与限制:
- [Workers 定价](https://developers.cloudflare.com/workers/platform/pricing/)、[Workers 限制](https://developers.cloudflare.com/workers/platform/limits/)
- [Durable Objects 定价](https://developers.cloudflare.com/durable-objects/platform/pricing/)、[DO 设计规则](https://developers.cloudflare.com/durable-objects/best-practices/rules-of-durable-objects/)
- [KV 工作原理](https://developers.cloudflare.com/kv/concepts/how-kv-works/)、[Queues 定价](https://developers.cloudflare.com/queues/platform/pricing/)
- [D1 定价](https://developers.cloudflare.com/d1/platform/pricing/)、[D1 限制](https://developers.cloudflare.com/d1/platform/limits/)
- [Observability 定价(2026-12-01 起)](https://developers.cloudflare.com/observability/pricing/)
- [AI Gateway 定价](https://developers.cloudflare.com/ai-gateway/reference/pricing/)、[AI Gateway 限制](https://developers.cloudflare.com/ai-gateway/reference/limits/)
- [China Network](https://developers.cloudflare.com/china-network/)、[China Network 可用产品](https://developers.cloudflare.com/china-network/reference/available-products/)
- 阿里云(价格来自 aliyun CLI 询价接口,规则来自文档):
- [CDT 公网流量](https://help.aliyun.com/zh/cdt/internet-data-transfers/)、[升级至 CDT 计费](https://help.aliyun.com/zh/cdt/user-guide/upgrade-to-cdt-billing)
- [ALB 计费规则](https://help.aliyun.com/zh/slb/application-load-balancer/product-overview/alb-billing-rules)、[NAT 网关计费](https://help.aliyun.com/zh/nat-gateway/nat-gateway-billing)
- [函数计算计费](https://help.aliyun.com/zh/functioncompute/fc/product-overview/billing-overview-of-fc)、[单实例多并发](https://help.aliyun.com/zh/functioncompute/fc/configure-the-concurrency-of-a-single-instance)
- [AI 网关规格与价格](https://help.aliyun.com/zh/api-gateway/ai-gateway/product-overview/free-product-templates)
- [ESA 函数和 Pages 限制](https://help.aliyun.com/zh/edge-security-acceleration/esa/user-guide/what-is-functions-and-pages)、[ESA 函数计费](https://help.aliyun.com/zh/edge-security-acceleration/esa/user-guide/functions-and-pages-billing)
- [通过私网连接访问百炼](https://help.aliyun.com/zh/model-studio/access-model-studio-through-privatelink)、[私网连接计费](https://help.aliyun.com/zh/privatelink/private-link-billing-description)、[AI 网关 Serverless 计费](https://help.aliyun.com/zh/api-gateway/ai-gateway/product-overview/overview-of-billing-during-the-serverless-public-preview-of-the)
- 腾讯云(价格来自 tccli 询价接口,规则来自文档):
- [公网流量价格](https://cloud.tencent.com/document/product/213/113026)、[CLB LCU 计费](https://cloud.tencent.com/document/product/214/58387)
- [云函数计费](https://cloud.tencent.com/document/product/583/12281)、[云函数 SSE](https://cloud.tencent.com/document/product/583/90617)、[Web 函数请求多并发](https://cloud.tencent.com/document/product/583/123888)
- [EdgeOne 套餐](https://cloud.tencent.com/document/product/1552/94158)、[EdgeOne 超额单价](https://cloud.tencent.com/document/product/1552/94159)、[边缘函数限制](https://cloud.tencent.com/document/product/1552/81344)
- 案例与故障:
- Cloudflare 故障复盘:[2025-06-12](https://blog.cloudflare.com/cloudflare-service-outage-june-12-2025/)、[2025-11-18](https://blog.cloudflare.com/18-november-2025-outage/)
- [阿里云香港可用区 C 故障说明(2022-12)](https://www.alibabacloud.com/zh/notice/resolved_service_outage_in_zone_c_of_the_china_hong_kong_region_1ae)
- [Higress](https://github.com/higress-group/higress)、[阿里云 AI 网关产品页](https://www.aliyun.com/product/apigateway)
- [GreatFire:workers.dev](https://en.greatfire.org/domain/workers.dev)(第三方观测)、[Cloudflare 社区:workers.dev 在大陆被封](https://community.cloudflare.com/t/cloudflare-workers-suspected-of-being-blocked-in-china/382155)
- [Cloudflare 条款更新(2023)](https://blog.cloudflare.com/updated-tos/)、[Cloudflare 日志数据集(平均条目大小)](https://developers.cloudflare.com/observability/logs/datasets/)
- 海外四家(价格来自公开价格接口或官方价目页,规则来自文档):
- AWS:[EC2 按需价(含出流量)](https://aws.amazon.com/ec2/pricing/on-demand/)、[Lambda 定价](https://aws.amazon.com/lambda/pricing/)、[Lambda 响应流式](https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html)、[ALB 空闲超时](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/edit-load-balancer-attributes.html)、[AgentCore Gateway 推理目标](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-targets-inference.html)
- Google Cloud:[网络价格](https://cloud.google.com/vpc/network-pricing)、[Cloud Run 定价](https://cloud.google.com/run/pricing)、[负载均衡超时](https://cloud.google.com/load-balancing/docs/https/request-distribution#timeouts_and_retries)、[Apigee 定价](https://cloud.google.com/apigee/pricing)
- Azure:[带宽价格](https://azure.microsoft.com/en-us/pricing/details/bandwidth/)、[Application Gateway 与 SSE](https://learn.microsoft.com/en-us/azure/application-gateway/use-server-sent-events)、[Application Gateway 502 排查(超时重试)](https://learn.microsoft.com/en-us/troubleshoot/azure/application-gateway/application-gateway-troubleshooting-502)、[API Management 的 AI 网关能力](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities)、[API Management 限制(并发后端连接)](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#api-management-limits)、[Front Door 与 WebSocket / SSE](https://learn.microsoft.com/en-us/azure/frontdoor/standard-premium/websocket)
- Oracle:[价目表(网络)](https://www.oracle.com/cloud/price-list/#pricing-networking)、[VCN 价格](https://www.oracle.com/cloud/networking/virtual-cloud-network/pricing/)、[10 TB 按大区计的新闻稿(2021-11-10)](https://www.oracle.com/news/announcement/oracle-joins-cloudflare-bandwidth-alliance-2021-11-10/)
- 模型价格:[DeepSeek 定价](https://api-docs.deepseek.com/zh-cn/quick_start/pricing)、[通义 qwen-flash](https://help.aliyun.com/zh/model-studio/qwen-flash)
- 工具版本:aliyun CLI 3.5.1、tccli 3.1.180.1、wrangler 4.148.0(npm 上最新是 4.149.0);海外四家没有用 CLI,用的是上文的公开价格接口。
[^scope]: 阿里云用 aliyun CLI 3.5.1 的询价接口,腾讯云用 tccli 3.1.180.1;Cloudflare 没有价格接口,用官方价目页和限制页;海外四家用不登录也能查的公开价格接口(AWS、Azure、Oracle)和 Google 的官方价目页。同一天做过两轮核实,仍查不实的在文中标出。汇率 1 美元 = 6.7153 元(open.er-api.com,2026-10-09)。
[^cpu-price]: 机器按 4 vCPU 的月价折算成每 vCPU 小时,是 100% 用满时的价;国内两家是包月价(含系统盘),海外是按需价。Lambda 按 AWS 文档「1,769 MB 内存约等于 1 个 vCPU」折算。比的是量级,不是精确单价。
[^limits]: Workers 每个 isolate 内存 128 MB,单次 CPU 默认 30 秒、最多可配 5 分钟;Lambda 单次最长 15 分钟、内存最大 10,240 MB;DO 的 WebSocket 休眠期间不收时长费;R2 出流量免费。均为 2026-10-09 在官方页面核对。
[^cpu-ms]: Cloudflare 限制页给的参考:平均每个 Worker 约 2.2 毫秒,做鉴权、解析大包体的重负载通常 10–20 毫秒。
[^cf-wall]: Cloudflare 定价页写明,Standard 计费模式下挂钟时长不收费、也不设上限;限制页说明,等 `fetch()`、KV、数据库的时间不计入 CPU 时间。
[^one-per-instance]: 腾讯云云函数的 SSE 文档写明默认一个实例同一时刻只处理一条 SSE 连接;Web 函数的「请求多并发」文档把长连接列为主要用途,但只举了 WebSocket 的例子,图里「开请求多并发」那组数按每实例 100 并发、装填率 70% 估算,没实测。AWS Lambda 一个执行环境同时只处理一个请求,流式响应要等整个流结束才停止计费,客户端断开也照扣。
[^do]: DO 空闲 10 秒左右就可能休眠。官方给单个对象的软上限约每秒 1,000 次请求,带存储写入时约 200–500 次;设计规则页明确反对用单个 DO 做全局限速。
[^kv]: KV 是最终一致的,其他机房要 60 秒以上才看得到改动。Pydantic 开源的 AI 网关在旧版 README 里承认,用 KV 缓存状态时额度控制只能做成「软上限」。
[^d1]: 按分钟聚合的用量表,每月 10 亿次时约 1.2 个月写满,1 亿次时约 4.6 个月;每个账号可以建 5 万个库。
[^redis]: 阿里云 Tair 双副本 1 GB 每月 ¥76.98;每秒约 4,600 次是按峰值为平均 3 倍估的,没有压测。
[^tos]: Cloudflare 2023 年改条款后,「不能大量传非网页内容」只约束 CDN,开发者平台不受此限([条款更新说明](https://blog.cloudflare.com/updated-tos/))。
[^model]: 模型价格取自 DeepSeek 和通义的官方价目页(2026-10-08 核对)。30 KB 的 prompt 约 7,500 token、回答 300 token 是粗估。
[^latency]: 2026-10-09 深圳电信家宽,每个入口 ICMP 20 次:国内地域和 Cloudflare 取 10:49 的中位数,境外地域取 11:55 的平均值。局域网把 TCP 都做了透明代理(任何地址握手都是 2–3 毫秒),所以用 ping;`mtr` 显示去 Cloudflare 的包在电信 163 骨干上从约 9 毫秒跳到约 160 毫秒。这只是一条线路、一个时段,两天前的晚上用 TCP 握手测 Cloudflare 是 193 毫秒。Azure 和 Google Cloud 的入口是全球任播地址,ping 测不出某个地域,没有列。
[^workers-dev]: Cloudflare 员工在[官方社区](https://community.cloudflare.com/t/cloudflare-workers-suspected-of-being-blocked-in-china/382155)确认过,GreatFire 的第三方观测到 10-07 仍是全部被封。
[^regions]: [OpenAI](https://developers.openai.com/api/docs/supported-countries)、[Anthropic](https://www.anthropic.com/supported-countries) 的支持地区名单,2026-10-09 核对。
[^logs]: Workers Logs 在 2026-12-01 从按条计费改成按 GB 计费(Cloudflare 2026-10-02 的博客公布),本文按新价算,每月 10 亿次时日志费约 $260(旧价 $588)。计量包含 Cloudflare 自动附加的字段,所以「每条 1 KB」只是下限;按平均每条 4.84 KB 算,常规写法每月 10 亿次的总价从约 3.3 万元涨到约 4 万元,日志费从约 $260 涨到约 $5,300。
[^oci-10tb]: Oracle 2021 年的[新闻稿](https://www.oracle.com/news/announcement/oracle-joins-cloudflare-bandwidth-alliance-2021-11-10/)和 2025 年的官方白皮书都写明是每个 regional zone 各一份。
[^appgw]: 这是按排错文档的推论,没有实测。
---
title: "让 Agent 替你管十几个海外社媒号——2026 年 10 月约 60 家发帖、评论、私信 API 与 MCP 逐格横评"
description: "想让 Claude Code 这类 Agent 一键把内容发到 X、LinkedIn、YouTube、Instagram 等多个账号,还能看评论、回私信,能做到哪一步先由平台官方 API 决定,工具只是把这些官方能力搬进一个接口。X 从 2026-02 起只许程序回复 @ 了你的人,LinkedIn 个人号的评论和私信谁都读不到,Facebook 只能发主页,Threads 和 YouTube 没有私信接口。在这个上限之内,我把约 60 家统一 API、社媒管理套件、开源项目和会话型接口按「平台 × 发帖 / 评论 / @ / 私信」逐格核对 2026-10-09 的官方文档,按对接完善度打分排序,价格只放第二列,并写清哪些实测过、哪些只读了文档。"
url: "https://zhuoqidev.com/posts/social-publishing-inbox-apis-2026/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-10-09"
updated: "2026-10-09"
tags: ["社媒自动化","MCP","Claude Code","X API","统一 API","架构选型"]
translation: "https://zhuoqidev.com/en/posts/social-publishing-inbox-apis-2026/index.md"
---
# 让 Agent 替你管十几个海外社媒号——2026 年 10 月约 60 家发帖、评论、私信 API 与 MCP 逐格横评
> **TL;DR** 按对接完善度,面向开发者的统一 API 里 Zernio 以 35.3 分(满分 38)领先,SocialAPI.ai、Ayrshare 各约 29 分;老牌社媒管理套件里能用一个 MCP 同时发帖、读收件箱、回复的只有 Vista Social 和 Eclincher;能多拿到 LinkedIn 个人私信的,只有要你交出 cookie 的会话型接口,而这正是平台明文禁止的。先查平台给了什么,再比工具搬了多少。
**版本范围**:所有产品能力、价格和平台规则都是 2026-10-09 在官方文档、价目页和 API 参考上核对的,价格是美元目录价,不含折扣和活动价。GitHub 项目的星数、最后提交和许可证用 `gh api` 当天实查。这一行变化很快:X 的 API 在 2026 年改了两次规则,Reddit 公开 API 2027-03 关闭,好几家 MCP 是 9 月才上架的。
> [!NOTE]
> **实测边界:这篇大部分是读文档。** 实测过的只有 Zernio 一家(2026-10-09):REST 只读接口、MCP 握手和工具列表、接进 Claude Code,以及用我自己的 X 号发一条帖子、再回复帖子下我自己的一条留言(第七节)。**回复别人的留言会不会被 X 的「被召唤」规则挡住,还没测到**;其余厂商的结论都来自文档。没有任何厂商赞助,文中也没有推荐链接。
---
## 先说结论
1. **能做什么由平台决定。** X 只允许程序回复 @ 了你或引用了你的人(2026-02-23 起),AI 自动生成并发出的回复要先经 X 批准;LinkedIn 个人号能发帖,但评论和私信只开放给法人和合作伙伴;Facebook 个人主页不能用 API 发;Instagram 要切成专业号;Threads、YouTube 平台本身没有私信接口。**换哪家工具都绕不过这些。**
2. **面向开发者的统一 API(19 家)**:Zernio 35.3 分第一:不用自带 X 开发者 key 就能读、回 X 帖子下的回复和 X 私信(实测:读要先打开会持续计费的后台开关),示例的 17 个号全能接,MCP 里有评论和私信工具,有新消息推送。Ayrshare(29.3)覆盖面接近,但要自带 X key、同平台多个号要开多个 profile、价格高 4 倍;SocialAPI.ai(29.5)便宜,但 X 只能发纯文字、没有 Reddit;OmniSocials(23.8)按工作区计价最便宜,但读不到 X 帖子下的回复。
3. **老牌社媒管理套件(23 家)**:能经 MCP 发帖、读收件箱、回复的只有 Vista Social(示例账号约 $217–257/月)和 Eclincher($134–149/月,不支持 Bluesky,部分能力未核实);Agorapulse 的 MCP 发帖只能存草稿;Hootsuite 的 MCP 只能存草稿和只读,而且开着时屏蔽 X、LinkedIn、Reddit、YouTube 的数据;Metricool 的 REST 能回评论和私信,MCP 却没有。
4. **开源自托管**:没有一个做全。Postiz 最流行但没有收件箱;BrightBean Studio 的收件箱工具最全,但不支持 X;OpenPost 支持 X 评论和私信,但才 7 个月大。
5. **会话型接口**(要你的密码或 cookie)能多给的几样,恰好是平台明文禁止的:LinkedIn 个人私信、绕过 X 回复限制、绕过 Instagram 私信 24 小时窗口。
6. **不管选谁,先用真号测一条 X 回复。** 别人在你帖子下的回复算不算「@ 了你」,X 文档没写清,所有厂商都受这条限制。
---
## 这篇写给谁
写给已经在用 Agent(Claude Code、Cursor 或自己写的)做事、想让它顺手接管社媒发帖和回复的工程师。不需要你事先了解各平台的 API;需要知道 MCP 大概是什么(Agent 调外部工具的协议)。
**七个词先说清楚**:
| 词 | 在本文里的意思 |
|---|---|
| 官方 API | 平台公开给开发者的接口,能做什么、收多少钱都由平台定 |
| 授权令牌(OAuth token) | 你在平台授权页点「同意」后,第三方服务拿到的一把钥匙,只能做你授权过的事,随时能在平台设置里撤销 |
| 统一 API | 一家服务把十几个平台的官方 API 包成一套接口,你只对接它一家 |
| 收件箱 | 评论、@ 提及、私信这三类「别人找你」的消息合在一起 |
| 会话型接口 | 拿你的密码或浏览器 cookie 模拟你本人登录,绕过官方 API |
| 被召唤 | X 的规则:只有原帖作者 @ 了你或引用了你,程序才能回复那条帖子 |
| P / C / M / D | 表格里的缩写:发帖 / 读并回评论 / 读并回 @ 提及 / 读并发私信 |
---
## 最小因果链:工具只是搬运工
不管哪家工具,调用都要走这条链:
```mermaid
flowchart TD
AG(["你的 Agent
Claude Code 等"]) --> T["MCP 工具或 REST 接口"]
T --> UNI["统一服务
拿着你授权的令牌"]
UNI --> API["平台官方 API
能做什么由平台定"]
API --> ACC(["你的账号"])
T -.-> SES["会话型服务
拿着你的密码或 cookie"]
SES -.->|"模拟你本人登录,平台条款禁止"| ACC
YOU(["你本人
浏览器或手机 App"]) -->|"登录会话"| ACC
classDef ok fill:#d3f9d8,stroke:#2f9e44
classDef bad fill:#ffe3e3,stroke:#e03131
class UNI,API ok
class SES bad
```
*示意图:画的是调用路径,不是某家厂商的内部实现。实线是官方路线,虚线是会话型接口走的路。*
从这条链能推出三件事,后面整篇都建立在它们上面:
1. **统一服务能做的,一定不超过平台官方 API 给的。** 工具之间比的是「把官方能力搬进来了多少」,以及搬得稳不稳。
2. **想要超出官方的能力,只能走虚线**,也就是交出密码或 cookie。这条路的能力确实多一些,但平台条款明文禁止。
3. **服务调官方 API 和你本人登录是两回事。** 平台安全设置里,「已连接的应用」和「登录设备 / 会话」是两张表。统一服务从它自己的服务器调 API,不算你从别的 IP 登录。
---
## 一、平台给了什么:先查上限
| 平台 | 发帖(P) | 读并回评论(C) | 私信(D) | 前提 / 坑 |
|---|---|---|---|---|
| X | 能 | 能读;**只能回 @ 了你或引用你的人** | 能;已换成加密 X Chat 的私信 API 读不到 | 按量付费,没有免费档;AI 自动回复要 X 事先批准 |
| YouTube | 只能发视频 | 能,每条回复耗 50 配额单位,每天默认 1 万单位 | 平台没有私信 | 未审计项目上传的视频会不会被锁成私享,官方两处写法冲突 |
| Instagram | 能 | 能 | 能,但要对方先发、24 小时内回 | 要切成创作者或商业号(免费) |
| Threads | 能 | 能 | 平台 API 没有 | — |
| Facebook | 只能发主页 | 主页能 | 主页能 | 个人主页一律不行 |
| LinkedIn 个人号 | 能 | **读不到**:读评论的权限只给「选定开发者」 | **不行**:私信 API 只给合作伙伴 | 公司页能读评论 |
| TikTok | 自己申请的应用过不了审计,未审计只能发私密内容;要走已过审的服务 | 商业号 | 商业号;欧盟、英国、瑞士不可用 | 审计规则明说不接受「帮自己或团队账号上传的工具」 |
| Reddit | 能 | 能 | 能 | **新 API 申请 2026-10-31 截止,公开 Data API 2027-03 关闭** |
| Bluesky / Mastodon | 能 | 能 | 能 | 免费、不用审核;Bluesky 只能回先 @ 你的人 |
X 是这里唯一按次收钱的平台(官方价目页,2026-10-09):
| 操作 | 单价 |
|---|---|
| 发一条帖子 | $0.015 |
| 发一条带链接的帖子 | **$0.20** |
| 读一条帖子 | $0.005 |
| 读自己的数据(如自己的 @ 提及) | $0.001 |
| 读一条私信 / 发一条私信 | $0.010 / $0.015 |
首次绑卡送 $20 额度,每月最多读 300 万条帖子。以每月发 30 条(10 条带链接)、读 500 条回复、回 50 条估算,X 部分约 $3–6,带链接的帖子是大头。
---
## 二、怎么打分
为了让比较有个统一口径,我设了一个**示例场景**:一个人在 9 个平台上各有一个个人号和一个品牌号,共 17 个号(X、YouTube、Instagram、Threads、TikTok、Bluesky、Reddit 各 2 个,LinkedIn 是个人号 + 公司页,Facebook 只有品牌主页,因为个人主页接不了)。
| 项 | 满分 | 怎么给 |
|---|---|---|
| 发帖覆盖 | 10 | 能接上并发帖的号 / 17 × 10;X 只能发纯文字的按半个号算 |
| X 帖子下的回复能读能回 | 4 | 单独加权,X 上的互动最依赖它 |
| X 私信 | 2 | |
| 其余 8 个平台的评论 | 8 | 每个平台 1 分 |
| 其余私信 | 5 | Instagram、Facebook、TikTok、Bluesky、Reddit 各 1 分 |
| @ 提及 | 3 | 6 个平台各 0.5 分 |
| MCP | 3 | 评论和私信都有专用工具 3 分;只有评论 2 分;只有一个通用执行工具 1 分 |
| 新消息推送(webhook) | 2 | 新评论、新私信都推 2 分,只推一类 1 分 |
| 同平台多号放在一起 | 1 | 要开多个工作区才能接的不给分 |
只限「经本工具发出的帖子」、只读、beta、文档自相矛盾的,记半分。**这套权重是我定的**;把「X 帖子下的回复」从 4 分降到 1 分,排名前三不变。
---
## 三、面向开发者的统一 API(19 家)
| 名次 | 产品 | 得分 | X 帖子下的回复 | X 私信 | 能接的号(/17) | MCP 有评论 + 私信工具 | 17 个号的月费(第二排序) |
|---|---|---|---|---|---|---|---|
| 1 | Zernio(原 Late) | **35.3** | 能,含不是经它发的帖 | 能 | 17 | 有 | $69 + X 按官方价转嫁 |
| 2 | SocialAPI.ai | 29.5 | 能(自带 X key) | 能 | 15(X 只能发纯文字) | 有 | $29 + 自付 X |
| 3 | Ayrshare | 29.3 | 能(自带 X key) | 能 | 17 | 有(没有 @ 提及) | $299(年付 $249)+ 私信每会话 $0.09 |
| 4 | OmniSocials | 23.8 | **不能** | 能 | 15(没有 Reddit) | 有 | 2 个工作区 $20–24 |
| 4 | Aidelly | 23.8 | 文档矛盾 | 部分 | 15 | 有 | $29–39 |
| 6 | Postproxy | 23.3 | 不能 | 不能 | 15 | 有 | $49 |
| 7 | RelayAPI | 22.0 | 文档不清 | 文档不清 | 17 | 只有通用执行工具 | $5 起 |
| 8 | Outstand | 21.0 | 只限经它发的帖 | 不能 | 17 | 只有评论 | $19 + 自带 X key |
| 9 | Upload-Post | 20.8 | 文档冲突 | 不能 | 15(Reddit 发帖返回 503) | 有 | $16–24 |
| 10 | bundle.social | 19.0 | 文档自相矛盾 | 不能 | 17 | 只有评论 | $90–100 |
第 11–19 名是 UniPost、Mallary、Postqued、PostFast、Blotato、Post for Me、Post Bridge、Unipile、Typefully:要么只做发帖,要么评论只限经它发出的帖子。
前几名逐个平台看(P 发帖、C 评论、M @ 提及、D 私信;½ 是部分支持,? 是文档不清):
| 产品 | X | LinkedIn 个人 | LinkedIn 公司页 | YouTube | Instagram | Facebook 主页 | Threads | TikTok | Bluesky | Reddit |
|---|---|---|---|---|---|---|---|---|---|---|
| Zernio | P C D M½ | P | P C M½ | P C | P C D M½ | P C D | P C | P C D½ | P C D | P C D |
| SocialAPI.ai | P½ C D M½ | P | P C½ | P C | P C D M½ | P C D M | P C M½ | P | P C D M | — |
| Ayrshare | P C D M½ | P C? | P C | P C½ | P C D M½ | P C D | P C? | P C | P C½ | P C¼ |
| OmniSocials | P D | P | P C M | P C | P C D M | P C D M | P C M | P C | P | — |
| Postproxy | P | P | P | P C | P C D | P C D | P C | P C | P C½ D | — |
几个容易漏看的地方:
- **「自带 X key」是两种计费方式**:Zernio、bundle.social 用它们自己的 X 应用,按 X 原价转嫁;Ayrshare(2026-03-31 起强制)、SocialAPI.ai、Outstand 要你自己去 X 开发者后台开应用、绑卡、自付。自带 key 读自己的 @ 提及能拿到 $0.001 的价,Zernio 转嫁的读取是 $0.005/次。
- **OmniSocials 的定价方式不一样**:一个工作区 $10–12/月、账号数不限,但每个工作区每个平台只能接 1 个号;X 不带链接的帖子它替你付了。它的收件箱明确写着「X 上的评论和 @ 提及不在收件箱里」。
- **新厂商的文档一致性普遍偏弱**:Aidelly、bundle.social、Upload-Post 都有营销页说有、接口文档说没有的情况;RelayAPI 文档里的 MCP 安装命令,装到的是别人发布的同名 npm 包,有供应链风险。SocialAPI.ai 是一个工程师的个人公司,没有公开的更新日志。
- **Zernio 也有坑**:X 的所有读取默认关着,要打开会持续计费的后台开关(第七节实测);X 评论列表只翻第一页,回复线程缓存 2 分钟,评论列表最长延迟 10 分钟;Threads 没有新评论推送;TikTok 私信只能回、不能发起。
---
## 四、老牌社媒管理套件(23 家)
这一类有成熟的网页收件箱,问题在于收件箱能不能被程序驱动:
| 产品 | Claude Code 怎么接 | 发帖 | 评论 读 / 回 | 私信 读 / 回 | 示例 17 个号的月费(月付 / 年付) |
|---|---|---|---|---|---|
| Vista Social | 官方 MCP | 能直接发 | 能 / 能 | 能 / 待确认(回复接口写的是「公开回复」) | $257 / $217 |
| Eclincher | 官方 MCP | 能直接发 | 能 / 能 | 能 / 能(X 私信未核实) | $149 / $134(不支持 Bluesky) |
| Agorapulse | 官方 MCP(beta) | **只能存草稿** | 能 / 能 | 能 / 能 | $282 / $247(X 每号要 $69 的附加包) |
| Metricool | 发帖走 MCP,收件箱走 REST | 能 | REST 能(X 帖子下的回复不进收件箱) | REST 能(含 X 私信) | $87 / $73 |
| Statusbrew | MCP | 只能存草稿 | 只读 | 只读 | $429 / $359 |
| Hootsuite | MCP + REST | REST 能发,MCP 只存草稿 | MCP 只读,**且屏蔽 X、LinkedIn、YouTube、Reddit** | 同左 | $199 起(按人头,年付) |
| Sprout Social | REST(要找销售开);MCP 只给 ChatGPT | 只能存草稿 | API 只读 | API 只读 | 约 $399 |
| SocialPilot | 官方 MCP | 能直接发 | 不能 | 不能 | $50 / $42.5(最便宜的纯发帖) |
| Buffer / Publer / Zoho Social | MCP 或 REST | 能 | 不能(只在网页里) | 不能 | $59–98 |
Later、Loomly、SocialBee、Sendible 没有公开 API 也没有 MCP,直接淘汰;Brandwatch、Khoros、Emplifi、Sociality.io、NapoleonCat 的 API 要签企业合同。
> [!WARNING]
> Vista Social 和 Metricool 都说能把 **LinkedIn 个人号的评论**收进收件箱,这和 LinkedIn 官方文档(读个人帖评论的权限只给「选定开发者」)冲突。可能它们是获批的合作伙伴,但我没查到证据,按「厂商说法、未核实」处理。
---
## 五、开源自托管与 MCP 目录里的新厂商
| 项目 | 现状(2026-10-09) | 收件箱 | MCP 里的收件箱工具 | 坑 |
|---|---|---|---|---|
| Postiz | 36,914★,AGPL-3.0,最新 v2.25.0 | **没有**,MCP 文档明说不能读、回评论 | 无 | 2026 年 18 个安全公告,含多个无需认证的严重漏洞 |
| Mixpost Pro | 闭源,$299 一次性 | 有(FB、IG、Threads、X 的评论和 @ 提及) | 无,只能在网页里回 | — |
| BrightBean Studio | 2,421★,AGPL-3.0,有免费托管版 | 有,评论、@ 提及、私信 | **最全**:列消息、起草、发送分开授权 | **不支持 X** |
| OpenPost | 659★,2026-03 才建 | 有,含 X 评论和私信 | 只有评论,私信只在 HTTP 接口里 | 托管版自己写着「还没完成最终实测」 |
| Chatwoot | 37,646★ | 只有私信,X 渠道还没合并 | 只有社区版 | 不发帖 |
在官方 MCP 注册表、Smithery、Glama 和 Anthropic 连接器目录里,还找到几家 9 月前后才上架、带收件箱工具的:Ignix(174 个工具,示例 17 个号约 $121/月,私信支持哪些平台它的帮助页和落地页说法矛盾)、PostLake(收件箱接口最规整,但 X、Instagram、Facebook、Threads 还没对公众开放)、Pinlyx(只有私信)、SocialClaw(只有 Instagram)。覆盖面都不如第三节的前几名。
---
## 六、会话型接口:多出来的能力,正好是违规的那部分
Unipile、Linked API、Beeper、twitterapi.io 这一类,要你在它们的页面上登录账号,或交出浏览器 cookie。能比官方多拿到的只有四样:
| 多出来的能力 | 平台怎么说 |
|---|---|
| LinkedIn 个人号私信、个人帖评论 | 用户协议 8.2 禁止「机器人或其他未经授权的自动化方式」 |
| Instagram 私信不受「对方先发、24 小时」限制 | 使用条款禁止未经授权的自动化访问 |
| X 回复不受「被召唤」限制,价格约为官方的 1/10 | 自动化规则:非 API 的自动化可能永久封号 |
Unipile 文档自己承认部分接口是非公开的、账号受限是「预期中的失败」;Beeper 文档提醒发消息太多可能被封号。**如果账号对你重要,这一类别碰。**
---
## 七、一条评论的完整路径(以 Zernio 为例)
拿排名第一的 Zernio 走一遍:有人在你的 X 帖子下回复,到你的回复发出去,中间发生了什么。
```mermaid
flowchart TD
A(["有人在你的 X 帖子下回复"]) --> B["① 统一服务从 X 拉评论
X 收件箱要先手动打开,每次读按 X 价 $0.005"]
B --> C["② Agent 调 MCP 列出评论
先列有评论的帖子,再取某条帖子的评论"]
C --> D["③ Agent 起草回复"]
D --> E["④ 你确认后才发"]
E -->|"改"| D
E -->|"发"| F["⑤ 调回复工具
X 按一条帖子收 $0.015"]
F --> G["⑥ X 检查是否「被召唤」
不算就拒绝,待实测"]
```
*示意图:步骤来自 Zernio 文档和 X 价目页;第 ④ 步是我加的人工确认,用来满足 X「AI 自动回复要事先批准」的规则。只有 ② 里的工具存在性是实测过的。*
**实测过的部分**(2026-10-09,免费):先在 Zernio 后台建 API key,再用一行命令把 MCP 接进 Claude Code:
```bash
claude mcp add --transport http -s user zernio https://mcp.zernio.com/mcp --header "Authorization: Bearer <你的 Zernio API key>"
```
`claude mcp list` 显示已连接,但**当前会话里看不到这些工具,要开新会话才出现**。为了不等新会话,我直接按 MCP 协议握手,先 `initialize`,再列工具:
```http
POST https://mcp.zernio.com/mcp
Authorization: Bearer <你的 Zernio API key>
Content-Type: application/json
Accept: application/json, text/event-stream
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
```
返回结果(摘要,不是原文):
| 观察 | 说明 |
|---|---|
| 直接列出 52 个工具 | 发帖 13 个(`posts_create`、`posts_cross_post`、`posts_publish_now` 等)、账号、排期、数据分析 |
| 评论工具在列表里 | `comments_list_inbox_comments`、`comments_get_inbox_post_comments`、`comments_reply_to_inbox_post` |
| @ 提及工具在列表里,但范围窄 | `mentions_list_inbox_mentions` 目前只返回 LinkedIn 公司页;`mentions_reply_to_mention` 只支持 Instagram |
| **私信工具不在列表里** | 要先调 `search_tools` 搜,再用 `call_tool` 调;搜到 `messages_list_inbox_conversations`、`messages_send_inbox_message`、`messages_create_inbox_conversation` |
最后这条在文档里没写:如果 Agent 只看工具列表,会以为 Zernio 没有私信功能。
### 用真号走一遍(2026-10-09 晚)
我把自己的 X 号连上 Zernio,发了一条测试帖,在帖子下用同一个号留言,再让 Agent 回复这条留言:
| 步骤 | 怎么做 | 结果 |
|---|---|---|
| 连 X 号 | Zernio 后台授权 | 文档写明连第一个 X 号前必须先绑卡(X 按次收费,要能转嫁给你);连上后状态健康、可以发帖 |
| 发帖前检查 | `validate_post` | 免费,没有问题 |
| 发一条纯文字帖 | `posts_create`,立即发布 | 成功,约 $0.015 |
| 读帖子下的留言 | `comments_get_inbox_post_comments` | **403**,`X_INBOX_NOT_ENABLED` |
| 换成搜索、按编号查推文 | `twitter_engagement_search_tweets`、`twitter_engagement_get_tweet` | **403**,`X_ANALYTICS_NOT_ENABLED` |
| 回复指定留言 | REST `POST /v1/posts`,`platformSpecificData.replyToTweetId` 填留言编号 | 成功,约 $0.015;用 X 的公开嵌入接口核对,回复确实挂在那条留言下 |
从这一轮看出四件事:
- **Zernio 上 X 的所有「读」默认都关着。** X 账号设置里有两个开关:收件箱(同步帖子下的回复、轮询私信)和数据分析(定期读帖子数据)。两个都关时只能发帖、删帖,读留言、搜索、按编号查推文都回 403。打开后 Zernio 会在后台定期调 X 的接口,每次按 X 价计费,**多久调一次文档没写**。想长期用它看 X 评论,要把这笔持续费用算进去,后台可以设 X 月度花费上限。
- **MCP 的发帖工具不能指定回复哪条推。** `posts_create` 没有这个参数,要用 REST 接口,或用 `search_tools` 找到的完整工具。
- **自己回复自己的留言没被拦。** 回复别人的留言算不算「被召唤」,这一轮没测到。
- 两条帖子发完,账单接口仍显示 $0,可能有延迟,最终扣费以月底账单为准。
**这套流程换到别的服务上一样成立**,因为收件箱的操作就这四步,各家只是名字不同:
| 步骤 | Zernio | OmniSocials | Ayrshare |
|---|---|---|---|
| 列出待处理 | `comments_list_inbox_comments` | `get_next_unanswered`(直接给下一条没回的) | `get_comments` |
| 读上下文 | `comments_get_inbox_post_comments` | `get_inbox_conversation`(带原帖) | `get_comments` |
| 回复 | `comments_reply_to_inbox_post`(支持幂等键,重试不会发两次) | `reply_to_inbox` | `reply_comment` |
| 发私信 | `messages_send_inbox_message` | `reply_to_inbox` | `send_message` |
自己写的话,记住三点:列表要有「已处理」记录(轮询会把同一条评论拉到两次);回复要带幂等键;发出前留一步人工确认。
---
## 八、会不会封号
- **统一服务调官方 API,不算你异地登录。** 封号风险来自行为,不来自工具本身:X 禁止多个账号发相同或高度相似的内容(翻译成另一种语言的可以)、禁止自动回复没 @ 你的人、禁止自动点赞和批量关注;LinkedIn 对重复内容直接拒发;Meta 把重复内容当垃圾信号。
- **多 IP 影响的是你本人登录。** 频繁换国家的代理节点登录,X 会要求验证或临时锁号,Facebook / Instagram 会弹安全验证,LinkedIn 会临时限制账号。这部分平台官方没写细节,来自第三方资料。固定一个出口节点、开两步验证就能避开大部分麻烦。
- **一人多号有上限**:X 一个人最多 10 个号,每个号用途要不同,违规可能被要求只留一个、其余封掉;LinkedIn 用户协议写明一个人只能有一个账号,品牌那个要用公司页;Facebook 个人账号也只能有一个,品牌用主页。
- **停用一家服务时,去平台的「已连接应用」里手动撤销授权。** 有用户反馈在服务里断开后,授权并没有收回。
---
## 九、费用怎么算(放在第二位)
| 计价方式 | 代表 | 示例 17 个号 |
|---|---|---|
| 按连接的号数累进 | Zernio:前 2 个免费,第 3–10 个每个 $6,第 11–100 个每个 $3 | $69 |
| 按工作区,账号数不限,每个平台只能接 1 个号 | OmniSocials:每个工作区 $10–12 | 个人、品牌两个工作区 $20–24 |
| 按 profile 档位 | Ayrshare:Launch 档 | $249–299 |
| 按套餐或人头 | Vista Social、Hootsuite、Sprout | $200–400 |
X 的接口费几乎每家都另算:要么按原价转嫁,要么要你自带 key 自己付,要么按积分折算;SocialPilot 和 Upload-Post 把它包进套餐,Upload-Post 的代价是默认删掉帖子里的链接。
---
## 十、什么时候选哪一类
```mermaid
flowchart LR
%% layout: tree
Q(["你最在意什么?"])
Q --> A["在 X 上读、回帖子下的回复
→ Zernio;或 SocialAPI.ai、Ayrshare(要自带 X key)
先用真号测「被召唤」规则"]
Q --> B["网页收件箱 + 团队协作 + 老牌厂商
→ Vista Social(贵)、Eclincher
Agorapulse 的 MCP 只能存草稿"]
Q --> C["号多,主要在 IG、Threads、YouTube、LinkedIn 公司页
X 只发不回 → OmniSocials(按工作区计价)"]
Q --> D["只发帖、不管回复
→ SocialPilot、Buffer、Post for Me"]
Q --> E["想全部自己掌控
→ 开源:BrightBean(不支持 X)、OpenPost(很新)"]
Q --> F["必须程序化处理 LinkedIn 个人私信
→ 没有合规方案,手动处理"]
classDef q fill:#fff3bf,stroke:#f08c00
classDef pick fill:#d3f9d8,stroke:#2f9e44
class Q q
class A,B,C,D,E pick
```
*示意图:按 2026-10-09 的文档整理,不是实测排名。*
---
## 事实、推断和没核实的
- **实测过**:Zernio REST 只读接口返回 200;MCP 握手后列出 52 个工具,私信工具要经 `search_tools` 找;Claude Code 接上后要开新会话才看到工具;用真号发帖、回复指定留言成功,X 的读取在两个开关关着时全部 403。
- **官方文档原文**:各平台的能力上限、X 的价格和「被召唤」规则、各家的价格、MCP 工具名。
- **我的推断**:Vista Social、Metricool 能读 LinkedIn 个人评论,可能是因为合作伙伴权限;评分权重。
- **没核实、要实测的**:
- 别人在你帖子下的回复算不算「被召唤」(自己回自己已测通);
- Zernio 打开收件箱 / 数据分析开关后,后台多久调一次 X、每月多少钱;
- Vista Social 能不能回私信;
- Eclincher 的 X 私信;
- Ignix、Aidelly 互相矛盾的文档;
- YouTube 未审计项目上传会不会被锁私享。
---
**平台给的是天花板,工具比的是搬了多少;会话型接口多给你的那一截,恰好是平台不让你用的。**
---
## 来源(2026-10-09 查)
- X:[价目页](https://docs.x.com/x-api/getting-started/pricing) · [更新日志](https://docs.x.com/changelog.md) · [发帖接口与回复限制](https://docs.x.com/x-api/posts/manage-tweets/introduction.md) · [平台操纵与多账号政策(存档)](https://archive.ph/Ga9ef)
- LinkedIn:[用户协议](https://www.linkedin.com/legal/user-agreement) · [评论 API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/comments-api) · [消息 API](https://learn.microsoft.com/en-us/linkedin/shared/integrations/communications/messages) · [禁用的第三方软件](https://www.linkedin.com/help/linkedin/answer/a1341387)
- Meta:[Instagram 评论管理](https://developers.facebook.com/docs/instagram-platform/comment-moderation) · [Instagram 私信 API](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/messaging-api) · [Threads API](https://developers.facebook.com/docs/threads/overview) · [Meta 服务条款](https://www.facebook.com/terms/)
- YouTube:[配额](https://developers.google.com/youtube/v3/determine_quota_cost) · [videos.insert](https://developers.google.com/youtube/v3/docs/videos/insert)
- TikTok:[内容分享审计规则](https://developers.tiktok.com/doc/content-sharing-guidelines)
- Reddit:[Responsible Builder Policy](https://support.reddithelp.com/hc/en-us/articles/42728983564564-Responsible-Builder-Policy) · [Data API 关闭时间表(PPC Land)](https://ppc.land/reddit-to-halt-public-data-api-access-in-march-2027-as-14-000-apps-register/)
- Bluesky:[机器人指南](https://docs.bsky.app/docs/starter-templates/bots)
- Zernio:[价目](https://zernio.com/pricing.md) · [llms.txt](https://zernio.com/llms.txt) · [X 收件箱](https://docs.zernio.com/platforms/twitter/inbox) · [服务条款](https://zernio.com/tos)
- OmniSocials:[价格](https://omnisocials.com/pricing) · [收件箱](https://docs.omnisocials.com/inbox) · [X 说明](https://docs.omnisocials.com/platforms/x)
- Ayrshare:[价格](https://www.ayrshare.com/pricing/) · [评论 API](https://www.ayrshare.com/docs/apis/comments/overview)
- SocialAPI.ai:[价格](https://social-api.ai/pricing) · [文档](https://social-api.ai/llms-full.txt)
- Vista Social、Hootsuite、Agorapulse、Metricool 等套件:[Hootsuite MCP](https://www.hootsuite.com/integrations/mcp) · [Metricool MCP](https://help.metricool.com/how-to-connect-metricools-mcp-with-claude-0l84v.md)
- 开源:[Postiz](https://github.com/gitroomhq/postiz-app) · [Postiz MCP](https://docs.postiz.com/mcp/introduction) · [BrightBean Studio](https://github.com/brightbeanxyz/brightbean-studio) · [OpenPost](https://github.com/getopenpost/openpost)
- MCP:[安全最佳实践](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices)
---
title: "让 Agent 自己验收、自己整理记忆——Outcomes 与 Dreaming 的文档拆解,附 Claude Code /goal 实测"
description: "Outcomes 让一个看不到写手思路的独立评审按评分标准逐条验收,不合格就返工;Dreaming 在会话之间读旧记忆和历史会话,另外生成一份整理好的记忆库,原库不动。这两个都是 Claude 开发者平台 Managed Agents 的接口,Claude Code 里没有。本文按官方文档讲清它们为什么必要、怎样工作,再找出 Claude Code 里的近亲(/goal 和没放开的 auto-dream),用同一个任务把 /goal 实测了 12 次,最后把设计翻译成能搬到任何 Agent 技术栈的两份契约。"
url: "https://zhuoqidev.com/posts/managed-agents-outcomes-dreaming/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-10-06"
updated: "2026-10-09"
series: "Agent 架构深度"
tags: ["Claude","Claude Code","Managed Agents","AI Agent","Agent 架构","Outcomes","Dreaming","Agent Memory","LLM-as-Judge","Prompt Injection"]
translation: "https://zhuoqidev.com/en/posts/managed-agents-outcomes-dreaming/index.md"
---
# 让 Agent 自己验收、自己整理记忆——Outcomes 与 Dreaming 的文档拆解,附 Claude Code /goal 实测
> **TL;DR** Agent 很擅长交出「看起来做完了」的东西,也很擅长把记忆越记越乱。Anthropic 在 Claude Managed Agents 里给这两个问题各配了一个机制:Outcomes 在一次会话里请一位独立评审逐条验收,Dreaming 在两次会话之间把记忆重新整理一遍。但在平时用的 Claude Code 里,你看不到这两个名字。本文先按文档拆开这两个机制,再讲 Claude Code 里实际能用到什么、实测效果如何,以及怎样在自己的 Agent 里复刻。
**版本范围**:本文依据 2026-10-06 核对的官方文档,2026-10-09 补充了 Claude Code 2.1.294 上的核对和实测。Outcomes 和记忆库处于公开 beta;Dreaming 是研究预览,要先申请才能用。两者都在 2026-05-06 的 Code with Claude 大会上发布。beta 阶段字段和限额都可能变,接入前以当时的文档为准。
> [!WARNING]
> **先说清哪些是实测、哪些只是读文档。** Outcomes 和 Dreaming 是 Claude 开发者平台上 Managed Agents 的接口,要用 API 余额付费,**Claude Code 里没有这两个功能**。这两部分我没有亲自跑:Outcomes 需要充值 API 余额,这次没花这笔钱;Dreaming 的研究预览没开给我的账号。所以前半篇的请求、事件和三轮返工案例全部来自官方文档和 cookbook。我实测的是 Claude Code 里的近亲:`/goal` 用同一个任务跑了 12 次,用一套写手和评审都看不到的隐藏测试打分;auto-dream 则核对了它为什么在我的账号上从没运行过。见「[Claude Code 里能看到什么](#claude-code-里能看到什么)」和「[实测:/goal 的评审到底起了多大作用](#实测goal-的评审到底起了多大作用)」。
> [!NOTE]
> **最重要的一句提醒:Outcomes 的效果几乎完全取决于评分标准怎么写。** 标准写得含糊,评审会什么都放行,循环一轮就结束,你付了钱却什么都没检查。
---
## 这篇文章写给谁
默认读者是这样的工程师:知道 LLM、上下文窗口和基础的工具调用,做过或配置过 Agent,平时用 Python、TypeScript、Go 之类的语言,但没用过 Claude Managed Agents。
你**不需要**事先知道:
- Managed Agents 的 API 结构和 SDK 写法;
- 「grader」「rubric」「memory store」这些名字,下文第一次出现时都会解释;
- 任何 Anthropic 内部实现。Anthropic 没有公开这两个机制的内部代码,本文所有「怎样实现」的部分都标明了是官方文档写明的行为,还是我的推断,还是示意伪代码。
---
## 先只记住三句大白话
1. **Outcomes 解决「这一次交付合不合格」。** 你写下合格的标准,平台另起一个评审,它看不到干活那个 Agent 的思路,只拿着标准逐条检查产出物;不合格就把逐条意见交回去返工,直到合格或者用完次数。
2. **Dreaming 解决「下一次能不能做得更好」。** 它在会话之外读一份旧记忆和最多 100 段历史会话,产出一份**新的**整理过的记忆库:重复的合并、过时的换成最新值、补上单次会话看不出的规律。旧记忆库一个字都不改。
3. **两者共用一个底座:记忆库。** 它就是一组文本文件,挂进 Agent 的沙箱,Agent 用普通的读写文件工具来用它,每次写入都留一个不可改的版本。
| | 记忆库 | Dreaming | Outcomes |
|---|---|---|---|
| 什么时候运行 | 会话进行中,Agent 随手读写 | 两次会话之间,离线异步 | 一次会话之内 |
| 解决什么 | 会话之间默认失忆 | 增量写入越积越乱 | 「看起来做完了」不等于做对了 |
| 输入 | Agent 的文件读写 | 一份旧记忆库 + 1 到 100 段会话记录 | 任务描述 + 评分标准 + 最大轮数 |
| 输出 | 记忆文件和它们的历史版本 | 一份新的记忆库 | 五种结果之一 + 评审的逐条说明 |
| 发布状态(2026-10-06) | 公开 beta | 研究预览,需申请 | 公开 beta |
---
## 从约束推出这两个机制
先别看 API,从 Agent 躲不开的几条约束往下推。
**约束一:上下文有限,而且会话结束就清空。** Managed Agents 的会话默认从空白上下文开始,结束时攒下的东西全部丢掉。想让 Agent 记住用户偏好、项目约定、上次踩过的坑,就只能把它们写到上下文之外,这就是记忆库。
**约束二:增量写入会让记忆越来越乱。** 每次会话只看得到眼前的事,于是它往记忆里追加一条「部署前要先跑迁移」,下一次又追加一条意思差不多的;某个配置改了,旧条目没人删。官方文档原话是,多次会话后记忆库会积累重复、矛盾和过时的条目。记忆本身不会变整齐,**需要一个能同时看到很多次会话的角色定期整理**,这就是 Dreaming。
**约束三:模型给自己打分不可靠。** 写手在自己的上下文里检查自己的产出,看到的是自己刚写下的思路。它会在「自认为」完成时宣布完成,不会回头重新打开一个已经引用过的链接,也不会发现自己记住的引文和原文差了几个字。要可靠地检查,**检查者必须看不到写手的思路,手里只有标准和产出物**,这就是 Outcomes 的独立评审。
**约束四:循环必须有出口。** 评审永远能挑出毛病,所以返工必须有上限,标准自相矛盾时还要能直接判失败,不能空转。
把四条串起来,最小的因果链是:
```mermaid
flowchart LR
A["上下文有限
会话结束即清空"] --> B["记忆写到上下文之外
(记忆库)"]
B --> C["增量写入
重复、矛盾、过时"]
C --> D["跨会话整理
(Dreaming)"]
E["模型自评有偏差"] --> F["上下文隔离的评审
+ 可检查的标准"]
F --> G["有上限的返工循环
(Outcomes)"]
```
*图:概念示意,表达因果关系,不是 Anthropic 的内部架构。*
哪些是必须保留的、哪些可以替换:
| 必须保留(设计本身) | 可以替换(具体选择) |
|---|---|
| 评审的上下文和写手隔离 | 评审用哪个模型、用不用同一个模型 |
| 标准写成可逐条检查的条目 | 标准用 markdown 还是 JSON |
| 返工次数有上限,标准不适用时能判失败 | 上限具体是 3 还是 5 |
| 整理记忆时不改原件,结果先审后用 | 原件存在 Anthropic、Git 还是数据库 |
| 每次写入记忆都留版本 | 版本靠托管服务还是靠 Git |
---
## 全文只需要认识五样东西
| 大白话名字 | 官方名字 | 是什么 | 属于哪一层 |
|---|---|---|---|
| 会话 | session | Agent 一次干活的完整过程,有自己的沙箱和事件流 | 运行时 |
| 记忆库 | memory store | 一组文本文件,挂进沙箱的一个目录,跨会话保留 | 托管存储 |
| 评分标准 | rubric | 一份 markdown,逐条写明合格的样子和怎样验证 | 你写的输入 |
| 评审 | grader | 平台自动拉起的另一个 Agent,独立上下文,只按评分标准检查 | 运行时 |
| 整理任务 | dream | 一个异步任务:读旧记忆和历史会话,写出新记忆库 | 托管任务 |
下文正文尽量用左边的大白话,官方名字只出现在请求示例和来源里。
---
## 底座:记忆库怎样工作
**它在系统里的位置**:建会话时把记忆库作为一个资源挂上去,平台把它挂到沙箱里 `/mnt/memory/` 下的一个目录,并自动在系统提示里加一段说明,告诉 Agent 这个目录叫什么、放的是什么、能不能写。Agent 用和读写其他文件完全一样的工具读写它,没有专门的「记忆工具」。
几条官方写明的规则:
- **每次改动都生成一个不可改的版本**,可以审计、回滚到某个时间点,也可以从历史里抹掉敏感内容。
- **限额**:单条记忆最大 100 kB(约 2.5 万 token),一个记忆库最多 1 万条,一个会话最多挂 8 个记忆库。官方建议拆成很多小文件,而不是几个大文件。
- **只能在建会话时挂**,会话跑起来以后不能增删。
- **权限分读写和只读**,默认是读写,在文件系统层面强制执行。
- 记忆库相关接口用 `agent-memory-2026-07-22` 这个 beta 头,**不能**和会话接口的 `managed-agents-2026-04-01` 同时发,同时发会报 400;挂到会话上这一步仍然用会话接口的头。
建会话并挂上一个记忆库,完整的最小请求(取自官方文档):
```bash
curl -s https://api.anthropic.com/v1/sessions \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
--data @- < [!CAUTION]
> **默认的读写权限是一个注入通道。** 官方文档自己写明:如果 Agent 处理不可信的输入(用户给的提示词、抓来的网页、第三方工具的输出),一次成功的提示词注入就可能把恶意内容写进记忆库,**之后的会话会把它当作可信的记忆来读**。参考资料、共享查询表这类不需要改的记忆库,一律挂成只读。
---
## Outcomes:会话内的「写手 + 评审」循环
### 一次循环发生了什么
用产品语言说一遍:
1. 你不发普通的用户消息,而是发一个「定义目标」事件,里面有三样东西:给写手看的**任务描述**、给评审看的**评分标准**、**最多返工几轮**。Agent 收到就开工,不需要再发别的消息。
2. 写手干完一轮,平台拉起一个**全新的评审**。它和写手用同一个模型、同一套工具,但上下文是独立的:看不到写手的思路,不知道写手走了哪些捷径,手里只有评分标准和产出物。
3. 评审逐条给结论。全部通过,会话结束;有不通过的,评审的说明原样交回写手,写手改完,平台再拉起一个新的评审从头查一遍。
4. 到达轮数上限还没通过,写手再做最后一轮收尾,不再评审,会话结束。
```mermaid
sequenceDiagram
participant U as 你的程序
participant P as 平台
participant W as 写手(会话里的 Agent)
participant G as 评审(每轮新开)
U->>P: 定义目标:任务描述 + 评分标准 + 最多几轮
P->>W: 任务描述,开工
loop 每一轮
W->>W: 干活,产出物写进 /mnt/session/outputs/
P->>G: 拉起新评审,只给评分标准和产出物
G->>G: 逐条检查(可以用全部工具去核实)
alt 全部通过
G-->>P: satisfied
P-->>U: 会话空闲
else 有不通过
G-->>P: needs_revision + 逐条说明
P->>W: 逐条说明原样交回
end
end
Note over P,G: 到上限仍未通过:max_iterations_reached,
写手做最后一轮收尾,不再评审
```
*图:依据官方文档描述的行为绘制的简化时序;评审的内部推理不对外公开。*
### 完整的请求和事件
定义目标的事件(取自官方文档,`max_iterations` 可省略,默认 3,最大 20):
```json
{
"events": [
{
"type": "user.define_outcome",
"description": "Build a DCF model for Costco in .xlsx",
"rubric": { "type": "text", "content": "# DCF Model Rubric\n..." },
"max_iterations": 5
}
]
}
```
这个请求发到 `POST /v1/sessions/{session_id}/events`。评分标准也可以先通过文件接口上传,然后写成 `{"type": "file", "file_id": "..."}`,方便多个会话复用、像代码一样评审。也可以在建会话时直接把这个事件放进 `initial_events`,一次请求建会话并开工。
之后从事件流里能看到三种评审事件:
```json
{ "type": "span.outcome_evaluation_start", "outcome_id": "outc_01a...", "iteration": 0 }
{ "type": "span.outcome_evaluation_ongoing", "outcome_id": "outc_01a...", "iteration": 0 }
{
"type": "span.outcome_evaluation_end",
"outcome_id": "outc_01a...",
"result": "satisfied",
"explanation": "All 12 criteria met: revenue projections use 5 years of historical data, ...",
"iteration": 0,
"usage": { "input_tokens": 2400, "output_tokens": 350, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 1800 }
}
```
*上面三条为便于阅读删去了 `id`、`processed_at` 等字段,字段值是官方文档里的示例值。*
`iteration` 从 0 开始数:0 是第一次评审,1 是第一次返工后的评审。中间的 `ongoing` 只是心跳,**评审在想什么你看不到**,只能看到最后的 `explanation`。
结束事件里的 `result` 决定接下来发生什么:
| 结果 | 接下来 |
|---|---|
| `satisfied` | 会话进入空闲 |
| `needs_revision` | 写手开始新一轮 |
| `max_iterations_reached` | 写手再做一轮收尾,不再评审,然后空闲 |
| `failed` | 会话进入空闲。评分标准和产出物对不上时返回,比如任务描述和评分标准互相矛盾 |
| `interrupted` | 目标进行中被你中断(发 `user.interrupt`) |
同一时间只能有一个目标,但可以串起来:上一个目标结束后再发一个「定义目标」事件。目标结束后会话照样能继续对话,历史都在。产出物写在沙箱的 `/mnt/session/outputs/`,用文件接口按会话 ID 列出来下载。
### 官方 cookbook 里的三轮返工
下面这个例子是 Anthropic 在 cookbook 里跑过并公开记录的,不是我跑的。他们让写手写一页美国直流快充站单位经济的研究简报,最多引 6 个来源,每个来源附一句原文引文。评分标准有两部分:
- **覆盖清单 7 项**,每项都比任务描述更具体。比如「运营商财务」一项要求:必须是某家上市充电运营商最近一份 10-K 或 10-Q 里的 GAAP 净利润或净亏损,**引用必须是 sec.gov 上的申报文件本身,不能是新闻稿、业绩会纪要或新闻报道**。
- **引用核查 3 步**:每个链接必须真的能用网页抓取工具直接打开(不许拿镜像、转载或搜索摘要代替);在页面里搜引文原文;判断引文是否真的支撑它被引用的那句话。
评审实际跑出来的三轮:
| 轮次 | 评审结论 | 抓到的问题 | 写手怎么改 |
|---|---|---|---|
| 第 0 轮 | 覆盖 5/7 | 需求电费只有定性描述,没有 $/kW 数字;EVgo 的净亏损引自一个第三方新闻网站 | 补上 $20/kW;去 sec.gov 找 EVgo 2024 财年的数字 |
| 第 1 轮 | 覆盖 6/7,引用 6/6 | 新引用是 **8-K 的附件 99.1**,也就是一份提交给 SEC 的业绩新闻稿,不是 10-K | 去 EDGAR 找到真正的 10-K |
| 第 2 轮 | `satisfied`,7/7 | 无 | 共 3 轮,耗时 12 分 56 秒 |
第 1 轮最值得看。8-K 附件 99.1 也在 sec.gov 上,一个只检查「链接是不是 sec.gov」的评审会放过它。它被抓住,是因为评分标准写的是「必须是 10-K 或 10-Q,不能是新闻稿」,评审只能打开文件去确认它到底是什么。
### 评分标准怎样写才有用
cookbook 总结的原则,我按自己的理解重新归纳成四条:
| 原则 | 反例 | 正例 |
|---|---|---|
| **逼评审拿出证据** | 「检查简报是否覆盖了需求电费」:评审扫一眼看到相关段落就打勾,一个来源都不用打开 | 「找到需求电费那一节,确认它给出了 $/kW 数字或占运营成本的百分比」 |
| **比任务描述更具体** | 任务写「运营商财务」,标准也只写「运营商财务」 | 指定数据口径(GAAP 净亏损)、文件类型(10-K / 10-Q)、来源域名 |
| **写目标,不写步骤** | 规定评审必须跑某条命令;那条命令不可用时,这项检查就悄悄没做 | 定义「什么算证据」,评审有写手的全部工具,自己会找办法 |
| **堵住写手的捷径、规定反馈格式** | 不写「不许用镜像」,死链会被换成抄来的页面并通过 | 写明禁止的替代做法;要求先给一行总分,再每个失败项一条「错在哪、怎么改」,并列出不要挑的毛病(文风、范围外的问题) |
如果手里没有现成的标准,官方建议拿一份公认合格的产出物给 Claude,让它分析好在哪,再把分析改写成逐条标准。这比从空白页开始写效果好。
> [!NOTE]
> **为什么不直接把评分标准写进写手的系统提示?** 写进去能让写手瞄得更准,但它仍然是在给自己打分。它会在自认为通过时宣布通过,不会重新抓一遍已经引用过的链接,也不会发现记住的引文和原文差了几个字。独立评审开局只有标准和产出物,平台不拿到它对每一条的结论就不让循环继续,这种隔离靠一段提示词做不到。
### 什么时候用、什么时候别用
**适合用**:
- 能把「好」写成可检查条目的任务:要求覆盖完整、细节准确、引用可核实;
- 主观质量也可以,比如文案是否符合品牌口吻,前提是标准把口吻钉死到可检查的程度;
- 产出物是文件,评审能用工具打开核实。
**别用,或者先想清楚**:
- **写不出可检查的标准。** 标准含糊,评审就什么都放行,你多付了评审的 token,却什么都没检查到。
- **每轮都撞上限,而且评审指出的是同一类问题。** 这说明写手根本改不了,比如缺工具、缺权限、拿不到数据。继续加轮数只是在为不收敛付钱。
- **对延迟敏感。** 每一轮都要整份重新评审,例子里 3 轮花了 13 分钟。
- **评审和写手的盲区可能重合。** 这一条是我的推断,官方没有说:评审默认和写手用同一个模型、同一套工具,写手判断不出来的东西,评审也可能判断不出来。上下文隔离解决的是「被写手的思路带偏」,解决不了「两边都不懂」。高风险的结论仍然需要人或者确定性的检查(测试、校验脚本)。
官方给的效果数字:比标准提示词循环的任务成功率最高高 10 个百分点,难题上提升最大;生成 docx 成功率高 8.4%,pptx 高 10.1%。**这些都是厂商内部基准,需要辩证看待**,具体到你的任务,最好自己拿一批样本跑对照。
---
## Dreaming:会话之间的记忆整理
### 一次整理任务发生了什么
整理任务是一个**异步任务**,输入两样东西:
- 一份**已有的记忆库**:要核对、去重、重新组织的对象;
- **1 到 100 段会话**:过去的完整记录,从里面找规律,折进新记忆。
它产出**另一份记忆库**,和输入分开。任务开始运行后,平台先把输入的记忆库复制一份,新库的 ID 随后出现在任务的 `outputs[]` 里。官方明确写了:**整理任务从不删除、从不修改它的输入。**
一份旧记忆都没有、只有会话记录时,先建一个空记忆库当输入。
### 完整的请求和响应
整理任务的接口需要两个 beta 头,只有会话接口那个不够。完整的最小请求(取自官方文档):
```bash
curl -s https://api.anthropic.com/v1/dreams \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01,dreaming-2026-04-21" \
-H "content-type: application/json" \
--data @- <` 给当前会话设一个完成条件。每轮结束后,Claude Code 把**条件和到目前为止的整段对话**交给一个小模型(默认 Haiku)判断,回答「未达成 / 达成 / 不可能」并附一句理由;未达成就把理由交回去,Claude 接着干。官方文档写明,它是一个「只在本会话生效、由提示词判断的 Stop 钩子」外面包了一层。
| | Outcomes(Managed Agents) | `/goal`(Claude Code) |
|---|---|---|
| 评审是谁 | 平台每轮新拉起一个评审,默认和写手同一个模型 | 配置的小模型,默认 Haiku |
| 评审看到什么 | 只有评分标准和产出物,**看不到写手的思路** | 条件 + **整段对话**:写手说了什么、跑了什么 |
| 评审能不能自己查证 | 能,有写手的全部工具,可以打开文件、跑代码 | **不能**,不调工具,只能根据对话里已经出现的内容判断 |
| 写手和标准的关系 | 任务描述给写手,评分标准给评审,两者分开写 | 条件本身就是给写手的指令,写手全看得到 |
| 轮数上限 | `max_iterations`,默认 3、最多 20,平台强制 | 没有硬上限;「最多 N 轮」只能写进条件让评审去判断,硬刹车是 `/goal clear` 或 `claude -p` 的 `--max-budget-usd` |
| 可能的结果 | 通过 / 需返工 / 到达上限 / 标准不适用 / 被中断 | 达成 / 未达成 / 不可能,外加出错自动清除、连续几轮不动手时停下 |
| 评审的花费 | 按评审所用模型的标准价计费,会话另收运行时长费 | 走小模型,官方说通常可以忽略 |
一句话:**Outcomes 的评审是另一个能动手核查的人,`/goal` 的评审是一个只读会议记录的人。** 前文说 Outcomes 的关键是评审和写手的上下文隔离,`/goal` 恰好没有这层隔离:写手在对话里跑出了什么、说了什么,评审就据此下结论。所以用 `/goal` 时,条件要写成「对话里能证明的东西」,比如「`npm test` 退出码为 0,汇总行显示 0 failed」,并要求写手把证据真的跑出来。
### Dreaming 的近亲:auto-dream
Claude Code 的 auto memory(Claude 自己往记忆目录里记笔记)还有一个后台整理功能,程序里叫 auto-dream:距上次整理满一定小时数、又攒够几次新会话后,后台派一个子 Agent 读最近的会话记录,合并、修正记忆目录里的文件。
| | Dreaming(Managed Agents) | auto-dream(Claude Code) |
|---|---|---|
| 怎么触发 | 你调接口建任务 | 自动触发。程序里的默认门槛是距上次至少 24 小时、至少 5 次新会话;我的账号拿到的远程配置是 24 小时、3 次 |
| 输入 | 一份记忆库 + 1 到 100 段会话 | 本项目的记忆目录 + 上次整理以来的会话记录 |
| 输出 | **另出一份**新记忆库,原库不动,先审后用 | **直接改**记忆目录里的文件 |
| 官方文档 | 有,研究预览,需申请 | 没有。设置项 `autoDreamEnabled` 不在官方设置参考里,只出现在程序内置的设置说明中 |
| 谁能用 | 申请通过的账号 | 服务端灰度开关决定 |
*auto-dream 一行的门槛和行为是我从 2.1.294 的程序和配置里读出来的,不是官方承诺,随时可能变。*
我这台机器上,两条路都不通:
- **API**:个人 key 调 `GET /v1/dreams` 返回 404,和请求一个根本不存在的路径一样;同一把 key 调会话、记忆库的列表接口都是 200。也就是 Managed Agents 能用,Dreaming 研究预览没开给这个账号。
- **Claude Code**:`~/.claude.json` 里缓存的灰度开关 `tengu_onyx_plover` 是 `{"enabled": false, "minHours": 24, "minSessions": 3, "remoteEnabled": false}`;整个 `~/.claude` 和记忆目录里都找不到 auto-dream 运行时会留下的锁文件 `.consolidate-lock` 或任何整理状态,说明它从没跑过。在设置里写 `"autoDreamEnabled": true` 也没用:程序先看灰度开关,开关关着就根本不读这个设置。官方仓库的 [issue #86209](https://github.com/anthropics/claude-code/issues/86209) 报过一模一样的现象,后来因为长期没人跟进被自动关闭,没有修。
### 名字都叫 Dreaming 的三样东西
搜「Claude Dreaming」会搜到三样东西,名字一样,来源和用法都不同:
| | Claude Code 的 auto-dream | Managed Agents 的 Dreaming | OpenClaw 的 Dreaming |
|---|---|---|---|
| 谁做的 | Anthropic | Anthropic | 开源项目 OpenClaw,和 Anthropic 无关 |
| 给谁用 | Claude Code 用户,靠灰度开关逐步放出 | 在 Claude 开发者平台上自己搭 Agent 的团队 | 自己部署 OpenClaw 的人 |
| 怎么触发 | 自动,攒够时间和会话数后在后台跑 | 调 `/v1/dreams` 建任务 | 定时任务,当前文档写的是默认开启 |
| 改不改原件 | 直接改记忆目录里的文件 | 另出一份新记忆库,原库不动 | 改写 `MEMORY.md`,改之前把旧版存进插件状态,增删合并的摘要写进 `DREAMS.md` 供人看 |
| 怎么防注入 | 没找到公开说明 | 文档没讨论整理任务本身(见下文「风险」) | 拼整理提示词之前,先剔除来源标记为不可信或由系统产生的候选 |
| 官方文档 | 没有 | 有 | 有 |
时间线:
- **2026-03-24**:已经有用户在 Claude Code v2.1.81 的 `/memory` 里看到「Auto-dream: on · last ran 1d ago」([issue #38461](https://github.com/anthropics/claude-code/issues/38461)),说明当时已在小范围放出。
- **2026-03-31**:npm 上的 Claude Code v2.1.88 误带了 source map,完整源码外泄。按 Layer5、Soma 等分析文章的转述,里面的 `autoDream` 就是上表第一列:后台派一个子 Agent,满 24 小时、攒够 5 次会话、拿到锁才运行,分「定位、收集、整合、修剪」四步,只给只读的 shell。
- **2026-04-05**:OpenClaw 仓库里 Dreaming 文档的第一次提交。时间上前后脚,我没有证据说明两者有关系。
- **2026-05-06**:Anthropic 发布 Managed Agents 的 Dreaming 研究预览。
- **2026-10-09**:Claude Code 官方更新日志全文没有一处提到 dream。
所以,**在 Claude Code 里看不到 Dreaming,不是漏开了某个选项,而是账号没被放进灰度。** 能看到的「验收循环」只有 `/goal`,而它和 Outcomes 恰好差在最关键的那一点上。
---
## 实测:/goal 的评审到底起了多大作用
Outcomes 这次没跑(原因见开头),这里实测它在 Claude Code 里的近亲 `/goal`。要回答的问题是:**每轮之后多一个评审,产出有没有变好?变好是因为评审,还是因为把标准写清楚了?**
### 怎么测
- **任务**:写一个 Python 函数 `parse_duration`,把 "1h30m"、"45s" 这样的时长换算成秒,并附 pytest 测试。给写手的说明只有一句:
```text
Write a Python module `duration.py` that exposes `parse_duration(text: str) -> int`, converting compact duration strings such as "1h30m" or "45s" into a whole number of seconds. Invalid input must raise an exception. Also write a pytest file `test_duration.py` for it. Put both files in the current directory.
```
- **严格标准**:23 个例子,分四类,外加「测试覆盖每条规则并通过」。数值 7 个(含「组件之间允许一个空格」「首尾空白忽略」两条);必须报 `ValueError` 的写法 13 个(空串、只有数字、只有单位、未知单位、大写 "1H"、小数、正负号、单位顺序颠倒 "30m1h"、单位重复 "1h1h"、数字和单位之间有空格、两个空格);必须报 `TypeError` 的类型 3 个(`None`、`90`、`b"1h"`)。
- **宽松标准**:只要求「常见格式能换算、非法输入抛异常、测试通过」。
- **隐藏测试**:用严格标准的 23 个例子写了一个打分脚本,写手和评审都看不到,全部跑完后统一给每次的产出打分。脚本先自检过:参考实现得 23/23,一个故意写得粗糙的实现得 13/23。
- **分组**:四组各跑 3 次,写手统一用 Claude Sonnet 5.5。每次都用 `claude -p` 在一个空目录里跑,不加载我的用户级设置和记忆。
| 组 | 写手拿到什么 | 评审 |
|---|---|---|
| A 一句话 | 上面那一句 | 无 |
| B 规则写进提示词 | 一句话 + 严格标准全文 | 无 |
| C `/goal` + 严格条件 | 和 B 完全相同的文字,前面加 `/goal` | Haiku,每轮结束后读对话判断 |
| D `/goal` + 宽松条件 | 一句话 + 宽松标准,前面加 `/goal` | 同上 |
C、D 两组的条件原文:
```text
C: /goal Write a Python module `duration.py` that exposes `parse_duration(text: str) -> int`, converting compact duration strings such as "1h30m" or "45s" into a whole number of seconds, plus a pytest file `test_duration.py`, both in the current directory. The goal is met only when all of the following hold, each shown in this conversation by actually running the examples and pytest: (1) "1h30m" -> 5400, "45s" -> 45, "2d" -> 172800, "0s" -> 0, "1d2h3m4s" -> 93784, units d=86400 h=3600 m=60 s=1, return type int; (2) one space between components is allowed ("1h 30m" -> 5400) and leading/trailing whitespace is ignored (" 45s " -> 45); (3) each of these raises ValueError: "", " ", "90", "h", "5w", "1H", "1.5h", "-5m", "+5m", "30m1h", "1h1h", "1 h", "1h 30m"; (4) None, 90 and b"1h" raise TypeError; (5) test_duration.py covers every rule above and `python -m pytest` passes.
D: /goal Write a Python module `duration.py` that exposes `parse_duration(text: str) -> int`, converting compact duration strings such as "1h30m" or "45s" into a whole number of seconds, plus a pytest file `test_duration.py`, both in the current directory. The goal is met when parse_duration converts common duration strings such as "1h30m" and "45s" to seconds, invalid input raises an exception, and the tests in test_duration.py pass.
```
### 结果
2026-10-09 跑的 12 次:
| 组 | 隐藏测试得分(3 次,满分 23) | 评审判定 | 每次用时 | 每次花费(订阅额度折算) |
|---|---|---|---|---|
| A 一句话 | 22、20、19 | 无评审 | 14–15 秒 | 约 $0.05 |
| B 规则写进提示词 | 23、23、23 | 无评审 | 15–26 秒 | $0.05–0.08 |
| C `/goal` + 严格条件 | 23、23、23 | 3 次都在第一轮后判「达成」 | 24–27 秒 | 约 $0.07 |
| D `/goal` + 宽松条件 | 18、18、18 | 3 次都在第一轮后判「达成」 | 13–20 秒 | $0.04–0.07 |
评审每判一次耗时 1.6 到 8.2 秒。它的结论不在 `claude -p` 的输出流里,而是记在会话记录 `~/.claude/projects/<目录>/<会话 ID>.jsonl` 中一条 `goal_status` 记录里。D 组第 2 次的原样是:
```json
{"type": "goal_status", "met": true, "iterations": 1,
"reason": "The transcript shows duration.py and test_duration.py were written with the Write tool to the current directory. The test file asserts ('45s', 45) and ('1h30m', 5400), and test_invalid covers inputs such as '' , 'abc' and '30m1h' with pytest.raises(ValueError). The Bash run returned '19 passed in 0.01s', so the tests pass."}
```
*为便于阅读删去了 `durationMs`、`tokens` 两个字段。*
三点观察:
1. **这次起作用的是把标准写清楚,不是评审。** B 组没有任何评审,3 次同样满分;C 组的评审 3 次都在第一轮后就判「达成」,一次也没让 Claude 多干一轮。A 组丢的分全在说明里没写的规则上:「组件之间允许一个空格」3 次都没猜中;有两次自己加了「周」这个单位,把 "5w" 当成 5 周接受;另外分别有一次接受了大写 "1H"、接受了颠倒的 "30m1h"、拒绝了带首尾空白的 " 45s "。写手不是能力不够,而是不知道你要什么。
2. **条件写松,评审照样放行。** D 组 3 次都判「达成」,隐藏测试却只有 18/23,而且 3 次错得一模一样:大写 "1H" 被当成 1 小时接受,`None`、`90`、`b"1h"` 抛的是 `ValueError` 而不是 `TypeError`,组件之间的空格被拒绝。评审的理由都是「pytest 通过了」(比如「19 passed」),而那份测试是写手按自己的理解写的。这就是开头那句提醒在 `/goal` 上的样子:标准含糊,评审就放行。D 组还比什么都不加的 A 组低,我的猜测是「非法输入抛异常」这句把写手引向了一律抛 `ValueError`,3 次样本不足以下结论。
3. **评审只看对话。** 6 次判定的理由引用的全是对话里出现过的东西:「Bash 运行打印了……」「Write 工具报告 File created successfully」「pytest 显示 22 passed」。它没有、也没法自己打开 `duration.py` 跑一遍。
**局限**:任务很小,Sonnet 5.5 一轮就做完,评审没有机会发挥 `/goal` 的本来用途,也就是把还没做完就想停下的 Claude 推回去接着干(长时间的迁移、清空待办这类任务)。每组只跑了 3 次。所以这组数据只能说明一件事:**在一轮就能做完的任务上,`/goal` 的评审不会替你补上没写进条件的要求。** Outcomes 的评审能用工具核查,换成 Outcomes 能不能抓住 D 组那几处错,仍然取决于评分标准里有没有写,这一点我没有实测,是按设计推断的。
---
## 风险:记忆是注入的长期通道
前面引过官方的警告:读写权限的记忆库,一次注入就能把恶意内容写进去,之后的会话都会信它。Dreaming 让这个风险多了一层。
**下面是我的推断,官方文档没有讨论**:整理任务会读最多 100 段会话的完整记录。如果其中一段会话读过被注入的网页或工具输出,整理任务可能把注入的内容当成「多次会话里反复出现的规律」,写进新记忆库,而且写得比原来更像一条正经的经验。
能做的防守:
- **按信任度拆记忆库。** 处理不可信输入的 Agent,只给它挂只读的参考记忆库;需要写的记忆库,只挂给不碰外部输入的 Agent。这和 Meta 提出的 [Rule of Two](https://ai.meta.com/blog/practical-ai-agent-security/) 是同一个思路:「处理不可信输入」「接触敏感数据或系统」「改变状态或对外通信」三者,一个会话最多同时占两样;三样都占,就不能让它自主运行,至少要有人审批或别的可靠验证手段。
- **整理结果先审后用。** Dreaming 默认不覆盖原库,审核这一步就是防线。处理不可信输入的场景,别配成自动采用。
- **挑选喂给整理任务的会话。** 读过大量外部内容的会话,要么不喂,要么在 `instructions` 里明确只提炼某类信息。
- **按来源过滤候选,而不只靠提示词。** OpenClaw 的 Dreaming 把这条做成了结构性规则:每条候选记忆都带来源标记,拼整理提示词之前,先把标记为不可信或由系统产生的候选剔除,模型根本看不到它们。自己实现时,给每条记忆记下来自哪次会话、什么类型的输入,比事后审核更早拦住注入。
---
## 搬到自己的技术栈:两份契约
Outcomes 和 Dreaming 都是托管功能,但设计和 Anthropic 无关,任何模型、任何语言都能复刻。下面把它们翻译成输入、输出、状态和不变量。
### 契约一:有上限的独立验收循环
| | 内容 |
|---|---|
| 输入 | 任务描述(给写手)、评分标准(给评审)、产出物位置、最大轮数 |
| 状态 | 当前轮次、每轮评审结论 |
| 输出 | 通过 / 需返工 / 到达上限 / 标准不适用 / 被中断,加上逐条说明 |
| 不变量 | 每轮评审都是新上下文;评审只拿到评分标准、产出物和工具,拿不到写手的思路;循环一定会结束 |
示意伪代码(Python 写法,不是任何 SDK 的真实接口):
```python
def run_outcome(task, rubric, max_iterations=3):
workspace = new_workspace() # 产出物放这里,写手和评审都能读
feedback = None
for iteration in range(max_iterations):
writer_turn(workspace, task, feedback) # 写手:同一个会话,带着上一轮意见接着干
verdict = grade_in_fresh_context(rubric, workspace) # 评审:新上下文,只有标准和产出物
if verdict.result in ("satisfied", "failed"):
return verdict
feedback = verdict.explanation # 逐条意见原样交回写手
writer_turn(workspace, task, feedback, final=True) # 收尾一轮,不再评审
return Verdict("max_iterations_reached", feedback)
```
要点在 `grade_in_fresh_context`:它不能复用写手的对话历史,要用一个全新的请求,系统提示里只有评分标准,产出物通过工具去读。换成 Go、TypeScript 或者别家模型都一样。如果你用 Temporal 这类工作流引擎(我在[为什么从 Celery 迁移到 Temporal](https://zhuoqidev.com/posts/why-temporal-not-celery/)里讲过),它就是一个有上限的循环工作流,写手和评审各是一个独立的 activity,轮次和结论天然落在工作流历史里。
### 契约二:不改原件的记忆整理
| | 内容 |
|---|---|
| 输入 | 当前记忆的快照、若干段会话记录、整理方向 |
| 输出 | 一份新记忆 |
| 不变量 | 永远不改输入;新记忆可以和旧的逐条比对(diff);采用前有审核点;每条记忆能追溯来自哪次会话 |
示意伪代码(Anthropic 没有公开整理流水线的内部做法,这里只表达契约):
```python
def consolidate(store, transcripts, focus=None):
draft = clone(store) # 先复制;文档写明官方也是先复制输入记忆库
for t in transcripts:
draft = synthesize(draft, t, focus) # 合并重复、用新值替换过时条目、补跨会话规律
return draft # 交给人看 diff(store, draft),通过再换上
```
我在[LLM 记忆调研](https://zhuoqidev.com/posts/llm-memory-research/)里把 Agent 记忆拆成一条闭环:经历、写入、存储、检索、组装上下文、行动反馈,最后是巩固、修订和遗忘。Dreaming 就是把「巩固 / 修订」这一步单独拿出来,做成一个离线、可审核的批处理任务。
---
## 我的手动版:用 Git 仓库做「Dreaming」
我自己的 Agent 工作记忆是一个私有 Git 仓库,回头看,它几乎是 Dreaming 的手动版:
| Managed Agents | 我的工作记忆仓库 |
|---|---|
| 记忆库:很多小文件 | `memory/` 目录:一个文件只记一个事实,带 frontmatter;另有一个索引文件,每次会话自动载入,限制在 200 行以内 |
| 每次写入一个不可改的版本 | Git 提交历史 |
| 整理任务的输入:历史会话 | 每日日志:记录「发生了什么」的原始材料 |
| 整理任务:合并重复、替换过时、补规律 | 定期整理:合并重复条目、删掉被证伪的、把只埋在日志里的关键教训提升成独立的记忆条目 |
| 新记忆库先审后用 | 改动先看 `git diff`,确认了再提交 |
差别也很清楚:
- **我是原地改,它是另出一份。** 我靠 Git 回滚兜底,它从设计上就不碰原件,更安全。值得借鉴的做法是整理时先开一个分支,审过 diff 再合并,这就等于「原库不动」。
- **它会读会话原文,我只读日志。** 日志已经是我筛过一遍的东西,单次会话里没被我注意到的模式,我的整理看不到。
- **我的验收靠规则。** 我给 Agent 定过一条规矩:不能先写「已合并」「已部署」再去做,必须以 PR 状态、CI、部署日志这类机器证据为准。这和 Outcomes 的评分标准是同一个道理:**让检查者必须拿出证据,而不是相信干活的人说自己做完了。**
---
## 最值得带走的七点
1. **Outcomes 解决的是自评偏差。** 关键不是「多一个 Agent」,而是评审的上下文和写手隔离,手里只有标准和产出物。
2. **评分标准决定一切。** 标准要比任务描述更具体,每一条都要逼评审拿出证据,否则它会什么都放行。
3. **循环必须收敛。** 每轮都撞上限且问题同类,说明写手改不了,加轮数没用。
4. **Dreaming 解决的是记忆的熵增。** 它离线读很多次会话,另出一份整理好的记忆库,原库不动,先审后用。
5. **记忆是注入的长期通道。** 默认读写的记忆库、加上会读会话原文的整理任务,都要按信任度拆分并保留审核点。
6. **两者都能脱离 Anthropic 复刻。** 一个是有上限的独立验收循环,一个是不改原件的记忆整理,换任何模型和语言都成立。
7. **Claude Code 里只有 `/goal`,它的评审只读对话。** 实测里起作用的是把标准写清楚;条件写松,它照样判「达成」。auto-dream 还在灰度,没被放进去的账号设置了也没用。
**铁律:别让干活的人自己验收,也别让记忆只增不理。**
---
## 来源
- [Define outcomes(官方文档)](https://platform.claude.com/docs/en/managed-agents/define-outcomes):定义目标事件、三种评审事件、五种结果、评分标准写法
- [Dreams(官方文档)](https://platform.claude.com/docs/en/managed-agents/dreams):整理任务的输入输出、状态、`instructions` 的边界、计费与限额
- [Using agent memory(官方文档)](https://platform.claude.com/docs/en/managed-agents/memory):挂载方式、版本、限额、只读与注入警告、beta 头
- [Outcomes: agents that verify their own work(Claude Cookbook,2026-05-03)](https://platform.claude.com/cookbook/managed-agents-cma-verify-with-outcome-grader):EV 快充简报的三轮返工与评分标准原则
- [New in Claude Managed Agents: dreaming, outcomes, and multiagent orchestration(Anthropic 博客)](https://claude.com/blog/new-in-claude-managed-agents):发布状态、+10 / +8.4% / +10.1%、Harvey 约 6 倍
- [Memory for Claude Managed Agents(Anthropic 博客,2026-04-23)](https://claude.com/blog/claude-managed-agents-memory):记忆库公开 beta、Rakuten 数据
- [ZDNET:Your Claude agents can 'dream' now(2026-05-06)](https://www.zdnet.com/article/your-claude-agents-can-dream-now-how-anthropics-new-feature-works/)、[SiliconANGLE(2026-05-06)](https://siliconangle.com/2026/05/06/anthropic-letting-claude-agents-dream-dont-sleep-job/)、[Simon Willison 的 Code w/ Claude 2026 现场记录](https://simonwillison.net/2026/May/6/code-w-claude-2026/):发布日期与第三方报道
- [Meta:Agents Rule of Two: A Practical Approach to AI Agent Security(2025-10-31)](https://ai.meta.com/blog/practical-ai-agent-security/)
- [Keep Claude working toward a goal(Claude Code 文档)](https://code.claude.com/docs/en/goal):`/goal` 的评审方式、只读对话不调工具、评审 token 通常可忽略
- [All settings(Claude Code 文档)](https://code.claude.com/docs/en/settings-reference):2026-10-09 核对,没有 `autoDreamEnabled` 这一项
- [anthropics/claude-code#86209](https://github.com/anthropics/claude-code/issues/86209):灰度开关关着时 `autoDreamEnabled` 被静默忽略
- [anthropics/claude-code#38461](https://github.com/anthropics/claude-code/issues/38461):2026-03-24 已有用户看到「Auto-dream: on」
- [Claude Code CHANGELOG](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md):2026-10-09 核对,全文没有 dream
- [Layer5:The Claude Code Source Leak](https://layer5.io/blog/engineering/the-claude-code-source-leak-512000-lines-a-missing-npmignore-and-the-fastest-growing-repo-in-github-history/)、[Soma:The Architecture of Forgetting(2026-04-03)](https://soma.gravicity.ai/blog/the-architecture-of-forgetting):2026-03-31 源码外泄经过与 `autoDream` 的第三方分析
- [Dreaming(OpenClaw 文档)](https://docs.openclaw.ai/concepts/dreaming):OpenClaw 的三阶段整理、默认开启、按来源剔除不可信候选
- [Pricing(官方文档)](https://platform.claude.com/docs/en/about-claude/pricing):Managed Agents 按 token 加会话运行时长($0.08 / 小时)计费
- [为什么 Claude 订阅之外还要单独为 API 付费(Claude 帮助中心)](https://support.claude.com/en/articles/9876003-i-have-a-paid-claude-subscription-pro-max-team-or-enterprise-plans-why-do-i-have-to-pay-separately-to-use-the-claude-api-and-console):订阅额度不含 API 用量
> 核对日期:2026-10-06;2026-10-09 补充 Claude Code 2.1.294 的核对与 `/goal` 实测。Outcomes 与记忆库为公开 beta,Dreaming 为研究预览,字段、限额和支持的模型都可能变化;本文 Outcomes、Dreaming 和记忆库的请求示例均取自上述官方文档,我没有实际运行;伪代码只表达设计契约,不代表 Anthropic 的内部实现。
---
title: "Codex 为什么不把所有工具都交给模型?讲透 tool_search、BM25 与模型替换"
description: "基于 Codex rust-v0.147.0 源码,用大白话解释工具为什么要按需加载、BM25 如何从大量工具中排出最相关的几个,以及怎样把同一设计搬到 Python、Go 和其他模型。"
url: "https://zhuoqidev.com/posts/codex-first-request-tools/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-08-08"
updated: "2026-08-09"
series: "Agent 架构深度"
tags: ["Codex","OpenAI","Tool Calling","Tool Search","BM25","MCP","Code Mode","AI Agent"]
translation: "https://zhuoqidev.com/en/posts/codex-first-request-tools/index.md"
---
# Codex 为什么不把所有工具都交给模型?讲透 tool_search、BM25 与模型替换
> **TL;DR** 把模型想成只有一张小桌子的工程师。工具说明书有几百本时,全部摊在桌上既贵又难找;更好的办法是先给它一本目录,需要什么再取出最相关的几本。本文就讲清 Codex 怎样做这件事、为什么使用 BM25 排序,以及怎样把同一办法搬到 Python、Go 和其他模型上。
本文固定在 2026-08-07 发布的稳定版 [`rust-v0.147.0`](https://github.com/openai/codex/releases/tag/rust-v0.147.0),源码 commit 为 [`be6e8eac`](https://github.com/openai/codex/commit/be6e8eac029b183056b7e4402879f15d2c85f61b);另复查了 2026-08-09 的 `main` commit [`646f7c0a`](https://github.com/openai/codex/commit/646f7c0a91b8e327d263335da68ae8ef212895ce)。先给两个不会误导人的结论:
1. **模型第一次能看到哪些工具,不是一张永远不变的名单。** 它会随模型、接入方式、运行环境和功能开关变化。
2. **`tool_search` 和背后的 BM25 排序代码都已经写好,普通直连方式可以使用;但这个稳定版给 GPT-5.6 采用的 `exec` 集中调用方式漏掉了搜索入口。** 这不是功能没开发,而是一条接线没有接通。
## 这篇文章写给谁
默认读者只需要知道“大模型可以调用外部工具”。**不要求会 Rust,也不要求读过 Codex 源码。** 如果你主要写 Python 或 TypeScript,后面的 Rust 连写可以直接理解成“筛选列表 → 改造每一项 → 收集结果”。
有两条阅读路线:
- **只想理解做法**:读小桌子问题、机场图、工具搜索过程、跨语言复用和模型替换;
- **想跟进源码**:继续读伪代码、Rust 对照、完整查询实例和源码索引。
## 先只记住四句大白话
先把 Rust 函数名和 OpenAI 的专有叫法全部擦掉,一个会使用工具的模型仍逃不开四件事:
1. **模型只能使用它看得见的工具。** 真正干活的代码即使已经写好,没有把工具说明交给模型,模型也不知道它存在;
2. **模型眼前的空间有限,而且放进去的每个字都要付出费用和时间。** 工具越多,重复发送的说明书越长,模型也越容易选错;
3. **搜索就是先缩小范围。** 先从几百个工具里排出最相关的几个,再让模型选择,比让它同时读几百本说明书更稳;
4. **Agent 外壳和大模型可以分开。** 只要双方对“问题怎样发、工具怎样调用、结果怎样交回”达成一致,编程语言、搜索方法和模型都可以替换。
因此全文最小的知识骨架其实只有这一条:
```mermaid
flowchart TD
A["模型眼前的空间有限"] --> B["不能一直摆着所有工具说明书"] --> C["先给少量常用工具和一本目录"] --> D["搜索并取出排名靠前的几份完整说明"] --> E["模型调用"] --> F["程序真正执行,再把结果交回模型"]
classDef cause fill:#fff3bf,stroke:#f08c00
classDef out fill:#d3f9d8,stroke:#2f9e44
class A cause
class F out
```
理解这条链以后,下面哪些必须保留、哪些可以摘掉就很清楚:
| 阅读源码时看到的内容 | 是否必须理解 | 原因 |
| --- | --- | --- |
| 工具是一开始就给、搜索后再给,还是完全不给 | 必须 | 它决定模型在什么时候看见什么 |
| `可搜索文字 → 排名后的编号 → 完整工具说明` | 必须 | 这是可以搬到任何 Agent 的核心做法 |
| 模型接入层怎样收发数据、支持哪些能力 | 必须 | 它决定能否安全替换模型 |
| Rust iterator、具体类型名、文件路径 | 可以先摘掉 | 它们是本项目实现,不是架构定律 |
| `bm25` crate 名称与数组下标技巧 | 可以替换 | Python、Go 或数据库检索都能实现同一接口 |
| GPT-5.6 当前工具名单 | 只能当版本快照 | 模型、环境和功能开关改变后会重新计算 |
## 全文只需要认识四样东西
| 白话名字 | 它是什么 | 源码里偶尔出现的名字 |
| --- | --- | --- |
| 工具说明书 | 工具叫什么、能做什么、需要哪些参数 | Schema / Tool Spec |
| 工具总表 | 程序目前知道的全部工具;不等于全部都交给模型 | Registry |
| 真正干活的代码 | 模型点名某个工具后,负责执行它的程序 | Runtime / Handler |
| 模型接入层 | 把 Codex 的请求翻译成模型服务能够收发的格式 | Provider |
后文再次出现英文原名时,它只是为了和源码对照。理解正文不需要背这些词。
## 为什么很多工具要收进 `exec`
这里讨论的不是“用户只能写代码”的聊天模式,而是**工具以什么方式交给模型**:
| 白话说法 | 源码名称 | 模型看到什么 |
| --- | --- | --- |
| 每个工具单独摆出来 | Direct | 多数工具都有自己的调用入口 |
| 常用的单独摆,其他收进总入口 | Code Mode | 一部分工具直接调用,另一部分通过 `exec` 里的 `tools.xxx()` 调用 |
| 几乎都收进总入口 | Code Mode Only | 模型主要写一小段 JavaScript,通过 `exec` 统一调用;只有 `wait` 等少量入口仍单独保留 |
在本文稳定版里,GPT-5.6 Sol 的模型配置已经写明采用“几乎都收进 `exec`”的方式。**选择这个模型后 Codex 会自动采用,不需要在 UI 里再开一个按钮。** 源码也保留了下面这个实验开关,方便开发者测试其他模型:
```toml
[features]
code_mode_only = true
```
CLI 也可临时传 `codex --enable code_mode_only`。公开配置仍把它标成“开发中”;模型没有经过这种调用方式的训练和验证时,强行开启可能让表现变差。因此它适合测试,不适合对任意模型盲开。`--code-mode-host` 是另一回事,它只决定这段工具编排代码在哪里运行。
现在再看分层图。它同时画出“设计中的延迟发现”与“稳定版 Code Mode Only 的首轮实际状态”;虚线和路障表示 `tool_search` 已实现,但在这个特定首轮工具布局中没有暴露给模型。

## 模型第一次到底能看到什么:先分清公开 API 与 Codex 内部封装
先纠正一个很容易造成误解的说法:**开发者直接调用 [OpenAI 公开 Responses API](https://developers.openai.com/api/docs/guides/tools) 时,即使用 GPT-5.6,工具仍然放在顶层 `tools`。不应该因为 Codex 源码里出现了 `additional_tools`,就把自己的公开 API 请求改成这个写法。**
这里其实混在了一起的是两个问题:
1. **模型能使用哪些工具**:取决于服务最终交给模型哪些工具说明;
2. **这些说明怎样穿过网络送到服务**:取决于客户端和服务端约定的请求格式。
模型不会亲自阅读 HTTP 包并比较 JSON 键名。服务端会先把请求还原成“模型可调用的工具”。所以 `tools` 与 `additional_tools` 更像是**两种信封**:信封写法不同,里面都可以装同一份工具说明书。真正受这个区别影响的是客户端能否与服务端对上格式,而不是模型突然多了或少了某种推理能力。
### 对比一:开发者直接调用公开 Responses API
下面是一份完整的最小请求。为了让两边能逐项比较,只放一个 `weather` 工具组:
```bash
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6",
"instructions": "需要实时天气时调用工具,不要猜测。",
"input": [
{
"role": "user",
"content": "巴黎现在多少度?"
}
],
"tools": [
{
"type": "namespace",
"name": "weather",
"description": "查询实时天气。",
"tools": [
{
"type": "function",
"name": "get_current_weather",
"description": "按城市查询当前温度。",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"],
"additionalProperties": false
},
"strict": true
}
]
}
],
"tool_choice": "auto"
}'
```
这里有三条彼此平行的输入:`instructions` 告诉模型做事原则,`input` 装用户问题,`tools` 装可以调用什么。工具既不是系统提示词里的自然语言清单,也不是用户消息的一部分。
### 对比二:Codex 选择 Responses Lite 后发出的请求
Codex 的稳定版模型配置会为 GPT-5.6 Sol 选择 `use_responses_lite`。此时源码仍然构造一个 Responses 请求体,但做了两次搬家:顶层 `tools` 搬到 `input` 的第一项,顶层 `instructions` 搬到第二项。下面仍用同一个人为示例的 `weather` 工具组,方便逐字段比较;字段位置和固定值来自源码,尖括号中的缓存键与追踪值则由 Codex 每轮生成:
```http
POST
Authorization: Bearer <当前登录或 API 凭据>
Content-Type: application/json
x-openai-internal-codex-responses-lite: true
{
"model": "gpt-5.6-sol",
"input": [
{
"type": "additional_tools",
"role": "developer",
"tools": [
{
"type": "namespace",
"name": "weather",
"description": "查询实时天气。",
"tools": [
{
"type": "function",
"name": "get_current_weather",
"description": "按城市查询当前温度。",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"],
"additionalProperties": false
},
"strict": true
}
]
}
]
},
{
"type": "message",
"role": "developer",
"content": [
{
"type": "input_text",
"text": "需要实时天气时调用工具,不要猜测。"
}
]
},
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "巴黎现在多少度?"
}
]
}
],
"tool_choice": "auto",
"parallel_tool_calls": false,
"reasoning": { "effort": "low", "context": "all_turns" },
"store": false,
"stream": true,
"include": ["reasoning.encrypted_content"],
"prompt_cache_key": "<由 Codex 会话生成>",
"text": { "verbosity": "low" },
"client_metadata": {
"x-codex-turn-metadata": "<由 Codex 生成的 JSON 字符串>"
}
}
```
最值得注意的不是字段多了一个,而是请求头已经明确写着 **`internal-codex-responses-lite`**。[Codex 的源码测试](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/tests/suite/responses_lite.rs#L28-L139)也明确检查:这种请求没有顶层 `tools` 和 `instructions`,`input[0]` 必须是 `additional_tools`,下一项才是 `developer` 指令。它是 Codex 与支持该格式的模型服务之间的内部传输合同,不是公开 Responses API 文档要求普通开发者采用的新格式。
| 精准对比 | 公开 Responses API | Codex Responses Lite |
| --- | --- | --- |
| 面向谁 | 普通 API 开发者 | Codex 与明确支持 Lite 的模型接入层 |
| 工具放哪里 | 顶层 `tools` | `input[0].tools`,这一项的类型叫 `additional_tools` |
| 基础指令放哪里 | 顶层 `instructions` | 下一条 `developer` 消息 |
| 顶层 `tools` 是否存在 | 存在 | 源码设为 `None`,发出时省略 |
| 是否公开、通用 | 是 | 否,请求头明确标记为 Codex internal |
| 模型最终是否能看到工具 | 能 | 服务端支持这份内部合同才能看到 |
因此,之前真正应该表达的区别不是“GPT-5.6 改用了新的公开 API”,而是:**同一个 Codex 客户端内有两种请求编码;模型目录为某些模型选择了内部 Lite 编码。** 在 Lite 路径里,第一次可见的入口仍分两种:`exec`、`wait` 等入口可以直接点名;收在 `exec` 使用指南里的工具也能被读到,但要写成 `tools.xxx(...)` 调用。
这和机场很像:公开 API 与 Lite 是两家航空公司的登机牌版式,不是两座不同的机场。`exec` 则像其中一张联程票,票面已经写明过闸后可以到哪些柜台。
## GPT-5.6 Sol/Terra 第一次能直接点哪些入口
在默认 App/CLI 配置、存在执行环境、未被企业策略关闭的前提下,核心顶层入口可以归纳为:
| 首轮顶层入口 | 它给模型的能力 | 为什么留在顶层 |
| --- | --- | --- |
| `exec` | 执行一段 JavaScript,在同一个 cell 里编排多个嵌套工具,并只返回压缩后的结果 | Code Mode 的总入口,减少多次模型—工具往返 |
| `wait` | 等待先前 yield 的长时间 `exec` cell | 等待期间不占住一次长工具调用 |
| `request_user_input` | 在允许的协作模式中向用户提短问题 | 它是人与 Agent 的控制面,不适合藏进代码编排 |
| `collaboration` | 一组多 Agent 协作动作,包含创建、发消息、等待和中断子 Agent 等 | 这些动作负责协调多个 Agent,源码选择让它们保持直接可用 |
`collaboration` 看起来是一个入口,里面实际装着六个协作动作。Luna 使用另一套多 Agent 方式,所以第一次看到的入口会不同;达到子 Agent 层数限制后,协作入口还可能被关闭。
## `exec` 里面又有哪些工具
`exec` 不是 Shell 的别名,而像一张能在候机楼内转乘的通票。Codex 会把命令执行、文件修改、外部服务和工作编排等能力汇总进去。常见成员如下:
| 类别 | 常见工具 | 出现条件 |
| --- | --- | --- |
| 命令执行 | Windows 常见 `shell_command`;启用 Unified Exec 时是 `exec_command` + `write_stdin` | 必须有执行环境;具体形态由模型与 Feature 决定 |
| 文件修改与检查 | `apply_patch`、`view_image` | 模型支持相应工具,且当前环境允许 |
| 工作编排 | `update_plan` | 配置默认开启,可显式关闭 |
| MCP 资源 | `list_mcp_resources`、`list_mcp_resource_templates`、`read_mcp_resource` | 至少连接了一个 MCP Server |
| App 扩展 | `web.run`、`image_gen.imagegen` | 模型接入、登录状态、图片能力和网络均满足条件 |
| 插件安装 | `request_plugin_install`,有时还有候选列表工具 | Apps、Plugins、ToolSuggest 开启且确有候选项 |
| 其他扩展 | Goal、自动化、App Server 动态工具等 | 相应扩展已经安装或由客户端加入 |
所以“真正干活的代码已经写好”不等于“模型第一次就能看到这个工具”。程序还要依次决定:**把工具加入总表 → 判断一开始就给、搜索后再给还是不给 → 决定是否收进 `exec` → 生成最终请求**。运行环境缺失、功能关闭、模型服务不支持、账号权限不足或工具重名,都可能让某个工具中途消失。
## 那 `tool_search` 到底在不在第一次请求里
程序只有同时满足两件事,才会把 `tool_search` 加入工具总表:
- 当前模型声明“我会使用工具搜索”,模型接入层也支持成组工具;
- 工具总表里确实存在至少一个“需要搜索后再给模型”的工具。
在 `rust-v0.147.0` 中,外部 MCP 工具可以先进入工具总表,但标记成“搜索后再给”。这样它们的完整说明书不必全部塞进第一次请求。模型调用 `tool_search` 后,程序才把最相关的完整工具说明放进下一次请求,默认最多返回 8 个。
这里有一个容易错过的源码细节:**GPT-5.6 采用“几乎都收进 `exec`”的方式时,负责组装工具的代码恰好跳过了 `tool_search`。** 同时,这种方式又会隐藏 `exec`、`wait` 之外的普通独立入口。因此:
- 每个工具单独摆出来时,只要存在“搜索后再给”的工具,`tool_search` 就可以成为第一次请求里的独立入口;
- GPT-5.6 采用 `exec` 集中调用时,BM25 排序代码即使已经准备好,`tool_search` 也**不会进入**模型第一次可调用的名单;模型看不见入口,自然无法发起搜索;
- 这正是公开 issue [#32101](https://github.com/openai/codex/issues/32101) 描述的桥接缺口。截至 2026-08-09,`main` 的 commit [`646f7c0`](https://github.com/openai/codex/commit/646f7c0a91b8e327d263335da68ae8ef212895ce) 仍保留该跳过分支。
这一区分很重要:**程序知道一个工具存在,不等于模型看得见;设计上准备了搜索,也不等于每种工具摆放方式都已接通。**
## 一次工具搜索到底怎样发生
先暂时放下上面的接线缺口,观察一条**模型确实能看见 `tool_search`** 的正常路径。搜索不是每轮固定执行,而是模型可以选择的一个动作。
### 不是每轮自动重搜,而是模型按需决定
Codex 没有一段“现有工具都不合适,就自动执行 BM25”的兜底程序。只有模型主动发出 `tool_search_call`,Codex 才真正开始搜索。模型可以直接使用手边的工具,也可以一开始就查目录,不需要先把现有工具逐个试错。
可以把它想成开发者走进五金店:手边已有锤子和螺丝刀,店员还能查后仓。拧螺丝时不必每走一步都查一次;遇到“测光纤”这类货架外需求时,才去翻目录。查出来的工具说明只要还留在本次会话里,后面就能继续使用,**不必每轮重复搜索同一个工具**。只有需求变了、工具目录更新、开启新任务,或旧结果已经不在模型眼前时,才需要再搜。
OpenAI 的通用做法会先给模型一点点“可发现信息”,例如某组工具叫什么、能做什么,但把冗长参数留到搜索命中后再发。Codex 的本地 BM25 路径则先给模型 `tool_search` 入口和可搜索范围,命中后再返回完整工具说明。共同点都是:**先给目录,不先搬来整个仓库。**
### 开发新 Agent 时,到底要不要使用这个区别
先从第一性原理判断:一个 Agent 只需要打通四件事——**你有哪些工具 → 模型收到哪些说明 → 模型用什么格式提出调用 → 你的程序怎样执行并返回结果**。`tools` 与 `additional_tools` 只属于第二件事的“运输包装”,不应该侵入你的业务工具代码。
| 你的场景 | 应该怎样做 | 不应该怎样做 |
| --- | --- | --- |
| 直接使用 OpenAI 公开 Responses API | 继续使用顶层 `tools`;需要官方工具搜索时,在这里加入 `tool_search` 和标记为延迟加载的工具 | 不要自己构造 `additional_tools` |
| 工具数量很少 | 直接把完整工具放进 `tools`,先保持系统简单 | 不要为了模仿 Codex 强行增加一层搜索 |
| 工具有几百个,而且创建请求时已经知道完整目录 | 使用官方托管的 `tool_search`,让 OpenAI 服务完成搜索与加载 | 不必先复制 Codex 的本地 BM25 |
| 工具随租户、项目或权限实时变化 | 使用客户端执行的 `tool_search`;模型提出搜索,你的程序用 BM25、向量或混合方法查找,再回传 `tool_search_output` | 不要把所有租户的全部工具都发送给模型 |
| Fork Codex,且模型服务明确支持 Responses Lite | 保留 Codex 的接入层,让它负责生成 `additional_tools` 和内部请求头 | 不要让业务代码直接依赖这份内部格式 |
| 接 DeepSeek、GLM、Kimi 或本地模型 | 在 Agent 内部保留统一的工具说明,再为每个模型写一个很薄的格式转换层 | 不要假设对方认识 OpenAI 的内部 `additional_tools` |
如果你现在用公开 Responses API 开发一个有大量工具的新 Agent,真正可以直接复用的是下面这种官方写法,而不是 Lite 写法:
```python
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-5.6",
input="列出客户 CUST-12345 的未完成订单。",
tools=[
{
"type": "namespace",
"name": "crm",
"description": "客户资料与订单工具。",
"tools": [
{
"type": "function",
"name": "list_open_orders",
"description": "按客户 ID 查询未完成订单。",
"defer_loading": True,
"parameters": {
"type": "object",
"properties": {
"customer_id": {"type": "string"}
},
"required": ["customer_id"],
"additionalProperties": False,
},
"strict": True,
}
],
},
{"type": "tool_search"},
],
parallel_tool_calls=False,
)
```
这段代码表达的设计才值得带走:先给模型 `crm` 这本目录,需要时再加载 `list_open_orders` 的完整参数。至于底层最终用顶层 `tools`、Lite 输入项,还是另一个模型厂商的格式,应由最外层的模型接入代码转换。
### 搜索结果怎样进入上下文,又能用多久
一次客户端执行的完整时序如下:
```text
模型:tool_search_call(query="calendar")
Codex:BM25 排出前几个 → 取回完整工具说明
上下文尾部:tool_search_output { call_id, status, tools: [...] }
模型:function_call(name="create_event", ...)
后续轮次:仍可调用已加载的 create_event
```
`tool_search_output` 是请求里专门存放搜索结果的数据块,不是 system 文本或 user 文本,也不会回头修改第一次的 `additional_tools`。Codex 会把“搜索请求”和“搜索结果”一起记进会话历史。下一次请求带上这段历史后,模型就能使用结果里的完整工具说明。新内容放在上下文末尾,还能尽量保持前半部分不变,提高缓存复用机会。
“以后都能用”要加一个边界:它指同一活跃会话里,该输出仍在传给模型的上下文中。它不是进程级永久安装;新会话、上下文压缩策略或工具目录更新,都可能重新触发发现。可以把它想成把后仓取来的说明书夹进本次工单,而不是把机器永久焊在工作台上。
### 为什么稳定版只给 GPT-5.6 选择 Code Mode Only
源码能证明的是:稳定版给 GPT-5.6 Sol、Terra、Luna 都选择了“几乎都通过 `exec` 调用”的方式,GPT-5.5 没有。源码只把它记录成**这个模型适合哪些调用方式**,并没有写“因为 5.6 更聪明,所以开启”。而且 `tool_search` 本身并非 5.6 独占,官方文档说 GPT-5.4 及更新模型也支持。
下面是工程推断,不是源码原话:这种方式要求模型写对 JavaScript、按工具说明填写参数、处理异步和错误,还要在一次执行里筛选结果。更强的代码、推理和工具调用能力显然有帮助;但能否开启还取决于专门训练、评测、模型服务是否支持和发布策略。**“强模型让它更可行”是合理解释,“只因为模型更强”则过度简化。**
## 从几百份说明书缩到几份,真正省了什么
假设 Agent 接入 N 个工具,每份完整说明平均占 S 个 Token。全部塞给模型时,一轮请求里的工具部分近似是:
```text
Full cost ≈ N × S
```
改成“需要时再加载”后,平时只放搜索入口和简短目录,命中后只取 K 个工具:
```text
按需加载 ≈ 简短目录 + 搜索入口 + K × S
```
当 `K ≪ N`,节省的不只是 Token。模型面对的相似名称和参数组合变少,最终 Tool 选择也更容易。严格说,`tool_search` 没有提高模型参数中的“智力”;它通过**减少噪声、提供恰当信息、把大决策拆成两次小决策**,提高了 Agent 的有效智能。
这不是无条件免费:搜索会多一次“模型 → 搜索程序 → 模型”的往返,还可能漏掉正确工具。只有十几个工具、每轮几乎都会用时,全部摆出来可能更快;工具达到几百或几千、每次只用少数时,先搜索再取前几个才明显占优。这个判断不依赖 Codex 或 Rust。
## BM25 不是“不要关键词”,而是“给关键词排座次”
常有人问:为什么不用关键词匹配,反而用 BM25?其实 **BM25 仍然在匹配关键词,只是会给结果排先后**。它不真正理解词义,也不知道“订会议”和“安排日程”表达的是同一件事。普通匹配只回答“有没有这个词”,BM25 还会判断“哪一张工具卡更值得排在前面”。

先用一条概念式记住三个力:
```text
score(D, Q) = Σ IDF(t) × TF_saturation(t, D) × length_normalization(D)
```
这不是代码里的完整公式。`bm25` 2.3.2 实际采用的标准形式是:
```text
score(D, Q) = Σ IDF(qᵢ) ×
f(qᵢ,D) × (k₁ + 1)
─────────────────────────────────────────
f(qᵢ,D) + k₁ × (1 - b + b × |D| / avgdl)
```
其中 `f(qᵢ,D)` 是查询词在这张工具卡里出现的次数,`|D|` 是卡片长度,`avgdl` 是全部卡片的平均长度。Codex 直接调用 `with_documents(...).build()`,没有覆盖参数,因此使用 crate 默认值 `k₁ = 1.2`、`b = 0.75`;建索引时会由整批文档拟合 `avgdl`。换句话说,Codex 负责“把哪些工具卡交给馆员”,crate 负责“分词、统计和按公式排队”。
仍用图书馆来理解:
- **稀有词更值钱(IDF)**:几乎每张工具卡都写着 `get`,它像“本馆藏书”一样没什么区分度;只有少数卡片写着 `calendar`,这个词更能定位目标;
- **重复会饱和**:描述里写三次 `calendar` 可以增强信号,但不会比写一次机械地强三倍,避免关键词堆砌霸榜;
- **长描述要归一**:一本厚说明书天然更容易碰到查询词,BM25 会校正长度,避免“写得最多”自动等于“最相关”;
- **直接排出前几个**:`tool_search` 默认需要 8 个按相关程度排好的候选,而不是一大包无序的“命中 / 未命中”。
Codex 不只搜索工具名。它会把原名、把下划线换成空格后的名字、用途说明、参数名和参数说明拼成一张可搜索卡片。例如同时写入 `create_event` 和 `create event`,可以让搜索程序分别看到 `create` 与 `event`。外部 MCP 工具还会加入服务名、标题、插件名等信息。卡片写得越接近用户真正会说的话,越容易搜对。
## 源码深挖(可以跳过):Codex 怎样接上 BM25
如果只想把设计搬进自己的 Agent,记住“工具卡 → BM25 排名 → 用编号取回完整工具说明”已经够了。下面这一节只负责用真实源码证明三件事:工具卡从哪里来、排名结果怎样找回工具、工具说明怎样交给下一轮模型。
先说最容易误解的一点:**Codex 没有自己手写一遍 BM25 公式。** `codex-rs/Cargo.toml` 依赖 [`bm25 = "2.3.2"`](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/Cargo.toml#L294),分词、文档频率、平均文档长度和评分由这个内存检索库完成;Codex 写的是围绕它的数据准备与结果映射。
不懂 Rust 也可以先把实现读成四行伪代码:
```text
cards = deferred_tools.map(search_text + loadable_schema)
engine = BM25.build(cards.search_text)
ids = engine.search(query, limit = 8)
return merge(cards[id].loadable_schema)
```
下面的 Rust 代码只是这四步的强类型版本;可以把 `.filter()`、`.map()`、`.collect()` 分别当成列表的筛选、转换和收集。
### 第一步:只收集“需要搜索后再给”的工具
`append_tool_search_executor()` 从工具总表里挑出“需要搜索后再给”的工具,再向真正管理该工具的代码索取可搜索文字。下面保留真实源码名称,方便核对:
```rust
let search_infos = registry
.entries()
.filter(|tool| tool.exposure.is_deferred())
.filter_map(|tool| tool.runtime.search_info())
.collect::>();
registry.register_trusted(
tool_search_handler_cache.get_or_build(search_infos, source_listing),
);
```
每个条目把“用于搜索的卡片文字”和“命中后要返回的工具定义”绑在一起:
```rust
pub struct ToolSearchEntry {
pub search_text: String,
pub output: LoadableToolSpec,
}
```
`search_text` 是卡片正面,用来搜索;`output` 是卡片背面,保存命中后要交给模型的完整工具说明。它就像图书馆索引卡:正面供馆员查找,背面写着怎样取到真正的书。
### 第二步:给每张卡一个数字编号
[`ToolSearchHandler::new()`](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/tools/handlers/tool_search.rs#L76-L110) 枚举所有卡片,把 `search_infos` 的数组下标作为 BM25 文档 ID:
```rust
let documents = search_infos
.iter()
.map(|info| info.entry.search_text.clone())
.enumerate()
.map(|(idx, text)| Document::new(idx, text))
.collect();
let search_engine =
SearchEngineBuilder::::with_documents(
Language::English,
documents,
)
.build();
```
这个 `usize` 很关键。BM25 引擎只需返回诸如 `7、2、11` 这样的有序文档 ID,Codex 就能用它们回查 `search_infos[7]`、`search_infos[2]`、`search_infos[11]`。没有数据库主键,也没有第二张映射表,数组位置本身就是“取书号”。整个语料一次性装进内存,并明确采用 `Language::English`。
### 第三步:取排名靠前的编号,再换回完整工具说明
调用时,真正执行搜索的代码会先去掉首尾空格,拒绝空查询和 `limit = 0`;没有指定数量时默认取 8 个。核心代码很短:
```rust
let results = self.search_engine
.search(query, limit)
.into_iter()
.map(|result| result.document.id)
.filter_map(|id| self.search_infos.get(id))
.map(|info| &info.entry);
coalesce_loadable_tool_specs(
results.map(|entry| entry.output.clone()),
)
```
这里有两个值得注意的细节:
1. Codex 没有把 BM25 的数值分数继续传给模型,只使用检索库已经排好的顺序;
2. `coalesce_loadable_tool_specs()` 会把同一工具组里的多个命中合在一起,避免返回几份重复外壳。
于是完整数据流就是:
```mermaid
flowchart TD
A["搜索后再给的工具"] --> B["可搜索文字 + 完整工具说明"] --> C["带数字编号的 BM25 卡片"] --> D["排名靠前的编号"] --> E["按编号取回完整工具说明"] --> F["合并同一工具组"] --> G["放进下一次模型请求"]
classDef out fill:#d3f9d8,stroke:#2f9e44
class G out
```

### 一个查询怎样走完全程
把查询设为 `calendar`。索引中的某张卡片可能包含下面这样的文字(为便于阅读做了缩短):
```text
create_event create event Create a calendar event title start_time end_time
```
英文分词器会统一大小写、去掉一些没有区分度的常见词,并把词形尽量归一。BM25 如果把这张卡排在前面,程序就通过数字编号找回与它绑定的完整工具说明。**搜索命中的是卡片编号,不是直接执行工具;用编号取回说明后,模型才能调用。**
随后 [`ToolSearchOutput::to_response_item()`](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/tools/context.rs#L149-L185) 把结果包装成下一次请求能携带的数据。源码测试验证的格式如下;这里只保留一个空参数对象:
```json
{
"type": "tool_search_output",
"call_id": "search-1",
"status": "completed",
"execution": "client",
"tools": [{
"type": "function",
"name": "create_event",
"description": "",
"strict": false,
"defer_loading": true,
"parameters": {"type": "object", "properties": {}}
}]
}
```
它不是普通文字,也不会回头修改第一次请求;它以 `tool_search_output` 的名字进入会话历史,并随下一次请求送回模型服务。这样模型就能使用本次搜索找到的完整工具说明。上面的测试只证明“结果怎样包装”,真正的先后排名由前面的 `SearchEngine::search()` 完成。
最后还有一个省时间的办法:[`ToolSearchHandlerCache`](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/tools/handlers/tool_search.rs#L34-L74) 会检查工具目录是否改变。没变化就继续使用已经建好的内存索引;MCP、插件或动态工具变化后才重建。它像图书馆继续使用昨天的卡片柜,只有进了新书才重新编目。
对应源码入口可从 [`append_tool_search_executor()`](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/tools/spec_plan.rs#L1165-L1185)、[`ToolSearchHandler`](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/tools/handlers/tool_search.rs#L76-L197) 和 [`ToolSearchEntry` / `search_text` 构造](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/tools/src/tool_search.rs#L12-L150) 三处顺着读下来。
## 换成 Python 或 Go,照样能做
真正需要照搬的只有三步,与编程语言无关:
```text
build(cards: [{id, search_text, schema}]) -> index
search(index, query, top_k) -> ranked_ids
load(cards, ranked_ids) -> callable_schemas
```
Codex 用 Rust 列表保存卡片、用 `bm25` 库排名、用数字编号取回完整工具说明。换一种语言只是在替换容器和库:
| 实现环境 | 怎样存卡片 | 怎样排名 | 命中后怎样取说明 |
| --- | --- | --- | --- |
| Rust / Codex | `Vec` | `bm25` crate | `search_infos[id]` |
| Python Agent | list / dataclass / dict | 任意 BM25 库或搜索服务 | `cards[id]["schema"]` |
| Go Agent | `[]ToolCard` struct | 任意 Go BM25 实现或搜索服务 | `cards[id].Schema` |
| 多服务架构 | 数据库或工具总表 | 独立搜索服务 | 通过工具编号拉取完整说明 |
真正需要复刻的是数据契约,不是源码写法:
1. 为每个工具生成高质量的可搜索文字,至少包含名称、动作、对象、参数和风险词;
2. 让 BM25 返回按相关程度排好的工具编号,而不是一堆无序的“包含关键词”;
3. 用编号取回完整、可检查的工具说明,并交给下一轮模型;
4. 记录命中率、误召回、最终调用成功率和新增往返延迟,再决定 K 和是否增加向量召回。
因此,业务 Agent 即使完全由 Python 或 Go 编写,也可以照搬这个设计。BM25 的公式、输入卡片和排名结果不会因为编程语言改变;Rust 只是 Codex 选用的一种实现方式。
## 为什么这个场景选 BM25 很合理
源码明确告诉我们“用了什么”,但没有留下“为什么选它”的设计备忘录。根据数据特点判断,BM25 很适合从大量工具里找候选:
1. **工具说明短,而且技术词很明确。** `calendar`、`pull_request`、`spawn_agent`、`issue_number` 这类名字和参数本来就适合按关键词找;
2. **可以完全在本机建立索引。** 不需要请求向量模型,也不需要维护向量数据库;
3. **工具目录经常变化。** MCP、插件或动态工具可能随时增减,BM25 重建快、成本低,也容易解释为什么排在前面;
4. **这里只需要缩小范围,不需要回答开放问题。** 搜索把几十或几百张工具卡缩成几张,再让模型做最终选择;
5. **精确技术词不能被“语义相似”冲淡。** `delete_issue` 与 `get_issue` 语义相关,但权限风险完全不同。名称与参数的词法证据在工具选择里很重要。
简单的“字符串里有没有这个词”不会认真排序;向量搜索虽然更擅长同义词和跨语言,却要增加模型服务、索引维护,也可能把“意思相近但不能乱用”的工具排在一起。对短小的工具说明来说,BM25 是一个务实的中间选择。
## BM25 的边界也很清楚
当前实现只按英文文字搜索,没有再加向量搜索、同义词表、工具名额外加分或第二轮精排。因此这些查询并非它的强项:
- 中文问“安排会议”,工具只写 `create_calendar_event`;
- 用户说“查代码评审”,工具只写 `pull_request_review`;
- 两个工具的说明大量使用相同套话,真正差异没有明确写出来。
这也是为什么工具名称、用途和参数说明不能随便写。BM25 像馆员,但只能阅读卡片上已有的字。好的卡片要写出用户会说的动作、对象、限制和危险性。工具更多、语言更多或表达更灵活时,可以升级成 **BM25 先找一批 + 工具名完全命中时加分 + 再做一次轻量精排**,不必立刻全部改成向量搜索。
## BM25 能不能用来搜索记忆
**能用,但更适合负责“按原词找一批候选”,不能单独承担完整的长期记忆。** 还要先澄清一个源码事实:这个稳定版 Codex 搜索记忆时并没有使用 BM25,而只是查找“文字里是否包含查询内容”,再按文件路径和行号整理结果。
为什么 BM25 仍值得用在 Memory?错误码、函数名、文件路径、项目代号、人名和已经写入的决策措辞,都是非常强的词法锚点。用户问“上次 `EADDRINUSE` 怎么处理”,BM25 往往比纯向量更稳定,也比子串匹配更会给结果排序。
但记忆比工具卡更难:同一件事可能换一种说法,旧结论可能被新结论推翻,还要考虑“谁的记忆、哪个项目、多久以前、可信度多高”。BM25 不理解这些关系:
| 记忆搜索需求 | 只用 BM25 的效果 |
| --- | --- |
| 精确名称、路径、错误码、API | 强 |
| 同义改写、跨语言表达 | 弱 |
| 用户 / 项目 / 时间范围 | 需要先按这些信息筛选 |
| 新旧结论冲突、可信度和重要性 | 需要额外排序与治理 |
一个更可靠的生产方案是:
```mermaid
flowchart TD
A["先按用户 / 项目 / 时间筛选"] --> B1["BM25 按原词找"] & B2["向量搜索按意思找"]
B1 & B2 --> C["合并两边的候选"]
C --> D["按新鲜度、重要性、可信度、使用次数加权"] --> E["再做一次轻量精排"] --> F["返回原文片段与出处"]
classDef out fill:#d3f9d8,stroke:#2f9e44
class F out
```
对于很小、主要存技术日志的本地记忆,先用 BM25 完全合理;它至少会比简单的“包含文字”多一层有效排序。自然语言转述、跨语言和冲突记忆增多后,再升级成“按原词找 + 按意思找”。这是通用建议,**不是说 Codex 当前已经这样实现。**
## 开源 Codex 可以换模型,但不是只改一个名字
准确的说法不是“把 GPT-5.6 变成 DeepSeek、GLM 或 Kimi”,而是:**保留 Codex 这套 Agent 外壳,换掉背后的模型和模型接入层。** Codex CLI、SDK 和 App Server 等核心组件已经开源,所以沙箱、操作确认、工具总表、MCP 外部工具、会话历史和执行循环都可以继续复用。
最小控制循环与具体模型品牌无关:
```mermaid
flowchart TD
A["用户任务 + 会话记录 + 工具说明"] --> B["模型接入层"]
B --> C["工具调用 / 文字回答"]
C --> D["Codex 执行、请求确认、记录结果"]
D -->|"下一轮模型请求"| B
classDef swap fill:#e5dbff,stroke:#6741d9
class B swap
```
但模型替换不是只改一行 `model = "..."`。至少要保证三层能够对上:
| 适配层 | 必须对齐什么 | 没对齐时的表现 |
| --- | --- | --- |
| 网络连接 | 地址、密钥、请求头、流式连接、失败重试 | 连不上、401、回答到一半中断 |
| 数据格式 | 问题、工具调用、工具结果、流式事件和错误分别怎样表示 | 能聊天,但一调用工具就断 |
| 模型能力 | 能读多长、能否稳定按格式调用工具、能否并行、能否搜索工具 | 请求能跑,但 Agent 变笨或功能消失 |
官方配置允许自定义模型接入层。如果业务网关已经支持 Codex 使用的 Responses 请求格式,最短配置类似:
```toml
model = "your-business-model"
model_provider = "business_gateway"
[model_providers.business_gateway]
name = "Business model gateway"
base_url = "https://llm-gateway.example.com/v1"
env_key = "BUSINESS_LLM_API_KEY"
wire_api = "responses"
```
这里必须结合本文固定版本加一句重要限制:`rust-v0.147.0` 的 [`WireApi`](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/model-provider-info/src/lib.rs#L49-L79) 只接受 `responses`,并明确拒绝 `wire_api = "chat"`。因此接入 DeepSeek、GLM、Kimi 或其他业务模型时:
- 若服务端完整兼容 Responses、流式回答和工具调用,可以先从自定义模型接入配置开始;
- 若只有 Chat Completions 一类接口,需要加一个网关,在 Codex 格式与厂商格式之间双向翻译;也可以直接修改开源的模型接入代码;
- “接口能返回文字”不等于 Agent 已适配完成,还要实际测试工具调用、结果回传、长对话、错误恢复和并行调用。
还有一个关键细节:Codex 不认识的新模型会采用一套 [`保守默认设置`](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/models-manager/src/model_info.rs#L137-L180),先关闭工具搜索、精简请求、`exec` 集中调用和并行工具。也就是说,换模型后,简单的“模型点名工具 → 程序执行 → 结果交回”可能先跑起来,但 GPT-5.6 的特殊优化**不会自动继承**。要重新打开它们,必须补充该模型的能力说明,并用实际任务证明它确实做得稳。
对业务团队来说,最快且风险最低的迁移顺序是:
1. 先让新模型完成文字回答和少量直接工具调用;
2. 验证参数填写、工具调用与结果能否成对、回答中断后能否恢复、长对话能否压缩;
3. 用业务任务集比较成功率、成本和延迟,而不是只看聊天效果;
4. 再逐步打开并行调用、按需搜索工具、`exec` 集中调用等高级能力。
这才是开源 Agent 框架的真正复用价值:不必重写成熟的执行和安全底座,只替换模型接入部分,再把业务工具、规则、记忆和评测加在自己可控的层上。
## 最值得带走的七点
第一,第一次请求不会把所有工具都塞给模型;程序会根据模型、环境和功能开关,决定哪些一开始就给、哪些搜索后再给、哪些不给。
第二,公开 Responses API 把工具放在顶层 `tools`;Codex 的内部 Responses Lite 才把它们放进 `additional_tools` 输入项。两者都是结构化数据,不是藏在 system 或 user 提示词中。
第三,`tool_search` 由模型按需选择,不是每轮自动执行;搜到的工具说明留在同一会话里,后面可以继续使用。
第四,把大量工具收进 `exec` 需要模型更会写代码和组织工具,但源码没有证明“模型更强”是 GPT-5.6 使用它的唯一原因。
第五,BM25 仍然是关键词搜索,只是会认真排序。它适合工具说明,也适合从记忆中找错误码、路径等原词;完整记忆还需要按项目和时间筛选、按意思搜索、再次精排。
第六,“可搜索文字 → 排名后的编号 → 完整工具说明”与语言无关,可以在 Rust、Python、Go 或独立搜索服务中复现。
第七,开源 Codex 可以替换模型,但网络连接、请求格式和模型能力必须同时对上;新模型不会自动继承 GPT-5.6 的工具搜索和 `exec` 调用方式。
## 源码索引
- [GPT-5.6 Sol 模型能力与 Tool Mode](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/models-manager/models.json#L4-L22)
- [GPT-5.6 Terra 的 Code Mode Only 配置](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/models-manager/models.json#L119-L137)
- [GPT-5.6 Luna 的 Code Mode Only 配置](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/models-manager/models.json#L232-L250)
- [GPT-5.5 相邻模型条目没有选择 Code Mode Only](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/models-manager/models.json#L341-L359)
- [OpenAI Tool Search 官方指南:Deferred Loading、调用时序与未来轮次复用](https://developers.openai.com/api/docs/guides/tools-tool-search)
- [OpenAI Programmatic Tool Calling 官方指南](https://developers.openai.com/api/docs/guides/tools-programmatic-tool-calling)
- [Codex 自定义 Model Provider 官方配置](https://learn.chatgpt.com/docs/config-file/config-advanced#custom-model-providers)
- [Codex 开源组件边界](https://learn.chatgpt.com/docs/open-source)
- [Tool Registry、Exposure 与首轮可见 Spec 规划](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/tools/spec_plan.rs#L319-L486)
- [核心 Tool 的条件化注册](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/tools/spec_plan.rs#L818-L1118)
- [Responses Lite 的 `additional_tools` 请求封装](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/client.rs#L849-L885)
- [Responses Lite 测试:内部请求头、顶层字段缺席与输入项顺序](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/tests/suite/responses_lite.rs#L28-L139)
- [`tool_search` 的 BM25 Engine 与 Top-K 返回](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/tools/handlers/tool_search.rs#L76-L169)
- [Tool Search 索引文本如何由名称、描述和 Schema 组成](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/tools/src/tool_search.rs#L23-L150)
- [`tool_search` 结果如何变成下一次请求的专用输入项](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/tools/context.rs#L149-L185)
- [`tool_search_call` / `tool_search_output` 如何保留在会话历史](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/context_manager/history.rs#L350-L386)
- [MCP Tool 的 Direct / Deferred 注册策略](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/core/src/mcp_tool_exposure.rs#L17-L89)
- [BM25 crate 版本:2.3.2](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/Cargo.toml#L294)
- [`bm25` 2.3.2 的公式与默认参数](https://github.com/Michael-JB/bm25/blob/8ef726045b41702e148d8996d344f3500844fde1/src/embedder.rs#L146-L205)
- [`bm25` 2.3.2 默认分词器的切词、归一、停用词与词干化](https://github.com/Michael-JB/bm25/blob/8ef726045b41702e148d8996d344f3500844fde1/src/default_tokenizer.rs#L263-L289)
- [Codex Memory Tool 当前的子串查询语义](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/ext/memories/src/tools/search.rs#L28-L62)
- [Codex Memory 本地搜索与结果排序实现](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/ext/memories/src/local/search.rs#L17-L88)
- [稳定版只接受 Responses Wire API,并拒绝旧 `chat` 配置](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/model-provider-info/src/lib.rs#L49-L79)
- [未知模型怎样落到保守的 fallback capability metadata](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/models-manager/src/model_info.rs#L137-L180)
> 源码核对日期:2026-08-09。稳定版固定到 `be6e8eac`,同时核对了当日 `main` 的 `646f7c0a`;BM25 库固定到 `v2.3.2` 的 `8ef72604`。模型能看到哪些工具以及模型目录都会继续变化,排查具体环境时应以实际请求和对应 commit 为准。
> 配图生成说明:中英文两版共六张技术图,均通过 Codex 内置 `image_gen.imagegen` 生成或编辑;在固定版本源码中,该扩展把 [`IMAGE_MODEL` 写死为 `gpt-image-2`](https://github.com/openai/codex/blob/be6e8eac029b183056b7e4402879f15d2c85f61b/codex-rs/ext/image-generation/src/tool.rs#L53-L58),并用它构造图片请求。发布前人工复核了模式状态、返回数量、示意编号和中英文标签。
---
title: "读完 Codex 源码后,我认为最值得企业 Agent 借鉴的是这 5 个设计"
description: "基于 openai/codex 源码,拆解选择性并行、Unified Exec、ExecCell、Skills 渐进上下文、Goal 自动续跑,以及如何通过 App Server 嵌入同一套 Runtime。"
url: "https://zhuoqidev.com/posts/codex-agent-design/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-08-02"
updated: "2026-08-03"
series: "Agent 架构深度"
tags: ["Codex","OpenAI","AI Agent","Agent Harness","Tool Calling","Agent Runtime","Skills","Goal","App Server"]
translation: "https://zhuoqidev.com/en/posts/codex-agent-design/index.md"
---
# 读完 Codex 源码后,我认为最值得企业 Agent 借鉴的是这 5 个设计
> **TL;DR** 可以把 Codex 想成一支小型施工队:模型像会判断下一步的现场负责人,Agent Harness 则是围绕他的派工台、门禁、档案柜和进度看板。源码真正展示的,不只是“负责人会下命令”,而是整套系统如何让工作安全开工、暂停后接着做,并让用户始终知道事情进行到了哪里。
很多 Agent 教程只有这样一个循环:
```python
while True:
response = model(messages, tools)
if response.tool_calls:
messages += execute(response.tool_calls)
else:
return response.text
```
它没有错,只是省略了真正困难的部分:几张工单能否同时开工?测试十分钟不退出时由谁保管现场?每次查看进度是否都要打扰用户?一句“还在检查”会不会被误认为已经交付?用户不知道操作手册叫什么时,系统能否主动找到?用户明确说“不要停”后,任务怎样跨越多轮处理仍不丢失?
这次我没有再从界面现象反推实现,而是阅读了官方 [openai/codex](https://github.com/openai/codex) 仓库在 commit [`bb5054f`](https://github.com/openai/codex/commit/bb5054fe47abe73ecbbd454751066a28c89f4bb9) 的 Rust 源码。下文的组件名、状态分支和数值都能在对应源码中找到;产品未来仍可能演进,但这些设计已经足够回答一个问题:**企业 Agent Harness 应该替模型承担什么?**
## 先看懂:Agent 的一轮 Turn 到底做了什么
把一次完整用餐看成一个 Thread:它是整件事情的总账。前菜、主菜和甜点可以是不同 Turn;每个 Turn 都是从“用户提出这一轮要求”到“这一轮结果真正交付”的完整周期。

顺着图走,一轮通常包含六件事:
1. 用户提出本轮要求,Runtime 同时准备对话历史、环境规则、可用工具以及 Skill/Goal 状态;
2. 模型判断下一步,可以播报进度、更新计划、开出 Tool Call(工具工单),也可以直接给 Final;
3. 若调用工具,Harness 先检查权限、隔离环境、并行与等待规则,再让相应工位执行;
4. Tool Result(工具结果和证据)返回模型。事情没做完时,模型会继续判断并再次调用工具,所以一次 Turn 可以包含零次、一次或多次 Tool Call;
5. 用户在进行中可以 steer(纠偏);Commentary(进度播报)用户看得到,但不会关闭 Turn;
6. 模型验证要求和证据后给出 Final,Runtime 再发出 `turn/completed`,这一轮才真正结束。若 Goal 仍是 active,线程空闲后还会自动开启下一个 Turn。
后面遇到英文源码名时,不必记住拼写。只要先判断它对应图里的“资料、工单、工位、结果、播报还是交付”,理解就会容易很多。
## 1. 像图书馆门禁一样管理并行任务
先想象一座图书馆:许多读者可以同时进阅览室,因为大家都只是看书;但管理员要移动书架、重新编号时,其他人就得暂时等一下,否则有人读到一半,书的位置却变了。
Codex 的并行控制就是这个思路。模型一次可能开出多张 Tool Call(工单),但 Runtime 不会不加判断地把它们全部 `spawn`——也就是“一声令下,同时开工”。[`ToolCallRuntime`](https://github.com/openai/codex/blob/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/core/src/tools/parallel.rs) 像门口的值班员,`Arc>` 则是那套共享门禁。无需理解这串 Rust 类型,只要记住它有两种通行方式:
- “只读通行”:已证明可以并行的工位拿共享钥匙,许多任务能同时进入;
- “独占通行”:可能改变共享状态的工位拿唯一钥匙,其他任务必须在门外等候;
- 新 Handler(接单工位)默认没有并行通行证,只有主动声明安全才开放;
- `shell_command`、`exec_command` 与 `write_stdin` 是源码里明确申请了并行通行证的例子。

并行之后还有一个小心思:后厨可以先做完第三张单,但服务员仍按原工单顺序把结果放回模型,避免上下文忽前忽后。源码测试专门验证了这一点。
图底部的几块“秒表”也可以这样理解:`dispatch duration` 是排队和派单时间,`handler duration` 是工位真正干活的时间,`total duration` 是从收到工单到彻底结束的总时间。`CancellationToken` 则像全场停工广播;收到取消后,某些工位可以先收拾现场,再报告 `aborted by user`(已被用户中止)。
企业里最值得复制的不是 `RwLock` 这个具体类型,而是三条规则:
1. 并行是工具能力声明,不是模型凭感觉决定的特权;
2. 默认保守,只有证明安全的工具才 opt in;
3. 执行可以乱序,提交给模型的上下文必须稳定、有序、可重放。
这比在 Prompt 里写“能并行时尽量并行”可靠得多。模型负责找出可能独立的工作,Harness 负责守住实际并发边界。
## 2. 长命令像取号办事:窗口可以让开,事情不能丢
去办证大厅时,小业务可以站在窗口前等;需要审核半小时的业务,工作人员会先登记材料,再给你一张取号单。你可以离开窗口,稍后凭号码回来查询,而不是一直堵住队伍。
Codex 的 [`unified_exec`](https://github.com/openai/codex/tree/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/core/src/unified_exec) 就把长命令设计成这样的“取号业务”,而不是只有 `command → stdout` 的一次性问答:
1. `ToolOrchestrator` 是受理柜台:检查是否得到批准(Approval),并决定把命令放进哪块隔离工作区(Sandbox);
2. PTY 或 pipes 是与命令保持联系的“电话线”,让系统继续接收输出或输入字符;
3. `ProcessStore` 是业务登记簿。**Codex 先登记进程,再让当前窗口结束等待(yield)**,这样即使这一轮 Turn 被打断,后台任务也不会因为无人保管而消失;
4. 命令很快结束就直接交结果;还没结束就给模型一张 `session_id/process_id` 取号单;
5. `write_stdin` 相当于凭号回来:既能补充输入,也能什么都不写、只问“办好了吗?”同一号码一次只接待一个查询,不同号码可以同时查询。
等待多久也不是随意猜的。源码把观察窗口写进工具说明:初次最多观察 250–30,000ms,Windows 至少观察 10 秒;补充输入后最多等 30 秒,只查询状态时可以安静等待 5–300 秒。这样 Agent 不必每秒追问一次。
命令输出太长时,系统也不会把整本流水账塞给模型。默认最多返回约 10,000 tokens,并受 1MiB 上限保护;`head-tail buffer` 就像一份事故报告只保留“开头发生了什么”和“最后结果怎样”,中间省略多少会明确标出来。

但“用户看得到进度”不等于“把打印机吐出的每一行纸都递给用户”。Codex TUI 的 [`ExecCell`](https://github.com/openai/codex/blob/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/tui/src/exec_cell/model.rs) 更像快递 App:十几个扫描事件不会变成十几张卡片,而会被压缩成“正在运输”。
源码只会把 `Read / ListFiles / Search` 这类查看、列目录和搜索操作归入 `Exploring`,其他命令单独显示。`call_id` 就像快递单号:结束事件必须回到原来的那张卡片;没有对应单号的事件宁可单独显示,也不能误塞进别的任务,藏住尚未完成的工作。

连续读取会合成一行并去重文件名,重复的等待记录可以不再显示;实时预览最多 50 行、1MiB,并保留开头与结尾。用户默认看到的是“查了哪些地方、现在什么状态”,需要排错时才展开完整命令记录(transcript)。
阶段性 commentary 也可以用装修来理解:“水电已完成,正在贴砖”是进度播报,不是交房。App Server 的 [`AgentMessage.phase`](https://github.com/openai/codex/blob/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/app-server-protocol/src/protocol/v2/item.rs) 能区分进度和 final answer(最终交付),客户端再用 `turn/completed` 确认这一轮真的关单。**一段文字可以让用户安心,却不必迫使 Agent 停工。**
## 3. Skills 像“菜单 + 操作手册”,不用把整座图书馆搬进脑子
进餐厅时,菜单只告诉你“有哪些菜、各自大概是什么”,不会把每道菜的完整配方都印上去。等你点了菜,厨师才拿对应操作手册。
Codex Skills 也是这个结构:`SkillCatalog` 是菜单,`SKILL.md` 是完整操作手册。[`skills` extension](https://github.com/openai/codex/tree/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/ext/skills/src) 从本机、执行环境、编排器和 Plugin 等来源汇总目录,先只把名称、简介和“手册放在哪里”贴到模型能看到的公告栏——源码称它为 WorldState。
公告栏空间有限,所以长地址会用别名缩短,过长简介会截短,放不下时还会明确写“另外省略了 N 个 Skill”。WorldState diff 就像物业只张贴“本次发生变化的通知”,目录没变便不用重复贴一遍。源码中的 cheap selector 目前只是暗中做效果统计,不会偷偷改变模型看到的菜单。

[`catalog_prompt.rs`](https://github.com/openai/codex/blob/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/ext/skills/src/catalog_prompt.rs) 给模型的规则很直接:用户点名某本手册,**或者当前任务明显符合菜单描述**,这一轮就必须使用它。选中后先完整读 `SKILL.md`,再按手册指引只取真正需要的参考资料、脚本或模板,避免无关知识占满上下文。
用户显式选择 Skill 时,Runtime 会按准确地址把完整手册交给模型。模型若主动打开某个 `SKILL.md`,或运行它附带的脚本,[`invocation_utils.rs`](https://github.com/openai/codex/blob/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/core-skills/src/invocation_utils.rs) 还能像图书馆借阅机一样识别“哪本手册真的被使用了”,并在同一 Turn 去重后记录一次 implicit invocation(隐式使用)。这只是使用记录,不代表预先加载了所有手册。
这套设计的企业价值是:用户只需描述目标,Harness 用一个低成本目录让模型发现工作流,领域规则在真正需要时才占用上下文。同时边界仍然清楚:**Skill 可以改变如何完成已授权任务,不能扩大用户的授权范围。**
## 4. Goal 像跨班次项目工单:员工下班了,项目不会失忆
想象一个需要三班人接力的维修项目。如果目标只写在第一班员工的便签上,换班后很容易变成“我修了一部分,先到这里”。更可靠的做法是把目标、预算、已用时间和验收条件写进中央项目工单,每一班都从同一张工单继续。
用户说“把迁移真正做完,在验证通过前不要停”,表达的正是这种跨班次要求。只把这句话留在聊天记录里,经过多次工具调用、上下文压缩或进程恢复后很容易失真。
Codex 的 [`goal` extension](https://github.com/openai/codex/tree/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/ext/goal/src) 会建立一张持久 `ThreadGoal` 工单,记录目标、状态、预算、已用 token 和耗时。`create_goal` 是建单,`get_goal` 是查单,`update_goal` 是填写最终状态。为了防止任何小事都变成无限项目,工具说明设置了严格规则:
- 只有用户或 system/developer 明确要求持久 Goal 时才能创建,普通任务不能被擅自推断成 Goal;
- `token_budget` 也只有用户明确要求时才设置;
- 模型通过 `update_goal` 只能写 `complete` 或 `blocked`;
- complete 必须有逐项证据证明目标全部达成;
- blocked 必须是同一阻碍连续出现至少 3 个 Goal Turn,而且已无法继续产生有意义的进展。

真正让项目接班的不是模型反复背诵“不要停”,而是 [`on_thread_idle`](https://github.com/openai/codex/blob/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/ext/goal/src/extension.rs) 在一班工作结束、线程空闲时调用 `continue_if_idle()`。可以把它理解成值班主管的交接检查:项目仍启用吗?有没有要求暂缓?工作现场还在吗?工单是否仍是 active?全部通过后,系统把完整目标、剩余预算和验收清单交给下一班,并用 `try_start_turn_if_idle()` 开启新的 Goal Turn。
因此 Final 只是“一班下班报告”,不会自动把整个项目标成完工。每次工具完成、Turn 结束或中止,系统都会记账;达到预算就像预付工时用完,系统把状态改为 `budget_limited`,要求停止新工作、汇报现状和下一步,而不是假装已经验收。mutation lock 则像只能由一名文员同时修改总账,避免记账、外部改工单和自动接班互相覆盖。
这正是模型智能与 Harness 设计的分界:模型从自然语言判断“这是一个明确的持久目标”并构造 objective;Runtime 保证状态、续跑、预算和终止证据不会只靠模型记忆。
## 5. 企业落地:先设计“谁做决定,谁守规矩”
这些实现可以浓缩成一个职责表:
| 问题 | 模型负责 | Harness 负责 |
| --- | --- | --- |
| 哪些工作可能独立 | 判断任务之间有没有依赖 | 发并行通行证、必要时封场,并稳定排列结果 |
| 长任务何时再看 | 选择有意义的检查点 | 保存后台进程、发取号单、支持取消并限制输出长度 |
| 用户看到什么 | 写简洁的阶段说明 | 把零散事件折叠成进度卡,同时保留完整记录 |
| 何时加载领域规则 | 根据目录选择合适 Skill | 控制菜单长度,在选中后提供完整操作手册 |
| 是否持续到目标完成 | 识别明确的长期 Goal 意图 | 保存中央工单、自动接班、计量预算并检查验收证据 |
如果要把同一套 Codex Runtime 嵌入 IDE 或企业产品,可以把 [Codex App Server](https://learn.chatgpt.com/docs/app-server) 理解成一个“标准化前台”:你的产品不必自己重写后厨,只需按统一表单送入任务,再持续接收进度、审批请求和最终结果。技术上这套表单是双向 JSON-RPC;它暴露 Thread(整件事)、Turn(一轮处理)、Item(一个事件或产物)、Skill、Goal 与 Approval 等对象。
```mermaid
flowchart TD
A["initialize"] --> B1["thread/start"] & B2["thread/resume"]
B1 & B2 --> C["turn/start"]
C --> D["消费 item/*、plan、diff、approval 与 delta"]
D --> E["只以 turn/completed
关闭本轮 UI 生命周期"]
classDef out fill:#d3f9d8,stroke:#2f9e44
class E out
```
App Server 客户端可用 `skills/list` 取得“菜单”,也可在 `turn/start` 时直接指定操作手册;Goal 可通过 `thread/goal/*` 管理,运行中可用 `turn/steer` 像给施工队发变更单一样纠偏。远程 WebSocket transport 目前仍是实验性能力,生产客户端应固定 Codex 版本,并从对应版本生成 TypeScript 类型或 JSON Schema。
真正值得复制的设计结论只有一句:
> 让模型做语义判断;把并发、时间、状态、权限、可观察性和终止证据做成 Harness 能强制执行的协议。
这比继续增加 Tool 数量更能改善 Agent 的可靠性和用户体验。一个企业 Agent 团队在上线前,至少应该能回答:并行准入在哪里?长进程由谁保存?重复轮询如何抑制?进度与 Final 如何区分?领域规则如何按需进入上下文?“不要停”如何持久化?完成声明由什么证据支持?
## 源码索引
- [选择性并行:parallel.rs](https://github.com/openai/codex/blob/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/core/src/tools/parallel.rs)、[router.rs](https://github.com/openai/codex/blob/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/core/src/tools/router.rs)、[registry.rs](https://github.com/openai/codex/blob/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/core/src/tools/registry.rs)
- [Unified Exec](https://github.com/openai/codex/tree/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/core/src/unified_exec)
- [TUI ExecCell](https://github.com/openai/codex/tree/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/tui/src/exec_cell) 与 [command_lifecycle.rs](https://github.com/openai/codex/blob/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/tui/src/chatwidget/command_lifecycle.rs)
- [Skills extension](https://github.com/openai/codex/tree/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/ext/skills/src) 与 [core-skills](https://github.com/openai/codex/tree/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/core-skills/src)
- [Goal extension](https://github.com/openai/codex/tree/bb5054fe47abe73ecbbd454751066a28c89f4bb9/codex-rs/ext/goal/src)
- [Codex App Server](https://learn.chatgpt.com/docs/app-server)
> 源码与文档核对日期:2026-08-03。本文解释的是上述 commit 的公开实现,不把当前行为描述为所有未来版本的永久 API 保证。
---
title: "Agent 如何记住你:人脑记忆史与六大开源系统代码审计"
description: "从 Ebbinghaus、H.M.、工作记忆与 engram 出发,重建 Agent 记忆发展史;再对 Mem0、Letta、Graphiti、LangMem、Cognee、MemoryOS 做宣传卖点与实际代码架构对照。"
url: "https://zhuoqidev.com/posts/llm-memory-research/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-07-30"
updated: "2026-07-30"
tags: ["AI Agent","LLM","Memory","记忆系统","认知科学","开源架构"]
translation: "https://zhuoqidev.com/en/posts/llm-memory-research/index.md"
---
# Agent 如何记住你:人脑记忆史与六大开源系统代码审计
几乎每个 Agent 项目都说自己有「长期记忆」。
有的意思是把聊天记录做 embedding,有的意思是维护一份用户画像,有的意思是让模型自己修改 Markdown,还有的已经做到了双时序知识图谱。它们都叫 memory,却不是同一种东西,也不该放在一张跑分榜上直接比较。

要判断一个系统是不是真的「会记」,我更愿意问三个问题:
1. **一次经历之后,系统里的什么状态发生了变化?**
2. **这个状态存在哪里,谁能修改,什么时候失效?**
3. **下一次行动前,它如何被准确、合规地带回来?**
这篇文章从这三个问题出发。前半段把人类记忆科学与 Agent 记忆技术放在同一条历史轴上;后半段直接读代码,对照 Mem0、Letta、Graphiti、LangMem、Cognee 与 MemoryOS 的宣传卖点、实际数据流、系统边界和对应的记忆范式。
> [!TIP]
> **先给结论:** 今天主流的 Agent 并没有获得一种像人脑那样的统一「记忆器官」。工程上真正有效的是一条闭环:**经历 → 写入门控 → 表征 → 存储 → 检索 → 上下文组装 → 行动反馈 → 巩固 / 修订 / 遗忘**。不同开源项目,只是选择接管这条闭环的不同部分。
下面这张图不是某个产品的组件架构,而是全文共用的**判断坐标系**。它要回答的不是「数据放在哪」,而是「一次过去的经历如何真正影响下一次行动」。阅读时先沿中间的七步主环看信息如何从经历变成行动;再看左侧三种载体,区分当前任务、跨会话记忆与真实世界状态;右侧说明每一步完成的变换,底部则展示长期运行后必须发生的巩固、修订与遗忘。这样能避免把数据库、Context、缓存和真实状态都笼统地叫作“记忆”。

---
## 一、先把最容易混淆的五种「记忆」分开
LLM 系统里至少有五种状态载体。它们在物理位置、写入速度、生命周期和治理方式上完全不同。
这张图专门负责**术语消歧**。横向比较五个载体,纵向依次看「谁写、能活多久、最擅长什么、不擅长什么」。它不是要找一个最好的 memory,而是避免把性能缓存当成长期记忆、把 Context 当成持久层,或把真实业务状态复制成可能过期的自然语言回忆。

### 1. 参数记忆:模型权重
预训练和微调把统计规律写进参数。它容量大、泛化强,但写入慢、难精确删除、很难回答「这条知识来自哪次经历」。
它适合语言能力、世界知识和稳定技能,不适合每个用户每轮对话后的实时更新。把用户偏好持续 fine-tune 进权重,不仅昂贵,还会遇到灾难性遗忘、租户隔离、删除权和审计问题。
### 2. 工作记忆:Context Window
当前 system prompt、对话、工具结果、scratchpad 和检索片段都在这里。模型能直接注意到它们,所以这是推理时最强的工作区。
但 context 不是跨调用自动持久的。窗口再长,也只是「这次桌面更大」;它没有自动决定哪些内容值得保留,更不会自动形成稳定的用户模型。
### 3. 计算缓存:KV Cache / Prompt Cache
KV Cache 保存注意力层已经算过的 K/V 张量,Prompt Cache 复用相同前缀的 prefill 结果。它们让推理少算一次,但不负责判断信息价值,也不形成可编辑、可检索的记忆条目。
所以:
- Cache 命中,可能什么都没「记住」,只是省了计算;
- Cache 失效,也不代表长期记忆丢了;
- Prompt 前缀里的记忆发生变化,反而可能让 Cache miss。
**Cache 是性能机制,Memory 是状态治理机制。**
### 4. 外部长期记忆:文件、SQL、向量库、图数据库
这是当前 Agent 记忆的主战场。它可以按用户隔离、保留来源、支持删除,并在下一次调用前检索回 context。
但「放进向量库」不等于「完成记忆」。向量库只解决近似相似搜索;它不天然解决事实冲突、时序真值、重要性、权限、错误写入和遗忘。
### 5. 环境记忆:Git、CRM、日历与真实世界状态
很多信息根本不该复制成一条自然语言记忆。代码是否已部署、客户是否付款、会议是否改期,这些事实应优先回源查询。
一个可靠 Agent 要区分:
- **该回忆的**:用户偏好、过去决策、成功经验;
- **该查询的**:订单、权限、库存、代码状态;
- **该重新计算的**:价格、统计值、派生指标。
这也是为什么「把所有东西都做 embedding」通常会得到一个信息很多、事实却不可靠的系统。
---
## 二、人类是怎样逐步理解记忆的
把向量库类比成海马、把 context 类比成工作记忆,可以帮助入门,但不能当成结构等价。人类记忆研究一百多年最重要的发现,恰恰是:**记忆不是一个位置,也不是一次写入后永久不变的文件。**
这里放历史图,不是为了给文章增加一段背景知识,而是为了说明本文为什么拒绝「记忆 = 存储」这个模型。阅读时间线时,不必记住所有年份;要观察的是底部的三次概念转向:从单一仓库,到多个可分离系统,再到会在提取中被重构的动态过程。后文的事件、事实、程序、巩固与修订,全部来自这条问题线索。

### 1885:Ebbinghaus 把记忆变成可测量对象
Hermann Ebbinghaus 用无意义音节在自己身上做重复学习实验,并用「节省法」测量遗忘:即使无法直接回忆,重新学习所需时间仍会缩短。记忆第一次从哲学讨论变成可以画曲线、比较间隔与重复次数的实验对象。
今天 Agent 评估里只测「最终答对多少题」,其实退回到了比 Ebbinghaus 更粗糙的尺度。一个记忆系统还应测:
- 写入后多久可用;
- 多次正确提取后是否更稳定;
- 旧事实被新事实替代时是否仍会误召回;
- 完全想不起来时能否正确弃权。
原始资料:[Ebbinghaus, *Memory: A Contribution to Experimental Psychology* (1885/1913)](https://psychclassics.yorku.ca/Ebbinghaus/)
### 1900:记忆不是瞬间写盘,而要经历巩固
Georg Elias Müller 与 Alfons Pilzecker 发现,新学习之后立刻插入其他材料会增加干扰,并提出记忆痕迹需要时间稳定的思路。后来「consolidation」成为记忆研究的核心概念。
对 Agent 的启示不是简单做一个 nightly cron,而是区分两份东西:
- **原始经历**:完整、可追溯、尽量 append-only;
- **巩固结果**:画像、事实、规则、摘要,可以更新或推翻。
如果只保存后者,模型一次错误总结就可能改写历史;如果只保存前者,检索会被大量低价值事件淹没。
原始资料:[Müller & Pilzecker, *Experimentelle Beiträge zur Lehre vom Gedächtniss* (1900)](https://books.google.com/books?id=5RdCAQAAMAAJ)
### 1949:Hebb 把持久变化放到连接上
Donald Hebb 提出细胞集群与连接效率随共同活动而改变的理论框架。那句广为流传的「fire together, wire together」不是原文,但准确概括了关键方向:经验通过网络连接的可塑性留下痕迹。
这对应 Agent 里的一个重要分界:
- 把经历放进 context,只是**激活状态变化**;
- 把经历写入持久存储,才是**系统状态变化**;
- 更新模型权重,则是更慢、更难治理的**参数变化**。
### 1957:H.M. 证明「记忆」可以分成不同系统
Scoville 与 Milner 报告患者 H.M. 在双侧内侧颞叶手术后出现严重的顺行性遗忘,但短时保持和部分技能学习并非以同样方式受损。这项研究击碎了「记忆是一种单一能力」的直觉。
它也是今天 Agent 设计最值得继承的认知:不要让同一个 collection 同时承担当前任务状态、历史事件、用户事实和执行技能。
原始论文:[Scoville & Milner, “Loss of Recent Memory after Bilateral Hippocampal Lesions” (1957)](https://pmc.ncbi.nlm.nih.gov/articles/PMC497229/)
### 1970s:情景、语义、程序与工作记忆逐渐分开
Endel Tulving 区分了:
- **情景记忆(episodic)**:我在何时何地经历了什么;
- **语义记忆(semantic)**:脱离具体经历后,我知道什么。
Baddeley 与 Hitch 则用多组件工作记忆替代单一短时仓库。与此同时,技能学习与陈述性知识的分离让程序记忆逐渐成为独立范畴。
这套分类到今天仍然比「短期 / 长期」更能指导 Agent 架构:
| 人类记忆范畴 | Agent 中的对应物 | 典型存储 | 典型读取 |
|---|---|---|---|
| 工作记忆 | 当前目标、计划、中间结果、工具输出 | Context / graph state / scratchpad | 每步直接注入 |
| 情景记忆 | 对话、行动、失败、环境观察 | 事件日志 + 时间索引 | 时间、实体、相似度联合检索 |
| 语义记忆 | 用户偏好、稳定事实、概念与关系 | Profile / KV / 向量 / 知识图谱 | 精确键、语义或图查询 |
| 程序记忆 | Prompt、规则、技能、成功轨迹 | 文件 / 版本库 / skill registry | 按任务路由或显式挂载 |
**「短期 / 长期」描述的是寿命;「情景 / 语义 / 程序」描述的是内容与功能。** 两个维度不能互相替代。
原始资料:[Tulving, “Episodic and Semantic Memory” (1972)](https://cir.nii.ac.jp/crid/1574231874408386176?lang=en);[Baddeley & Hitch, “Working Memory” (1974)](https://doi.org/10.1016/S0079-7421%2808%2960452-1)
### 1971–2012:从海马空间表征到可操控 engram
O'Keefe 发现海马 place cells;此后 grid cells 等研究进一步展示了空间表征的神经机制。2012 年,Liu、Ramirez、Tonegawa 等用光遗传方法重新激活在恐惧记忆形成时被标记的海马细胞群,并诱发记忆提取相关行为。
这并不意味着找到了一个装着完整记忆的「地址」。现代 engram 研究更接近一个分布式、可再激活的细胞集群。记忆内容仍依赖跨脑区网络与提取条件。
资料:[2014 Nobel Prize scientific background](https://www.nobelprize.org/prizes/medicine/2014/advanced-information/);[Liu et al., “Optogenetic stimulation of a hippocampal engram activates fear memory recall” (2012)](https://pmc.ncbi.nlm.nih.gov/articles/PMC3331914/)
### 2000:提取不是只读,记忆会再巩固
Nader、Schafe 与 LeDoux 的实验显示,已经巩固的恐惧记忆在被重新激活后会进入可塑状态,并需要蛋白质合成才能再次稳定。这推动了 reconsolidation 研究。
对 Agent 来说,最有价值的类比是:**每次 recall 都可能成为一次 update。**
用户说「我现在不喝咖啡了」,系统不应只是把新旧两条偏好并排丢进向量库。它至少要表达:
```text
旧事实:user likes coffee
有效期:2025-03 → 2026-07
新事实:user avoids coffee
来源:conversation/event/...
关系:new_fact supersedes old_fact
```
原始论文:[Nader, Schafe & LeDoux, “Fear memories require protein synthesis in the amygdala for reconsolidation after retrieval” (2000)](https://pubmed.ncbi.nlm.nih.gov/10963596/)
### 从脑科学真正能借走的四条原则
1. **记忆是多个系统,不是一个向量库。**
2. **巩固是从事件到稳定表征的转换,不是简单压缩文本。**
3. **提取具有重构性,必须保留来源与版本。**
4. **遗忘不是纯故障;它也是抑制干扰、控制成本和保护隐私的机制。**
但不要把工程组件和脑区做一一映射。海马不是 Redis,向量相似度不是联想记忆的完整模型,LLM 摘要也不等于睡眠巩固。类比的作用是提出问题,不是替代证据。
---
## 三、Agent 记忆是怎样走到今天的
人类记忆时间线解释「我们为什么这样提问」,下面的 Agent 时间线则解释「工程为什么走成今天这样」。阅读重点不是模型名称本身,而是状态边界怎样一步步外移:先写在程序和网络内部,后来放进 Context,再通过检索连接外部数据,最后形成包含写入、权限、时间与删除的独立记忆层。

### 第一阶段:状态写在程序里
早期符号 AI 与认知架构已经有工作记忆、产生式规则和长期知识,只是状态由程序员定义,表征结构也高度人工化。它们解决的是「推理系统如何维护状态」,而不是今天的自然语言个性化记忆。
### 第二阶段:让神经网络学会保存与寻址
LSTM 用门控循环状态缓解长程依赖;2014 年 Neural Turing Machine 又把网络与可微读写的外部记忆矩阵连接起来。这里的目标是端到端学会复制、排序和关联回忆等算法。
原始论文:[Neural Turing Machines (2014)](https://arxiv.org/abs/1410.5401)
这条路线把 memory 做进模型架构,但训练难度、规模化和可治理性限制了它成为通用 Agent 的用户记忆层。
### 第三阶段:Transformer 把 Context 变成通用工作区
Transformer 让序列内部任意位置之间直接交互,prompt 逐渐成为统一接口。模型权重不再需要为每次任务变化;规则、示例、文档和工具结果都可以在推理时提供。
代价是每次调用默认从一份新 context 开始。所谓「LLM 无状态」,更准确地说是:**模型 API 不替应用承诺跨调用状态语义。**
### 第四阶段:RAG 把非参数记忆接到生成前
2020 年的 RAG 把参数模型与可检索的非参数语料结合。它最初解决的是知识密集型任务与来源更新,不是个人记忆;但「先检索,再生成」很快成为 Agent 长期记忆的基本读取模式。
原始论文:[Lewis et al., “Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks” (2020)](https://arxiv.org/abs/2005.11401)
这里要保留一个边界:**RAG 是读取机制,不是完整记忆系统。** 如果数据从未经过经历驱动的写入、更新、冲突处理或遗忘,它更像外部知识库。
### 第五阶段:2023 年把写入、反思、技能与分层组合起来
2023 年几项工作几乎同时补上了不同缺口:
- **Generative Agents**:事件流 + recency / relevance / importance 检索 + reflection,把情景事件巩固为高层理解;
- **Voyager**:把成功代码沉淀为可复用 skill library,突出程序记忆;
- **MemGPT**:借用操作系统虚拟内存,把有限 context 当作工作集,由 Agent 工具化地在不同层级间搬运信息;
- **CoALA**:把工作记忆、情景、语义、程序记忆与决策循环统一成语言 Agent 的认知架构。
原始论文:[Generative Agents](https://arxiv.org/abs/2304.03442) · [Voyager](https://arxiv.org/abs/2305.16291) · [MemGPT](https://arxiv.org/abs/2310.08560) · [CoALA](https://arxiv.org/abs/2309.02427)
### 第六阶段:2024–2026 年,竞争从「能记」转向「怎么治理」
今天开源项目真正分化的地方已经不是有没有 vector search,而是:
- 谁决定写入;
- 记忆是事件、事实、文档、图边还是可执行技能;
- 冲突是覆盖、并存还是带时间失效;
- 是否保留 provenance;
- 是否有后台巩固;
- 是否能按 user / agent / tenant 隔离;
- 是否能查看、修改、导出与删除;
- 记忆失败时是否可观测。
这就是接下来代码审计的尺子。
---
## 四、怎么读一个「Agent 记忆」开源项目
我没有按官网 benchmark 排名。跑分会受到基础模型、回答 Prompt、裁判模型、检索预算和数据清洗影响,而且「记忆问答」高分不等于权限、删除、稳定性和成本都适合生产。
本次审计固定在 2026-07-30 可见的仓库版本,并观察七个层面:
1. **写入路径**:谁触发,是否有门控、去重和结构化抽取;
2. **记忆表征**:原始事件、事实、画像、图、Prompt 还是技能;
3. **存储抽象**:文件、SQL、向量、图,能否替换;
4. **检索路径**:精确、向量、BM25、图遍历、rerank;
5. **时间与冲突**:覆盖、失效、版本、双时序;
6. **系统边界**:只是库,还是含 Agent Runtime、API、租户和运维;
7. **闭环完整度**:有没有巩固、反馈、遗忘、删除和观测。
下面这张图承担的是**选型导航**,不是展示项目 Logo,也不是给出总分。最有用的读法是逐行看:官网卖点在左,真实代码主链路在中间,系统边界和记忆范式在右。这样可以迅速判断一个项目是在提供 SDK、Runtime、图引擎、框架工具箱、知识管线,还是研究实现,再决定是否值得进入后面的详细审计。

### 总表:宣传卖点、代码事实与记忆范式
| 项目 | 对外卖点 | 实际代码主链路 | 更像哪类记忆 | 架构类型 | 主要边界 |
|---|---|---|---|---|---|
| **Mem0** | Universal memory layer、个性化、跨会话学习 | 历史与向量召回 → LLM 增量事实抽取 → 批量 embedding → vector store;SQLite 记消息/变更 | 语义事实为主,兼顾用户/Agent scope | 可插拔 Memory SDK | Runtime、任务状态和完整治理不在核心内 |
| **Letta** | Stateful agents、自我改进、先进记忆 | AgentState + memory blocks + messages/passages + context window calculator + agent loop/tools | 工作 + 情景 + 语义,Agent 主动管理 | 完整 Stateful Agent Runtime | 较重;采用它往往也采用它的 Agent 运行模型 |
| **Graphiti** | 实时 temporal context graph、历史真值 | episode → entity/fact 抽取 → 双时序边 → semantic/BM25/graph hybrid search | 带情景溯源的时序语义记忆 | 专用 Temporal Graph Engine | 不自带完整用户/会话/Agent 服务 |
| **LangMem** | Agent 持续学习、热路径工具、后台记忆 | manage/search tools + background manager + LangGraph BaseStore + prompt optimizer | 语义 / 情景模板 + 程序记忆 | Framework Toolkit | 持久化、部署、权限主要继承 LangGraph/自建系统 |
| **Cognee** | 把数据 cognify 成 AI memory、替代传统 RAG | add → cognify pipeline → graph/vector/relational storage → search/memify | 企业语义记忆与知识图谱 | Knowledge Pipeline / Infrastructure | 个人对话记忆生命周期不是唯一中心 |
| **MemoryOS** | OS 式短/中/长期分层、个性化 | 短期 QA 队列 → 中期 segment/heat → LLM 画像与知识抽取 → 长期 JSON/embedding 检索 | 情景到语义的分层巩固 | Research Reference Implementation | 生产级租户、事务、治理与代码收敛度仍需补齐 |
下面逐个拆开。
---
## 五、六大开源系统:宣传与代码之间到底隔着什么
### 1. Mem0:不是「大脑」,而是一条事实蒸馏与检索管线
Mem0 的定位非常清楚:给现有应用加一个统一的长期记忆层。API 围绕 `add / search / get / update / delete`,并提供多种 LLM、embedding、vector store 和 reranker provider。
在当前 OSS Python 实现里,v3 写入主链路可以直接从 [`mem0/memory/main.py`](https://github.com/mem0ai/mem0/blob/9c2d6222ce86bf6a73ae7ca97464a8e1a55ab3ca/mem0/memory/main.py) 读出来:
1. 根据 `user_id / agent_id / run_id` 建立 scope;
2. 从 SQLite 取最近消息;
3. 用当前对话去 vector store 召回既有记忆;
4. 把「旧记忆 + 新消息 + 最近上下文」交给 LLM 做一次增量抽取;
5. 对抽出的 memory texts 批量 embedding;
6. 写回向量库,并记录历史。
这说明它的核心不是保存原始聊天,而是让 LLM 把对话**蒸馏成较短的可检索事实**。
**宣传成立的部分:**
- 接入成本低;
- provider abstraction 完整;
- scope、metadata、history、async API、reranker 等工程接口较成熟;
- 很适合「记住偏好、身份信息、过去决定」。
**宣传容易让人误解的部分:**
- universal 不代表自动适合所有记忆类型;
- 核心默认仍偏语义事实,不是完整的工作记忆或技能系统;
- 事实抽取依赖 LLM,因此写入时就可能发生遗漏、归因错误和错误概括;
- vector search 能找相似内容,但不能天然回答复杂历史真值。
> [!NOTE]
> **对应范式:** 以 semantic memory 为主的 external memory layer。
### 2. Letta:记忆不是外挂,而是 Agent Runtime 的状态模型
Letta 的前身就是 MemGPT。它和 Mem0 最大的差别,不是检索算法,而是系统边界。
从 [`AgentState`](https://github.com/letta-ai/letta/blob/b76da9092518cbaa2d09042e52fdcbde69243e18/letta/schemas/agent.py)、[`Memory`](https://github.com/letta-ai/letta/blob/b76da9092518cbaa2d09042e52fdcbde69243e18/letta/schemas/memory.py)、[`Passage`](https://github.com/letta-ai/letta/blob/b76da9092518cbaa2d09042e52fdcbde69243e18/letta/schemas/passage.py) 和 [`agent_loop.py`](https://github.com/letta-ai/letta/blob/b76da9092518cbaa2d09042e52fdcbde69243e18/letta/agents/agent_loop.py) 可以看到:
- memory blocks 是 Agent 状态的一部分;
- messages、passages、tools、model config 与 Agent identity 一起持久化;
- context window calculator 决定哪些状态进入本轮;
- Agent 可以通过工具修改自己的记忆,而不只是被动接受后台抽取;
- server、API、ORM、multi-agent group 都在同一代码库的运行模型中。
**宣传成立的部分:**
- 它确实是 stateful agent platform,不只是一个 vector wrapper;
- memory 与 Agent loop、工具和 context budgeting 一体化;
- 很适合长期运行、需要自我维护状态的 Agent。
**代价:**
- 你采用的不只是一个 memory library,而是一整套 Agent Runtime;
- 自主改写记忆提高了能力,也扩大了 prompt injection、错误写入和权限边界;
- Runtime 很完整,不代表每个具体场景都需要这么重。
> [!NOTE]
> **对应范式:** OS-style hierarchical memory + model-managed memory;覆盖 working、episodic、semantic,程序记忆则更多通过 tools/files/skills 承载。
### 3. Graphiti:真正的卖点不是「图」,而是时间与溯源
很多知识图谱项目都能存 `subject - predicate - object`。Graphiti 的差异在于 episode provenance 与双时序关系。
从 [`graphiti_core/edges.py`](https://github.com/getzep/graphiti/blob/2645dee20fd71797a61e1c6177a93cccd5584574/graphiti_core/edges.py) 和 [`graphiti.py`](https://github.com/getzep/graphiti/blob/2645dee20fd71797a61e1c6177a93cccd5584574/graphiti_core/graphiti.py) 可以看到:
- episode 保存原始输入与来源;
- entity node 表示人、物、组织或概念;
- entity edge 表示事实关系;
- `valid_at / invalid_at` 描述事实何时在现实中有效;
- `created_at / expired_at` 描述系统何时知道、何时失效;
- 搜索 recipes 组合 semantic、BM25、graph traversal 与 rerank。
这让系统可以同时回答:
- 现在什么是真的?
- 2025 年 3 月时什么是真的?
- 我们什么时候才知道它变了?
- 这条边由哪次 episode 推导出来?
**宣传成立的部分:**
- 时序与 provenance 是真实的数据模型,不只是 Prompt 里写一句「考虑时间」;
- 对变化的实体关系、多跳查询、审计非常有价值;
- graph backend 和搜索 recipe 有明确抽象。
**必须看清的边界:**
- 开源 Graphiti 是图引擎,不是完整的用户、会话与 Agent 产品;
- README 也明确区分 Graphiti 与托管 Zep:用户管理、规模化检索、治理、SLA 和开发工具属于更外层;
- 图构建依赖 LLM 结构化抽取,schema 与模型质量会直接影响写入正确性。
> [!NOTE]
> **对应范式:** temporal semantic memory + episodic provenance。
### 4. LangMem:最像一盒积木,而不是一台记忆服务器
LangMem 的价值在于把常见记忆动作做成可组合原语:
- 热路径里的 `manage_memory` / `search_memory` tools;
- 后台 manager 做抽取、合并和更新;
- profile 与 collection 两种语义记忆形态;
- 从成功/失败轨迹优化 Prompt 的 procedural memory;
- 通过 LangGraph `BaseStore` 持久化。
代码核心可见 [`knowledge/extraction.py`](https://github.com/langchain-ai/langmem/blob/56d85939d80bb731bd5e237567148d817d7bfd16/src/langmem/knowledge/extraction.py) 与 [`prompts/optimization.py`](https://github.com/langchain-ai/langmem/blob/56d85939d80bb731bd5e237567148d817d7bfd16/src/langmem/prompts/optimization.py)。
**宣传成立的部分:**
- 同时覆盖 in-the-loop 与 background 两种写入模式;
- procedural memory 不是一句口号,确实有 Prompt optimizer;
- 对已经使用 LangGraph 的项目,组合自由度很高。
**边界:**
- 它不自带独立的生产数据库、用户系统或完整 Agent Server;
- `InMemoryStore` 示例重启就丢,生产需要 Postgres Store 或其他 BaseStore;
- 一致性、权限、删除与观测取决于外层 LangGraph 平台或你自己的实现。
> [!NOTE]
> **对应范式:** memory primitives;重点覆盖 semantic 与 procedural memory。
### 5. Cognee:更像知识基础设施,而不是聊天偏好记忆
Cognee 宣称把原始数据转成 AI memory,代码里真正成熟的部分是一条 ECL 风格知识管线:
```mermaid
flowchart TD
A["add"] --> B["classify / chunk"] --> C["cognify
LLM 抽实体与关系"] --> D["graph + vector + relational storage"] --> E["search / memify"]
classDef out fill:#d3f9d8,stroke:#2f9e44
class E out
```
[`cognify.py`](https://github.com/topoteretes/cognee/blob/88aa09b4e3289e3dbf12c0c090080920816e2fb7/cognee/api/v1/cognify/cognify.py) 组织 pipeline;数据库层有 graph/vector interface;上层还有 dataset、user、role 与 ACL。
**宣传成立的部分:**
- 数据摄取、任务管线、图/向量/关系数据库适配做得很完整;
- 支持多种 search type、ontology 和多租户权限;
- 对文档、代码、企业数据形成持久知识图很有吸引力。
**需要校准的部分:**
- 它最强的是 semantic knowledge infrastructure;
- 如果需求只是「记住这个用户不吃香菜」,完整 cognify pipeline 可能过重;
- 如果需求是大量异构资料、关系查询、权限隔离,它又比纯聊天 memory SDK 更合适。
> [!NOTE]
> **对应范式:** graph-structured semantic memory / knowledge memory。
### 6. MemoryOS:脑科学启发最直观,生产架构仍偏研究参考
MemoryOS 把短期、中期和长期做成显式层级:
- 短期保存最近 QA;
- 容量达到阈值后迁移到中期 session segment;
- segment 具有 heat;
- 热度达到阈值后,用 LLM 更新 user profile、用户知识与 assistant knowledge;
- Retriever 从中期页面与长期知识中取回内容,再拼回生成 Prompt。
这些链路可以在 [`memoryos-pypi/memoryos.py`](https://github.com/BAI-LAB/MemoryOS/blob/587ed7755c7aed179965792830ff1b5ad9a6fa92/memoryos-pypi/memoryos.py) 直接读到。
**宣传成立的部分:**
- 分层、迁移、热度和巩固都有明确实现;
- 很适合复现论文,观察 episodic → semantic consolidation;
- 代码直观,研究者容易修改策略。
**代码层面的现实:**
- 默认实现大量使用本地 JSON、SentenceTransformer 与 LLM 调用;
- `memoryos-pypi`、`memoryos-playground`、`memoryos-chromadb`、`memoryos-mcp` 保留多份近似模块;
- 事务、并发、租户隔离、统一 schema、迁移、监控与细粒度删除仍需要应用补齐。
这不是说它「差」,而是它交付的是研究型参考实现,不应和完整平台用同一把尺子衡量。
> [!NOTE]
> **对应范式:** hierarchical episodic memory → profile/semantic consolidation。
---
## 六、真正公平的比较:不是一张总分榜,而是八个能力面
| 能力面 | Mem0 | Letta | Graphiti | LangMem | Cognee | MemoryOS |
|---|---|---|---|---|---|---|
| 当前任务状态 | 外部 Runtime 负责 | **核心能力** | 非核心 | LangGraph 负责 | 非核心 | 短期层覆盖一部分 |
| 情景事件 | 可由应用保留,核心偏蒸馏事实 | messages / passages | **episode 是一等对象** | 可用 schema 抽取 | 可摄取 | **短中期核心** |
| 语义事实 / 画像 | **核心能力** | memory blocks | entity / fact graph | **核心能力** | **核心能力** | 长期层 |
| 程序记忆 | 有 agent/procedural 路径,但非最强项 | tools / files / skills | 非核心 | **Prompt optimizer** | rules / memify 可扩展 | 非核心 |
| 时序冲突 | 主要靠抽取与 metadata 策略 | 由 Agent / 应用策略决定 | **双时序原生** | 由 schema / manager 决定 | 有 temporal search,取决于数据模型 | 画像合并与热度迁移 |
| 存储可插拔 | **强** | 平台持久化模型 | 图 backend 可换 | BaseStore 可换 | **图/向量/关系适配强** | 有不同发行实现 |
| 完整 Agent Runtime | 否 | **是** | 否 | 否 | 否 | 带生成流程的研究 runtime |
| 最自然的使用方式 | 给现有应用加记忆 API | 直接构建长期有状态 Agent | 给动态关系加时间图 | 给 LangGraph 拼记忆策略 | 建企业知识记忆层 | 做实验与论文复现 |
加粗不是绝对优劣,而是该项目把主要复杂度投入在哪里。
### 为什么 benchmark 不能替代这张表
LoCoMo、LongMemEval 等 benchmark 很有价值,但它们主要观察回答是否利用了历史。生产系统还要面对:
- **写错**:LLM 把推测当成用户事实;
- **记太多**:每轮都生成重复、低价值条目;
- **旧事实复活**:相似度高但已经过期;
- **跨用户泄漏**:scope 或 filter 漏掉;
- **记忆投毒**:外部内容诱导 Agent 写入长期指令;
- **不可删除**:衍生摘要、向量、图边和缓存没有级联清理;
- **不可解释**:回答用了哪条记忆无法追踪;
- **成本失控**:每轮抽取、embedding、rerank 与图构建叠加。
一个 LoCoMo 分数更高的系统,完全可能不适合医疗、金融或多租户 SaaS。
---
## 七、怎样为自己的 Agent 选记忆范式
### 场景 A:编程 Agent、个人工具、几百条稳定规则
优先:
```text
Markdown / JSON
+ 明确命名空间
+ Git 版本
+ BM25 或简单全文检索
```
原因是可读、可 diff、可审查。文件记忆不是「落后版向量库」;在精确术语多、数据量小、规则必须确定性加载的场景,它往往更可靠。
只有出现跨语言、同义改写或数千条以上内容时,再加 embedding 做辅助召回。
### 场景 B:聊天助手、客服、轻量个性化
优先 Mem0 或 LangMem 风格:
```mermaid
flowchart TD
A["对话"] --> B["写入门控"] --> C["事实 / profile 抽取"] --> D["user-scoped store"] --> E["semantic retrieval"]
classDef out fill:#d3f9d8,stroke:#2f9e44
class E out
```
关键不是选哪个向量库,而是先设计:
- 哪些内容禁止写;
- 用户能否查看与删除;
- 新旧偏好如何替代;
- 模型不确定时是否只存原始 episode。
### 场景 C:长期自治、模型要主动维护自身状态
优先 Letta 风格的完整 Runtime。
你需要的不只是「搜索历史」,而是 Agent identity、memory blocks、消息持久化、context budgeting、工具权限与 Agent loop 一起工作。
### 场景 D:事实会变化,还要回答历史状态
优先 Graphiti 风格的 temporal graph。
典型问题:
- 客户之前属于哪个合同版本?
- 某人何时从团队 A 转到团队 B?
- 系统在做出决定时掌握的是哪一版事实?
这些不是把 top-k 从 5 调到 20 能解决的。
### 场景 E:企业文档、多源数据、知识关系与权限
优先 Cognee 风格的 knowledge pipeline,或在现有数据平台上构建图 + 向量 + SQL 混合层。
这里 memory 更接近「可持续更新、可检索、可授权的组织知识」,不只是聊天回忆。
### 场景 F:研究分层巩固、热度、遗忘策略
MemoryOS 适合当可读、可改的实验基线;不要把论文参考实现未经补强就当成高并发多租户服务。
---
## 八、我会怎样设计一个可上线的 Agent 记忆层
前面的图负责定义概念和比较项目,这张图才是**落地蓝图**。阅读顺序是自上而下:顶部是一次请求经过的在线读写链路,中部是按记忆类型拆开的持久层,底部是来源血缘、巩固、冲突修订和删除等后台治理。它的目的不是要求所有团队照抄组件,而是确保生产设计没有漏掉生命周期中的关键责任。

### 1. 原始事件与派生记忆分开
```mermaid
flowchart LR
EL["event_log
不可变、可审计"] --> e1["conversation"] & e2["tool_result"] & e3["user_correction"] & e4["environment_observation"]
DM["derived_memory
可更新、可失效"] --> d1["profile_fact"] & d2["episodic_summary"] & d3["entity_relation"] & d4["procedure"] & d5["policy"]
classDef raw fill:#d0ebff,stroke:#1971c2
classDef derived fill:#e5dbff,stroke:#6741d9
class EL raw
class DM derived
```
任何派生记忆都保存 `source_event_ids`。删除源数据时,系统才知道哪些摘要、embedding 和图边需要重建或撤销。
### 2. 写入门控先于 embedding
门控至少判断:
- 是否与未来任务有关;
- 是明确事实还是模型推断;
- 是否含敏感信息;
- 用户是否授权持久化;
- 是否已存在;
- 应写成 episode、fact、relation 还是 procedure。
**每条记忆都是未来每次检索的税。**
### 3. 不同记忆类型使用不同主键与检索
| 类型 | 推荐主键 | 首选检索 |
|---|---|---|
| Profile fact | `tenant/user/fact_type` | 精确键 + 版本 |
| Episode | `tenant/user/time/event_id` | 时间过滤 + hybrid search |
| Relation | entity IDs + relation type + validity | 图查询 + 时间 |
| Procedure | task signature + version | 路由 + 语义召回 |
| Policy | scope + priority + version | 确定性挂载 |
不要用一个 embedding collection 代替 schema 设计。
### 4. 把时间和来源当成一等字段
最小字段建议:
```yaml
id:
tenant_id:
subject_id:
memory_type:
content:
source_event_ids:
confidence:
valid_from:
valid_to:
created_at:
expired_at:
supersedes:
access_scope:
```
如果事实会变化,再考虑 Graphiti 式双时序;如果事实简单,至少保留 `valid_from / valid_to / supersedes`。
### 5. 读取要做候选生成、过滤与组装
一个稳健读取链路通常是:
```mermaid
flowchart TD
A["query"] --> B["scope / ACL filter"] --> C["exact + BM25 + vector + graph candidates"] --> D["recency / importance / validity rerank"] --> E["contradiction check"] --> F["token budget packing"] --> G["provenance-preserving context"]
classDef out fill:#d3f9d8,stroke:#2f9e44
class G out
```
「相似」只是其中一个信号。
### 6. 巩固与遗忘都走后台任务
后台任务负责:
- 相似 episode 聚类;
- 提取稳定事实;
- 更新画像;
- 生成 procedure;
- 标记被替代事实;
- 低价值内容降权;
- TTL 与用户删除;
- 重建受影响索引。
在线路径只做必要的快速写入,避免每轮对话承担全部 LLM 成本。
### 7. 用任务结果评估,而不只用问答准确率
至少观察:
- write precision:写入的条目有多少真的值得保留;
- stale recall rate:召回结果中有多少已失效;
- provenance coverage:有多少回答能追到源事件;
- cross-tenant leakage:必须为零;
- deletion completeness:删除后派生数据是否残留;
- task success delta:加记忆后任务成功率是否真的提高;
- token / latency / cost:每次有效记忆带来的边际成本。
---
## 九、最后的判断:Agent 记忆会向哪里演进
短期内,不会有一个「最像人脑」的项目统一市场。更可能出现三层收敛:
1. **Runtime 层**:维护当前任务、Agent identity、工具权限和 context;
2. **Memory service 层**:管理事件、事实、关系、技能、时间与删除;
3. **Model 层**:更长 context、更好的 test-time learning,以及可能的架构内 memory module。
今天大多数 Agent 的记忆只会一个动作:把一句话丢进向量库,找几条意思相近的(`vector_db.search(text)`)。这像一个只会翻旧笔记本找相似句子的助理。真正稳定下来的记忆接口,会更像一位靠谱的秘书,至少要会五件事:
| 秘书要会的事 | 接口大概长这样 | 白话解释 |
|---|---|---|
| **记下** | `remember(event, policy)` | 发生了一件事,先按规矩判断值不值得记、能记多久,而不是什么都往本子上抄 |
| **想起** | `recall(query, scope, time, budget)` | 找回时限定是谁的、哪个项目的、什么时候的、最多拿几条——别把别人的、过期的、太多的东西一股脑塞给模型 |
| **改正** | `revise(memory, evidence)` | 有了新证据就改旧记忆:「不喝咖啡了」要替换掉「喜欢咖啡」,而不是两条并排留着 |
| **忘掉** | `forget(subject, reason)` | 用户说「把我的地址忘了」,就连同由它派生出的摘要、索引一起删干净,并记下为什么删 |
| **说出处** | `explain(memory_id)` | 能回答「你凭什么这么记得」:这条记忆来自哪次对话、什么时候、现在还可不可信 |
**今天的记忆只会「找相似」,将来的记忆要会「记下、想起、改正、忘掉、说出处」。**
人类记忆科学用了一百多年,才从「记忆存在哪里」走到「多个系统怎样在提取中重构过去」。Agent 记忆工程也正在经历同样的概念升级:
> [!TIP]
> **我们不再问 Agent 有没有 memory,而是问:它把什么变化保留下来,为什么保留,何时想起,怎样修订,以及谁有权让它忘记。**
---
## 关键一手资料与代码入口
### 人类记忆科学
- [Ebbinghaus — *Memory: A Contribution to Experimental Psychology*](https://psychclassics.yorku.ca/Ebbinghaus/)
- [Müller & Pilzecker — *Experimentelle Beiträge zur Lehre vom Gedächtniss*](https://books.google.com/books?id=5RdCAQAAMAAJ)
- [Scoville & Milner — H.M. 双侧海马相关病例](https://pmc.ncbi.nlm.nih.gov/articles/PMC497229/)
- [Baddeley & Hitch — Working Memory](https://doi.org/10.1016/S0079-7421%2808%2960452-1)
- [2014 Nobel Prize — place cells 与 grid cells 科学背景](https://www.nobelprize.org/prizes/medicine/2014/advanced-information/)
- [Nader, Schafe & LeDoux — Reconsolidation](https://pubmed.ncbi.nlm.nih.gov/10963596/)
- [Liu et al. — Optogenetic activation of a hippocampal engram](https://pmc.ncbi.nlm.nih.gov/articles/PMC3331914/)
### Agent 记忆论文
- [Neural Turing Machines](https://arxiv.org/abs/1410.5401)
- [Retrieval-Augmented Generation](https://arxiv.org/abs/2005.11401)
- [Generative Agents](https://arxiv.org/abs/2304.03442)
- [Voyager](https://arxiv.org/abs/2305.16291)
- [CoALA](https://arxiv.org/abs/2309.02427)
- [MemGPT](https://arxiv.org/abs/2310.08560)
- [Lost in the Middle](https://arxiv.org/abs/2307.03172)
### 固定提交的代码审计入口
- [Mem0 `memory/main.py` @ `9c2d622`](https://github.com/mem0ai/mem0/blob/9c2d6222ce86bf6a73ae7ca97464a8e1a55ab3ca/mem0/memory/main.py)
- [Letta `schemas/memory.py` @ `b76da90`](https://github.com/letta-ai/letta/blob/b76da9092518cbaa2d09042e52fdcbde69243e18/letta/schemas/memory.py)
- [Graphiti `graphiti_core/edges.py` @ `2645dee`](https://github.com/getzep/graphiti/blob/2645dee20fd71797a61e1c6177a93cccd5584574/graphiti_core/edges.py)
- [LangMem `knowledge/extraction.py` @ `56d8593`](https://github.com/langchain-ai/langmem/blob/56d85939d80bb731bd5e237567148d817d7bfd16/src/langmem/knowledge/extraction.py)
- [Cognee `cognify.py` @ `88aa09b`](https://github.com/topoteretes/cognee/blob/88aa09b4e3289e3dbf12c0c090080920816e2fb7/cognee/api/v1/cognify/cognify.py)
- [MemoryOS `memoryos.py` @ `587ed77`](https://github.com/BAI-LAB/MemoryOS/blob/587ed7755c7aed179965792830ff1b5ad9a6fa92/memoryos-pypi/memoryos.py)
---
*审计时间:2026-07-30。开源项目变化很快,因此代码判断全部链接到固定提交;官网卖点只用于说明项目自我定位,不作为架构事实。*
---
title: "LLM 推理引擎怎么选——2026 年从本地单机到 PD 分离的全景选型地图"
description: "阿里云那张 Ollama / vLLM / SGLang / HF Pipeline 四引擎表在 2026 年已经不够用了。这篇把推理引擎按「本地 → 高性能服务 → 分布式分离」三层重新梳理,覆盖 8 个主流引擎和 PD 分离 / 投机解码 / FP4 量化三大新趋势,附决策矩阵和决策树。"
url: "https://zhuoqidev.com/posts/llm-inference-engine-selection/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-07-19"
updated: "2026-07-30"
tags: ["LLM","推理引擎","vLLM","SGLang","模型部署","推理优化","选型指南","调研报告"]
translation: "https://zhuoqidev.com/en/posts/llm-inference-engine-selection/index.md"
---
# LLM 推理引擎怎么选——2026 年从本地单机到 PD 分离的全景选型地图
> **TL;DR** 这篇文章把这张地图更新到 2026 年年中。它不替你拍板选哪个产品——它给你一套分层框架、一张决策矩阵和一棵决策树,让你自己把候选收敛到 1–2 个。
阿里云 CAP 有一篇讲推理引擎选型的文章,把候选收敛到四个:**Ollama、vLLM、SGLang、Hugging Face Pipeline**。这个划分在 2024 年是够用的。
但到 2026 年,它至少漏掉了半张地图——NVIDIA 的 TensorRT-LLM 完成了「PyTorch 化」转身、SGLang 因为首个开源复现 DeepSeek 大规模部署而封神、Hugging Face 自己给 TGI 挂上了「维护模式」横幅并劝你改用 vLLM,而整个 2025 年推理引擎领域真正的主线,其实是一个字:**拆**。
第一张图先解决最常见的比较错误:Ollama、KTransformers 和 vLLM 并不在解决同一层问题。先按本地运行、异构卸载与高性能服务划层,再在层内比较吞吐、格式和硬件支持,才有意义。

---
## 为什么 2026 年「选推理引擎」才是个真问题
三年前不需要纠结这个。那时候能把一个 7B 模型在 GPU 上跑起来、返回还算流畅的 token 流,就已经过关。
现在情况变了,原因有三个:
1. **模型能力在趋同,部署形态在分化。** 开源侧从 Llama、Qwen 到 DeepSeek-V3/R1、Kimi K2,能力差距在缩小;真正拉开差距的,变成了「你能不能把它高效、低成本、稳定地跑起来」。同一个 DeepSeek-R1,有人一张 4090 勉强跑起来做原型,有人用 96 张 H100 做到接近官方吞吐、成本压到官方 API 的五分之一——差的就是推理引擎和部署架构。
2. **大规模 MoE 把推理从「单卡工程」推成了「分布式系统工程」。** DeepSeek-V3 是 671B 总参 / 37B 激活,Kimi K2 更是 1T 总参。这类模型单机放不下,必须跨节点做专家并行(EP),推理引擎的选择直接决定了你要不要碰 all-to-all 通信、专家负载均衡这些硬骨头。
3. **同一个模型,引擎之间的吞吐/成本差异可以到数倍。** 这不是「快一点点」的差别,是「同样的卡,能不能多服务几倍用户」的差别,直接写进云账单。
所以选型不再是「随便挑一个能跑的」,而是 **先想清楚你在地图的哪一层**。
---
## 一句话全景:八个引擎的定位速查表
先给结论。下面这张表把 2026 年主流引擎放在一起,一眼看清各自的位置:
| 引擎 | 主导方 | 核心机制一句话 | 最新版本(截至 2026-07) | 典型定位 |
|---|---|---|---|---|
| **vLLM** | 社区 / Red Hat | PagedAttention + V1 全异步引擎 | v0.25.x(2026-07) | 通用开源事实标准 |
| **SGLang** | LMSYS / 社区 | RadixAttention 前缀树 KV 复用 | v0.5.x | 大规模 MoE / DeepSeek 部署 |
| **TensorRT-LLM** | NVIDIA | 编译级内核 + In-flight Batching | v1.2.1(2026-04) | NVIDIA 卡上的极致性能 |
| **LMDeploy** | 上海 AI Lab | TurboMind 高性能 C++ 引擎 | v0.13.x | 国产模型 / 混合精度量化 |
| **Ollama** | Ollama | llama.cpp/GGML + 自研多模态引擎 | v0.30.x | 本地 / 桌面一键跑 |
| **llama.cpp** | ggml-org | GGUF + CPU/GPU 通吃 | 滚动更新 | 端侧 / CPU / 量化 |
| **HF TGI** | Hugging Face | 早期 Rust 服务网关 | v3.3.7(**维护模式**) | ⚠️ 官方已建议迁走 |
| **KTransformers** | 清华 kvcache-ai | CPU/GPU 异构,专家放 CPU | 滚动更新 | 单卡跑超大 MoE |
注意到 HF 这一行的 ⚠️ 了吗?这是 2025 年格局最重要的变化之一,后面单独说。
---
## 分层看:推理引擎其实分三层
把八个引擎平铺在一张表里对比是有陷阱的——它们根本不在解决同一个问题。**Ollama 和 SGLang 放在一起选,就像拿家用轿车和集装箱卡车比油耗。** 正确的做法是先分层:
```
L1 本地 / 单机层 —— 一个人用,跑得起、装得简单最重要
Ollama · llama.cpp · LM Studio · HF Pipeline(原型)
L2 高性能服务层 —— 一个服务撑多用户,吞吐 / 延迟 / 显存效率最重要
vLLM · SGLang · TensorRT-LLM · LMDeploy · (TGI 已退场)
L3 分布式 / 分离层 —— 一个机房跑一个大模型,架构比引擎本身更重要
PD 分离 · NVIDIA Dynamo · Mooncake · 大规模专家并行(EP)
```
**先定位到层,再在层内选引擎。** 大部分人的纠结,其实是把 L1 的需求拿到 L2 去选,或者反过来。
---
## L1 本地 / 单机层:跑得起、装得简单
这一层的核心诉求不是吞吐,是 **「我一个人、一台机器、最好一条命令就能把模型跑起来」**。隐私敏感、离线、端侧、快速原型都归这里。
### Ollama / llama.cpp:门面与内核
这俩要一起讲,因为它们是同一件事的两面:
- **llama.cpp** 是内核——C/C++ 实现,用 **GGUF** 量化格式,CPU / GPU / Apple Silicon 通吃。端侧量化推理的地基。
- **Ollama** 是门面——在 llama.cpp 之上做「一键拉起 + 模型管理」的产品层,`ollama run qwen3` 就完事。
关于这俩,有一个**很多旧文没跟上的 2025 年变化**:
> **Ollama 正在从「llama.cpp 套壳」走向自研引擎。** 2025 年 5 月它上线了全新的多模态引擎,把视觉模型(gemma3、qwen2.5vl、mistral-small3.1 等)做成一等公民,并直接对接 GGML 张量库、逐步与 llama.cpp 解耦([Ollama 博客](https://ollama.com/blog/multimodal-models))。到 0.30 版本,Apple Silicon 上引入了 MLX 引擎与 llama.cpp 并存,Vulkan 默认开启(覆盖到 AMD/Intel GPU),NVIDIA 上性能约 +20%([Ollama 博客](https://ollama.com/blog))。
所以如果你还停留在「Ollama 就是个 llama.cpp 的启动器」的印象,该更新了。
**什么时候选它们:** 本地开发、桌面应用、隐私数据不能出内网、端侧部署、快速验证一个模型「行不行」。GGUF 是这一层的通用交换格式。
**什么时候别用:** 需要扛高并发的线上服务。它们面向的是单机 / 单用户体验,不是数据中心级吞吐——这跟 L2 的引擎定位是正交的,不是「谁更强」的问题。
### HF Pipeline 与 TGI:一个是原型玩具,一个已经退场
阿里云原文把「Hugging Face Pipeline」列为选型候选,这里要拆成两件事说清楚,因为**它俩经常被混为一谈,而且都有坑**:
- **`transformers.pipeline`** 是几行代码调用模型的高级 API,适合教学、原型、离线小批量。但它**从来就不是生产服务方案**——没有 continuous batching、没有 PagedAttention,并发上去就崩。把它当选型候选,只能是「原型阶段」的候选。
- **TGI(Text Generation Inference)** 才是 HF 曾经的服务产品。但——
> [!CAUTION]
> **2025 年最重要的格局变化之一:Hugging Face 亲手给 TGI 挂上了「维护模式」横幅。** GitHub README 明确写着 *"text-generation-inference is now in maintenance mode"*,只接受小 bug 修复,并**反过来建议用户改用 vLLM、SGLang 或本地 llama.cpp / MLX**([TGI GitHub](https://github.com/huggingface/text-generation-inference))。HF 战略性地把 serving 让位给了 vLLM/SGLang,自己回去专注 transformers 的模型架构标准。
**结论:** 如果有人还在 2026 年推荐你用 TGI 上生产,说明他的信息停在了 2024 年。原型用 `pipeline`,生产直接看 L2。
---
## L2 高性能服务层:一个服务撑住多用户
这是绝大多数「要上线」的团队真正要选的一层。核心诉求变成了**吞吐(throughput)、延迟(TTFT / TPOT)、显存效率**——同样的 GPU,能不能多服务几倍请求。
这一层的两个地基技术,值得先记住:
- **Continuous Batching(连续批处理)**:不等一批请求全部结束,而是每生成一步就动态放入新请求、送走完成的请求,把 GPU 榨干。这是所有现代服务引擎的标配。
- **PagedAttention / RadixAttention**:两种 KV cache 管理哲学,分别是 vLLM 和 SGLang 的看家本领,下面细说。
### vLLM:开源事实标准,2025 年完成 V1 重写
**核心机制**:**PagedAttention** 把 KV cache 切成固定大小的「页」,像操作系统管虚拟内存一样把逻辑位置映射到非连续的物理块,消除显存碎片、提升并发。
**2025 年最大的事是 V1 架构重写**:2025-01 随 v0.7.0 发布 alpha,宣称相比旧架构**最高 1.7× 吞吐**,卖点是默认打开所有优化、隔离出独立的执行循环、零开销前缀缓存([vLLM 博客](https://vllm.ai/blog/2025-01-27-v1-alpha-release));自 v0.8.0 起 **V1 成为默认引擎**([Red Hat 实测](https://developers.redhat.com/articles/2025/04/28/performance-boosts-vllm-081-switching-v1-engine))。V1 的调度器能在**同一步里混合 prefill 和 decode**,并内置 chunked prefill、prefix caching、投机解码、PD 分离等能力。
**投机解码、量化、PD 分离全都有**:内置 n-gram / EAGLE / Medusa / MTP 投机解码;量化覆盖 FP8 / INT8 / AWQ / GPTQ / GGUF 到 Blackwell 上的 MXFP4 / NVFP4;PD 分离通过 KV Connector 对接 Mooncake / LMCache 等外部 KV 服务。
**一句话定位**:**模型覆盖最广、生态最全、从单卡到多机都能用的「默认选项」。** 拿不准选什么的时候,先上 vLLM 基本不会错。
### SGLang:大规模 MoE / DeepSeek 部署的明星
**核心机制**:**RadixAttention** 用基数树(前缀树)+ LRU 自动复用多个请求间共享的前缀 KV cache,把 prefix caching 做成一等公民——这天生适合 Agent、多轮对话、few-shot 这类**前缀高度重复**的负载。
**让 SGLang 封神的一战**:2025-05,它成为**首个开源复现 DeepSeek 官方级别大规模部署**的引擎——12 节点 × 8×H100 = **96 张 H100**,2000-token 输入下每节点 52.3K input tok/s、22.3K output tok/s,**比朴素张量并行快 5×**,成本压到约 **$0.20 / 百万 output token(约为 DeepSeek 官方 Chat API 定价的 1/5)**,与官方 profile 差距仅 5–6%([LMSYS 博客](https://www.lmsys.org/blog/2025-05-05-large-scale-ep/))。背后是 DeepEP、DeepGEMM、EPLB、DP Attention、Two-Batch Overlap 一整套组合拳。
投机解码这边,SGLang 在 DeepSeek-V3 上开 MTP,输出吞吐最高 +60%、约 1.8× tokens/s([LMSYS 博客](https://www.lmsys.org/blog/2025-07-17-mtp/))。
**一句话定位**:**超大规模 MoE 整机房部署、长共享前缀(Agent / 多轮)场景的首选。** 到 2026 年,SGLang 在 DeepSeek 类模型的「整集群 PD + EP 工程化」上是开源里最激进的——它早已不是「四选一之一」,而是这个细分赛道的头号玩家。
### TensorRT-LLM:NVIDIA 官方引擎,完成「PyTorch 化」
**核心机制**:把模型编译成 TensorRT 引擎做算子级优化,配合 In-flight Batching(就是 continuous batching 的 NVIDIA 叫法)。
**历史包袱与 1.0 的转身**:TensorRT-LLM 过去的口碑是「编译式、极致快、但难用」。 **v1.0.0(2025-09-24,注意是 2025 不是 2024)** 是转折点——PyTorch 后端成为默认且稳定、LLM 高层 API 稳定,易用性明显向 vLLM/SGLang 靠拢,同时新增 NVFP4、投机解码增强、Wide-EP(大规模专家并行)([v1.0.0 Release](https://github.com/NVIDIA/TensorRT-LLM/releases/tag/v1.0.0))。
> ⚠️ **辩证看待营销数字**:网络上流传的「H100 FP8 上 10,000+ tok/s、比原生 PyTorch 快 4×」等数字多来自第三方博客,未在 NVIDIA 官方一手材料里稳定复现,引用需谨慎。
**一句话定位**:**纯 NVIDIA 硬件(尤其 Hopper / Blackwell)、追求极致吞吐延迟、要走企业级(Triton / NIM)落地的团队。** 代价是绑定 NVIDIA 生态、闭源内核多。
### LMDeploy / TurboMind:国产力量,混合精度与 KV 量化强
**核心机制**:上海 AI Lab 自研的 **TurboMind** 高性能 C++/CUDA 引擎,主打 blocked KV cache、KV cache 在线量化(INT4/INT8)、AWQ、自动前缀缓存。
**亮点数据**:TurboMind 的混合精度推理,相比现有混合精度框架**延迟最高降 61%、吞吐最高升 156%**,在 H800 上跑 gpt-oss 达到 vLLM 的 1.5×([arXiv:2508.15601](https://arxiv.org/abs/2508.15601))。对 DeepSeek 集成了 FlashMLA、DeepGEMM、DeepEP,PD 分离通过 DLSlime + Mooncake 落地。
**一句话定位**:**国产模型(InternLM / Qwen / DeepSeek)生产部署、对 KV cache 量化和低延迟有强需求的团队。** 在中低批量延迟和量化推理上常优于 vLLM,代价是生态和模型覆盖面小一些。
---
## L1.5 特殊场景:KTransformers——用内存换「跑得起」
这个引擎值得单列,因为它解决的是一个很具体但很痛的问题:**显存不够,但我就是想在本地把满血 671B MoE 跑起来。**
**核心机制**:利用 MoE「稀疏激活」的特点做 CPU/GPU 异构——把稀疏激活的专家权重甩给 CPU(大内存 + AMX 指令集),把注意力等稠密计算留给 GPU(高带宽)。
**代表成绩**:在**单张 24GB 显卡(RTX 4090D)+ 大内存**上跑起**满血 DeepSeek-R1/V3 671B**,预填充最高 286 tok/s、生成约 14 tok/s([KTransformers 教程](https://github.com/kvcache-ai/ktransformers/blob/main/doc/en/DeepseekR1_V3_tutorial.md))。2025-10 它的 CPU 内核还被合并进了 SGLang,让「异构 offload」从玩具走向可服务化。
**一句话定位**:**显存受限却想跑前沿大 MoE 的科研 / 低成本私有化场景。** 它不追高并发吞吐,追的是「能不能跑起来」——是「消费级硬件跑前沿模型」的代表方案。
---
## 模型格式 × 引擎:先看你的权重是什么格式
选引擎之前,先看手里的模型是什么格式——这直接砍掉一半候选:
| 格式 | 说明 | 主要适配引擎 |
|---|---|---|
| **GGUF** | 端侧通用量化容器(Q4_K_M 等),CPU 友好 | llama.cpp / Ollama |
| **Safetensors** | 加载快、不易受恶意代码影响,HF 生态主流 | vLLM / SGLang / TRT-LLM / LMDeploy |
| **PyTorch (.pt/.pth)** | 原始 state_dict 权重 | vLLM / HF Pipeline |
### 量化:2026 年已经进入 4-bit(FP4)时代
阿里云原文的量化视角还停在 FP8 / GGUF,这块需要升级——2025 年量化的主线是**从 8-bit 走向 4-bit**:
| 量化格式 | 特点 | 主要适配引擎 |
|---|---|---|
| **FP8 (E4M3)** | Hopper/Ada 起原生,权重 + 激活 + KV cache | vLLM / TRT-LLM / SGLang / LMDeploy |
| **AWQ** | 激活感知 4-bit 权重量化,精度好 | vLLM / LMDeploy / SGLang |
| **GPTQ** | 经典 4-bit 训练后量化 | vLLM / TRT-LLM |
| **GGUF (Q4_K_M…)** | 端侧通用量化容器 | llama.cpp / Ollama |
| **MXFP4** | 每 32 值共享一个 2 的幂 scale(微缩放) | gpt-oss 原生 / vLLM / LMDeploy / TRT-LLM |
| **NVFP4** | 每 16 值共享一个 FP8 scale,更细 | Blackwell 原生 / TRT-LLM / vLLM |
标志性事件是 **OpenAI 的 gpt-oss(2025)直接以 MXFP4 发布**——MoE 专家权重(占参数 90%+)做量化感知训练,让 120B 能塞进单张 80GB 卡([NVIDIA 博客](https://developer.nvidia.com/blog/fine-tuning-gpt-oss-for-accuracy-and-performance-with-quantization-aware-training/))。NVIDIA 则称 NVFP4 的 KV cache 比 FP8 省约 50%、精度损失 <1%、整体 1.6× 于 BF16 吞吐([NVIDIA Research](https://research.nvidia.com/labs/eai/blogs/pushing-intelligence-to-4-bit/))。
> **一条经验法则**:**格式和量化决定了你的候选池,先过这一关再谈引擎。** 手里是 GGUF 就别惦记 SGLang,想上 NVFP4 就得有 Blackwell 卡加 TRT-LLM/vLLM。
---
## 阿里云原文没讲的三个新趋势
这是这篇文章相比原文最大的增量。2025 年推理引擎领域真正的进展,全在这三件事上。
下面这张图专门解释其中最容易被一句话带过的 **PD 分离**。Prefill 和 Decode 虽然服务同一次请求,却分别受吞吐与尾延迟约束;分开资源池之后,才能独立扩容和调度。

### 趋势一:PD 分离——2025 年的架构主线
前面说的「拆」,主要就是指这个。
**为什么要拆**:一次推理有两个阶段,特性完全相反——
- **Prefill(预填充)**:算完整个 prompt 的 KV,**计算密集**,吃算力。
- **Decode(解码)**:每步只生成 1 个 token,**访存密集**,吃显存带宽。
把它俩塞在同一张卡上会互相干扰,首 token 延迟(TTFT)和逐 token 延迟(TPOT)两个指标很难同时满足。**拆开到不同的 GPU 池,就能各自独立扩缩容、各自选最优并行策略**——这就是 Prefill-Decode 分离(disaggregated serving)。
两个代表作:
- **Mooncake**(月之暗面 Kimi 的服务平台):以 **KVCache 为中心**的分离架构,不光拆 prefill/decode,还把集群里闲置的 CPU / DRAM / SSD 组织成一个**全局 KV cache 池**。论文拿了 **FAST 2025 最佳论文**,模拟长上下文场景吞吐最高 +525%([arXiv:2407.00079](https://arxiv.org/abs/2407.00079))。现在已被 vLLM、SGLang、TensorRT-LLM、Dynamo 广泛集成。
- **NVIDIA Dynamo**(见趋势三)把 PD 分离直接产品化了。
> **这背后是一个认知转变**:**KV cache 正从「单机显存里的东西」变成「集群级的存储层」。** Mooncake、LMCache 把 KV 下沉到 CPU/DRAM/SSD,实现跨实例、近 GPU 的前缀缓存。这一步,把推理彻底带进了分布式系统的领域。
### 趋势二:投机解码——从论文走向标配
**一句话原理**:用一个轻量「草稿模型」快速猜出接下来的多个 token,再让大模型**一次前向就并行验证**这一串——猜中的部分直接省掉了逐个生成的算力。
2025 年它从「论文里的技巧」变成了「vLLM/SGLang 里开个开关就能用」:
- **EAGLE-3**([arXiv:2503.01840](https://arxiv.org/abs/2503.01840)):放弃预测特征、改为直接预测 token + 多层特征融合,**最高 6.5× 加速**,还发现了「训练数据越多、加速比越高」的 scaling 规律。
- **MTP(Multi-Token Prediction)**:DeepSeek-V3 自带 MTP 头,天然可当草稿。SGLang 实测约 1.8× tokens/s、接受率 85–90%;vLLM 能自动识别 MTP 头。
- **Medusa**:2024 年的代表作(多解码头 + 树状注意力),2025 年多被 EAGLE-3 超越,但仍是内置选项之一。
**对选型的意义**:如果你的引擎(vLLM / SGLang / TRT-LLM)支持投机解码,而你还没开——这是一笔几乎免费的 1.4–2× 加速,值得优先试。
### 趋势三:NVIDIA Dynamo——把分离式推理产品化
**定位要点**:Dynamo **不是又一个推理引擎,而是引擎之上的编排层**。GTC 2025 发布,数据中心级、开源、专门做分布式推理编排。
它把前两个趋势打包成了产品,四大组件:
1. **Planner**:按 SLO 和负载,在「分离 vs 聚合」之间动态切换、动态增减 GPU。
2. **Smart Router**:用基数树跟踪全集群的 KV cache,把请求路由到能命中缓存的那张卡。
3. **KV Cache Manager**:把冷 KV 下沉到 CPU / SSD / 对象存储。
4. **NIXL**:跨异构内存 / 存储的高吞吐传输库。
**性能**:DeepSeek-R1 671B 在 GB200 NVL72 上吞吐提升 30×;Llama 70B 在 Hopper 上吞吐翻倍。关键是**它兼容 vLLM / SGLang / TensorRT-LLM 作为后端**([NVIDIA 博客](https://developer.nvidia.com/blog/introducing-nvidia-dynamo-a-low-latency-distributed-inference-framework-for-scaling-reasoning-ai-models/))。
> **这件事的信号很清楚**:NVIDIA 用 Dynamo 把「PD 分离 + 智能路由 + KV 分层」标准化,而且**不绑定自家引擎**——等于承认了 vLLM/SGLang 是既成事实,自己退到编排层和硬件层去做价值。开源引擎的江湖地位,由此盖章。
---
## 选型决策矩阵
把 L2 这几个真正要「二选一 / 三选一」的引擎放到一起,按维度打分:
| 维度 | vLLM | SGLang | TensorRT-LLM | LMDeploy |
|---|---|---|---|---|
| **模型覆盖广度** | 最广 | 广 | 中(NVIDIA 优先) | 中(国产友好) |
| **上手难度** | 低 | 中 | 中(1.0 后改善) | 中 |
| **通用高并发吞吐** | 强 | 强 | 最强(NV 卡) | 强 |
| **超大 MoE / EP 部署** | 强 | 最强 | 强 | 强 |
| **前缀复用(Agent/多轮)** | 好 | 最好(RadixAttention) | 好 | 好 |
| **KV 量化 / 低延迟** | 好 | 好 | 强 | 最强(TurboMind) |
| **硬件绑定** | 无(含 TPU) | 无 | 强绑 NVIDIA | 主要 NVIDIA |
| **生态 / 社区** | 最活跃 | 活跃 | NVIDIA 官方 | 国产社区 |
没有一列全是「最」——**这正是「中立」的意思:不存在通吃的引擎,只有匹配你负载的引擎。**
---
## 实战决策树
把上面所有东西压缩成一棵树:
```mermaid
flowchart LR
%% layout: tree
Q(["你要在哪一层?"])
Q --> L1["本地 / 单机 / 隐私 / 原型"]
Q --> L2["高性能线上服务
一个服务撑多用户"]
Q --> L3["机房级
一个大模型吃满整个集群"]
L1 --> C1["就想一键跑起来、日常用"] --> E1["Ollama"]
L1 --> C2["要抠 CPU / 端侧 / 极致量化"] --> E2["llama.cpp"]
L1 --> C3["几行代码验证个模型"] --> E3["HF transformers.pipeline"]
L1 --> C4["显存不够但要跑满血 671B MoE"] --> E4["KTransformers"]
L2 --> C5["拿不准 / 要模型覆盖最广"] --> E5["vLLM(默认选它)"]
L2 --> C6["大规模 MoE / DeepSeek / 多轮前缀重"] --> E6["SGLang"]
L2 --> C7["纯 NVIDIA 卡 + 要极致性能 + 企业级"] --> E7["TensorRT-LLM"]
L2 --> C8["国产模型 + 强 KV 量化 / 低延迟"] --> E8["LMDeploy"]
L3 --> C9["别只盯引擎,先上编排层"] --> E9["NVIDIA Dynamo
PD 分离 + Mooncake KV 池 + EP
后端仍是 vLLM / SGLang / TRT-LLM"]
classDef q fill:#fff3bf,stroke:#f08c00
classDef tier fill:#d0ebff,stroke:#1971c2
classDef engine fill:#d3f9d8,stroke:#2f9e44
class Q q
class L1,L2,L3 tier
class E1,E2,E3,E4,E5,E6,E7,E8,E9 engine
```
⚠️ 还在考虑上 TGI 生产?—— 别了,官方已维护模式,改道 vLLM/SGLang。
---
## 分场景总结:一句话给答案
| 如果你是…… | 大概率选 |
|---|---|
| 个人开发者,本地跑模型、做原型 | **Ollama**(+ GGUF) |
| 要抠端侧 / CPU / Apple Silicon | **llama.cpp** |
| 教学、离线小批量、几行代码验证 | **HF `pipeline`**(别上生产) |
| 显存受限但想跑满血大 MoE | **KTransformers** |
| 要上线,拿不准,想一个能打的默认 | **vLLM** |
| 大规模跑 DeepSeek / MoE / Agent 多轮 | **SGLang** |
| 纯 NVIDIA、追极致性能、要企业级栈 | **TensorRT-LLM** |
| 国产模型、重 KV 量化 / 低延迟 | **LMDeploy** |
| 一个大模型要吃满整个机房 | **Dynamo**(编排层)+ 上面某个引擎(后端) |
---
## 收尾
阿里云那篇文章没有错,它只是**停在了「单机跑模型」这一层**。而 2025 年整个行业的重心,已经从「用哪个引擎把模型跑起来」,挪到了「用什么架构把一个大模型高效地摊到一整个集群上」。
如果只留一句话,我会留这句:
> **推理引擎的选择,本质是「你在哪一层」的选择。** 先定位到本地 / 服务 / 分布式这三层里的一层,候选就自动收敛到一两个;剩下的差异,交给你自己的负载去基准测试——**没有普适最优的引擎,只有跑过你自己流量的那个数字。**
三年前,能把模型跑起来就赢了。今天,怎么跑,才是真正的分水岭。
---
*本文的最新进展数据(vLLM V1、SGLang 复现 DeepSeek、TensorRT-LLM 1.0、Mooncake、Dynamo、EAGLE-3、NVFP4 等)来自对官方博客、GitHub release、arXiv 论文的多源交叉验证,关键数字均在正文附了来源链接。厂商自测数字(如 TGI 长上下文自测、TRT-LLM 第三方吞吐)已在文中标注「需辩证看待」。截至 2026-07。*
---
title: "OpenClaw 实战:一行路径省掉 84% 的工具调用——Cron Job 排障实录"
description: "OpenClaw 的 daily-ai-news 定时任务连续超时。根因不是模型不够强、不是 prompt 太长、不是上游 API 挂了——是 SKILL.md 里少写了一行绝对路径,导致 Agent 每次执行花 15 次 exec 调用搜索一个 skill 的位置。消息数从 54 膨胀到 165,工具调用结果从 204KB 暴涨到 1.1MB。这篇记录完整的排障过程和修复方法。"
url: "https://zhuoqidev.com/posts/openclaw-cron-skill-optimization/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-06-20"
updated: "2026-07-30"
series: "OpenClaw 生产实战"
tags: ["OpenClaw","AI Agent","Cron Job","SKILL.md","Prompt Engineering","性能优化"]
translation: "https://zhuoqidev.com/en/posts/openclaw-cron-skill-optimization/index.md"
---
# OpenClaw 实战:一行路径省掉 84% 的工具调用——Cron Job 排障实录
> [!NOTE]
> 这是 [OpenClaw 生产实战](https://zhuoqidev.com/series/openclaw-生产实战/) 系列的第三篇。[第一篇](https://zhuoqidev.com/posts/openclaw-pitfalls/) 讲 compaction 静默吞回复,[第二篇](https://zhuoqidev.com/posts/openclaw-memory-text-to-vector/) 讲记忆系统从“向量搜索挂了也能跑”到用 NVIDIA 免费 API 补全。这篇讲一个更朴素但影响更大的问题:**当你的 AI Agent 不知道工具在哪,它会怎么做?**
这张前后对比图先给出事故的完整因果链。左侧不是模型“笨”,而是模糊指令迫使它反复探索目录;右侧的一行绝对路径直接缩小了动作空间,因此消息数、搜索次数和执行调用同时下降。

## 现象:定时任务连续超时
OpenClaw 有一个每天 21:05 执行的 cron job——`daily-ai-news`,功能是抓取当日 AI 领域新闻、生成摘要、配一张 banner 图、发到飞书群。
某天我注意到这个任务连续失败。看日志:
```
Status: error
Duration: 1200s+ (timeout)
Tokens: in=2,800,000+ out=15,000+
```
20 分钟超时,消耗了 280 万 input token。一个日报任务。
## 排障:从日志反推 Agent 行为
OpenClaw 的 cron 执行会留完整的对话日志。我拉出最近一次失败的日志,统计关键指标:
| 指标 | 失败运行 |
|------|---------|
| 总消息数 | 165 |
| exec 工具调用 | 44 次 |
| tool_result 总大小 | 1,122 KB |
| 运行时间 | 1200s+(超时) |
165 条消息?44 次 exec?一个"抓新闻 → 写摘要 → 生图 → 发飞书"的流程,正常情况下不该超过 60 条消息和 10 次工具调用。
逐条翻日志,发现了罪魁祸首。
## 根因:Agent 不知道工具在哪
`daily-ai-news` 的 SKILL.md(skill 的指令文件)里有一行:
```markdown
生成 banner 图片时使用 create-img / gpt-image-2 生成。
```
就这么一句。没有路径,没有调用方式,没有参数。
Agent 拿到这个指令后,它知道要调用 `create-img`,但不知道这个东西在哪。于是它开始**搜索**:
```bash
# Agent 的第 1 次尝试
find /home/openclaw -name "create-img" -type d
# 没找到想要的,继续
grep -r "create-img" /home/openclaw/.openclaw/
# 找到了一些引用,但不确定是不是可执行的
ls -la /home/openclaw/.openclaw/plugin-skills/
# 没看到,换个目录
ls -la /home/openclaw/.agents/skills/
# 找到了!但不确定怎么调用
cat /home/openclaw/.agents/skills/create-img/SKILL.md
# 读完了,但还要确认脚本路径
find /home/openclaw/.agents/skills/create-img -name "*.py"
# 确认了脚本,但不确定 Python 环境
which python3
python3 --version
# 确认了 Python,但不知道虚拟环境在哪
ls /home/openclaw/workspace/.venv*/
# 找到了虚拟环境,开始拼命令...
```
**15 次 exec 调用**,只是为了找到一个工具的位置和调用方式。
更要命的是,这个搜索过程产生的 `tool_result` 体积巨大——每次 `find`、`grep`、`ls` 的输出都会被塞进对话上下文。44 次 exec 调用累计产出 1.1MB 的工具结果,直接把上下文撑爆,触发 compaction,进一步拖慢执行。
### 恶性循环
这是一个经典的**上下文膨胀恶性循环**:
```mermaid
flowchart TD
A["SKILL.md 缺路径"] --> B["Agent 搜索工具位置
15 次 exec"]
B --> C["搜索结果塞满上下文
+900KB"]
C --> D["触发 compaction
压缩耗时 + 可能丢信息"]
D --> E["压缩后 Agent 忘记之前搜到的路径"]
E -->|"再搜一遍(又 15 次 exec)"| B
E -->|"上下文再次膨胀"| F["超时"]
classDef cause fill:#fff3bf,stroke:#f08c00
classDef bad fill:#ffe3e3,stroke:#e03131
class A cause
class F bad
```
没错——**compaction 压缩掉的恰恰是 Agent 好不容易搜到的工具路径**,于是下一轮它又从头搜。这就是 165 条消息的来源:Agent 至少做了 2-3 轮完整的"搜索 → 找到 → 被压缩 → 再搜"循环。
## 修复:一行绝对路径
修复方法极其简单。在 `ai-news-digest` 的 SKILL.md 末尾加一段,告诉 Agent 工具的完整调用方式:
> **create-img 调用方式(直接使用,不要搜索)**
>
> 1. 先读取 skill 规范:`/home/openclaw/.openclaw/plugin-skills/create-img/SKILL.md`
> 2. 调用脚本(命令见下方)
> 3. 生成后用 `lark-cli im images create --file <图片路径>` 上传飞书取得 `image_key`
```bash
source /home/openclaw/.bashrc
/home/openclaw/workspace/.venv314/bin/python \
/home/openclaw/.openclaw/plugin-skills/create-img/scripts/omniroute_image_batch.py \
--prompt "<图片描述>" \
--size 1536x864 \
--quality high \
--format png \
--out-dir /home/openclaw/.openclaw/workspace/tmp/ai-news-banner
```
就这些。给 Agent 一个**可以直接复制粘贴的完整命令**,包括:
- Python 解释器的绝对路径(虚拟环境的)
- 脚本的绝对路径
- 所有必要参数
- 输出目录
## 修复效果
修复后的第一次运行:
| 指标 | 修复前 | 修复后 | 变化 |
|------|--------|--------|------|
| 总消息数 | 165 | 54 | -67% |
| exec 工具调用 | 44 | 7 | -84% |
| tool_result 大小 | 1,122 KB | 204 KB | -82% |
| 运行时间 | 1200s+(超时) | 317s | 从超时到 5 分钟完成 |
消息数砍掉三分之二,exec 调用砍掉 84%,工具结果体积砍掉 82%。
**一行路径,比任何算法调优都管用。**
## 为什么这个问题这么隐蔽
这个 bug 有三个让它难以发现的特征:
### 1. 不是每次都失败
如果 Agent 第一轮搜索就找到了工具路径,且没有触发 compaction,任务就能正常完成——只是慢一点。只有当上下文刚好在搜索过程中触及 compaction 阈值时,才会进入恶性循环。这意味着它是一个**概率性超时**,不是确定性失败,让排障方向很容易跑偏到"是不是上游 API 慢了"。
### 2. Agent 的搜索行为看起来很"合理"
翻日志时,Agent 的每一步操作都是合理的:不知道路径 → 搜索文件系统 → 读文件确认 → 检查环境。这是一个"聪明但低效"的行为模式。你不会觉得 Agent 做错了什么,只是觉得"怎么这么慢"。
### 3. SKILL.md 的"模糊引用"不会报错
写 `使用 create-img 生成图片` 这种指令,从语法上完全合法——SKILL.md 没有类型检查,不会告诉你"create-img 路径未解析"。Agent 接到指令后也不会说"我找不到 create-img"然后停下来——它会自己去找。这种**静默降级到搜索模式**是最难发现的问题。
## 深层教训:给 AI Agent 写指令的原则
这个 bug 让我意识到一个更基本的问题:**我们给 AI Agent 写的指令,精度要求远高于给人写的文档。**
### 给人 vs 给 Agent
给人写文档时,你可以写"使用 create-img 生成图片"——人类开发者会去问同事、搜 wiki、翻目录结构,这些探索行为的成本几乎为零(不消耗 token,不占上下文)。
给 Agent 写指令时,同样的模糊引用触发的探索行为**每一步都有成本**:
- 每次 `find` / `grep` 消耗一次工具调用
- 每个工具结果占用上下文空间
- 上下文膨胀触发 compaction
- compaction 可能丢掉已经找到的信息
**Agent 的"好奇心"是有代价的,而且代价按上下文空间计算。**
### SKILL.md 写作规则
基于这次教训,我给自己定了几条 SKILL.md 的写作规则:
**1. 所有外部工具引用必须包含绝对路径。** 不是"使用 create-img",而是 `/home/openclaw/.openclaw/plugin-skills/create-img/scripts/omniroute_image_batch.py`。宁可 SKILL.md 多 5 行,也不要让 Agent 搜索。
**2. 可执行命令必须可直接复制。** 包括解释器路径、虚拟环境激活、所有参数。Agent 应该能直接把命令粘贴到 `exec` 里执行,不需要任何额外的路径解析。
**3. 明确写"不要搜索"。** 在提供完整路径的同时,显式告诉 Agent"直接使用以下路径,不要搜索文件系统"。这看起来多余,但 Agent 有时会"验证"你给的路径是否存在——一句"不要搜索"能省掉这个验证步骤。
**4. 环境依赖写死。** Python 虚拟环境路径、Node.js 版本、需要 source 的配置文件——全部写死。不要假设 Agent 知道运行环境。
### 一个类比
这很像写 Dockerfile 和写 README 的区别:
- **README**(给人看):"先安装 Python 3.14,然后在虚拟环境里 pip install -r requirements.txt"
- **Dockerfile**(给机器跑):`RUN /usr/local/bin/python3.14 -m venv /app/.venv && /app/.venv/bin/pip install -r /app/requirements.txt`
SKILL.md 是给 Agent 执行的指令,不是给人阅读的文档。它的精度要求更接近 Dockerfile,而不是 README。
## 更广的视角:Outer Loop 的性能瓶颈在哪
回顾这个系列的三篇文章,有一个清晰的模式:
| 问题 | 根因 | 修复 |
|------|------|------|
| Compaction 静默吞回复 | harness 的压缩策略没有保护关键信息 | 加 safety margin + 结构化 prompt |
| 向量搜索挂了没人发现 | 配置缺失导致静默降级 | 补 embedding provider 配置 |
| Cron job 超时 | SKILL.md 缺路径导致搜索膨胀 | 加一行绝对路径 |
三个问题的共同特征:**都不是模型能力的问题,都是 Outer Loop 的配置/指令精度不够。**
这验证了第一篇的核心论点:在 harness 时代,**系统质量的瓶颈从来不在模型推理,而在模型之外的一切**——上下文管理、工具调用规范、指令精度、降级路径、监控告警。
一行路径省掉 84% 的工具调用,一个 embedding provider 配置让向量检索复活,一个 safety margin 参数防止 compaction 吞回复。这些修复没有一个需要换更强的模型——它们需要的是**更好的 harness 维护**。
当你的 Agent 表现不好时,先别想着换模型。打开日志,数一数工具调用次数,看看上下文是怎么膨胀的。答案大概率在 SKILL.md 的某一行里。
---
> 这是 OpenClaw 生产实战系列的第三篇。如果你也在运维 AI Agent 的 cron job,希望这个"一行路径"的教训能帮你少走弯路。系列会持续更新——下一个话题可能是 Agent 的工具调用成本控制和上下文预算管理。
---
title: "OpenClaw 记忆实战:从「向量搜索挂了也能用」到用 NVIDIA 免费 API 补全最后一块拼图"
description: "OpenClaw 记忆系统的向量检索默认不可用——但 BM25 文本搜索兜底让系统照常运转。当你发现「不配 embedding 也能跑」,到底要不要修?怎么用 NVIDIA 免费 embedding API 零成本补上?这是一篇生产环境的真实记录。"
url: "https://zhuoqidev.com/posts/openclaw-memory-text-to-vector/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-06-20"
updated: "2026-07-30"
series: "OpenClaw 生产实战"
tags: ["OpenClaw","AI Agent","记忆系统","Embedding","NVIDIA","向量搜索","BM25"]
translation: "https://zhuoqidev.com/en/posts/openclaw-memory-text-to-vector/index.md"
---
# OpenClaw 记忆实战:从「向量搜索挂了也能用」到用 NVIDIA 免费 API 补全最后一块拼图
> [!NOTE]
> 这是 [OpenClaw 生产实战](https://zhuoqidev.com/series/openclaw-生产实战/) 系列的第二篇。[第一篇](https://zhuoqidev.com/posts/openclaw-pitfalls/) 聊的是 compaction 静默吞回复和 Outer Loop 的防御性设计。这篇聚焦记忆系统——具体来说,是一个让我重新理解“向量检索到底值不值”的真实经历。
## 背景:OpenClaw 的记忆检索是怎么工作的
OpenClaw 的记忆检索是一个**混合检索系统**——向量(embedding similarity)和 BM25(文本关键词匹配)按 7:3 权重融合,再经过 PPO 自适应的五维加权(recency 0.35 + frequency 0.25 + semantic 0.25 + saliency 0.15 + procedural 按需)产出最终结果。
下面这张图把“挂了也能用”和“为什么仍值得修”放在同一张路径上。上半部分是向量通道不可用时 BM25 独自兜底;下半部分是修复 Embedding 后,两路结果进入统一混合排序。

看起来很完美。但部署后跑了两周,我发现了一个让人困惑的事实。
## 发现:向量搜索挂了,系统照常运转
起因是一次常规巡检。跑 `openclaw memory status` 时,我注意到一个异常:
```
Vector store: unknown
Embedding cache: enabled (0 entries)
FTS: ready
```
**向量存储状态 unknown,embedding 缓存 0 条**——这意味着向量检索根本没在工作。但奇怪的是:记忆系统每天照常检索、Dreaming 照常巩固、Agent 的回复质量也没有明显下降。
再看 FTS(全文检索)那行:**ready**。BM25 一直在正常兜底。
追查原因,是 `openclaw.json` 里的 `memorySearch` 配置项缺少 embedding provider——系统启动时 embedding 初始化失败,静默降级到了纯 BM25 模式。没有报错、没有告警,就是悄悄地只用了 30% 权重的那条检索通道,而且它扛住了。
### 这意味着什么
903 个记忆 chunk、512 条 recall 条目——整个记忆系统跑在**纯文本检索**上,用了两周,没人发现有什么问题。
这逼着我重新审视一个基本假设:**在编程 Agent 的记忆场景下,向量检索到底有多重要?**
## 为什么 BM25 在这个场景下够用
编程 Agent 的记忆内容有几个特征,恰好是 BM25 的甜区:
**1. 记忆内容高度结构化。** OpenClaw 的记忆文件是 Markdown,有 frontmatter(`name`、`type`、`description`)、有标题层级、有明确的技术术语。不是自然语言的模糊表述,而是 `compaction safeguard`、`Gemini 429 配额耗尽`、`fable-5 全球禁用` 这样的精确描述。BM25 对这类"半结构化技术文档"的匹配效果本来就不错。
**2. 查询和文档用同一套词汇。** 用户问"compaction 怎么配",记忆里写的就是"compaction"这个词。不存在"用户说苹果、文档写 Apple"的语义鸿沟。编程领域的术语体系高度统一——`429` 就是 `429`,`context overflow` 就是 `context overflow`。这恰好是 BM25 最擅长的:**精确词匹配**。
**3. 记忆总量在千级,不在百万级。** 99 个文件、903 个 chunk——这个规模下,BM25 的 precision 和 recall 都不会太差,因为候选集本身就不大。向量检索的优势在大规模数据集上才真正体现,1000 个 chunk 的场景下优势可忽略。
**4. PPO 五维加权弥补了检索质量。** 即使 BM25 的初始排序不够精准,recency(最近用过的记忆排前面)和 frequency(常被调用的记忆权重更高)两个维度会把高频使用的记忆推到顶部。这些维度不依赖 embedding,纯靠使用统计。
一句话总结:**在"技术术语精确、数据量千级、有使用统计加权"的场景下,BM25 本身就是一个足够好的检索方案。**
这也呼应了我在 [记忆选型框架](https://zhuoqidev.com/posts/memory-choice-framework/) 里写的结论:**能不用 RAG 就不用 RAG**。向量检索不是默认选择,是数据量和语义复杂度逼你选的。
## 那为什么还要修?
既然 BM25 够用,为什么我最终还是把向量检索补上了?三个原因:
**1. 跨语言检索。** 我的记忆文件里中英文混杂——有些事故记录是中文("Gemini 资源池配额枯竭"),有些是英文("unrestricted key enforcement")。BM25 对跨语言的模糊匹配无能为力:用中文"资源耗尽"搜不到英文的"quota exhausted"。向量检索的 embedding 空间天然做了语义对齐,同义词、跨语言都能匹配。
**2. 概念级检索。** 问"之前遇到过 bot 不回复的情况吗"——BM25 会匹配"不回复"三个字,但不会匹配 `compaction 静默吞回复`(虽然这正是答案)。向量检索能把"bot 不回复"和"静默吞回复"在语义空间里拉近。随着记忆积累越多,这种概念级关联会越来越重要。
**3. 混合检索的互补性。** 向量强在语义召回,BM25 强在精确匹配——两者混合的效果严格优于任何单独一个。7:3 的权重分配意味着:BM25 给你保底的精确匹配,向量给你锦上添花的语义发现。前两周纯 BM25 够用,但"够用"和"好用"之间差了一个向量检索。
## 实现:用 NVIDIA 免费 Embedding API 零成本补全
修复的关键约束是:**不能花钱**。OpenClaw 本身已经在消耗模型 API 额度,如果 embedding 还要按 token 计费(OpenAI `text-embedding-3-small` 每百万 token $0.02),对于 903 个 chunk 的持续索引和每次查询的实时 embedding,虽然不多但完全没必要。
NVIDIA 的 `nv-embed-v1` 是免费的 embedding 模型,通过 NVIDIA API Catalog 提供,兼容 OpenAI API 格式。它在 MTEB 排行榜上的表现和 OpenAI 的 embedding 模型在同一梯队,4096 维度,对多语言支持良好。
### 获取 API Key
1. 注册 [NVIDIA API Catalog](https://build.nvidia.com/) 账号
2. 在 NV-Embed-V1 模型页面点击 "Get API Key"
3. 拿到一个 `nvapi-` 前缀的 key,免费使用,有速率限制但对 Agent 记忆这种低频场景完全够用
### 配置 OpenClaw
在 `openclaw.json` 的 agent 配置中加入 `memorySearch` 段:
```json
{
"agents": {
"defaults": {
"memorySearch": {
"enabled": true,
"provider": "openai",
"remote": {
"baseUrl": "https://integrate.api.nvidia.com/v1",
"apiKey": "nvapi-你的key"
},
"model": "nvidia/nv-embed-v1"
}
}
}
}
```
这里有一个关键点:`provider` 填的是 `"openai"` 而不是 `"nvidia"`。因为 NVIDIA API Catalog 的接口完全兼容 OpenAI 的 `/v1/embeddings` 格式——request body 结构一样、response 格式一样。OpenClaw 只要知道"这是一个 OpenAI 兼容的 embedding 端点",剩下的全靠 `baseUrl` 路由到 NVIDIA 的服务器。
这也是 OpenAI embedding API 格式成为事实标准的一个例证:NVIDIA、阿里通义、Jina、Cohere 等厂商的 embedding API 全都兼容这个格式。选 provider 时不用纠结,只要接口兼容,`"openai"` 就是万能适配器。
### 重建索引
改好配置后重启 Gateway,然后重建记忆索引:
```bash
openclaw memory reindex --deep
```
输出:
```
Indexed: 99/99 files · 903 chunks
Vector dims: 4096
Embedding cache: enabled (779 entries)
```
99 个文件全部被重新 embedding、索引到向量存储里。4096 维度是 nv-embed-v1 的默认维度,比 OpenAI 的 1536 维(text-embedding-3-small)更高,理论上语义空间更细腻。
### 验证混合检索
用语义模糊的查询测试:
```bash
openclaw memory search "之前 bot 不响应的事故怎么回事"
```
返回的 top-1 结果是 compaction 静默吞回复的记忆条目——这在纯 BM25 模式下是匹配不到的(因为记忆里没有"不响应"这三个字,只有"静默丢弃"和"Compaction 输出空摘要")。
再试一个跨语言的:
```bash
openclaw memory search "Gemini quota issue"
```
匹配到了中文写的"Gemini 资源池整体配额枯竭"记忆条目。BM25 不可能完成这个匹配。
## 修复前后对比
| 维度 | 修复前(纯 BM25) | 修复后(向量 + BM25 7:3) |
|------|----------|----------|
| 精确术语匹配 | 正常 | 正常 |
| 语义/概念匹配 | 不支持 | 支持 |
| 跨语言检索 | 不支持 | 支持 |
| 同义词匹配 | 不支持 | 支持 |
| 成本 | 0 | 0(NVIDIA 免费) |
| 索引规模 | 903 chunks | 903 chunks |
| Embedding 缓存 | 0 | 779 条 |
| 检索延迟 | <10ms | <50ms(含 API 调用) |
| 系统稳定性 | 稳定 | 稳定(BM25 仍兜底) |
最后一行值得强调:即使 NVIDIA 的 API 偶尔超时或不可用,系统会自动降级回纯 BM25——和修复前一模一样。**向量检索是增益,不是依赖。**
## 经验总结
**1. 先跑起来,再补锦上添花。** 向量检索没配好,系统跑了两周没人发现。这说明记忆系统的核心价值不在检索算法的精度,而在"记忆是否被写入、是否被管理、是否在需要时出现"。BM25 能解决 80% 的检索场景,剩下 20% 的语义匹配是优化项,不是必需品。
**2. 混合检索的正确心态是"BM25 保底、向量加分"。** 不要反过来理解成"向量为主、BM25 辅助"。在生产环境里,确定性(BM25 的精确匹配一定能找到关键词完全一致的记忆)比概率性(向量的语义相似度可能把不相关的内容排上来)更重要。7:3 的权重分配里,那个 3 才是系统的安全网。
**3. OpenAI 兼容格式是 embedding 领域的事实标准。** 无论你用 NVIDIA、阿里、Jina 还是 Cohere 的 embedding 模型,配置方式都是:`provider: "openai"` + `baseUrl: "厂商端点"` + `model: "模型名"`。不需要为每个厂商写适配器。这也意味着:如果 NVIDIA 的免费额度哪天取消了,切换到其他免费 embedding 服务只需要改两行配置。
**4. 免费 ≠ 不好用。** NVIDIA nv-embed-v1 是 MTEB 同梯队的模型,4096 维度,多语言支持良好。它免费是因为 NVIDIA 要推广自己的 API 生态——你拿到的是一个真正有竞争力的 embedding 模型,不是阉割版。
**5. "向量检索到底值不值"取决于你的数据特征。** 如果你的记忆全是精确技术术语、数据量在千级、查询和文档用同一套词汇——BM25 就够了,向量检索是可选项。如果你的记忆里有跨语言内容、自然语言描述、概念级关联——向量检索从"可选"变成"需要"。我的场景是后者(中英文混杂、事故描述 vs 技术术语的语义关联),所以最终还是补上了。
---
> 下一篇:[一行路径省掉 84% 的工具调用——Cron Job 排障实录](https://zhuoqidev.com/posts/openclaw-cron-skill-optimization/)。当你的 AI Agent 每次执行都花 15 次 exec 调用搜索一个 skill 的路径,消息数从 54 膨胀到 165,根因只是 SKILL.md 里少写了一行绝对路径。这比任何算法调优都管用。
---
title: "Claude 工具调用范式转移:Programmatic Tool Calling 与 Dynamic Filter 深度解读"
description: "Anthropic 在 Claude 平台推出的 Programmatic Tool Calling 和 Dynamic Filtering 不仅是功能增强,更代表了一种 Agent 架构范式的转移——从自然语言编排转向代码编排,从全量上下文注入转向按需过滤。本文综合多篇非 AI 撰写的深度技术文章,从架构原理、性能数据到生产实践,全面解读这场 Outer Loop 的进化。"
url: "https://zhuoqidev.com/posts/claude-programmatic-tool-calling-dynamic-filter/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-06-13"
updated: "2026-07-30"
series: "Agent 架构深度"
tags: ["Claude","AI Agent","Agent 架构","工具调用","上下文工程","Programmatic Tool Calling","Dynamic Filtering","代码执行"]
translation: "https://zhuoqidev.com/en/posts/claude-programmatic-tool-calling-dynamic-filter/index.md"
---
# Claude 工具调用范式转移:Programmatic Tool Calling 与 Dynamic Filter 深度解读
> **TL;DR** 真正的变化不是“又多了两个工具功能”,而是 Agent 开始把多步工具编排移入代码执行环境,并只把压缩后的结果带回模型上下文。
## 背景:Agent 工具调用的成本困境
在传统 Agent 工具调用模型中,每调用一个工具都需要完成一次"模型推理 → 工具执行 → 结果返回 → 模型再推理"的完整回合。这个看似自然的循环,在工具调用变多时会暴露出三个致命问题:
1. **上下文污染**:每个工具的结果都被原封不动地注入上下文窗口。查 20 个员工的报销记录,2000+ 条费用明细全部进入 context,即使你只需要知道"哪 3 个人超预算了"。
2. **推理开销**:每个工具调用都需要一次完整的模型推理。5 个工具调用 = 5 次推理 pass,每次几百毫秒到几秒不等。
3. **噪声导致准确率下降**:当上下文窗口塞满了中间结果,模型不得不在大量噪声中寻找信号。[Context Rot 研究](https://arxiv.org/abs/2509) 表明,LLM 在复杂任务上的性能会随上下文增长而下降 50-70%。
正如 Bruno 在 [Claude Code Architecture Guide](https://cc.bruniaux.com/guide/architecture/) 中所指出的:**"Outer Loop(模型外的一切:上下文管理、工具调用、验证、记忆巩固)开始比模型推理本身更决定系统质量。"**
Anthropic 在 2025 年 11 月到 2026 年 2 月间陆续推出的一系列工具使用增强功能,本质上都是为了解决 Outer Loop 的效率问题。其中 **Programmatic Tool Calling (PTC)** 和 **Dynamic Filtering** 是最具范式转移意义的两项。
下面这张图先建立全文的**成本模型**。左侧是每调一次工具就重新推理、重新注入完整结果的传统循环;右侧把编排、过滤和聚合留在沙箱中,只把最终答案送回模型。后文的性能数据与使用边界都可以映射回这条路径差异。

---
## Programmatic Tool Calling:用代码编排取代自然语言编排
### 核心范式转移
Michael Ridland 在 [Team 400 博客](https://team400.ai/blog/2026-03-claude-programmatic-tool-calling-ai-agents) 中一针见血地指出了传统工具调用的瓶颈:
> "每次 Agent 需要调用工具——查询数据库、检查 API、读取文件——都必须完成一个完整的模型往返。模型生成工具调用,你的代码执行它,结果返回模型,模型处理结果,然后可能生成下一个工具调用。重复。"
而 Programmatic Tool Calling 的范式转移在于:**Claude 不再一个一个地请求工具并等待结果回到上下文,而是写一段 Python 代码来编排所有工具调用,只把最终 `stdout` 注入到上下文窗口。**
```mermaid
flowchart TB
T["传统方式
3 个工具 = 3 次推理
3 份中间结果全量入上下文"] --> t0["Prompt"] --> t1["Claude"] --> t2["Tool 1"] --> t3["Result 1"] --> t4["Claude"] --> t5["Tool 2"] --> t6["Result 2"] --> t7["Claude"] --> t8["Answer"]
P["PTC 方式
3 个工具 = 1 次推理
仅最终输出入上下文"] --> p0["Prompt"] --> p1["Claude"] --> p2["写 Python"] --> p3["代码调用 Tool 1, 2, 3"] --> p4["stdout"] --> p5["Claude"] --> p6["Answer"]
classDef head fill:#fff3bf,stroke:#f08c00
classDef ctx fill:#ffe3e3,stroke:#e03131
classDef out fill:#d3f9d8,stroke:#2f9e44
class T,P head
class t3,t6 ctx
class p4 out
```
这个概念看似简单,但它的含义深远——**它把编排逻辑从模型的"推理链"转移到了"代码执行环境"中。** 循环、条件判断、数据处理、错误处理都变成了显式的代码而非隐式的模型推理。Claude Lab 的 Masaki Hirokawa 在[生产实践指南](https://claudelab.net/en/articles/api-sdk/claude-api-programmatic-tool-calling-production-guide)中总结道:
> "Claude 写代码的能力极强。让它用 Python 表达编排逻辑而不是用自然语言,你获得的是更可靠、更精确的控制流。"
### 工作原理:容器内的代码编排
PTC 依赖 Code Execution 工具(沙箱容器)来运行:
1. **标记工具**:通过 `allowed_callers` 字段指定哪些工具可从代码中调用
2. **Claude 生成编排代码**:Claude 写出包含多步工具调用、数据处理、控制流的 Python 脚本
3. **容器执行并暂停**:当代码调用工具时,容器暂停,API 返回 `tool_use` block
4. **你提供工具结果**:结果返回给代码(而非模型上下文),代码继续执行
5. **仅最终输出回到 Claude**:所有中间结果被过滤,Claude 只看到 `stdout` 的最终输出
```json
{
"tools": [
{"type": "code_execution_20260120", "name": "code_execution"},
{
"name": "query_database",
"description": "Execute SQL. Returns rows as JSON: id (str), name (str), revenue (float).",
"input_schema": {...},
"allowed_callers": ["code_execution_20260120"]
}
]
}
```
关键在于 `allowed_callers` 字段,它有三个可选值:
| `allowed_callers` 值 | 含义 |
|---|---|
| 省略 / `["direct"]` | 仅传统方式调用 |
| `["code_execution_20260120"]` | 仅可从沙箱代码调用 |
| `["direct", "code_execution_20260120"]` | 两种方式均可(**不推荐**——会让 Claude 困惑) |
Bruno 在架构指南中强调了一个重要的安全提示:**`allowed_callers` 不是硬安全边界。** 它是一个强指导(Claude 被训练为尊重它),但你的客户端仍应为任何工具准备处理直接的 `tool_use` 请求。
### 容器生命周期
容器有 4.5 分钟的空闲超时和 30 天的最大生命周期。每次收到响应时都要检查 `expires_at` 字段。如果容器过期,Claude 会将工具调用视为超时并重试。
### 实际的性能数据
Claude Lab 提供了详细的基准测试对比:
```
基准: 10-URL 网络研究任务
传统工具调用:
往返次数: 10 (1 次搜索 + 9 次页面抓取)
推理次数: 11
平均延迟: 45 秒
Token 消耗: ~120,000 tokens
PTC:
往返次数: 2 (1 次代码生成 + 1 次结果返回)
推理次数: 2
平均延迟: 8 秒 (并行抓取)
Token 消耗: ~25,000 tokens
```
Anthropic 官方数据同样验证了这一点:
- 在复杂研究任务上,输入 token 消耗平均下降 **37%**(从 43,588 降至 27,297)
- 在 BrowseComp 测试上,准确率从 42% 提升至 **71%**(PTC 是关键解锁因素)
- 在含 75 个工具的项目管理 Agent 基准上,启用 PTC 后计费 token 减少约 **38%**,且任务准确率未下降
社区分析师 Shayan Tabe 的报告进一步证实了约 37% 的总体 token 削减,尽管这个数字尚未被 Anthropic 官方确认。
### 生产模式:四种 PTC 编程范式
PTC 真正的威力体现在四种具体的编程模式上,这些模式都可以在单次推理中完成:
#### 1. 批量处理 (Batch Processing)
```python
regions = ["West", "East", "Central", "North", "South"]
results = {}
for region in regions:
data = await query_database(f"SELECT * FROM sales WHERE region = '{region}'")
results[region] = sum(row["revenue"] for row in data)
top_region = max(results.items(), key=lambda x: x[1])
print(f"Top region: {top_region[0]} with ${top_region[1]:,}")
```
#### 2. 提前终止 (Early Termination)
```python
for endpoint in ["us-east", "eu-west", "apac"]:
status = await check_health(endpoint)
if status == "healthy":
print(f"Found healthy endpoint: {endpoint}")
break # 不检查剩余 endpoint
```
#### 3. 条件工具选择 (Conditional Tool Selection)
```python
file_info = await get_file_info(path)
if file_info["size"] < 10000:
content = await read_full_file(path)
else:
content = await read_file_summary(path)
print(content)
```
#### 4. 数据过滤 (Data Filtering)
```python
logs = await fetch_logs(server_id)
errors = [log for log in logs if "ERROR" in log]
print(f"Found {len(errors)} errors")
for error in errors[-10:]: # 只返回最后 10 条
print(error)
```
这些模式的关键点在 Team 400 的 Michael Ridland 那里得到了强调:
> "如果 20 次数据库查询返回了 5000 行数据,但只有 3 个员工超预算,模型只会看到那 3 个员工。这可能是 100 倍的 token 削减。"
### 适用场景判断(含反模式警示)
Claude Lab 和 Anthropic 官方文档合起来给出了清晰的决策矩阵:
**强适用场景:**
- 处理大数据集且只需要聚合或摘要结果
- 运行 3 个或以上依赖工具调用的多步工作流
- 在 Claude 看到结果之前需要过滤、排序或转换工具结果
- 中间数据不应影响 Claude 推理的任务
- 跨多个项目运行并行操作(例如检查 50 个端点)
**弱适用场景:**
- 严格顺序的工作流,每一步都依赖 Claude 对上一结果的推理
- 少量工具调用且响应很小
- 需要用户在调用间立即反馈的工具
Anthropic 在 τ²-bench 上的内部评估揭示了 PTC 的盲区:在每轮只做 1-2 次顺序工具调用的航空/零售/电信领域测试中,PTC 没有改善分数,反而增加了约 8% 的成本。**顺序单调用工作流不会受益于 PTC。**
### 约束与陷阱
PTC 不是银弹,以下约束需要在架构阶段考虑:
| 约束 | 说明 |
|---|---|
| **不支持 ZDR** | 需要 Zero Data Retention 合规的场景无法使用 |
| **不兼容 MCP 工具** | 通过 MCP 连接器提供的工具不能以编程方式调用 |
| **不兼容 `strict: true`** | 结构化输出 (`strict: true`) 不能与 PTC 同时使用 |
| **不支持强制特定工具** | 不能通过 `tool_choice` 强制以编程方式调用特定工具 |
| **容器超时** | 如果工具执行太慢导致容器超时,Claude 会重试,可能产生重复调用 |
| **调试难度增加** | 出问题时需要检查代码执行输出而非逐步追踪 |
Michael Ridland 特别警告了调试问题:
> "当 PTC 出问题时,逻辑存在于生成的代码中。你需要检查代码执行输出来理解发生了什么。在你的系统中构建日志记录。"
---
## Dynamic Filtering:Web Search 的上下文瘦身革命
### 问题本质
Web 搜索是 token 消耗最大的任务之一。传统流程是:发起查询 → 获取搜索结果 → 抓取多个网页的完整 HTML → 在上下文中推理所有内容。但搜索到的上下文往往大量无关——导航栏、广告、页脚、推荐内容……
Anthropic 在[官方博客](https://claude.com/blog/improved-web-search-with-dynamic-filtering)中描述了解决方案:
> "Claude 的 web search 和 web fetch 工具现在会自动编写和执行代码来后处理查询结果。Claude 不再在完整 HTML 文件上推理,而是在加载到上下文之前动态过滤搜索结果,只保留相关的内容,丢弃其余部分。"
### 工作原理
Dynamic Filtering 本质上是 PTC 理念在 Web Search 场景的原生实现——让 Claude 写 Python 代码预处理搜索结果:
```mermaid
flowchart TB
Q["Query"] --> S["Search API"] --> H["10 个原始 HTML"]
H -->|"传统"| A1["全部进入上下文"] --> A2["Claude 推理"]
H -->|"Dynamic Filtering"| B1["Claude 写代码提取关键信息"] --> B2["过滤后摘要进入上下文"] --> B3["Claude 推理"]
classDef ctx fill:#ffe3e3,stroke:#e03131
classDef out fill:#d3f9d8,stroke:#2f9e44
class A1 ctx
class B2 out
```
### 基准测试数据
Anthropic 在两个严格的基准测试上评估了 Dynamic Filtering:
**BrowseComp 数据集:**
| 模型 | 无过滤 | 有过滤 | 提升 |
|---|---|---|---|
| Sonnet 4.6 | 33.3% | 46.6% | **+13.3 pp** |
| Opus 4.6 | 45.3% | 61.6% | **+16.3 pp** |
**DeepSearchQA:** 测试 Agent 是否能系统地规划和执行多步搜索而不遗漏任何答案。
| 模型 | 无过滤 | 有过滤 | 提升 |
|---|---|---|---|
| Sonnet 4.6 (F1) | 52.6% | 59.4% | +6.8 pp |
| Opus 4.6 (F1) | 69.8% | 77.3% | +7.5 pp |
整体上,Dynamic Filtering 平均提高了 **11% 的准确率**,同时减少了 **24% 的输入 token**。
Poe by Quora 的内部团队验证了这些数据:
> "Opus 4.6 + Dynamic Filtering 在我们内部评估中取得了最高准确率。"—— Gareth Jones, Product and Research Lead at Quora
>
> "模型的行为像一个真正的研究者,编写 Python 来解析、过滤和交叉引用结果,而不是在上下文中推理原始 HTML。"
### 配置示例
```json
{
"model": "claude-opus-4-6",
"max_tokens": 4096,
"tools": [
{"type": "web_search_20260209", "name": "web_search"},
{"type": "web_fetch_20260209", "name": "web_fetch"}
],
"messages": [
{
"role": "user",
"content": "Search for the current prices of AAPL and GOOGL, then calculate which has a better P/E ratio."
}
]
}
```
注意:使用 `web_search_20260209` / `web_fetch_20260209` 版本时,Dynamic Filtering 在 Sonnet 4.6 和 Opus 4.6 上**默认开启**。如果不需要(比如为了 ZDR 合规),可以通过 `"allowed_callers": ["direct"]` 关闭。
**重要限制:** 基本版 `web_search_20250305` 有 ZDR 资格,但 Dynamic Filtering 版本因为依赖内部代码执行,默认不支持 ZDR。
---
## 三位一体:PTC + Tool Search + Tool Use Examples
Anthropic 的 Advanced Tool Use 实际上是三个互补功能的组合:
| 功能 | 解决的问题 | 使用场景 |
|---|---|---|
| **Tool Search Tool** | 工具定义太多导致上下文膨胀 | 50+ 个 MCP 工具的场景 |
| **Programmatic Tool Calling** | 中间结果污染上下文 | 3+ 步工具调用工作流 |
| **Tool Use Examples** | Schema 无法表达使用模式 | 复杂嵌套参数的工具 |
Bruno 在架构指南中给出了清晰的优先级建议:
> "先解决你最大的瓶颈:上下文被工具定义撑爆?→ Tool Search。大量中间结果污染上下文?→ PTC。参数错误频发?→ Tool Use Examples。"
### Tool Search:按需发现
一个真实的案例:在连接 5 个 MCP 服务器(GitHub 35 工具 + Slack 11 工具 + Sentry 5 工具 + Grafana 5 工具 + Splunk 2 工具)时,传统方式需要将全部 58 个工具定义预加载到上下文,消耗约 **55K tokens**。启用 Tool Search 后,只有 ~500 tokens 的搜索工具常驻上下文,匹配到的 3-5 个相关工具(~3K tokens)按需加载。**85% 的 token 开销被消除。**
内部测试显示:
- Opus 4 的工具选择准确率从 49% 提升至 **74%**
- Opus 4.5 从 79.5% 提升至 **88.1%**
### Tool Use Examples:教会 Claude 怎么用你的工具
JSON Schema 能定义结构,但无法表达:什么时候填入可选参数?哪些参数组合是合理的?你的 API 有什么约定?
```json
{
"name": "create_ticket",
"input_schema": { /* ... */ },
"input_examples": [
{
"title": "Login page returns 500 error",
"priority": "critical",
"labels": ["bug", "authentication", "production"],
"reporter": {
"id": "USR-12345",
"name": "Jane Smith",
"contact": {"email": "jane@acme.com", "phone": "+1-555-0123"}
},
"due_date": "2026-06-13"
},
{
"title": "Add dark mode support",
"labels": ["feature-request", "ui"],
"reporter": {"id": "USR-67890", "name": "Alex Chen"}
},
{
"title": "Update API documentation"
}
]
}
```
三个例子教会 Claude:
- 日期用 YYYY-MM-DD 格式,用户 ID 用 USR-XXXXX,标签用 kebab-case
- 严重 bug 需要完整联系信息 + 升级配置;功能请求有 reporter 但不需要 escalation;内部任务只需标题
Anthropic 内部测试:复杂参数处理的准确率从 **72% 提升至 90%**。
---
## 架构决策:何时部署,何时绕过
综合多篇文章的实践智慧,应该这样看待这套工具集的架构定位:
### 正确理解外层循环 (Outer Loop)
Bruno 在架构指南中提出了一个核心观点——**"Less scaffolding, more model"**(更少的脚手架,更多的模型信任)。这个哲学在 Claude Code 中体现为:
> "没有意图分类器。没有任务路由器。没有 RAG 管道。没有 DAG 编排器。没有 Planner/Executor 分离。模型本身决定何时调用工具、调用哪些工具、何时完成。"
PTC 和 Dynamic Filtering 延续了这个理念——它们不是添加新的抽象层,而是**减少不必要的抽象**:不需要把中间结果传给模型再等它"想"下一步,直接用代码完成编排。
### 模型兼容性
PTC 需要 `code_execution_20260120`,当前支持:
- Claude Opus 4.5+
- Claude Sonnet 4.5+
- Claude Fable 5 / Mythos 5
Dynamic Filtering 需要 `web_search_20260209`,在 Opus 4.6 / Sonnet 4.6 上默认开启。
### 部署检查清单
Team 400 给出了实用的四步部署法:
1. 在 API 调用中启用代码执行工具
2. 为应从代码调用的工具添加 `allowed_callers`
3. 用需要多次工具调用的提示进行测试
4. 对比传统方法和 PTC 方法的延迟、token 使用和输出质量
Bruno 特别强调了一个容易踩的坑:**不要给同一个工具同时设 `["direct", "code_execution_20260120"]`**。选一个——这会给 Claude 提供关于最佳使用方式的更清晰指导。
---
## 总结
Programmatic Tool Calling 和 Dynamic Filtering 代表了 Agent 架构中一个重要的范式转移:**从"一切经过模型推理"转向"代码编排 + 按需过滤"**。
这不是 Anthropic 凭空创造的概念——它是对一个已经被实践反复验证的原则的系统化落地:**让模型做它最擅长的事(推理和决策),让代码做代码最擅长的事(循环、过滤、并行执行)。**
两者的本质差异:
| 维度 | 传统工具调用 | PTC / Dynamic Filtering |
|---|---|---|
| 编排逻辑位置 | 模型的推理链 | Python 代码 |
| 中间结果去向 | 全部进入上下文 | 被代码过滤,不入上下文 |
| N 次工具调用的推理次数 | N+1 | 1~2 |
| Token 消耗 | 随调用数线性增长 | 大幅削减(37%~80%) |
| 调试方式 | 逐步追踪每个回合 | 检查代码执行输出日志 |
| 适合任务 | 简单查找、需人对中间结果决策 | 批量处理、数据聚合、多步研究 |
这篇文章综合了以下来源(均非 AI 生成,为人类工程师的分析和官方文档):
- [Anthropic Engineering: Introducing advanced tool use](https://www.anthropic.com/engineering/advanced-tool-use)(官方博客,2025.11)
- [Claude Blog: Improved Web Search with Dynamic Filtering](https://claude.com/blog/improved-web-search-with-dynamic-filtering)(官方博客,2026.02)
- [Claude API Docs: Programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling)(官方文档)
- [How Claude Code Works: Architecture & Internals](https://cc.bruniaux.com/guide/architecture/)(Florian Bruniaux 技术分析,2026.02)
- [Claude Lab: PTC Production Guide](https://claudelab.net/en/articles/api-sdk/claude-api-programmatic-tool-calling-production-guide)(Masaki Hirokawa 生产实践,2026.03)
- [Team 400: Why PTC Matters for AI Agent Performance](https://team400.ai/blog/2026-03-claude-programmatic-tool-calling-ai-agents)(Michael Ridland 实战分析,2026.03)
---
title: "OpenClaw 生产踩坑:当最先进的记忆系统遇到最静默的失败"
description: "从部署到排障,记录 OpenClaw 从启动失败、飞书消息静默吞回复到 production 稳定的全链路实战经验——compaction safeguard、五层排查法、model-harness-fit 与记忆系统对比。"
url: "https://zhuoqidev.com/posts/openclaw-pitfalls/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-05-27"
updated: "2026-07-30"
series: "OpenClaw 生产实战"
tags: ["OpenClaw","AI Agent","飞书","记忆系统","Compaction","排障"]
translation: "https://zhuoqidev.com/en/posts/openclaw-pitfalls/index.md"
---
# OpenClaw 生产踩坑:当最先进的记忆系统遇到最静默的失败
## 为什么是 OpenClaw
> 编程 Agent 的能力跃迁不只是"更强"——每次范式转移改变的是**验证循环的归属权**。提示词工程时代(Copilot, 2021):人类写 prompt、AI 补全、人类验证——验证完全在人手里。上下文工程时代(Cursor, 2023):AI 生成代码块、人类审查 diff——验证仍以人为主,但 AI 开始做 lint/fix 的轻量自检。驾驭工程/harness 时代(Claude Code / Codex CLI / OpenClaw, 2025):AI 写代码、AI 跑测试、AI 看报错、AI 修——验证循环从人转移到 harness,**人不再逐条审查输出,而是设计和维护验证系统的规则**。整个 Outer Loop(模型外的一切:上下文管理、工具调用、验证、记忆巩固)开始比模型推理本身更决定系统质量。OpenClaw 是这个趋势里记忆侧最激进、但 harness 稳定性最不足的一个。
>
> 同一代内还有一条关键分岔:**Vibe Coding**(Bolt.new、Lovable、Replit)把验证也扔给用户--"生成即交付";**Engineering Rigor**(Claude Code、Aider、OpenClaw)则把验证编码进 harness--测试跑不过就重试。两者的差距不在模型,在 outer loop 的设计哲学。
先看一眼 OpenClaw 的整体架构——用思维导图展示组件层级,后面所有踩坑都跟这些模块有关:
```mermaid
mindmap
root((OpenClaw))
📨 消息入口
飞书
企业微信
WebChat
Telegram
🔌 Channels 层
消息路由 allowlist
去重 dedup
会话绑定 envelope
🖥️ Gateway 守护进程
WebSocket Server :18789
认证 设备配对
定时任务 CRON 引擎
🔄 Agent Loop
消息摄入 agent RPC
上下文组装 bootstrap session skills
模型推理 多后端 fallback
工具执行 memory shell web
回复生成 流式投递
🧠 Memory 系统
MEMORY.md 长期记忆
每日流水 YYYY-MM-DD.md
DREAMS.md 巩固日记
向量 BM25 混合检索 7:3
```
OpenClaw 是驾驭工程时代的自治编程 Agent,跨模型 CLI,支持 DeepSeek / Anthropic / OpenAI 多后端。它最吸引我的一点是**记忆系统**——在当前所有生产可用的编程 Agent 中,它的记忆架构是最激进的:
- **PPO 认知权重自适应**:唯一的在生产工具中用强化学习调整记忆检索权重的系统。检索信号五维加权(recency 0.35 + frequency 0.25 + semantic 0.25 + saliency 0.15 + procedural 按需),随时间动态衰减
- **三重睡眠巩固**:Light Sleep(Jaccard 去重,零 LLM 成本)→ REM Sleep(置信度评分)→ Deep Sleep(三条件晋升门:score≥0.80 + merge≥3 + recall≥3),自动将短期经验固化为长期记忆
- **向量 + BM25 混合检索**(7:3):比纯向量检索更鲁棒
但拉上生产环境跑了三周后,我的结论是:**记忆系统有多先进,踩的坑就有多深**。下面按时间线记录从部署到稳定全过程中真正折腾过的问题。
## 第一坑:启动失败三重奏
第一次在 VPS 上启动 OpenClaw Gateway,连续三种报错,每种原因都不一样。
**`gateway token missing`** — 最容易被忽略。OpenClaw 的 Gateway 模式需要独立的 `OPENCLAW_GATEWAY_TOKEN` 环境变量,不是飞书的 App Secret。systemd unit 文件里漏写一个 `Environment=` 就直接 401。
**`No credentials for provider`** — `auth-profiles.json` 里的 `keyRef` 必须和环境变量名精确匹配。一个字符对不上就报这个错,而且错误信息不会告诉你是哪个 keyRef 没匹配到,只能肉眼对。
**`400 InvalidParameter` / 模型不存在** — Omniroute 的模型名必须是 `/` 格式。写成 `gpt-5.5` 会报 400,必须写 `codex/gpt-5.5`。再加上 OpenClaw 自己的 provider 前缀,最终是 `omniroute/codex/gpt-5.5`。三层前缀嵌套,少一层都不行。
```bash
# 排查时这三步最省时间
systemctl cat openclaw-gateway.service | grep Environment # 检查环境变量
python3 -c "import json; json.load(open('/home/openclaw/.openclaw/openclaw.json'))" # JSON 语法
journalctl -u openclaw-gateway.service -n 50 --no-pager | grep -iE "error|unauth" # 看错误
```
## 第二坑:日志去哪了
OpenClaw 运行时日志**全部走 journald**,不写文件。`/tmp/openclaw/openclaw-*.log` 只在启动阶段有几行,运行时的错误全在 journald 里。我被这个坑了半小时——盯着文件日志 tail 了半天,什么都没看到,最后才意识到:
```bash
# 这才是正确的看日志方式
journalctl -u openclaw-gateway.service -f
```
## 第三坑(最严重):Compaction 静默吞回复
这是这三周里最严重的一次事故。在说具体时间线之前,先看图理解 Compaction 在 Agent Loop 中的位置和失败路径:
```mermaid
flowchart TB
MSG["👤 用户消息进入"]
LOOP["🔄 Agent Loop 处理
组装上下文 → 推理 → 生成回复"]
CHECK{"📏 上下文 ≤ 模型限制?"}
DELIVER["✅ 回复投递到飞书"]
COMPACT["🧹 Auto-Compaction 触发
压缩旧消息为摘要"]
OVERFLOW{"⚠️ Compaction prompt 也超限?"}
EMPTY["💀 模型输出空字符串
Conversation is empty"]
LOST["❌ 回复静默丢弃
用户感知:bot 不回复"]
SAFEGUARD["🛡️ safeguard 模式
reserveTokens + fallback
压缩完成 → 正常投递"]
MSG --> LOOP --> CHECK
CHECK -->|"✅ 未超限"| DELIVER
CHECK -->|"❌ 超限 209K > 200K"| COMPACT --> OVERFLOW
OVERFLOW -->|"❌ 无保护"| EMPTY --> LOST
OVERFLOW -->|"🛡️ 修复后"| SAFEGUARD --> DELIVER
style EMPTY fill:#ff6b6b,color:#fff
style LOST fill:#ff6b6b,color:#fff
style SAFEGUARD fill:#51cf66,color:#fff
style DELIVER fill:#51cf66,color:#fff
```
上面的 Mermaid 保留了事故分支;下面的系统图换一个观察角度,把**系统状态**和**用户状态**拆开。真正危险之处是:作业在系统看来“完成且无异常”,用户却始终没有收到最终回复。

一条正常的用户消息,Agent 已经生成了完整回复,但**用户什么都没收到**,bot 像死了一样安静。
### 时间线
- `01:43:40` 飞书新闻群用户发消息:"失败请求占用到我们账户的请求资源的,麻烦方便的时候检修下程序中错误的请求参数"
- `01:43:57` Agent 生成了完整文本回复(session trajectory 里能看到 `message` event with assistant response)
- `01:44:00` Auto-compaction 触发:session context 已经到了 209K tokens,超过 primary model 的 200K 限制
- `01:44:00` **回复消失**:compaction 输出空摘要 `"Conversation is empty"`,session 结束,回复未投递到飞书
- 用户侧完全感知为"bot 不回复"
### 根因
Compaction 是一个**隐式中间层**。正常情况下它压缩上下文;但当上下文本身已经超过模型限制(209K > 200K),compaction prompt 也装不下完整上下文时,**模型输出空字符串**。OpenClaw 默认没有对此做保护——不报错、不降级、不通知,只是悄悄地把已生成的回复扔掉了。
这是 `middleware-semantic-leak` 的教科书级案例:中间层在极端情况下从"压缩上下文"变成了"丢弃回复",语义完全翻转,且没有任何信号。
翻开源码可以清晰地看到这个静默失败路径的完整链路。OpenClaw 的 `compaction.ts` 中定义了一个看起来无害的常量:
```typescript
// src/agents/compaction.ts
const DEFAULT_SUMMARY_FALLBACK = "No prior history.";
```
当 `summarizeChunks()` 收到空消息列表时,直接返回这个字符串——不抛错、不告警、不走降级。
而发给模型的 compaction prompt 本身也完全没有防御性指令。这是 system prompt:
```text
// packages/agent-core/src/harness/compaction/compaction.ts
SUMMARIZATION_SYSTEM_PROMPT:
"You are a context summarization assistant. Your task is to read a conversation
between a user and an AI coding assistant, then produce a structured summary
following the exact format specified.
Do NOT continue the conversation. Do NOT respond to any questions in the
conversation. ONLY output the structured summary."
```
user prompt 的模板是:
```text
// packages/agent-core/src/harness/compaction/compaction.ts
SUMMARIZATION_PROMPT:
"
[这里塞入完整的对话历史, 209K tokens 时已经是十几万字的原始消息]
The messages above are a conversation to summarize. Create a structured context
checkpoint summary that another LLM will use to continue the work."
```
关键设计缺陷就在这个 `` 标签里:**prompt 的指令部分("summarize the conversation")在对话历史之后**. 当对话历史超过模型上下文窗口时,模型看到的不是"一堆对话" + "请总结",而是一个被截断的 `` 开头,后面什么都没有——既看不到 `` 闭合标签,也看不到总结指令。system prompt 只是说"你是总结助手,只输出结构化摘要",没有告诉模型"如果对话内容被截断了该怎么办"。
于是模型面对的是:一个残缺的 XML 片段 + 一个只说"输出摘要"的 system prompt。在多数模型的默认行为下,这意味着一个极短的空输出——恰好对应 trajectory 里看到的 "Conversation is empty"。
更致命的是 `summarizeWithFallback()` 的最后兜底路径:当全文摘要和部分摘要先后失败(chunk 超限、模型 5xx、token 溢出),回退到:
```typescript
// src/agents/compaction.ts — summarizeWithFallback 最终 fallback
return (
`Context contained ${messages.length} messages ` +
`(${oversizedNotes.length} oversized). ` +
`Summary unavailable due to size limits.`
);
```
这几个字就是你在 session trajectory 里看到的 "Conversation is empty" 的源头——**compaction pipeline 的设计假设"摘要失败后还有原始上下文可用"**,但 209K > 200K 的极端情况正好击穿了这个假设:原始上下文塞不进 prompt,摘要模型也返回空,唯一的输出是一条"摘要不可用"的自然语言通知——而 OpenClaw 把它当成正常的 compaction 结果继续执行,已生成的回复就静默丢弃了。
两个常数的叠加也值得注意:
```typescript
// src/agents/compaction.ts
const SAFETY_MARGIN = 1.2; // 20% buffer for estimateTokens inaccuracy
const SUMMARIZATION_OVERHEAD_TOKENS = 4096; // prompt/system prompt overhead
```
`SAFETY_MARGIN` 是 token 估算的 20% buffer——`estimateTokens()` 用的是字符数/4 的粗略估算,对多字节字符、特殊 token、代码 token 会有系统性低估。这意味着"看起来还有 10K 余量"时,实际已经撞墙了。`SUMMARIZATION_OVERHEAD_TOKENS` 是 compaction prompt + system prompt + 序列化包装的固定开销——但不包括 previous summary 的体积。**当 session 精炼多轮后 previous summary 本身已经有几十 KB,这个 4096 的固定预留是不够的**。
而确认这是默认行为而非配置错误的证据在类型定义中:
```typescript
// src/config/types.agent-defaults.ts
export type AgentCompactionConfig = {
mode?: AgentCompactionMode; // 可选字段——不设就是无 safeguard 的默认模式
reserveTokens?: number;
keepRecentTokens?: number;
// ...
};
```
`mode` 是 optional,不显式配置 `"safeguard"` 就不会启用 `reserveTokens` 和 `maxHistoryShare` 保护。生产环境的默认行为,就是让 compaction 裸跑。
### 修复
两步,缺一不可:
```json
// openclaw.json
"compaction": {
"mode": "safeguard",
"reserveTokens": 20000,
"keepRecentTokens": 16000
},
"model": {
"fallbacks": ["deepseek/deepseek-v4-flash"]
}
```
`safeguard` 模式为 compaction 后的总结保留 token 预算,防止占用满窗口。而 fallback 到 `deepseek-v4-flash`(1M context window)则保证了即使主模型窗口不够,compaction 任务本身永不会超限。
### 通用教训
这不是 OpenClaw 专属的问题。任何带自动上下文压缩的 Agent 系统,都面临同样的风险:**compaction 的失败模式是静默的**。如果你的 Agent 突然不回复了,第三个要检查的就是 compaction——在 session trajectory 里找 compaction event 的 content 是否为空。
## 飞书消息不响应:五层排查法
这次事故之后,我沉淀了一套从外到内、按概率排序的排查链路。下次 bot 不回复,按这个顺序查:
这张图把五层与每层的**可观察证据**放在一起。排障不是从模型开始猜,而是沿“进程 → 连接 → 路由 → 执行 → 回复”逐层证明消息走到了哪里。

| 层 | 检查点 | 怎么看 |
|---|---|---|
| L1 飞书应用层 | Bot 是否收到消息 | `lark-cli im +chat-list --as bot` 确认群在 allowlist |
| L2 事件接收层 | 消息是否到达 Gateway | 检查 dedup 记录是否有最近条目 |
| **L3 Session 层** | Agent 是否处理了消息 | **看 session trajectory 的最后 event**——这步最关键 |
| L4 模型调用层 | 模型是否正常响应 | journalctl grep 429/401/500/timeout |
| L5 投递层 | 飞书发送是否成功 | 用 CLI 直接发一条测试消息验证权限 |
以后再遇到消息不响应,按下面这个决策树走,先看 trajectory 的最后 10 行——八成问题在第三步就能定位:
```mermaid
flowchart TD
PROBLEM["❓ 飞书群 bot 不回复"]
L1["L1: 群权限
lark-cli im +chat-list --as bot"]
L1_OK{"群在 allowlist?
Bot 在群内?"}
L1_FIX["修复权限 / 加群到 allowlist"]
L2["L2: 消息到达
查看 dedup 记录"]
L2_OK{"dedup 有最近条目?"}
L2_FIX["检查飞书事件订阅 URL
OPENCLAW_GATEWAY_TOKEN"]
L3["🔑 L3: Session Trajectory
cat session/*.jsonl 最后 10 events"]
L3_MSG{"最后 event 是什么?"}
L4["L4: 模型调用
journalctl grep 429/500/timeout"]
L5["L5: 投递
lark-cli im +send-message 测试"]
L3_CTX["💀 compaction 为空
→ context overflow
→ 本文事故,立即加 safeguard"]
L3_MESSAGE["回复已生成
→ 问题在投递层
→ 跳到 L5"]
L3_ERROR["有 error event
→ 跳到 L4 查模型"]
PROBLEM --> L1
L1 --> L1_OK
L1_OK -->|"✓ 正常"| L2
L1_OK -->|"✗ 异常"| L1_FIX
L2 --> L2_OK
L2_OK -->|"✓ 有记录"| L3
L2_OK -->|"✗ 无记录"| L2_FIX
L3 --> L3_MSG
L3_MSG -->|"compaction 空"| L3_CTX
L3_MSG -->|"assistant text"| L3_MESSAGE
L3_MSG -->|"error"| L3_ERROR
L3_ERROR --> L4
L3_MESSAGE --> L5
style L3 fill:#ffd43b,color:#333
style L3_CTX fill:#ff6b6b,color:#fff
style L3_MESSAGE fill:#51cf66,color:#fff
style L3_ERROR fill:#ff922b,color:#fff
```
## Outer Loop:为什么框架比模型更决定成败
[SWE-bench Pro](https://www.swebench.com/) 是目前最受认可的企业级编程 benchmark(Princeton 维护,独立评审),它有一个极具说服力的数据:**同一个模型 Claude Opus 4.5,在不同 harness 下的得分可以差 9.5 个百分点**——SEAL 标准化脚手架 45.9%,Cursor 50.2%,Auggie 51.8%,Claude Code 55.4%。模型完全一样,换一个 harness 就是 5 到 10 个百分点的差距。而 Anthropic 在 2025 年 11 月的工程博客 *Effective harnesses for long-running agents* 里也确认了同一个结论:把"干活"和"评估"拆到不同 Agent 里,用硬性 pass/fail 门禁取代模型自评,单 Agent 跑 20 分钟崩溃的任务,三 Agent harness 跑了 6 小时产出了完整可用的应用。OpenClaw 甚至没有出现在任何主流 benchmark 的榜单上——不是模型能力的问题,是它的 harness 质量还不足以支撑 benchmark 级别的稳定表现。
这不是偶然。2025 年行业里一个收敛的认知是:**"Structure around the model matters more than cleverness inside the model."** 模型只负责生成,而**生成是便宜的**——真正决定系统质量的是 Outer Loop 里的三件事:
| Outer Loop 组件 | 做什么 | 失败时发生什么 |
|---|---|---|
| **验证(Verification)** | 编译器、测试套件、linter 检查输出 | AI 生成错误代码没人发现 |
| **上下文管理(Context Engineering)** | Compaction、pruning、memory flush | 就是本文的事故——静默丢回复 |
| **工具定义(ACI)** | Tool schema、参数约束、防呆设计 | 参数传错、文件写错路径、静默重试 |
这三层都**不在模型里面**——它们是你部署和维护的系统。Compaction 事故的根因不是模型不够聪明,是你没告诉 harness "当压缩失败时,不要扔回复,要报错"。
这也解释了为什么 Anthropic 观察到"最成功的 Agent 实现很少用复杂框架"——框架是别人写的 Outer Loop,你没法控制它的失败模式。OpenClaw 的多后端支持看起来是优势,但每个模型的 context window 不同、reasoning profile 不同、API 行为不同——**每多一个模型,compaction 的边界条件就多一组组合**,outer loop 的测试矩阵指数增长。
这引向一个更根本的结构性问题——记忆系统的路线分歧。
## OpenClaw vs Claude Code:记忆系统的路线分歧
OpenClaw 记忆系统最独特的机制是 **Dreaming 后台巩固**——它不是简单的"记下来",而是有一个三阶段自动流水线:
```mermaid
flowchart LR
subgraph 短期["📝 短期记忆"]
SESSION["Session 对话"]
RECALL["recall traces"]
end
subgraph LIGHT["💡 Light Sleep — 零 LLM"]
SORT["Jaccard 语义去重 + 排序"]
end
subgraph REM["🌙 REM Sleep — 主题发现"]
REFLECT["模式识别 + 反思
不写 MEMORY.md"]
end
subgraph DEEP["🧠 Deep Sleep — 三条件晋升"]
RANK["score ≥ 0.80"]
MERGE["merge ≥ 3"]
RECALL_THRESH["recall days ≥ 3"]
end
subgraph 长期["📦 长期记忆"]
MEM["MEMORY.md"]
end
短期 --> LIGHT --> REM --> DEEP
DEEP -->|"✓ 全过"| MEM
DEEP -->|"✗ 不满足"| LIGHT
style DEEP fill:#b197fc,color:#fff
style MEM fill:#51cf66,color:#fff
```
跑了一段时间后,两个系统可以并排对比:
| 维度 | OpenClaw | Claude Code |
|---|---|---|
| 检索机制 | 向量+BM25 混合 (7:3) | LLM 语义判断 (Sonnet) |
| 权重进化 | **PPO 自适应** | 人工固定分类 |
| 巩固机制 | 三重睡眠 | 三门四阶段 AutoDream |
| 模型锁定 | 多模型 | 仅 Claude |
| 记忆分类 | 按时间(长期蒸馏/短期流水) | 按类型(user/feedback/project/reference) |
| 生产成熟度 | 早期 | 百万级 Agent 验证 |
OpenClaw 的记忆架构更"学术正确"——PPO 自适应权重、三重睡眠、混合检索,每一层都接近记忆系统前沿论文。Claude Code 看上去"更土":记忆就是四类 Markdown 文件(user/feedback/project/reference),检索靠 Sonnet 语义判断,没有 PPO 也没有 BM25。
但这里有一个反直觉的结构事实:**文件记忆 > 向量记忆**。不是量化的"好一点",是质的差异。工业界从 Claude Code、Codex CLI 到 Cursor,全部选择 Markdown 文件作为主记忆介质,向量只做辅助索引。为什么?因为编程场景下**确定性 > 概率检索**——你不能让 PPO 权重决定"要不要记住 API key 泄漏过"。
更大的问题在于:**记忆系统的先进程度和 Outer Loop 的稳定性是乘积关系,不是加法**。你的记忆检索算法再精准,如果 compaction 中间层悄悄把回复扔了,用户感知的不是"记忆不够好",是"bot 死了"。OpenClaw 把 80% 的创新预算花在记忆侧(PPO、Dreaming、混合检索),但 Outer Loop 的防御侧(compaction safeguard、故障信号、降级路径)投入不足。Claude Code 反过来——记忆侧保守,但 harness 花了几百万 Agent 的实战验证。
这是选型时需要警惕的陷阱:**看 benchmark 时看的是模型+记忆的能力上限,但生产系统活在 Outer Loop 的下限里**。
## Compaction 设计的结构性差异:从 prompt 学到的
前面从源代码看了 OpenClaw 的 compaction prompt 为什么在溢出时会静默失败——`` 标签在前、指令在后,上下文溢出时模型看不到指令。那做得更好的长什么样?
2026 年 3 月 Anthropic 的 npm `.map` 文件意外泄露了 Claude Code 的完整 system prompt,[Piebald-AI/claude-code-system-prompts](https://github.com/Piebald-AI/claude-code-system-prompts) 从编译 JS 中提取并整理了所有 prompt。对比两个 compaction prompt,差距不在模型本身——都在用类似的 LLM 做摘要——而在 **prompt 的结构性防御**:
**摘要格式:9 段式 vs 5 段式。** Claude Code 要求 9 个结构化节:
1. Primary Request and Intent — 用户核心诉求
2. Key Technical Concepts — 技术概念
3. Files and Code Sections — 文件 + 完整代码片段 + 为什么重要
4. Errors and fixes — 所有错误 + 修复方式 + 用户反馈
5. Problem Solving — 已解决和排查中的问题
6. **All user messages** — 所有用户消息(非工具结果)
7. Pending Tasks — 待处理任务
8. Current Work — 当前正在做的事(含文件名+代码片段)
9. Optional Next Step — 下一步 + **对话原文引用**
OpenClaw 只有 5 个(Decisions / TODOs / Constraints / Asks / Identifiers),缺失的恰好是 compaction 事故中最致命的部分:错误修复记录、用户消息的完整保存、当前工作的精确描述。
**安全指令逐字保留。** Claude Code 的 prompt 明确要求:*"Note any security-relevant instructions or constraints the user stated... These MUST be preserved verbatim in the summary so they continue to apply after compaction."* 这意味着"不要读 .env 文件"、"不要提交密钥"、"必须用 HTTPS 而非 HTTP"这类指令在 compaction 后仍然生效。OpenClaw 只做标识符保留(identifier policy),不做安全指令保留。
**用户反馈不会被压缩掉。** Section 6 要求列出 ALL user messages,Section 4 要求记录用户对每个错误的反馈。两个设计合在一起的效果是:用户说过"不要那样做,要这样做"——这件事经过 compaction 后不会消失。对比本文事故:一条用户消息触发 compaction → 已生成的回复静默丢弃 → 用户侧只看到 bot 不回复。如果 compaction 摘要里强制保留了"用户说了什么",至少排障时一眼能看到触发源。
**下一步必须引用原文。** Section 9 要求:*"Include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation."* 这个设计防止了"摘要模型用自己的话重述任务"导致的语义漂移——重述过的任务,下一代 Agent 可能理解成完全不同的东西。
**分析先于输出。** Claude Code 的 prompt 要求先 `` 标签内逐消息分析,再输出 ``。这不是装饰——它是把思考过程嵌入质量控制:如果 analysis 里没提到某个错误修复,summary 就不会有它。OpenClaw 的 prompt 没有这个内置的完整性检查。
**断路器。** 连续 3 次 auto-compact 失败后停止。OpenClaw 无此机制,理论上可以无限 compaction → 失败 → 再 compaction。
**部分压缩。** Claude Code 支持只压缩前半段、保留后半段原文。独立 prompt 告诉模型:"this summary will be placed at the start of a continuing session; newer messages will follow"——模型清楚自己输出的是前缀,不会试图假装有全文视野。
把这些差异放一张表里:
| 设计维度 | Claude Code | OpenClaw |
|---|---|---|
| 摘要格式 | 9 段式,含错误修复+用户消息 | 5 段式 |
| 安全指令保留 | verbatim 逐字保留 | 仅标识符保留 |
| 用户反馈保护 | ALL user messages + 原文引用 | 无 |
| 输出前分析 | `` → `` 两步 | 直接输出 |
| Circuit breaker | 3 次连续失败停止 | 无 |
| Prompt 架构 | 指令在前,对话在后 | 对话在前,指令在后 |
| 失败信号 | 显式错误消息 | 静默 fallback |
| 部分压缩 | 独立 prompt,"前缀心态" | 不支持 |
| 文件引用追踪 | post-compaction 提醒重读 | post-compaction 读 AGENTS.md |
这里有一个值得停下来想的结构性观察:**Claude Code 的 9 段格式不是"更聪明",是"更不容易丢东西"**。每一个设计决策——安全指令 verbatim、用户消息逐条列出、下一步引用原文——都在防御同一个风险:compaction 是信息损失的天然节点,prompt 的唯一职责是把这个损失降到最低。OpenClaw 在 compaction 上投入的 prompt engineering 明显不足——不是在模型能力上不够,是在"什么东西绝对不能丢"这个问题上没有做足够的防御性设计。
这也回到了本文的核心论点:**Outer Loop 的质量不取决于你用了多强的模型,取决于你写的 prompt 在极端情况下会不会漏东西**。本事故中的 `SAFETY_MARGIN = 1.2`、`SUMMARIZATION_OVERHEAD_TOKENS = 4096`、`` 在指令之前的排列——这些都不是模型的问题,是 prompt 没考虑溢出场景。而 Claude Code 的 prompt 即使溢出,至少也会留下错误修复记录、用户消息、原文引用——排障所需的上下文不会全部蒸发。
## 总结:三个比踩坑更底层的判断
**1. Outer Loop 是新的护城河,不是模型。** 2025 年后,模型能力在快速趋同——Gemini 2.5 Pro 有 1M context、DeepSeek-V4 有 1M、GPT-5.5 有 200K。模型之间的差距在收敛,但 harness 质量(compaction safeguard、验证循环、降级路径、故障信号)的差距在拉大。本文的 compaction 事故就是证据——它不是"模型不够好"的问题,是 harness 少了一行配置。Claude Code 2026年1月 ARR 到 ~$2B,不是因为它用的 Claude 模型比别人聪明,是它的 outer loop 经过了百万级 Agent 的实战验证。
**2. 确定性优于概率性——在记忆、在路由、在故障处理。** OpenClaw 的 PPO 自适应权重和混合检索在论文上是对的,但当 compaction 静默失败时,用户不关心你的检索 recall 提高了多少。在系统边界上(compaction、fallback、错误处理),**确定性规则(rule-based)比概率规则(RL-based)更安全**。Claude Code 选"土"的分类法不是因为它不会做 PPO,是因为手工规则在"不丢东西"这件事上更可预期。
**3. 2025-2026 的行业趋势是 BYOK 和 harness 开源化。** 大量用户从 Cursor 迁移到 Claude Code CLI、Aider、OpenCode——不是因为这些工具功能更多,是因为 BYOK(Bring Your Own Key)让你**掌控 outer loop**。SaaS 工具的 compaction 策略、缓存策略、重试策略全是黑盒,出问题你只能等厂商修。而 OpenClaw 的 `openclaw.json` 里每一行配置你都能看到、能改、能版本控制。这也是 OpenClaw 真正的长期价值——不是记忆系统多先进,而是 **harness 完全透明**。
---
title: "生产环境 Agent 实践:为什么我们从 Celery 迁移到 Temporal"
description: "Agent 流水线不是普通的异步任务——它有状态、会卡住、需要重放排障、单条失败不能拖垮整批。Celery 每一条都踩坑后才明白 Temporal 不是\"另一个任务队列\",而是在解决不同层级的问题。"
url: "https://zhuoqidev.com/posts/why-temporal-not-celery/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-05-16"
updated: "2026-07-30"
tags: ["Agent 工程","Temporal","Celery","工作流引擎","生产实践","后端架构","Python"]
translation: "https://zhuoqidev.com/en/posts/why-temporal-not-celery/index.md"
---
# 生产环境 Agent 实践:为什么我们从 Celery 迁移到 Temporal
> **TL;DR** 2026 年 4 月,我们把 seo-project 的任务队列从 Celery 全面迁移到了 Temporal。删除的依赖只有一个(celery),新增的核心代码有 11 个文件(src/infrastructure/temporal/),容器从 api/worker/beat 变成了 api/temporal_worker_blue/green(蓝绿部署)。
这件事做完后,最常被问到的问题是:**为什么不用 Celery?已经能跑的东西换它干什么?**
这篇文章就是答案。它不来自文档对比,来自生产环境跑 Agent 流水线时逐条撞上的坑。
这张图先划清两个工具的**问题层级**。Celery 负责把任务交给 Worker;Temporal 额外保存事件历史,让多阶段流程能够从故障点恢复。迁移的核心不是换一个更快的队列,而是获得可重放的状态机。

---
## Celery 能做的事,为什么在 Agent 场景里开始不够用
先说清楚一个基本判断:**Celery 是好工具**。对于"发封邮件、生成一张缩略图、推送一条通知"这类标准异步任务,它完全够用,工业界跑了十几年。
但我们跑的负载和这不一样:
```
一个 Run 包含 N 条 longtail
每条 longtail 跑 A → B → C → D 四个 Agent 阶段
每个阶段调一次或多次 AI API
总耗时任意一条都在 60-180 秒区间
每一步的中间结果需要持久化
任何一步失败需要知道"停在哪、为什么、能不能只重试这一步"
```
这是**有状态的、长时的、多阶段的业务流程**。任务队列和业务流程引擎之间的分界线,就在这里。
---
## 第一条裂缝:任务状态只有"成功"和"失败"
Celery 的任务模型是:入队 → worker 拿到 → 执行 → 成功 or 失败。中间过程是一个黑盒。
当 Agent D 在生成 SEO 元数据时超时了,Celery 告诉你的是:"task failed, state=FAILURE"。但你想知道的是:
- Agent A(关键词分析)成功了吗?结果是什么?
- Agent B(大纲生成)跑完了吗?
- Agent C(正文生成)生成的 HTML 落库了没有?
- Agent D 是在调哪个模型时超时的?重试应该从 D 重跑,还是从 C 重跑?
这些问题,Celery 的答案都是"你自己记"。你可以往 Redis/DB 里写状态,但你本质上是在**任务队列上手工搭状态机**——而这恰恰是 Temporal workflow 的原生能力。
Temporal 的 workflow 代码就是你写的那段业务逻辑,每一步执行到哪、什么值、抛了什么异常,全部自动持久化。Worker 重启后 workflow 从断点继续,不需要任何额外代码。
```python
# Celery 版本:你要自己管每一步的状态
@app.task(bind=True, max_retries=3)
def run_pipeline(self, longtail_id):
try:
result_a = agent_a(longtail_id) # 如果这里成功
save_checkpoint("step_a_done", result_a)
result_b = agent_b(result_a) # 这里失败
# 重试时你从哪开始?自己判断 checkpoint
except Exception:
self.retry()
# Temporal 版本:workflow 代码就是状态机
@workflow.defn
class PipelineWorkflow:
@workflow.run
async def run(self, longtail_id: str) -> dict:
result_a = await workflow.execute_activity(agent_a, longtail_id)
result_b = await workflow.execute_activity(agent_b, result_a)
result_c = await workflow.execute_activity(agent_c, result_b)
result_d = await workflow.execute_activity(agent_d, result_c)
return result_d
```
注意 Temporal 版本里**没有 checkpoint 管理代码**。如果 `agent_c` 超时,重试直接从 `agent_c` 开始——`agent_a` 和 `agent_b` 的返回值已经持久化在 workflow history 里,不需要重新执行。
---
## 第二条裂缝:整批回滚——"一条超时,八条陪葬"
这是真正的生产事故。
我们的 Agent 批量流水线是:一个 task 里循环跑 N 条 longtail,每条跑 A→B→C→D。历史代码把数据库 commit 放在循环外面:
```python
# 反模式:整批一个事务
async with unit_of_work(autocommit=False) as uow:
for longtail in batch:
await run_pipeline(longtail)
await uow.commit() # 一次提交所有
```
问题来了:第 3 条的 Agent C 调 AI 超时 → SQLAlchemy session 进入 `invalid` 状态 → 第 4-8 条所有后续 `get/save/commit` 全部抛 `PendingRollbackError` → 整批 8 篇文章一个没落库。
修复方案是把事务边界收缩到单条 longtail:每轮自己 commit,失败那轮也 commit 失败状态。配合 Temporal 后,这个模式变得更自然——**每条 longtail 是一个独立的 workflow execution,Temporal 原生保证它的幂等性和可重放性**。
Celery 模型里,task 失败 = 整个 task 回滚 = 你要自己控制重试粒度。Temporal 模型里,一个 workflow 的失败不影响其他 workflow,每条 workflow 的 history 独立可查。运营看到的是 "1 failed / 7 succeeded" 而不是 "8 vanished"。
---
## 第三条裂缝:长时任务与 Worker 重启
Agent 流水线单条跑 60-180 秒是常态。Celery 的 worker 重启(部署、OOM、节点回收)会直接杀掉正在跑的任务,默认行为是 ack 后重新入队——从头开始跑。
这对短任务(<5 秒)问题不大。对一条已经跑了 120 秒、AI token 已经烧了十几万、就差最后一步的 Agent 流水线来说,**从头跑意味着前面的 token 成本全部白费**。
Temporal 的 durable execution 模型不同:即使 worker 重启,workflow 状态在 Temporal Server 中持久存在。新 worker 拿到 workflow history,从最后完成的步骤继续执行。这对于 AI 流水线的成本控制有直接意义。
---
## 第四条裂缝:排障时看不到"发生了什么"
生产上一条 Agent 流水线失败后,Celery 的排障路径是:
1. 找到 task ID
2. 去 Flower(如果装了)看 task 状态
3. grep 应用日志里的 trace_id
4. 手动拼出"A 生成了什么 → B 生成了什么 → C 卡在哪一步"
5. 如果中间有重试,信息更乱
Temporal 的排障路径是:
1. 打开 Temporal Web UI
2. 点进这个 workflow execution
3. **每一步的输入、输出、耗时、异常信息全部可视化在一条时间线上**
4. 你可以直接看到"Agent C 的 `generate_article` activity 在第 47 秒超时,重试了 2 次,每次的 prompt 和返回的 HTML 片段都可以展开查看"
这不是"Temporal 有个好看的 UI",而是 **调试效率的量级差异**。对于多 Agent 流水线这种"调用链长、状态多、中间产物重要"的场景,可视化的 workflow history 是刚需,不是锦上添花。
---
## 第五条裂缝:定时调度的可靠性
Celery Beat 是一个独立进程,用 `celery beat` 启动。它的常见问题:
- Beat 挂了 → 定时任务停了,没有 failover
- Beat 重启 → 可能重复触发同一时间的任务
- 错过了调度窗口 → 不会自动补发,需要手工处理
- `schedule` 变更 → 需要重启 Beat
我们在 Agent 项目中有一个需求:每天为每个站点生成一批文章。如果某天调度器宕机或错过窗口,需要**自动检测缺失日期并补发**。
Celery 做这件事的方案是"写个脚本对比日期差,手动投递"。Temporal Schedule 原生支持:
- **Overlap Policy**: 上一个 workflow 还在跑时下一次触发怎么办
- **Catch-up Window**: 错过了多久以内的窗口自动补发
- **Pause/Resume**: 不需要改 cron 表达式,直接暂停调度
对于内容生产这类"错过一天就得补"的业务,Schedule 的补发语义是硬需求,不是 nice-to-have。
---
## 选型对照表
| 维度 | Celery | Temporal | Agent 场景谁赢 |
|------|--------|----------|--------------|
| **任务模型** | 无状态 task | 有状态 workflow | Temporal(Agent 流水线天然有状态) |
| **中间状态持久化** | 自己实现(Redis/DB) | 自动持久化 workflow history | Temporal(零额外代码) |
| **重试语义** | task 级别重试,从头跑 | activity 级别重试,断点续跑 | Temporal(长时 AI 调用 token 成本差异大) |
| **失败可视性** | 日志 grep + Flower | Web UI 时间线 + 每步 I/O | Temporal(排障效率量级差异) |
| **定时调度** | Beat 进程,无 failover | Schedule,内置 catch-up | Temporal(内容补发是硬需求) |
| **批量任务失败半径** | 取决于你怎么写 commit | 每条 workflow 天然隔离 | Temporal(独立幂等单元) |
| **Worker 重启影响** | 正在跑的任务丢失,从头重试 | workflow 从断点恢复 | Temporal(120 秒的 AI 调用不白跑) |
| **运维复杂度** | Worker + Beat + Flower + Broker (Redis/RabbitMQ) | Worker + Server (自带 Web UI) | 相当(Temporal Server 需要独立部署,但省了 Flower 和 Beat 的运维) |
| **学习曲线** | 低(Python 生态原生) | 中(workflow/activity 确定性约束需要理解) | Celery 胜(上手更快) |
| **适用场景** | 短时、无状态、失败成本低 | 长时、有状态、中间产物重要、需要可审计 | 看任务性质 |
---
## 什么时候你**不需要** Temporal
不是说 Celery 就该被替换。如果你面对的是下面这些场景,Celery 仍然是最优解:
- 任务耗时 <10 秒,失败从头跑成本可以忽略
- 不需要看到中间步骤的输入输出(比如发邮件)
- 不需要按步骤重试(整个任务成功或失败即可)
- 团队对 Temporal 的确定性约束不熟悉,短期无学习预算
- 现有 Celery 系统运行稳定,迁移收益 < 迁移成本
我们的决定不是"Temporal 比 Celery 好",而是 **"Agent 流水线的特征恰好卡在 Celery 的薄弱面和 Temporal 的核心能力之间"**。
---
## 迁移后落地的架构模式
迁移到 Temporal 后,几个关键的架构决策也随之定型:
**1. 一条 longtail = 一个 workflow = 一个事务边界**
批量任务变成"外层投递 N 个 workflow,内层每个 workflow 独立管理自己的事务"。这是 `concepts/backend` 中"[数据库] 批量任务:per-iteration 事务边界"和 Temporal 的天然结合。
**2. Unit of Work 的两种语义**
FastAPI 路由用 `autocommit=True`(请求级短事务),Temporal worker 用 `autocommit=False`(workflow 级长事务,自己接管 commit/rollback 时机)。同一套 UoW 代码,不同的生命周期语义。
**3. 蓝绿部署**
容器从单实例 `worker` 变成 `temporal_worker_blue` + `temporal_worker_green`。发版时先切流量到新颜色,旧颜色等正在跑的 workflow 全部完成后再回收。这是 Temporal Task Queue 的 routing 能力带来的零停机部署。
**4. 幂等重放是免费的**
每条 workflow execution 的 history 存在 Temporal Server 里。出问题时,你可以用同一份 history 在本地重放整个 workflow——输入相同、路径相同、数据库操作被 mock 掉——来定位是哪个 activity 的哪次调用的哪个返回值导致了错误。这在 Celery 时代是好几个小时的体力活。
---
## 总结
把 Temporal 当成"更强大的 Celery"是对两者的误解。它们解决的是**不同层级**的问题:
- Celery 解决的是"把一段代码放到另一台机器上跑"
- Temporal 解决的是"保证一段多步骤业务流程可靠地跑到终点,并让过程中的每一步都可审计"
Agent 流水线恰好是一个多步骤、长耗时、中间产物重要、失败代价高的业务流程。用任务队列去模拟业务流程引擎,不是在用错工具——是用了正确工具的上一个版本,然后手工实现了正确工具已经内置的能力。
---
*这篇文章基于 [seo-project](https://github.com/zhuoqidev/ai-seo-pipeline) 的生产实践。迁移记录见 LLM Wiki → `log.md` (2026-04-10) 及 `concepts/backend.md`。*
---
title: "ChatGPT Business 优惠码究竟从哪来?一次溯源与更正"
description: "重新核对 OpenAI、合作伙伴、Stripe 与 linux.do 的一手资料:优惠并未被统一取消,公开链接、资格审核后邮件发码等机制仍在并存。"
url: "https://zhuoqidev.com/posts/chatgpt-business-promo-investigation/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-05-14"
updated: "2026-07-30"
tags: ["ChatGPT","OpenAI","Stripe","优惠码","信息溯源","OSINT"]
translation: "https://zhuoqidev.com/en/posts/chatgpt-business-promo-investigation/index.md"
---
# ChatGPT Business 优惠码究竟从哪来?一次溯源与更正
> **TL;DR** 2026 年 5 月,一批带有公司名称的 ChatGPT Business 优惠码在 linux.do 等社区传播。初版文章试图回答“这些码从哪里来”,但把若干合理猜测写成了事实。
> [!WARNING]
> **更正说明(2026-07-29)**
>
> 本文初版把“公司名出现在优惠码中”“结账页由 Stripe 承载”和社区里的传播记录拼成了一条过于确定的故事:代码由 Stripe 合作伙伴网络发放,再由合作企业员工泄露。现有证据不足以支持这条因果链。
>
> 修订后的结论是:**这些活动更可靠的上游是 OpenAI 的合作伙伴/渠道活动;Stripe 是支付与促销码基础设施,而不是已被证实的活动发起方。公开链接、表单审核后邮件发码、合作伙伴自费返利等不同机制同时存在,也没有证据证明 OpenAI 已在全球统一取消公司级 `/p/...` 链接。**
这次重查只保留能够由一手页面或可复核时间戳支持的部分,并明确区分:
- 官方事实;
- 合作伙伴自己的声明;
- 社区用户的观察;
- 无法验证的推断。
下面这张图是全文的**证据边界**:它把官方事实、基础设施能力、渠道声明和社区观察分层,重点不是证明某个猜想,而是阻止我们从“Stripe 能执行优惠”跳到“Stripe 策划并发放优惠”。

## 先给结论
1. **合作伙伴渠道是真实存在的。** OpenAI 已公开建立 [OpenAI Partner Network](https://openai.com/business/partners/),允许合作伙伴与 OpenAI 联合销售、构建和交付解决方案。CodeStone 也在 4 月宣布加入 OpenAI SMB Channel Partner Programme;First Focus 在 3 月宣布签署 OpenAI Services Partner Agreement。
2. **“Stripe 是源头”没有证据。** Stripe 确实支持订阅促销码、指定客户、首次购买、到期日和最大兑换次数等能力,但这只能说明结账系统如何执行优惠,不能说明谁策划、出资或发放了活动。
3. **“公开出现=员工泄露”不成立。** OpenAI 自己有公开的 [Dropbox × ChatGPT Business 优惠页](https://openai.com/business/partners/dropbox/)。Think Technologies 也在公司 LinkedIn 内容中公开传播 `chatgpt.com/p/THINK26`。至少一部分链接显然是有意公开营销,而非泄露。
4. **发放方式正在分化,而不是被统一替换。** AI Build 当前采用“填写工作邮箱和团队规模—审核资格—邮件发码”的流程;与此同时,公开 `/p/...` 链接仍然存在。
5. **linux.do 证明了不同阶段发生过不同变化。** 4 月底的首月试用停止、6 月 4 日的控制台“Stripe 长链接”生成方式失效,与合作伙伴促销链接是否存在,是三个不同问题。
所以,对读者提醒“英国现在改为审核后邮件发码”,我认为**就 AI Build 这个渠道而言是对的**;但把它扩展成“OpenAI 已取消所有公司级共享链接,全部改为个人码”,目前并不成立。AI Build 的落地页本身也没有写“每个码仅绑定一人”或“OpenAI 已全局取消公开链接”。
## 旧文错在哪里
| 初版说法 | 复核结果 | 更正 |
|---|---|---|
| “真正源头是 Stripe 企业合作伙伴网络” | 无一手证据 | 更可靠的表述是“OpenAI 合作伙伴/渠道活动”;Stripe 只被证实为支付技术层 |
| “合作企业员工把内部码泄露出来” | 无法验证,且有公开营销反例 | 只能说公开社区进行了二次传播,不能推断首次发布者身份与授权状态 |
| “每个码都按固定配额关门,与日期无关” | Stripe 同时支持到期日、兑换上限、客户与首次购买限制 | 单凭“无法使用”无法判断具体失效原因 |
| “每个码都代表 OpenAI 的合作补贴” | 被 First Focus 当前条款直接反驳 | 有的优惠可能由合作伙伴自行出资,必须逐活动判断 |
| “英国 IP + 对应码即可” | 过度简化且会误导跨区尝试 | 资格、账单国家、付款方式、税费和账号状态均可能参与校验 |
| “蹲到就立刻上车” | 不负责任 | 应先确认受众、续费价、席位数和结账页最终金额 |
## 能把“源头”追到哪一层
### 1. 可以确认:OpenAI 有正式合作伙伴渠道
[OpenAI Partner Network](https://openai.com/business/partners/) 的公开说明包含联合销售、技术支持、交付和合作伙伴门户。这足以证明 OpenAI 存在正式 B2B 渠道体系。
带公司名的代码也与部分公司的公开身份相吻合:
- [CodeStone 的公告](https://www.codestone.com/news/codestone-is-now-an-official-openai-partner/)称其加入了 OpenAI SMB Channel Partner Programme,并把关系描述为正式的商业与技术合作;
- [First Focus 的公告](https://www.firstfocus.com.au/insights/first-focus-becomes-an-openai-services-partner/)称其已签署 OpenAI Services Partner Agreement,面向澳大利亚与新西兰帮助企业采用 ChatGPT Business;
- [AI Build 当前落地页](https://www.ai-build.ltd/chatgpt-business/get-offer)自称英国 OpenAI SMB Channel Partner,并说明由其团队审核英国、爱尔兰企业的资格。
这支持“代码与合作伙伴活动有关”,但仍然**不能单靠代码字符串**证明某次公开传播是获授权的,更不能证明是某位员工泄露。
### 2. 可以确认:Stripe 能执行优惠,但不能据此认定它发起优惠
[Stripe 官方文档](https://docs.stripe.com/billing/subscriptions/coupons)说明,商家可以创建面向客户的 promotion code,并设置:
- 指定客户;
- 首次购买资格;
- 到期时间;
- 最大兑换次数;
- 最低订单金额。
[Stripe Payment Links 文档](https://docs.stripe.com/payment-links/promotions)还说明,链接可以预填促销码。
这能解释为什么社区用户会看到 Stripe 托管的结账页,也能解释同一个代码为何可能因账号、时间或次数而失效。但 Stripe 提供工具,不等于 Stripe 决定了 OpenAI 的渠道名单、优惠预算或传播方式。旧文把“基础设施”误写成“商业源头”,这是最关键的错误。
### 3. 可以确认:公开链接不必然是泄露
OpenAI 官方的 [Dropbox 合作伙伴页面](https://openai.com/business/partners/dropbox/)明确公开了一个限时 ChatGPT Business 活动:符合资格的 Dropbox 和 ChatGPT 用户可通过促销链接兑换两个 Business 席位及 50 美元 credits,免费使用 30 天。
Think Technologies 的[公司 LinkedIn 帖文](https://www.linkedin.com/posts/think-technologies_chatgpt-activity-7464703850562785281-TvQV)也公开宣传 ChatGPT Business “买一送一”,并直接给出 `chatgpt.com/p/THINK26`。
截至 2026 年 7 月 29 日,直接访问:
```text
https://chatgpt.com/p/THINK26
```
仍会跳转到:
```text
https://chatgpt.com/?promoCode=THINK26
```
这说明 `/p/{slug}` 至少仍是有效的跳转格式。它**不保证登录后的账号一定符合资格,也不保证活动尚有名额**,但足以反驳“这类链接格式已被 OpenAI 全局撤销”的说法。
## linux.do 时间线:到底取消了什么
社区记录很有价值,但它记录的是用户当时“看见了什么”,不是 OpenAI 的正式政策。把几条时间线并排后,变化会清楚很多:
| 日期 | 可复核记录 | 能说明什么 |
|---|---|---|
| 4 月 29 日 | linux.do 用户报告 Team/Business 首月免费试用不再出现 | 一类旧试用活动停止;不等于后来合作伙伴促销也停止 |
| 5 月 8–10 日 | 大量品牌化 48 个月代码传播;5 月 9 日帖子保留了 `THINKTECHNOLOGIESUS` 的引用 | 社区在这一阶段集中扩散合作伙伴代码 |
| 6 月 4 日 | 用户报告通过浏览器 Console 生成“Stripe 长链接”的方法失效 | 一种非标准结账链接生成方式被改变或限制 |
| 6 月 11–14 日 | 仍有新优惠讨论,用户对“长链接”是否还能生成意见不一 | 代码、优惠资格和长链接工具并不是同一个状态 |
| 6 月 30 日–7 月 1 日 | `penguinaius` 公开链接被分享,多名用户报告兑换结果 | 6 月 4 日之后,公开 `?promoCode=` 形式仍在传播并被报告可用 |
| 7 月 | Think 公开宣传 `/p/THINK26`;AI Build 则改用审核后邮件发码 | 公开链接与定向发码并存 |
相关社区原始记录:
- [4 月 29 日:首月试用停止](https://linux.do/t/topic/2079203)
- [5 月 9 日:48 个月优惠讨论与旧帖引用](https://linux.do/t/topic/2141860?page=2)
- [6 月 4 日:Console 生成 Stripe 长链接失效](https://linux.do/t/topic/2302827)
- [6 月 11 日:新代码与长链接状态讨论](https://linux.do/t/topic/2380243)
- [6 月 30 日:公开 `penguinaius` 链接及用户回报](https://linux.do/t/topic/2502020)
因此,“取消了”必须带宾语:**可以说某一批旧码失效了、某个合作伙伴改成审核后发码了,或某种长链接生成办法失效了;不能把它们合并成 OpenAI 的一项全球政策。**
## 当前发放机制并不只有一种
```mermaid
flowchart LR
A["OpenAI / 合作伙伴活动条款"] --> B["公开促销页或 /p/ 链接"]
A --> C["资格表单与邮件发码"]
A --> D["合作伙伴自费返利"]
B --> E["ChatGPT 账号与资格校验"]
C --> E
E --> F["支付与订阅系统"]
D --> G["合作伙伴服务 credits / 账单调整"]
H["社区转载"] --> B
```
目前至少能看到四种模式:
1. **OpenAI 官方公开活动。** Dropbox 页面公开提供兑换入口,并在 FAQ 中列明新客户或取消至少 90 天的旧客户等资格。
2. **合作伙伴公开链接。** Think Technologies 公开传播 `/p/THINK26`。
3. **合作伙伴先审核再邮件发码。** AI Build 要求提交工作邮箱和团队规模,并称通常在一个工作日内发码。
4. **合作伙伴自费返利。** [First Focus 当前活动](https://www.firstfocus.com.au/services/core/chatgpt/)明确写明:客户直接向 OpenAI 购买许可证,前六个月最高 50% 的返利由 First Focus 出资,以服务 credits 或账单调整交付,并非 OpenAI 折扣。
第四种模式尤其重要:它证明“优惠”甚至不一定是 ChatGPT 结账页里的 promotion code,更不能一概解释为 OpenAI 或 Stripe 在补贴。
## 价格、期限和“失效原因”应怎样写
旧文保存的 £11、AU$25、US$20 与 48 个月,是特定时间、地区和账号下的社区观察,不应继续作为可购买的当前报价。
OpenAI 当前[官方计费说明](https://help.openai.com/en/articles/8792536-managing-billing-and-seats-in-chatgpt-business)给出的美国自助 Business 基准价是:
- 月付:每用户每月 25 美元;
- 年付:每用户每月 20 美元,按年计费;
- 标准 ChatGPT 席位至少购买 2 个;
- 实际价格会随地区和本地货币变化。
一个优惠码显示“无法使用”,可能来自多种原因:
- 活动已到期或被停用;
- 达到最大兑换次数;
- 仅限指定客户或首次购买;
- 账号、地区或账单资料不符合资格;
- 活动只适用于新建 Business workspace;
- 结账流程或产品配置发生变化。
除非拿到该活动的后台配置或正式条款,否则无法仅凭报错断言“肯定是配额用完”。结账前应以发行方最新条款和 ChatGPT 最终结账页为准,同时核对续费价格、税费、席位数和取消条件。
## 修订后的传播判断
linux.do 确实是 2026 年 5 月中文圈的重要集散地:它保存了代码、截图、时间戳和用户回报,也让信息迅速扩散。但现有证据最多支持:
> 合作伙伴相关活动通过公开营销、定向发放或不明渠道进入社区,再被论坛和博客放大。
它不支持:
> Stripe 把代码发给企业,企业员工泄露,linux.do 再转发。
前一句是证据范围内的描述;后一句仍是一个未经证实的故事。
## 给以后调查同类活动的检查清单
1. 先找 OpenAI 或活动发行方的原始页面,不把论坛截图当正式条款。
2. 区分 `/p/...` 短链接、`?promoCode=...`、Stripe Checkout Session 和合作伙伴返利。
3. 记录页面更新时间、适用国家、账号资格、席位数、优惠期和续费价。
4. 把“某个码失效”“某种结账技巧失效”和“整个渠道被取消”分开。
5. 不通过跨区伪装、控制台脚本或来路不明的支付工具绕过资格与风控。
6. 对缺少一手证据的“首发”“泄露者”“资金来源”明确标注为未知。
## 最终结论
原文抓对了一个大方向:这些品牌化代码与 B2B 合作伙伴渠道有关,linux.do 则是中文社区的高效传播节点。
但更准确的表述应当是:
**ChatGPT Business 优惠来自多种 OpenAI 或合作伙伴活动;Stripe 提供了其中一部分支付与促销码能力,却不是已被证实的活动源头。部分合作伙伴转向资格审核后邮件发码,但公开优惠页和 `/p/...` 链接截至 2026 年 7 月仍然存在。每个活动的受众、资金来源、失效条件和续费规则都必须逐项核对。**
*本文复核截至 2026-07-29。OpenAI 与合作伙伴可以随时改变活动,最终以发行方最新条款和结账页为准。*
---
title: "什么时候用 RAG,什么时候用 LLM Wiki,什么时候用纯文本记忆——一个 Agent 记忆选型框架"
description: "RAG、结构化知识库、纯文本上下文记忆——三种 Agent 长期记忆方案的成本、延迟、精度和可维护性对比,附决策树。"
url: "https://zhuoqidev.com/posts/memory-choice-framework/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-05-11"
updated: "2026-07-30"
tags: ["AI Agent","记忆系统","RAG","上下文工程","Agent 架构"]
translation: "https://zhuoqidev.com/en/posts/memory-choice-framework/index.md"
---
# 什么时候用 RAG,什么时候用 LLM Wiki,什么时候用纯文本记忆——一个 Agent 记忆选型框架
> **TL;DR** 做 Agent 系统的人迟早会撞上这个选择题:用户的数据往哪放,下次对话怎么记住?
目前工业界有三条主流路线——RAG(向量检索)、LLM Wiki(结构化知识注入)、纯文本上下文记忆(CLAUDE.md / Cursor Rules 模式)。三条路各有拥趸,但**选错的代价很大**:RAG 做轻了是噪音生成器,纯文本做重了是 token 焚化炉。
这篇给出一个可以直接用的决策框架。
先看三条路线的系统边界。它们的核心差异不是数据库品牌,而是**检索发生在什么时候、多少内容进入 Context、写入和维护由谁负责**。

---
## 三种方案一句话定义
| 方案 | 核心机制 | 代表产品/模式 |
|------|---------|-------------|
| **RAG** | 向量检索 → top-k 片段 → 拼入 prompt | Mem0, Zep, LangChain RAG, Cursor Codebase Index |
| **LLM Wiki** | 结构化文档 → 全量或按需注入 system prompt | Claude Projects, GPTs Knowledge, Notion AI |
| **纯文本上下文** | Markdown/文本文件 → 直接拼入 system prompt | CLAUDE.md, Cursor Rules, AGENTS.md, Devin Knowledge |
关键区别不在于"存哪里",而在于**检索方式**和**注入时机**。
---
## 决策矩阵
| 维度 | RAG | LLM Wiki | 纯文本上下文 |
|------|-----|----------|------------|
| **数据量** | 大(>100 篇文档) | 中(10–100 篇) | 小(<10 个文件,<200 行) |
| **更新频率** | 高频,实时或近实时 | 中频,周/天级 | 低频,项目级约定 |
| **检索精度要求** | 需要语义匹配("找和这个问题最相关的段落") | 需要结构化浏览("看第三章第四节") | 不需要检索,全量加载 |
| **延迟** | +50–500ms(embedding + 检索 + 重排) | 0(预加载) 或 +100ms(按需 fetch) | 0(全量在 prompt 里) |
| **token 成本** | 低(只注入相关片段) | 中(按章节注入) | 高(全量每次塞入) |
| **可维护性** | 低(chunk 策略、embedding 模型、检索参数都要调) | 中(文档结构需维护) | 高(就是 Markdown,改一下 commit 就行) |
| **可解释性** | 低("为什么检索到这段?"难以回答) | 高("因为你看了第三章") | 最高(全部可见) |
| **幻觉风险** | 高(检索噪音 → 错误上下文 → 幻觉) | 低 | 低 |
| **适合场景** | 客服知识库、代码库搜索、大规模文档 QA | 项目文档、产品手册、合规知识库 | 项目约定、编码规范、个人偏好 |
---
## 什么时候选 RAG
RAG 不是银弹。**只有当数据量超过 prompt 容量上限时才值得上 RAG。** 如果你只有 20 篇文档,直接全塞进去都比 RAG 好——因为检索噪音引入的错误比省下的 token 钱贵得多。
**RAG 的正确使用场景:**
- 文档量 >100 篇,且用户每次只关心其中 1-3 篇
- 需要语义匹配而非关键词匹配("报错了"要能匹配到 "error log")
- 数据实时更新(比如接入了实时数据库)
- 你可以接受偶尔检索到不相关的内容
**RAG 的常见翻车姿势:**
- 把 10 篇文档建了个向量库——杀鸡用牛刀,检索噪音 > 信息增益
- Chunk size 乱设——太小丢上下文,太大检索不准
- 不看重排(rerank)——top-k 里混入无关片段,模型被带偏
- Embedding 模型和生成模型不匹配——用 OpenAI embedding 但用 Claude 生成,语义空间不一致
> 有一条经验法则:**先试着把所有相关内容直接塞进 prompt。如果塞不下,再上 RAG。** 这个顺序不能反过来——RAG 是不得已的选择,不是默认选择。
---
## 什么时候选 LLM Wiki
LLM Wiki 指的是结构化的、可供模型按需检索或全量加载的知识库。它不像 RAG 那样靠向量相似度,也不像纯文本那样全量堆进去——**它是"有目录的文档"**。
**适合 LLM Wiki 的场景:**
- 知识有清晰的层级结构(产品手册、API 文档、合规条例)
- 用户需要"翻到某一节"而不是"搜一个片段"
- 需要人工审核和版本管理(合规场景尤其重要)
Claude Projects 里的 Project Knowledge 就是典型的 LLM Wiki——你把文档传进去,模型在需要时引用。GPTs 的 Knowledge 功能同理。
**LLM Wiki vs RAG 的本质区别:**
RAG 是"我猜你想看这几段",LLM Wiki 是"这是目录,你要看哪一章"。前者靠语义相似度,后者靠结构导航。前者可能猜错,后者不会——但后者要求用户或 Agent 知道该看哪一章。
---
## 什么时候选纯文本上下文记忆
这就是 CLAUDE.md / Cursor Rules / AGENTS.md 模式。一个 Markdown 文件,每次对话全量注入 system prompt。**听起来很笨,但在正确场景下是最优解。**
**为什么这个"笨办法"反而是最好的:**
1. **可审计**——每次改了啥,`git diff` 一目了然
2. **可版本管理**——记忆有了 git history,你可以回到三天前的状态
3. **零延迟**——不需要 embedding,不需要检索,直接拼进 prompt
4. **零噪音**——你写的每个字都在 prompt 里,不存在"检索到错误段落"
5. **不容易被 prompt injection**——内容是人工写的,不是 AI 自动生成的
Cursor 1.2 给 Memories 加 user approval、Devin Knowledge 默认走 suggestion——都是被 prompt injection 教训之后的设计共识。纯文本记忆不需要"信任 AI 自己记的东西",因为每一条都是人写的。
**适合纯文本记忆的场景:**
- 项目级约定("我们用的 Java 17,Spring Boot 3.x")
- 编码规范("不要用 Lombok,用 record")
- 个人偏好("回答用中文,代码注释用英文")
- Agent 行为约束("调用工具前先确认")
**不适合的场景:**
- 数据量超过约 200 行——占用太多 context window,挤压真正有用的 token
- 需要动态更新的知识——每次改都要手动编辑文件
- 多项目共享的知识——容易拷贝粘贴导致不一致
---
## 实战决策树
```mermaid
flowchart LR
%% layout: tree
Q(["你的知识数据有多大?"])
Q --> A["<10 个 Markdown 文件
总计 <200 行"] --> A1["纯文本上下文
CLAUDE.md / Cursor Rules
优势:零延迟、可 git、零噪音"]
Q --> B["10–100 篇文档
有清晰结构"] --> B1["LLM Wiki
Claude Projects / GPTs Knowledge
优势:结构化导航、按需加载、人工可审核"]
Q --> C[">100 篇文档
或需要语义搜索"] --> C1["RAG
Mem0 / Zep / 自建向量库
前提:你已经验证过“全量塞 prompt”真的塞不下
提醒:RAG 的维护成本是另外两种方案的 10 倍"]
classDef q fill:#fff3bf,stroke:#f08c00
classDef pick fill:#d3f9d8,stroke:#2f9e44
class Q q
class A1,B1,C1 pick
```
---
## 一个常见的错误组合
很多团队一上来就同时用了三种:
```
CLAUDE.md(纯文本)
+ 向量库 RAG(一堆文档)
+ Wiki 知识库(产品文档)
```
三套系统同时往 prompt 里塞东西。结果:
- Token 成本爆炸
- 信息互相矛盾(纯文本说用 Go,Wiki 里的旧文档说用 Python)
- 排查问题变成"到底是哪段上下文导致的?"
**正确做法:选一种作为主记忆层,另外两种仅在必要时补充。** 大多数个人开发者和 10 人以下团队,纯文本 + 一个 LLM Wiki 就够了。RAG 是规模化之后的事。
---
## 和 LLM 记忆调研的关系
这篇文章是 [大模型为什么没有记忆](https://zhuoqidev.com/posts/llm-memory-research/) 调研的工程落地篇。调研里讲的四层栈(裸 LLM → 架构内记忆 → 长上下文 → Agent 记忆层),本文聚焦在 **L4 Agent 记忆层的选型**。
调研中 Karpathy 的类比这里再引用一次,因为太好用了:
> **权重 = ROM**(训练时烧入,静态)
> **Context Window = RAM**(推理时直接寻址)
> **KV Cache = Working Memory**(test-time 形成)
> **外部存储 = Disk**(持久但需要 retrieve)
你选的方案决定了 Agent 的"硬盘"长什么样——是高速 SSD(纯文本)、按需挂载的文件系统(LLM Wiki)、还是带搜索引擎的数据库(RAG)。
---
## 总结
| 如果你... | 选这个 |
|-----------|--------|
| 数据少、变更低频、需要可审计 | **纯文本上下文** |
| 数据有清晰结构、需要按章节引用 | **LLM Wiki** |
| 数据多、需要语义搜索、能接受检索噪音 | **RAG** |
没有银弹。但有一个铁律:**能不用 RAG 就不用 RAG——等你真的需要它的时候,你会知道的。**
---
title: "在 Hugo 文章里内嵌 CSS 动画 Demo"
description: "用自定义 shortcode 在 Hugo 博客里直接运行 CSS 动画,无需 CodePen 账号,文章即 demo。"
url: "https://zhuoqidev.com/posts/css-animation-demo/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-05-04"
updated: "2026-07-30"
tags: ["Hugo","CSS","动画","Shortcode"]
translation: "https://zhuoqidev.com/en/posts/css-animation-demo/index.md"
---
# 在 Hugo 文章里内嵌 CSS 动画 Demo
> **TL;DR** Hugo 用 shortcode 可以很优雅地内嵌代码演示。这里展示三种方式:
先用一张图确定边界:原生 shortcode、CodePen iframe 和 CodeSandbox 分别适合不同复杂度,也把代码放在不同的执行与隔离位置。

## 1. 内联 CSS demo(无需外部服务)
直接在文章里跑一个旋转加载动画:
```html demo height=200 caption="纯 CSS 旋转加载器"
```
一个渐变色文字动画:
```html demo height=160 caption="CSS 渐变文字"
ZhuoQi Dev
```
## 2. 嵌入 CodePen
如果已有 CodePen 作品,用一行 shortcode 嵌入:
```
{{< codepen id="你的PenID" height="400" tab="result" >}}
```
## 3. 嵌入 CodeSandbox
React / Vue 组件用 CodeSandbox:
```
{{< codesandbox id="你的沙盒ID" height="450" view="preview" >}}
```
---
这三个 shortcode 覆盖了大多数代码展示场景,写博客基本不需要其他工具了。
---
title: "用 Hugo 和双栈 CDN 搭建个人网站"
description: "记录如何用 Hugo + Blowfish,通过阿里云 OSS/CDN(国内)+ Cloudflare Pages(国际)实现双栈加速,备案、DNS 分线路解析一次走通。"
url: "https://zhuoqidev.com/posts/hugo-dual-cdn-blog/"
language: "zh-CN"
author: "刘卓琪"
date: "2026-05-04"
updated: "2026-08-03"
tags: ["Hugo","阿里云","Cloudflare","CDN","ICP备案"]
translation: "https://zhuoqidev.com/en/posts/hugo-dual-cdn-blog/index.md"
---
# 用 Hugo 和双栈 CDN 搭建个人网站
> **TL;DR** 同一份 Hugo 静态产物,同时发布到阿里云 OSS/CDN 与 Cloudflare Pages,再由 DNS 分线路把国内外访问导向不同边缘网络。
## 为什么选 Hugo
做个人博客选框架,我的第一标准是**维护成本低**——不想三个月后因为 npm 依赖地狱放弃写作。
Hugo 是单二进制文件,无需 Node.js,构建几千篇文章只需 1-2 秒,Blowfish 主题开箱就有暗色模式、全文搜索、多语言、RSS、Open Graph、阅读时间估算。日常写作只需碰 Markdown。
## 整体架构
这张图同时画出两条不同的路径:从 Git Push 向下看是**发布路径**,国内外访客经 GeoDNS 进入各自 CDN 则是**请求路径**。两边必须来自同一次构建,才能避免内容版本漂移。

这套方案全年花费约 ¥212:
- 域名 `zhuoqidev.com`:¥85/年(已买 3 年)
- 函数计算资源包(ICP 备案载体):¥101/年(¥126 年包叠加 8 折优惠)
- 阿里云 CDN 100GB 流量包:¥20/年
- OSS 存储:~¥6/年
- Cloudflare Pages:¥0
## ICP 备案那些事
国内域名备案需要"备案载体"(服务器 IP)。不买服务器的话,阿里云**函数计算资源包**(¥101/年)可以作为备案载体,拿到备案服务码。
备案周期:阿里云初审 1-5 个工作日 + 管局审核 10-20 个工作日,整体约 3-6 周。等备案期间正好把网站搭完。
## DNS 分线路解析
阿里云云解析免费版支持"境内/境外"两条线路:
- 境内 → 阿里云 CDN CNAME
- 境外(默认)→ Cloudflare Pages CNAME
这样国内用户走备案过的阿里云节点,海外用户走 Cloudflare 免费全球 CDN,一套域名两套加速。
## 部署流程
推送到 GitHub → Actions 自动 `hugo build` → 并行上传到 OSS 和 Cloudflare Pages。
整个流程大约 2-3 分钟,文章发布基本无感。
## 后来补上的细节:能看清 4K 架构图的图片预览器
技术文章里的架构图经常有很多小字。Blowfish 自带的 `medium-zoom` 只会把图片放大到适合屏幕,再点一次便关闭;原图即使是 4K,读者也不能继续按百分比放大、拖动查看局部。因此我后来把它替换成了一个站点级图片查看器。
实现没有引入新的前端依赖:在 `hugo.yaml` 中关闭主题自带缩放,将 `assets/js/image-viewer.js` 和对应 CSS 加进 Hugo Pipes 的资源管线。构建时,它们会和其他资源一起压缩、生成内容指纹,再作为同一份静态产物发布到两套 CDN。脚本只接管 `.article-content img:not(.nozoom)`,所以正文配图可以预览,头像、图标和卡片封面不会被误触发。
查看器打开后先根据视口与图片原始尺寸计算“适应屏幕”的比例;滚轮或按钮缩放时,以鼠标所在位置为锚点重新计算位移,让读者指着哪里就放大哪里。放大后的图片可以拖动,位移会限制在有效边界内。触摸端用 Pointer Events 同时跟踪两个触点,实现双指缩放;键盘则支持 `+`、`-`、方向键、`0`(适应屏幕)、`1`(原始尺寸)和 `Esc`。单击图片或周围空白处也能退出,拖动超过阈值则不会误关。界面显示实时百分比,并根据文章语言切换中英文标签。
你可以在[这篇 Codex Agent 设计解读](https://zhuoqidev.com/posts/codex-agent-design/)中点击任意 4K 配图实际体验。这个小功能也说明了静态站点的一个好处:交互逻辑仍然只是可缓存的 JS 和 CSS,发布拓扑完全不需要改变。
---
后续文章会深入 Agent 架构设计、记忆层选型、多 Agent 编排等话题。