第一章 初识智能体
一句话总结
智能体(Agent)就是一个"数字员工":你只需要告诉它目标(比如"帮我规划一次北京旅行"),它会自己看情况、自己想办法、自己动手干活、干完还会检查结果——而不像普通软件那样,你按一个按钮它只会做一件固定的事。
本章你能学到什么
| # | 你会学到 | 学完能干什么 |
|---|---|---|
| 1 | 智能体的定义:四个零件 + 一个灵魂(自主性) | 拿到任何一个系统,能一句话判断它是不是智能体 |
| 2 | AIMA 的四级架构 + 学习作为元能力 | 面试被问"智能体有哪些类型"能答准,不会把学习错答成第五类 |
| 3 | 三个分类维度(决策架构 / 时间与反应性 / 知识表示) | 能从三个角度分析一个智能体,而不只知道最基础的那个 |
| 4 | LLM 智能体新在哪:隐式世界模型 + 涌现能力 | 能回答"为什么智能体这几年才火"这个高频问题 |
| 5 | 感知 → 思考 → 行动 → 观察循环,和 Thought/Action/Observation 协议 | 看懂任何 Agent 框架的内核 |
| 6 | 从空目录手写一个能跑的智能体(三个文件,约 100 行,含离线模式) | 不依赖任何框架,独立做出一个会用工具的智能体 |
| 7 | 本章最重要的工程原则:工具只取事实,不做决策 | 避免写出"披着 Agent 皮的 if-else"——你会亲手验证它怎么静默出错 |
| 8 | Workflow vs Agent 怎么选,以及生产上的混合方案 | 拿到一个真实需求能给出合理的技术选型和理由 |
| 9 | PEAS 建模的实际用法 | 用它检查出"你的要求超出了智能体的传感器能力"这类隐藏问题 |
动手部分需要多久:离线路线约 90 分钟,全程不需要 API 密钥、不需要联网。
这一章讲了什么
这一章是整本教程的开篇,回答了四个最基本的问题:
- 智能体到底是什么?——给出定义,并回顾了在 ChatGPT 出现之前,"老一代"智能体是怎么一步步进化的。
- 智能体是怎么工作的?——讲清楚它内部那个"感知 → 思考 → 行动 → 观察"的循环。
- 怎么亲手做一个?——用一百行左右的代码搭一个"智能旅行助手",能查真实天气、按天气推荐景点。
- 智能体在现实中怎么用?——它既可以当人类的助手(比如帮程序员写代码的工具),也可以当独立干活的"虚拟员工"。
读完这一章,你会建立起对"智能体"这个概念的完整轮廓。后面所有章节都是在这个轮廓上添砖加瓦。
核心知识点详解
1. 先弄懂几个"地基"级别的词
在讲智能体之前,必须先扫清几个术语障碍。这些词后面每一章都会反复出现。
大语言模型(LLM,Large Language Model)
先打个比方:LLM 像一个"读过全世界几乎所有书和网页的超级实习生"。它读了太多东西,所以你用日常语言问它任何问题,它都能像模像样地回答。ChatGPT、DeepSeek、豆包背后都是这种模型。
准确地说,LLM 是一种通过海量文本训练出来的人工智能程序,它的基本能力是:给它一段文字,它预测并生成接下来最合理的文字。别小看"接着往下写"这个能力——聊天、翻译、写代码、做规划,本质上都能靠它完成。
API(应用程序编程接口)
打个比方:API 就是程序世界里的"服务窗口"。你去银行办事不需要知道银行内部怎么运作,只要到窗口递材料、拿结果就行。程序也一样——天气网站开一个"窗口",别的程序把城市名递进去,窗口吐出天气数据。程序之间就是靠无数个这样的窗口互相帮忙的。
后面说的"调用 API",翻译成大白话就是"让自己的程序去别人家的窗口办个事、拿个结果"。
提示词(Prompt)
你发给 LLM 的那段话就叫提示词。它相当于你给那位"超级实习生"下的任务说明书。说明书写得越清楚(角色是什么、要做什么、按什么格式回答),实习生干得越好。专门研究"怎么把说明书写好"的技巧,叫提示工程(Prompt Engineering)。
2. 智能体的定义:四个零件加一个灵魂
教科书式的定义是:智能体是能通过传感器感知环境,并自主地通过执行器采取行动以达成目标的实体。
听起来很绕?用"管家"来类比就通了。想象你雇了一位管家:
| 要素 | 含义 | 管家类比 |
|---|---|---|
| 环境 | 智能体所处的外部世界 | 你的家、家里发生的一切 |
| 传感器 | 获取信息的入口 | 管家的眼睛和耳朵 |
| 执行器 | 影响外界的出口 | 管家的双手 |
| 行动 | 具体做的每件事 | 打扫、采购、修水管 |
对一个数字世界里的智能体来说,"眼睛"就是它读到的用户消息、API 返回的数据;"双手"就是它调用的各种工具(查天气、搜网页、执行代码)。
但真正的灵魂是第五个东西——自主性(Autonomy)。这是智能体和普通程序的分水岭:
- 普通程序:像一台自动售货机。你按 A3,它必然掉出 A3 那瓶水,每一步都是工程师提前写死的。
- 智能体:像真正的管家。你说"家里要来客人了,准备一下",具体是先打扫还是先买菜、买什么菜,是它自己临场决定的。
判断一个系统是不是智能体,就问一句话:它的做事步骤是提前写死的,还是它自己现场决定的?
3. 老一代智能体的五级进化(重点是逻辑链,不用背名字)
在 ChatGPT 出现之前的几十年里,科学家已经造出了各种智能体。它们像打游戏升级一样,一代比一代聪明,而且每一代都是为了解决上一代的毛病。理解这条进化链比记名字重要得多。
这套阶梯出自哪里:这是人工智能领域最经典的教科书《Artificial Intelligence: A Modern Approach》(作者 Stuart Russell 与 Peter Norvig,业内简称 AIMA,第 4 版 2020 年出版)系统提出的分类。你在任何 AI 课程里看到"反射/模型/目标/效用/学习"这五级,源头都是这本书。
第 1 级:简单反射智能体(Simple Reflex Agent)——"条件反射"
代表作:空调恒温器。规则就一条:"温度高于 26 度就制冷"。
它像人的膝跳反射,快而可靠,但没有记忆——它只看"此刻",完全不知道"刚才发生了什么"。如果决策需要参考历史,它就没辙了。
第 2 级:基于模型的反射智能体(Model-Based Reflex Agent)——"心里有本账"
为了解决"没记忆"的问题,第二代在脑子里维护了一份世界模型(World Model)——对外部世界的内部记录。它要回答的问题是:"世界现在是什么样子的?"
例子:自动驾驶汽车开进隧道,摄像头暂时看不到前车了,但它心里"记得"前面有辆车、大概什么速度、现在应该开到哪了。这就是初级的记忆。但它仍然只是被动应付眼前的情况,没有自己的目标。
第 3 级:基于目标的智能体(Goal-Based Agent)——"心里有个终点"
第三代开始主动规划,要回答的问题变成了:"我该做什么才能达成目标?"代表作:GPS 导航。你给它一个目标"到公司",它基于地图(世界模型)用搜索算法(比如 A* 算法)搜索出一条完整路线。
关键跃迁在于:它不再是"被什么刺激就做什么反应",而是"为了未来的某个结果,主动选择现在的行动"。新问题是:现实中目标往往不止一个。
第 4 级:基于效用的智能体(Utility-Based Agent)——"会打分、会权衡"
你不光要到公司,还想最快、最省油、别堵车——多个目标互相冲突怎么办?第四代给每一种可能的世界状态都赋一个"满意度分数"(术语叫效用值,Utility),目标从"达成某个状态"变成"最大化期望效用"。它回答的问题是:"哪种行为能给我带来最满意的结果?"这就把"理性权衡"变成了数学计算。
但到此为止,所有的聪明都还是工程师提前设计好的——规则是人写的,世界模型是人建的,效用函数也是人定的。
第 5 级:学习型智能体(Learning Agent)——"自己练级"
第五代能通过和环境反复互动、试错来自我改进,不依赖人类预设的规则。这背后的方法叫强化学习(Reinforcement Learning, RL)——大白话就是"做对了给糖,做错了不给,多练几万次自然就会了",和训练小狗握手是一个原理。
它的内部结构值得单独记一下,因为这是个常被忽略的要点:一个学习型智能体由两个元件组成——
| 元件 | 干什么 | 对应上面哪一级 |
|---|---|---|
| 性能元件(Performance Element) | 真正做决策、采取行动的那部分 | 就是前面 1~4 级中的任意一种 |
| 学习元件(Learning Element) | 观察性能元件的行动带来了什么结果,据此修正它的决策策略 | 新增的部分 |
所以"学习能力"不是第五种智能体,而是一种可以叠加到前四种上的"元能力"(meta-capability)。 你可以有一个"会学习的反射智能体",也可以有一个"会学习的效用智能体"。这个区分很重要——很多人把五级理解成互斥的五类,其实第五级是正交的一维。
里程碑案例是 AlphaGo Zero:从随机乱下棋开始,纯靠自我对弈"练级",最终发现了许多超越人类既有知识的有效策略。
记忆口诀:没记忆 → 有记忆 → 有目标 → 会权衡 →(叠加)会自学。前四级是阶梯,第五级是插件。
4. LLM 智能体:新范式到底"新"在哪(本章最重要的观点)
上面五代智能体有个共同点:它们的"聪明"来自工程师一行一行写出来的规则或公式,所以只能干一件专门的事——恒温器不会下棋,导航不会聊天。
LLM 智能体彻底变了:它的"聪明"来自预训练时读过的海量人类知识,所以它是通用的。
| 维度 | 传统智能体 | LLM 智能体 |
|---|---|---|
| 能力从哪来 | 工程师手写规则 | 海量数据训练出的知识 |
| 世界模型 | 显式的:工程师明确建好的地图、状态表 | 隐式的:藏在神经网络的参数里,说不清在哪,但确实存在 |
| 能干什么 | 只能干设计好的那件事 | 灵活通用,靠涌现能力应付没见过的情况 |
| 行为边界 | 确定、有边界、可穷举 | 开放、难穷举、需要额外约束 |
| 怎么跟它说话 | 必须输入规定格式的数据 | 直接说人话("帮我规划厦门之旅") |
名词补课——隐式世界模型(Implicit World Model):传统智能体的世界模型是你能打开看的——一张地图、一张状态表。LLM 的世界模型是训练时从海量文本里学出来的、编码在几十亿个参数里的"世界大概是怎么运作的"。你问它"把冰块放在太阳下会怎样",它答得出来,但你没法在它的代码里找到一条写着"冰遇热融化"的规则。看不见但确实在起作用,这就叫"隐式"。
名词补课——涌现能力(Emergent Abilities):模型规模小的时候完全不会做某件事,规模大到某个程度后突然会了,而且没人专门教过它。比如没有人拿"多步算术"专门训练过基础模型,但大到一定程度它就能做了。这是 LLM 智能体能"应对没见过的情况"的底层原因,也是它和传统智能体最本质的区别——传统智能体的能力上限就是工程师写进去的那些,不会多出来一分。
用"智能旅行助手"感受一下差别。以前你规划旅行,要自己开天气 App、地图 App、订票 App,自己汇总、自己决策。而 LLM 智能体收到"规划一次厦门之旅"这句模糊的话后,会:
- 规划与推理:自己把高层目标拆成一串子任务:
[确认出行偏好] → [查询目的地信息] → [制定行程草案] → [预订票务住宿]; - 工具使用:发现缺天气信息,就主动去调天气 API;查到"预报有雨",就在后续规划里倾向推荐室内活动;
- 动态修正:你说"这家酒店超预算了",它把这当成新的约束条件,重新搜索推荐。
作者用一句话总结这个时代转变:过去是"写代码实现业务逻辑",现在是"引导一个通用的大脑去规划、行动和学习"——你负责给目标、给工具、定规矩,具体怎么推理它自己来。
4.5 智能体的三个分类维度(面试常问,很多人只知道第一个)
上面讲的五级阶梯只是一个分类维度。完整地看,智能体有三个互补的分类视角——它们不是互斥的,同一个智能体在三个维度上各有一个位置。
| 维度 | 分成哪几类 | 关心什么 |
|---|---|---|
| ① 按内部决策架构 | 反射 / 模型式 / 基于目标 / 基于效用(+ 学习能力作为元能力) | 它的脑子有多复杂 |
| ② 按时间与反应性 | 反应式 / 规划式 / 混合式 | 它是先想还是先动 |
| ③ 按知识表示 | 亚符号主义 / 符号主义 / 神经符号主义 | 它的知识以什么形式存在 |
维度① 就是上面第 3 节的五级阶梯,出自 AIMA。
维度②:反应式 vs 规划式 vs 混合式 —— 这个维度关心的是一个核心权衡:速度 vs 最优。
| 类型 | 特点 | 代表 | 优势 | 代价 |
|---|---|---|---|---|
| 反应式(Reactive) | 感知直接映射到行动,几乎不规划 | 安全气囊、高频交易机器人 | 快、计算开销低 | 短视,容易陷入局部最优,做不了多步协调的任务 |
| 规划式(Deliberative) | 先用世界模型推演未来若干步,再选最优路径 | 下棋 AI、商业计划制定 | 有战略、有远见 | 慢、贵。它还在深思熟虑时,最佳时机可能已经过去 |
| 混合式(Hybrid) | 两者结合 | 现代 LLM 智能体 | 既灵活又有远见 | 架构更复杂 |
对应关系是:维度①里的"反射式"和"模型式"属于反应式,"基于目标"和"基于效用"属于规划式。
LLM 智能体为什么是混合式? 因为它的"思考-行动-观察"循环把两种模式融在了一起:
- 思考阶段(Reasoning):分析状况、规划下一步 → 这是审议过程;
- 行动+观察阶段(Acting & Observing):调工具、拿反馈 → 这是反应过程。
它把一个需要长远规划的大任务,拆成一串"规划一小步 → 反应一小步"的微循环。这就是为什么它既能应对即时变化("这家酒店太贵了"),又能完成多天行程规划。经典的混合架构是分层的(底层反应模块管紧急情况,高层规划模块管长远目标),而 LLM 智能体是一种更灵活的混合——不分层,而是在时间上交替。
维度③:知识以什么形式存在 —— 这是三个维度里最根本的一个,也是 AI 领域吵了半个多世纪的路线之争。
| 流派 | 知识长什么样 | 类比 | 强 | 弱 |
|---|---|---|---|---|
| 亚符号主义(Sub-symbolic,又叫连接主义) | 知识内隐地分布在神经网络的大量参数里,是从海量数据学到的统计模式 | 牙牙学语的孩童:看过上万张猫图就认识猫,但说不出规则 | 模式识别强,处理图像/声音这类非结构化数据,对噪声鲁棒 | 黑箱、不可解释、纯逻辑推理弱、会幻觉 |
| 符号主义(Symbolic,又叫传统 AI) | 知识是人类可读的符号(词语、概念)+ 严格的逻辑规则 | 一丝不苟的图书管理员:一切整理成规则库和知识图谱 | 透明、可解释、推理步骤可完整追溯(金融/医疗刚需) | 脆弱:依赖完备的规则体系,任何没覆盖到的新情况都可能让系统失灵——这叫"知识获取瓶颈" |
| 神经符号主义(Neuro-Symbolic) | 两者融合:既能从数据学习,又能做逻辑推理 | 完整的人 | 兼具两者之长 | 架构复杂,仍在探索 |
和"思考,快与慢"的对应:诺贝尔经济学奖得主丹尼尔·卡尼曼在《Thinking, Fast and Slow》(2011)里提出的双系统理论,给这三者提供了一个绝佳类比——
- 系统 1:快速、凭直觉、并行 → 对应亚符号主义的模式识别;
- 系统 2:缓慢、有条理、基于逻辑 → 对应符号主义的推理;
- 神经符号主义 = 让系统 1 和系统 2 协同工作。
人的智能正源于这两个系统的配合。
LLM 智能体是神经符号主义的一个极佳实践范例,而且这个论断很具体,不是套话:它的内核是一个巨大的神经网络(负责模式识别和语言生成 = 系统 1),但它工作时会生成一串结构化的中间步骤——Thought:(明确写出的思路)、Action: get_weather(city="北京")(明确的调用指令)——这些都是明确的、可操作的符号(= 系统 2)。
这解释了一个"时代之问":为什么智能体这个几十年的老概念偏偏在这几年火了? 因为在 LLM 之前,你只能选一边:要么用符号系统(可解释但脆弱),要么用神经网络(灵活但黑箱且不会推理)。LLM 第一次让"一个神经网络内核 + 明确的符号化行动"这条路真的走通了。
4.6 判断练习:这四个到底是不是智能体?
这道题(源自本章习题 1)是检验你有没有真懂三个维度的最好方式。先自己判断,再看答案。
| 主体 | 场景 | |
|---|---|---|
| A | 一台符合冯·诺依曼结构的超级计算机 | 拥有高达每秒 2 EFlop 的峰值算力 |
| B | 特斯拉自动驾驶系统 | 高速上突然检测到前方障碍物,需在毫秒级做出刹车或变道决策 |
| C | AlphaGo | 与人类棋手对弈,需评估当前局面并规划未来数十步的最优策略 |
| D | ChatGPT 扮演的智能客服 | 处理用户投诉:查订单、分析原因、给方案、安抚情绪 |
EFlop 是什么? ExaFLOPS,每秒 10^18 次浮点运算。2 EFlop 是当前顶级超算的量级。这个数字大得吓人,但它和"是不是智能体"完全无关——这正是这道题想让你踩的坑。
参考答案:
A:不是智能体。 它有算力,但没有传感器、没有执行器、没有目标、不会自主行动。它是一台等着被人喂程序的机器——算力是"能力",不是"自主性"。
一个好类比:一台顶级跑车不是赛车手。回到定义的四个零件 + 一个灵魂:它连传感器和执行器都没有,第一关就没过。 判断是不是智能体,永远先问"它自己会不会动",而不是"它有多强"。
B:是智能体,典型的反应式。 三个维度上的位置:
| 维度 | B 的位置 | 理由 |
|---|---|---|
| ① 决策架构 | 模型式反射(偏反射) | 有对周边车辆位置/速度的内部状态,但紧急刹车这一步几乎是感知直接映射到动作 |
| ② 时间与反应性 | 反应式 | 毫秒级这个约束本身就排除了深思熟虑。留给"想"的时间不够 |
| ③ 知识表示 | 亚符号主义 | 障碍物识别靠神经网络(视觉模型) |
注意:整个特斯拉自动驾驶系统作为一个整体是混合式的(导航规划路线 = 规划式,紧急避障 = 反应式,正好是经典的分层混合架构)。但题目限定的是"毫秒级刹车"这个场景,所以答反应式。这是这道题的考点:分类要看具体场景,不要看产品名。
C:是智能体,典型的规划式。
| 维度 | C 的位置 | 理由 |
|---|---|---|
| ① 决策架构 | 基于效用 + 学习 | 用价值网络给局面打效用分,追求胜率最大化;而且它是通过自我对弈学出来的 |
| ② 时间与反应性 | 规划式 | "规划未来数十步"就是字面意思的 deliberative(用蒙特卡洛树搜索推演) |
| ③ 知识表示 | 神经符号主义(偏融合) | 神经网络(策略网络+价值网络)负责直觉判断,树搜索负责符号化推演。这是神经符号结合的经典案例 |
有人会答"亚符号主义",因为看到"神经网络"就选它。但 MCTS 的搜索树是显式的符号结构——每个节点是一个明确的棋局状态。所以答神经符号更准确。
D:是智能体,典型的 LLM 智能体。
| 维度 | D 的位置 | 理由 |
|---|---|---|
| ① 决策架构 | 基于目标(+ 效用权衡) | 目标是"解决投诉";在"赔多少钱"和"用户满意度"之间做权衡时带效用色彩 |
| ② 时间与反应性 | 混合式 | 先规划"查订单→分析→给方案",每步看反馈调整。在时间上交替,不分层 |
| ③ 知识表示 | 神经符号主义 | 神经网络内核 + 显式的工具调用指令 |
D 和本章你要动手写的旅行助手是同一类东西,只是工具不同(一个查天气,一个查订单)。你写完那 100 行,就等于写了一个智能客服的骨架。
这道题的真正考点,用一句话说:判断智能体看的是"有没有自主的感知-决策-行动闭环",不是看算力、参数量或者名气。 A 算力最强,恰恰不是智能体。
5. 智能体的运行机制:一个永不停歇的循环(全书的核心骨架)
智能体不是"一锤子买卖",它靠一个循环反复运转来完成任务,术语叫智能体循环(Agent Loop):
感知 → 思考 → 行动 → 观察结果 → 再感知 → ……
拿管家做类比走一遍:
- 感知(Perception):听到你说"客人一小时后到"。(收到输入信息,术语叫"观察",Observation)
- 思考(Thought):心里盘算——先看看冰箱有什么(规划:拆解任务、定计划),决定第一步是去厨房检查食材(工具选择:挑一个最合适的手段)。
- 行动(Action):真的走去厨房打开冰箱。
- 观察(Observation):发现没有饮料——这是行动带来的新信息,成为下一轮循环的输入。
- 回到第 2 步:那我得先下楼买饮料……
如此往复,直到客人满意地喝上饮料。智能体做任何任务都是这个套路:每一轮只走一小步,靠反馈不断修正,一步步逼近目标。
6. Thought / Action / Observation:让 LLM 能"动手"的交互协议
这里有个非常关键的工程问题:LLM 本质上只会"生成文字",它没有手,怎么让它真的去调用工具?
答案是约定一种固定格式,让 LLM 的每次回答都写成两行:
Thought: 用户想知道北京的天气。我需要调用天气查询工具。
Action: get_weather(city="北京")
- Thought(思考):LLM 用大白话写出它此刻的分析和计划——相当于让它"想出声",我们能看见它的思路;
- Action(行动):它决定要执行的具体操作,写成"工具名(参数名=参数值)"的样子。
为什么参数要写成
city="北京"而不是直接"北京"? 因为带参数名的写法(关键字参数)不依赖顺序。工具有三四个参数时,模型很容易把顺序搞乱;而写了参数名,顺序错了也不影响,而且参数名写错时你能给出一句精准的报错("没有 town 这个参数,应该是 city"),模型下一轮就改对了。下面实战里的解析器就是按这个格式写的。
然后由一段普通程序(术语叫解析器,Parser,可以理解为"传令兵")盯着这个输出:一看到 Action: get_weather(city="北京"),就真的去调用查天气的函数,拿到结果后再把结果翻译成一句人话喂回给 LLM:
Observation: 北京当前天气为晴,气温25摄氏度,微风。
LLM 读到这个 Observation,开始下一轮 Thought 和 Action。
所以整个魔法可以概括成一句话:LLM 负责动脑(生成 Thought 和 Action 文字),一段普通代码负责动手(解析 Action、真正执行、把结果喂回去)。 两者一配合,"只会说话"的模型就变成了"能干活"的智能体。这个格式后面章节会正式叫它 ReAct 范式,是全书出镜率最高的概念。
7. 动手案例:5 分钟的智能旅行助手(看懂流程即可)
本章的实战部分用 Python(一种以简单易读著称的编程语言)搭了一个真实可运行的智能体,任务是:"查一下今天北京的天气,然后根据天气推荐一个景点。"
零基础的你不需要看懂每行代码,只需要看懂它由哪几块积木拼成:
- 一份说明书(系统提示词):告诉 LLM"你是旅行助手,你有两个工具可用(查天气、列景点候选),你的回答必须写成 Thought + Action 的格式,信息够了就用 Finish[最终答案] 收尾"。
- 两个工具(就是两个普通函数):
-
get_weather(city):去一个免费天气网站的"窗口"(API)要数据,拿回来整理成一句话; -get_attraction(city):返回这个城市的景点候选清单,每条都标好"室内"还是"户外"。
注意第二个工具只收一个参数
city,不收天气。 原书这里传了weather进去,我们在下面的实战里改掉了——原因见实战 §2 的"两条铁律"和实战 §5 的对照实验,这是本章最重要的一个工程判断,请务必看那两节。 一句话预告:工具一旦自己判断天气,智能体就退化成 if-else 了。 3. 一个 LLM 客户端:一小段代码,负责把消息发给大模型服务并收回复。值得一提的是,现在各家大模型服务(OpenAI、DeepSeek、本地部署的模型等)的"窗口格式"基本统一了(都兼容 OpenAI 的接口规范),所以同一套代码换个地址和密钥就能连不同的模型。 4. 一个主循环:把上面三样串起来——把历史对话发给 LLM → 收到 Thought+Action → 解析出工具名和参数 → 真正执行工具 → 把结果作为 Observation 追加到历史 → 再发给 LLM……最多循环 5 次(防止它没完没了地转圈)。
实际运行时它走了三轮,非常典型(这是下面实战里真实跑出来的输出,不是示意):
- 第 1 轮:Thought"先查天气" → Action 调
get_weather→ 得知"Sunny,26 度"; - 第 2 轮:Thought"天气知道了,去拿景点候选" → Action 调
get_attraction→ 拿到室内 4 个 + 户外 4 个; - 第 3 轮:Thought"天气晴好,可以放心安排户外行程" → Action: Finish[北京天气晴好,推荐北京城市公园或北京古城墙……]。
关键在第 3 轮:工具把室内户外都给了它,是它自己决定选户外的。换成下雨,前两轮的工具调用一模一样,第 3 轮的判断会变成室内。这就是"自主决策"的字面意思。
这个小案例展示了智能体的四项基本功:拆解任务、调用工具、理解上下文、汇总结果。而且它的核心就是"提示词 + 工具 + 循环"——市面上那些大名鼎鼎的智能体框架(LangChain 等),精髓也不过如此。
这个小案例展示了智能体的四项基本功:拆解任务、调用工具、理解上下文、汇总结果。而且它的核心就是"提示词 + 工具 + 循环"——市面上那些大名鼎鼎的智能体框架(LangChain 等),精髓也不过如此。
8. 智能体的两种用法:当工具 vs 当同事
用法一:作为人类的辅助工具
智能体嵌进人的工作流程里,人是主角,它打下手。最典型的是 AI 编程助手:GitHub Copilot(在你写代码时自动接着往下写)、Cursor(一个"出生就带 AI"的代码编辑器)、Claude Code(在命令行里用说话的方式指挥它改代码)等。这部分对不写代码的读者了解即可,记住一个趋势就行:AI 正在深入渗透各行各业的工作流,人机协同成为常态。
用法二:作为自主协作者(虚拟员工)
这是更激进的模式:你不再手把手指挥,而是把一个大目标整个委托出去,它像项目组成员一样独立规划、执行、反思,最后交付成果。你和 AI 的关系从"命令-执行"变成了"目标-委托"。
实现思路主要有三派(先混个眼熟,第六章会详细讲):
① 单智能体自主循环 —— 一个智能体反复"思考-执行-反思"直到完成目标。早期的 BabyAGI、AutoGPT 是这一派的代表。
② 多智能体协作 —— 这是当前最主流的探索方向,模拟人类团队的协作模式。它内部还能再分成两种模式,这个细分很多资料没讲清楚:
| 子模式 | 怎么组织 | 代表框架 |
|---|---|---|
| 角色扮演式对话 | 给两个智能体设定明确的角色和沟通协议(比如"程序员"和"产品经理"),让它们在一个结构化的对话里协同完成任务 | CAMEL |
| 组织化工作流 | 模拟一个分工明确的"虚拟团队"(软件公司、咨询小组)。每个智能体有预设的职责和工作流程(SOP),通过层级化或顺序化协作,产出完整代码库、研究报告这类复杂成果 | MetaGPT、CrewAI |
| 灵活对话网络 | 不预设固定 SOP,允许开发者自定义智能体之间的复杂交互网络 | AutoGen、AgentScope |
名词补课——SOP(Standard Operating Procedure,标准作业程序):就是"这类活儿该按哪几步走"的固定流程。MetaGPT 之所以能产出完整软件,是因为它把真实软件公司的 SOP(需求分析 → 写设计文档 → 写代码 → 写测试)编码进了智能体的协作流程里。这也是"多智能体"和"一个智能体自己转圈"的核心差别:不只是人数多了,而是有了分工和流程。
③ 图结构精确管控 —— 如 LangGraph,把智能体的执行过程画成一张严格的状态图,让复杂流程更可控、可回溯。
9. Workflow 和 Agent 的区别(面试和实际选型都常问)
这是本章最有实用价值的辨析。两者都是"自动化",但骨子里完全不同:
Workflow(工作流)= 按剧本演。 像公司的报销审批流程:金额小于 500 元找经理批,大于 500 元还要财务总监批——每一步、每个判断条件都是提前画好的流程图,执行时不会有任何"即兴发挥"。
Agent(智能体)= 给目标,自由发挥。 还是那个旅行助手:没有任何一行代码写着"如果晴天就推荐颐和园"。它是查到晴天后现场推理出"晴天适合户外",再自己筛选出户外景点。换成雨天,它会自己推理出该推荐博物馆。
| Workflow | Agent | |
|---|---|---|
| 核心逻辑 | 预先编排好的固定流程 | 围绕目标自主决策 |
| 灵活性 | 低,遇到没定义的情况就卡住 | 高,能应对没见过的情况 |
| 可控性 | 高,每步可预测、可审计 | 相对低,可能出错或跑偏 |
| 适合场景 | 规则明确、重复性强(审批、对账) | 开放性强、情况多变(咨询、研究、规划) |
选型的朴素直觉:流程能画清楚就用 Workflow(便宜又稳),画不清楚、变数太多才上 Agent。 实践中最常见的是两者结合。
动手实战:从空目录敲出你的第一个智能体
前面第 7 节只讲了思路,现在我们一行不落地把它敲出来。成品是一个能查天气、并据此推荐景点的命令行智能体。
这一节有两条路可走,都能跑通:
| 路线 | 需要什么 | 适合谁 |
|---|---|---|
离线路线(设一个环境变量 OFFLINE=1) |
什么都不需要,不用密钥、不用联网 | 想先看清骨架、或者还没申请到密钥的读者 |
| 联网路线(默认) | 一个大模型 API 密钥 | 想看真模型真实反应的读者 |
建议先跑离线路线——它用一个几十行的"假模型"代替真 LLM,让你能确定"如果哪里不对,一定是我代码写错了,不是模型今天心情不好"。跑通之后再切到联网路线。
0. 成品长什么样
用户输入: 帮我查一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。
========================================
--- 循环 1 ---
Thought: 用户想要根据天气选景点,我得先知道北京现在的天气。
Action: get_weather(city="北京")
Observation: 北京当前天气:Sunny,气温26摄氏度
========================================
--- 循环 2 ---
Thought: 天气已经知道了,现在去拿北京的景点候选清单。
Action: get_attraction(city="北京")
Observation: 北京的景点候选(已标注室内/户外,请自行根据天气挑选):
户外类:北京城市公园、北京古城墙、北京登山步道、北京湖边骑行道
室内类:北京历史博物馆、北京美术馆、北京水族馆、北京室内温泉
========================================
--- 循环 3 ---
Thought: 天气晴好,可以放心安排户外行程,从候选里挑户外的。
Action: Finish[北京天气晴好,推荐北京城市公园或北京古城墙,都是户外景点。]
任务完成,最终答案: 北京天气晴好,推荐北京城市公园或北京古城墙,都是户外景点。
注意第 2 轮那个 Observation。 工具返回的是两类候选都给你,没有做任何筛选。"晴天该挑户外"这个判断是模型在第 3 轮自己做的——代码里一行都没写。这是本节的设计核心,后面实战 §5 会用一个对照实验专门解释为什么必须这么设计。
1. 建项目(5 分钟)
first-agent/
├── .env ← 存密钥,绝对不能提交到 Git
├── requirements.txt ← 依赖清单
├── tools.py ← 智能体的"手":工具函数 + 工具注册表
├── llm.py ← 智能体的"嘴":跟大模型通话(含离线假模型)
└── agent.py ← 智能体的"大脑循环":主程序
名词补课——
.env文件:一个纯文本文件,每行写一个名字=值,用来存密钥这类不能写进代码的配置。程序运行时把它读进"环境变量"(操作系统给程序传参数的一种方式)。铁律:密钥只能放.env,并且.env必须写进.gitignore(告诉 Git "别管这个文件")。
requirements.txt(依赖清单,一行一个包名):
openai>=1.40.0
python-dotenv>=1.0.0
requests>=2.31.0
在项目目录下执行安装:
python3 -m venv .venv # 建一个"独立药箱",避免污染系统 Python
source .venv/bin/activate # Windows 用 .venv\Scripts\activate
pip install -r requirements.txt
名词补课——虚拟环境(venv):给每个项目单独一份 Python 包目录。不同项目依赖的版本经常冲突,虚拟环境就是给每个项目发一个独立药箱,互不干扰。看到命令行提示符前面多了
(.venv)就说明进去了。
.env 内容(三个变量,按你用的服务填):
LLM_API_KEY=sk-你的密钥
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL_ID=deepseek-chat
用智谱就填
https://open.bigmodel.cn/api/paas/v4/+glm-4-flash(有免费额度);用 ModelScope 填https://api-inference.modelscope.cn/v1/。为什么换个地址就能换模型? 因为现在各家都兼容 OpenAI 的接口规范,这是行业事实标准。走离线路线的话,
.env可以完全不建。 后面所有命令前面加OFFLINE=1就行(Windows PowerShell 用$env:OFFLINE=1单独设一次)。
2. tools.py —— 给智能体装上"手"(25 分钟)
工具就是普通的 Python 函数,没有任何魔法。但这一节要立两条铁律,它们决定了你的智能体到底是真智能体还是伪装的 if-else:
| 铁律 | 说明 |
|---|---|
| 返回值必须是人话 | 因为它要喂回给模型看。返回 {"code":200,"data":{...}} 模型也能勉强读,但返回一句自然语言它读得又快又准 |
| 工具只取事实,不做决策 | 天气工具只负责"告诉你现在下雨",绝不能替模型决定"所以该去博物馆"。判断留给模型——这才是"自主性" |
第二条铁律是最容易被违背的,而且违背了你自己都察觉不到。后面实战 §5 有一个专门的对照实验。
第 1 段:工具注册表
"""智能体的工具箱:每个工具就是一个普通函数,返回一句人话。
关键设计:工具只负责"取事实",不负责"做决策"。
"""
import os
import requests
OFFLINE = os.getenv("OFFLINE", "0") == "1"
TOOL_REGISTRY = {}
def register_tool(name: str, description: str, func):
"""把一个函数登记为工具。描述会自动出现在系统提示词里"""
TOOL_REGISTRY[name] = {"description": description, "func": func}
def get_available_tools() -> str:
"""生成给模型看的工具清单。唯一数据源就是 TOOL_REGISTRY"""
return "\n".join(f"- {info['description']}"
for info in TOOL_REGISTRY.values())
def call_tool(name: str, **kwargs) -> str:
return TOOL_REGISTRY[name]["func"](**kwargs)
为什么要费劲搞一个注册表,不直接写个字典
{"get_weather": get_weather}?因为工具信息有两个消费者:① 提示词里的工具清单(给模型看的说明书);② 派发用的名字→函数映射(给代码用的)。如果分开维护,你加第三个工具时很容易只改一处——比如在提示词里写了
search_hotel,但忘了往字典里加,模型信心满满地调用,然后收到"没有这个工具"。这是新手最高频的 bug 之一,而且报错信息完全不指向真正的原因。注册表让描述和函数在同一行代码里登记,
get_available_tools()从注册表自动生成清单。一处登记,两处受益,不可能不一致。 这个设计在第七章会长成完整的工具管理器。
os.getenv("OFFLINE", "0") == "1":读环境变量,没设就当"0"。这就是离线开关。
第 2 段:工具一,查天气
def get_weather(city: str) -> str:
"""查真实天气。用免费的 wttr.in,不需要密钥"""
if OFFLINE:
fake = {"北京": "Sunny,气温26摄氏度", "上海": "Light rain,气温19摄氏度"}
return f"{city}当前天气:{fake.get(city, 'Cloudy,气温22摄氏度')}(离线模拟)"
try:
resp = requests.get(f"https://wttr.in/{city}?format=j1", timeout=10)
resp.raise_for_status()
cur = resp.json()["current_condition"][0]
return (f"{city}当前天气:{cur['weatherDesc'][0]['value']},"
f"气温{cur['temp_C']}摄氏度")
except requests.exceptions.RequestException as e:
return f"查询天气失败(网络问题):{e}"
except (KeyError, IndexError):
return f"解析天气数据失败,城市名「{city}」可能无效,请换用拼音或英文名"
为什么每个工具都要
try/except? 这是专家习惯:工具是最容易挂的一环(网断、对方限流、城市名拼错)。工具挂了不能让整个智能体崩,而要返回一句模型能读懂的错误,让它自己决定换个参数重试还是降级作答。docs/2 第 16 篇把这条列为坑 6。为什么
timeout=10很重要? 不写超时,对方服务器不响应时你的程序会一直傻等,整个智能体卡死。所有网络请求都必须带超时,一个例外都不能有。注意两个
except分得很细:网络挂了和"数据回来了但结构不对"是两种完全不同的故障,给模型的提示也完全不同——前者"重试可能就好了",后者"你的城市名有问题,换个写法"。报错信息越具体,模型自我纠错的成功率越高。
第 3 段:工具二,查景点(本节的关键设计)
CATALOG = {
"户外": ["城市公园", "古城墙", "登山步道", "湖边骑行道"],
"室内": ["历史博物馆", "美术馆", "水族馆", "室内温泉"],
}
def get_attraction(city: str) -> str:
"""返回该城市的景点候选,每个都标注室内/户外。
注意:这个函数里没有任何一行代码判断天气 —— 选哪个是模型的活。
"""
lines = [f" {tag}类:" + "、".join(f"{city}{p}" for p in items)
for tag, items in CATALOG.items()]
return (f"{city}的景点候选(已标注室内/户外,请自行根据天气挑选):\n"
+ "\n".join(lines))
仔细看这个函数的签名:
get_attraction(city),只有一个参数,没有weather。一个很自然的写法是让工具收下天气、自己判断:
```python
❌ 看着方便,实则毁掉了智能体
def get_attraction(city, weather): bad = any(k in weather for k in ("雨", "雪", "Rain", "Snow")) picks = indoor if bad else outdoor # ← 决策在这里,不在模型 return ... ```
这么写,程序确实能跑,输出看起来也对。但"下雨就去室内"这个判断是这一行
if做的,模型只是个传话筒。 你以为在演示自主决策,其实演示的是一个 if-else 加了个花哨的外壳。换成"雾霾很重"或"38 度高温"呢?你的if里没有这两个词,它就会推荐户外——而真模型本来是能判断的。正确做法就是上面那样:工具把两类候选都摆出来并标好标签,让模型自己挑。 这样"雾霾去室内""高温去有空调的地方""刚下过雨路滑别爬山"这些判断,模型全都能做——因为这些常识本来就在它的参数里,你只要别拦着它用。
一句话记住:凡是需要"看情况"的判断,都留给模型;工具只做"取事实"这种不需要判断的事。
第 4 段:登记工具 + 自测
register_tool("get_weather",
'get_weather(city: str) —— 查询指定城市的实时天气。'
'例:get_weather(city="北京")',
get_weather)
register_tool("get_attraction",
'get_attraction(city: str) —— 列出指定城市的景点候选,每条都标注了'
'"室内"或"户外"。例:get_attraction(city="北京")',
get_attraction)
if __name__ == "__main__":
print(get_weather("北京"))
print(get_attraction("北京"))
print("\n--- 注册表自动生成的工具清单 ---")
print(get_available_tools())
工具描述里为什么要带一个调用示例? 因为描述是模型选择和调用这个工具的唯一依据。带上
例:get_weather(city="北京"),模型照抄就不会把参数名写成town。这是最便宜的准确率提升手段,几个 token 换来格式正确率的显著提高。第四章会把这条上升为"工具三要素"。
运行 OFFLINE=1 python tools.py。
✅ Checkpoint 1 —— 实际运行输出:
北京当前天气:Sunny,气温26摄氏度(离线模拟)
北京的景点候选(已标注室内/户外,请自行根据天气挑选):
户外类:北京城市公园、北京古城墙、北京登山步道、北京湖边骑行道
室内类:北京历史博物馆、北京美术馆、北京水族馆、北京室内温泉
--- 注册表自动生成的工具清单 ---
- get_weather(city: str) —— 查询指定城市的实时天气。例:get_weather(city="北京")
- get_attraction(city: str) —— 列出指定城市的景点候选,每条都标注了"室内"或"户外"。例:get_attraction(city="北京")
去掉 OFFLINE=1 再跑一次,第一行会变成真实天气(需要联网)。报网络错误也不影响后面——那正是我们做离线路线的原因。
3. llm.py —— 给智能体装上"嘴"(20 分钟)
这个文件里藏着一个对学习效率影响极大的东西:MockLLM(假模型)。
为什么要写一个假模型? 因为调试智能体时你会遇到三类故障:代码写错了、提示词写得不好、模型今天不听话。如果三类混在一起,你会被折磨很久。 假模型的作用是把第三类彻底排除:它的输出是确定的,所以只要输出不对,一定是你的代码问题。
第 1 段:两种模式的开关
"""与大模型通话的唯一入口。全项目只在这里碰 API。
设 OFFLINE=1 时走 MockLLM,不需要密钥、不需要网络。
"""
import os
import re
OFFLINE = os.getenv("OFFLINE", "0") == "1"
if not OFFLINE:
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
_client = OpenAI(api_key=os.getenv("LLM_API_KEY"),
base_url=os.getenv("LLM_BASE_URL"))
MODEL = os.getenv("LLM_MODEL_ID")
import为什么写在if里面? 这样离线模式下根本不需要装openai和python-dotenv。Python 允许在任何地方 import,这是个实用技巧:把可选依赖的 import 放进条件分支,用不到就不装。
第 2 段:假模型
def _mock(history: str) -> str:
"""假模型:用几条规则模仿"真模型在这一步会说什么"。
它只看历史里已经有了什么 Observation,来决定下一句说什么。
"""
city_m = re.search(r"(北京|上海|广州|杭州|厦门)", history)
city = city_m.group(1) if city_m else "北京"
if "当前天气" not in history:
return (f"Thought: 用户想要根据天气选景点,我得先知道{city}现在的天气。\n"
f'Action: get_weather(city="{city}")')
if "景点候选" not in history:
return (f"Thought: 天气已经知道了,现在去拿{city}的景点候选清单。\n"
f'Action: get_attraction(city="{city}")')
rainy = re.search(r"(雨|雪|Rain|Snow)", history, re.IGNORECASE)
if rainy:
return (f"Thought: 天气里出现了「{rainy.group(1)}」,户外会淋雨,"
f"所以我从候选里挑室内的。\n"
f"Action: Finish[{city}目前有雨,不适合户外。建议去"
f"{city}历史博物馆或{city}美术馆,都是室内场馆。]")
return (f"Thought: 天气晴好,可以放心安排户外行程,从候选里挑户外的。\n"
f"Action: Finish[{city}天气晴好,推荐{city}城市公园或"
f"{city}古城墙,都是户外景点。]")
看清假模型的判断依据:它只看"历史里有没有出现某个字符串"。
- 历史里没有"当前天气"→ 说明还没查过天气 → 去查天气;
- 有天气但没有"景点候选"→ 去查景点;
- 两个都有了 → 根据天气里有没有"雨/雪"给出最终答案。
这就是在模仿真模型的行为逻辑:真模型也是看完整历史来决定下一步的,只不过它靠理解,假模型靠
if "xxx" in history。必须诚实说明一件事:假模型里的"下雨挑室内"是这几行
if写死的——它是个 Workflow,不是 Agent。 我们用它来验证代码骨架(循环、解析、派发都对不对),而"自主决策"这个能力必须切到真模型才能真正验证。实战 §5 的对照实验会讲清楚怎么验证。
第 3 段:统一入口
def chat(system_prompt: str, user_prompt: str, temperature: float = 0.0) -> str:
"""把一轮对话发给模型,返回它的回复文本"""
if OFFLINE:
return _mock(user_prompt)
resp = _client.chat.completions.create(
model=MODEL,
messages=[{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}],
temperature=temperature,
)
return resp.choices[0].message.content
if __name__ == "__main__":
if OFFLINE:
print(chat("", "用户请求: 帮我看看北京天气再推荐景点"))
print("---")
print(chat("", "用户请求: 上海\n上海当前天气:Light rain(离线模拟)\n景点候选"))
else:
print(chat("你是一个只会说四个字的助手。", "你好"))
关键在于
chat()的签名两种模式完全一样。agent.py里只写from llm import chat,它根本不知道背后是真模型还是假模型。这个技巧叫"依赖倒置"——上层代码依赖的是接口(函数签名),不是具体实现。换实现不用改上层一行代码。
运行 OFFLINE=1 python llm.py。
✅ Checkpoint 2 —— 实际运行输出:
Thought: 用户想要根据天气选景点,我得先知道北京现在的天气。
Action: get_weather(city="北京")
---
Thought: 天气里出现了「rain」,户外会淋雨,所以我从候选里挑室内的。
Action: Finish[上海目前有雨,不适合户外。建议去上海历史博物馆或上海美术馆,都是室内场馆。]
第一次调用历史里什么都没有,所以它决定先查天气;第二次调用我们手动喂了一段"已经查过天气和景点"的假历史,而且天气里有 rain,所以它直接给出室内推荐。这两次输出验证了假模型的状态判断是对的。
联网路线的验收:python llm.py 能打印出模型的回复。报 401 就是密钥错了,报连接错误就是 LLM_BASE_URL 写错了(注意大多数服务的地址要带 /v1)。
名词补课——
system/user角色:发给模型的消息带角色标签。system是"岗前培训手册"(身份、规则、输出格式),权重最高、模型最当真;user是这一轮用户说的话。人设和格式要求永远放system。为什么
temperature=0.0? 这个智能体需要模型严格按Thought:/Action:格式输出,不需要任何创意。要格式稳定就把温度调到 0,这是通用经验。温度这个参数的原理见第三章。
4. agent.py —— 大脑循环(40 分钟,本章灵魂)
这是整章最核心的一段。它做的事就是把"感知 → 思考 → 行动 → 观察"这个圈用一个 for 循环转起来。
第 1 段:提示词与保险丝
"""智能体主程序:感知 → 思考 → 行动 → 观察,循环往复"""
import re
from llm import chat
from tools import TOOL_REGISTRY, call_tool, get_available_tools
SYSTEM_PROMPT = f"""你是一个智能旅行助手。你的任务是分析用户请求,并使用可用工具一步步解决问题。
# 可用工具
{get_available_tools()}
# 输出格式要求
你的每次回复必须严格遵循以下格式,只包含一对 Thought 和 Action:
Thought: [你的思考过程和下一步计划]
Action: [你要执行的具体行动]
Action 必须是以下两种之一:
1. 调用工具: function_name(arg_name="arg_value")
2. 结束任务: Finish[最终答案]
# 重要提示
- 每次只输出一对 Thought-Action,不要预测 Observation
- 调用工具的 Action 必须写在同一行
- get_attraction 返回的候选已标注"室内"/"户外",请你自己结合天气挑选
- 收集到足够信息后,必须用 Action: Finish[最终答案] 结束
"""
MAX_STEPS = 5 # 保险丝:最多转 5 圈,防止无限循环烧钱
注意
SYSTEM_PROMPT前面那个f,以及中间的{get_available_tools()}。 工具清单是从注册表自动生成的,不是手抄的。这样你加第三个工具时只改tools.py一处,提示词自动跟着更新。这是本节两处"防止不一致"设计里更重要的那一处。手抄工具清单的后果:你在
tools.py里把参数从weather改成了没有,但提示词里还写着get_attraction(city, weather),模型照着提示词调用,收到"参数不对"的报错——而你会去查模型为什么不听话,完全想不到是提示词过时了。为什么
MAX_STEPS是必须的? 模型可能永远不说Finish,或者在两个工具之间来回打转。每一轮都是一次真实的 API 调用,都在花钱。保险丝不是"以防万一",是"一定会用上"。 这条在第四章会升级成更精细的终止条件。
第 2 段:主循环(本章灵魂的灵魂)
def run(user_input: str) -> str:
history = [f"用户请求: {user_input}"]
print(f"用户输入: {user_input}\n" + "=" * 40)
for step in range(1, MAX_STEPS + 1):
print(f"--- 循环 {step} ---")
# ① 思考:把完整历史发给模型
output = _keep_first_pair(chat(SYSTEM_PROMPT, "\n".join(history)))
print(output) # ← 把 Thought 打出来,别省这一行
history.append(output)
# ② 解析:从文字里抠出 Action
action = _extract_action(output)
if action is None:
history.append("Observation: 错误:未解析到 Action 字段,"
"请严格按 'Thought: ... Action: ...' 格式回复。")
print(history[-1] + "\n" + "=" * 40)
continue
# ③ 收尾判断
if action.startswith("Finish"):
m = re.match(r"Finish\[(.*)\]", action, re.DOTALL)
final = m.group(1) if m else action[6:].strip("[] ")
print(f"任务完成,最终答案: {final}")
return final
# ④ 行动:真正调用工具
observation = _execute(action)
history.append(f"Observation: {observation}")
print(f"Observation: {observation}\n" + "=" * 40)
fail = "抱歉,我在限定步数内没能完成任务。" # ← 保险丝烧了走这里
print(fail)
return fail
这 30 行就是"感知 → 思考 → 行动 → 观察"的字面实现,对应关系:
循环里的代码 对应哪一步 chat(SYSTEM_PROMPT, "\n".join(history))思考:把当前全部认知交给模型 _extract_action(output)解析:从文字里找出它想干什么 _execute(action)行动:真的去调函数 history.append(f"Observation: {observation}")观察:结果进入认知,下一轮它就知道了
print(output)这一行千万别省。 它把模型的Thought:打在屏幕上,是你唯一能看见模型思路的地方。出问题时它直接告诉你模型是"想错了"还是"想对了但格式写错了"——这两种故障的修法完全不同(前者改提示词,后者改解析器)。很多人调试半天没头绪,就是因为没打 Thought。
history为什么每轮都全量发送? 因为模型是无状态的:它不记得上一轮说过什么,你必须每次把完整历史重发一遍。这直接导致成本随轮数线性增长,是第九章上下文工程要解决的核心问题。
第 3 段:三个辅助函数
def _extract_action(output: str):
"""抠出 Action。Finish[...] 可能跨行,所以先用 DOTALL 抓全部,
再判断:是 Finish 就整段留下,是工具调用就只取第一行。"""
m = re.search(r"Action:\s*(.*)", output, re.DOTALL)
if not m:
return None
rest = m.group(1).strip()
return rest if rest.startswith("Finish") else rest.split("\n")[0].strip()
def _keep_first_pair(text: str) -> str:
"""模型有时会一口气编出好几轮(连 Observation 都替你编了),只保留第一对"""
m = re.search(r"(Thought:.*?Action:.*?)(?=\n\s*(?:Thought:|Observation:)|\Z)",
text, re.DOTALL)
return m.group(1).strip() if m else text.strip()
def _execute(action: str) -> str:
"""把 'get_weather(city="北京")' 这段文字变成真正的函数调用"""
name_m = re.search(r"(\w+)\s*\(", action)
if not name_m:
return (f"错误:无法识别的 Action 格式「{action}」,"
f'正确写法如 get_weather(city="北京")')
tool_name = name_m.group(1)
if tool_name not in TOOL_REGISTRY:
return f"错误:没有名为「{tool_name}」的工具,可用工具:{list(TOOL_REGISTRY)}"
args_str = re.search(r"\((.*)\)", action, re.DOTALL).group(1)
kwargs = dict(re.findall(r'(\w+)\s*=\s*"([^"]*)"', args_str))
try:
return call_tool(tool_name, **kwargs)
except TypeError as e:
return f"错误:调用「{tool_name}」的参数不对({e}),请检查参数名"
if __name__ == "__main__":
import sys
q = sys.argv[1] if len(sys.argv) > 1 else \
"帮我查一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。"
run(q)
_extract_action为什么要分两种情况处理? 这是一个真实踩出来的坑。工具调用必须在一行(
get_weather(city="北京")),所以取第一行就够;但最终答案经常是多行的——模型给出一段有换行的推荐理由。如果统一只取第一行,Finish[北京天气晴好,\n推荐城市公园……]会被截成Finish[北京天气晴好,,答案残缺,而且没有任何报错。所以先用
re.DOTALL(让.也能匹配换行)把Action:后面全部抓下来,再看开头是不是Finish:是就整段留着,不是就只取第一行。分开处理的成本是两行代码,统一处理的代价是答案被静默截断。
_execute里三种错误都返回给模型而不是抛异常,这是设计要点:模型收到"没有名为 get_weathers 的工具,可用工具:[...]"之后,下一轮就会自己改对。这就是自我纠错的机制——它需要的不是完美的代码,而是有信息量的报错。
第 4 段:跑起来
OFFLINE=1 python agent.py
✅ Checkpoint 3(晴天路径) —— 实际运行输出:
用户输入: 帮我查一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。
========================================
--- 循环 1 ---
Thought: 用户想要根据天气选景点,我得先知道北京现在的天气。
Action: get_weather(city="北京")
Observation: 北京当前天气:Sunny,气温26摄氏度(离线模拟)
========================================
--- 循环 2 ---
Thought: 天气已经知道了,现在去拿北京的景点候选清单。
Action: get_attraction(city="北京")
Observation: 北京的景点候选(已标注室内/户外,请自行根据天气挑选):
户外类:北京城市公园、北京古城墙、北京登山步道、北京湖边骑行道
室内类:北京历史博物馆、北京美术馆、北京水族馆、北京室内温泉
========================================
--- 循环 3 ---
Thought: 天气晴好,可以放心安排户外行程,从候选里挑户外的。
Action: Finish[北京天气晴好,推荐北京城市公园或北京古城墙,都是户外景点。]
任务完成,最终答案: 北京天气晴好,推荐北京城市公园或北京古城墙,都是户外景点。
✅ Checkpoint 4(雨天路径) —— 换个城市:
OFFLINE=1 python agent.py "上海今天天气怎么样,推荐个景点"
--- 循环 1 ---
Thought: 用户想要根据天气选景点,我得先知道上海现在的天气。
Action: get_weather(city="上海")
Observation: 上海当前天气:Light rain,气温19摄氏度(离线模拟)
========================================
--- 循环 2 ---
Thought: 天气已经知道了,现在去拿上海的景点候选清单。
Action: get_attraction(city="上海")
Observation: 上海的景点候选(已标注室内/户外,请自行根据天气挑选):
户外类:上海城市公园、上海古城墙、上海登山步道、上海湖边骑行道
室内类:上海历史博物馆、上海美术馆、上海水族馆、上海室内温泉
========================================
--- 循环 3 ---
Thought: 天气里出现了「rain」,户外会淋雨,所以我从候选里挑室内的。
Action: Finish[上海目前有雨,不适合户外。建议去上海历史博物馆或上海美术馆,都是室内场馆。]
任务完成,最终答案: 上海目前有雨,不适合户外。建议去上海历史博物馆或上海美术馆,都是室内场馆。
注意第 2 轮的 Observation 和晴天那次完全一样——工具把室内室外都给了。是第 3 轮的判断变了。 这就是我们要的结构:工具中立,决策在上层。
✅ Checkpoint 5(保险丝) —— 把 MAX_STEPS 改成 1:
--- 循环 1 ---
Thought: 用户想要根据天气选景点,我得先知道北京现在的天气。
Action: get_weather(city="北京")
Observation: 北京当前天气:Sunny,气温26摄氏度(离线模拟)
========================================
抱歉,我在限定步数内没能完成任务。
保险丝有效。 注意它是优雅退出(返回一句话),不是崩溃——生产环境里这句话应该变成"任务未完成,已执行的步骤是……",让上层能接手。
✅ Checkpoint 6(三种错误路径) —— 直接测 _execute。新建一个 test_errors.py:
from agent import _execute
print(_execute('get_weathers(city="北京")'))
print(_execute('get_weather(town="北京")'))
print(_execute('随便瞎写'))
OFFLINE=1 python test_errors.py
错误:没有名为「get_weathers」的工具,可用工具:['get_weather', 'get_attraction']
错误:调用「get_weather」的参数不对(get_weather() got an unexpected keyword argument 'town'),请检查参数名
错误:无法识别的 Action 格式「随便瞎写」,正确写法如 get_weather(city="北京")
三种错误都带着"怎么改"的信息。 这三行输出决定了你的智能体遇到问题时是能自己爬起来,还是原地卡死。
✅ 最终验收(全过 = 通关):
- [ ]
OFFLINE=1 python tools.py输出天气 + 两类景点候选 + 自动生成的工具清单(Checkpoint 1) - [ ]
OFFLINE=1 python llm.py输出两段 Thought/Action(Checkpoint 2) - [ ]
OFFLINE=1 python agent.py在 3 轮内输出任务完成,最终答案: ...(Checkpoint 3) - [ ] 换成上海,第 2 轮的 Observation 和北京那次结构完全一样,但第 3 轮的判断从户外变成了室内(Checkpoint 4)
- [ ] 把
MAX_STEPS改成 1,确认输出"没能完成任务"——保险丝是有效的(Checkpoint 5) - [ ] 三种错误 Action 都返回带修改建议的错误文本,程序不崩溃(Checkpoint 6)
- [ ] 跑一遍反例函数,看到"Light rain"也被判成户外(Checkpoint 7)——这是硬编码规则失效的铁证
- [ ]
grep -n "室内" tools.py只有 4 处命中:CATALOG里的分类键、函数注释、返回给模型的提示文字、注册时的工具描述。没有任何一处是if—— 工具里没有一行代码判断天气
关于"自主决策"这一条要说清楚:上面 6 个 Checkpoint 全过,证明的是骨架正确——循环、解析、派发、保险丝、错误恢复都对。但它们证明不了自主决策,因为离线假模型里那句"下雨挑室内"本身就是几行
if。要验证自主决策,必须做下一节的对照实验(实战 §5),那里需要真模型。 这是本章唯一一个离线跑不出来的能力,也恰好是"Agent"这个词的全部含义所在,所以单独拎出来讲。
5. 一个必做的对照实验:验证"自主决策"是真的
前面的离线路线有个诚实的局限:假模型里的"下雨挑室内"是 if 写死的,所以离线跑通只能证明骨架对,不能证明自主决策。这一小节告诉你怎么真正验证。
准备:切到联网路线(去掉 OFFLINE=1,配好 .env)。
实验 A:天气工具里出现一个你没写过的词
把 get_weather 临时改成固定返回一个词表里绝对没有的天气:
def get_weather(city: str) -> str:
return f"{city}当前天气:重度雾霾,AQI 280,能见度不足500米"
跑一次。观察它推荐室内还是户外。
- 如果你用的是原来那个有
weather参数、内部if "雨" in weather的工具:"重度雾霾"里既没有"雨"也没有"雪",bad是False,于是它推荐户外——让用户在 AQI 280 的天里去爬山。 - 用本节这个中立工具:模型看到"重度雾霾,AQI 280",会自己判断出该待在室内。这个判断你一行代码都没写过。
这个对照就是"Agent"和"披着 Agent 皮的 Workflow"的分界线。
实验 B:换一个更刁的情况
return f"{city}当前天气:晴,气温39摄氏度,发布高温红色预警"
"晴"是好天气,if 一定判它户外。但真模型会告诉你 39 度别去爬山,去有空调的博物馆或水族馆。
实验 C:把那个 if 单独拎出来跑,亲眼看它错
不用联网也能做这个。把反例函数抄进一个文件跑一遍:
CATALOG = {"户外": ["城市公园", "古城墙"], "室内": ["历史博物馆", "美术馆"]}
def bad_get_attraction(city, weather): # ❌ 反例:工具自己做决策
bad = any(k in weather for k in ("雨", "雪", "Rain", "Snow"))
picks = CATALOG["室内"] if bad else CATALOG["户外"]
return ("室内" if bad else "户外") + ":" + "、".join(f"{city}{p}" for p in picks)
print(bad_get_attraction("北京", "北京当前天气:重度雾霾,AQI 280,能见度不足500米"))
print(bad_get_attraction("北京", "北京当前天气:晴,气温39摄氏度,发布高温红色预警"))
print(bad_get_attraction("上海", "上海当前天气:Light rain,气温19摄氏度"))
✅ Checkpoint 7 —— 实际运行输出:
户外:北京城市公园、北京古城墙
户外:北京城市公园、北京古城墙
户外:上海城市公园、上海古城墙
三行全是"户外",包括第三行。 前两行我们预料到了(雾霾、高温不在关键词表里)。第三行才是最扎心的:那明明是下雨。
为什么?因为关键词表里写的是 "Rain"(大写 R),真实天气数据里是 "Light rain"(小写 r)。"Rain" in "Light rain" 是 False。一个大小写就让整个判断失效,而且程序不报错、返回值格式完全正常。
这就是硬编码规则的真实成本。 它不是"覆盖不全"这么温和的问题——它是静默地给出错误答案。你要维护的不只是"有多少种天气",还有"每种天气有多少种拼法、大小写、语言"。而模型看到
Light rain就知道那是下雨,不需要你列清单。
结论:你的 if 能覆盖多少种情况,取决于你想得起多少种情况;模型能覆盖多少种,取决于人类写下来过多少种。 后者大得多。所以——凡是"看情况"的判断,交给模型。
反过来也要清楚边界:凡是必须百分之百确定的判断(金额能不能退款、这个操作有没有权限),绝对不能交给模型,要写成硬规则。这正是本章最后"Workflow vs Agent"那一节的实践含义:要不要确定性,决定了这个判断该由代码还是模型来做。
6. 五个正则各自在干什么(零基础必看)
代码里的正则表达式(用来在文本里找模式的小语言)是这份代码唯一的门槛,逐个拆开:
| 正则 | 作用 | 拆解 |
|---|---|---|
Action:\s*(.*) + re.DOTALL |
抠出 Action 后面的全部内容 | \s*=若干空白;(.*)=捕获后面所有字符;re.DOTALL 让 . 也能匹配换行,不加它多行的 Finish[...] 会被截断 |
Finish\[(.*)\] |
抠出最终答案 | \[ \] 是转义后的真方括号(方括号在正则里有特殊含义) |
(\w+)\s*\( |
抠出工具名 | \w+=一串字母数字下划线;后面紧跟左括号 |
\((.*)\) |
抠出括号里的参数串 | 同样配合 re.DOTALL |
(\w+)\s*=\s*"([^"]*)" |
把 city="北京" 变成键值对 |
[^"]*=除引号外的任意字符,即引号内的内容 |
re.findall 会找出所有匹配并返回列表,所以 dict(re.findall(...)) 一步就把多个参数变成了 {"city": "北京"}。
re.DOTALL是这五个里最容易漏的一个。 漏了它,程序不报错,只是最终答案被静默截断——你以为模型答得不完整,其实是你的正则把后半截扔了。这类"不报错但结果错"的 bug 最难查,所以单独标出来。
7. 踩坑与专家提示
| 现象 | 根因 | 解法 |
|---|---|---|
| 模型一口气把 Observation 也编出来了 | 它在"续写"而不是"对话" | _keep_first_pair 截断;system 里明说"不要预测 Observation" |
| 最终答案被截了一半 | 解析 Action 时漏了 re.DOTALL |
见上表;Finish 分支要整段保留 |
一直不用 Finish,循环到上限 |
没意识到该收尾 | system 里强调收尾格式;必要时在最后一轮追加"请立即用 Finish 给出答案" |
| 同一个工具反复调用 | 工具返回值它看不懂 | 让工具返回人话;检测到连续相同调用就注入"请勿重复调用" |
| 参数名对不上 | 模型编了参数名 | 捕获 TypeError 后把正确参数名回喂(代码里已实现);工具描述里带调用示例 |
| 提示词里的工具清单和真实工具不一致 | 两处手工维护 | 用注册表自动生成清单(本节已实现) |
| 明明是 Agent 却表现得像 if-else | 决策逻辑写进了工具里 | 工具只取事实,判断交给模型;用实战 §5 的对照实验验证 |
| 调试时完全看不出模型在想什么 | 没打印 Thought | print(output),一行代码的事 |
| 每轮都把全部历史发过去,越来越贵 | 上下文线性膨胀 | 本章规模小无所谓;真实项目要截断/摘要,见第九章 |
专家视角:这不到 100 行就是所有 Agent 框架的内核。LangChain、AutoGPT 再花哨,拆开也是"提示词 + 工具注册表 + 解析器 + 循环 + 保险丝"这五件套。你已经手写过一遍,后面学任何框架都只是在认它的零件名。
安全提示:本节的
_execute是用正则解析参数、再用**kwargs调用注册表里已有的函数。绝对不要图省事写成eval(action)——那等于把任意代码执行权交给了模型的输出。模型的输出是不可信文本(它可能被用户的输入诱导),eval会让__import__("os").system("rm -rf ~")这类字符串真的执行。规则:凡是要把模型输出变成可执行动作,永远走"白名单查表"这一条路。
8. 面试怎么讲(实战部分)
"我不用框架手写过一个 ReAct 风格的智能体:用 system prompt 约束模型输出 Thought/Action 两段式,主循环用正则解析 Action 并派发到工具注册表,执行结果作为 Observation 追加进历史再喂回模型,用 max_steps 做保险丝防止死循环。工具层每个函数独立 try/except 且带超时,失败时返回自然语言错误让模型自行降级或改参数——解析失败、工具挂掉、模型不守格式这三类故障都有兜底。
有两个设计我会特别强调。一是工具只取事实、不做决策:早期版本我让天气工具自己判断'下雨就返回室内景点',结果遇到'重度雾霾'这种关键词表里没有的情况就推荐户外了——本质上退化成了 if-else。改成'工具返回带室内/户外标签的全部候选、由模型挑选'之后,雾霾、高温预警这些我没预料到的情况模型都能正确处理。二是工具清单从注册表自动生成,避免提示词和实际工具不一致——这类 bug 的报错完全不指向真正的原因,很难查。"
顺带要懂的背景知识
PEAS 模型:设计智能体前的"填表工具",逼你想清楚四件事——性能度量(Performance,怎么算干得好)、环境(Environment,它在什么世界里干活)、执行器(Actuators,它能做什么动作)、传感器(Sensors,它能看到什么信息)。类比招聘:写岗位 JD 时要先写清楚考核标准、工作场所、工作内容、能接触到的资源。
把本章的旅行助手填进 PEAS(这张表建议你自己动手填一遍,比看十遍定义有用):
| PEAS | 旅行助手填什么 | 为什么这么填 |
|---|---|---|
| P 性能度量 | ① 推荐的景点符不符合当前天气 ② 用了几轮循环(越少越省钱) ③ 有没有在 MAX_STEPS 内收尾 ④ 用户满意度 |
注意 P 不只有"对不对",还有"多快多省"。只盯准确率会做出一个又贵又慢的智能体 |
| E 环境 | 互联网(天气 API、景点库)+ 用户的对话 | 这个环境是部分可观察的(只能看到 API 返回的那点数据)、随机的(天气随时变)、动态的(你思考时世界在变) |
| A 执行器 | get_weather、get_attraction、以及"输出最终答案"这个动作 |
Finish 也是一个执行器,很多人漏掉它。不能收尾的智能体等于不能交付 |
| S 传感器 | 工具返回的 Observation 文本 + 用户输入 | 它的全部世界就是这些文字。 所以工具返回值写得清不清楚,直接决定它聪不聪明 |
PEAS 最大的实用价值在 S 和 A 这两栏:填完你会立刻发现"我想让它做的事,它其实看不到(S 缺)或做不到(A 缺)"。比如你想让它推荐"人少的景点"——但没有任何工具能返回人流量,S 里就没这个信息,它只能编。这就是幻觉的一大来源:要求超出了传感器能力。
环境的四个麻烦特性(智能体设计难,难就难在这里):
- 部分可观察:看不到全貌。旅行助手查机票只能看到 API 返回的那部分数据,就像你从猫眼看门外——所以它需要记忆和多次探索。
- 随机性:同样的动作结果可能不同。这一秒查的票价,下一秒可能就变了。
- 多智能体:环境里还有别的"玩家"。别的用户可能抢走最后一张特价票。
- 序贯且动态:每一步都影响后面,而且你思考的时候世界还在变。
这四条不是学术分类,是四个具体的工程麻烦: 部分可观察 → 你需要给它记忆和多轮探索的能力(第八章);随机性 → 同一个问题跑两次结果可能不同,所以你不能用"跑一次对了"来证明它可靠(第十一章评估);多智能体 → 它拿到的信息可能已经过期;序贯且动态 → 它规划到第 5 步时,第 1 步的前提可能已经不成立了,所以必须能中途改计划(第四章)。后面十一章的很多设计,追根溯源都是在对付这四条。
反应式 / 规划式 / 混合式、符号主义 / 亚符号主义 / 神经符号主义 —— 这两组都是"三个分类维度"里的维度 ② 和 ③,已在上面 4.5 节详细展开(含对比表和卡尼曼双系统的对应关系),此处不再重复。如果你跳着读,那一节请务必回去看,它是本章面试题密度最高的地方。
了解即可的内容
- 各种 AI 编程工具的产品介绍(Copilot、Cursor、Trae、Claude Code 的功能差异):属于产品科普,知道"有这么一类工具"就行。
- 各智能体框架的名字大全(BabyAGI、AutoGPT、CrewAI、CAMEL、AutoGen、AgentScope、MetaGPT、LangGraph):现在不用记,第五、六章会挑重点实操。只需记住第 8 节那张三分法表(角色扮演式对话 / 组织化工作流 / 灵活对话网络),框架名到时候能挂上去就行。
剩下两道习题的答案(都是高频面试题)
习题 2:给"智能健身教练"填 PEAS。 自己先填,然后对照:
| PEAS | 填什么 |
|---|---|
| P 性能度量 | 目标达成率(减脂/增肌/耐力各有指标)、动作纠正的准确率、用户依从性(还愿不愿意练)、安全性(有没有导致受伤或心率超危险区) |
| E 环境 | 用户的身体 + 训练场地 + 时间安排 |
| A 执行器 | 语音指导、调整训练计划、生成饮食建议、必要时叫停训练 |
| S 传感器 | 可穿戴设备的心率/运动强度数据、摄像头(动作识别)、用户主观反馈 |
环境特性:部分可观察(心率能测,疲劳程度、睡眠质量、心理状态测不到)、随机性(同样的训练量,今天的身体反应和昨天不同)、动态性(用户在运动过程中状态实时变化,你没法"暂停世界"再慢慢算)、连续状态空间(心率是连续值,不是几个离散档位)、单智能体(只和一个用户交互)。
这道题真正的考点是 P 里的"安全性"那一栏。 只写"目标达成率"的答案是不合格的——一个只追求减脂速度的教练会把人练进医院。性能度量里必须有约束项,否则智能体会用你没想到的方式"达标"。 这个问题在强化学习里叫奖励函数设计(reward hacking),是后面章节的重点。
习题 3:电商退款该用 Workflow 还是 Agent?(本章最好的一道题)
| 方案 A(Workflow:金额分档 + 7 天规则) | 方案 B(Agent:自主决策) | |
|---|---|---|
| 优点 | 结果可预测、可审计;成本极低(不用调模型);合规(每笔退款都能说清依据哪条规则);不会被话术诱导 | 能处理规则没覆盖的情况(商品有瑕疵但过了 7 天、老客户首次投诉);体验好;能给出解释和安抚 |
| 缺点 | 僵硬:定制品"一律拒绝"会赶走本来该赔的用户;规则一多就成一团乱麻;改政策要改代码 | 不确定:同一个申请两次可能不同结果;有被诱导的风险("我是你们VIP,这单必须退");难审计;贵 |
什么时候用 Workflow:规则清晰、后果不可逆、需要审计、必须百分之百确定。钱和权限就是典型。
什么时候用 Agent:情况开放多变、要理解自然语言里的细节、错了也能挽回。
方案 C 一定存在,而且这就是真实生产的做法——Agent 做理解和建议,Workflow 做决定和执行:
用户投诉(自然语言)
↓
[Agent] 读懂投诉 → 查订单 → 提取结构化事实:
{商品类型: 定制品, 天数: 9, 金额: 320,
用户历史退款次数: 0, 商品状况: 收到即破损(有照片),
建议: 破损属于质量问题,建议全额退}
↓
[Workflow] 拿这些事实走硬规则:
质量问题 → 不受 7 天限制 → 320元 落在 100-500 档 → 转客服审核
↓
[Agent] 把结果翻译成人话回复用户 + 安抚
这个分工的原则值得背下来:让模型负责"理解"和"表达",让代码负责"判断"和"执行"。
注意它和实战 §5 那个结论看起来相反(那里说"看情况的判断交给模型"),其实完全一致——区别在于错了的代价。推荐错一个景点,用户换一个就行;退错一笔钱,公司亏钱且无法追回。代价决定了这个判断该由谁做。 这是本章最有价值的一条工程判断力,比任何框架 API 都值钱。
习题 4(给旅行助手加功能) 属于动手题,思路已经在上面的实战里了:加功能 = 加工具(往 TOOL_REGISTRY 里 register_tool 一个新函数)+ 在 SYSTEM_PROMPT 里说清它什么时候该用。不需要改主循环一行代码——这就是注册表设计的回报。
面试视角
Q1:请解释一下什么是智能体,它和一个普通的程序、和 ChatGPT 有什么区别?
智能体是能自主感知环境、决策并行动以达成目标的系统。和普通程序的区别在自主性——普通程序的执行路径是我写死的,智能体的路径是它自己在运行时决定的。和裸用 ChatGPT 的区别在有没有闭环:ChatGPT 只能输出文字,智能体在它外面套了"解析输出 → 执行工具 → 把结果喂回去"的循环,所以能真正改变外部世界的状态。加了这个循环,"会说话"才变成"能干活"。
Q2:AIMA 的智能体分类你了解吗?
四类加一个元能力:简单反射(条件-动作规则,没记忆)、基于模型的反射(有内部世界模型,回答"世界现在什么样")、基于目标(有目标,会搜索规划,回答"我该做什么才能达成目标")、基于效用(用效用函数在多个可行方案里做权衡取舍)。学习能力不是第五类,是可以叠加在前四类之上的元能力——学习型智能体的结构是"性能元件 + 学习元件",性能元件可以是上面任意一种。很多人把它答成五个互斥的类别,这是不准确的。
Q3:LLM 智能体和传统智能体最本质的差别是什么?
我会答世界模型从显式变成了隐式。传统智能体的世界模型要工程师手工建模,所以只能在封闭领域工作;LLM 通过预训练获得了一个覆盖极广的隐式世界模型,第一次让智能体有了开放域的通用性。附带的差别是任务表述从形式化语言变成了自然语言,以及涌现能力——推理、规划这些没被专门训练的能力在规模足够大时自己出现了。
Q4:为什么智能体这个 60 年代就有的概念,偏偏这几年才火?
因为它需要"神经 + 符号"两者同时成立。符号主义时代的智能体可解释但脆弱,一遇到规则没覆盖的情况就失灵(知识获取瓶颈);纯神经网络灵活但不会做显式的多步推理。LLM 恰好把两者接上了:内核是神经网络(对应卡尼曼的系统 1,直觉),但它输出的
Thought和工具调用指令是显式的符号(系统 2,推理)。所以 LLM 智能体是神经符号主义的一个实践范例,这不是套话,是它能工作的机制解释。
Q5:Workflow 和 Agent 怎么选?
一句话:看错了的代价能不能承受。规则清晰、后果不可逆、需要审计(退款、权限、支付)用 Workflow;情况开放多变、错了能挽回(推荐、客服问答、信息整合)用 Agent。生产上多数是混合:Agent 负责理解和表达,Workflow 负责判断和执行。我做过一个退款场景就是这个结构,模型把自然语言投诉转成结构化事实,硬规则拿事实做决定。
Q6:你手写过智能体循环吗?说说会踩什么坑。
(见实战 §8"面试怎么讲"的完整答案。核心三个:工具做了决策导致退化成 if-else;提示词里的工具清单和实际工具不一致;解析 Action 时漏了
re.DOTALL导致多行答案被静默截断。)
Q7:智能体设计里 PEAS 有什么实际用处?不就是四个定义吗?
它的价值在逼你检查 S 和 A 是不是够。填完 S 那一栏你会发现"我要求它做的判断,它其实看不到相应信息"——比如让它推荐"人少的景点"但没有任何工具返回人流量。这时它只能编,也就是幻觉。 很多所谓的"模型不行",其实是传感器缺失。同理填 A 时会发现有的目标它根本没有对应的执行器。另外 P 那一栏必须写约束项,不然智能体会用你没预料的方式达标。
Q8:为什么每一轮都要把完整历史重发给模型?这不是很贵吗?
因为 LLM 的接口是无状态的,它不记得上一轮。这确实导致 token 消耗随轮数线性增长,长任务里会成为主要成本和主要故障源(撑爆上下文窗口、上下文腐蚀)。工程上的应对是截断、摘要压缩(compaction)、把结论落成外部笔记、用 sub-agent 隔离长上下文——这一整套就是"上下文工程",本书第九章专门讲。
划重点
- 智能体 = 环境 + 传感器 + 执行器 + 行动 + 自主性。 判断标准就一句话:做事步骤是写死的,还是它自己决定的? 算力、参数量、名气都不是判断依据(习题里那台 2 EFlop 超算就不是智能体)。
- AIMA 四级 + 学习是元能力:反射 → 模型式("世界现在什么样")→ 目标式("我该做什么才能达成目标")→ 效用式(权衡取舍)。学习不是第五级,是能叠加到任意一级上的插件(性能元件 + 学习元件)。
- 三个分类维度:① 内部决策架构 ② 时间与反应性(反应式/规划式/混合式)③ 知识表示(亚符号/符号/神经符号)。同一个智能体在三个维度上各有位置,面试问"哪种类型"时要三个维度都说。
- LLM 智能体最本质的新意是"隐式世界模型",这带来了开放域通用性;配套的还有自然语言任务表述和涌现能力。
- 它是神经符号主义的实践范例:神经网络内核(系统 1)+ 显式符号化行动(系统 2)。这就是"时代之问"的答案。
- 核心骨架只有一个循环:感知 → 思考 → 行动 → 观察,往复。全书剩下的十一章都是在给这个循环的某个环节加装备(记忆、工具、多智能体、评估……)。
- Thought / Action / Observation 是让 LLM 能动手的协议:模型输出文字,你的代码解析并执行,结果再变成文字喂回去。框架内核就是"提示词 + 工具注册表 + 解析器 + 循环 + 保险丝"这五件套,你已经手写过一遍了。
- 工具只取事实,不做决策。 这是本章最重要的工程原则。你亲手验证过:把判断写进工具,
"Rain" in "Light rain"一个大小写就让它静默出错,雾霾和 39 度高温也全判成"适合户外"。中立的工具 + 模型决策,这些情况它自己就对了。 max_steps保险丝、每个工具独立 try/except、错误返回自然语言而不是抛异常、把 Thought 打印出来。 这四条是让智能体"能自己爬起来"和"你能看懂它为什么倒下"的全部条件。- Workflow 按剧本演,Agent 自由发挥;选哪个取决于错了的代价。 生产上通常混合:模型负责理解和表达,代码负责判断和执行。
- 安全:绝不用
eval()执行模型输出的动作,一律走白名单查表;密钥只放.env且.env必须写进.gitignore。模型的输出是不可信文本——它可能被用户输入诱导。
下一章预告
这一章你知道了智能体是什么、亲手让它转起来了。但有个问题还没回答:这套"感知-思考-行动"的思路是谁想出来的?为什么是这个形状,而不是别的形状?
第二章会带你走一遍智能体的前世今生:从 1956 年达特茅斯会议、图灵测试、纽厄尔和西蒙的物理符号系统假设,到专家系统 MYCIN 的辉煌与崩塌(它失败的原因正是本章讲的"知识获取瓶颈"),再到强化学习、多智能体系统、直到 LLM 时代。你会看到本章那三个分类维度不是谁凭空发明的,是几十年路线之争打出来的结果。
而且第二章不只是历史故事——你会动手写三个小实验:一个迷你专家系统(体会符号主义的强与脆)、一个"心智社会"涌现模拟(体会多智能体的意外行为)、一个表格 Q-learning(体会"从反馈中学习"到底是怎么发生的)。跑完这三个,你对"为什么现在这么做"的理解会完全不同。