全栈开发知识库

第二章 智能体发展史

一句话总结

人工智能这七十年,其实就是一场"怎么才能让机器变聪明"的接力赛:一开始人们想把所有规则一条条写给机器(符号主义),后来发现写不完,改成让机器像大脑一样自己学(连接主义),再让它在试错中学会做决定(强化学习/行为主义),最后用读遍整个互联网的大语言模型当大脑,才有了今天的智能体。每一代新方法,都是为了解决上一代解决不了的难题。

本章你能学到什么

# 你会学到 学完能干什么
1 物理符号系统假说(1976,纽厄尔 & 西蒙)的两个论断 答出符号主义的理论基石,以及它被什么现象挑战了
2 专家系统的知识库 + 推理机结构、正向链/反向链、置信因子 CF 手写一个迷你专家系统,并说清"知识与推理相分离"为什么重要
3 符号主义倒在哪三堵墙上(知识获取瓶颈 / 常识无底洞 / 框架问题) 解释为什么 MYCIN 很成功却没进临床
4 ELIZA 的四步骗术 + ELIZA 效应 亲手戳穿一个"假聪明",并识别自己什么时候在给 AI 脑补
5 明斯基心智社会:智能体 / 机构 / 涌现 说清"没有中央规划,塔却搭起来了"到底是怎么回事
6 它如何催生 DAI → 多智能体系统,以及三条遗产 把蚁群算法、ACL、契约网这些名字挂到正确的位置上
7 学习时代三级台阶:连接主义 → 强化学习 → 预训练 说清每一级解决了上一级的什么缺口
8 强化学习五要素、策略 π、累积奖励与折扣 手写表格 Q-learning,并演示折扣因子 γ 的作用
9 自监督预训练 + 微调,以及涌现能力(ICL / CoT) 说清预训练为什么正好解开了符号主义的死结
10 三大流派在现代智能体里的会师位置 面试时把七十年历史压缩成两分钟讲清楚

动手部分:四个实验(迷你 ELIZA、迷你专家系统、心智社会涌现模拟、表格 Q-learning),全部纯离线、只用 Python 标准库,不需要密钥也不需要联网,约 100 分钟。

读这一章不需要任何技术背景。它不是背年份的一章——每一段历史都对应一个你今天写 Agent 时还会遇到的问题。


先看一眼全局:一条时间线

后面会逐个展开,先建立坐标系。只需记住这条链的因果关系,不用背年份。

年代 关键事件 / 成果 属于哪个流派 解决了什么 / 暴露了什么
1950s 达特茅斯会议,AI 作为学科诞生 —— 立起了"机器能思考吗"这个问题
1966 ELIZA(魏泽鲍姆) 符号主义 暴露:模板匹配能骗过人,但完全不懂语义
1968–1970 SHRDLU(特里·威诺格拉德) 符号主义 首次把"听懂话 + 规划 + 记忆"装进一个系统;但只在积木世界里聪明
1970s MYCIN(斯坦福)约 600 条规则 符号主义 证明窄领域可行,并首创置信因子 CF 处理不确定性
1976 物理符号系统假说 PSSH(纽厄尔 & 西蒙,图灵奖得主) 符号主义 给整个流派提供理论基石
1980s 专家系统商业化,随后遇冷;Cyc 项目开始手工录常识 符号主义 撞上三堵墙:知识获取瓶颈、常识无底洞、框架问题
1986 反向传播算法经 Rumelhart / McClelland / 辛顿(Geoffrey Hinton)等人推动进入主流 连接主义 让多层神经网络能训练了,连接主义复苏
1986 明斯基《心智社会》 (新视角) 提出智能可以从大量"无心"简单单元的交互中涌现
1990s TD-Gammon(西洋双陆棋);分布式人工智能 DAI → 多智能体系统 MAS 成形 行为主义 / MAS 试错学习首次达到人类顶尖水平;协作机制成为独立课题
1997 深蓝击败国际象棋世界冠军卡斯帕罗夫 符号主义 符号搜索在封闭规则领域的巅峰
2012 起 深度学习爆发(CNN 等) 连接主义 感知能力(认图、听声)产生质变
2016 AlphaGo 击败李世石(Nature 论文) 行为主义 + 连接主义 深度学习 + 强化学习 + 树搜索的融合
2017 起 Transformer → GPT 系列 → 大语言模型 连接主义(但干符号的活) 预训练解开了知识获取瓶颈
2020s LLM 驱动的智能体、多智能体框架 三大流派会师 神经内核 + 符号化行动 + RL 优化 + 群体协作

这张表最值得注意的是 1976 和 1986 这两个年份挨得很近:符号主义刚立起理论大旗十年,明斯基(他自己就是符号主义的代表人物之一)就在《心智社会》里反过来质疑"追求单一完美逻辑系统"这条路。流派之争从来不是外人打进来的,往往是内部反思出来的。


一、第一代思路:把聪明"写"进机器(符号主义)

1.1 核心信念:智能 = 摆弄符号

上世纪五六十年代,计算机刚诞生不久,科学家们有一个非常朴素的想法:

人思考的时候,不就是在脑子里摆弄各种概念、按逻辑推理吗?那我把这些概念变成"符号"、把推理变成"规则",输入计算机,机器不就能思考了?

这个流派叫符号主义。你可以把它想象成教一个完全不懂人情世故的外星人过日子:你给它写一本超级厚的手册,"如果下雨,就带伞""如果对方说谢谢,就回不客气"——它照着手册查条目、做动作,看起来就像懂事了一样。

这个时代智能体的一句话画像:它的"智慧"完全来自设计者预先编码的知识库和推理规则,不是自己学来的。

1.2 理论基石:物理符号系统假说(PSSH,1976)

是什么:1976 年,两位图灵奖得主艾伦·纽厄尔(Allen Newell)和赫伯特·西蒙(Herbert A. Simon)提出了物理符号系统假说(Physical Symbol System Hypothesis,简称 PSSH),它包含两个方向相反的论断:

论断 原话大意 翻译成大白话 它保证了什么
充分性论断 任何一个物理符号系统,都具备产生通用智能行为的充分手段 只要会摆弄符号,就有可能造出智能 给了研究者信心:这条路走得通,智能不是玄学
必要性论断 任何能展现通用智能行为的系统,其本质必然是一个物理符号系统 凡是智能的东西,剥开看一定是在摆弄符号 给了研究者判据:别的路都不用走,符号是唯一形式

两个论断的分量完全不同,这是面试考点。 充分性说的是"可以",必要性说的是"只能"。必要性论断才是那句最霸道的话——它等于宣称"不走符号这条路的都不可能有智能"。后来连接主义(神经网络)的成功,恰恰是对必要性的反驳:神经网络内部没有明确的符号,知识散在几十亿个权重里,可它确实展现出了智能。

"物理符号系统"这个词怎么理解? "物理"指它能真实存在于世界上(不是纯数学抽象);"符号系统"指它由一组可区分的符号 + 一套操作这些符号的过程组成。符号能组合成更复杂的结构(表达式),过程能创建、修改、复制、销毁这些结构。你写的每一行代码,本质上都是在做这件事。

怎么用(今天还有用吗):有用,而且是一道经典面试题——"LLM 驱动的智能体符合 PSSH 吗?" 一个漂亮的答法是:

它挑战了必要性,却又在行为上印证了充分性。 LLM 的内核是神经网络,知识以分布式权重存在,内部没有可区分的符号——这不符合必要性论断。但它工作时输出的 Thought:、Action: get_weather(city="北京")、生成的计划和代码,全都是标准的符号结构,而且它确实靠这些符号完成了推理和规划——这又印证了充分性。所以现代智能体是"亚符号内核 + 符号外壳",PSSH 只说对了一半。

1.3 代表作一:专家系统——把老专家的脑子"抄"下来

符号主义最成功的产品叫专家系统。思路很直接:找一位行业老专家,把他几十年的经验一条条问出来,写成"如果……就……"的规则,存进计算机。

一个专家系统主要有两个零件,这两个零件必须分开,这是整个设计的精髓:

  • 知识库(Knowledge Base):存放所有规则的仓库。最常用的表示方法叫产生式规则(Production Rules),就是一堆 IF-THEN 语句。比如:"IF 病人有发烧症状 AND 咳嗽 THEN 可能患有呼吸道感染。"一个复杂系统可能有成百上千条。
  • 推理机(Inference Engine):负责"查手册"的引擎。它有两种查法:
  • 正向链(Forward Chaining,正向推理):从已知事实出发往前推——"病人发烧了 → 触发规则A → 得出新结论并加入事实库 → 又触发规则B……"直到推出目标或者没有新规则可用。这叫数据驱动。
  • 反向链(Backward Chaining,反向推理):先假设一个目标("他是不是得了肺炎?"),去找哪条规则能推出这个目标,再把这条规则的 IF 部分当成新的子目标,递归下去,直到所有子目标都被已知事实证明。这叫目标驱动。

名词补课——"知识与推理相分离",这是本节最该带走的一句话。

注意上面两个零件的关系:推理机完全不认识任何具体规则的内容。它只会做一件事——拿事实去匹配规则的 IF 部分,命中就把 THEN 部分加进事实库。所以换一套知识库(从诊断疾病换成诊断电脑故障),同一个推理机不用改一行代码就能工作。

这个"引擎和知识分开"的设计思想,你在第一章已经用过一次了——那里的 TOOL_REGISTRY 就是"知识"(有哪些工具),_execute() 就是"引擎"(不认识任何具体工具,只会查表派发)。四十年了,这个设计还是对的。 下面的动手实验会让你亲手验证"换知识库不换引擎"。

正向链 vs 反向链怎么选? 事实少、可能结论多 → 用正向链(从少数事实出发往外推);目标明确、要验证的假设有限 → 用反向链(从假设倒推需要什么证据)。MYCIN 用反向链,因为医生的目标很明确:确定致病菌。 反向链还有个巨大好处:它知道自己缺什么,所以能主动向医生提问——正向链只会被动等事实。

明星案例 MYCIN:斯坦福大学在 20 世纪 70 年代开发的医疗诊断系统,辅助医生诊断细菌性血液感染并推荐抗生素方案。三个关键事实:

  1. 知识库约 600 条由医学专家提供的 IF-THEN 规则;
  2. 推理机主要用反向链:从"确定致病菌"这个最高目标出发反向推导需要哪些证据,然后主动向医生提问来获取;
  3. 首创了置信因子(Certainty Factor,CF):用一个 −1 到 1 之间的数值表示结论的可信度。−1 是"完全否定",0 是"不知道",1 是"完全确定"。

CF 为什么是个大创新? 因为医学诊断里几乎没有百分百确定的事。布尔逻辑只有真和假两档,装不下"70% 像链球菌感染"这种真实世界里最常见的状态。 CF 让系统能处理模糊、不确定的知识,并给出带可信度的结论。

多条规则同时支持同一个结论时,CF 要合并。MYCIN 的合并公式(两个都为正时)是:cf_new = cf1 + cf2 × (1 − cf1)。这个公式的性质很妙:结果一定比两者都大,但永远不会超过 1——"两个独立证据都指向同一个结论,可信度上升,但再多证据也到不了 100% 确定"。下面的实验会让你亲眼看到这个数字怎么变。

成就:在一项评估中,MYCIN 在血液感染诊断上的表现超过了非专业医生,甚至达到了人类专家的水平。它有力地证明了 PSSH 的充分性论断:通过精心的知识工程和符号推理,机器确实可以在高度复杂的专业领域展现出卓越的"智能"。

但请注意一个历史事实:MYCIN 最终并没有大规模进入临床。 除了后面 1.5 节要讲的三堵墙,还有几层现实阻力——责任归属(诊断错了谁负责?)、法律与监管(医疗器械要审批)、医生的接受度(要向一个不能对话、只能一问一答的终端敲入几十项数据)、知识更新成本(医学指南每几年就变,600 条规则要重新访谈专家维护)。"技术上成功"和"能落地"之间的距离,这个案例给了七十年前的第一课。 你今天做 Agent 落地时遇到的阻力,往往和当年一模一样。

1.4 代表作二:SHRDLU——积木世界里的全能选手

如果说专家系统是"某一科的状元",那 1968–1970 年由特里·威诺格拉德(Terry Winograd)开发的 SHRDLU 项目就是第一个"全科小学生"。它生活在一个虚拟的"积木世界"里:一个模拟的三维空间,有各种颜色形状大小的积木,和一只能抓取移动它们的虚拟机械臂。你可以用日常英语跟它对话:

  • 你说"捡起那块大的红积木",它就照做;
  • 你说"把蓝积木放到红积木上",如果红积木上已经压着绿积木,它会自己想到要先把绿积木挪开——这是它自己规划出来的步骤,没人教过;
  • 你问它"你为什么捡起红积木?"它会回答"因为你让我这么做的"——它记得自己做过什么、为什么做。

SHRDLU 第一次把"听懂话(自然语言理解)、做规划、有记忆"这几种能力装进了同一个系统。今天智能体"感知—思考—行动"的循环雏形就在这里——你在第一章手写的那个循环,血脉可以一直追到 1970 年。

但请注意一个关键前提:它只在积木世界里聪明。积木世界之所以能让它表现得这么好,恰恰因为那是一个规则完备、对象有限、没有意外的封闭小房间。出了这个房间,它什么都不会。

这个对比值得记住:专家系统是深度上的突破(一个领域做到专家水平),SHRDLU 是广度上的突破(多种能力集成在一个系统里)。两条路都成功了,但都被同一件事挡住——真实世界不是积木世界。

1.5 符号主义为什么走不下去了

八十年代起,人们想把这些系统从"实验室小房间"搬到真实世界,结果撞上了几堵推不倒的墙:

墙一:知识根本写不完(知识获取瓶颈)。 抄一个专家的脑子要靠工程师反复访谈、提炼、编码,又慢又贵。更麻烦的是,专家很多本事是"凭手感"的——老中医号脉、老司机躲车,他们自己都说不清规则,你怎么帮他写成"如果……就……"?

墙二:常识是个无底洞。 人类的日常行为建立在海量常识之上:"水是湿的""绳子能拉不能推""人不能同时在两个地方"。这些东西你从没觉得需要学,但机器不写就不知道。有个叫 Cyc 的项目真的尝试把全人类常识手工录入电脑,干了几十年,至今收效有限。

墙三:世界一变,系统就懵(框架问题 + 脆弱性)。 真实世界随时在变。机器每做一个动作,理论上都要重新确认"哪些事情变了、哪些没变"——人类本能地会忽略无关变化,机器却要一条条排查,算不过来。而且规则系统极其"脆":只要遇到规则没覆盖的情况,它不是降级应对,而是直接失灵。就像那本外星人手册,遇到手册上没写的场面,外星人当场死机。

一句话总结第一代:在小而封闭的世界里很能打,一进开放的真实世界就崩溃。聪明是设计者给的,不是它自己的。


二、亲手拆穿一个"假聪明":聊天机器人 ELIZA

2.1 一个心理医生的"话术模板"

1966 年,麻省理工的魏泽鲍姆做了一个聊天程序 ELIZA,扮演一位心理咨询师。它的对话效果好到吓人——连作者的秘书都跟它聊出了感情,深信它能理解自己。

但它的原理简单到让人失望,本质上就是一套"话术模板",四步走:

  1. 找关键词:扫一遍你说的话,看有没有命中预设的关键词(比如"妈妈""我需要""我很难过")。
  2. 拆句子:用模板把你的话拆开,抠出有用的部分。你说"我为我的男朋友感到难过",它抠出"男朋友感到难过"。
  3. 换人称:把"我"换成"你"、"我的"换成"你的"。
  4. 套模板回复:从几个预设句式里随机挑一个填空——"你为什么会为你的男朋友感到难过?"

就这样。它对"男朋友"是什么、"难过"是什么感受一无所知,纯粹在做文字搬运。这就像酒桌上的老油条,根本没听你说什么,只会把你的话尾巴接过来反问一句"哦?此话怎讲?"——竟然也能把天聊下去。

2.2 它暴露了规则系统的三个死穴

教程里带读者用几十行代码复现了一个迷你 ELIZA(代码细节不重要,思想才重要),一上手就能发现三个致命问题:

  • 不懂语义:它眼里只有字符,没有意思。原版英文 ELIZA 遇到 I am not happy,会照样命中 I am (.*) 规则(因为 not happy 整段被当成"你是什么"的内容抓走了),于是回一句语法通顺、意思完全反了的话。而本章动手实战里的中文版遇到"我不需要安慰",是另一种死法:我需要(.+) 要求"我需要"三个字紧挨着,中间插了个"不"就整条不匹配了,直接掉进兜底回复"为什么这么说?"。

    这两种失败方向相反,但病根是同一个。 英文那种是自信地答错(模板匹配上了,语义反了);中文这种是沉默地空转(模板没匹配上,只能装深沉)。前者更危险——它看起来在正常工作。根因都是:正则看见的是字符,不是意思。 动手实战第 3 步会让你亲手把这两种情况都跑出来。

  • 没有记忆:每句回复只看你当前这一句话,上一轮聊了什么它完全不记得,没法进行连贯对话。
  • 规则爆炸:想让它应付更多话题?加规则。但规则越加越多,互相打架,优先级理不清,最后维护不动。语言的可能性是无限的,靠穷举模板注定追不上。

2.3 ELIZA 效应:人类太容易"脑补"了

尽管如此简陋,很多人还是坚信 ELIZA 理解自己。这个现象后来被称为 ELIZA 效应:人类天生倾向于给机器的输出"脑补"出理解和情感。记住这个效应,今天依然适用——当你觉得 ChatGPT"懂你"的时候,值得想一想:是它真的懂,还是你在脑补?


三、一个颠覆性的想法:聪明来自"一群笨蛋的合作"(心智社会)

3.1 换个角度想问题

规则越写越多还是不行,一些顶尖学者开始怀疑:是不是方向本身错了?

AI 先驱马文·明斯基在《心智社会》一书里给出了一个惊人的回答:

"是什么魔法让我们拥有智能?答案是:根本没有魔法。智能的力量来自我们内部巨大的多样性,而不是来自某个单一、完美的原理。"

他的意思是:别再执着于造一个"全知全能的中央大脑"了。人的心智本身就不是一个统一的逻辑机器,而更像一个热闹的社会——由无数个各管一摊的"小职员"组成,每个小职员笨得要命,但它们分工协作,整体上就涌现出了聪明。

名词补课——明斯基自己的三个术语,用词很讲究。

术语 明斯基的原意 大白话
智能体(Agent) 极其简单、高度专门化、"无心"的心智过程 一个只会干一件小事的笨函数。比如 LINE-FINDER(只会在图像里找线条)、GRASP(只会握紧手)。"无心"是关键词:它没有目标、没有理解、不知道自己在为什么服务。
机构(Agency) 一组相互协作的智能体构成的更大单元 一个"部门"。比如 BUILD 这个机构由 SEE/FIND/GET/PUT 几个智能体组成。机构对外看起来像一个能力,对内其实是一群笨蛋。
涌现(Emergence) 目标导向的行为从没有任何成员持有全局计划的交互中产生 整体能干的事,任何一个成员都干不了,也不知道正在被干。

注意"智能体"这个词在这里的含义和第一章不一样。 第一章的 Agent 是"能感知、能决策、能行动的完整系统";明斯基的 agent 是"心智里的一个笨零件"。同一个词,一个指整体,一个指零件——看到 Society of Mind 语境里的 agent,要切换到"零件"这个意思上。

3.2 搭积木的例子:没有人懂全局,事却办成了

想象一条"搭积木塔"的指挥链,从上到下每一层都比你想的更笨:

层级 成员 它会的全部本事 它不知道的事
顶层 BUILD-TOWER 激活 BUILDER,然后等结果 积木怎么抓、塔怎么摆
上层 BUILDER 一句话:塔没到目标高度,就再激活一次 ADD-BLOCK 一块积木是怎么加上去的
中层 ADD-BLOCK 依次激活 FIND-BLOCK、GET-BLOCK、PUT-ON-TOP 为什么要加积木、要加几块
中层 FIND-BLOCK / GET-BLOCK / PUT-ON-TOP 各自激活下面的工人 塔是什么
底层 SEE-SHAPE / REACH / GRASP 看一眼 / 伸手臂 / 握紧手 一切。它连"积木"是干什么用的都不知道

重要澄清:这里说的"激活"不是"下命令",别把它读成公司里的层级指挥。

明斯基描述的机制是去中心化的激活与抑制:上层只是把下层的"兴奋度"推高,让它开始活动;下层被激活后按自己那一点本事行动,不会收到任何关于全局目标的说明。BUILDER 从来没告诉 GRASP"我们在搭塔",它甚至没有能力表达这句话。

所以"总经理"这个比喻是有害的——真实的公司总经理脑子里装着完整战略。这里更准的比喻是:一个装了很多开关的机器,上层的开关会拨动下层的开关,没有任何一个开关知道整台机器在做什么。 智能不在任何一层里,它在连接方式里。

这条链上没有任何一个成员知道整个计划,但塔就是这么搭起来了。这种"整体能力大于所有个体之和、且没有中央规划"的现象,叫做涌现(Emergence)。就像一只蚂蚁笨得可以,但一个蚁群能修出结构精巧的巢穴——没有哪只蚂蚁是建筑师,也没有哪只蚂蚁见过巢穴的图纸。

动手实战三会让你把这条链完整敲出来,并且做一件书上不会做的事:把最底层的 GRASP 弄坏,看失败怎么一路往上冒泡。你会看到涌现的另一面——没有哪一层懂全局,所以也没有哪一层能想办法绕过去。

3.3 这个思想的后劲:从 DAI 到多智能体系统

心智社会理论当时更多是哲学思辨,但它埋下了一颗种子:

既然一个脑子里的智能可以靠许多简单单元协作涌现,那多台计算机、多个程序之间,能不能也靠协作涌现出更强的群体智能?

这条线的学名演进是:心智社会 → 分布式人工智能(Distributed Artificial Intelligence,DAI)→ 多智能体系统(Multi-Agent System,MAS)。DAI 关心的是"怎么把一个问题拆给多台机器算",MAS 更进一步,关心"每个智能体都有自己的目标时,它们怎么共处"——分工、通信、协商、合作、甚至讨价还价。今天你听到的"多个 AI 角色组队干活"(一个当产品经理、一个当程序员、一个当测试员)就是这个思想的直系后代(第十一章会动手搭)。

它留下三个核心遗产,每一个都有对应的具体技术,面试时能报出名字才算真懂:

遗产 意思 生活类比 它具体长成了什么技术
去中心化控制 没有中央指挥,每个个体只按局部规则行动 菜市场没有总调度,但供需自然平衡 今天多智能体框架里的"无主控编排"、去中心化任务认领
涌现式计算 复杂的全局结果从简单的局部互动中自发产生 鸟群没有领队,却能变换出整齐队形 蚁群算法(Ant Colony Optimization,ACO)、粒子群优化(Particle Swarm Optimization,PSO)——都是直接照抄生物群体的涌现行为来解优化问题
智能体的社会性 个体之间必须有共同的语言和规矩才能协作 公司要有邮件规范和开会制度 智能体通信语言(Agent Communication Language,ACL)、契约网协议(Contract Net Protocol)——一个智能体广播任务、其他智能体投标、中标者执行,本质就是招投标;还有协商策略、信任模型、组织结构

契约网协议值得单独记一下,因为它是今天"任务分发"类多智能体设计的祖师爷:招标(announce)→ 投标(bid)→ 中标(award)→ 汇报(report)。你在第十一章看到的"由一个 Agent 拆任务、其他 Agent 领任务",机制上就是它。1980 年代的协议,今天换了个说法又火了一遍。


四、第二代思路:别教了,让机器自己学

符号主义的失败让大家意识到:如果智能没法被完全设计出来,那能不能让它被"学"出来? 从此 AI 进入"学习"时代。这个时代有三级台阶。

4.1 台阶一:连接主义——模仿大脑的学习机器

连接主义的灵感来自人脑。先用大白话解释两个词:

  • 神经网络:人脑由几百亿个神经元组成,每个神经元很简单(接收信号、处理一下、传给下家),智能藏在它们之间的连接强度里。人工神经网络就是用数学模拟这个结构:一大堆简单的计算单元连成网,每根连线上有一个"权重"(可以理解为音量旋钮,决定信号传过去时放大还是减弱)。
  • 训练:给这个网络看大量例子(比如一万张猫的照片和一万张狗的照片),每看一个例子,就按某种算法自动微调所有旋钮,让网络的输出离正确答案更近一点。重复几百万次,网络就"学会"了认猫认狗。

它和符号主义是完全相反的哲学:

符号主义 连接主义
知识在哪 明文写在规则库里,一条条看得见 藏在亿万个旋钮的位置里,看不见摸不着
知识哪来 人类专家写进去的 机器从大量例子中自己学的
好比 背下一整本菜谱 跟着师傅炒一千次菜,手上有了感觉
擅长 清晰的逻辑推理 感知与模式识别(认图、听声、读文本)

连接主义(也就是后来大火的深度学习)让机器第一次拥有了强大的感知能力——能看懂图片、听懂语音。但光会感知不够,看见了之后该做什么?这需要下一级台阶。

4.2 台阶二:强化学习——在试错中学会做决定

强化学习解决的是"怎么做决定"的问题。它的原理你其实早就懂,因为这就是训练小狗的方法:

小狗做对动作 → 给块肉干(奖励);做错 → 不给(或轻罚)。狗不懂人话,但为了多吃肉干,它慢慢就学会了握手、坐下。

把这套逻辑用在机器上,就是强化学习的五个要素:

  • 智能体:学习者本人(那只狗)
  • 环境:它面对的世界(训练场和驯犬师)
  • 状态:当前的处境(驯犬师喊了"坐下")
  • 行动:它选择做的事(坐了 / 没坐)
  • 奖励:环境给的反馈(肉干 / 没肉干)

机器就在"观察处境 → 选择行动 → 收到奖励 → 调整策略"的循环里滚上几百万圈,把策略越磨越好。

最出名的例子是 AlphaGo(DeepMind,2016 年发表于 Nature):它靠自己跟自己下棋(自我博弈),赢了加分、输了扣分,几百万盘之后棋力超越人类冠军。

这里有个特别容易讲错的细节,面试时说错会露怯。 常见说法是"AlphaGo 全程不需要人类棋谱"——这是错的,那是 2017 年的 AlphaGo Zero。 2016 年击败李世石的那一版 AlphaGo,先用人类高手的对局棋谱做了监督学习来初始化策略网络(相当于先临摹名家棋谱),之后才进入自我博弈的强化学习阶段。准确的说法是:它的自我博弈阶段不依赖人类棋谱的指导,但整个训练流程的起点用了人类数据。AlphaGo Zero(2017)才是真正从零开始、完全不看人类棋谱的版本,而且棋力反而更强——这件事本身就说明:人类经验有时是加速器,有时是天花板。

强化学习还有个精髓:它追求的不是眼前这一步的奖励,而是长远的总收益。就像下棋时故意送掉一个子来换取全局胜势,或者你为了将来的健康放弃眼前的奶茶——会"忍",才是高级的决策能力。

名词补课——两个必须记住的术语。

  • 策略(Policy,记作 π):智能体的"行为准则",本质就是一个从状态到行动的映射——"看到什么处境,就做什么动作"。强化学习训练的全过程,说到底就是不断改进这个 π。你可以把 π 想成那本外星人手册,只不过这本手册不是人写的,是机器自己试错试出来的,而且会一直改版。
  • 累积奖励(Cumulative Reward),也叫回报(Return):从现在起到任务结束,所有奖励加起来的总和。强化学习的目标函数是最大化回报,不是最大化当前这一步的奖励——这一个字的差别,就是"弃子换势"和"贪吃眼前一子"的分野。

实际计算回报时还要乘一个折扣因子 γ(gamma,0 到 1 之间):离现在越远的奖励,乘的 γ 次数越多,权重越小。γ=0 表示"只看眼前这一步"(极度短视),γ=0.9 表示"很看重未来"。动手实战四会让你把 γ 从 0.9 改成 0,亲眼看着一个会走迷宫的智能体变成一个原地打转的傻子。

另一个里程碑值得记一笔:1990 年代的 TD-Gammon(西洋双陆棋程序),是强化学习第一次在复杂棋类上达到人类顶尖水平,比 AlphaGo 早了二十多年。它证明了"试错学习"这条路本身是通的,只是当时缺少深度神经网络这个强大的"感知器"。AlphaGo = TD-Gammon 的思路 + 深度学习的眼睛 + 树搜索的算力。

4.3 台阶三:预训练——先读万卷书,再干具体活

强化学习有个尴尬的短板:它是从零开始试错的,没有任何背景知识。就像让一个刚出生的婴儿直接学开车——试错一亿次也许能学会,但太浪费了。能不能让机器先"接受通识教育",再学具体技能?

答案来自语言处理领域,叫预训练(Pre-training),分两步:

  1. 预训练(读万卷书):让一个超大的神经网络把互联网上几乎所有文本读一遍,任务简单粗暴——不停地玩"猜下一个词"的填空游戏。"今天天气真___",猜"好"。就靠这一个游戏玩上万亿次,模型被迫学会了语法、事实、逻辑,甚至各领域的专业知识——因为想猜得准,就必须真的"懂"上下文。
  2. 微调(Fine-tuning,岗前培训):读完万卷书的模型已经是个"通才",再用少量特定任务的数据稍加训练,它就能快速上岗做翻译、写代码、当客服。

名词补课——"自监督学习",这是预训练能成立的关键,也是最容易被讲漏的一个词。

传统机器学习要监督学习(Supervised Learning):给一万张猫的照片,还得有人一张张标上"这是猫"。标注是要花钱雇人做的,所以数据规模天然受限于预算。

而"猜下一个词"这个任务不需要任何人标注——答案本来就在原文里!把"今天天气真好"的最后一个字盖住,"好"就是标准答案。数据自己给自己当标签,所以叫自监督学习(Self-supervised Learning)。这一下,全互联网的文本瞬间都变成了可用的训练数据,规模从"几十万条人工标注"跳到"上万亿词"。

这就是为什么预训练能解开知识获取瓶颈,而专家系统解不开。 专家系统的知识要工程师访谈专家、一条条手写(人工标注的极端版本);预训练的知识是模型自己从文本里读出来的,成本几乎为零,规模没有上限。同一个难题,一个用了三十年攻不下,另一个换了个数据获取方式就绕过去了。很多技术死结的解法不在死结本身,而在换一条路径。

这个"先通识、后专业"的模式,恰好解开了符号主义时代最大的死结——知识获取瓶颈:过去要靠工程师一条条手写的知识,现在模型从海量文本里自动吸收了。

更神奇的是,当模型大到一定程度,会冒出一些没人专门教过的能力(术语叫涌现能力,Emergent Abilities——注意这个"涌现"和明斯基那个"涌现"是同一个词、同一个精神:整体规模上去了,出现了任何单个部分都不具备的性质):

涌现能力 术语 大白话 你今天怎么用
看几个例子就会举一反三 上下文学习(In-Context Learning,ICL) 不用重新训练,你在对话里给它两三个例子,它就照葫芦画瓢做新任务 给 0 个例子叫 zero-shot(零样本),给几个例子叫 few-shot(少样本)。写提示词时"给两个示例"就是在用 few-shot ICL
会"打草稿" 思维链(Chain-of-Thought,CoT) 让它把推理过程一步步写出来,答对复杂问题的概率大幅上升 提示词里加一句"请一步步思考"。第一章那个 Thought: 字段就是 CoT 的工程化——你已经用过了

CoT 为什么有效?一个直观解释: 模型每生成一个词,都只能基于"已经写出来的内容"做计算。不让它写草稿,它就必须在一次前向计算里蒙出答案;让它写草稿,中间结果就变成了它自己的输入,等于给了它更多计算步数。 就像让你心算 47×83 和让你在纸上列竖式——后者不是你变聪明了,是你有地方放中间结果了。第四章会把 CoT 做成完整的 ReAct 范式。

这样的模型就是大语言模型(Large Language Model,LLM),也就是 ChatGPT 们的学名。第三章会拆开它的内部结构。


五、集大成者:大语言模型驱动的智能体

到这里,历史上所有关键零件都齐了。现代智能体就是把它们组装在一起:

用大语言模型当大脑,配上记忆、规划和工具,在一个循环里自主干活。

它的工作流程是一个不断转动的圈,用"请了一位私人助理帮你订餐厅"来打比方:

  1. 感知:助理听到你的需求:"帮我订一家附近评分高的川菜馆。"
  2. 思考:大脑(LLM)开始盘算,把大目标拆成小步骤——先搜附近川菜馆、再比评分、再看有没有空位。它还会翻记忆:"上次主人说不吃太辣。"必要时它会自我反省:"我这个计划靠谱吗?要不要调整?"
  3. 行动:光想没用,得动手。助理拿起"工具"——打开点评软件搜索、给餐厅打电话。对智能体来说,工具就是搜索引擎、计算器、各种软件接口。
  4. 观察,然后再来一圈:工具返回结果("这家周五晚上没位了"),助理据此更新认知、调整计划("那换下一家"),进入新一轮循环,直到任务完成。

对照历史,你会发现现代智能体是一场"三大流派的世纪大和解"。先把三大流派本身摆清楚——这张表是本章最该背下来的东西,面试问"AI 三大流派"就答它:

流派 别名 核心信念 代表人物 代表成果 一句话软肋
符号主义 逻辑主义 智能 = 对符号做逻辑操作 纽厄尔、西蒙、明斯基 SHRDLU、专家系统(MYCIN)、深蓝(1997 击败卡斯帕罗夫) 知识写不完,出了封闭世界就崩
联结主义 连接主义 智能藏在大量简单单元的连接强度里 杰弗里·辛顿(Geoffrey Hinton) 反向传播(1986)、CNN、Transformer → LLM 是个黑箱,说不出自己为什么这么答
行为主义 进化主义 / 控制论学派 智能来自与环境交互中的试错和适应 萨顿等(强化学习) TD-Gammon(1990s)、AlphaGo(2016) 从零试错太贵,且奖励函数难设计

深蓝值得单独说一句,因为它是符号主义的巅峰,也是它的天花板。 1997 年深蓝击败国际象棋世界冠军卡斯帕罗夫,靠的是极快的符号搜索 + 人类专家写的评估函数——每秒算两亿个局面。它证明了符号主义在规则完备、状态可枚举的封闭领域能打到世界第一。但同一套思路搬到围棋上就不行了(围棋的局面数比宇宙原子还多),更别说搬到"帮我订个餐厅"这种开放任务。深蓝赢的那一天,也是符号主义路线走到尽头的那一天。

然后是它们在现代智能体里的会师位置:

历史流派 留下的遗产 在现代智能体中的位置 你在哪一章会碰到
符号主义 逻辑推理、规划、"如果-那么"式思考、可解释性 智能体的推理与规划行为;Thought/Action 这种符号化的输出协议;工具白名单查表 第一章的 Action: 协议、第四章 ReAct/Plan-and-Solve
连接主义 神经网络、从数据中学习、强大的感知与语言能力 LLM 本身就是一个巨型神经网络,是智能体的"大脑" 第三章
行为主义(强化学习) 试错学习、追求长期回报 用于训练和优化模型的决策能力(RLHF 就是它) 第三章的对齐、第十二章的微调
心智社会 分工协作、群体涌现、去中心化 多智能体系统(AI 组队干活)、契约网式任务分发 第十一章

有趣的一点:大语言模型本身是连接主义的产物(一个大神经网络),但它干的活——推理、规划、调用工具——恰恰是当年符号主义梦想做到的事。绕了七十年,两条路在山顶会师了。 更准确地说:现代智能体 = 亚符号的内核 + 符号化的外壳 + 强化学习打磨的决策 + 心智社会式的群体结构。

有一张业界常被引用的"智能体技术栈全景图"(Letta,2024 年 11 月),把做智能体用到的工具和平台按层分类(模型层、存储层、框架层、编排层、托管层等)。属于扩展阅读,初学阶段扫一眼建立"生态有多大"的印象即可,不必深究——这些工具的具体用法本书第五章(低代码平台)和第十章(协议)会讲到。


动手实战:四个离线实验

这一章的四个实验全部只用 Python 标准库(re、random),不需要 API 密钥、不需要联网、不需要 pip install 任何东西。建议按顺序做,因为它们串起了本章的四个核心思想:

实验 文件 你会亲手验证 约需
一 eliza.py 规则系统怎么"假聪明",以及它三种死法 25 分钟
二 expert_system.py 知识与推理相分离、CF 合并、知识边界之外的失语 30 分钟
三 society_of_mind.py 涌现是什么样子,以及它怎么被一个点带塌 25 分钟
四 q_learning.py 策略 π 怎么被试错磨出来,γ=0 时智能体变傻 20 分钟

先建一个目录放它们:

mkdir -p ~/agent-history && cd ~/agent-history

实验一:从0到1手敲一个中文迷你 ELIZA

第二节拆穿了 ELIZA 的"话术骗局",现在你亲手造一个——不到 60 行、完全离线、不需要任何密钥,敲完立刻能跑。造完它,你会对"规则系统为什么走不下去"有切肤体会。

名词补课——正则表达式(Regular Expression):一种描述"文字长什么样"的小语言。比如 我需要(.+) 的意思是"以'我需要'开头,后面跟至少一个任意字符,并把后面这部分抓出来"。Python 里用 re 模块来使用它。现在不用系统学,看懂本例中的几条即可。

第 1 步:新建 eliza.py,逐段敲入

import re
import random

# 规则库:每条规则 = (匹配模式, 一组回复模板)
# {0} 是占位符,会被用户话里抓出来的内容填进去
RULES = [
    (r"我需要(.+)", [
        "你为什么需要{0}?",
        "得到{0}真的能帮到你吗?",
        "你确定你需要{0}吗?",
    ]),
    (r"我感到(.+)", [
        "你为什么感到{0}?",
        "经常感到{0}吗?",
        "感到{0}的时候,你一般会做什么?",
    ]),
    (r"我是(.+)", [
        "你当{0}多久了?",
        "成为{0},你感觉如何?",
    ]),
    (r".*(妈妈|爸爸|家人).*", [
        "多和我讲讲你的家人吧。",
        "你和家人的关系怎么样?",
    ]),
    (r".*(你好|在吗).*", [
        "你好,今天想聊点什么?",
    ]),
]

# 兜底回复:一条规则都没命中时随机挑一句(酒桌老油条模式)
DEFAULT = ["嗯,继续说。", "有意思,然后呢?", "为什么这么说?", "能展开讲讲吗?"]


def reflect(text: str) -> str:
    """人称转换:把"我"换成"你"、"你"换成"我"(逐字处理,避免换完又被换回去)"""
    mapping = {"我": "你", "你": "我"}
    return "".join(mapping.get(ch, ch) for ch in text)


def respond(user_input: str) -> str:
    for pattern, templates in RULES:
        m = re.search(pattern, user_input)
        if m:
            template = random.choice(templates)
            groups = [reflect(g) for g in m.groups()]
            return template.format(*groups)
    return random.choice(DEFAULT)


if __name__ == "__main__":
    print("迷你ELIZA:你好,我是你的聊天伙伴。(输入 退出 结束)")
    while True:
        user = input("你: ").strip()
        if user in ("退出", "quit", "exit"):
            print("ELIZA: 再见!")
            break
        if user:
            print("ELIZA:", respond(user))

第 2 步:运行并跟它聊几句

python eliza.py

试试这些输入(回复模板是 random.choice 随机挑的,所以每次跑到的句子可能不同,但句式一定来自命中那条规则):

你: 我需要一个假期
ELIZA: 得到一个假期真的能帮到你吗?
你: 我感到很焦虑
ELIZA: 感到很焦虑的时候,你一般会做什么?
你: 我需要你陪我
ELIZA: 你为什么需要我陪你?

✅ Checkpoint 1:三句都得到"像模像样"的回复,并且第三句里的"你/我"被正确调换了——你说"我需要你陪我",它答"你为什么需要我陪你"。这一个 reflect() 函数,就是当年骗过无数人的核心戏法。

第 3 步:亲手戳穿它(本实验的真正目的)

现在做三个实验,亲眼验证 2.2 节说的三个死穴。下面每一条的输出都是真实跑出来的:

  1. 不懂语义,只认字面。 输入 我不需要安慰 和 我不是小孩子:

你: 我不需要安慰 ELIZA: 嗯,继续说。 你: 我不是小孩子 ELIZA: 嗯,继续说。

(这两句是 DEFAULT 里随机挑的,你跑到的可能是"为什么这么说?"或"有意思,然后呢?"——只要是 DEFAULT 里的四句之一,就说明规则全部落空了,这才是要看的点。)

两句都掉进了兜底回复。为什么?因为 我需要(.+) 要求"我需要"三个字紧挨着,而"我不需要"中间插了个"不",正则就不认了。可以单独验一下:

```python

import re bool(re.search(r"我需要(.+)", "我不需要安慰")) False bool(re.search(r"我是(.+)", "我不是小孩子")) False ```

同一个"不"字,在英文原版里是完全另一种死法。 英文的 not 落在"I am"后面,所以规则照样命中,只是把否定词一起抓进了占位符:

```python

m = re.search(r"I am (.+)", "I am not happy") bool(m) True m.group(1) 'not happy' "How long have you been {0}?".format(m.group(1)) 'How long have you been not happy?' ```

两种失败,方向相反,病根相同。 英文版是自信地答错(模板命中了,语义反了,而且句子读起来毫无破绽);中文版是沉默地空转(模板没命中,只能装深沉)。前者危险得多——它看起来在正常工作。 根因是同一个:正则表达式看见的是字符,不是意思。

那能不能补规则?能救这一句:再写一条 我不需要(.+)。但"我并不太需要""我压根不需要""需要什么啊我"又会漏。语言的变形是无限的,模板是有限的——这就是"规则写不完"的第一手体感,也是符号主义知识获取瓶颈在最小尺度上的现场演示。

  1. 没有记忆,每轮从零开始。 先说 我感到难过,再问 我刚才说我感到什么?:

你: 我刚才说我感到什么? ELIZA: 感到什么?的时候,你一般会做什么?

它不但没记住上一轮,还把你的提问本身当成了新的情绪陈述去套模板,答出一句语法都不通的话。对比第一章的 LLM 智能体:那里只要把历史消息拼进 messages 列表,模型就能接着聊——"有记忆"是第八章的主题,而这里让你先看清"没记忆"长什么样。

  1. 规则爆炸,而且规则之间会抢答。 注意 RULES 是按顺序匹配、命中即返回的(respond() 里那个 for 循环),所以顺序就是优先级:把 .*(你好|在吗).* 挪到列表最前面,再输入 你好,我需要一个假期,它就只会回"你好,今天想聊点什么?",完全无视后半句的真实诉求。话题越多,规则互相打架的概率越高,维护成本随规则数量非线性上涨——这就是符号主义的宿命。

改一改:把 RULES 换成"毒舌闺蜜"或"客服"人设的模板,你会发现换个人设=重写全部规则;而第一章的 LLM 智能体换人设只要改一句系统提示词。两相对比,你就彻底明白了这七十年技术进化到底进化了什么。


实验二:迷你专家系统——亲手验证"知识与推理相分离"

这个实验要做的事,是把 1.3 节那段抽象的话变成能跑的代码:推理机一个字都不认识具体规则的内容。做完你会拥有一个 MYCIN 的极简版——带置信因子、带推理链、能换领域。

名词补课——字典(dict):Python 里存"名字→值"的容器,写作 {"叶片有黄斑": 1.0},意思是"事实『叶片有黄斑』的置信度是 1.0"。列表(list):一串有顺序的东西,写作 [规则1, 规则2]。本实验的知识库就是"一个列表,里面装着一堆字典"。

第 1 步:写知识——expert_system.py 的第一部分

新建 expert_system.py。先只写知识,一行推理代码都不写。注意这两个知识库领域完全不同,但数据格式一模一样:

# -*- coding: utf-8 -*-
"""实验 2:迷你专家系统 —— 体会符号主义的"强"与"脆" """

# 一条规则读作:如果 if_all 里的事实全部成立,那么 then 成立,可信度是 cf。
# 知识库 A:诊断植物病害
RULES_PLANT = [
    {"id": "R1", "if_all": ["叶片有黄斑", "空气潮湿"],       "then": "得了叶斑病",   "cf": 0.7},
    {"id": "R2", "if_all": ["叶背有白粉"],                   "then": "得了白粉病",   "cf": 0.9},
    {"id": "R3", "if_all": ["叶片有黄斑", "叶缘焦枯"],       "then": "得了叶斑病",   "cf": 0.5},
    {"id": "R4", "if_all": ["得了叶斑病", "连续下雨"],       "then": "喷波尔多液",   "cf": 0.8},
    {"id": "R5", "if_all": ["得了白粉病"],                   "then": "喷硫制剂",     "cf": 0.85},
    {"id": "R6", "if_all": ["根部发黑", "土壤积水"],         "then": "得了根腐病",   "cf": 0.8},
]

# 知识库 B:诊断电脑故障。领域完全不同,但数据格式一模一样。
RULES_COMPUTER = [
    {"id": "C1", "if_all": ["按电源键没反应", "电源灯不亮"], "then": "供电有问题",       "cf": 0.8},
    {"id": "C2", "if_all": ["供电有问题"],                   "then": "换根电源线试试",   "cf": 0.9},
    {"id": "C3", "if_all": ["开机有滴滴声", "屏幕全黑"],     "then": "内存条没插稳",     "cf": 0.75},
    {"id": "C4", "if_all": ["内存条没插稳"],                 "then": "拔下内存擦金手指", "cf": 0.8},
]

看出门道了吗? R4 的前提 "得了叶斑病" 不是用户能直接观察到的事实,而是 R1 的结论。这就是链式推理:规则的输出可以当成另一条规则的输入。C1→C2、C3→C4 同理。这种链条能自动接起来,靠的不是你写了顺序,而是下一步那个引擎会反复扫。

第 2 步:写推理——CF 合并公式

接着往同一个文件里写。先写 MYCIN 的合并公式:

def combine_cf(old_cf, new_cf):
    """MYCIN 合并公式:两条规则都支持同一个结论时,置信度怎么叠加。

    都为正:cf1 + cf2 * (1 - cf1)      —— 越来越确信,但永远到不了 1.0
    都为负:cf1 + cf2 * (1 + cf1)
    一正一负:(cf1 + cf2) / (1 - min(|cf1|, |cf2|))
    """
    if old_cf >= 0 and new_cf >= 0:
        return old_cf + new_cf * (1 - old_cf)
    if old_cf < 0 and new_cf < 0:
        return old_cf + new_cf * (1 + old_cf)
    return (old_cf + new_cf) / (1 - min(abs(old_cf), abs(new_cf)))

为什么正正合并是 cf1 + cf2 × (1 − cf1) 而不是简单相加? 拆开看:cf1 是已有的确信度,1 − cf1 是"还没被说服的那部分",新证据只能去啃剩下这部分。0.7 和 0.5 合并 = 0.7 + 0.5 × 0.3 = 0.85。结果一定比两者都大(证据变多了,更信了),但永远够不到 1.0(再多间接证据也不等于亲眼所见)。 这是符号主义处理不确定性最优雅的一个设计,1970 年代的产物,今天做多来源证据融合还在用同样的思路。

第 3 步:写推理机——本实验的灵魂

def infer(rules, initial_facts, threshold=0.2):
    """正向推理引擎(forward chaining)。

    rules         —— 规则列表,引擎只看 if_all / then / cf 这三个键,不管里面写的是啥
    initial_facts —— 字典 {事实: 置信度}
    threshold     —— 置信度低于这个值的结论不采纳
    """
    facts = dict(initial_facts)   # 事实库:事实 -> 置信度
    trace = []                    # 推理链:人能读的每一步
    used = set()                  # 已经用过的规则。一条规则只准放一次,
                                  # 否则每扫一轮都合并一次,置信度会假性膨胀到 1。
    round_no = 0

    while True:
        round_no += 1
        something_new = False

        for rule in rules:
            if rule["id"] in used:
                continue
            # 前提里每一条都必须已经是事实
            if not all(p in facts for p in rule["if_all"]):
                continue

            # 结论的置信度 = 规则自身的 cf × 前提中最弱的那一环
            weakest = min(facts[p] for p in rule["if_all"])
            cf = rule["cf"] * weakest
            if cf < threshold:
                continue

            used.add(rule["id"])
            conclusion = rule["then"]
            premise_text = " 且 ".join(rule["if_all"])

            if conclusion not in facts:
                facts[conclusion] = cf
                trace.append("第%d轮 %s: 因为「%s」-> 得出「%s」 CF=%.3f"
                             % (round_no, rule["id"], premise_text, conclusion, cf))
            else:
                old = facts[conclusion]
                merged = combine_cf(old, cf)
                facts[conclusion] = merged
                trace.append("第%d轮 %s: 因为「%s」-> 加强「%s」 CF=%.3f 与 %.3f 合并 -> %.3f"
                             % (round_no, rule["id"], premise_text, conclusion,
                                old, cf, merged))
            something_new = True

        # 一整轮下来没有任何新东西,推理到此结束
        if not something_new:
            break

    return facts, trace

这三十行里有四个设计决策值得单独讲,每一个都是踩过坑才写成这样的:

决策 代码 不这么写会怎样
反复扫,直到没新东西 while True + something_new 只扫一遍,R1→R4 这种链式推理就断了:扫到 R4 时"得了叶斑病"还不在事实库里
规则只准用一次 used 集合 每轮都重复合并同一条规则,CF 会假性膨胀:0.7→0.91→0.973→…→无限接近 1.0,看起来越来越确信,其实一份证据被数了十遍
取最弱的一环 weakest = min(...) 前提里有一条只有 0.3 的把握,结论却敢给 0.9——链条的强度等于最弱一环,这是常识,但代码里很容易漏
低于阈值不采纳 if cf < threshold 微弱证据会不断堆积出一堆"CF=0.03 的可能诊断",把真正的结论淹掉

最关键的一点,请从头到尾找一遍:infer() 里出现过"叶斑病""内存条"这些字吗? 一次都没有。它只认识 if_all / then / cf 这三个键。这就是"知识与推理相分离"——它不是一句口号,它是"你从代码里搜不到领域词汇"这个可验证的事实。

第 4 步:打印结果 + 四个案例

def run_case(title, rules, initial_facts):
    print("=" * 64)
    print(title)
    print("=" * 64)
    print("已知事实:")
    for fact, cf in initial_facts.items():
        print("    - %s (CF=%.2f)" % (fact, cf))

    facts, trace = infer(rules, initial_facts)

    print("推理链:")
    if trace:
        for line in trace:
            print("    " + line)
    else:
        print("    (空) 一条规则的前提都没凑齐,引擎连一步都走不出去。")

    conclusions = {k: v for k, v in facts.items() if k not in initial_facts}
    print("结论:")
    if conclusions:
        for name, cf in sorted(conclusions.items(), key=lambda kv: -kv[1]):
            print("    %s  ->  CF=%.3f" % (name, cf))
    else:
        print("    >>> 无法得出结论 <<<")
        print("    规则库里没有任何一条规则提到这些事实。")
        print("    这就是专家系统的『脆』: 知识边界之外它一句话也说不出来,")
        print("    而且没有任何办法自己补上 —— 只能请专家来手写新规则。")
        print("    这个人工写规则的过程就叫『知识获取瓶颈』。")
    print()


if __name__ == "__main__":
    # 案例 1:知识库A 正常推理,能看到 R1 -> R4 的两步链式推理
    run_case("案例1 | 知识库A(植物病害) | 正常情况",
             RULES_PLANT,
             {"叶片有黄斑": 1.0, "空气潮湿": 1.0, "连续下雨": 1.0})

    # 案例 2:R1 和 R3 都指向叶斑病,看 MYCIN 合并,并看合并后的值继续往下传
    run_case("案例2 | 知识库A | 置信度合并(R1 和 R3 都指向叶斑病)",
             RULES_PLANT,
             {"叶片有黄斑": 1.0, "空气潮湿": 1.0, "叶缘焦枯": 1.0, "连续下雨": 1.0})

    # 案例 3:换知识库B,引擎源码一个字都没改
    run_case("案例3 | 知识库B(电脑故障) | 同一个 infer(),换了领域",
             RULES_COMPUTER,
             {"开机有滴滴声": 1.0, "屏幕全黑": 1.0})

    # 案例 4:规则没覆盖的事实 —— 本实验的重点
    run_case("案例4 | 知识库A | 规则没覆盖的事实(重点:脆)",
             RULES_PLANT,
             {"叶子被虫啃了洞": 1.0, "茎上爬满蚂蚁": 1.0})

第 5 步:运行

python3 expert_system.py

案例 1——链式推理跑通了:

================================================================
案例1 | 知识库A(植物病害) | 正常情况
================================================================
已知事实:
    - 叶片有黄斑 (CF=1.00)
    - 空气潮湿 (CF=1.00)
    - 连续下雨 (CF=1.00)
推理链:
    第1轮 R1: 因为「叶片有黄斑 且 空气潮湿」-> 得出「得了叶斑病」 CF=0.700
    第1轮 R4: 因为「得了叶斑病 且 连续下雨」-> 得出「喷波尔多液」 CF=0.560
结论:
    得了叶斑病  ->  CF=0.700
    喷波尔多液  ->  CF=0.560

✅ Checkpoint 2:R4 用上了 R1 的结论——两步链条自动接起来了,而你并没有告诉引擎"先跑 R1 再跑 R4"。0.560 = 0.8 × 0.7:诊断本身只有 0.7 的把握,基于它开的药方自然不可能比 0.7 更确定。不确定性会沿着推理链衰减,这个性质你在后面做 Agent 多步调用时会反复遇到(第四章会看到"每一步都可能出错,步数越多总成功率越低"的同一个数学)。

案例 2——CF 合并:

推理链:
    第1轮 R1: 因为「叶片有黄斑 且 空气潮湿」-> 得出「得了叶斑病」 CF=0.700
    第1轮 R3: 因为「叶片有黄斑 且 叶缘焦枯」-> 加强「得了叶斑病」 CF=0.700 与 0.500 合并 -> 0.850
    第1轮 R4: 因为「得了叶斑病 且 连续下雨」-> 得出「喷波尔多液」 CF=0.680
结论:
    得了叶斑病  ->  CF=0.850
    喷波尔多液  ->  CF=0.680

✅ Checkpoint 3:三件事同时发生了,逐个对: 1. 0.700 与 0.500 合并成了 0.850,正好是 0.7 + 0.5 × (1 − 0.7)。你可以手算一遍。 2. 它比两个输入都大,但没到 1.0——多了一个独立证据,更确信了,但没有变成"铁证"。 3. 合并后的 0.850 继续往下传,所以药方的 CF 从案例 1 的 0.560 涨到了 0.8 × 0.85 = 0.680。证据变强,连下游的建议都跟着变强了——这就是当年 MYCIN 敢把 CF 拿去辅助真实临床决策的原因。

案例 3——换知识库,引擎一个字不改:

推理链:
    第1轮 C3: 因为「开机有滴滴声 且 屏幕全黑」-> 得出「内存条没插稳」 CF=0.750
    第1轮 C4: 因为「内存条没插稳」-> 得出「拔下内存擦金手指」 CF=0.600
结论:
    内存条没插稳  ->  CF=0.750
    拔下内存擦金手指  ->  CF=0.600

✅ Checkpoint 4(本实验最重要的一个):从"诊断植物"切换到"修电脑",你改的只有 run_case 的一个参数,infer() 和 combine_cf() 一行都没动。请顺手把这句话验证一下——新建 check.py,让 Python 自己去读推理机的源码:

import inspect
import expert_system as es

src = inspect.getsource(es.infer) + inspect.getsource(es.combine_cf)
words = ["叶斑", "白粉", "内存", "电源", "病", "积水"]
hits = [w for w in words if w in src]
print("推理机源码里出现的领域词汇:", hits if hits else "无")
推理机源码里出现的领域词汇: 无

推理机的函数体里搜不到任何一个领域词汇。 这就是四十年前的架构智慧,也正是你第一章 TOOL_REGISTRY + _execute() 的同一个设计:把"会变的东西"(知识 / 工具清单)和"不变的东西"(引擎 / 派发逻辑)分开放。 面试被问"你怎么设计一个可扩展的工具系统",答这个思想就够了。

案例 4——知识边界之外:

已知事实:
    - 叶子被虫啃了洞 (CF=1.00)
    - 茎上爬满蚂蚁 (CF=1.00)
推理链:
    (空) 一条规则的前提都没凑齐,引擎连一步都走不出去。
结论:
    >>> 无法得出结论 <<<
    规则库里没有任何一条规则提到这些事实。
    这就是专家系统的『脆』: 知识边界之外它一句话也说不出来,
    而且没有任何办法自己补上 —— 只能请专家来手写新规则。
    这个人工写规则的过程就叫『知识获取瓶颈』。

✅ Checkpoint 5:推理链是空的。注意这个事实有多刺眼——"叶子被虫啃了洞"对任何一个花农来说都是极其明确的症状,但它不在那 6 条规则里,系统就一个字也说不出来。

这里是本章最值得停下来想三分钟的地方。把同样的输入给今天的 LLM: 它会告诉你可能是哪几种虫害、建议你翻开叶背看看、提醒你蚂蚁通常是跟着蚜虫来的。它凭什么能答?因为它从互联网上几百万篇园艺文章里读到过这些,没有任何人为它手写规则。

这一个对比,就是符号主义与预训练时代的全部差距。 不是"LLM 更聪明",而是知识的获取方式换了:从"雇工程师访谈专家、一条条录入"换成"自监督学习自动吸收"。1970 年代最聪明的一批人被这堵墙挡了三十年,最后不是靠更努力地写规则翻过去的,是靠换了条路绕过去的。

但也别把专家系统一脚踢开。 它有一样东西 LLM 至今给不了:那条推理链是真的、可审计的、能一步步复查的。 LLM 说"可能是蚜虫",你没法验证它内部到底为什么这么说。所以今天在责任重大、需要审计留痕的场景(金融风控规则、医保报销核算、合规检查),规则引擎仍然是主力——准确说,是让模型负责"理解和表达",让规则负责"判定和留痕"。 这正是第一章习题 3 方案 C 的思路,两章在这里对上了。


实验三:心智社会——让"涌现"发生在你眼前,再把它弄塌

3.2 节说"没有任何一层懂全局,塔却搭起来了"。这句话听起来像哲学,但它是可以跑出来的。这个实验只有一百多行,做完你会对"涌现"有一个再也忘不掉的画面。

名词补课——全局变量:写在函数外面的变量,所有函数都能看到。本实验用 BLOCKS(地上的积木)、TOWER(搭好的塔)、HAND(机械手里抓着什么)三个全局变量来当"世界"。真实项目里应该避免滥用全局变量,这里故意这么写,是为了让你能一眼看到"世界的状态",不被参数传递干扰。

第 1 步:新建 society_of_mind.py,先写"世界"和"激活"机制

# -*- coding: utf-8 -*-
"""实验 3:心智社会 —— 用一群"无心"的笨函数搭一座塔"""

DEPTH = 0          # 当前调用深度,只为打印缩进用
BLOCKS = []        # "世界":地上散落的积木
TOWER = []         # "世界":已经搭好的塔
HAND = None        # "世界":机械手里现在抓着什么
GRASP_BROKEN = False   # 抓握工是否坏了


def say(who, what):
    print("  " * DEPTH + "[%s] %s" % (who, what))


def activate(agent, *args):
    """激活一个智能体。唯一的通信方式就是这个 —— 没有共享计划,没有全局目标说明。"""
    global DEPTH
    DEPTH += 1
    result = agent(*args)
    DEPTH -= 1
    return result

activate() 这个函数是整个实验的思想所在,值得多看两眼。 它只做一件事:调用下一层,然后把结果带回来。它不传递目标、不传递计划、不传递"我们为什么要这么做"。 明斯基说的"去中心化的激活与抑制",落到代码上就是这么朴素的一个函数调用。

DEPTH 那两行只是为了让日志带缩进,方便你看层级。但这个缩进会成为本实验最直观的教具——你会看到一个漂亮的树形结构,而每一层都不知道自己身处一棵树里。

第 2 步:最底层的三个"无心"工人

def SEE_SHAPE():
    """只会一件事:报告地上还有没有积木。它不知道积木要拿去干什么。"""
    if BLOCKS:
        say("SEE-SHAPE", "看见地上有积木: %s" % BLOCKS[0])
        return BLOCKS[0]
    say("SEE-SHAPE", "地上空了")
    return None


def REACH(target):
    """只会把手臂伸过去。"""
    say("REACH", "手臂伸向 %s" % target)
    return True


def GRASP(target):
    """只会握紧手。它不知道自己握的是积木还是别的什么。"""
    global HAND
    if GRASP_BROKEN:
        say("GRASP", "握不住! 手指打滑 -> 返回 False")
        return False
    HAND = target
    say("GRASP", "握住了 %s" % target)
    return True


def RELEASE():
    global HAND
    say("RELEASE", "松手,放下 %s" % HAND)
    HAND = None
    return True

请注意这四个函数里没有出现过"塔"这个字。 它们就是明斯基说的"无心的心智过程"——有能力,没有理解。

第 3 步:中层的三个小组

def FIND_BLOCK():
    say("FIND-BLOCK", "被激活,去找一块积木")
    return activate(SEE_SHAPE)


def GET_BLOCK(target):
    say("GET-BLOCK", "被激活,去取 %s" % target)
    if not activate(REACH, target):
        return False
    if not activate(GRASP, target):
        say("GET-BLOCK", "下属 GRASP 失败,我也只能失败 -> 返回 False")
        return False
    BLOCKS.remove(target)
    return True


def PUT_ON_TOP():
    say("PUT-ON-TOP", "被激活,把手里的东西放到最上面")
    TOWER.append(HAND)
    activate(RELEASE)
    say("PUT-ON-TOP", "现在塔是 %s" % TOWER)
    return True

GET_BLOCK 里那句 "下属 GRASP 失败,我也只能失败" 是本实验的第二个看点。它不是在处理错误——它是在承认自己无能。 它没有备用方案,因为它压根不知道自己在干什么大事,也就无从想象"换个方式"。记住这句话,第 6 步会用到。

第 4 步:上面三层——注意最顶层什么都不会干

def ADD_BLOCK():
    say("ADD-BLOCK", "被激活,加一块积木")
    target = activate(FIND_BLOCK)
    if target is None:
        say("ADD-BLOCK", "没积木可加 -> 返回 False")
        return False
    if not activate(GET_BLOCK, target):
        say("ADD-BLOCK", "下属 GET-BLOCK 失败,我也只能失败 -> 返回 False")
        return False
    activate(PUT_ON_TOP)
    return True


def BUILDER(target_height):
    """整个系统里唯一像"计划"的东西,只有一句:塔没搭完就继续加。"""
    say("BUILDER", "被激活,只知道一件事: 塔没到 %d 层就继续加" % target_height)
    while len(TOWER) < target_height:
        if not activate(ADD_BLOCK):
            say("BUILDER", "下属 ADD-BLOCK 失败,我停在 %d 层 -> 返回 False" % len(TOWER))
            return False
    say("BUILDER", "塔到 %d 层了,我的活干完了" % len(TOWER))
    return True


def BUILD_TOWER(target_height):
    """总经理:什么具体活都不会干,只会激活 BUILDER。"""
    say("BUILD-TOWER", "收到目标: 搭一座 %d 层的塔。我自己不会搭,激活 BUILDER。"
        % target_height)
    ok = activate(BUILDER, target_height)
    say("BUILD-TOWER", "BUILDER 回报 %s" % ("成功" if ok else "失败"))
    return ok

BUILDER 里那个 while 是整个系统里唯一一点点像"计划"的东西,而它的全部内容只有一句:"没到目标高度就再来一次"。它不知道要来几次,不知道每次会发生什么,不知道积木在哪。"搭一座三层塔"这个复杂行为,就是从这一句极其贫瘠的循环条件 + 一堆无心的工人里长出来的。

第 5 步:跑场景一——看"涌现"

def reset(n_blocks):
    global BLOCKS, TOWER, HAND
    BLOCKS = ["积木%d" % i for i in range(1, n_blocks + 1)]
    TOWER = []
    HAND = None


if __name__ == "__main__":
    print("=" * 64)
    print("场景1 | 一切正常 —— 看『涌现』")
    print("=" * 64)
    reset(3)
    GRASP_BROKEN = False
    BUILD_TOWER(3)
    print("\n最终世界状态: 塔=%s 地上=%s" % (TOWER, BLOCKS))
    print("注意: 从头到尾没有任何一个函数里写着完整计划。")
    print("     BUILD-TOWER 不知道要伸手,GRASP 不知道有塔。")
    print("     『搭一座三层塔』这个行为是从互相激活里涌现出来的。\n")
python3 society_of_mind.py

(下面只摘了前两块积木,第三块的结构完全一样)

================================================================
场景1 | 一切正常 —— 看『涌现』
================================================================
[BUILD-TOWER] 收到目标: 搭一座 3 层的塔。我自己不会搭,激活 BUILDER。
  [BUILDER] 被激活,只知道一件事: 塔没到 3 层就继续加
    [ADD-BLOCK] 被激活,加一块积木
      [FIND-BLOCK] 被激活,去找一块积木
        [SEE-SHAPE] 看见地上有积木: 积木1
      [GET-BLOCK] 被激活,去取 积木1
        [REACH] 手臂伸向 积木1
        [GRASP] 握住了 积木1
      [PUT-ON-TOP] 被激活,把手里的东西放到最上面
        [RELEASE] 松手,放下 积木1
      [PUT-ON-TOP] 现在塔是 ['积木1']
    [ADD-BLOCK] 被激活,加一块积木
      [FIND-BLOCK] 被激活,去找一块积木
        [SEE-SHAPE] 看见地上有积木: 积木2
      [GET-BLOCK] 被激活,去取 积木2
        [REACH] 手臂伸向 积木2
        [GRASP] 握住了 积木2
      [PUT-ON-TOP] 被激活,把手里的东西放到最上面
        [RELEASE] 松手,放下 积木2
      [PUT-ON-TOP] 现在塔是 ['积木1', '积木2']

最后收尾:

  [BUILDER] 塔到 3 层了,我的活干完了
[BUILD-TOWER] BUILDER 回报 成功

最终世界状态: 塔=['积木1', '积木2', '积木3'] 地上=[]
注意: 从头到尾没有任何一个函数里写着完整计划。
     BUILD-TOWER 不知道要伸手,GRASP 不知道有塔。
     『搭一座三层塔』这个行为是从互相激活里涌现出来的。

✅ Checkpoint 6:塔是 ['积木1', '积木2', '积木3'],地上空了。现在做一件事:从上往下把每个函数读一遍,找出"计划"写在哪里。 你会发现:

  • BUILD-TOWER 只有一句"激活 BUILDER";
  • BUILDER 只有一句"没到高度就再来一次";
  • GRASP 连"积木"是干什么用的都不知道。

计划不在任何一个函数里,它在缩进里——在调用关系里。 那个漂亮的树形日志,就是"智能藏在连接方式中"这句抽象话的可视化。这就是涌现。

第 6 步:跑场景二——把 GRASP 弄坏(本实验的真正目的)

在 __main__ 末尾追加:

    print("=" * 64)
    print("场景2 | 最底层的 GRASP 坏了 —— 看失败如何一路冒泡")
    print("=" * 64)
    reset(3)
    GRASP_BROKEN = True
    BUILD_TOWER(3)
    print("\n最终世界状态: 塔=%s 地上=%s" % (TOWER, BLOCKS))
    print("注意: 坏的只是最底层一个『握紧手』的动作,")
    print("     但 GET-BLOCK / ADD-BLOCK / BUILDER / BUILD-TOWER 全部跟着失败。")
    print("     没有哪一层能『绕过去』,因为没有哪一层懂全局、能想别的办法。")
    print("     这就是去中心化的代价: 涌现出来的能力,也会被一个点整体带塌。")
================================================================
场景2 | 最底层的 GRASP 坏了 —— 看失败如何一路冒泡
================================================================
[BUILD-TOWER] 收到目标: 搭一座 3 层的塔。我自己不会搭,激活 BUILDER。
  [BUILDER] 被激活,只知道一件事: 塔没到 3 层就继续加
    [ADD-BLOCK] 被激活,加一块积木
      [FIND-BLOCK] 被激活,去找一块积木
        [SEE-SHAPE] 看见地上有积木: 积木1
      [GET-BLOCK] 被激活,去取 积木1
        [REACH] 手臂伸向 积木1
        [GRASP] 握不住! 手指打滑 -> 返回 False
      [GET-BLOCK] 下属 GRASP 失败,我也只能失败 -> 返回 False
    [ADD-BLOCK] 下属 GET-BLOCK 失败,我也只能失败 -> 返回 False
  [BUILDER] 下属 ADD-BLOCK 失败,我停在 0 层 -> 返回 False
[BUILD-TOWER] BUILDER 回报 失败

最终世界状态: 塔=[] 地上=['积木1', '积木2', '积木3']
注意: 坏的只是最底层一个『握紧手』的动作,
     但 GET-BLOCK / ADD-BLOCK / BUILDER / BUILD-TOWER 全部跟着失败。
     没有哪一层能『绕过去』,因为没有哪一层懂全局、能想别的办法。
     这就是去中心化的代价: 涌现出来的能力,也会被一个点整体带塌。

✅ Checkpoint 7(本章最有价值的一个实验结果):塔是空的 [],三块积木一块没动。坏掉的只是一个"握紧手"的动作,但整条链从下到上四层全部报失败。

这个结果值得展开讲,因为它直接对应你今后做多智能体系统时最常踩的坑。

为什么没有哪一层能"想办法绕过去"? 因为绕过去需要理解全局目标。要想出"抓不住?那试试用两只手推""或者干脆换一块表面粗糙的积木",你必须知道"我们的目标是把积木摞起来"。而这条链上没有任何一层知道这件事——GET-BLOCK 只知道"取那块积木",ADD-BLOCK 只知道"加一块"。去中心化换来了简洁和涌现,代价就是失去了全局视野,也就失去了变通能力。

这正是今天多智能体框架最难的部分。 你在第十一章会看到:一堆各司其职的 Agent 组队干活,一个环节的输出格式不对,整条流水线就停了,而且每个 Agent 都"觉得自己没错"——它按自己的职责做了该做的事。工程上的解法通常是这三条,都不便宜:

解法 做法 代价
引入"知道全局的角色" 加一个 Orchestrator / Supervisor,它持有目标,能在下层失败时重新规划 牺牲了纯粹的去中心化——变成半中心化架构
每层都能局部重试 GET-BLOCK 失败了自己换个抓法再试三次 每层都要写重试逻辑,复杂度上升,还可能重试风暴
失败信息带足上下文往上传 不只返回 False,而是返回"为什么失败、试过什么" 这就是第一章 _execute() 里"把报错内容返回给模型"的同一个思想:上层要能变通,前提是它收到的失败信息里有足够的判断依据

第三条最值得记。 对比一下:这个实验里 GRASP 只返回 False,上层什么也不知道;而第一章的 _execute() 返回的是 错误:没有名为「get_wether」的工具,可用工具:['get_weather', 'get_attraction']——信息量完全不同,所以 LLM 能自己改对,而 ADD-BLOCK 只能躺平。

一句话:涌现给你能力,但不给你韧性。韧性要靠"某处存在全局视野" + "失败信息足够丰富"来买。

改一改(推荐做):让 GRASP 有 30% 的概率打滑(if random.random() < 0.3),然后给 GET_BLOCK 加上"失败就重试三次"。你会看到塔又能搭起来了——这是你第一次亲手给一个涌现系统装上韧性。


实验四:表格 Q-learning——让机器在试错中学会走迷宫,再把它变傻

4.2 节讲了强化学习的五要素和"追求长远回报"。这个实验让你亲眼看见一个策略 π 从全是 0 的白纸上长出来,然后只改一个数字(γ 从 0.9 变 0),看它当场变成傻子。

名词补课——三个词,看懂就够: - Q 表:一张大表格,记录"在某个格子做某个动作,预计最终能拿到多少总回报"。学习的全过程就是把这张表填对。表的键是"(状态, 行动)",值是一个数。 - ε-贪心(epsilon-greedy):ε=0.2 意思是"20% 的时候随便走一步(探索,可能发现更好的路),80% 的时候走目前认为最好的(利用)"。不探索会困在第一次偶然发现的烂路上,不利用则永远学不到东西——这个平衡叫"探索-利用困境",是强化学习的经典难题。 - 学习率 α:每次更新往新估计的方向走多少。α=0.5 = 走一半。太大会震荡,太小学得慢。

第 1 步:新建 q_learning.py,先写"环境"

环境是一个 4×4 的格子世界。注意:环境的全部逻辑就是一个函数——给它状态和行动,它返回新状态、奖励、是否结束。这个签名就是强化学习的标准接口(OpenAI Gym 的 step() 也是这样)。

# -*- coding: utf-8 -*-
"""实验 4:表格 Q-learning —— 让机器在试错中学会走迷宫,
      并亲眼看折扣因子 γ 从 0.9 变成 0 之后,智能体怎么变傻。"""
import random

# 环境:一个 4x4 的格子世界
#   S 起点(0,0)   G 终点(3,3) 奖励 +1   X 陷阱(2,1) 奖励 -1
#   每走一步奖励 0(不额外惩罚),踩到 G 或 X 就结束
SIZE = 4
START = (0, 0)
GOAL = (3, 3)
TRAP = (2, 1)

ACTIONS = ["上", "下", "左", "右"]
MOVE = {"上": (-1, 0), "下": (1, 0), "左": (0, -1), "右": (0, 1)}
ARROW = {"上": "↑", "下": "↓", "左": "←", "右": "→"}


def step(state, action):
    """环境的全部逻辑:给我状态和行动,我返回新状态、奖励、是否结束。"""
    dr, dc = MOVE[action]
    r, c = state[0] + dr, state[1] + dc
    if not (0 <= r < SIZE and 0 <= c < SIZE):
        return state, 0.0, False          # 撞墙:原地不动
    new = (r, c)
    if new == GOAL:
        return new, 1.0, True
    if new == TRAP:
        return new, -1.0, True
    return new, 0.0, False

对照 4.2 节的五要素,在这段代码里逐个指出来(这是一道很常见的面试基本功题):

五要素 在代码里是什么
环境 step() 函数 + SIZE/GOAL/TRAP 这些常量
状态 state,一个 (行, 列) 元组。整个世界只有 16 个状态
行动 ACTIONS 里的四个方向
奖励 step() 返回的第二个值:到终点 +1,踩陷阱 −1,其余一律 0
智能体 下面要写的 train() 和那张 Q 表

请特别注意"其余一律 0"这一点,它是本实验的关键设计。 环境没有给"朝终点靠近"任何奖励——它只在终点给一次 +1。所以智能体必须自己想明白"走到某个格子虽然当场没奖励,但它离终点更近了"。这个'想明白'的机制,全靠下一步那个 γ。

第 2 步:写智能体——那一行核心公式

def train(gamma, episodes=800, alpha=0.5, epsilon=0.2, seed=42):
    """Q-learning。Q[(状态,行动)] = 在这个状态做这个动作,预期能拿到的总回报。

    更新公式(整个强化学习最核心的一行):
        Q(s,a) <- Q(s,a) + alpha * [ r + gamma * max_a' Q(s',a') - Q(s,a) ]
                                     ^^^^^^^^^^^^^^^^^^^^^^^^
                                     "眼前的奖励" + "折扣后的未来最好情况"
    gamma 就掐在"未来"那一项前面。gamma=0 时整个未来项归零,
    Q(s,a) 只剩下"眼前这一步的奖励" —— 智能体彻底短视。
    """
    rng = random.Random(seed)
    Q = {(s, a): 0.0
         for s in [(r, c) for r in range(SIZE) for c in range(SIZE)]
         for a in ACTIONS}

    for _ in range(episodes):
        state = START
        for _ in range(100):                       # 单局步数上限,防死循环
            if rng.random() < epsilon:             # 探索:随便走一步
                action = rng.choice(ACTIONS)
            else:                                  # 利用:走目前认为最好的
                action = best_action(Q, state)
            new_state, reward, done = step(state, action)

            future = 0.0 if done else max(Q[(new_state, a)] for a in ACTIONS)
            Q[(state, action)] += alpha * (reward + gamma * future - Q[(state, action)])

            state = new_state
            if done:
                break
    return Q


def best_action(Q, state):
    """挑 Q 值最大的行动。并列时按 上下左右 的顺序取第一个 —— 保证可复现。"""
    return max(ACTIONS, key=lambda a: (Q[(state, a)], -ACTIONS.index(a)))

那一行更新公式请用大白话读一遍,它值得你花五分钟彻底搞懂:

Q(s,a) ← Q(s,a) + α × [ r + γ·max Q(s',·) − Q(s,a) ]

中括号里那一坨叫时序差分误差(TD error),意思是"我原本以为能拿多少(Q(s,a)),实际体验之后觉得能拿多少(r + γ·max Q(s',·)),差了多少"。然后按学习率 α 朝这个差的方向挪一点。整个强化学习就是"不断修正自己的预期"这一件事。

而 r + γ·max Q(s',·) 拆成两半就是 4.2 节那句话的数学形式:r 是眼前的奖励,γ·max Q(s',·) 是打了折的未来最好情况。"会忍"就写在这里——即使 r=0,只要下一个格子的 Q 值高,这一步的 Q 值也会被抬起来。

best_action 里那个 -ACTIONS.index(a) 是个工程细节但很重要:Q 值并列时(比如全是 0),max 默认返回第一个,但这个"第一个"依赖字典遍历顺序,不同环境可能不同。显式指定并列时的顺序,你跑出来的结果才能和这份文档一模一样——这也是 random.Random(seed) 的用意。可复现性是调试强化学习的命根子,否则你根本分不清"改动生效了"还是"这次随机数好"。

第 3 步:把学到的东西画出来

def show_value_map(Q, title):
    print(title)
    for r in range(SIZE):
        cells = []
        for c in range(SIZE):
            if (r, c) == GOAL:
                cells.append("  GOAL")
            elif (r, c) == TRAP:
                cells.append("  TRAP")
            else:
                cells.append("%6.3f" % max(Q[((r, c), a)] for a in ACTIONS))
        print("   " + " ".join(cells))


def show_policy_map(Q, title):
    print(title)
    for r in range(SIZE):
        cells = []
        for c in range(SIZE):
            if (r, c) == GOAL:
                cells.append("G")
            elif (r, c) == TRAP:
                cells.append("X")
            else:
                cells.append(ARROW[best_action(Q, (r, c))])
        print("   " + " ".join(cells))


def rollout(Q, limit=20):
    """训练完之后关掉探索,纯按学到的策略走一遍,看它到底行不行。"""
    state, path, total = START, [START], 0.0
    for _ in range(limit):
        action = best_action(Q, state)
        state, reward, done = step(state, action)
        path.append(state)
        total += reward
        if done:
            return path, total, True
    return path, total, False


def report(gamma):
    print("=" * 64)
    print("γ = %.1f" % gamma)
    print("=" * 64)
    Q = train(gamma)
    show_value_map(Q, "学到的价值地图(每格里最大的那个 Q 值):")
    show_policy_map(Q, "学到的策略 π(每格该往哪走):")
    path, total, done = rollout(Q)
    print("按策略实走: %s" % " -> ".join(str(p) for p in path))
    if done:
        print("结果: %d 步后结束,拿到总回报 %.1f" % (len(path) - 1, total))
    else:
        print("结果: 走了 %d 步还没结束,总回报 %.1f —— 它迷路了。"
              % (len(path) - 1, total))
    print()


if __name__ == "__main__":
    report(0.9)
    report(0.0)

rollout() 里 limit=20 这个保险丝,和第一章 agent.py 里的 MAX_STEPS 是完全一样的东西:任何带循环的自主系统都必须有一个硬性步数上限,否则出问题时它会一直转下去。这个实验里它会救你一次——第 5 步就会看到。

第 4 步:运行,看 γ=0.9 学到了什么

python3 q_learning.py
================================================================
γ = 0.9
================================================================
学到的价值地图(每格里最大的那个 Q 值):
    0.590  0.656  0.729  0.810
    0.531  0.590  0.810  0.900
    0.419   TRAP  0.729  1.000
    0.000  0.000  0.000   GOAL
学到的策略 π(每格该往哪走):
   → → → ↓
   ↑ ↑ → ↓
   ↑ X ↑ ↓
   ↑ ↑ ↑ G
按策略实走: (0, 0) -> (0, 1) -> (0, 2) -> (0, 3) -> (1, 3) -> (2, 3) -> (3, 3)
结果: 6 步后结束,拿到总回报 1.0

✅ Checkpoint 8:三件事要逐个看清:

  1. 价值地图上出现了一条"山坡"。 从起点 0.590 一路升到终点旁边的 1.000,离终点越近数值越大。这个坡不是谁画的,是从"终点 +1"这唯一一个奖励信号里,通过 γ 一层层往回渗透出来的。你可以验算:0.9 × 1.0 = 0.9(终点上方那格),0.9 × 0.9 = 0.81,0.9 × 0.81 = 0.729……每往回一格,乘一次 0.9。这就是折扣因子在物理上的含义。
  2. 策略 π 是一串箭头,而且它是一条真能走通的路。 从 (0,0) 沿最上一行往右,到 (0,3) 再一路往下——6 步,绕开了陷阱,拿到 +1。
  3. 最下面一行是 0.000 0.000 0.000。 这不是 bug——那三格从来没被走到过(学到的路走上面),所以它们的 Q 值一次都没更新过。Q-learning 只学它到过的地方,这是表格法最大的局限:状态一多就学不完(围棋有 10¹⁷⁰ 个状态,表格法当场爆炸)。这正是需要神经网络来"猜"没见过的状态的原因——深度强化学习就是这么来的,AlphaGo 就站在这个交叉点上。

它学到的不是"这一步有奖励",而是"这一步能通向奖励"。 这句话就是强化学习和监督学习最本质的区别:没有人告诉它每一步的正确答案,它只在最后收到一个分数,然后自己把功劳/责任分摊到之前的每一步上(这个难题有个名字:信用分配问题,Credit Assignment Problem)。

第 5 步:把 γ 改成 0,看它变傻

代码不用改——report(0.0) 已经在 __main__ 里了:

================================================================
γ = 0.0
================================================================
学到的价值地图(每格里最大的那个 Q 值):
    0.000  0.000  0.000  0.000
    0.000  0.000  0.000  0.000
    0.000   TRAP  0.000  1.000
    0.000  0.000  0.500   GOAL
学到的策略 π(每格该往哪走):
   ↑ ↑ ↑ ↑
   ↑ ↑ ↑ ↑
   ↑ X ↑ ↓
   ↑ ↑ → G
按策略实走: (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0) -> (0, 0)
结果: 走了 20 步还没结束,总回报 0.0 —— 它迷路了。

✅ Checkpoint 9(本实验的高潮):同一份代码、同样训练 800 局,只把一个 0.9 改成 0,智能体就从"6 步走到终点"退化成"在起点原地撞墙 20 次"。请对照着看清三件事:

  1. 价值地图几乎全是 0。 只有紧挨着 GOAL 的两格非零(1.000 和 0.500)。为什么? 因为 γ=0 让更新公式变成了 Q(s,a) ← Q(s,a) + α × [r − Q(s,a)]——未来项被彻底抹掉了。奖励信号一步都传不回来,它只能学到"紧挨着终点的那一步有奖励",再往前一格就什么都不知道了。
  2. 策略退化成了一片 ↑。 因为绝大多数格子的四个 Q 值全是 0,完全并列,best_action 只能按我们指定的"上下左右"顺序返回第一个——"↑" 不是它的选择,是它没得选。 这一片整齐的箭头看起来煞有介事,实际上是一张白纸。
  3. 它在 (0,0) 原地撞墙 20 次,靠 limit=20 那个保险丝才停下。 起点在左上角,往上是墙,step() 返回"原地不动",于是它又选"↑",再撞……如果没有那个步数上限,这个程序会永远转下去。

这就是"会忍"和"不会忍"的差别,而它只是一个参数。 γ 掐在"未来"那一项前面:γ 大,奖励能沿着路径往回渗透,智能体学会"眼前没好处但通向好处"的动作;γ=0,它只认当场兑现的东西,于是永远学不会任何需要铺垫的行为。

把这件事翻译到做 Agent 上: 一个 Agent 如果只优化"这一步的回复看起来好不好",它就永远学不会"先花一步调用工具查证,虽然这一步用户看不到任何有用输出"。RLHF(第三章会讲)训练模型时同样面临这个问题——奖励信号来自人类对最终回复的偏好,中间那些思考和工具调用的功劳怎么分摊,正是这个信用分配难题的现代版。七十年前的 γ,今天还在同一个位置上发挥作用。

改一改(推荐做,两个都跑一下,输出如下):

把 report(0.9) 改成 report(0.5):

学到的价值地图(每格里最大的那个 Q 值):
    0.031  0.062  0.125  0.250
    0.062  0.125  0.250  0.500
    0.000   TRAP  0.500  1.000
    0.000  0.000  0.000   GOAL
按策略实走: (0, 0) -> (0, 1) -> (0, 2) -> (0, 3) -> (1, 3) -> (2, 3) -> (3, 3)
结果: 6 步后结束,拿到总回报 1.0

山坡还在,路一模一样,但数字整体小了一个量级(0.031 vs 刚才的 0.590),因为每往回一格乘的是 0.5 而不是 0.9,衰减快得多。结论:γ 只要不是 0,它就能学到路;γ 的大小决定的是"它愿意为多远的未来忍多久"。

再把 episodes=800 改成 episodes=50(训练不足):

学到的价值地图(每格里最大的那个 Q 值):
    0.000  0.000  0.000  0.000
    0.000  0.000  0.000  0.000
    0.000   TRAP  0.000  0.000
    0.000  0.000  0.000   GOAL
结果: 走了 20 步还没结束,总回报 0.0 —— 它迷路了。

全零,连终点旁边那格都是 0——50 局里它一次都没走到过终点,所以从来没收到过那个 +1。这是"训练不充分"长什么样,而且它和 γ=0 的失败长得几乎一样,但病因完全不同:一个是没收到信号,一个是收到了传不回来。 顺手把 episodes 试到 100,就能成功了(6 步,回报 1.0)——从"完全学不会"到"学会了"之间只差 50 局,强化学习的训练曲线常常就是这种悬崖式的。

这个对照有个很实际的用处:调试强化学习时,"跑不通"至少有三种完全不同的病因,症状却一样。

症状 可能病因 怎么区分
策略全是同一个方向、价值全 0 训练局数不够,从未触达过奖励 看终点旁边那格的 Q 值:是 0 就说明连一次都没到过
策略全是同一个方向,只有终点旁边非 0 γ 太小,信号传不回来 看价值地图有没有"山坡"形状
价值在涨但策略乱跳 α 太大(震荡)或 ε 太大(一直在瞎走) 把 α 降到 0.1、ε 降到 0.05 再看

先看价值地图,再看策略——顺序反了会误诊。 策略图是价值图的结果,价值图才是病历。


踩坑与专家提示

这一章的"坑"分两类:做实验时会踩的,和把历史教训用错时会踩的。后者更贵。

实验层面的坑

# 坑 症状 解法 本章哪里演示过
1 推理机只扫一遍规则 链式推理断掉,R4 永远不触发 while True + something_new 反复扫到收敛 实验二第 3 步
2 同一条规则被反复合并 CF 假性膨胀,看起来越来越确信 used 集合,一条规则只准放一次 实验二第 3 步的决策表
3 结论 CF 不看前提强弱 前提只有 0.3 的把握,结论敢给 0.9 weakest = min(...),链条强度 = 最弱一环 实验二第 3 步
4 正则要求关键词紧挨着 中文"我不需要"整条不匹配,静默掉进兜底 无解——这就是规则法的天花板 实验一第 3 步
5 规则按顺序匹配、命中即返回 前面的宽泛规则(.*你好.*)抢掉后面精确规则的答 精确规则放前面;或改成"全部匹配后按优先级选" 实验一第 3 步第 3 点
6 循环没有步数上限 γ=0 时智能体原地撞墙,程序永不停止 limit=20 / MAX_STEPS——任何自主循环都必须有保险丝 实验四第 5 步
7 Q 值并列时靠字典顺序 换台机器跑,策略图不一样,无法复现 显式指定并列时的次序 + random.Random(seed) 实验四第 2 步
8 只看策略图调试 RL 三种完全不同的病因症状一样,误诊 先看价值地图(病历),再看策略图(结果) 实验四改一改
9 失败只返回 False 上层拿不到任何信息,只能整体躺平 失败信息要带上"为什么、试过什么" 实验三第 6 步

认知层面的坑——这些更致命

坑 A:把"规则系统"当成过时的东西,全用模型替代。

这是学完这一章最容易犯的错。历史讲的是"符号主义走不进开放世界",不是"规则没用了"。实验二那条可审计的推理链,是 LLM 至今给不了的东西。正确的姿势是第一章习题 3 的方案 C:

让模型负责"理解"和"表达",让代码/规则负责"判断"和"执行"。

判断标准是错了的代价:错了只是尴尬(措辞、排版、摘要)→ 交给模型;错了要赔钱、要担责、要过审计(金额计算、权限判定、合规检查)→ 交给规则。用 LLM 去算退款金额,是这一章最该避免的错误。

坑 B:以为"多 Agent 协作"天然比"单 Agent"强。

实验三的场景二就是反例:去中心化换来了简洁和涌现,代价是没有全局视野,也就没有变通能力。 一个环节坏了,整条链躺平,而且每个成员都"觉得自己没错"。加 Agent 之前先问一句:这个任务真的需要分工吗?还是我只是想让架构图好看? 第十一章会给出判断标准。

坑 C:把 ELIZA 效应当成别人的毛病。

你会在自己的项目里犯这个错,形式是:模型输出格式漂亮、语气自信,你就默认它答对了。 第一章那个 _keep_first_pair() 的存在就是这个原因——模型能一口气编出连 Observation 都自己写好的完整对话,看起来完美,全是幻觉。对治方法只有一个:验证输出,不要验证观感。 每个 ✅ Checkpoint 都在训练这个习惯。

坑 D:把年份和人名当成这一章的内容。

面试不会问"PSSH 是哪一年提出的",会问"你怎么看 LLM Agent 的可解释性问题"、"为什么你这里用规则而不用模型"。这一章的价值全在因果链上:每一代范式解决了上一代的什么死穴,又留下了什么新死穴。 年份只是挂钩子的钉子。


面试视角

Q1:请用两分钟讲一下 AI 智能体的发展史。

按"问题驱动"这条线讲,别按年份讲:

大致分三个阶段。第一阶段符号主义,代表是专家系统和 SHRDLU,信念是"智能等于摆弄符号",1976 年纽厄尔和西蒙的物理符号系统假说是它的理论基石。它在封闭领域很成功——MYCIN 的诊断水平达到人类专家,深蓝 1997 年赢了国际象棋世界冠军。但它撞上三堵墙:知识获取瓶颈(规则要人手写,专家的经验很多说不清)、常识无底洞(Cyc 项目手工录常识几十年收效有限)、框架问题和脆弱性(世界一变就失灵,遇到规则外的情况直接崩,不是降级)。

第二阶段是学习,分三级台阶。连接主义(1986 年反向传播进入主流,2012 年后深度学习爆发)解决了感知;强化学习(TD-Gammon → AlphaGo)解决了序贯决策,核心是最大化累积回报而不是眼前奖励;预训练(自监督学习 + 微调)真正解开了知识获取瓶颈——因为"猜下一个词"不需要人工标注,全互联网的文本都变成了训练数据。

第三阶段是会师。 现代智能体用 LLM 当大脑(连接主义的产物),干的活是推理和规划(符号主义的梦想),用 RLHF 打磨(行为主义),多 Agent 协作放大(明斯基 1986 年的心智社会)。有意思的是,LLM 内部没有可区分的符号,但它输出的 Thought/Action 全是符号结构——所以它挑战了 PSSH 的必要性论断,却在行为上印证了充分性。

Q2:物理符号系统假说的两个论断分别是什么?LLM 驱动的智能体符合吗?(习题 1 的核心)

充分性论断:任何物理符号系统都具备产生通用智能行为的充分手段——说的是"可以",给了研究者信心。必要性论断:任何展现通用智能行为的系统,其本质必然是一个物理符号系统——说的是"只能",这才是霸道的那一条。

LLM Agent 的答案是"一半符合":它的内核是神经网络,知识以分布式权重存在,内部没有可区分的符号——这直接反驳了必要性。但它工作时输出的 Thought:、Action: get_weather(city="北京")、生成的计划和代码,全都是标准的符号结构,而且它确实靠操作这些符号完成了推理和规划——这印证了充分性。所以现代智能体是"亚符号内核 + 符号外壳"。

追问:哪些实践问题挑战了充分性? 严格说,三堵墙挑战的是"符号主义这条工程路径",而不是充分性论断本身——假说说的是"符号系统具备产生智能的充分手段",没说"人类有能力把这些手段都写出来"。知识获取瓶颈是一个工程可行性问题,不是逻辑反驳。 这个区分很能体现思辨深度。

Q3:MYCIN 技术上成功了,为什么没进临床?(习题 2)

除了知识获取瓶颈和脆弱性,还有四层非技术阻力,这四条今天做 Agent 落地时一条不少:

阻力 1970 年代的样子 今天的样子
责任归属 诊断错了,是医生的责任还是系统的? AI 客服承诺了一个不存在的优惠,谁兑现?
法律与监管 医疗器械要审批,规则集怎么审? 算法备案、数据合规、医疗/金融行业准入
用户接受度 医生要在终端上一问一答敲几十项数据,比自己判断还慢 用户不愿意为了用 AI 改自己的工作流
知识更新成本 医学指南几年一变,600 条规则要重新访谈专家 模型知识截止、RAG 知识库要持续维护

追问:今天你怎么设计一个医疗诊断智能体? 答方案 C 的结构:LLM 负责读懂病历、追问缺失信息、把结果讲成人话;确定性的规则引擎负责剂量计算、禁忌症检查、指南符合性判定;每一步留可审计的推理链;最终结论必须由医生签字,系统只做"辅助"不做"决策"。 把 MYCIN 的可解释性优点留下,把它的知识获取瓶颈用 LLM 绕过去。

追问:哪些领域规则引擎至今更好? 金融风控的硬规则、医保报销核算、税务计算、合规检查、工业设备的安全联锁——共同特征:规则由法律或标准明文规定(不存在"知识获取瓶颈")、必须可审计、错一次代价极高、且不需要处理开放语义。

Q4:强化学习和监督学习的本质区别是什么?(习题 5)

数据形式不同:监督学习要 (输入, 正确答案) 成对的标注数据;强化学习只要一个奖励信号,而且往往是延迟的、稀疏的——实验四里 16 个格子只有 2 个格子有奖励,其余全是 0。

谁提供信号不同:监督学习的答案由人给;强化学习的数据由智能体自己的行为产生(它走哪条路,就只能学到哪条路——实验四那最下面一行全是 0,就是因为它从没去过)。

核心难题不同:监督学习难在泛化;强化学习难在信用分配(最后才知道输赢,功劳怎么摊到之前每一步)和探索-利用平衡。

超级马里奥这个例子:用监督学习,你需要录制大量人类玩家的 (画面, 按了哪个键) 数据,模型学会模仿——上限是人类水平,而且没见过的关卡就不会了。用强化学习,你只需要定义奖励(前进 +、吃金币 +、死亡 −、通关大 +),它自己试错——没有标注成本,且可能发现人类想不到的打法。这个任务强化学习明显更合适:游戏能高速模拟(试错便宜)、奖励天然存在(分数)、且是典型的序贯决策。反过来"看 CT 片判断有没有肿瘤"就该用监督学习——试错的代价是人命,不能让它"探索"。

Q5:AlphaGo 用了人类棋谱吗?

这是一道送分也送命的题。 2016 年击败李世石的 AlphaGo 用了——先用 KGS 上的人类高手对局做监督学习初始化策略网络,之后才进入自我博弈的强化学习。AlphaGo Zero(2017)才是完全不看人类棋谱、纯自我博弈从零开始的版本,而且更强。 说"AlphaGo 全程不用人类棋谱"是把两代混为一谈。

Q6:RLHF 在大模型训练里起什么作用?(习题 5 最后一问)

预训练让模型"什么都会说",但它学的目标是"猜下一个词",不是"说人类想听的话"。RLHF(基于人类反馈的强化学习)负责对齐:让人类给模型的不同回复排序,训一个奖励模型,再用强化学习去优化。它解决三件事——有用(真的回答问题而不是复述问题)、诚实(少编)、无害(拒绝有害请求)。

注意这里的信用分配问题和实验四是同一个:奖励来自人类对最终整段回复的偏好,而回复是一个词一个词生成的,每个词的功劳怎么分摊正是难点。这也是为什么"让模型学会先花一步调用工具查证"很难——那一步对用户来说毫无输出,短视的优化目标会把它优化掉。γ 的问题换了个身体,又回来了。 第三章讲对齐,第十二章讲微调时会展开。

Q7:明斯基说智能体是"无心"的,可今天的 LLM Agent 推理能力很强。心智社会理论过时了吗?(习题 4)

没过时,但适用的层次变了。 明斯基的核心主张有两条:(1)智能可以从大量简单单元的交互中涌现;(2)不需要一个全知全能的中央控制器。

今天的多智能体系统里,第 2 条完全还在——CrewAI、MetaGPT、CAMEL-Workforce 的价值恰恰是"分工 + 协作"而不是"造一个更大的模型"。但第 1 条的"简单"被重新定义了:单元不再是 GRASP 这种笨函数,而是有推理能力的 LLM Agent。所以涌现的层级上移了——不再是"从无心到有心",而是"从单个有能力的个体到群体智能"。

一个更锋利的说法:明斯基证明了『涌现不需要每个成员都聪明』,今天我们发现『成员聪明了,涌现的东西更值钱,但也带来了新问题』——比如成员之间会互相被幻觉带偏、会陷入"礼貌性同意"、会无限循环讨论。这些问题明斯基那个年代不存在,因为他的 agent 连"同意"都不会。

和现代框架的对比也值得说清: 明斯基的机构是固定的、硬编码的调用树;现代框架的角色分工是用自然语言描述的、运行时动态编排的。而且现代框架几乎都引入了明斯基刻意避免的东西——一个持有全局目标的 Orchestrator(CAMEL 的 Workforce、MetaGPT 的 SOP 流程)。为什么?就是实验三场景二那个原因:纯去中心化没有韧性。

Q8:为什么说预训练解决了知识获取瓶颈?两者在知识表示上有什么本质区别?(习题 6)

解决的机制是"换了获取方式":专家系统的知识要工程师访谈专家、逐条编码(人工标注的极端形式,成本线性于知识量);预训练靠自监督学习,"猜下一个词"的答案本来就在原文里,不需要任何人标注,全互联网文本瞬间都成了训练数据。同一个难题,不是靠更努力地写规则翻过去的,是靠换一条路径绕过去的。

知识表示的区别是根本性的:

专家系统 预训练模型
形式 离散的、明文的 IF-THEN 规则 连续的、分布式的权重
可读性 人能逐条读、逐条改 黑箱,改不了单条知识
可审计性 推理链完整可查 说不出为什么这么答
边界外行为 直接失语(实验二案例 4) 会流畅地编(幻觉)
覆盖广度 窄,且线性增长 极广

注意最后两行的对照,这是今天最重要的取舍:专家系统的失败是"不说话",模型的失败是"胡说得很像"。一个安全但没用,一个有用但危险。 所以工程上要给模型加 RAG(把知识挪回可控的外部知识库)、加引用溯源、加规则做终审——本质上就是把符号主义的可审计性再装回来。

追问:互联网数据带来什么问题? 事实错误和过时信息、偏见与歧视(数据里有,模型就学)、版权与隐私、低质量与 AI 生成内容污染(模型吃自己的输出)、以及知识截止日期。缓解手段:数据清洗与去重、RLHF 对齐、RAG 引入可更新的外部知识、引用溯源、以及针对性的红队测试。第三章会展开。


习题参考答案

习题 1、2、4(后两问)、5、6 的核心答案已经在上面的面试视角 Q2、Q3、Q4、Q6、Q7、Q8 里,这里只补三道需要动手或单独展开的。

习题 3:扩展 ELIZA——加规则、加记忆,并算清组合爆炸

这道题分四问,前两问动手,后两问思辨。新建 eliza_v2.py:

# -*- coding: utf-8 -*-
"""习题 3:给 ELIZA 加规则 + 加"上下文记忆",然后算清楚组合爆炸。"""
import re
import random

random.seed(7)   # 固定随机,方便你对照输出

# ---------- 1) 新增规则(工作/学习/爱好/睡眠) ----------
RULES = [
    (r"我叫(.+)",         ["{0},很好听的名字。跟我说说你自己吧。"]),
    (r"我今年(\d+)岁",     ["{0}岁正是好时候。这个年纪你最在意什么?"]),
    (r"我是一名(.+)",      ["当{0}最累的地方是什么?"]),
    (r"我的工作(.+)",      ["工作{0},这种状态持续多久了?"]),
    (r"我在学(.+)",        ["学{0}的时候,哪一部分最卡住你?"]),
    (r"我喜欢(.+)",        ["{0}是什么时候开始喜欢的?"]),
    (r"我睡不着",          ["睡不着的时候你脑子里在想什么?"]),
    (r"我需要(.+)",        ["你为什么需要{0}?", "得到{0}真的能帮到你吗?"]),
    (r"我感到(.+)",        ["你为什么感到{0}?", "感到{0}的时候,你一般会做什么?"]),
]

DEFAULT = ["嗯,继续说。", "有意思,然后呢?", "为什么这么说?"]

# ---------- 2) 上下文记忆:一个字典 + 几条抽取规则 ----------
MEMORY = {}

EXTRACTORS = [
    ("姓名", r"我叫(.+)"),
    ("年龄", r"我今年(\d+)岁"),
    ("职业", r"我是一名(.+)"),
    ("爱好", r"我喜欢(.+)"),
]


def remember(text):
    """从用户这句话里抽取可记住的信息,存进 MEMORY。"""
    for key, pattern in EXTRACTORS:
        m = re.search(pattern, text)
        if m:
            MEMORY[key] = m.group(1)
            return key, m.group(1)
    return None, None


def reflect(text):
    mapping = {"我": "你", "你": "我"}
    return "".join(mapping.get(ch, ch) for ch in text)


def respond(user_input):
    key, value = remember(user_input)

    for pattern, templates in RULES:
        m = re.search(pattern, user_input)
        if m:
            reply = random.choice(templates).format(
                *[reflect(g) for g in m.groups()])
            # 记忆的用法:如果记住了姓名,就在回复前面加称呼
            if "姓名" in MEMORY and key != "姓名":
                reply = MEMORY["姓名"] + ",  " + reply
            return reply

    # 兜底时也用一下记忆,显得"记得你"
    if MEMORY:
        known = "、".join("%s=%s" % (k, v) for k, v in MEMORY.items())
        return "我还记得你告诉过我: %s。再多说点?" % known
    return random.choice(DEFAULT)


def dialog(lines):
    for line in lines:
        print("你: " + line)
        print("ELIZA: " + respond(line))
    print("\n[MEMORY 里现在存着] %s" % MEMORY)


if __name__ == "__main__":
    dialog([
        "我叫小林",
        "我今年28岁",
        "我是一名前端工程师",
        "我的工作让我很累",
        "我睡不着",
        "我喜欢爬山",
        "今天天气不错",
    ])
python3 eliza_v2.py
你: 我叫小林
ELIZA: 小林,很好听的名字。跟我说说你自己吧。
你: 我今年28岁
ELIZA: 小林,  28岁正是好时候。这个年纪你最在意什么?
你: 我是一名前端工程师
ELIZA: 小林,  当前端工程师最累的地方是什么?
你: 我的工作让我很累
ELIZA: 小林,  工作让你很累,这种状态持续多久了?
你: 我睡不着
ELIZA: 小林,  睡不着的时候你脑子里在想什么?
你: 我喜欢爬山
ELIZA: 小林,  爬山是什么时候开始喜欢的?
你: 今天天气不错
ELIZA: 我还记得你告诉过我: 姓名=小林、年龄=28、职业=前端工程师、爱好=爬山。再多说点?

[MEMORY 里现在存着] {'姓名': '小林', '年龄': '28', '职业': '前端工程师', '爱好': '爬山'}

✅ Checkpoint 10:它现在会叫你的名字了,最后一句还能把记住的四项信息复述出来。这个"记忆"效果甚至不差——用户体验上比原版 ELIZA 强很多。

但请立刻看清它到底做到了什么、没做到什么。 它做到的是:用一个字典存了四个字段。它没做到的是:

  • "我的工作让我很累"这句话里的信息,一个字都没进 MEMORY——因为 EXTRACTORS 里没有"工作状态"这一项。要存,你得再写一条正则。每多记一类信息,就要多写一条抽取规则。
  • 它记住的是字符串,不是含义。 你说"我改行了,现在做产品",它不会更新"职业"字段——因为那句话不匹配 我是一名(.+)。
  • 它无法回答"你觉得我最近状态怎么样"——那需要把多轮信息综合起来推理,而它只会查表填空。

对比第一章的 LLM 智能体:把历史消息拼进 messages 列表就行了,模型自己决定什么值得记、怎么关联。 这就是第八章"记忆"要讲的东西——记忆不是一个字典,是一套"该记什么、怎么存、怎么在对的时候取出来"的机制。

第三问:扩展后的 ELIZA 与 ChatGPT 有哪些本质差异? 至少五个维度:

维度 扩展版 ELIZA ChatGPT
知识来源 你手写的 9 条规则 从海量文本自监督学习而来
泛化能力 零。规则没覆盖的句式一律兜底 能处理从没见过的表达
语义理解 无。只做字符匹配和字符串搬运 词被映射为向量,"难过/沮丧/不开心"天然靠在一起
记忆方式 手写抽取规则 + 定死的字段 上下文窗口 + 模型自己判断什么相关
可扩展性 加话题 = 加规则,规则间会互相抢答 加话题 = 什么都不用做
可解释性 完全可解释(命中哪条规则一目了然) 黑箱

注意最后一行——ELIZA 赢了这一项。 这不是玩笑:"能说清自己为什么这么答"是规则法唯一但极其重要的优势,也是今天在高风险场景还离不开规则引擎的原因。

第四问:组合爆炸能用数学说明吗? 能,而且很简单。在 eliza_v2.py 末尾加一个函数:

def explosion():
    print()
    print("=" * 60)
    print("组合爆炸:为什么规则法追不上语言")
    print("=" * 60)
    print("假设一句话由若干『槽位』组成,每个槽位有若干种说法:")
    slots = [("句式(陈述/疑问/否定/反问)", 4),
             ("情绪词(难过/焦虑/生气/迷茫/开心)", 5),
             ("对象(工作/家人/朋友/自己/未来)", 5),
             ("程度(很/有点/特别/不太)", 4),
             ("时态(现在/过去/一直)", 3)]
    total = 1
    for name, n in slots:
        print("    %-28s %d 种" % (name, n))
        total *= n
    print("    " + "-" * 40)
    print("    组合总数 = " + " x ".join(str(n) for _, n in slots)
          + " = %d 种" % total)
    print()
    print("每加一个新维度(比如『是否带否定词』2 种),总数就再乘一遍:")
    print("    %d x 2 = %d" % (total, total * 2))
    print("    %d x 2 x 2 = %d   (再加一个维度)" % (total, total * 4))

(在 __main__ 里 dialog(...) 之后调用 explosion())

============================================================
组合爆炸:为什么规则法追不上语言
============================================================
假设一句话由若干『槽位』组成,每个槽位有若干种说法:
    句式(陈述/疑问/否定/反问)              4 种
    情绪词(难过/焦虑/生气/迷茫/开心)          5 种
    对象(工作/家人/朋友/自己/未来)           5 种
    程度(很/有点/特别/不太)               4 种
    时态(现在/过去/一直)                 3 种
    ----------------------------------------
    组合总数 = 4 x 5 x 5 x 4 x 3 = 1200 种

每加一个新维度(比如『是否带否定词』2 种),总数就再乘一遍:
    1200 x 2 = 2400
    1200 x 2 x 2 = 4800   (再加一个维度)

✅ Checkpoint 11:只用 5 个维度、每维 3~5 种说法,就已经 1200 种组合了,而你的 RULES 里只有 9 条。数学形式:维度数 d、每维 k 种说法,需要的规则数量约为 k^d——指数增长。

关键在于 d 有多大。 真实语言的维度几乎无限:反讽、省略、方言、错别字、新造词、表情符号、中英夹杂、上下文指代……d 每增加 1,规则数就乘一遍 k。 人手写规则的速度是线性的,需求是指数的——这个差距不是靠努力能补上的,这就是知识获取瓶颈的数学本质。

那 LLM 凭什么行?因为它换了问题。 它不去枚举 1200 种组合,而是把每个词映射成一个向量,让"难过""沮丧""不开心""心里堵得慌"在向量空间里天然靠在一起。从"枚举组合"换成"度量相似",指数问题就变成了几何问题。 第三章讲词嵌入时你会看到这件事具体怎么做到的。

习题 4 第一问:GRASP 失效会怎样?

这道题就是实验三的场景二,已经跑出来了:塔一层都搭不起来,失败从 GRASP 一路冒泡到 BUILD-TOWER,四层全部报失败,没有任何一层能想办法绕过去。原因和工程解法见 Checkpoint 7 下面那段。优势是简洁、可涌现、易扩展(加新工人不用改别人);劣势是没有韧性、没有全局视野、难调试(要顺着整棵调用树找)。

习题 7:三个时代的"智能代码审查助手"

这道题最能体现"技术演进如何把不可能变成可行",值得完整答:

1980 年代(符号主义):写规则。

做法:把代码解析成抽象语法树,然后写一堆 IF-THEN 规则去匹配模式——"IF 有个 malloc 没有对应的 free THEN 内存泄漏"、"IF 循环变量在循环体内被修改 THEN 警告"。这条路真的走通了,而且它今天还活着——lint、Coverity、SonarQube 静态分析,本质就是这个。

能做到的:查出明确定义的缺陷模式,而且零误判率的那些规则极其可靠、完全可解释。 做不到的: - "概括代码的实现逻辑"——完全不可能。 这需要理解意图,规则法没有任何抓手。 - "提出改进建议"——只能给出规则里预先写好的固定建议,无法说"这段逻辑可以提取成一个函数,因为它在三个地方重复了类似的语义"。 - 命名是否合理、注释是否与代码一致、这个改动是否符合团队约定——全都超出能力边界。

2015 年左右(深度学习,无 LLM):训分类器。

做法:把代码转成向量(词袋、AST 路径嵌入、或早期的 code2vec),然后训练有监督的分类模型: - "这个函数有没有 bug"→ 二分类,训练数据来自历史上"引入 bug 的提交"和"修复提交"; - "这段代码质量高不高"→ 用 code review 的历史评论做标签; - 相似代码检测、克隆检测——这个时代做得相当不错。

关键瓶颈是数据:你需要大量带标注的样本(哪次提交引入了 bug),而这种标注极难获得且噪声很大(很多 bug 几年后才被发现,很多"修复提交"其实是重构)。而且它只能输出一个分数或类别,不能生成自然语言解释——它能说"这个函数有 73% 的概率有问题",但说不出哪里有问题、为什么。对开发者来说,一个没有解释的告警等于噪音。

今天(LLM Agent):

架构上至少这几个模块,注意它是一个循环,不是一条流水线:

模块 干什么 用什么实现
感知 拉取 PR 的 diff、改动文件、PR 描述、关联 issue Git / GitHub API 工具
规划 把"审查这个 PR"拆成子任务:先读懂改动意图 → 逐文件审 → 跨文件看影响面 → 汇总 LLM + ReAct 或 Plan-and-Solve(第四章)
工具 读文件、跑测试、跑静态分析、查 git 历史(这行代码上次为什么改)、查依赖文档 工具注册表(第一章的 TOOL_REGISTRY 那套)
记忆 团队的编码规范、这个仓库的历史约定、过去被驳回过的类似写法 向量库 + RAG(第八章)
反思 自查:"我这条建议有没有依据?会不会是误判?" 第四章的 Reflection
输出 生成结构化评论,逐行定位 结构化输出 + GitHub API

关键设计决策(这才是面试真正想听的):

  • lint 和静态分析不要扔掉,作为工具挂上去。 确定性的东西交给确定性工具——"这里有语法错误"用规则查是 100% 准确且免费的,让 LLM 去看是又贵又不可靠。 这就是本章坑 A 那条原则的应用。
  • LLM 负责三件规则法永远做不到的事:概括实现逻辑、结合上下文判断"这样写在这个项目里合不合适"、把发现讲成开发者能听懂的话。
  • 必须让它调用工具看更多上下文,而不是只看 diff。一个 bug 常常不在改动的那几行里,而在"改动破坏了别处的假设"——这需要主动去读别的文件,正是 Agent 相对于"一次性调 API"的价值所在。
  • 保守输出策略:宁可漏报也别误报。代码审查工具一旦被开发者认定为"爱瞎叫",就会被永久忽略,这时它的价值是负的(占用注意力)。所以要让它对不确定的发现降级为"疑问"而不是"缺陷"。

三代对比,演进的到底是什么:

1980s 规则 2015 分类器 今天 LLM Agent
概括实现逻辑 ❌ 不可能 ❌ 不可能 ✅ 这是它的强项
发现明确缺陷模式 ✅ 可靠且可解释 ⚠️ 有分数没解释 ✅ 但应该交给挂载的静态分析工具
结合项目约定判断 ❌ ❌ ✅ 靠 RAG + 长上下文
给出可读的改进建议 ❌ 只有固定文案 ❌ ✅
需要的标注数据 0(但要人写规则) 大量且难获得 0(预训练已完成)
可解释性 最强 最弱 中(能给理由,但理由本身可能是编的)

一句话总结这个演进:规则时代缺的是"理解意图"的能力,深度学习时代缺的是"标注数据"和"表达能力",而预训练一次性解决了后两个——它让"理解意图"和"生成解释"变成了免费的默认能力。 于是这个任务从"几乎不可能"变成了"可行"。但请注意:最可靠的那部分(明确缺陷检测)至今仍然由 1980 年代的技术承担。 这就是这一章最该带走的态度——新范式解决了旧范式解决不了的问题,但它不废除旧范式的长处。



划重点

历史脉络(这条因果链是本章的骨架)

  1. AI 发展的主线是"问题驱动":每代范式都是为了解决上一代的死穴——规则写不完,所以改成让机器学;只会感知不会决策,所以有了强化学习;从零试错太笨,所以先预训练。理解这条因果链,比背年份人名有用一百倍。
  2. 符号主义 = 把聪明写给机器。理论基石是 1976 年纽厄尔与西蒙的物理符号系统假说(充分性 + 必要性两个论断)。专家系统(MYCIN)和深蓝证明它在规则完备、状态可枚举的窄领域极其有效;但它撞上三堵墙——知识获取瓶颈、常识无底洞、框架问题与脆弱性,注定走不进开放世界。
  3. 学习时代三级台阶:连接主义(神经网络,管感知)→ 强化学习(试错拿奖励,管决策)→ 预训练(读遍互联网当通才,管知识)。注意预训练之所以能解开知识获取瓶颈,靠的是自监督学习——"猜下一个词"的答案本来就在原文里,不需要人标注。很多技术死结的解法不在死结本身,而在换一条路径。
  4. 现代智能体 = 历史的合体:LLM 当大脑(连接主义),干着推理规划的活(符号主义的梦想),用 RLHF 打磨(行为主义),靠多智能体协作放大(心智社会)。它是"亚符号内核 + 符号外壳"——反驳了 PSSH 的必要性,却印证了充分性。

四个亲手验证过的结论(比上面四条更重要,因为你跑出来了)

  1. 规则法的失败是"字符级"的(实验一):我需要(.+) 匹配不到"我不需要",中文里整条规则静默落空掉进兜底;英文 I am (.+) 反而会匹配到 not happy 然后自信地答错。方向相反,病根同一个——正则看见的是字符,不是意思。 而组合爆炸给出了数学形式:规则数 ~ k^d,人手写规则是线性速度,需求是指数速度。
  2. "知识与推理相分离"是可验证的事实,不是口号(实验二):同一个 infer() 函数,换一份知识库就从诊断植物变成诊断电脑,而 inspect.getsource() 证明推理机源码里一个领域词汇都没有。这和第一章 TOOL_REGISTRY(知识)+ _execute()(引擎)是同一个设计。另外记住 CF 沿链条衰减:0.8 × 0.7 = 0.56——这就是第四章多步 Agent 可靠性的同一个数学。
  3. 涌现给你能力,但不给你韧性(实验三):四层机构里没有任何一个成员知道"塔"是什么,"搭一座三层塔"却涌现出来了;而只要最底层的 GRASP 坏掉,四层全部躺平——因为"绕过去"需要理解全局目标,而没有哪一层有。这是今天做多智能体系统时最该先想清楚的取舍。
  4. γ 一个参数决定它有没有远见(实验四):同一份代码,γ=0.9 学出一条通向终点的"价值山坡",γ=0 价值地图几乎全零、原地打转。它学的不是"这一步有奖励",而是"这一步能通向奖励"——这就是信用分配问题,RLHF 里它换了个身体又回来了。

带走的工程态度

  1. 新范式不废除旧范式的长处。 可解释性、可审计性是规则法唯一但极其重要的优势,至今在金融风控、合规、医疗剂量这些"错一次代价极高"的地方无可替代。判断标准是错了的代价:错了只是尴尬 → 交给模型;错了要赔钱担责 → 交给规则。
  2. ELIZA 效应是你自己会犯的错:模型输出格式漂亮、语气自信,你就默认它答对了。对治方法只有一个——验证输出,不要验证观感。 本章 11 个 ✅ Checkpoint 都在训练这个习惯。

下一章预告

这一章反复出现一句话:LLM 把"枚举组合"换成了"度量相似"。 但它到底怎么做到的?

第三章会打开大语言模型这个黑箱,讲清楚:

  • 词是怎么变成向量的(词嵌入),以及为什么"难过"和"沮丧"在向量空间里天然靠在一起——本章那个 1200 种组合的问题,就是在这里被绕过去的;
  • Transformer 的注意力机制在算什么,以及为什么它需要额外的位置编码(不加的话,"我打你"和"你打我"对模型是同一句话);
  • Token 到底是什么,一个汉字算几个 token,为什么这直接决定你的 API 账单;
  • 温度、Top-k、Top-p 这三个采样参数是流水线上的三道工序,不是三选一——调错了模型会胡说,调死了模型会变成复读机;
  • base 模型和 instruct 模型的区别,以及"指令调优"到底调了什么;
  • 幻觉分几类,其中"忠实性幻觉"(原文明明有,它还是编)为什么是做 RAG 时最难的那种;
  • 缩放定律与 Chinchilla 的修正:为什么 70B 的模型喂 4 倍数据能打败 175B。

动手部分是一个完全离线的采样实验——不需要 API Key,你会亲手把温度从 0 调到 2,看着同一个模型从"复读机"变成"胡言乱语",然后明白那两个极端之间的甜点区在哪。

本页由 Markdown 自动渲染 · 共 628 篇笔记