RussellLuo

让思想在文字间徜徉

在上一篇《LLM 如何逐个生成 Token?》中,How are 经过分词后得到 Token ID 序列 [2437, 389]。那么,Tokenizer(分词器)是如何完成这一步的?

本文先介绍 Token、Token ID 和词表,再说明文本与 Token ID 如何相互转换,最后用 llama.cpp 查看真实的 Token、预切分规则、词表和合并规则。

Token、Token ID 与词表

GPT-2 模型本身不直接处理字符串。文本进入模型计算之前,Tokenizer 会依据固定规则完成下面的转换:

How are 转换为 Token ID 2437 和 389

这里的 " are" 包含一个前导空格。这个空格不会被丢弃,而是输入文本的一部分。

Token 不等于单词

Token 是模型处理文本时使用的基本单位。在 GPT-2 的词表中,它可能是完整单词,也可能只是单词的一部分:

1
2
3
4
5
6
7
unbelievable!
↓
"un" + "bel" + "iev" + "able" + "!"

"你好"
↓
[19526, 254, 25001, 121]

它还可能包含前导空格、标点或换行。你好 只有两个 Unicode 字符,在这个 GPT-2 Tokenizer 中却对应四个 Token ID。对于中文等非 ASCII 文本,一个 Token 甚至可能只对应某个 UTF-8 字节序列的一部分,单独显示时不一定是完整字符。

因此,Token 不能简单等同于“单词”或“字符”,而是 Tokenizer 根据固定规则得到的文本片段。

词表保存 Token 与 ID 的对应关系

词表(Vocabulary)可以看作一张固定的映射表,其中每个 Token 都有一个整数编号,也就是 Token ID。

按照文本中的先后顺序,How are you doing? 对应下面的 Token ID 序列:

1
2
"How are you doing?"
→ [2437, 389, 345, 1804, 30]

但在词表中,这些 Token 按照 Token ID 排列在不同位置:

Token ID Token
0 !
… …
30 ?
… …
345 Ġyou
389 Ġare
… …
1804 Ġdoing
… …
2437 How
… …
50256 <|endoftext|>

省略号表示未展示的词表项;Token 在句子中的顺序与它在词表中的位置无关。表中的 Ġ 对应前导空格,这种表示方式将在后面的“字节级映射”中说明。

GPT-2 的 Byte-level BPE

BPE 是 Byte Pair Encoding(字节对编码)的缩写。GPT-2 使用 Byte-level BPE(字节级 BPE)将文本转换成 Token ID。一次编码可以概括为:

How are 的 Byte-level BPE 编码过程

预切分

Tokenizer 先用一条固定的文本匹配规则(正则表达式)扫描输入,将内容大致划分为:

  • 连续的字母,包括汉字等 Unicode 字母
  • 连续的数字
  • 连续的标点或其他符号
  • 连续的空白
  • 常见的英文缩写后缀,如 's、't、're

其中,字母、数字或符号片段可以带上紧邻其前的一个普通空格。例如:

1
2
3
How are
↓
"How" + " are"

这也是 "are" 和 " are" 可能得到不同 Token ID 的原因。在当前词表中,不带空格的 "are" 是 Token ID 533,带空格的 " are" 则是 Token ID 389。

预切分只负责划分片段,并不直接决定最终 Token。

字节级映射

GPT-2 先把每个片段表示为 UTF-8 字节,再通过一张可逆映射将每个字节变成便于处理的字符。常见例子包括:

1
2
空格 0x20 → Ġ
换行 0x0A → Ċ

所以 " are" 在 BPE 内部会从下面四个初始符号开始:

1
Ġ a r e

这种设计让 GPT-2 可以从字节层面表示不同语言和符号,而不必为每个 Unicode 字符准备一个基础 Token。代价是一个非 ASCII 字符可能被拆到多个 Token 中,单个 Token 文本片段不一定能独立解码成完整的 Unicode 字符。

为什么还需要预切分?

预切分先确定哪些内容属于同一个 BPE 处理片段;字节级映射则只负责把片段转换成可逆的字符表示,不会重新划分片段。对于 How are,BPE 会分别在 "How" 和 " are" 内部尝试合并,不能跨越两个片段的边界。

BPE 合并

BPE 合并规则(merges)规定哪些相邻符号可以合并。每条规则在数组中的位置就是 rank(优先级)。BPE 只比较同一片段中当前相邻的候选;rank 越小的规则越先执行。

How 从三个初始符号开始:

1
2
3
4
5
H o w
↓ o + w → ow rank 66
H ow
↓ H + ow → How rank 2181
How

" are" 的合并过程则是:

1
2
3
4
5
6
7
Ġ a r e
↓ Ġ + a → Ġa rank 1
Ġa r e
↓ r + e → re rank 4
Ġa re
↓ Ġa + re → Ġare rank 133
Ġare

如果一个片段不能继续合并,它就会保留为多个较小 Token。前面的 unbelievable 被拆成 un、bel、iev 和 able,正是这个过程的结果。

查询词表

BPE 合并结束后,Tokenizer 用每个 Token 查询前面介绍的词表:How 对应 Token ID 2437,Ġare 对应 Token ID 389。查询结果按照原有顺序组成 Token ID 序列 [2437, 389]。

GPT-2 的词表与合并规则是配套生成的,因此 BPE 合并结果能够在词表中找到。

词表和合并规则从哪里来?

词表和合并规则在 Tokenizer 训练阶段根据训练语料确定,后续编码文本时保持不变。它们是 Tokenizer 的数据,不是 GPT-2 用于模型计算的神经网络权重。

反分词

在 How are → you doing? 的续写过程中,GPT-2 在 How are 后依次生成了 Token ID 345、1804 和 30。这些 ID 不需要再次分词;要把它们显示为可读文本,需要执行相反的转换:反分词(Detokenize)。

Token ID 345、1804 和 30 还原为 you doing?

Ġyou 和 Ġdoing 中的 Ġ 都会还原成前导空格,而 ? 不带前导空格。因此,这三个 Token 依次拼接后的反分词结果是 " you doing?"。它与原输入拼接后,得到完整文本 "How are you doing?"。

这个过程不会倒着执行 BPE 合并规则,只需根据 ID 查回 Token 文本片段,再逆转字节级映射并拼接结果。

对于英文 ASCII 文本,单个 Token 通常可以直接显示;而对于中文等非 ASCII 文本,单个 Token 可能只包含一个 UTF-8 字符的部分字节。流式输出时,推理框架需要先积累这些字节,才能显示完整字符,不能假设每个 Token 都能独立解码为完整的 Unicode 文本。

llama.cpp 实战

下面使用 llama.cpp b10435 和 GPT-2 Q8_0 模型,验证 How are → [2437, 389],查看 GPT-2 的预切分规则,以及 GGUF 中真实的词表与 BPE 合并规则。基础环境准备见《LLM 如何逐个生成 Token?》。

进入 llama.cpp 目录,设置模型路径,并构建本篇使用的工具:

1
2
3
4
5
6
7
cd llama.cpp

GPT2_MODEL=models/QuantFactory/gpt2-GGUF/gpt2.Q8_0.gguf

cmake --build build \
--target llama-tokenize \
--config Release -j

查看 Token ID 与文本片段

运行 llama-tokenize:

1
2
3
4
5
./build/bin/llama-tokenize \
-m "$GPT2_MODEL" \
--no-bos \
--log-verbosity 0 \
--prompt 'How are'

输出:

1
2
2437 -> 'How'
389 -> ' are'

--no-bos 表示不添加 BOS Token。输出共有两个 Token:" are" 的前导空格属于第二个 Token,两个 Token ID 分别是 2437 和 389。

再观察一个不能按完整单词切分的例子:

1
2
3
4
5
./build/bin/llama-tokenize \
-m "$GPT2_MODEL" \
--no-bos \
--log-verbosity 0 \
--prompt 'unbelievable!'

输出:

1
2
3
4
5
  403 -> 'un'
6667 -> 'bel'
11203 -> 'iev'
540 -> 'able'
0 -> '!'

这说明 Token 既可以是单词片段,也可以是标点。

llama-tokenize 默认启用 --escape,会解析 \n 等转义序列。把输入文本改成 'How\nare',可以观察到换行对应 Token ID 198,换行后的 "are" 对应 ID 533,而不是带前导空格的 389。

查看 GPT-2 的预切分规则

llama-tokenize 展示的是经过全部分词步骤得到的最终 Token,不会直接输出预切分产生的中间片段。要查看 GPT-2 使用的具体预切分规则,可以检查 llama.cpp 源码:

1
rg -n -A 1 'GPT2 system regex' src/unicode.cpp

输出:

1
2
214:// GPT2 system regex:  's|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+
215-static std::vector<size_t> unicode_regex_split_custom_gpt2(const std::string & text, const std::vector<size_t> & offsets) {

其中,\p{L}、\p{N} 和 [^\s\p{L}\p{N}] 分别匹配字母、数字和其他符号;前面的 ? 表示可以带上一个普通空格,其他分支用于匹配英文缩写后缀和空白。这与前面“预切分”小节概括的规则一致。

查看 GGUF 中的 Tokenizer 数据

gguf-dump 位于 llama.cpp 的 gguf-py 包中。如果当前环境里还没有这个工具,可以安装到仓库内的 Python 虚拟环境:

1
2
python3 -m venv .venv
.venv/bin/pip install --editable ./gguf-py

查看 GPT-2 的主要 Tokenizer 元数据:

1
2
.venv/bin/gguf-dump "$GPT2_MODEL" --no-tensors |
grep -E 'tokenizer\.ggml\.(model|pre|tokens|merges|bos_token_id|eos_token_id)'

筛选后的标准输出如下:

1
2
3
4
5
6
13: STRING     |        1 | tokenizer.ggml.model = 'gpt2'
14: STRING | 1 | tokenizer.ggml.pre = 'gpt-2'
15: [STRING] | 50257 | tokenizer.ggml.tokens = ['!', '"', '#', '$', '%', '&', ...]
17: [STRING] | 50000 | tokenizer.ggml.merges = ['Ġ t', 'Ġ a', 'h e', 'i n', 'r e', 'o n', ...]
18: UINT32 | 1 | tokenizer.ggml.bos_token_id = 50256
19: UINT32 | 1 | tokenizer.ggml.eos_token_id = 50256

tokenizer.ggml.pre = 'gpt-2' 表示当前模型选择了上一节查看的 GPT-2 预切分方式。

tokenizer.ggml.tokens 就是词表。它有 50257 个元素,数组下标范围是 0~50256;tokenizer.ggml.merges 则按 rank 顺序保存 50000 条 BPE 合并规则。

普通输出会截断大数组。要查看指定 Token ID,可以输出完整 JSON,再使用 jq 提取目标元素:

1
2
3
4
5
6
7
.venv/bin/gguf-dump "$GPT2_MODEL" \
--no-tensors --json --json-array |
jq -r '
.metadata["tokenizer.ggml.tokens"].value as $tokens
| [0, 30, 345, 389, 1804, 2437, 50256][]
| "\(.)\t\($tokens[.])"
'

输出:

1
2
3
4
5
6
7
0	!
30 ?
345 Ġyou
389 Ġare
1804 Ġdoing
2437 How
50256 <|endoftext|>

这些条目按照 Token ID 从小到大排列,与前面的词表示例一致。它们在 How are you doing? 中的出现顺序则是 2437、389、345、1804、30。

GGUF 内部的 Ġare、Ġyou 和 Ġdoing 都包含表示前导空格的 Ġ;llama-tokenize 显示这些 Token 时,会把它还原成真实空格。

查看 BPE 合并规则

从 GGUF 中提取 How 和 " are" 用到的合并规则及其 rank:

1
2
3
4
5
6
7
8
9
10
11
12
.venv/bin/gguf-dump "$GPT2_MODEL" \
--no-tensors --json --json-array |
jq -r '
.metadata["tokenizer.ggml.merges"].value
| to_entries[]
| select(
.value == "Ġ a" or .value == "r e" or
.value == "Ġa re" or .value == "o w" or
.value == "H ow"
)
| "rank=\(.key)\t\(.value)"
'

输出:

1
2
3
4
5
rank=1	Ġ a
rank=4 r e
rank=66 o w
rank=133 Ġa re
rank=2181 H ow

这些真实规则与前面手工展开的 BPE 合并过程一一对应。

小结

本文以 How are → [2437, 389] 为例,说明了 GPT-2 如何将文本转换为 Token ID:

  • Token 是词表中的文本片段,不等同于单词或字符;Token ID 是它在词表中的整数编号。
  • GPT-2 依次经过预切分、字节级映射、BPE 合并和词表查询,得到按照原文顺序排列的 Token ID。
  • 反分词完成相反方向的转换:根据 Token ID 查询词表、还原字节并依次拼接,不需要逆向执行 BPE。

当我们与 ChatGPT 对话,或让 Agent 完成任务时,背后发挥核心作用的都是 LLM(大语言模型)。文本生成是 LLM 最基础的能力,也是理解其工作原理的起点。

本文以 GPT-2(Small) 为例,先从文本续写的整体过程入手,再解释自回归生成(Autoregressive generation)和单次预测,最后用 llama.cpp 进行验证。

文本续写

How are 续写为 you doing?

就功能而言,可以把 LLM 看作一个文本续写系统:输入 How are,输出 you doing?。从外部看,整个续写过程像是一次完成的。

自回归生成

实际上,you doing? 由多个 Token 依次生成。Token 是模型处理文本的基本单位,通常对应一个文本片段。每轮根据已有的 Token 序列预测下一个 Token,再将它追加到序列中,进入下一轮,直到满足停止条件。

you doing? 的三轮自回归生成过程

需要注意," you" 和 " doing" 的前导空格也是 Token 的一部分,因此 Token 并不等同于单词。

一次 Token 预测

以 GPT-2 第一次预测 " you" 为例,一次 Token 预测可以分为分词(Tokenize)、GPT-2 计算、采样(Sampling)和反分词(Detokenize)四个步骤:

从 How are 预测下一个 Token " you"

分词

分词将 How are 划分为两个 Token,并将它们映射为词表(Vocabulary)中的整数编号,也就是 Token ID:

1
2
"How"  → 2437
" are" → 389

因此,模型接收的 Token ID 序列是 [2437, 389]。这些整数后续还要转换为向量才能参与计算。

GPT-2

GPT-2 接收 Token ID 序列,输出 50257 个 logits(原始预测分数)。每个 logit 对应词表中的一个 Token,表示它作为下一个 Token 的原始评分。

得到 logits 后,GPT-2 的前向计算(Forward Pass)就结束了。接下来由推理框架根据这些评分进行采样,选出下一个 Token ID。

采样

采样根据 logits 选择下一个 Token ID,可以通过 Temperature(温度)、Top-k 和 Top-p 等参数控制选择结果。本例将 Temperature 设为 0,以确定性方式选中 Token ID 345。

即使 logits 相同,不同的采样参数也可能选出不同的 Token。因此,模型计算和 Token 选择是两个不同阶段。

反分词

反分词是分词的逆向转换:Token ID 345 根据同一份词表还原为文本片段 " you"。

为什么无须重新分词?

反分词得到的文本片段只用于显示,采样选出的 Token ID 会直接追加到已有序列:[2437, 389] + 345 → [2437, 389, 345]。GPT-2 基于更新后的 Token ID 序列继续预测。不断重复这一过程,就形成了自回归生成。

llama.cpp 实战

下面用 llama.cpp 实际观察分词结果、候选分数与采样结果,以及最终的续写结果。

GPT-2 与 llama.cpp 在这一过程中承担不同职责:

  • GPT-2:包含模型结构和训练得到的参数,将 Token ID 序列转换为 logits。
  • llama.cpp:加载 GGUF 中的模型与 Tokenizer(分词器)的相关信息,构建并执行计算图(Computation Graph),完成采样和文本输出。

环境准备

下面的实验基于 llama.cpp(版本标签:b10435),需要预先安装 Git、CMake、cURL 和 C/C++ 编译器。

克隆代码并构建 llama-completion 和 llama-server:

1
2
3
4
5
6
7
8
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
git checkout b10435

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build \
--target llama-completion llama-server \
--config Release -j

接着从 QuantFactory/gpt2-GGUF 下载 GPT-2 Q8_0 GGUF 模型:

1
2
3
4
5
6
GPT2_MODEL=models/QuantFactory/gpt2-GGUF/gpt2.Q8_0.gguf
mkdir -p models/QuantFactory/gpt2-GGUF

curl -L --fail \
-o "$GPT2_MODEL" \
https://huggingface.co/QuantFactory/gpt2-GGUF/resolve/main/gpt2.Q8_0.gguf

查看分词结果

借助 llama-completion,可以查看输入文本的分词结果,包括 Token 及其对应的 ID:

1
2
3
4
5
6
7
./build/bin/llama-completion \
-m "$GPT2_MODEL" \
--prompt 'How are' \
--predict 0 \
--verbose-prompt \
2>&1 |
grep -E "llama_completion: (prompt:|number of tokens in prompt)| I +[0-9]+ -> '"

输出:

1
2
3
4
0.00.366.307 I llama_completion: prompt: 'How are'
0.00.366.307 I llama_completion: number of tokens in prompt = 2
0.00.366.309 I 2437 -> 'How'
0.00.366.310 I 389 -> ' are'

查看候选分数与采样结果

先在一个终端启动 llama-server:

1
2
./build/bin/llama-server \
-m "$GPT2_MODEL"

模型加载完成后,另开一个终端,请求生成一个 Token:

1
2
3
4
5
6
7
8
9
10
curl -sS http://127.0.0.1:8080/completion \
-H 'Content-Type: application/json' \
-d '{
"prompt": "How are",
"n_predict": 1,
"temperature": 0,
"return_tokens": true,
"n_probs": 3,
"response_fields": ["content", "tokens", "completion_probabilities"]
}'

省略响应中的 bytes 后,关键结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"content": " you",
"tokens": [345],
"completion_probabilities": [
{
"id": 345,
"token": " you",
"logprob": -1.0721638,
"top_logprobs": [
{"id": 345, "token": " you", "logprob": -1.0721638},
{"id": 356, "token": " we", "logprob": -2.1492436},
{"id": 262, "token": " the", "logprob": -2.3353780}
]
}
]
}

tokens[0] 表示最终选中的 Token ID,即 345;对应的文本片段是 " you"。top_logprobs 列出了本轮预测中排名靠前的候选 Token,其中的 logprob(对数概率)由 logits 转换而来,并非 GPT-2 直接输出的原始 logits。Temperature 为 0 时,llama.cpp 会确定性地选择排名第一的 Token。

查看续写结果

最后运行 llama-completion,从 How are 开始连续生成三个 Token:

1
2
3
4
5
6
./build/bin/llama-completion \
-m "$GPT2_MODEL" \
--prompt 'How are' \
--predict 3 \
--temp 0 \
--log-verbosity 1

续写结果:

1
How are you doing?

小结

本文以 How are → you doing? 为例,串起了 LLM 逐个生成 Token 的基本流程:

  • How are 被分词为 [2437, 389],第一次预测选中 Token ID 345(" you"),连续预测后得到 you doing?。
  • 一次 Token 预测依次经过分词、GPT-2 计算、采样和反分词;新 Token ID 会追加到已有序列,形成自回归生成。
  • GPT-2 负责根据 Token ID 计算 logits,llama.cpp 负责模型执行、Token 选择和文本输出。

AI 正在融入我们的生活

AI应用全景图

如果说过去的人工智能更多存在于实验室和专业领域,那么今天,AI 已经来到我们身边。从学习辅导、内容创作,到软件开发、办公协作,再到医疗、科研、自动驾驶和机器人,AI 正在进入越来越多真实场景。

AI 应用的三次跃迁

AI 的范围远不止大语言模型,还包括计算机视觉、语音识别、推荐系统、机器学习、机器人控制等多个领域。它的能力可以体现在感知、预测、生成、决策和行动等不同环节。

近年来,以大模型为重要推动力的新一轮 AI 应用浪潮正在展开:AI 先从对话工具进入工作流程,再从协作助手发展为自主执行任务的 Agent,最后与视觉、传感器、机器人和控制系统结合,进一步进入物理世界。

第一跃迁:从 Chatbot 到 Copilot

第一跃迁

从 Chatbot 到 Copilot,关键变化是 AI 从独立的对话工具进入具体任务环境,利用任务上下文,在人的操作过程中持续提供协助。人仍主导任务目标、执行步骤和关键操作,AI 负责协作,而不是独立完成任务。

学习:从通用答疑到个性化学习助手

Chatbot

学生:

植物细胞和动物细胞有什么区别?

AI 根据通用知识回答问题。

学生仍需要:

  • 补充课程背景
  • 判断回答是否符合教材
  • 自己整理考试重点
  • 自己安排后续练习

典型特征

针对单个问题作答,依赖用户补充上下文。

Copilot

学生在学习空间中添加:

  • 生物教材 PDF
  • 课堂笔记
  • 课程大纲

然后提出目标:

根据这些资料,帮我复习考试中的植物细胞和动物细胞。

AI 可以:

  1. 根据课程大纲确定考试范围
  2. 结合教材和笔记解释知识点
  3. 比较植物细胞与动物细胞
  4. 通过测验发现薄弱点
  5. 根据测验结果调整后续学习内容

典型特征

基于课程材料和学习目标持续提供协助。

代表产品

办公:从内容生成到工作助手

Chatbot

用户:

根据这份会议记录生成一份会议纪要。

AI:

提炼议题、结论和待办事项,生成一份会议纪要。

任务结束。

典型特征

根据一次提示生成内容,任务完成即结束。

Copilot

AI 进入整个工作流程:

会议前:

  • 根据主题生成会议材料

会议中:

  • 自动记录讨论内容

会议后:

  • 总结关键结论
  • 提取待办事项
  • 生成跟进邮件

典型特征

利用会议上下文贯穿整个工作流程。

代表产品

第二跃迁:从 Copilot 到 Agent

第二跃迁

从 Copilot 到 Agent,关键变化是 AI 从协助人操作,发展为围绕目标自主拆解任务、调用工具、执行操作,并根据结果调整后续步骤。人从逐步操作任务,转向提出目标和检查结果。

软件开发:从代码建议到自主执行开发任务

Copilot

开发者:

这段登录代码为什么会报错?

Copilot 根据当前文件和选中的代码:

  • 分析错误原因
  • 给出修改建议
  • 生成局部代码修改

开发者仍然需要:

  • 定位其他相关文件
  • 审查并应用修改
  • 运行测试
  • 根据结果继续修复

典型特征

围绕当前代码提供建议,执行步骤由人完成。

Agent

开发者:

修复用户登录失败的问题,并提交一个 PR。

AI Agent:

  1. 阅读代码仓库
  2. 理解项目架构
  3. 找到相关模块
  4. 复现并定位问题
  5. 修改相关代码
  6. 编写并运行测试
  7. 根据测试结果继续修复
  8. 创建 Pull Request

典型特征

围绕目标自主调用工具并完成开发任务。

代表产品

图形界面操作:从操作指导到自主执行

Copilot

用户:

如何在 Blender 中制作一个大炮模型?

Copilot 可以:

  • 说明建模步骤
  • 推荐使用的 Blender 工具
  • 生成 Blender Python 脚本
  • 解答操作过程中的问题

用户仍需要打开 Blender,并逐步完成建模操作。

典型特征

提供操作指导,界面操作由人完成。

Agent

用户:

在 Blender 中创建一个大炮的 3D 模型。

Agent:

  1. 观察 Blender 当前界面
  2. 创建并调整基础几何体
  3. 使用移动、缩放和旋转等工具
  4. 根据模型状态继续修改
  5. 完成 3D 模型

典型特征

直接操作界面并根据结果调整后续动作。

代表产品

第三跃迁:从数字 AI 到物理 AI

第三跃迁

从数字 AI 到物理 AI,关键变化是 AI 不再只处理文字、代码和数据,而是能够通过传感器感知环境、做出决策,并控制车辆或机器人。物理 AI 并非由单一大模型驱动,而是大模型、专用模型、传感器、控制算法与硬件共同作用的系统。

自动驾驶:从辅助决策到自主驾驶

数字 AI

AI 可以:

  • 分析路况
  • 预测拥堵
  • 规划路线

它向人提供建议,车辆仍由人驾驶。

典型特征

分析道路信息,辅助人做决策。

物理 AI

自动驾驶系统:

  • 通过摄像头和雷达感知环境
  • 识别车辆、行人与交通规则
  • 规划路线并决定转向、变道或减速
  • 控制车辆转向、加速和刹车

典型特征

感知道路环境,自主决策并控制车辆。

代表产品

机器人:从固定执行到自主感知与行动

预编程机器人

工厂机械臂通常按照预设程序工作:

  • 在固定位置执行固定动作
  • 根据预设规则响应传感器信号
  • 完成明确、重复的任务

任务或环境发生变化时,通常需要重新配置或编程。

典型特征

按预设程序执行固定任务。

自主机器人

面对“把桌上的杯子放到厨房”这样的任务,自主机器人需要:

  • 感知杯子、桌子和周围环境
  • 理解任务目标
  • 规划移动、抓取和避障动作
  • 根据执行结果不断调整
  • 控制身体完成任务

典型特征

根据任务、环境和反馈自主规划并行动。

代表产品

Agent 如何工作

Codex Agent

AI Agent 不是“更会聊天的 AI”,而是一个围绕目标持续行动的智能执行系统。以 Codex 这类编程 Agent 为例,它以 LLM 为决策核心,结合上下文、状态、工具和约束,通过 Agent Loop 不断规划、执行、观察和调整,直到任务完成。

Agent = LLM + 上下文与状态 + 工具 + 执行循环 + 约束

在每轮循环中,LLM 根据当前状态决定调用工具、调整计划、请求确认或结束任务;工具返回的日志、错误和测试结果又会成为下一轮决策的依据。这种反馈闭环使 Agent 能够动态处理问题,而不是按照固定流程运行。

实际系统还会通过最大步数、成本预算、权限控制和人工确认等机制限制循环,确保 Agent 的行动安全、可控。

实战演示:坦克大战

Codex 坦克大战

向 Agent 提出目标:

创建一个坦克大战小游戏,试玩并修复发现的问题。

Agent 将自主完成:

规划实现 → 编写代码 → 运行游戏 → Computer Use 试玩 → 发现问题 → 修改代码 → 再次试玩和验证

这个过程展示了 Agent 如何结合代码生成与界面操作,根据实际反馈持续调整,完成从创建到验证的闭环。

大模型如何工作

软件篇

大模型原理:软件篇

大模型生成回答,本质上是在根据已有内容预测下一个 Token。输入的文字会先被切分成 Token 并转换为向量,再经过多层 Transformer 计算。注意力机制帮助模型判断上下文中哪些信息更重要,最终为下一个 Token 的候选项计算概率。模型选出一个 Token 后,会把它加入上下文并重复这一过程,直到生成完整的回答。

模型的知识并不是以一条条记录的形式存放在内部,而是在海量文本训练中被编码进模型参数。温度和采样策略会影响模型是倾向于选择概率最高的结果,还是保留更多表达上的变化。由于它本质上是一个概率生成系统,回答流畅并不代表内容一定正确;面对冷门事实、过时信息和复杂推理,仍然需要进一步核实。

硬件篇

大模型原理:硬件篇

这些看似复杂的语言能力,落到硬件层面,主要是大规模的矩阵运算和数据搬运。GPU 拥有大量并行计算单元,能够同时执行许多结构相似的运算,因此成为大模型训练和推理的主力;Tensor Core 等专用单元则进一步提高了矩阵运算效率。不过,大模型的运行速度不仅取决于算力,也受到显存容量和带宽的制约。推理过程中,硬件需要不断读取模型参数、保存上下文缓存并传递中间结果,因此 HBM 和片上缓存同样重要。

一次推理通常分为 Prefill 和 Decode 两个阶段。Prefill 会集中处理用户输入,并为后续生成建立 KV Cache,它主要影响第一个 Token 出现前的等待时间;Decode 则利用缓存逐个生成新的 Token,主要影响回答持续输出的速度。上下文越长,KV Cache 占用的显存越多,数据搬运的成本也越高,因此长对话通常更消耗显存,生成速度也可能随之下降。

AI 时代,我们如何成长

AI 时代,我们如何成长

AI 带来的问题不只是“它会不会替代人”,更重要的是我们如何使用它:是把思考外包给 AI,还是借助 AI 学习、实验和验证,在提高效率的同时保持自主性和掌握感。

需要警惕

可靠性与安全

大模型可能产生幻觉、过时信息和推理错误,Agent 的工具调用还可能把错误判断变成真实行动。把资料和系统权限交给 AI,也会带来隐私泄露、数据滥用和安全攻击等风险。因此,事实核查、结果验证、权限控制和人工确认仍然不可缺少。

生成更快不等于真正交付

AI 可以快速生成大量内容和代码,但后续的调试、验证和修正仍可能耗费大量时间。只关注生成数量,而不确认结果是否正确、问题是否解决,就容易把“看起来快”误认为“真正交付快”。

便利可能削弱人的掌握感

AI 的即时反馈可能制造一种虚假的心流:人不断提出要求、看到结果快速出现,却没有真正理解问题。随着越来越多分析和判断被交给 AI,人也可能从任务的主导者变成结果的选择者,逐渐削弱独立判断、专业能力和创造力。

如何成长

从替人工作到增强人的能力

当 AI 工具和组织更强调效率与产出时,个人更需要主动保留提出问题、理解原理、检验结果和承担判断的过程,保护自己的自主性和掌握感。

AI 不应只是替人交出答案的黑盒,也可以成为扩展思考和创造能力的工具。它能够解释概念、查找资料、比较方案并快速构建实验;人则负责确定目标、提出问题和判断结果,通过“边做边学、边问边验证”加深理解,挑战更复杂的问题。

面向未来的核心能力

未来的竞争力并不只是“会不会使用 AI”,而是能否把 AI 与自己的专业知识、判断力和创造力结合起来:

专业能力 + AI 协作能力 + 判断与创造能力

专业能力帮助我们理解问题,AI 协作能力提高学习和实践的效率,判断与创造能力则让我们能够识别错误、提出新问题,并对最终结果负责。

不要用 AI 逃避思考,而要用 AI 加深思考;不要让 AI 替你完成工作,而要让 AI 帮你掌握工作。

Skills是当前Agent领域最热门的设计范式之一,风头一时无两。这也让很多人形成了一种直觉:

只要Skill写得足够详细,Agent就能搞定复杂任务。

在简单任务中,这种想法通常没什么问题。但一旦面对长程任务(Long-horizon tasks),效果往往大打折扣,甚至根本无法完成。

那么问题就来了——当处理长程任务时,Skills应该如何设计?

长程任务为什么困难

在Agent系统中,长程任务是指那些需要持续几十分钟、数小时甚至几天时间才能完成的任务,例如:

  • 自动化软件开发流程
  • 深度技术调研
  • 长链路业务流程执行

这类任务通常具有三个特点:

  1. 步骤数量多
  2. 执行时间长
  3. 上下文持续增长

从LLM的角度来看,本质问题在于:

任务执行过程中产生的token会不断累积,进而导致上下文膨胀,甚至会超出LLM的上下文限制。

有了Skills为什么还需要上下文工程

针对上述问题,Anthropic在Effective context engineering for AI agents中进行了深入探讨,并从上下文工程(Context engineering)的角度出发,提出了3种解决方案:

  • 压缩(Compaction):在上下文接近上限时,通过高保真摘要压缩历史对话并重启上下文窗口,以维持长程任务的连贯性。
  • Subagents(Sub-agent architectures):将复杂任务拆分给多个拥有独立上下文的专用Subagent处理,并由主Agent进行高层规划与结果整合,从而突破上下文限制并提升复杂任务的处理能力。
  • 结构化笔记(Structured note-taking):让Agent定期将重要信息记录到上下文之外的持久存储中,并在需要时拉回,以保持跨复杂任务的关键上下文和依赖。

细心的读者可能已经注意到,上述文章发表于Skills推出之前,那么Skills是否已经解决了长程任务的问题呢?答案是否定的。

关于Skills,我们在谈谈Agent Skills的底层原理中探讨过:它的三层加载技术(亦即Progressive Disclosure),本质上是一种Context Offloading策略。但有一点需要补充的是,Progressive Disclosure的设计是为了“让通用Agent具备处理各种特定任务的能力,但无需一次性加载所有特定任务的知识”,而不是为了更好地处理某个长程任务。

因此,即使有了Skills,我们仍然需要结合上下文工程的方法来应对长程任务。对于上述3种方法,压缩需要Agent系统本身支持(Claude Code等工具已经支持)。本文主要从使用者的角度出发,探讨如何利用Subagents和结构化笔记来优化Skills的设计。

一个简化案例:开发流程Agent

假设我们希望构建一个Agent,用于自动化软件开发流程。

实际的开发流程通常会涉及众多环节,这里为了简化讨论,我们只考虑以下三个步骤:

  • Issue → Code:根据Issue生成代码
  • Code → Tests:为代码生成测试
  • Code → Review:进行代码审查

针对这个流程,我们可以写一个“大Skill”:

1
2
3
4
5
6
7
8
9
10
---
name: dev-workflow
description: Complete development workflow for handling issues.
---

You help developers with three workflow tasks:

1. Issue to Code: Generate code from issue description.
2. Code to Tests: Generate tests for the generated code.
3. Code to Review: Perform code review on the generated code.

如果在Claude Code中使用这个Skill,执行过程中的Context大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[System] You are Claude Code.
[User] Complete issue.md
[Tool Call] Read(.claude/skills/dev-workflow/SKILL.md)
[Tool Output] <the skill instructions>
[Tool Call] Read(issue.md)
[Tool Output] <issue description>]
[Assistant] <generated code>
[Tool Call] Write(code.py)
[Tool Output] <success>
[Assistant] <generated tests>
[Tool Call] Write(tests.py)
[Tool Output] <success>
[Tool Call] Bash(python tests.py)
[Tool Output] <test results>
[Assistant] <generated review report>
[Tool Call] Write(review.md)
[Tool Output] <success>
[Assistant] <final output>

可以看到,3个任务共享同一个LLM上下文,随着任务的推进,上下文会不断膨胀。在真实场景中,如果Issue的需求比较复杂,上下文膨胀问题会更加严重,甚至可能导致任务失败。

技巧一:使用1个主Agent和3个Subagent

利用Subagents的思路,一种策略是创建3个独立的Subagent,分别负责开发、测试和审查(参考Create custom subagents)。

开发Subagent(位于.claude/agents/code-developer.md):

1
2
3
4
5
6
---
name: code-developer
description: Generate code from issue description.
---

You are a code developer. Your task is to generate code based on the provided issue description.

测试Subagent(位于.claude/agents/code-tester.md):

1
2
3
4
5
6
---
name: code-tester
description: Generate tests for the provided code.
---

You are a code tester. Your task is to generate tests for the provided code.

审查Subagent(位于.claude/agents/code-reviewer.md):

1
2
3
4
5
6
---
name: code-reviewer
description: Perform code review on the provided code.
---

You are a code reviewer. Your task is to perform code review on the provided code.

有了这3个Subagent之后,我们再创建一个Skill(位于.claude/skills/effective-dev-workflow/SKILL.md),指导主Agent调用这3个Subagent:

1
2
3
4
5
6
7
8
9
10
11
12
---
name: effective-dev-workflow
description: Complete development workflow for handling issues.
---

You are a project manager. Your task is to manage the development workflow for handling issues.

Here are the steps you need to follow:

1. Issue to Code: Use the `code-developer` agent to generate code from the issue description.
2. Code to Tests: Use the `code-tester` agent to generate tests for the generated code.
3. Code to Review: Use the `code-reviewer` agent to perform code review on the generated code.

同样地,我们可以在Claude Code中使用这个Skill,但执行过程中会产生4个独立的Context。

主Agent的Context:

1
2
3
4
5
6
7
8
9
10
11
12
13
[System] You are Claude Code.
[User] Complete issue.md
[Tool Call] Read(.claude/skills/effective-dev-workflow/SKILL.md)
[Tool Output] <the skill instructions>
[Tool Call] Read(issue.md)
[Tool Output] <issue description>
[Tool Call] Task(code-developer, "Generate code and save it as code.py")
[Tool Output] <final output from code-developer>
[Tool Call] Task(code-tester, "Generate tests for the code in code.py and save them as tests.py")
[Tool Output] <final output from code-tester>
[Tool Call] Task(code-reviewer, "Perform code review on the code in code.py and save the report as review.md")
[Tool Output] <final output from code-reviewer>
[Assistant] <final conclusion>

开发Subagent的Context:

1
2
3
4
5
6
[System] You are Claude Code.\nYou are a code developer...
[User] Generate code and save it as code.py
[Assistant] <generated code>
[Tool Call] Write(code.py)
[Tool Output] <success>
[Assistant] <final output>

测试Subagent的Context:

1
2
3
4
5
6
7
8
9
10
[System] You are Claude Code.\nYou are a code tester...
[User] Generate tests for the code in code.py and save them as tests.py
[Tool Call] Read(code.py)
[Tool Output] <code content>
[Assistant] <generated tests>
[Tool Call] Write(tests.py)
[Tool Output] <success>
[Tool Call] Bash(python tests.py)
[Tool Output] <test results>
[Assistant] <final output>

审查Subagent的Context:

1
2
3
4
5
6
7
8
[System] You are Claude Code.\nYou are a code reviewer...
[User] Perform code review on the code in code.py and save the report as review.md
[Tool Call] Read(code.py)
[Tool Output] <code content>
[Assistant] <generated review report>
[Tool Call] Write(review.md)
[Tool Output] <success>
[Assistant] <final output>

由上可见:

  • 主Agent只关注高层次的任务调度,每个Subagent只返回关键结果,大大减少了主Agent的上下文负担。
  • 同时,每个Subagent也都有自己独立的上下文,不会相互干扰,从而有效避免了上下文膨胀的问题。

技巧二:使用3个独立的Skill

利用结构化笔记的思路,我们也可以创建3个独立的Skill,分别负责开发、测试和审查。

开发Skill(位于.claude/skills/code-developer/SKILL.md):

1
2
3
4
5
6
7
8
9
10
---
name: code-developer
description: Generate code from issue description.
---

You are a code developer. Your task is to generate code based on the provided issue description.

There is a {project_root}/NOTES.md file that records the progress of the task:
- Please review this file before starting the task.
- After completing the task, record the key progress in this file.

测试Skill(位于.claude/skills/code-tester/SKILL.md):

1
2
3
4
5
6
7
8
9
10
---
name: code-tester
description: Generate tests for the provided code.
---

You are a code tester. Your task is to generate tests for the provided code.

There is a {project_root}/NOTES.md file that records the progress of the task:
- Please review this file before starting the task.
- After completing the task, record the key progress in this file.

审查Skill(位于.claude/skills/code-reviewer/SKILL.md):

1
2
3
4
5
6
7
8
9
10
---
name: code-reviewer
description: Perform code review on the provided code.
---

You are a code reviewer. Your task is to perform code review on the provided code.

There is a {project_root}/NOTES.md file that records the progress of the task:
- Please review this file before starting the task.
- After completing the task, record the key progress in this file.

然后,我们在3个不同的会话里分别调用这3个Skill,从而会产生3个独立的Context。

开发Skill的Context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[System] You are Claude Code.
[User] Complete issue.md
[Tool Call] Read(.claude/skills/code-developer/SKILL.md)
[Tool Output] <the skill instructions>
[Tool Call] Read(issue.md)
[Tool Output] <issue description>
[Tool Call] Read(NOTES.md)
[Tool Output] <notes content>
[Assistant] <generated code>
[Tool Call] Write(code.py)
[Tool Output] <success>
[Tool Call] Write(NOTES.md, "# Task Progress\n## Completed Tasks\n### 2026-03-08: Generated code and created code.py")
[Tool Output] <success>
[Assistant] <final output>

此时,NOTES.md的内容如下:

1
2
3
# Task Progress
## Completed Tasks
### 2026-03-08: Generated code and created code.py

测试Skill的Context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[System] You are Claude Code.
[User] Generate tests for code.py
[Tool Call] Read(.claude/skills/code-tester/SKILL.md)
[Tool Output] <the skill instructions>
[Tool Call] Read(code.py)
[Tool Output] <code content>
[Tool Call] Read(NOTES.md)
[Tool Output] <notes content>
[Assistant] <generated tests>
[Tool Call] Write(tests.py)
[Tool Output] <success>
[Tool Call] Bash(python tests.py)
[Tool Output] <test results>
[Tool Call] Edit(NOTES.md, "\n### 2026-03-08: Generated tests and created tests.py")
[Tool Output] <success>
[Assistant] <final output>

此时,NOTES.md的内容如下:

1
2
3
4
# Task Progress
## Completed Tasks
### 2026-03-08: Generated code and created code.py
### 2026-03-08: Generated tests and created tests.py

审查Skill的Context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[System] You are Claude Code.
[User] Perform code review on code.py
[Tool Call] Read(.claude/skills/code-reviewer/SKILL.md)
[Tool Output] <the skill instructions>
[Tool Call] Read(code.py)
[Tool Output] <code content>
[Tool Call] Read(NOTES.md)
[Tool Output] <notes content>
[Assistant] <generated review report>
[Tool Call] Write(review.md)
[Tool Output] <success>
[Tool Call] Edit(NOTES.md, "\n### 2026-03-08: Performed code review and created review.md")
[Tool Output] <success>
[Assistant] <final output>

此时,NOTES.md的内容如下:

1
2
3
4
5
# Task Progress
## Completed Tasks
### 2026-03-08: Generated code and created code.py
### 2026-03-08: Generated tests and created tests.py
### 2026-03-08: Performed code review and created review.md

由上可见:

  • 因为每个Skill都在不同的会话中被调用,所以它们之间不会共享上下文,从而避免了上下文膨胀的问题。
  • 每个Skill都通过NOTES.md文件来读取和记录关键进度,从而实现了跨Skill(跨会话)的信息传递,保证了整体任务的连贯性。

值得说明的是,这个技巧的本质是基于独立会话的多Agent协作。这里不同的Skill,实际上只是给每个Agent添加了不同的知识,使其能够执行特定的任务。

关于多Agent协作完成复杂任务的更多内容,Anthropic在另一篇文章Effective harnesses for long-running agents作了详细介绍,感兴趣的读者可以进一步阅读。

结语

Skills是一种强大的能力封装方式,但面对长程任务时,它并不是解决一切问题的银弹——LLM的上下文限制始终是绕不过去的瓶颈。

更实用的思路是将Skills与上下文工程结合起来:

  • 通过Subagents拆分上下文,让不同角色各自处理独立子任务;
  • 通过结构化笔记在上下文之外保存关键状态,实现跨会话的信息传递。

Skills负责告诉Agent“怎么做”,上下文工程决定Agent“能走多远”。两者结合,才是应对长程任务的完整答案。

大家好!今天给大家介绍cMCP v0.4.0的重要更新。

cMCP是什么?

cMCP是一个MCP服务器的命令行工具,可以理解为“MCP版的curl” —— 通过命令行就能快速调用和测试MCP服务器的功能。

基本用法:

1
2
3
4
5
# STDIO transport
cmcp 'python server.py' tools/list

# HTTP transport
cmcp http://localhost:8000/mcp tools/call name=add arguments:='{"a": 1, "b": 2}'

v0.4.0新特性:mcp.json配置支持

为什么引入mcp.json配置文件?

  1. 简化命令输入:以前每次调用都要输入完整的命令或URL以及各种配置参数,比较繁琐。
  2. 拥抱生态标准:MCP生态逐渐形成了MCP JSON配置标准,主流工具如Cursor、Claude Code都在使用。

有了配置文件,就可以统一管理所有MCP服务器了!

创建配置文件 .cmcp/mcp.json(或 ~/.cmcp/mcp.json):

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"mcpServers": {
"local-server": {
"command": "python",
"args": ["server.py"],
"env": {"API_KEY": "your-key"}
},
"remote-server": {
"url": "http://localhost:3000/mcp",
"headers": {"Authorization": "Bearer token"}
}
}
}

使用起来超级简单:

1
2
3
4
5
# 列出工具
cmcp :local-server tools/list

# 调用工具
cmcp :remote-server tools/call name=add arguments:='{"a": 1, "b": 2}'

只需要用 :server-name 就能引用预定义的服务器(及配置参数),大大提升效率!

兼容性

mcp.json配置格式与Cursor、Claude Code完全兼容,可以直接复用现有配置:

1
2
3
4
5
# 使用 Cursor 的配置
cmcp --config .cursor/mcp.json :my-server tools/list

# 使用 Claude Code 的配置
cmcp --config .mcp.json :my-server tools/list

快速开始

安装:

1
pip install cmcp

项目地址:https://github.com/RussellLuo/cmcp

欢迎大家体验 cMCP v0.4.0 的新功能!如果有任何问题或建议,欢迎在GitHub仓库中提出。

0%