AI agent 是什么:五种经典类型与两种相互竞争的定义
用四次打破吸尘器世界讲清五种经典 agent;再看一个 tool 如何把 39 个 input token 的调用变成 420 个。
本页内容
同一个问题,问同一个模型两次,权重相同,greedy decoding 相同。唯一的区别是,第二次目录里多了一个 tool。
no tools in the catalogue
turn 1 prompt= 39 out= 8 finish=stop TEXT "The capital of France is Paris."
=> model calls=1 prompt tokens=39 output=8 wall=974 ms
one tool in the catalogue: get_temperature(city)
turn 1 prompt= 185 out= 20 finish=tool_calls CALL get_temperature({"city": "Paris"})
tool get_temperature -> {"city":"Paris","celsius":11}
turn 2 prompt= 235 out= 18 finish=stop TEXT "The capital of France is Paris. It is
currently at 11 degrees Celsius."
=> model calls=2 prompt tokens=420 output=38 wall=6,685 ms一次调用变成了两次。三十九个 input token 变成了 420 个,放大 10.8 倍。不到一秒变成将近七秒。而答案还带上了一个没人问的事实,来自一个模型自行选择调用的 tool——尽管问题里从未提到天气。
第二个系统,就是 2026 年行业里大多数人会称为 agent 的东西。或者它并不是,取决于你打开两种最常被阅读的定义中的哪一种——而这两种定义说的并不是同一件事。其中一种甚至和自己也不一致。
这种分歧就是本章要讲的内容。这不是词汇之争:两种定义在不同轴线上划边界,而你选择哪条轴,决定了你要构建什么、要为哪些东西付费。两者都建立在一个更古老的分类法之上,而理解它最便宜的方法,就是构建世界上最糟糕的 agent。
查看详情
一个有两个房间的机器人
链接到此部分:一个有两个房间的机器人这个领域里最古老的例子,是一个在两个方格 A 和 B 组成的世界里工作的吸尘器,每个方格要么干净,要么脏。1 它留在了每一本教材里,因为这是能让 agent 做对或做错的最小世界。
percept 是一对值——我在哪里,以及这里是否脏——动作是 SUCK、LEFT 和 RIGHT。整个程序只有一行。
type Percept = { dirty: boolean; where?: "A" | "B" };
type Action = "SUCK" | "LEFT" | "RIGHT";
const textbook = (p: Percept): Action =>
p.dirty ? "SUCK" : p.where === "A" ? "RIGHT" : "LEFT"; 把它放到这个两格世界的每一种初始配置里运行:
A dirty, B dirty, start A -> steps=3 clean=true
A clean, B dirty, start A -> steps=2 clean=true
A dirty, B clean, start B -> steps=2 clean=true这就是一个 简单反射 agent:它只根据当前 percept 行动,不记得之前发生过的任何事。这不是玩具分类——恒温器就是这种,单次调用一个没有附带对话历史的语言模型也是这种。
现在按现实世界的方式把它弄坏。真实的吸尘机器人有尘土传感器和防撞器,而不是地毯下面贴着 A 标签的方格。把位置从 percept 里拿掉,其他什么都不改:
const dirtOnly = (p: Percept): Action => (p.dirty ? "SUCK" : "RIGHT");A dirty, B dirty, start A -> steps=3 clean=true still dirty=0
t=0 at=A percept={dirty:true} -> SUCK
t=1 at=A percept={dirty:false} -> RIGHT
t=2 at=B percept={dirty:true} -> SUCK
A dirty, B clean, start B -> steps=500 clean=false still dirty=1
t=0 at=B percept={dirty:false} -> RIGHT
t=1 at=B percept={dirty:false} -> RIGHT
t=2 at=B percept={dirty:false} -> RIGHT
t=3 at=B percept={dirty:false} -> RIGHT同一个程序,两个方格。从一种初始状态出发,它三步完成;从另一种初始状态出发,它会撞向右侧墙壁五百次,并且会一直撞到电池耗尽。它无法感知这两种情况的区别,所以也无法在其中采取不同动作。Russell 和 Norvig 用一句话给出了一般结论:在部分可观察环境中,简单反射 agent 往往无法避免无限循环。1
有一个修复办法只花一行代码,而且不需要 memory。在我们动用更聪明的东西之前,它值得先测量一下。
let seed = 12345;
const rnd = () => ((seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff);
const coin = (p: Percept): Action => (p.dirty ? "SUCK" : rnd() < 0.5 ? "LEFT" : "RIGHT"); 在三个尺寸下,对一条全脏走廊运行两千次,全程使用同一个带种子的生成器:
| rooms | mean steps | median | worst of 2,000 | never finished |
|---|---|---|---|---|
| 2 | 4.0 | 4 | 13 | 0 |
| 4 | 16.6 | 14 | 81 | 0 |
| 8 | 68.7 | 52 | 306 | 0 |
随机化完全消除了循环。它也有代价:如果你知道自己在做什么,八个房间只需要十五步;而这个 agent 平均要 68.7 步,最差一次用了 306 步。这就是整章的缩影。我们添加的每一种能力,都会换来在前一个 agent 无法处理的某种情况下的正确性,也都会以一种你必须先命名的货币向你收费。
到需要它们的时候再命名这些部件
链接到此部分:到需要它们的时候再命名这些部件一个 agent 通过 传感器 感知环境,并通过 执行器 行动。agent 程序 是从 percept 到动作的函数——上面的每段代码都是一个。percept 序列 是到目前为止感知到的一切,而简单反射 agent 会忽略除最后一项之外的全部内容。
理性 是大多数文章都会用错的词;把它用对,后面的内容才会有用。一个 agent 本身并不是理性或非理性的。Russell 和 Norvig 把理性 agent 定义为:对每一种可能的 percept 序列,它都会在给定该序列的证据和自身内置知识的情况下,选择预期能最大化其 performance measure 的动作。1 performance measure 不在 agent 内部:它属于设计者;理性也只能相对于它来定义。
这个规格通常写成四件事,PEAS:performance measure、environment、actuators、sensors。
| 吸尘机器人 | 生产环境中的客服 agent | |
|---|---|---|
| Performance measure | 每单位电量清洁的方格数 | 每美元解决的工单数,且不升级 |
| Environment | 地板、灰尘、家具、地毯 | 工单队列、你的数据库、客户 |
| Actuators | 轮子、吸力 | tool calls |
| Sensors | 尘土传感器、防撞器 | 用户消息、tool 结果 |
注意哪一行最不一样。2026 年构建 agent 的几乎每个团队都会写下 E、A 和 S——tool schemas、集成、消息格式——因为没有它们代码跑不起来。几乎没有人写下 P。没有它,「我们的 agent 表现不错」就没有任何可检查的含义,「理性」也根本无法应用到系统上,只能应用到一次演示上。第 29 章 讲的是如何把 P 变成一个数字,这就是它存在的原因。
┌───────────────────────── the environment ─────────────────────────┐
│ │
│ ┌──────────────────────── the agent ─────────────────────┐ │
│ │ │ │
───┼──►│ sensors ──► the agent program ──► actuators ─────┼──────┼──►
percept │ │ action
│ └────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
▲
the performance measure lives out here, in the head of
whoever built the thing, and the agent cannot change it任务环境还会沿着七条轴进一步分类,其中五条决定了这里大部分难度:完全可观察或部分可观察、确定性或非确定性、episodic 或 sequential、static 或 dynamic、known 或 unknown。1 一个通过真实网络和真实 tool 交互的 agent,位于这五条轴上最困难的角落——即使 temperature 为零也非确定(第 17 章),还有一个被低估的点:unknown,因为你并没有一个可靠模型来描述你自己的 tool 会如何改变世界。这就是为什么第 23 章的循环比起规划,更需要错误处理。
添加 memory,然后撞上下一堵墙
链接到此部分:添加 memory,然后撞上下一堵墙真实地板不是一维的,所以把世界升级成平面图。井号是墙,星号是灰尘,机器人从中间房间出发:
col 0 1 2 3 4 5 6
row 0 * . . # . . *
row 1 . # . # . # .
row 2 . # . S . # . S = the robot starts here
row 3 . # . # . # .
row 4 * . . # . . *显然的升级是 memory。agent 保留一张地图:它站过的每个方格,以及每个触发过防撞器的方格。它的规则是走进一个尚未访问过的相邻方格——先右,再下,再左,再上——当周围一切都已知时就后退。这是一个 基于模型的反射 agent:它从 percept 历史中维护 internal state,因此可以根据当前看不见的东西行动。
这是真正的改进,但仍然不够:
5,000 steps allowed -> steps=5,000 distinct squares visited=13/25 still dirty=2/4五千步之后,一半地板仍未见过。地图是正确的,规则也是正确的。agent 做不到的是使用地图去某个地方:它的规则永远只能回答「我四个邻居中的哪一个应该迈进去」,所以一旦它身边没有未访问过的方格,它就没有办法表达这个想法:八步之外有一个未访问方格,我想站到那里去。它知道自己在哪里。它不知道自己想去哪里。
一个目标,然后是偏好某条路线的理由
链接到此部分:一个目标,然后是偏好某条路线的理由一个 基于目标的 agent 在自己的世界模型之上,持有一份对它想要实现的状况的描述,并通过搜索动作序列来选择动作,直到找到一个能在那里结束的序列。目标把动作选择从查询变成了搜索。
目标是「没有脏方格留下」。搜索是一次到最近脏方格的广度优先遍历,它返回的路径就是计划。
goal-based (fewest moves) -> moves=27 battery=52 still dirty=0
from 2,3 -> 4,6 via 5 moves: 2,3 2,4 3,4 4,4 4,5 4,6
from 4,6 -> 0,6 via 4 moves: 4,6 3,6 2,6 1,6 0,6
from 0,6 -> 4,0 via 10 moves: 0,6 0,5 0,4 1,4 2,4 2,3 2,2 3,2 4,2 4,1 4,0
from 4,0 -> 0,0 via 4 moves: 4,0 3,0 2,0 1,0 0,0二十七步,地板清洁。但看看电量列和计划的最后一段。第 0 列铺了地毯:穿过一个地毯方格要消耗六单位电量,瓷砖方格只消耗一单位。agent 沿第 0 列回家,因为那是四步而不是八步;而这四个地毯步消耗 24,八步绕路本来只会消耗 13。
它无法做出别的选择。目标是一个 二元 测试:地板干净,或者不干净。每个以干净地板结束的计划都同样满足它,所以当多个计划都成功时,agent 没有依据在它们之间选择。偏好某个成功而不是另一个成功,需要一个定义在结果上的数字,这个数字就是 utility function。最大化它的 agent 就是 基于 utility 的 agent。
代码上的变化只是搜索里的一个项。广度优先搜索数的是步数;让它数成本,你就得到了 Dijkstra 算法和一个不同的 agent:
const nd = dist.get(k)! + (byCost ? cell.cost : 1); // <- the entire differencegoal-based (fewest moves) -> moves=27 battery=52 still dirty=0
utility-based (cheapest route) -> moves=31 battery=41 still dirty=0
from 4,0 -> 0,0 via 8 moves: 4,0 4,1 4,2 3,2 2,2 1,2 0,2 0,1 0,0多走四步,少耗十一单位电量:便宜了 21%。相同目标,相同地图,除了一个项以外相同的代码。这两个 agent 只在它们想擅长什么上不同,于是走了不同的回家路线。
这也是 agent 第一次需要某种它无法自己产生的东西。必须有人决定一单位电量相对于一步值多少钱。Utility 是以 agent 能计算的形式写下的 performance measure,而写下它是设计者的工作。人们说一个 agent「优化了错误的东西」时,他们几乎从来不是在说 bug。他们的意思是,这一行写得太草率了。
第五种类型,以及它出错的方式
链接到此部分:第五种类型,以及它出错的方式现在让灰尘重新出现。四个房间会以四种不同速率重新变脏,而 agent 从未被告知这些速率。它每 tick 访问一个房间,并且只能看到那个房间。performance measure 是 4,000 个 tick 中房间处于脏状态的 room-ticks——越低越好。
教材的分解中,一个 学习 agent 是上述任一种 agent 再加上三个部分:一个会改变 agent 的 learning element,一个告诉它相对于固定 performance standard 表现如何的 critic,以及一个提出值得尝试的动作以获得学习收益的 problem generator。1 同一环境中的三种策略。第一种不学习;第二种和第三种学习同样的东西,但使用方式不同。
| policy | dirty-room-ticks over 4,000 | versus the patrol |
|---|---|---|
| 固定 round-robin 巡逻,不学习 | 2,290 | — |
| learner A:估计每个房间的变脏速率,然后去最可能脏的地方 | 11,820 | 差 5.2× |
| learner B:相同估计,再按距上次访问的时间加权 | 1,576 | 好 31 % |
隐藏速率是:厨房 0.35,走廊 0.05,书房 0.02,阁楼 0.01——而 learner A 找到了它们。它正确识别出厨房是屋里最脏的房间,然后在模拟剩余的每一个 tick 都去厨房,而另外三个房间永远脏着。它比完全不学习还差五倍,而且它并没有坏。
教训和 utility 那一节相同。learner A 最大化的是「我马上要访问的房间为脏的概率」。performance measure 是「房间处于脏状态的 room-ticks」。这是两个不同的数字;第二个才是 critic 评分的对象,而没人告诉 agent。learner B 用同样学到的速率乘以上次访问以来的时间——也就是它预期会找到的灰尘量,而不是找到任意灰尘的概率——并且击败了它起步时的巡逻策略。
一个实现细节决定了结果。在 learner B 的第一个版本里,如果一个房间连续三次访问都没有发现灰尘,它的速率就会变成精确的零——而零乘以任何东西都是零,所以它再也不会被访问,估计也永远无法被纠正。对分数做平滑,成功次数加一除以试验次数加二,把 11,895 变成了 1,576。「还没有观察到」和「已经测量且结果为零」是不同断言;如果一个系统把它们存进同一个字段,就会做出自己无法撤销的决定。
五种类型,以及它们在 2026 年的样子
链接到此部分:五种类型,以及它们在 2026 年的样子 1 simple reflex percept ────────────────────────────────► rules ────► action
2 model-based percept ──► [state] ──────────────────► rules ────► action
3 goal-based percept ──► [state] ──► [goal] ──────► search ───► action
4 utility-based percept ──► [state] ──► [goal] ──► [U] ──► argmax ► action
5 learning all of the above, plus [critic] ──► changes the parts above这五种中的每一种,今天都以另一个名字在生产环境中运行。
| classic type | what it carries between percepts | its 2026 shape | what it cannot do |
|---|---|---|---|
| simple reflex | 无 | 没有历史的一次模型调用:分类器、抽取端点、single-turn completion | 任何依赖上一 turn 的事 |
| model-based reflex | 从 percept 历史构建的 internal state | 聊天:transcript,每次调用都完整重发 | 选择对话应该在哪里结束 |
| goal-based | state 加上对期望状况的描述 | 带 stopping condition 的 reason-and-act 循环2 | 在多个成功计划之间偏好其一 |
| utility-based | state、目标,以及定义在结果上的数字 | evaluator–optimiser 循环,以及按书面标准对候选答案排序(第 25 章) | 发明标准 |
| learning | 上述全部,再加上 critic 和 problem generator | Reflexion:它把自己的经验写入 episodic buffer,而不是更新权重;3 持久用户 memory(第 24 章) | 选择 critic 用来评分的标准 |
有两行不只是类比那么简单,而且这种接近会花钱。
聊天是一个模型不在内部的基于模型的反射 agent。 在教材里,state 是 agent 程序内部的一个变量。在聊天里,它是 transcript:它存在你这一侧,每次调用都完整重发,并且每次都在模型内部从零重建。这就是第 16 章的平方级账单,也是教材画成一个标着「state」的方框的那个对象。下面是在一个追问问题上,带不带前两条消息的差异测量:
with the transcript prompt=67 "The current temperature in Lisbon, Portugal is 15°C."
without the transcript prompt=29 "Lisbon is the capital of Portugal, not a city in Portugal."同一个模型,同样三个词的用户输入,第二个就是走廊机器人撞墙。那次运行里没有 tool,所以 15 是编出来的——但正是 state 让追问具有任何意义。你每次都重建它,并且在两 turn 对话上为它支付 2.3× 的 input token。第 16 章测量了这个倍数到第四十 turn 会变成多少。
Reflexion 是一种改变输入而不是改变程序的学习 agent。 在教材分解中,learning element 会修改 performance element。Reflexion 保持权重不变,把反思文本写进一个 episodic buffer,供下一次尝试读取。3 learning element 是一个 prompt,memory 是数据库中的一行,performance element 是一个冻结模型——而图示仍然是教材里的那张,没有改变。
这里也要诚实说明映射的边界。五种类型分类的是 agent 程序。在 2026 年,这个程序被从中间劈开:一部分是你的代码,一部分在你没有训练过的权重里面。当模型自行决定调用 tool 时,目标测试是在你的程序里,还是在模型里?这个分类法没有答案,因为它写成的时候,目标测试没有别的地方可去——而这个问题正是两种现代定义分道扬镳的地方。
在一条 trace 里回答、调用和停止
链接到此部分:在一条 trace 里回答、调用和停止这些定义争论的是行为;把 trace 放在眼前,会容易判断得多。
下面的循环把对话发送给模型;如果回复包含 tool call,它就执行 tool,追加结果,然后把整个内容再发送一次。它运行在这台机器上一个 OpenAI 形状端点后面的本地 Qwen2.5-0.5B-Instruct 上——也就是第 14 章的接口缝合处,所以这个循环既不知道也不关心端口后面是什么。
const BASE = process.env.LLM_BASE_URL ?? "http://127.0.0.1:8799/v1";
async function loop(question: string, maxTurns = 6) {
const messages: Msg[] = [
{ role: "system", content: SYSTEM },
{ role: "user", content: question },
];
for (let turn = 1; turn <= maxTurns; turn++) {
const reply = await call(messages, TOOLS);
const calls = reply.choices[0].message.tool_calls ?? [];
messages.push(reply.choices[0].message);
if (!calls.length) return messages;
for (const c of calls) {
const out = runTool(c.function.name, JSON.parse(c.function.arguments));
messages.push({ role: "tool", name: c.function.name, content: out });
}
}
throw new Error("turn cap reached");
}两行承载了整个想法,而且都被标出来了;其余都是记账。一次运行里可以看到三种行为。问它自己能做的事,模型会 回答。问它不能做的事,它会 调用:
=== a question the model cannot answer, one tool available
turn 1 prompt= 187 out= 21 finish=tool_calls CALL get_temperature({"city": "Oslo"})
tool get_temperature -> {"city":"Oslo","celsius":4}
turn 2 prompt= 238 out= 12 finish=stop TEXT "The current temperature in Oslo is 4
degrees Celsius."
=> model calls=2 prompt tokens=425 output=33 wall=6,257 ms
=> stopped by: the model produced text instead of a call然后它会 停止——第三种行为,也是最容易漏掉的一种,因为它看起来像是什么都没发生。循环结束,是因为第 2 个 turn 回来时没有 tool call。没人替它做这个决定;模型通过输出散文做了决定。这个程序的终止条件,是一个缺席的符号。
还有两次运行值得占用篇幅。要求比较两个城市时,模型在一个 turn 里发出了 两个 tool call,拿回了两个读数,然后把比较答错了:
turn 1 prompt= 188 out= 43 finish=tool_calls CALL get_temperature({"city": "Oslo"}),
get_temperature({"city": "Lisbon"})
tool get_temperature -> {"city":"Oslo","celsius":4}
tool get_temperature -> {"city":"Lisbon","celsius":19}
turn 2 prompt= 284 out= 13 finish=stop TEXT "Oslo is currently warmer than Lisbon
at 4°C."tool 正常。并行调用正常。循环正常。答案是假的,尽管两个正确数字都摆在 transcript 里。给模型包上一层循环,并不会让它会推理;它只是让一个错误的模型拥有了基于错误采取行动的能力——这提前指向了 第 30 章,也指向第 29 章的一半。
现在删掉标出的 return,让循环跑到上限。相同问题,相同模型:
turn 1 prompt= 187 out= 21 CALL get_temperature({"city": "Oslo"})
turn 2 prompt= 238 out= 12 TEXT "The current temperature in Oslo is 4 degrees Celsius."
turn 3 prompt= 261 out= 30 TEXT "Could you please specify the exact location you're..."
turn 4 prompt= 302 out= 14 TEXT "Sure! Could you tell me which city you're interested in?"
turn 5 prompt= 327 out= 35 TEXT "I'm sorry, but I need more details to provide an..."
turn 6 prompt= 373 out= 12 TEXT "Which city would you like to know the temperature for?"
=> model calls=6 prompt tokens=1,688 output=124 wall=25,261 ms stopped by: turn cap四倍 input token,四倍 wall clock,最后 agent 忘了自己被问了什么,并开始盘问用户一个他们在第一 turn 就已经回答过的问题。正确答案在第 2 个 turn 时已经出现在屏幕上,之后每一个 turn 都让 transcript 变得更糟。
所以 agent 不是循环。它是循环 加上一条离开循环的规则,而这个例子里正好只有这一条规则。第 23 章会找到五条,并展示每条缺失时会坏掉什么。
两种定义,并排放在一起
链接到此部分:两种定义,并排放在一起这里直接引用而不是转述,因为混乱正是在转述里被制造出来的。
第一种定义把边界放在谁控制流程上。 Anthropic 的 Building effective agents 点明了这种歧义,并作出判断:
「在 Anthropic,我们把所有这些变体都归为 agentic systems,但会在工作流和 agent 之间划出一个重要的架构区别:工作流是通过预定义代码路径编排 LLM 和 tool 的系统。另一方面,agent 是由 LLM 动态指挥自身流程和 tool 使用的系统,并且保持对如何完成任务的控制。」4
这个测试问的是你的源代码:谁选择了下一步? 你的程序里有一个 switch:工作流。模型选择:agent。同一份文档还说 agent「通常只是 LLM 在循环中基于环境反馈使用 tool」——这正是上面的代码。
第二种定义把边界放在相对于用户的独立性上。 OpenAI 的 A practical guide to building agents 在定义页开头这样写:
「传统软件让用户能够简化和自动化工作流,而 agent 能够以高度独立的方式代表用户执行相同的工作流。agent 是独立代表你完成任务的系统。」5
同一页两句之后,它排除了:
「集成了 LLM 但不使用它们来控制工作流执行的应用——比如简单聊天机器人、single-turn LLM 或情感分类器——不是 agent。」5
按顺序读这些引文。开头几句把界线画在 独立性 上:这个东西会不会离开我自己去完成工作?第四句把界线画在 执行控制 上,这和 Anthropic 的界线完全相同。不同测试,同一页;而且有真实系统会让它们得出不同判断。
底下还有一个词汇碰撞,并且它会在真实会议里引发争论。在第一份文档里,工作流 是一种架构,并且它是 不是 agent 的东西。在第二份文档里,工作流 是「为了达成用户目标而必须执行的一系列步骤」——也就是工作本身,每个 agent 都有一个。「我们用 agent 替换了工作流」在第一种定义下是连贯的,在第二种定义下几乎没有意义。
三个系统,被分类两次
链接到此部分:三个系统,被分类两次三个 2026 年存在的系统,分别放到两种定义下。
终端里的 coding agent
链接到此部分:终端里的 coding agent你描述一个任务;它读取文件,运行测试套件,编辑,再次运行测试,并在测试通过或它放弃时停止。你的代码里没有任何东西决定下一步是「运行测试」——是模型根据上一个 tool 返回的内容做出这个决定。
定义一:agent,因为模型指挥自身流程。定义二:agent,因为它独立完成任务,识别完成并把控制权交还。两份文档都把这种形态作为核心例子。
一个夜间工单分流流水线
链接到此部分:一个夜间工单分流流水线对每张新客服工单,按固定顺序进行三次模型调用——分类、抽取字段、起草回复——然后发送。没有任何模型选择下一步发生什么;一个 for 循环负责。它在 03:00 运行,而且没人看着。
定义一:不是 agent。 它是 prompt chaining,并且被点名列为工作流。定义二:两个答案都可以。 按开头几句,它会代表你独立完成任务;按第四句,它没有用模型来控制工作流执行,因此被排除。这个系统就是你应该读完整页而不是只读摘录金句的原因。
一个带搜索 tool 的聊天助手
链接到此部分:一个带搜索 tool 的聊天助手一个用户 turn。模型自行决定回答前是否搜索,然后回答并等待你。
定义一:agent,因为模型会基于环境结果动态指挥自己的 tool 使用,而这正是声明的测试。定义二:不是 agent,因为这里没有独立性——一个 turn,然后交还——而「简单聊天机器人」被明确列入排除清单。
三者中有两个会换边。这不是任何一份文档的失败。这是在提醒一种会议:两个人完全同意一个系统做什么,却会花一小时争论它应该叫什么。
出路是两条轴,而不是一条
链接到此部分:出路是两条轴,而不是一条这些定义发生冲突,是因为它们各自把两个独立问题压缩进了一个词。把它们拆开,分歧就会变成一张表,而表比裁决更有用。
| 你的代码选择下一步 | 模型选择下一步 | |
|---|---|---|
| 每个 turn 都有人看着 | 里面放了一个模型的表单:分类器、抽取、single-turn completion | 带 tool 的聊天——定义一说是 agent,定义二说不是 |
| 直到完成前都没人看着 | 流水线——定义二的开头说是 agent,第四句说不是 | 所有人都同意:agent |
每种定义都在争议不同的单元格,另外两个单元格完全没有争议。所以当标签很重要时——在合同、风险审查、事故复盘里——值得写下的两句话不是「它是不是 agent」,而是 谁选择了下一步 和 谁在看着。两者都能通过读代码回答,都不需要任何人的定义;合在一起,它们承载了这个标签原本代表的全部后果。
这些都不是新问题。Wooldridge 和 Jennings 在 1995 年就调查了「agent」的竞争性含义;6 Franklin 和 Graesser 在 1996 年提出了本章的问题,收集了当时流通的定义,并发现它们并不一致。7 一篇 2023 年综述仍然从第一原则定义 agent——「感知环境、做出决策并采取行动的人工实体」8——因为没有已经定型的现代定义可引用;CoALA 则描述部件,而不是划边界。9 三十年都拒绝达成一致,说明这个词承担的工作不止一种。
agent 是 N 次调用,不是一次
链接到此部分:agent 是 N 次调用,不是一次现在是先于哲学抵达的后果,也就是账单。
这里的每次测量都有同样的形状。单次调用花了 39 个 input token;带一个 tool 的同一个问题跨两次调用花了 420 个;去掉 stopping rule 的循环跨六次调用花了 1,688 个。增长比线性更糟,因为第 n 个 turn 会携带此前每个 turn:那次六 turn 运行的 prompt 列是 187、238、261、302、327、373。第 16 章推导出总量是 ,并在一段真实对话上拟合了曲线。agent 会把每个任务都变成那种对话,无论有没有人类看见它。
如果这些测得的 token 数按第 16 章在 2026 年 9 月 6 日读到的商业端点费率计费——每百万 input token $2.00,每百万 output token $12.00——四次运行的价格如下:
| run | model calls | input tokens | output tokens | cost |
|---|---|---|---|---|
| 问题,无 tool | 1 | 39 | 8 | $0.000174 |
| 同一个问题,目录里有一个 tool | 2 | 420 | 38 | $0.001296 |
| 一个需要 tool 的问题 | 2 | 425 | 33 | $0.001246 |
| 同上,但移除 stopping rule | 6 | 1,688 | 124 | $0.004864 |
第二行对第一行,就是要记住的数字。成本变成七倍半,换来的却是对一个模型本来就知道的问题给出更差答案。 没有什么配置错了:一个 tool 存在,所以模型使用了它——而第 18 章的发现,即伤人的是目录的价格而不是它的准确性,在这里用只有一个条目的目录给出了最便宜的演示。
这就是为什么两份文档真正有用的一半,都是关于不要构建这个。Anthropic 很直白:找到尽可能简单的解决方案,只在需要时增加复杂度,这「可能意味着根本不构建 agentic systems」,因为 agentic systems「用延迟和成本换取更好的任务表现」,而且「对很多应用来说,用 retrieval 和 in-context examples 优化单次 LLM 调用通常就够了」。4 它支持构建 agent 的场景很窄:开放式问题,你无法预测步骤数,也无法硬编码路径;你信任环境,并接受「更高成本以及错误复合的可能性」。4 OpenAI 的筛选标准像镜像——复杂判断、不可维护的规则集、非结构化数据——但结尾相同:「否则,确定性解决方案可能就足够了」。5
所以,按本章的分类法:固定数量的步骤按固定顺序执行,是流水线;叫它 agent 不会让它更快。如果步骤数取决于你在路上发现什么,你需要一个循环——而你会用 N 次调用、平方级 transcript,以及一个可能错 N 次而不是错一次的系统,来购买这种灵活性。
接下来去哪里
链接到此部分:接下来去哪里现在你已经有了分类法、两种现代定义、让它们兼容的两条轴,以及一个会回答、调用和停止的短循环。
这个循环只有一种结束方式:模型停止请求 tool。第 23 章会故意把它弄坏七次,每次损坏都会加上一块组件。一个不可能完成的任务,它永远不结束——turn 上限。跑了一夜,账单来了——美元预算。一个失败的 tool——模型可以据此行动的错误。同一次调用发生两次——idempotency key。它不该碰的文件——human approval。中途重启——session persistence。一个沉默三分钟的 tool——进度和取消。最后得到的是一个 harness,也就是本课程其余部分都会运行在其上的文件。
这留下了本章那个有争议的对角线真正想问的问题。一个自行决定下一步的循环,必须决定何时停止;而我们刚刚看到了它无法停止时会发生什么:六个 turn,四倍账单,一个 agent 在盘问用户一个它已经回答过的问题。停止并不是一个条件。到底有多少个条件,哪一个会先触发?
Sources and method
链接到此部分:Sources and methodLilian Weng 的 LLM Powered Autonomous Agents (2023) 是最知名的语言 agent 分解:planning、memory 和 tool use。它是和两份厂商文档一起阅读的最佳下一篇;它的三个组件,按顺序对应本课程的第 23、24 和 18 章。
本章每一个数字都在这台机器上生成,没有任何估算。走廊、平面图、在其中行走的四个 agent,以及三种巡逻策略,都是上面的 TypeScript,在 Node 22 上运行;随机化 agent 的数字是每个条件下 2,000 次带种子运行的均值,巡逻数字是 4,000 个 tick 的单次带种子运行。模型 trace 来自 CPU 上 float32 的 Qwen2.5-0.5B-Instruct,使用 greedy decoding,由一个小型本地 Python 端点通过 loopback 提供服务;该端点加载权重并讲 OpenAI chat-completions 形状——又是那个接口缝合处,张量在 Python 一侧,循环在 TypeScript 一侧——所以 token 计数来自那个模型的 tokenizer,latencies 来自那台机器。唯一取自别处的数字,是成本表里的两个价格,它们是第 16 章在 2026 年 9 月 6 日从 OpenAI 定价页读取的费率;这里把它们应用到本地测得的 token 数上,只作说明,不代表一张实际账单。
参考资料
链接到此部分:参考资料-
Russell, S. 和 Norvig, P. Artificial Intelligence: A Modern Approach,第 4 版,第 2 章,Intelligent Agents。吸尘器世界、PEAS 规格、相对于 performance measure 的理性定义、任务环境的七个性质、本文使用的五种 agent 类型,以及在部分可观察环境中简单反射 agent 往往无法避免无限循环这一观察,均来自这里。这本书的配套代码是 GitHub 上的
aimacode/aima-python(8,806 stars,最后推送于 2026 年 6 月 30 日,读取于 2026 年 9 月 7 日)——值得精确说明它是什么。它是一本书的配套 repository,而不是像karpathy/micrograd(17,412)和karpathy/nanoGPT(62,852)那样被其他项目用来构建的参考实现。这就是为什么本章引用并链接它,而不是翻译它;也是为什么让 第 5 章 留在 Python 的生态系统论证不适用于这里:本章没有任何内容触碰张量,上面写出的循环是第 23 章循环的直系祖先。 ↩ ↩2 ↩3 ↩4 ↩5 -
Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. 和 Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022)。映射表中 goal-based 那一行所指的,就是 reasoning traces 和 actions 的交错。 ↩
-
Shinn, N., Cassano, F., Berman, E., Gopinath, A., Narasimhan, K. 和 Yao, S. Reflexion: Language Agents with Verbal Reinforcement Learning. arXiv:2303.11366 (2023)。该论文自己对机制的总结,是它能映射到学习 agent 的原因:它强化 agent「不是通过更新权重,而是通过语言反馈」,agent 会「口头反思任务反馈信号,然后在 episodic memory buffer 中维护自己的反思文本,以在后续尝试中诱导更好的决策」。 ↩ ↩2
-
Anthropic,Building effective agents,2024 年 12 月 19 日,
anthropic.com/engineering/building-effective-agents,读取于 2026 年 9 月 7 日。上文引用的 workflow/agent 区分、总括术语「agentic systems」、对 agent 的描述「通常只是 LLM 在循环中基于环境反馈使用 tool」、寻找尽可能简单解决方案且这「可能意味着根本不构建 agentic systems」的指导,以及支持和反对 agent 的条件,包括「更高成本以及错误复合的可能性」和用「例如最大迭代次数」这样的 stopping conditions 来保持控制的建议,均来自这里。 ↩ ↩2 ↩3 -
OpenAI,A practical guide to building agents,第 4 至 7 页,读取于 2026 年 9 月 7 日。「Agents are systems that independently accomplish tasks on your behalf」、对「simple chatbots, single-turn LLMs, or sentiment classifiers」的排除、把 workflow 定义为「为了达成用户目标而必须执行的一系列步骤」、agent 的两个核心特征、三个组件——model、tools、instructions——以及何时构建 agent 的筛选标准并以「otherwise, a deterministic solution may suffice」作结,均来自这里。 ↩ ↩2 ↩3
-
Wooldridge, M. 和 Jennings, N. R. Intelligent Agents: Theory and Practice. The Knowledge Engineering Review,第 10 卷第 2 期 (1995)。这篇综述把该领域的用法分为弱 agency 概念——autonomy、social ability、reactivity、pro-activeness——以及借用心理词汇的更强概念。今天读来,它记录的正是本章两份文档仍在进行的同一场争论。 ↩
-
Franklin, S. 和 Graesser, A. Is It an Agent, or Just a Program? A Taxonomy for Autonomous Agents. Proceedings of the Third International Workshop on Agent Theories, Architectures, and Languages, Springer (1996)。这里引用它不是为了某句引文,而是因为它本身:一篇收集当时流通的「agent」定义、发现它们互不一致,并提出分类法以替代争论的综述。三十年后,这场争论进入了设计更好的文档,除此之外没有变化。 ↩
-
Xi, Z. 等,The Rise and Potential of Large Language Model Based Agents: A Survey. arXiv:2309.07864 (2023)。上文引用了它开头的定义:「AI agents are artificial entities that sense their environment, make decisions, and take actions」,这是 2023 年重新表述的教材定义,因为当时没有已达成共识的现代定义可引用。 ↩
-
Sumers, T. R., Yao, S., Narasimhan, K. 和 Griffiths, T. L. Cognitive Architectures for Language Agents. arXiv:2309.02427 (2023)。它把语言 agent 组织成「modular memory components, a structured action space to interact with internal memory and external environments, and a generalized decision-making process to choose actions」,并明确把它们放在 symbolic AI 和认知科学的历史中。memory 分类法会在第 24 章返回,那里三种存储的表格是它的实践影子。 ↩