人工智能部 · 新生手册
从一个手写的数字出发,走进人类探索智能的长河。
修订:2026-10-29 · 补全 Git 首次配置与提交
使用说明
这本书可以从序言和历史慢慢读起,也可以直接进入第 1 章 机器怎样看见一个字,先试着让机器认一个字。正文和配图无需登录;需要电脑或模型服务的练习,会在相应章节说明准备什么。
- 找路与返回。 电脑左侧是目录,手机上点击顶端的“目录”展开。用“搜索”查找词语;从正文链接或搜索结果跳到别处后,点击“返回刚才阅读处”,就能接着读。
- 动手读图。 看到按钮、滑块或可点击的图案,先猜一猜改变条件后会发生什么,再操作、比较结果。一次只改一个条件,更容易看清原因;需要时可以重置实验。长代码也可以展开,配合输入、输出一起读。
- 留下自己的想法。 鼠标拖选一句话,或在手机上长按选文,点击“写下想法”。默认保存为私人笔记;想与别人讨论时,主动选择公开并登录。公开讨论可以回复,本人可以编辑或撤回。顶端“讨论与笔记”可以找回这些记录。未登录或游客账号的私人笔记保存在当前浏览器,成员账号支持云同步。
- 把问题交给维护者。 错字、解释不清或实验报错,都可以通过“纠错反馈”提交。附上原句、操作和结果,会更容易查明问题;反馈不会自动变成公开评论。
- 带走这本书。 打开“讨论与笔记”里的“导出全书”,可下载包含当前已加载的本人私人批注的完整 HTML。账号笔记请先同步;下载后可离线读书、操作配图,也可用浏览器“打印”保存为 PDF。批注还可另存为 JSON。导出的书含你的私人想法,分享前请检查。
- 遇到正文更新。 新记录会保留写下时的原句与段落语境。如果文字改变或无法定位,页面会提示你查看旧内容;私人笔记可以重新关联原文。旧想法可以留下来,与后来读懂的东西并排看。
序:从你写下的一个字开始
请在纸上写一个“3”。
再写一个。潦草些也没有关系。两个字不会完全一样:这一笔短一些,那一处弯得更急。把纸转过一点,你依然认得它们。你甚至能读懂别人匆忙留下的字,而那个人从未向你说明过他的书写规则。
现在,把其中一个字拍成照片,交给计算机。
它拿到的是许多个数:这里亮一些,那里暗一些。数里面没有哪一个写着“这是3”。怎样从这些数中认出一个字?如果你说“看形状”,形状又该怎样告诉机器?如果你说“让它学”,学习究竟改变了什么?
我们对自己熟悉的能力,常常熟悉到忘了惊讶。认字、听懂一句话、绕开路上的障碍,似乎不值得专门思考。直到我们试着造一台也能这样做的机器,那些平常的事情才显出深度。
这本书从这里出发。
沿途会遇到一条理想化的纸带、一台从错误中调整判断的学习机器、一张能把误差信号反向传递的网络,也会遇到资料不足、承诺落空和实验失败。今天屏幕上的回答,身后站着许多年代的工作。有人提出问题,有人证明限制,有人制造计算机、整理数据、修正程序。他们未必知道自己的贡献会在哪一次后来者的尝试中派上用场。
你将逐渐认识这些工作,也亲手接触其中一些很小的部分:把图像写成数,让一个权重改变,让程序辨认数字,再追问它为何认错;让模型读一份通知,再追问它依据哪一句;让它选择行动,再观察行动怎样改变下一步。
“很小”使我们有机会看清。一个能由你亲自算完的例子,可以让“机器学习”从新闻里的词,变成眼前发生的计算。此后再看到更大的模型,你会有可以攀援的支点。
我们还无法完整解释人的意识,也不能仅凭一段流畅对话,就判断机器是否有主观体验。与此同时,机器已经在特定任务上表现出识别、生成和解决问题的能力。能力的增长与解释的空缺,可以同时存在。学习这个领域,需要容得下这两件事:为新出现的可能感到振奋,也愿意走近它,弄清它究竟做到了什么。
书的终点不会是一份装满全部知识的清单。更希望那时的你,能对一个现象提出自己的问题,找到可读的资料,设计一次小实验,并在结果不如预期时知道怎样继续。你也许还说不清想研究哪个方向,却已经有能力向感兴趣的地方走一小段。
那张写着“3”的纸可以先留着。读到第七章时,我们会回来看看:一个在教学数据上表现不错的模型,是否真的认识你刚刚写下的字。
从哪里开始
顺着读,可以先进入下面的历史,再到第一至四章看见表示、学习、检验与行动之间的联系。这几章的主要问题用纸笔就能参与。想先触摸一个具体问题,也可以直接读第 1 章 机器怎样看见一个字,随后沿文中的路标返回历史。
第五章开始准备电脑实验;第六、七章进入语言和自己的小作品;第八、九章把资料、工具与判断连成系统;第十章讨论怎样把工作交到别人手中。第十一至十三章是随时可翻的参考页。具体读法与基础补习放在附录 D 阅读与练习路线,正式培养要求集中在附录 A 关卡与验收标准。
第一次读,遇到长代码可以先看输入、输出和结果解释。准备运行时,再展开完整程序。纸笔练习、本地CPU实验和需要模型服务的实验会分别说明;没有服务账号或昂贵设备,也能开始这段探索。
边读边留下问题
读到“好像懂了”的地方,试着问一句:如果换一个输入,结论还成立吗?把猜测写下来,再去图里试一试;也可以留下你暂时答不出的疑问。划线、讨论和保存笔记的方法见卷首的使用说明。一个具体的问题,常常也会照亮后来读到这里的人。
你正站在哪:一段可选的历史
漫长的接力——写给刚刚走到这里的你
你今天在电脑前输入一句话,几秒钟后,一个模型给出了回答。屏幕不会告诉你:为了使这件事成为可能,人们曾怎样定义计算,怎样教机器辨认最简单的图案,怎样在失败之后修改自己的问题。那些工作发生在不同年代、不同国家,出自彼此合作、争论,甚至从未见过面的人。
这段历史有几条相互交织的线索:把推理写成规则,让机器从经验中学习,让行动接受环境的检验。它们时而分开,时而汇合。知识、数据、计算资源与评价方法,也在改变每条路能够走多远。
下面的长文可以慢慢读。若你今天想先动手,直接进入第 1 章 机器怎样看见一个字;学过一些模型和实验之后再回来,这些名字会有不同的分量。时间线中的年份是路标,不表示一种方法从此取代另一种方法。
一、一条纸带,和计算的边界
1936年,阿兰·图灵在论文中描述了一种极简的机器。它有一条分成格子的纸带,一个能够读写符号的部件,以及有限的一套状态和操作规则。每一步,它查看眼前的符号,按照规则写入、移动,进入下一个状态。今天,我们把这种数学模型称为图灵机。[1]
不妨让纸带上写着二进制的“111”,也就是 4+2+1=7。加一应该得到“1000”,但机器每次只能看见一格。它先向右找到末端,再切换到“进位”状态,回头把连续的 1 改成 0,直到能写下一个 1。同一个符号,在“扫描”和“进位”两种状态下,会引出不同动作。状态保存着此刻要做什么,纸带保存着已经做过什么。
它每次只能看一格,怎样完成进位、比较,甚至陷入无尽的往返?
读写头从 0 号格开始。□ 是空白,# 分开两段;X、Y 是程序留下的标记。窗口随读写头移动,两端还有空白格。
纸带上留下:111
查看规则与执行足迹
规则由“当前状态”和“读到的一个符号”共同决定。蓝色行是下一步将使用的规则。以进位示例为例:同样读到 1,扫描时向右;进位时却写 0、向左。
已执行的步骤
亲手改规则:同一台机器可以换一个程序
切回进位示例,试把“进位 1 0 L 进位”中的 0 改成 1,装入后再运行。也可以把往返示例最后的“向右”改成“停机”。机器会严格执行你的规则;能停下来,也未必算对了。
每行五项,用空格隔开:当前状态 读到 写入 方向 下一状态。L 向左,R 向右,S 不移动。“停机”“接受”“拒绝”是停止状态。最多 80 条规则、24 格初始输入。
你换的是一段可以写下来、保存、传递的规则,执行器始终只做查表、读写和移动。程序也可以成为输入的数据。把解释这些规则的过程也写成图灵机的规则,便走向通用机器的思想。本页用浏览器程序解释规则,没有完整搭建那台通用图灵机。
为什么不能造一个万能的“会不会停”按钮?
往返示例能用“完整局面重现”证明不停;不断写的示例则能从规则看出:它永远只会写下去,没有停止分支。这些判断适用于具体程序。只看见它运行了 32 步,不能判断所有程序——那串更长的 1 就会在第 50 步停下。
来做一个反证思考:假设有个判定器,能对任意程序与任意输入给出正确答案,并且自己总会结束。我们写一个“反着做”的程序 D:拿到程序 P,先问判定器“P 读自己的描述会停吗”;若回答会,D 就无限循环;若回答不会,D 就立即停止。
现在,把 D 自己的描述交给 D。判定器该怎样回答?
选一个答案,沿着“反着做”的规则推下去。这是逻辑思考,不是实际运行一个停机判定器。
两种答案都会与程序的行为冲突。因此,这样一个对所有程序都正确、自己又总能结束的判定器不存在。我们仍可以证明许多具体程序会停或不停;受到限制的是那个“对所有情况都管用”的承诺。
不操作也能读懂:二进制的 111 表示 7。程序先向右找到末端,再向左进位:遇到 1 就写 0、继续向左;遇到 0 或空白就写 1 并停止,得到 1000,也就是 8。比较两段字串时,则用状态记住一个符号、用纸带标记已核对的位置。同一个执行器装入不同规则,就完成不同任务。规则、状态与可延伸的记忆,使简单动作能够组合成复杂计算。理论纸带无界;本页最多演示 512 步,演示暂停不等于机器停机。
先试着执行几步,再退回一步。接着换成“两边一样吗”:机器要记住刚读过的符号,走到分隔符另一边核对,再返回。你看到的往返并不聪明,却把一次比较拆成了可以逐条检查的工作。最后,改掉规则纸条上的一个符号。机器仍然认真执行,结果却可能错了。执行得准确与程序写得正确,是两件需要分别确认的事。
图灵进一步讨论了通用机器:把另一台机器的规则也编码成符号,一台通用机器便能模拟它的计算。硬件不必为每一道新题重新制造;处理不同描述,就能执行不同的程序。这里的纸带可以无限延伸,是理论中的理想化条件,现实计算机的时间和存储都有限。
同一项工作也揭示了计算的边界。有些判定任务无法由一个对所有输入都有效的算法完成。用今天常见的表述,我们不能写出一个总能正确判定任意程序在任意输入上是否会停机的通用程序。力量与限制,从一开始就同时存在于计算理论之中。
这件事对人工智能的意义很深:我们终于能严格讨论一台机器按照什么规则工作,也必须认真区分“可以计算”“实际算得完”和“表现出智能”。图灵机不是一颗电子大脑,更不是对机器必将拥有意识的证明。它给后来的研究铺下了一块坚实的地面。
1950年,图灵把问题推进到机器智能。他讨论模仿游戏,也讨论让机器接受教育、从经验中学习的可能。我们今天回头读,会发现“学习的机器”并非大模型时代才出现的愿望。[2]
图灵于1954年去世,没有活到两年后的达特茅斯夏季研究。一个人的生命会结束,他认真提出的问题却可能进入许多人的一生。今天我们仍在讨论的部分疑问,曾经被他写在纸上,交给一个他无法亲眼看见的未来。[3]
插页:让逻辑住进电路
纸带说明一种计算可以怎样定义,现实机器却还需要另一条道路:让物质按照这些关系可靠地工作。
香农研究继电器与开关电路时,用布尔代数分析开关的组合。可以亲手画一个很小的例子:两个开关串联,两者都闭合才通路;两个开关并联,任意一个闭合就可能通路。给状态编码,再讨论组合,逻辑与电路就有了可以计算的对应。这条在1930年代展开的研究道路,使电路设计可以借助代数分析。[21]
逻辑关系有了物理实现,规模仍然是难题。器件要稳定,连线要可靠,热量要散去。1947年,贝尔实验室的巴丁与布拉顿制成点接触晶体管;随后发展的半导体技术,为计算电路提供了新的器件基础。1958年,基尔比完成集成电路的早期演示,诺伊斯随后提出了有利于制造与互连的方案。[22]
这些工作没有直接教会机器认字,却改变了人们能够制造怎样的计算机。模型中的一次乘法,需要在真实器件上执行;数据要存下、搬动,再参与下一轮运算。算法、器件、制造与软件彼此等待,也彼此推动。
今天打开一个程序,我们很少看见晶体管,更不会逐一安排它们的开关。许多层经过检验的约定,把底下的复杂性托住,使上面的人能够继续建造。理解技术之美,也包括看见这种分工:有人把一个微小环节做到足够可靠,让陌生的后来者能够把它当作起点。
二、一个夏天,装不下的愿望
1955年,约翰·麦卡锡、马文·明斯基、纳撒尼尔·罗切斯特和克劳德·香农提出一项计划:在1956年夏天,把一小群研究者聚在达特茅斯,研究能否使机器使用语言、形成概念、解决问题并改进自身。他们把这些工作放在“人工智能”这个名称之下。[4]
这份提案至今仍值得读。它既讨论语言与规则,也讨论神经网络、抽象和自我改进。后来经常被分开讲述的道路,已经出现在同一份研究设想里。
其中一条道路,是把推理表示为对符号的操作。假如事实与规则能够写清,程序就可以推导结论;假如一个问题能够拆成状态与动作,程序就可以搜索从起点到目标的路径。棋盘、逻辑题与形式明确的小世界,为这些想法提供了试验的地方。
但现实世界很快提出了更难的要求。一个人说“把那个东西拿过来”,句子里有多少没有说出的知识?“那个”指什么,东西在哪里,能不能碰,旁边有没有人?一条规则处理了眼前的情形,另一种情形又会带来例外。可选的步骤一多,搜索的数量也可能增长到机器无法承受。
回望早年的雄心,很容易产生一种廉价的优越感:他们竟然把问题想得那么简单。可那时并没有一部写好结局的历史可供参考。要检验一个大胆的设想,总得有人先把它变成程序,让它运行,再亲眼面对它做不到的部分。
那个夏天没有完成它所提出的愿望。它留下了一个能够被共同讨论的研究领域,以及足够几代人继续追问的问题。
三、感知机:让错误改变下一次判断
另一条路从神经系统得到启发。1943年,沃伦·麦卡洛克与沃尔特·皮茨研究了简化神经元及其网络的逻辑能力。他们把某些神经活动抽象成可以数学讨论的模型;这项工作并不等于已经造出了会学习的大脑。[5]
到了1950年代,弗兰克·罗森布拉特研究的感知机,把从样本中调整判断的想法向前推进了一步。1958年的一次演示中,程序学习区分标记位于左侧还是右侧的卡片。今天看来,这是一项很小的任务;当时,它使“机器可以从例子中改变自己的表现”成为了可以展示和检验的事情。[6]
理解感知机,可以先看一个简化的线性分类器:把输入的几个数分别乘上权重,加起来,再与一个阈值比较,决定输出哪一类。判断错了,就根据样本与错误调整权重。下一次面对输入时,机器使用的已经是修改过的参数。
这里出现了一个重要区别:人仍然规定模型的形式和学习规则,并提供带有正确答案的训练样本,但不必为每一种输入手工编写判断规则。模型从这些样本中调整参数,再去判断尚未见过的输入。
选一个分错的样本,让分界线真的动一次。
蓝色 ●:正类(+1);红色 ■:负类(−1)。虚线圈出当前分错的点。任务是把右侧三个圆点与左侧三个方块分开。
wₓ = 0,wᵧ = 1,b = 0
已选 A。先比较模型的判断与标签,再决定是否更新。
看看这一笔怎么算
分数 s = wₓ×x + wᵧ×y + b;s ≥ 0 判为正类,否则为负类。分错时:w ← w + 0.5×标签×输入,b ← b + 0.5×标签;分对不变。
不操作也能读懂:初始分界线横着穿过样本,A、B、E 分错。A 的坐标是 (−1, 1),标签是 −1;用它学习后,权重从 (0, 1) 变为 (0.5, 0.5),偏置从 0 变为 −0.5。这是真实计算的简化感知机更新;训练集分对不代表新样本必然分对。
感知机也有明确的边界。把四个点放在正方形的四角,让对角上的两个点属于一类,另两个点属于另一类。一条直线无论怎么画,都不能把这两类完全分开。这就是常用来说明“异或”问题的图景:对这种线性分类器,再多训练也无法补上模型表达能力的缺口。
转动、平移分界线,试着把四个点全部分对。
蓝色 ● 为相异输入;红色 ■ 为同值输入。坐标用 −1 和 +1 表示两种输入。背景淡蓝处预测为正类。
当前用原始的 x、y 作判断。
线性分数:cos(方向)×x + sin(方向)×y − 偏移。
不操作也能读懂:相同类别各占一条对角线,任何单一直线都无法把它们完全分开。加入人为设计的特征 z = x×y 后,相异输入的 z = −1,同值输入的 z = +1;用 z < 0 就能区分。这演示表示方式的重要性,不等于自动训练出隐藏层,也不表示普通线性感知机学会了异或。
解决更复杂的问题,需要研究新的表示、网络结构与训练方法。批评可以指出一种模型的局限,却不能单凭这个局限宣判所有学习机器的未来。反过来,后来神经网络的成功,也不能使早年的每一个乐观预言自动变成正确。
康奈尔大学保存的回忆给这段技术史留下了人的面目。罗森布拉特的学生回忆,他们曾经一起研究语音识别,也一起搭建天文观测设施。1971年,罗森布拉特在一次航行事故中去世。他没有看到几十年后深度学习的发展。[6]
他留下的工作使人停一停:有些人终其一生,只来得及使一种可能性变得清楚。后来的人继续研究它,修正它,越过它的边界。岁月没有把那段短暂的生命还给世界,却让他提出的问题,获得了新的回答。
四、走出小世界:知识、身体与失望
如果日常常识太庞杂,能否先把一个狭窄领域中的专业知识整理出来?专家系统沿着这条路线发展。MYCIN等研究尝试把专家判断中的知识组织成规则,也暴露了知识获取的困难:专家熟练地作出判断,并不意味着他能立即把每一步依据完整说出;知识工程师理解了几条规则,也不等于已经理解那个领域。[7]
知识需要反复澄清,规则需要调试,新的情形会要求系统修改。把研究原型放进现实工作,还会遇到责任、流程和维护问题。一个封闭测验中的好成绩,并不能独自承担这些要求。
还有人让机器走进房间。SRI在1966至1972年间研究的机器人Shakey,把感知、规划与移动放进同一个系统:为了完成任务,它需要辨认环境、安排动作,再实际行动。它活动的环境经过简化,但已经把困难摆在了地面上——程序中的计划,必须经得起身体与环境的检验。[8]
这些探索使人工智能获得了新的能力,也使过早的承诺越来越难以维持。后来被概括为“人工智能寒冬”的失望与收缩,发生在不同地区、不同项目与不同阶段;研究并没有在同一天停止。研究者的口述记录中,甚至能看见被称为寒冬的年代里,概率方法等方向仍在生长。[9]
“寒冬”两个字很轻,落到具体的工作上却会很重:一个设想不能再靠前景获得支持,研究者必须重新回答它究竟解决了什么问题。历史叙事容易略过这段时间,因为没有一个耀眼的演示可以替它命名。
可一门学科正是在这种时候学习诚实。哪些结果能够重现,哪些能力依赖特殊条件,哪些承诺需要收回,哪些问题值得换一种办法继续?这些追问本身,就是进步的一部分。
五、学会承认不知道,也学会把误差传回去
现实信息经常是不完整的。听到一段带噪声的语音,看到一张模糊图片,掌握几条互相冲突的线索,程序需要处理的常常是不同解释的可能性。概率方法让不确定性能够进入模型,而不是被藏在一句肯定的回答后面。朱迪亚·珀尔1988年的《智能系统中的概率推理》,是这一方向的重要著作之一。[10]
统计学习则不断追问:如何从有限样本获得能用于新样本的规律?模型有多复杂,数据有多少,训练误差和实际表现为什么不同?这些问题不会因为某一种架构流行就过时。今天新生学习划分训练集与测试集,正是在接触这条长期形成的方法传统。
神经网络的研究也没有只剩下等待。1986年,大卫·鲁梅尔哈特、杰弗里·辛顿和罗纳德·威廉斯发表工作,展示多层网络可以通过反向传播学习内部表示。这是反向传播获得广泛关注的重要节点,不能写成这个方法毫无前史地被突然发明。[11]
可以把困难想得具体一点:模型经过好几层计算才给出答案,最后答错了,前面每一个权重应该改多少?反向传播利用链式法则,从最终误差向前计算各个参数的梯度,再由优化方法更新参数。它使多层学习有了一条可操作的训练路径,但并不保证训练必然成功,更不保证模型理解了世界。
最后一步答错了,前面两个旋钮该朝哪个方向拧?
色条长度表示损失,固定刻度为 0–18;越短,当前样本的预测越接近目标。
平方误差的一半 L = (预测 − 2)² / 2 = 0.5
从 a = b = 1 开始,预测为 1,比目标 2 小。先计算梯度,再更新参数。
为什么有两个不同的梯度
h = a×x,ŷ = b×h,L = (ŷ−2)²/2。
∂L/∂b = (ŷ−2)×h;∂L/∂a = (ŷ−2)×b×x。
一次更新同时使用旧 a、b 算出的梯度:a ← a − 0.1×∂L/∂a;b ← b − 0.1×∂L/∂b。
不操作也能读懂:初始两个梯度均为 −1;学习率为 0.1,一次更新后 a = b = 1.1,预测从 1 变成 1.21,损失从 0.5 降到 0.31205。若 a = b = 0,两个梯度都是 0,损失却是 2:有更新算法也不保证每个起点都能学好。这是两次乘法的计算图,不是完整的深度网络。
从一层到多层,增加的既是可能性,也是困难。网络怎样组织,训练如何稳定,数据从哪里来,机器能否承担计算,仍要分别解决。后来一张漂亮的结果图,会把这些工作压缩成一条上升曲线;亲手做过实验的人知道,曲线背后还有跑不通的程序、需要重查的数据,以及必须放弃的假设。
研究的时间与新闻的时间并不相同。一种方法被报道时像是突然出现,真正使它可以使用的条件,却可能已经积累了很久。
六、让机器看见,也让人的工作被看见
一张照片对人来说,可能只是一只猫坐在窗边;对图像模型来说,它首先是一组数。怎样使这些数与物体、形状和类别联系起来,需要模型,也需要可以学习和评价的资料。
2009年,邓嘉、李飞飞等研究者介绍了ImageNet:按照概念层次组织的大规模图像数据库。收集图像、核对类别、组织标注,构成了这项工作的基础。数据集使研究者能够在共同任务上比较方法,也把许多人的细致劳动带进了机器学习的结果。[12]
2012年,亚历克斯·克里泽夫斯基、伊利亚·苏茨克维和辛顿的卷积网络在ImageNet竞赛中取得显著成果,后来广为人知的名字是AlexNet。其原论文同时讨论网络、训练技巧与GPU实现。这个节点值得记住,因为它展示了多种条件汇合时,深度学习可以取得怎样的效果。[13]
2015年,何恺明、张祥雨、任少卿和孙剑提出残差学习框架,帮助研究者训练更深的网络。突破继续来自一个个具体问题:深度增加后为何更难训练,结构怎样改变优化过程。模型的规模没有替人省去这些追问。[14]
我们容易记住论文首页的姓名,却很少在讲故事时停下来,看一看数据是怎样成为数据的。一幅图像的标签,一次对错误样本的排查,一段可复用的实现,都会影响最终的结果。研究的共同体比领奖台宽广得多。
向前人致意,也应当包括这些工作。承认别人的资料和代码,写清数据的来源,认真对待标注者和使用者,都会让技术史中那些容易被省略的人,重新拥有位置。
七、棋盘上的相遇:学习、搜索与行动
看见一个局面,与知道下一步怎样行动,是两种不同的困难。落子之后,对手会回应;一个当下看似有利的动作,可能在很多步之后造成失败。学习怎样评价长期结果,成为强化学习的重要问题。它有长期的理论与实验积累,并非随着一场围棋比赛才诞生。[15]
2016年的AlphaGo把深度神经网络与搜索等方法结合起来,并在与李世石的比赛中以四比一获胜。后来AlphaGo Zero进一步展示了从自我对弈中学习的路径。棋盘在这里成为几条技术道路相遇的地方:模型提供判断,搜索考察可能,行动产生新的经验。[16]
这段历史常被讲成“机器战胜人类”。但只盯着输赢,会漏掉更值得凝视的事:人类建立了这项游戏,积累了理解它的方法,又制造出一种能够帮助自己重新探索它的工具。棋手和研究者面对的,都有原先不曾看见的可能。
棋盘也保护了这个实验。规则明确,动作有限,胜负有清楚的判定。真实世界中的机器人没有这样的便利:传感器会出错,物体会滑落,人会突然走近,一次探索还可能造成不可撤销的后果。从模拟到身体,仍有很长的路。对棋盘成就的尊重,包括不把它直接换算成对整个世界的掌握。
当你在本书附录的五格世界中练习时,看到的只是一个极小的模型。不过,状态、动作、反馈与长期结果之间的关系,确实与这条研究路线相连。宏大的问题,也需要从可以检查的小世界开始。
八、语言成为入口,流畅仍需接受检验
2017年,《Attention Is All You Need》提出Transformer架构。注意力机制让序列中的位置能够结合其他位置的信息,原论文以机器翻译为重要实验任务。今天人们用它讲述大模型的来路,也应记住它最初面对的具体问题。[17]
随后,大规模预训练与任务适配不断发展。2020年的GPT-3论文研究了通过上下文中的任务说明和少量例子完成多种任务的能力;2022年的InstructGPT论文则研究如何用示范与人类反馈,使语言模型更好地遵循指令。生成文本、按要求回答、给出可靠结论,是需要分别检验的能力。[18]
语言逐渐成为许多人使用人工智能的入口。过去必须在专门界面里设定的任务,如今可以从一句说明开始。这种变化给普通使用者带来了接近复杂工具的机会,也让模型的局限更直接地进入日常生活。
当一个系统说得迟疑,人会追问;当它说得流畅,人反而可能忘记核对。错误的出处、遗漏的条件、貌似完整却无法执行的方案,不会因为语言自然就失去危险。为模型接入检索、工具和验证程序的研究与工程,也因此成为重要的工作。
2025年的DeepSeek-R1论文把强化学习与语言模型推理能力的训练放到显著位置,报告了相关方法与评测,并提供模型供进一步研究。它让这条发展路线有了新的可讨论材料;具体能力、成本与复现范围,仍需对应模型、任务和公开条件逐项判断。[19]
到这里,我们已经能看见早年问题的回声:规则、学习、搜索、行动与反馈重新相遇。没有一篇论文能替整个领域宣布完成。今天的模型也会成为后人回顾的早期模型,今天认为理所当然的办法,也可能需要重新检验。
九、把目光从屏幕移向世界
人工智能的历史还发生在聊天窗口之外。以蛋白质结构预测为例,研究者希望从氨基酸序列获得关于三维结构的信息。2021年发表的AlphaFold工作,展示了它在CASP14结构预测评测中的重要进展。原论文把模型与长期积累的实验结构资料、领域知识和严格评价联系在一起。[20]
这里没有一个可以代表全部意义的“机器战胜人类”瞬间。更重要的可能是,研究者多了一种探索自然的办法:提出假设,寻找线索,把计算结果与实验放在一起判断。预测仍有边界,生物学问题也远未解决,新的工具却能够改变人们下一步有条件提出什么问题。
这是技术发展令人动容的一面。一个领域留下的数学、一群人建设的数据、另一群人改进的程序,能够在后来相遇,帮助人类接近一个原本难以研究的对象。参与这些工作的人,不一定知道自己的贡献将在哪一年、哪一种任务中被用上。
因此,人工智能的史诗也应当容纳使用者的经验,以及没有进入模型名字的劳动。技术是否值得信任,最终要回到具体的人与事:它帮助谁做成了什么,使谁承担了额外的错误,又是否给受到影响的人留下了理解和纠正的机会。
插曲:没有一格知道,自己正在远行
走到这里,不妨暂时把那些庞大的机器放下。我们来看五个小小的格子。
它们排在一张方格纸上,有的亮着,有的暗着。每个格子只需要知道周围八格的情况:亮着时,有两三个亮邻居就保持,否则熄灭;暗着时,恰有三个亮邻居就点亮。所有格子同时迈向下一代。没有谁负责看全局,也没有谁接到“向右下方移动”的指令。
现在,走四代。
那个小小的形状变了几次,又重新出现了,只是比原先向右、向下各挪了一格。再走四代,它又走远一点。你盯住一个固定位置,会发现格子从未搬家,只是在亮与暗之间切换;你稍稍把目光放宽,却看见一个图案在旅行。
这是康威生命游戏中的滑翔机。1970年,马丁·加德纳向《科学美国人》的读者介绍了这个由简单规则展开的世界。它叫“生命”,但这里的亮灭只是数学状态。我们先借它学习一种观察的方式。[23]
先猜一猜:四代之后,是格子搬家了,还是图案走远了?
每格只看紧邻的八格,包括斜角。亮格有 2 或 3 个亮邻居就保持亮,否则熄灭;暗格恰有 3 个亮邻居就点亮。所有格子同时更新。
正在观察的世界
另一份:没有改那一格
点一格,看看它的下一代由哪些邻居决定;红色边框标出选中格与周围八格。键盘可用方向键选格。
同一条规则,为什么会有不同的世界
试过滑翔机,再换成方块或闪烁器。规则没有改变,改变的是起始排列。比较实验把此刻复制成两份,只翻转其中一格,再从第 0 代同步演化。差异可能消退,也可能改变后来的形状;页面显示的是实际计算结果。
“跟上图案”只移动观察窗口,不移动任何格子。计算采用按需扩展的方格平面,框外也参与更新,不从另一边绕回来。本页最多演示 128 代、每份最多 3000 个亮格;达到限制会暂停,不能据此推断它的最终命运。
这是康威生命游戏的数学规则,不是生物细胞或神经网络仿真。它没有学习目标,也没有感受或愿望的模型。滑翔机的移动来自格子间的相互作用,程序没有额外的“把滑翔机向右下搬一格”指令。
不操作也能读懂:亮格由周围八格决定去留,暗格在恰有三个亮邻居时点亮,全部同步更新。五格滑翔机每四代恢复相同形状,并向右下各移一格;固定位置的格子只会亮灭。方块保持静止,闪烁器每两代复原。同样的局部规则,能形成不同的整体模式。这种模式的出现不意味着它具有智能或意识。
试着让一个格子改变状态,再把两份世界一起往前推。也可以换成方块或闪烁器:同一套规则,有的排列保持安静,有的来回重复,有的向远处行进。规则规定了每个部分怎样回应邻居;整体会呈现什么,还取决于这些部分怎样相遇。
人们用“涌现”讨论的,正包括这样的现象:许多部分相互作用,形成了需要在整体层面描述的模式。对于眼前的滑翔机,“每四代向右下走一格”是一个有用的整体描述;如果只列出某一个格子的亮灭,就看不见这段旅程。知道底层规则,仍有许多关于整体结构的问题值得研究。物理学家菲利普·安德森在1972年的《More Is Different》中,讨论过不同复杂性层次为何需要各自的概念与研究。[24]
把目光移向天空,也能找到值得追问的景象。一群鸟转过方向,像一片正在改变形状的云。1987年,克雷格·雷诺兹发表了一种群集动画模型:让个体避开过近的邻居、调整方向、靠近局部群体,就能生成协调的群体运动。模拟没有为每只鸟逐帧写好整条路线。它提供了一种局部相互作用形成整体秩序的解释模型,并不等于把真实鸟群的全部行为解释完了。[25]
这样的观察,会改变我们看待智能的目光。你已经见过感知机的一次乘法、加法和参数更新;也知道一个结点的计算,并不能独自代表整张网络的能力。连接方式、数据、学习过程与环境反馈,使研究从“一个部件怎样工作”,走向“一个系统能形成怎样的行为”。但并不是把简单东西堆得足够多,任何能力就都会自行出现。刚才那个始终不动的方块,正提醒我们留意结构和条件。
大语言模型研究中也使用“涌现”这个词。2022年的一项研究讨论了一些在小模型上不明显、在更大模型上出现的任务表现;2023年的另一项研究则指出,某些看似突然的跃升,可以由评分方式造成。想象一次只计“全对或全错”的考试:从答对九成到全部答对,分数会从零跳到一,进步却未必在那一刻才发生。这个教学类比提醒我们,读能力曲线时也要读评分规则。这些工作讨论的是可以测量的任务表现,不是意识突然诞生的证据。[26]
而意识,把问题带到了更贴近我们自身的地方。
你能把“疼”写成一个字,也能说出疼痛发生在哪里。但对你而言,疼痛还有被经历的一面。记起一个人时,除了能够复述他的名字,也可能有思念;看见一束光时,除了区分波长,还有你眼前的明亮。这里所说的意识,主要指这种主观体验。完成任务的能力,与是否有这样的体验,需要分别研究。
当机器能认字、写诗、解释自己的回答,我们多了一批可以观察和检验的能力,也遇到了新的疑问。哪些结构与过程可能支持主观体验?怎样区分学会了谈论感受,与确实有感受?称它为“涌现”,还没有回答这些问题。研究者正在把不同意识理论的预测写清,交给实验比较,也尝试据此讨论人工系统;这些框架仍在接受检验,并非一张凭对话就能盖章的证明书。[27]
这条尚未走完的路,正把问题交到后来的人手里。我们能够用仪器记录神经细胞的活动,也能够读到一首诗,却还没有走完从物质活动到主观体验的解释之路。如今,我们又亲手造出一些能完成认知任务的系统,可以改变它的结构,观察它的错误,追问一种能力究竟怎样形成。关于智能的某些问题,正从想象走向可以动手研究的地方。
回头再看那个滑翔机。五个格子,一套规则,一段你可以逐格检查的旅程。知道它怎样发生,并没有使它失去美。理解让惊奇有了更深的根:原来,世界上值得探索的东西,既藏在最小的部分里,也藏在部分之间。
尾声:这一页之后
回望这些年代,你会发现,历史把事情排成一条清楚的线,身在其中的人却很少拥有这样的视野。他们看见的往往只是眼前的困难,以及一种尚未得到充分证实的可能。
图灵没有等到今天的计算机。罗森布拉特没有等到深度学习的浪潮。许多人的工作后来被修改、被吸收,甚至连名字也不再出现在通俗讲述里。我们今天能够从一个已有的工具开始,正因为有人曾经必须从问题本身开始。
想到这里,对技术的敬意就有了具体的对象。它可以给那些敢于提出问题的人,也可以给愿意承认错误的人;给创造方法的人,也给保存资料、核对结果、使下一位研究者少走一段弯路的人。
你刚刚入学,不需要立即许下改变世界的诺言。认真完成一次实验,发现结论不成立时保留失败的记录,用到别人的工作时注明来源,学会之后再耐心讲给一个后来的人——这些事情本身就有分量。
没有人能事先保证,一次诚实的工作最终会走到哪里。我们能够决定的是,在它经过自己手中的时候,尽力使它清楚、可靠,能够交给别人。
这本书接下来请你回到那张写着数字的纸。你认得它,却未必能写出让机器识别它的全部规则。问题很小,里面却藏着我们刚刚走过的两条道路:把规则写清,或让机器从例子中学习。
从这里开始,你会慢慢获得判断一个结果的能力。等到有一天,你也把一个尚未解决的问题和已经查明的事实留给后来的人,这场跨越许多生命的接力,就有了与你有关的一段。
现在,翻到第 1 章 机器怎样看见一个字。
史料与延伸阅读
以下资料用于核对文中人物、年份、方法与研究结果;技术解释和段落间的评述由本书组织。回忆材料按回忆使用,不据此虚构当事人的对话或心理。原有史料核对日期:2026-10-13;涌现与意识补充资料核对日期:2026-10-15。
- [1] 图灵,On Computable Numbers(1936):计算模型、通用机器与可计算性的限制。文中的进位、比较程序和现代停机问题反证写法均为教学改编,非原论文设备或表格的复原。
- [2] 图灵,Computing Machinery and Intelligence(1950)档案入口;论文全文:模仿游戏与学习机器。
- [3] 剑桥大学国王学院,Alan Mathison Turing(1912—1954):生平与档案。
- [4] 麦卡锡等,达特茅斯研究提案(1955):为1956年夏季研究所写,不能把提案日期与活动年份混为一谈。
- [5] 麦卡洛克、皮茨,A Logical Calculus of the Ideas Immanent in Nervous Activity(1943):简化神经活动的逻辑模型。
- [6] 康奈尔大学,罗森布拉特与感知机的校史报道及学生回忆;罗森布拉特,1958年感知机介绍原文。四角异或是本书说明线性分类边界的例子,不代表早期感知机研究的全部范围。
- [7] Buchanan、Shortliffe主编,MYCIN研究文集,第16章:知识获取(1984)。
- [8] SRI,Shakey研究记录:1966—1972年的机器人研究。
- [9] 计算机历史博物馆,专家系统研究者口述记录(2017):用于理解知识工程、研究路线与“寒冬”叙事的差异。
- [10] 珀尔,Probabilistic Reasoning in Intelligent Systems(1988)。
- [11] 鲁梅尔哈特、辛顿、威廉斯,Learning Representations by Back-propagating Errors(1986);诺贝尔奖科学背景材料,Toward Deep Learning(2024)。
- [12] 邓嘉等,ImageNet: A Large-Scale Hierarchical Image Database(2009)。
- [13] Krizhevsky、Sutskever、Hinton,ImageNet Classification with Deep Convolutional Neural Networks(2012)。
- [14] 何恺明等,Deep Residual Learning for Image Recognition(2015年预印本)。
- [15] Sutton、Barto,Reinforcement Learning: An Introduction(第二版,2018):强化学习的基础与历史背景。
- [16] DeepMind,AlphaGo研究与比赛记录;Silver等,Mastering the Game of Go without Human Knowledge(2017)。
- [17] Vaswani等,Attention Is All You Need(2017)。
- [18] Brown等,Language Models Are Few-Shot Learners(2020);Ouyang等,Training Language Models to Follow Instructions with Human Feedback(2022)。
- [19] DeepSeek-AI,DeepSeek-R1(2025)。
-
[20] Jumper等,Highly Accurate Protein Structure Prediction with AlphaFold(2021)。
-
[21] 香农,继电器与开关电路论文原件、MIT馆藏记录:原件署名日期与馆藏学位年份不同,文中分别说明。
- [22] 诺贝尔奖教育资料,点接触晶体管、集成电路与互连;基尔比,诺贝尔演讲。
- [23] 马丁·加德纳,The Fantastic Combinations of John Conway’s New Solitaire Game “Life”(1970),原文重收于《The Oxford Book of Modern Science Writing》,书内第276—283页。原规则采用八邻域、同步更新及无限方格平面;本页图案结果由规则计算,未复制原文插图。
- [24] P. W. Anderson,More Is Different(1972):关于不同复杂性层次、解释与研究的论述,不是“大模型越大便必然产生意识”的证明。
- [25] Craig Reynolds,Boids:作者对群集模型的说明及1987年论文入口。这是简化的行为动画模型;正文没有把其局部规则当作真实鸟群行为的完整理论。
- [26] Jason Wei等,Emergent Abilities of Large Language Models(2022);Rylan Schaeffer等,Are Emergent Abilities of Large Language Models a Mirage?(NeurIPS 2023)。不同工作对任务、指标和“涌现”的界定需要一起阅读;考试例子为本书类比,不是论文原始数据。
- [27] Patrick Butlin等,Consciousness in Artificial Intelligence: Insights from the Science of Consciousness(2023):基于多种意识理论提出人工系统的指标框架;其判断有理论前提,不是已确立的通用意识测试。COGITATE Consortium等,两种意识理论的对抗性检验(Nature,2025):在人类被试中比较预先登记的理论预测,部分结果支持、部分结果挑战两种理论;并非对AI意识的直接测定。
第 1 章 机器怎样看见一个字
1.1 你认得它,却说不完规则
再看序言里那两个“3”。如果身边有同学,请他也写一个。你们可以交换纸张,试着解释:自己凭什么把这几个不同的图案叫作同一个数字?
“有两个弯。”那么“8”也有弯。“左边开口。”有些潦草的“3”几乎闭合;有些人的“8”却留下缺口。你可以继续补充规则,但每增加一种写法,规则似乎就要照顾一种新的例外。
问题还可以更小。先不考虑整页文字,只给机器一张已经裁好、摆正、里面只有一个数字的小图,让它在0到9中选一个答案。这个任务叫图像分类。找出纸上每个字的位置、识别一整行文字,都比它多出别的困难,先留到后面。
我们现在有一个可以动手的问题了:同一个数字有许多形状,怎样让机器抓住它们之间有用的共同点?
1.2 把纸上的痕迹交给数
放大一张数字图像,会看见一个个像素。灰度图可以给每个像素一个数,表示该位置的亮度。下面是一张为理解表示而画的5×5小图,1表示有笔画,0表示空白;它只是示意,没有来自真实识别实验。
| 行\列 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 1 | 0 | 1 | 1 | 1 | 0 |
| 2 | 0 | 0 | 0 | 1 | 0 |
| 3 | 0 | 1 | 1 | 1 | 0 |
| 4 | 0 | 0 | 0 | 1 | 0 |
| 5 | 0 | 1 | 1 | 1 | 0 |
先从远处看,再逐行读这些数。你看见的轮廓和机器接收的数表,来自同一份表示。按固定顺序把五行接起来,就得到25个数;顺序不能随意打乱,否则原来的邻接关系会丢失。
试着把第2行第2列与第4行第2列改为1。新图在视觉上更接近哪个数字?一次只改一格时,你从哪一步开始犹豫?你的判断可能与同学不同。这恰好提醒我们:图像很小、笔画信息有限时,答案未必像标签那样干净。
本书第七章将使用8×8图像,每张图有64个灰度值。把这64个数按顺序排成一行,叫一个向量;许多这样的行排成一张表,叫矩阵。你会在数学课上更严格地学习它们。此刻先记住:这里的一行数,对应一张图,不能把两张图的行号弄混。
这一步叫表示:把要处理的事物变成计算能够接触的形式。选取的形式保留了什么,也就决定了后面的模型有机会利用什么。若缩小图像时把一个细小缺口抹掉,再复杂的模型也无法从这份输入中直接找回那条已经丢失的观测。
1.3 找一张最像的,行不行
假设我们各留一张“3”和“8”作为样板。新图来了,把对应像素相减、取绝对值,再把差异加起来;与哪张样板差得少,就猜哪一类。
先用只有四个格子的虚构数据手算。它们不画完整数字,只让比较过程短到能够一眼核对:
| 对象 | 四个值 |
|---|---|
| 甲类样板 | 0,1,1,0 |
| 乙类样板 | 1,0,0,1 |
| 新输入 | 0,1,0,0 |
新输入与甲的总差异是1,与乙是3。按这个约定,它归甲类。这里没有哪个部件知道“甲”是什么意思;程序只是按我们选定的表示与比较规则执行。
现在把一幅图整体向右挪一格。对人来说,它还是原来的字;对逐格相减的程序来说,许多位置却同时变了。于是,“看起来差不多”与“按这套计算差得少”发生了分歧。
这是一种值得停下来的时刻。我们已经写出了明确的方法,方法也确实会工作;可它擅长的事情,比最初的愿望窄。下一步可以改变比较方式、对齐图像、增加样板,也可以尝试让一个模型从更多样本中学出判断。模型先理解为一套把输入变成预测的计算办法,其中可能包含需要学习的数字。
1.4 让例子参与规则的形成
准备许多图像,并为每张图写下它应属于哪个数字。这个已知答案叫标签。图像与标签共同构成训练样本。
以前,我们试图直接告诉机器“什么样的笔画算3”;现在,我们先选一种有可调数字的模型,让它试着回答,再根据答案与标签的差别调整这些数字。训练完成后,模型带着调整后的数字去处理新图。
这就是监督学习的一种基本情形。“监督”指训练时有目标答案可供比较,不意味着每一次预测都有人在旁边指点。人仍然决定任务、整理数据、选择模型和学习方法;学习算法承担的是其中一部分调整工作。
一种变化在这里发生了:我们不必把每一种字形的判别步骤全部写出来,转而设计让例子能够改变程序表现的过程。卷首感知机的故事就在追问这件事。下一章会把“调整”拆到只剩一个数字,使你亲眼看见它怎样发生。
1.5 一张图像之外,还有一个世界
给图像贴标签的人也可能犹豫。一个数字写得像“3”又像“8”,是该强行选一个,还是留下“无法判断”?若样本全来自同一个人的工整字体,模型面对另一个人的笔迹会怎样?
先把这些问题留在页边。第三章会回来处理它们。现在不用急着相信“训练好了”这四个字。
先在下面的图里试一次:把“3”反色,观察像素差异与判断;再打开输入处理,看看是否恢复。也试试清空所有像素,再决定是否允许程序拒绝判断。输入怎样表示、模型怎样比较、任务允许什么答案,是三个彼此关联的问题。
图像分类还只是人工智能的一种任务。预测明天的数值、生成一段文字、选择下一步动作,会需要不同的输入、目标和验证方式。
把一张“3”反色,它在人眼里还是“3”。只比较像素的程序会怎样?先猜,再试。
对照术语与组成
本章所用8×8数据的规格可在scikit-learn数据说明核对(2026-10-13)。完整实验及来源许可见图像实验:64个数怎样变成一个数字。纸上的5×5和四格样板是本书自制的教学模型,不是那个数据集中的样本。
1.6 带着问题离开这一章
- 一张已经裁好的数字小图,与一张完整试卷照片,要求机器解决的任务有什么不同?
- 将四格新输入改为“1,1,0,0”,它与两张样板各差多少?如果出现平局,你会怎样规定输出?
- 请两个人各写几个数字。挑一张你们判断不一致的图,记录分歧,不急着把它消掉。
参考答案与思路见附录 E 练习参考思路。接下来,我们要弄清楚“机器从错误中学习”这句话里,究竟是哪一个数发生了变化。
第 2 章 把错误变成进步
2.1 猜错以后,哪一个数会改变
上一章,那张手写的“3”已经变成了64个数。现在我们把它放到桌面的一端,在另一端放下一个问题:这些数,怎样变成“这大概是3”?
高中时,你已经见过一种连接输入与输出的办法。对于 f(x)=2x+1,给它3,就得到7。函数描述这样的对应关系:在约定的输入范围内,每个输入都有一个确定的输出。它不必只有一个数进、一个数出。一个识字函数可以接收64个像素值,输出10个分数,分别对应0到9;再把分数转换成一组概率,用来表达模型对各个类别的判断。
你也可以把声音片段交给一个函数,得到文字;把棋盘局面交给另一个函数,得到各个落点的评价。许多看起来相距很远的任务,在这里拥有了共同的提问方式:输入是什么,希望得到什么输出,二者之间的关系怎样建立?
困难是:我们知道想要什么,却不知道怎样把那个函数写出来。给出64个数以后,该做哪些乘法、加法和判断,才能认出你的“3”、同桌的“3”,以及一个从未见过的人的“3”?一条 2x+1 太小了;逐个列举所有笔迹,也走不完。
一种办法,是先搭出一类带可调参数的函数,再让样本参与选择其中的一个。我们把它记作 fθ(x):x是这次输入,θ读作theta,代表一整组可调整的数字。固定这些数字,就得到一个具体函数;改变这些数字,函数的形状和判断也随之改变。训练调整参数θ;推理把输入x交给已经得到的函数。
这就是“把任务看成函数”的入口。它帮助我们组织认字、预测和决策等问题。至于主观体验为何存在,光写下一个函数符号,还没有给出解释。
回到那张“3”。假如识别器把它猜成了“8”,你纠正以后,它回答“抱歉”,然后下一次仍然猜错。
这段情境没有证明它进行过学习。道歉是一段输出;在本章讨论的参数训练中,学习需要模型内部的数字发生改变。一轮对话是否会被服务提供方另行用于训练,取决于具体产品与设置,不能从一句“我记住了”推断。
先在下面的玩具生成器里,把错误答案设为最可能项,再将温度调到0,生成一百次。它会稳定地答错。把温度调高,结果开始变化,却仍没有发生参数训练。函数可以输出一组概率,再由抽样过程选出不同答案;同一个函数也能参与产生不同的结果。我们要区分三件事:输出变了、输出对了、模型学到了能够用于新输入的规律。
问题:1 + 1 等于几?本地玩具生成器有三个候选:2、3、不知道。你可以故意让错误答案概率最高。
现在,把识字函数缩小到我们能看清每一步的程度。先研究一条线怎样改变,再把简单的计算接起来。
2.2 一条线能做什么
先把64个像素暂时放下,想象每张图只提取两个数:上半部笔画有多少,下半部笔画有多少。它们组成一个点。把许多已标好类别的点画出来,就得到一张可以观察的地图。
这里的两个输入可以记作x₁、x₂。先写一个带参数的函数 f(x₁,x₂)=w₁x₁+w₂x₂+b:给两个数各乘一个权重,相加,再加一个偏置,根据结果在阈值哪一侧来分类。在二维图上,它对应一条分界线。权重控制各项怎样共同影响结果,偏置帮助移动这条线。
例如,输入 x=[2, 3],权重 w=[0.5, -1],加权和为 2×0.5 + 3×(-1)=-2;偏置为1时,结果是-1。这种对应相乘再相加的运算叫点积。数字很少时可以手算,多起来以后仍由同样明确的运算组成。
回到卷首的三、感知机:让错误改变下一次判断,试一次“学习这个点”:先看它在哪一侧,再观察判断错误后分界线怎样移动。模型没有收到“画一条更聪明的线”这样的指令,它收到的是样本、标签和一条明确的更新规则。
不过,上下两部分的笔画数未必足以区分“3”和“8”。这是我们自选的简化表示,没有经过实验验证。参数能够改变,也不能弥补一切信息缺失。
2.3 用一个权重看见学习
现在把模型进一步缩小为 f_w(x)=w×x,只留下一个可调数字 w。这里的w就是刚才那组参数θ的全部。这样的函数不会认字,却足以让我们完整检查一次“根据错误改进”的计算。
第一遍的目标只有三件事:算出一个预测、算出平均平方误差、用给定的梯度完成一次更新。可以在纸上做,不要求先推导导数,也不要求立刻运行程序。本节后面的数学延伸和重复实验说明可以第二遍再读。
预测离散类别叫分类(classification),预测连续数值叫回归(regression)。先看一个回归的微练习:只有一个权重的模型,输入 x,预测 w×x。四条虚构训练记录为 (1,2)、(2,4)、(3,6)、(4,8)。我们能看出 w=2 正好匹配,但程序先从 w=0 开始。它需要一个表示“差多少”的数,才知道往哪里改。
把每条预测减去标签,平方后取平均,得到均方误差(mean squared error,MSE)。初始预测都是0,平均平方误差为 (4+16+36+64)/4=30。把用来驱动训练的差异称为损失(loss);训练尝试降低它。它与最终任务指标可以相关,却不等同:分类时,损失降低也不保证每次准确率都上升。
先手算更新:本例在 w=0 时的梯度给定为 -30,学习率为0.01,因此新权重是 0 - 0.01×(-30)=0.3。四个新预测是0.3、0.6、0.9、1.2;与标签之差的平方平均为21.675,小于原来的30。
下面用程序核对这一次更新。完成第五章环境与语法练习后,在同一练习目录保存为 one_weight.py,运行 python one_weight.py;尚未准备环境时可以跳过代码。
# 四条虚构测量;这个微练习只观察训练过程,不评估泛化。
xs = [1.0, 2.0, 3.0, 4.0]
ys = [2.0, 4.0, 6.0, 8.0]
w = 0.0
learning_rate = 0.01
def mse(weight):
return sum((weight * x - y) ** 2 for x, y in zip(xs, ys)) / len(xs)
gradient = 2 * sum(x * (w * x - y) for x, y in zip(xs, ys)) / len(xs)
print(f"更新前: w={w:.3f}, 损失={mse(w):.3f}, 梯度={gradient:.3f}")
w = w - learning_rate * gradient
print(f"更新后: w={w:.3f}, 损失={mse(w):.3f}")
预期是 w=0.000, 损失=30.000, 梯度=-30.000,更新后 w=0.300, 损失=21.675。梯度(gradient)在这里告诉我们损失随权重变化的局部方向与速度;沿梯度的反方向改一小步,叫梯度下降(gradient descent)。负梯度使本次权重增大。学习率0.01控制步子大小;它不是最终学出的权重。
先分清两类数字:w是从训练资料中学习的模型参数(model parameter);学习率是我们设置的超参数(hyperparameter)。前者是这次要改变的量,后者控制怎样改变。超参数怎样比较,会在第三章解释。
第二遍再读:梯度从哪里来。这个特定例子的损失可写成 7.5×(w-2)²,对 w 求导得到 15×(w-2),代入0即为-30。代码里的求和公式是同一计算的逐样本写法。还没学导数,可以先用给定的梯度做更新,随后在数学课程中补导数与梯度;不把会代入误当成已经会推导。
试一试:把学习率改为1.0,先算 w=30,再看损失应变成5880。步子变大不一定更快接近答案。这项手算没有独立测试集,仅用来观察优化,不证明模型能推广到新数据。继续实验时另做训练/开发/测试拆分;不要把重复这四条资料一百遍当成增加了独立信息。
以后做重复实验时再读。若三次独立训练的准确率是0.7、0.8、0.9,均值0.8;但另一组0.8、0.8、0.8也有同样均值,波动不同。重复运行同一随机种子只能核对复现,不能算三次独立训练。模型给出的0.8也不自动表示“十次一定对八次”;需要在相应人群与任务上检查概率校准。小样本、数据变化、错误代价都要和分数一起解释。
第一遍能解释30怎样变成21.675,以及学习率为何不是权重,就可以继续。线性代数、概率统计和优化需在后续系统学习,本例不代替这些课程。
2.4 学不会,也可能是模型装不下
你已经看见,合适的一小步可以让损失降低。接下来很容易产生一个猜想:只要再多训练一会儿,所有错误是不是都会消失?
卷首的三、感知机:让错误改变下一次判断里,有四个摆在正方形四角的点。对角同类,相邻异类,这叫异或的分类图景。试着在交互图里转动、移动那条直线:它总会留下分错的点。
困难来自模型能够表达的形状。它只能给出一条直线,而这个任务需要别的表示或边界。增加训练次数,无法让同一条直线具有它本来没有的能力。
图中还提供了一个人为构造的新特征:将两个坐标相乘。四个角中,两个坐标同号时乘积为正,异号时为负,于是可以按乘积的正负分类。原来难分的点,在这个新表示下变得容易区分了。这次特征是人给的;神经网络的重要能力之一,是在训练中学习有用的中间表示。
2.5 多层计算怎样一起改
一条直线装不下的关系,能不能交给几条简单规则共同完成?
把一个函数的结果,交给另一个函数
先不画网络。设 g(x)=2x+1,再让 h(u)=3u−2 接住它的结果。输入4,先得到9,再得到25。把这两步写在一起,就是 h(g(x)),称为函数复合。中间结果9是真实参与计算的量,只是我们最终关心的是25。
这给了我们一种搭建复杂计算的方法:让前一层的输出,成为后一层的输入。但先别急着多叠几层——刚才的组合展开后是 3(2x+1)−2=6x+1,仍然是一条直线。如果各层只有这类线性或带偏置的仿射变换,无论接多少层,整体仍能合并成同类变换。
我们需要一种让整体有机会弯起来的环节。
一个小小的折角
看这条规则:输入为负,就输出0;输入为正,就原样输出。写作 ReLU(z)=max(0,z)。它叫激活函数的一种:ReLU,中文常译作“修正线性单元”。它的曲线在0处折了一下,因此不是一条贯穿所有输入的直线。
现在先算 w×x+b,再经过ReLU,就得到一个小单元:h=max(0,w×x+b)。w改变倾斜方式,b和w一起决定折角在哪里。把几个这样的单元并排摆放,再给各自的输出乘上权重并相加,简单的折角就能拼出更复杂的形状。
例如,在0到1之间,把 max(0,x) 乘2,把 max(0,x−0.5) 乘−4,再把 max(0,x−1) 乘2,最后相加:前半段不断上升,过了0.5以后转而下降。三个简单单元,组合成了一座山峰。你可以代入0、0.5、1,核对得到0、1、0。
这已经是一张很小的前馈神经网络:一个输入,三个中间单元,一个输出。“前馈”表示这次计算沿着输入到输出的方向推进;中间这层常叫隐藏层,因为训练记录通常只给输入和目标答案,没有逐个指定这些中间单元应该输出什么。“神经元”是这些计算单元的惯用名字,它们是数学模型,与真实神经细胞仍有很大距离。
亲手改变这条曲线
下面先给你九个山峰状的样本。用一条直线去接近它们:转动斜率、移动截距,再训练几轮。某些误差会减少,可无论怎样调,一条直线都不能同时穿过先升后降的那些点。
随后换成三个单元。先选择一个单元,拖动它的参数:红虚线显示这一部分的贡献,蓝线显示它与其余部分合在一起的结果。再关掉非线性,看看三个单元为什么重新合成了一条直线。这个动作,比背下“神经网络需要激活函数”更能说明问题。
最后保留非线性,连续训练几轮。参数不再由你的手直接调节,而由样本误差驱动更新。也试试只改变“检查输入x”:蓝色空心点沿曲线移动,整条函数却保持原样。这分别是在改变函数与使用函数。
先试一试:一条直线能同时穿过这些山峰状的点吗?
先用滑块手调,再试训练。切换结构会重置参数,保留同一组样本;所有点都是本书构造的训练样本。
实心点:样本答案;蓝实线:当前函数;细竖线:预测与样本的差;蓝空心点:你正在检查的输入。红虚线:所选单元对输出的贡献。 纵轴随数值范围调整,请留意刻度。
移动输入只是在使用当前函数,不会训练;改变参数才会改变整条曲线。这里显示的小数经过四舍五入。
再追问:样本变了,函数会怎样?
这不会立即改变参数。先看误差,再训练;曲线追随的是给定样本,样本本身是否可靠仍要检查。
看清组成与训练的区别
每个单元计算 h = max(0, w×x+b),输出把三个 v×h 加起来,再加 c。十个参数是三组 w、b、v 和一个 c。红虚线只显示所选单元的 v×h,蓝线是它们相加后的结果。
这个按钮直接设置已知参数,不算训练,也不改样本。训练按钮则对全部样本计算均方误差,利用链式法则求每个参数的梯度,再同时更新;ReLU 折点处采用导数 0 的约定。未参与当前结构的参数不更新。
这是一个输入、一个输出的回归演示,不是识字网络。最多演示 2000 次更新;数值过大时保留上一步并暂停。训练误差不能证明对新数据有效,也不保证每一步下降。
不操作也能读懂:直线 f(x)=ax+b 只有斜率和截距可改,无法同时经过先升后降的三个非共线点。把 max(0,x)、max(0,x−0.5)、max(0,x−1) 分别乘 2、−4、2 再相加,在 0 到 1 内便得到一座山峰。若删掉 max,只把这些仿射函数相加,结果仍是一条直线。训练通过样本误差调整参数;输入的变化则只改变这次输出。
图中“载入手工参数”只是在展示这类网络能表达什么;训练按钮则让学习算法从起点自己寻找参数。这是两个不同的问题。能写出一个解,并不保证任何初始化和学习率都能顺利找到它。只追求贴住这九个点,也还没有检验新输入上的表现。
错误怎样找到前面的参数
我们又遇到了那个问题:最后答错了,前面每个权重各应改多少?
先看一条计算路径。某个权重改变一点,会让中间单元的输出改变多少?这个中间输出又会让最终预测改变多少?预测的变化会使损失增加还是减少?链式法则把沿途这些局部变化率相乘;若一个量经多条路径影响结果,就把各条路径的贡献相加。
反向传播沿着计算关系,从损失向前计算各个参数的梯度,并复用中间结果;梯度下降等优化方法再用这些梯度更新参数。预测沿网络向前流动,改变预测所需的信息沿计算关系传回来。没有哪个单元单独负责认出整个“3”,但它们可以通过同一个目标一起调整。
到卷首五、学会承认不知道,也学会把误差传回去的交互图里,把一次两段乘法展开。先预测修改前的输出,再走一次更新,看两个权重和损失怎样改变。那幅图只用两次乘法解释链式关系;本节的曲线实验则真的计算了三个非线性单元及全部十个参数的梯度。两幅图合起来,连接“误差怎样传递”与“函数怎样改变”。
回到那个手写的“3”
现在把一个输入换成64个像素,把一个输出换成10个类别分数。每个中间单元可以接收前一层的一组数,先加权求和、加偏置,再经过激活函数;下一层继续组合这些结果。把整张网络合起来看,它仍是 fθ(x)。增加的是结构、参数以及能够形成的中间表示。
在认字任务中,中间表示可能对某些笔画、位置或组合有用,但不必每个单元都对应一个人能命名的部件。我们知道每一步怎样计算,与我们已经理解网络学到了什么,是两回事。第一章的像素表示、卷首的感知机、这里的函数组合,以及第三章的新样本检验,至此连到了同一个问题上:怎样找到一个在任务中有用的函数?
回想插曲:没有一格知道,自己正在远行:理解局部规则之后,我们仍要研究组织起来的整体。生命游戏与神经网络采用不同的机制,后者还通过数据调整参数;它们共同邀请你把目光放在部分之间。一个“3”从纸面走进一组数,又经过许多简单计算,成为机器给出的判断。每一步都可以具体追问,组合的可能性却远没有被我们用尽。
本节的复合函数、山峰样本与配图为本书自制教学例子。关于多层结构、激活函数及链式求导,可继续读《动手学深度学习》的多层感知机与前向传播、反向传播和计算图(核对:2026-10-15)。
2.6 例子、结构与反馈
到目前为止,训练资料都有答案。监督学习把这些答案与预测比较。另一些学习任务从数据内部构造目标:例如遮住一部分,让模型根据其余部分预测被遮住的内容;或根据前面的文字预测后面的片段。这类方法常称为自监督学习,第六章会再遇到它。
也可以不给每条输入指定一个类别,研究数据怎样分组、怎样压缩,寻找其中的结构。行动任务则会用到来自环境的奖励,第4章讨论它。
这些方法各自改变了“学习信号从哪里来”。它们的名字不代表能力等级。选择方法前,先看自己有什么输入、能得到怎样的反馈,以及最终想解决什么问题。
2.7 模型外面那一层:让结果可检查
训练得到的模型,还要接到处理输入、保存结果和检查错误的程序里。第一章的一行数如果顺序错了,或者图像亮暗含义与训练时相反,模型仍可能安静地输出一个数字。
因此,一个识别程序至少要确认图像的形状与取值范围,说明无法处理的输入怎样返回,并保留出错样本。以后接上资料检索或工具调用,也要回答相同的问题:模型实际看到了什么,允许它做什么,结果怎样核对。
这层运行支撑有时叫 harness。下面的图描述一种组织方式。第一遍先理解输入、模型、检查与反馈之间的联系;第九章会把它用于多步行动。
一次虚构的“查通知并保存摘要”任务。先关闭一项支撑,再注入故障,比较执行记录。
对照术语与组成
没有外围结构能够保证模型永不出错。我们能做的是让错误更容易显露,让证据不足时仍有停止、拒答或复核的出口。
如果你开始好奇这些更新怎样被系统地推导,可以沿着附录 G 的深入模型:把直觉接到推导与实验去读 CS229 的线性回归或 CS231n 的反向传播笔记;需要补向量和矩阵时,前面的数学课入口也在那里。带着本章已经算过的例子去读,抽象符号会更容易找到落脚点。
2.8 练习
- 模型在对话里说“我学会了”,还需要知道什么,才能判断它的参数是否发生了训练更新?
- 用单权重例子算一遍:学习率0.01时为什么到0.3?若改为1.0,损失为何反而变大?
- 异或任务中,一条直线总分不对。你怎样区分“还没训练好”与“模型表达能力不足”这两种解释?
- 在曲线实验里,移动“检查输入x”和移动一个权重,分别改变了什么?只改一个训练样本的答案,曲线会立刻改变吗?
- 为什么把很多次
ax+b接起来仍不够?关掉激活函数后,实验中的网络还能画出山峰吗?
参考答案与思路见附录 E 练习参考思路。
错误已经能够改变参数。但训练时见过的题越来越熟练,是否就意味着它会做新题?下一章,我们把它带出练习本。
第 3 章 走出练习本
3.1 换一个人写,还认得吗
假设一个识别器把练习用的数字全认对了。你拿来同学刚写的一张,它却把“3”当成了“8”。这时继续展示原来的满分,已经回答不了眼前的问题。
我们需要的能力,是面对没有参与学习的新输入仍能作出有用判断。这叫泛化。训练题上的熟练程度提供了一条线索,却不能独自证明它。
若机器反复看过你的同一页笔迹,即使你把这一页复印成一百份,也没有因此获得一百种独立的书写习惯。数据怎样获得、怎样分开,会影响我们究竟检验了哪一种能力。先把这个问题记住,再看看“答对多少”本身还隐藏着什么。
3.2 两个八十分,意味着同一件事吗
十条虚构记录中,两条需要复核,八条不需要。方法甲把所有记录都判为普通,答对八条;方法乙找回了一条需要复核的记录,同时误报一条普通记录,也答对八条。
如果只看“准确率80%”,它们没有区别。负责复核的人却会看到不同的工作:甲没有发出任何提醒;乙发出两次提醒,其中一次有用。要比较它们,先说明我们更关心漏掉什么、又能承担多少误报。
先把认字问题暂放一旁,用十条虚构的复核结果,把一个总分里隐藏的差别拆开。这里直接给定预测,不训练模型。
把需要复核记作1,普通记作0,作为这次实验的标签。先不安装额外工具,在纸上检查两种方法的结果。
先逐条看这十条虚构结果;每一行始终代表同一条记录:
| 编号 | 实际标签 | 甲的预测 | 乙的预测 |
|---|---|---|---|
| 1 | 1 | 0 | 1 |
| 2 | 1 | 0 | 0 |
| 3 | 0 | 0 | 1 |
| 4 | 0 | 0 | 0 |
| 5 | 0 | 0 | 0 |
| 6 | 0 | 0 | 0 |
| 7 | 0 | 0 | 0 |
| 8 | 0 | 0 | 0 |
| 9 | 0 | 0 | 0 |
| 10 | 0 | 0 | 0 |
在纸上给甲、乙各画四个格子,每读一行,就在对应格子中加一:
| 实际情况与预测 | 预测需复核(1) | 预测普通(0) |
|---|---|---|
| 实际需复核(1) | TP,找到了 | FN,漏掉了 |
| 实际普通(0) | FP,误报了 | TN,正确放过 |
准确率看全部样本中答对的比例;精确率看“报出的需复核记录中,有多少确实需复核”;召回率看“全部需复核记录中,找回了多少”。方法乙报出两条、其中一条正确,所以精确率是1/2;实际有两条需复核、找回一条,所以召回率也是1/2。
这张计数表叫混淆矩阵(confusion matrix)。甲应是 TP=0、FP=0、FN=2、TN=8,乙应是 TP=1、FP=1、FN=1、TN=7。能解释这组差别,就可以继续3.3 先说明要解决什么。
准备好Python后:用程序核对四格计数
还没写过程序:这一轮跳过下面的代码和运行练习。准备动手时先完成5.4 安装、验证与独立环境,再回来核对。已有环境的读者,把下面代码保存到练习文件夹中的 count_predictions.py,在该目录运行 python count_predictions.py(未激活环境的 macOS/Linux 用 python3);只用 Python 本身,无须 Key 或额外依赖。
# 虚构的十条分类结果;1表示需复核,0表示普通样本。
labels = [1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
baseline = [0] * 10
candidate = [1, 0, 1, 0, 0, 0, 0, 0, 0, 0]
def report(name, predictions):
if not labels or len(labels) != len(predictions):
raise ValueError("标签与预测必须非空且等长")
if any(value not in (0, 1) for value in labels + predictions):
raise ValueError("这个练习只接受0或1")
tp = fp = fn = tn = 0
for actual, predicted in zip(labels, predictions):
if actual == 1 and predicted == 1:
tp += 1
elif actual == 0 and predicted == 1:
fp += 1
elif actual == 1 and predicted == 0:
fn += 1
else:
tn += 1
precision = tp / (tp + fp) if tp + fp else 0.0
recall = tp / (tp + fn) if tp + fn else 0.0
print(f"{name}: 正确{tp + tn}/{len(labels)},"
f"TP={tp} FP={fp} FN={fn} TN={tn},"
f"精确率={precision:.2f} 召回率={recall:.2f}")
report("总猜普通", baseline)
report("候选方法", candidate)
预期输出:
总猜普通: 正确8/10,TP=0 FP=0 FN=2 TN=8,精确率=0.00 召回率=0.00
候选方法: 正确8/10,TP=1 FP=1 FN=1 TN=7,精确率=0.50 召回率=0.50
完成第五章的语法小练习后,从这里读代码。先追踪一对标签与预测怎样被计数,其余校验和格式化可以分次理解:
labels是按顺序存放十个标签的列表,预测列表与它一一对应。zip把同一条样本的标签和预测配成一对;for让我们逐对检查。if / elif / else把每一对放进四个格子中的一个,+= 1表示计数加一。report把相同的计算收成一个函数,两种方法各调用一次。函数让比较共用同一口径。:.2f只把数字显示成两位小数,没有增加测量精度。
代码规定“没有报出任何正例时,精确率显示为0”。此时分母实际为0,严格地说精确率未定义;写明这个约定,才不会把程序显示的0误认成一次真实测量。实际正例数为0时,召回率也有同样问题。十条资料很少,还不足以估计部署后的误报和漏报。
编程轮再试一试:把候选预测改成全1,先手算,再运行。应得到正确2/10,精确率0.20、召回率1.00。召回率满分仍可能让大多数普通样本被误报。接着删去最后一个预测,程序应明确报“标签与预测必须非空且等长”,而不是悄悄少算一条。保留这两次结果,写一句“我在这个任务里更怕哪种错误,为什么”。
如果报 SyntaxError,先检查有没有把中文引号复制进代码,或漏了冒号;如果报 IndentationError,检查函数和条件分支的缩进。列表长度错误时先核对样本对应关系,不要用截短标签来压掉报错。下文的完整实验虽然用了现成评分工具,仍是在做这类可核对的计数。
3.3 先说明要解决什么
一个任务至少包含输入、期望输出和判定方法。本例的输入是记录,输出是二分类标签,判定依赖事先约定的参考标签。现实任务还要说明哪些记录属于范围、谁给标签、标签有分歧时怎样处理。
例如,把“帮我处理咨询”改写为:输入一条关于虚构活动的咨询,输出“时间、地点、报名、其他”之一;同时问两件事的输入先标为“需澄清”。这一步会改变数据怎么收集、模型怎么选、错误怎么统计。它通常比先选择一个热门模型更值得花时间。
简单办法可以成为基线。这里“全部猜普通”就是基线;文本里也可以先写关键词规则。基线提供一个比较起点,复杂方法要用实验说明它增加了什么价值。
“这个模型不错”很难检验。“在同一批未参与调参的咨询上,它能否比关键词规则少漏掉地点问题”则指出了比较对象、样本和关注的错误。
开始实验前,写清输入范围、判定规则、基线和指标。必要时同时记录延迟、资源消耗与人工复核量。它们可能改变方案的选择:一个只多答对一条、却让等待时间增加很多的方案,未必适合当前任务。
3.4 四种资料,各有用途
| 资料 | 用途 | 怎样避免混用 |
|---|---|---|
| 训练集 | 学习模型参数;提示词任务中可提供示例 | 不拿训练表现当作新输入表现 |
| 开发集 | 比较参数、提示词或流程,选择方案 | 可以反复检查,但承认选择已依赖它 |
| 测试集 | 方案固定后评价表现 | 不按测试答案改完,再宣称仍是独立验证 |
| 压力集 | 专门检查边界、罕见情形和已知失败 | 单独报告,不冒充自然输入的发生比例 |
拆分要尊重资料的来源。一个人的多张近似照片、同一模板改写出的句子、同一事件的多条记录,彼此可能很相似。如果它们跨越训练与测试两边,模型可能靠重复线索得分。按人、模板、事件或时间划分,往往比单纯随机分行更有意义。
测试集中的常见简单样本也有价值,前提是它们确实代表预期输入。100%的分数本身不能证明测试无效,也不能证明系统完美;要检查样本量、来源、泄漏与覆盖范围。另设压力集,才能同时了解日常表现和脆弱之处。
已经看过测试错误,还能继续改进。请把这轮结果作为开发证据,记录测试信息如何影响了选择;下一次独立评价再使用新的保留资料。原样复跑则是在核对复现,并不等于重新调参。
回到认字:若想研究新书写者,就尽量按书写者分开资料;若没有书写者信息,应把这一点写成限制。第七章的数据恰好存在这个限制,届时我们会看到它怎样影响结论。
3.5 结果变了,怎样找原因
回到本章的十条记录:乙原来只找回一条需复核记录。如果把第2条预测从0改成1,而其他九条保持不变,会怎样?先按表手算:TP从1变成2,FN从1变成0,FP仍是1,TN仍是7,答对9/10。然后再运行程序核对。
若程序没有得到这个结果,先核对是否只改了第2条预测、标签是否保持原样、两份列表是否仍一一对应,再看四格计数。这样就把输入、计数和解释分开检查了。不要为了得到预期分数去改参考标签。
这叫控制其他条件、只改变一个因素。它帮助我们判断本次差异与什么有关;不能据此声称这个方法遇到新资料也一定更好。之后再讨论多个因素怎样相互影响。
第八章会把这个方法用于检索问答:把人工找到的正确资料直接交给回答步骤,比较结果有没有变化。现在只需理解“固定其他条件、替换一个环节”,相关术语与操作到第八章再学。
3.6 留下一份可以核对的结论
一份短报告可以这样写:“固定拆分后,规则在18条测试咨询上答对18条,模型答对8条。规则与教学语料由同一示例设计,因此这不证明规则在真实咨询中更好。下一步收集独立来源的咨询,检查标签歧义,再比较。”第七章保留了这次负结果。
报告中保留分母、逐条预测与失败类型。若结果有随机波动,固定种子便于复现,再用多个种子观察变化。重复同一确定性程序一百次,不会得到一百份独立证据。
不必一开始就使用复杂平台。为这个练习写下四件事即可:标签是什么、两种方法各预测了什么、四格计数是多少、你怎样解释差异。把代码和输出放在一起,注明它们属于同一次运行。
“方法乙更好”还不够明确。“在这十条虚构记录上,方法乙比甲少漏报一条、多误报一条;能否采用取决于复核成本”更接近证据支持的结论。数据少不是不能做实验,而是结论需要留在数据能够支持的范围里。
公平比较看起来比换一个新模型慢一点,却让“更好”有了共同的含义。数据、基线和记录使一个人的观察能够被另一个人检查;也使我们有机会在自己相信的办法失败时,确实知道它失败了。
3.7 练习
- 方法甲与乙的准确率相同,为什么仍会给负责复核的人带来不同的工作?
- 同一个人的近似笔迹,或同一模板改写的短句,为什么不宜随意散到训练与测试两边?
- 只把乙的第2条预测改为1,四格计数怎样变化?这个改变有没有证明对新资料更好?
- 一次测试得到满分,你还想了解哪些信息?如果标签也有歧义,下一步应检查什么?
参考答案与思路见附录 E 练习参考思路。
到这里,我们能够检查一个预测。接下来的问题更难一点:有些选择要经过许多步,才能知道它的后果。那时,学习的反馈从哪里来?
第 4 章 当机器需要作出选择
4.1 答案不会在每一步出现
想象你在一栋不熟悉的教学楼里找教室。走到路口,有几条通道。墙上的指示牌可能帮到你,也可能只标了楼层。你选了一条路,直到走进尽头,才知道刚才应该往另一边。
数字分类的练习,每张图都有一个标签可以核对。找路却有不同的时间结构:一次选择改变下一次看见的情境,最后有没有到达,可能由许多步共同决定。该怎样让前面的选择从后来的结果中得到反馈?
强化学习研究的正是一类这样的序贯决策问题。系统观察环境,选择动作,得到新状态与奖励,再改进选择动作的方法。奖励是任务设计者提供的信号,不等于一份关于现实价值的完整判断。
4.2 一个只有五格的世界
把教学楼缩成纸上的五个格子,编号0、1、2、3、4。起点是0,终点是4,只能向左或向右。走到0的左边仍留在0,到4就结束。这是本书自制的小世界,不控制真实设备。
| 位置0 | 位置1 | 位置2 | 位置3 | 位置4 |
|---|---|---|---|---|
| 起点 ● | □ | □ | □ | 终点 |
每走一步,如果还没到终点,奖励-1;到终点的那一步,奖励+5。一直向右的奖励序列是 -1、-1、-1、+5,相加得到2。这一串奖励的累计值叫回报。
现在试两种办法。一种每次掷硬币决定方向;另一种始终向右。你甚至不需要程序,就能指出后一种更直接:我们知道终点的位置,环境也没有障碍。
这个比较值得保留。学习算法登场以后,不能把人已经写得清楚的好方法从比较表里拿掉。一个任务适不适合学习,与学习算法能不能在其中得到高分,是两个问题。
4.3 今天的选择,怎样看见明天的结果
用位置表示状态,左右移动是动作,“处于某状态时怎样选择动作”叫策略。
再画一张五行两列的表,记录每个位置向左、向右的价值。一个表格Q学习方法,会在尝试动作后调整对应格子的数。这个数要估计:先做这个动作,随后继续作较优选择,可以得到多少折扣后的累计奖励。
“折扣”表示较远的奖励按一个系数逐步乘小。取折扣因子γ=0.9,四步向右的折扣回报是 -1 + 0.9×(-1) + 0.9²×(-1) + 0.9³×5 = 0.935。它与上一节的不折扣回报2不同,报告时应说清算的是哪一个。
先检查最容易的一格。到达位置3后,再向右一步就结束,奖励为5。假设这格原来估计为0,学习率α=0.2,本次更新为:
新估计 = 旧估计 + α ×(目标 − 旧估计)= 0 + 0.2×(5−0)= 1。
它没有一下子变成5,而是向这次目标靠近了一些。对尚未结束的步骤,目标还要加上“下一个状态中最好动作价值的折扣”。因此,接近终点的位置先获得较有用的估计,之前的位置再通过后续经历把这些信息传回来。
这与第二章有一个相通的地方:根据差异作调整。但反馈变了。分类标签直接说明这张图是什么;行动的价值还依赖未来会发生什么,所以需要处理后续估计与探索。
4.4 为什么还要试一条不熟悉的路
假如系统总选当前估计最高的动作,它可能一直重复早先碰巧尝试过的路线,对另一条路一无所知。偶尔试试别的动作,叫探索;按已知的较好选择行动,叫利用。
探索会付出代价。在五格世界里,代价只是多走几步;对现实机器人,代价可能是一件打碎的物品。因此,不能把仿真中“多试几次”的自由直接交给真实设备。环境能否重置、动作有无危险、是否能从已有示范中学习,都会改变方法的选择。
完整实验放在选读:在五格世界里学习行动。先完成第五章的Python准备,再运行附录中的程序。它比较随机策略、一直向右和Q学习,保留三个预先选定的训练种子及逐回合记录。这个已验证示例中,学习策略追平了一直向右的策略,没有胜过它。
这个结果并不扫兴。我们知道它从交互中学到了足够完成小任务的选择,也知道在这个简单世界里,学习不是最省事的办法。两条认识可以一起留下。
4.5 当分数偏离了愿望
现在给五格世界增加“等待”动作,并设想每等待一步奖励+1。我们的愿望仍是尽快到达终点,但按γ=0.9无限等待的折扣回报会接近10,可能比尽快到达更诱人。
这是一个新设想,不是附录程序已经运行过的结果。它提醒我们:系统优化的是被写进任务的目标。奖励没有完整表达愿望时,一个很会提高分数的策略,仍可能做出我们不希望发生的事情。
第三章的准确率已经出现过同样的裂缝:高分可以掩盖漏掉的重要样本。这里裂缝进一步影响行动。设计指标与奖励,需要反复回到任务本身,看分数鼓励了什么,也忽略了什么。
4.6 从棋盘到地面
卷首的七、棋盘上的相遇:学习、搜索与行动中,围棋提供了规则明确、可以反复对弈的环境。你现在已经有几个可以理解它的词:状态、行动、策略、长期结果。它使用的方法远比五格表复杂,也结合了学习与搜索,但问题之间有了可以辨认的联系。
一只机器手抓杯子,还会遇到棋盘没有的事:杯子多重,摩擦够不够,相机位置是否准确,刚才的动作有没有让杯子滑动。具身智能把感知、身体与行动放在同一个循环中考察。它可能结合控制、规划、模仿学习与强化学习,不是给语言模型加上“抓取”按钮就完成了。
这种差别让我们重新看见人的日常能力。轻轻拿起一个杯子,包含了许多我们不必逐句说出的判断。造机器的过程,使这些判断重新成为值得研究的问题。
4.7 练习
- 一直向右为何已经是五格世界的一条好基线?学习算法追平它,支持什么结论,又不支持什么结论?
- 手算从位置3向右的首次更新,区分奖励5、新估计1、学习率0.2分别是什么。
- 为一个校园生活中的小目标设计奖励,再找一种“分数上升、目标却没完成”的可能行为。只作纸上讨论,不控制真实设备。
参考答案与思路见附录 E 练习参考思路。
这四章里,我们走过了图像、参数、陌生样本和行动。接下来,把纸上的这些关系放进自己能够运行的程序。第5章的每一次安装,都将服务于一个已经认识的问题。
第 5 章(上) 准备实验环境
本章目标:能够确认解释器、依赖与项目目录,运行一个本地程序
5.1 准备一个可以反复实验的地方
前四章里,你已经手算过预测、误差与一次价值更新。纸笔让步骤透明,程序则让我们能够处理更多样本,并把同一轮实验交给别人重跑。
先建立一个只属于本次练习的文件夹。在里面保存程序、运行它、找到输出。接着准备独立的Python环境,避免这个实验的依赖与别的项目混在一起,再用Git保留可以返回的版本。每件工具都对应一个具体需要。
本章上篇的环境支持第二、三、七章的本地实验。下篇接入已经训练好的语言模型,需要可用的服务、网络和相应费用。没有服务账号时,仍可先做CPU模型实验;正式L0要求另见章末与附录A。
每完成一步,观察一次实际结果。安装遇到系统、权限或网络问题时,保留原始报错即可求助;你不需要先独自解决整台电脑的问题,才有资格开始实验。
5.2 给自己配一个学习助手
可以先使用已有的对话工具,请它解释概念、报错和操作步骤;不必为了开始学习再安装一个复杂系统。需要助手读取或修改文件时,选择来源明确、有权限说明的工具,先在专用练习目录中使用。
把请求写具体:“我在 Windows 的 PowerShell 中创建 Python 虚拟环境,以下是完整报错。请先解释原因,再给一个最小检查步骤,并说明这个步骤是否会修改文件。”这样的请求比只贴一张截图更容易得到可检查的帮助。
收到建议后,先读命令的作用和路径,再决定是否运行。不要向助手发送密钥、未公开业务资料或真实案件材料。模型给出的工具名、下载地址和接口用法,需要到对应官方文档核对;助手也可能给出过期信息。
一个适合学习的工作方式是:自己说明目标,让助手给一个提示,自己执行并观察,再共同解释结果。即使代码由助手生成,提交作品时也需要能解释它。
5.3 一张地图:AI 工程的常用机制
虚构通知已从“周五”改到“周六”。任务:告诉我活动日期,并记进图内日历。迁到本机运行,能自动知道新通知吗?
使用现成模型时,通常会遇到六种机制:模型负责预测或生成,API 提供调用方式,本地推理提供另一种运行位置,编排组织步骤,RAG 将检索到的资料提供给模型,Agent 根据观察选择下一步行动。
配图用一份改期通知来分开观察运行位置、资料与工具权限。先只把“远端”换成“本机”,再逐个添加新版通知与日历工具,看看哪些结果发生了变化。这些机制可以组合,并不需要逐层安装;一个本地分类器可以直接在 Python 中运行,不需要远程 API、RAG 或 Agent。
本章先准备环境并学习 API 调用;第六章写清任务,第七章完成模型实验或小应用,第八章处理资料检索,第九章组织工具调用。遇到新名词时,先问它解决当前任务中的什么问题。
Agent 由哪几件组成
目标:查虚构通知里的新日期。每点一次,查看组件之间实际传递了什么;也可拔掉一个组件,观察链路停在哪里。
对照术语与组成
配图将本书的 Agent 示例分为模型、系统提示词、工具、状态、循环和护栏。它帮助你辨认一个工具请求怎样变成行动、结果怎样回到下一轮。
这是一种组成示意;第九章的六项能力则讨论怎样设计和验证系统。示例调用现成模型,也可以根据任务训练、微调或更换模型,之后重新评测。
5.4 安装、验证与独立环境
① 准备 Python 和编辑器
Python 运行本书的实验程序,编辑器用来保存和修改文件。可从 Python 官方下载页 与 VS Code 官方下载页选择适合系统的安装包。CPU 实验的已验证版本组合见CPU模型实验的工具包;不要为了追求最新而混用尚未核对的依赖。
先把文件放在一个找得到的地方,再运行它:
- 用系统的文件管理器,在自己的文档目录新建文件夹
my-first-ai,记下它的位置。 - 打开 VS Code,选择“文件 → 打开文件夹”(File → Open Folder),选中
my-first-ai。左侧应显示这个文件夹;这里以后保存本轮所有练习文件。 - 选择“终端 → 新建终端”(Terminal → New Terminal)。下方出现可以输入命令的面板,就是终端;输入一行后按回车执行。Windows 本章默认使用 PowerShell,面板中可以查看当前终端名称。
- 核对当前目录,也就是命令默认读写文件的位置:PowerShell 输入
Get-Location,macOS/Linux 输入pwd。结果应以my-first-ai结尾;不是的话,先关闭这个终端,重新打开正确文件夹并新建终端。
在这个终端检查解释器。Windows 输入 python --version;如果找不到,再试 py --version,并记住后续使用哪个。macOS/Linux 输入 python3 --version。应看到 Python 3.x.x。安装后仍找不到命令时,先重开终端;仍失败就记录系统、安装方式和原始提示,向助教求助。
在左侧文件夹中新建 hello.py,输入下面一行,用 Ctrl+S(macOS 为 Command+S)保存。只复制代码,不复制包住代码的反引号。
print("实验开始")
确认左侧文件名是 hello.py,不是 hello.py.txt。在刚才的终端按系统输入一条命令:
| 环境 | 运行命令 |
|---|---|
Windows PowerShell,刚才 python 可用 |
python hello.py |
Windows PowerShell,刚才只有 py 可用 |
py hello.py |
| macOS/Linux | python3 hello.py |
看到下一行输出 实验开始,就完成首次运行。若提示找不到文件,核对当前目录、文件名与是否保存;若看到 >>>,表示误进入了 Python 交互模式,输入 exit() 后回到终端再运行命令。以后每次修改代码都先保存,再运行同一命令。
编辑器运行按钮可能选择另一套解释器。本轮先用终端,方便明确究竟运行了哪个程序。操作入口可对照 VS Code 的 Python 教程(核对:2026-10-13)。
② 建立独立环境
每个项目使用一个虚拟环境,隔离它需要的库。继续使用刚才的 my-first-ai 目录和终端,不要再建一个同名子目录。
macOS/Linux:
python3 -m venv .venv
source .venv/bin/activate
Windows PowerShell(如果本机使用 py,创建环境时将 python 换成 py):
python -m venv .venv
.venv\Scripts\Activate.ps1
PowerShell 不允许激活脚本时,可以直接使用虚拟环境的解释器,无须为本练习修改系统策略:
.venv\Scripts\python.exe --version
.venv\Scripts\python.exe -m pip --version
后续命令里的 python 也相应替换为 .venv\Scripts\python.exe。如果选择 CMD,激活命令是 .venv\Scripts\activate.bat,与 PowerShell 不同。
激活后通常会显示 (.venv),但最可靠的检查是解释器路径:
python -c "import sys; print(sys.executable)"
python -m pip --version
路径应指向当前项目的 .venv。第二、三章的短程序和下面的语法练习只用 Python 本身,可以先跳过安装。准备进入第五章下篇的 API 对话时,再在这个环境安装两个库:
python -m pip install openai python-dotenv
用 python -m pip 能让安装工具与当前解释器对应。第七章模型实验另按工具箱安装其依赖,不必现在全部装好。虚拟环境的创建与使用可对照 Python 官方说明(核对:2026-10-11)。
五个语法小动作:读懂第三章的计数程序
在同一个文件夹新建 syntax_steps.py。下面是一个完整的小程序;先原样保存并运行 python syntax_steps.py(若没有激活环境,使用上面对应系统的解释器命令)。不要改变行首空格,缩进表示哪些语句属于同一段。
# 1. 赋值:给一个值起名字;print 把结果显示出来。
need_review = 1
print("标签:", need_review)
# 2. 列表:按顺序放多个值;下标从0开始。
labels = [1, 0, 1]
print("第一条:", labels[0])
# 3. 条件:== 比较是否相等;等号 = 则是赋值。
if labels[0] == 1:
print("第一条需复核")
else:
print("第一条普通")
# 4. 循环:逐项取出标签;每遇到一个1,计数加一。
count = 0
for label in labels:
if label == 1:
count += 1
print("需复核数:", count)
# 5. 函数:def 定义一组可重复使用的步骤,return 返回结果。
def is_review(label):
return label == 1
print("第二条需复核吗:", is_review(labels[1]))
预期依次输出:标签: 1、第一条: 1、第一条需复核、需复核数: 2、第二条需复核吗: False。False 表示“不成立”,True 表示“成立”。函数定义时不会自动处理每条记录,要写出 is_review(...) 才会调用。
现在只把列表改成 [0, 0, 1],先在纸上预测哪些输出会变,再保存运行。第一条应变成普通,需复核数变成1;第二条仍不需要复核。能解释这三个变化,就可以返回第三章的计数实验。那里新增的 zip(labels, predictions) 会将同一位置的两个值配成一对,循环同时取出它们;每一对对应纸上表格的一行。
这一小段只负责帮你读懂计数例子。之后继续按“变量与类型 → 条件与循环 → 函数 → 文件读写 → 模块与异常”的顺序补 Python;每学一项就改一个能观察结果的小程序,不必先完成一整门课才继续实验。
③ 安装 Git,记录自己的修改
从 Git 官方入口安装适合系统的版本,运行 git --version 核对。Git 用来记录文件变化;它不代替虚拟环境,也不会自动上传文件。
安装后重新打开编辑器的终端;若仍提示找不到 git,先检查安装是否成功、是否已加入命令搜索路径,再请同伴协助。下面的 Git 命令可在 Windows PowerShell、macOS 或 Linux 的终端运行,当前文件夹应是前面创建的 my-first-ai,不要输入到 Python 的 >>> 提示符中。
在这个项目文件夹中创建 .gitignore,文件名不要带 .txt,写入:
.venv/
.env
__pycache__/
然后在同一文件夹的终端运行,初始化仓库:
git init
首次提交前:设置并核对 Git 署名
Git 会把作者姓名和邮箱写进每次提交。先把下面两行引号中的文字换成自己的署名和提交邮箱,再执行;不要原样保留占位文字,也不要填密码或 API Key。
git config --local user.name "你的署名"
git config --local user.email "你的提交邮箱"
署名可以是你希望显示的名字,不要求等于 GitHub 用户名。邮箱会随提交记录保存;准备公开仓库又不希望公开个人邮箱时,可从 GitHub 的 Settings → Emails 复制它提供的 noreply 隐私邮箱,不要自己猜地址。这两项用于提交署名,不会登录 GitHub,也不会上传文件。关联 GitHub 账号与邮箱的说明见 GitHub 官方文档(核对:2026-10-29)。
--local 表示只设置当前仓库;换一个仓库时还要检查。若是自己的电脑,希望同一系统用户的各个仓库默认使用相同署名,可以把上面两条设置命令的 --local 换成 --global。本仓库的设置会优先于全局设置,共用电脑按本仓库设置即可。配置范围见 Git 首次配置说明(核对:2026-10-29)。
检查当前仓库实际使用的值:
git config --get user.name
git config --get user.email
两行应分别输出自己的署名和邮箱。若没有输出,重新检查对应设置;若提示 not in a git directory 或 not a git repository,先确认终端位于项目文件夹,并已运行 git init。
保存第一条提交,并确认结果
先只保存刚创建的 .gitignore,练习“查看—暂存—核对—提交”。其他练习文件以后再按需加入:
git status
git add .gitignore
git diff --cached
git commit -m "记录实验环境的忽略规则"
git log -1 --oneline
git log -1 --format="%an <%ae>"
git status
git diff --cached 应显示即将提交的三条忽略规则;如果进入分页阅读界面,按 q 返回终端。提交成功后,历史中应出现一个短编号和“记录实验环境的忽略规则”,下一行显示正确署名与邮箱。最后的 git status 仍可能列出尚未提交的 Python 练习文件,这是正常的;检查 .env 和 .venv 没有进入暂存清单。
git status 显示文件状态;git diff 查看已跟踪文件中尚未暂存的改动;git diff --cached 查看已经暂存、准备提交的改动。新文件在 git add 前通常只出现在状态列表里,单看 git diff 没有输出,不能据此判断“没有文件可提交”。参见 Git 差异查看说明(核对:2026-10-29)。
如果提交时出现 Author identity unknown 或 unable to auto-detect email address,回到上面的署名配置,设置并核对两项,再检查 git diff --cached,确认后重试原来的 git commit。这次身份错误不会清空已经暂存的文件;没有再修改文件时,无需重新 git add,也无需删除仓库或重装 Git。git status 和 git diff 都是查看命令,漏跑它们不会造成身份缺失。
常用操作见13.2.2 Git:够用的六个动作;对话程序的交付与远程上传见第三步:把仓库交出去。
④ 检查准备结果
你现在应能解释:文件保存在哪,程序由哪个 Python 运行,依赖安装在哪,Git 将记录哪些文件。把3.2 两个八十分,意味着同一件事吗的代码保存到同一文件夹中的 count_predictions.py,运行 python count_predictions.py,核对两行预期输出,再做一次先预测后改动。今天做到这里已经完成一次本地实验;需要 API 对话时继续下篇,模型实验见第七章。
想把“能运行”进一步变成“能自己写”,可以从附录 G 的起步:给好奇心准备工具选择 CS50P 的函数、条件和循环;遇到终端或 Git 问题,则按需查 Missing Semester。学到一个方法,就拿刚才的统计程序试一次。
第 5 章(下) 接入模型,完成一次对话
你刚刚运行的程序,输出由自己写下的规则直接决定。接下来让程序把一段话交给已经训练好的模型,再把它的回答带回来。几行接口代码背后,是别处完成的训练与推理;你正在学习的是怎样调用、观察和检验这项能力。
前置:已完成5.4 安装、验证与独立环境。接下来先按动手之前:配置模型服务建立
.env并检查是否读入,再运行对话代码。上篇的本地程序不需要 Key;下篇增加模型服务调用。正式 L0 还需要对话应用、Git 仓库和别人能复跑的 README,见本章末尾。
动手之前:配置模型服务
接下来的程序需要一个兼容接口的服务地址、模型名和你获准使用的 API Key。请在服务商官方控制台创建密钥,检查费用与预算设置。没有可用服务时,可以继续本地 CPU 练习;不要使用来路不明或他人的共享密钥。
本书以 DeepSeek 官方文档为配置示例。2026-10-11 核对的地址为 https://api.deepseek.com,示例模型名为 deepseek-flash。模型名和能力会变化,运行前再次核对;这里不承诺价格或免费额度。其他服务请使用其官方地址、对应地域和模型名,不能混搭不同服务的密钥。
建立 .env 与配置模板
在项目目录创建名为 .env 的纯文本文件,与程序放在一起。它没有 .txt 后缀。以下密钥是占位符,运行时替换成自己的值:
AI_API_KEY=替换为自己的密钥
AI_BASE_URL=https://api.deepseek.com
AI_MODEL=deepseek-flash
变量名与程序读取的名字必须一致。.env 不提交到仓库;再建立一份 .env.example,只保留变量名、可公开配置和密钥占位符,供别人照着填写。修改 .gitignore 不会移除已经被 Git 跟踪的文件,提交前仍要检查差异。
核对是否读入
在项目目录运行下面的命令。它只显示读取状态,不显示密钥:
python -c "from dotenv import load_dotenv; import os; load_dotenv(); print('密钥已读取' if os.environ.get('AI_API_KEY') else '没读到')"
“密钥已读取”只代表存在非空值,尚未验证账号权限和服务可用性。如果显示“没读到”,核对当前目录、文件名和变量名;如果出现 ModuleNotFoundError,核对是否使用安装了 python-dotenv 的环境。
load_dotenv() 把文件配置提供给程序读取。后续代码使用 os.environ 取值;缺失必需配置时,应先补配置,不把示例占位符当作真实模型名发送。
第八章还需要嵌入模型。对话模型与嵌入模型用途不同;届时可选择本地嵌入,或按服务商文档配置相应接口,不必现在填入一个猜测的名称。
5.5 第一次调用:让你的程序说第一句话
现在做正经事。下面这段代码,会向模型发一句话,并把它的回答打印出来。
import os
from openai import OpenAI
from dotenv import load_dotenv # ← 装上:pip install openai python-dotenv
load_dotenv() # ← 这一行负责把你的 .env 读进来(漏了会 KeyError)
# 从环境变量读取密钥——不要写死在代码里(原因见 5.6)
client = OpenAI(
api_key=os.environ["AI_API_KEY"],
base_url=os.environ["AI_BASE_URL"], # 漏填直接报 KeyError,好过拿假地址撞服务器
)
resp = client.chat.completions.create(
model=os.environ.get("AI_MODEL", "你的模型名"),
messages=[
{"role": "system", "content": "你是一个简洁的助手,回答不超过两句话。"},
{"role": "user", "content": "用一句话解释什么是向量。"},
],
)
print(resp.choices[0].message.content)
截至 2026-09 有效。
base_url与model的取值以你实际拿到的服务文档为准—— 这两个值是全书里最容易过期的东西,所以我们把它放在这里、标清日期, 而不是写进原理章节。以后你会看到很多这样的标记。⚠️
load_dotenv()那一行不要省。 很多教程的示例代码里没有它, 因为那些例子的密钥是直接写在代码里或已经在系统里设好了。 你用.env存密钥的话,没有这一行,os.environ["AI_API_KEY"]会直接报KeyError。 详细解释见第 13 章 工具箱 13.2.3 环境变量与.env。
把代码存成文件:用上一节装的 VS Code,新建文件、存成 chat.py,
就放在你那个 my-first-ai 目录里。然后在命令行里跑:
python chat.py
提示:跑之前确认虚拟环境是激活的(提示符前面有
(.venv))。 看到ModuleNotFoundError: No module named 'openai'就是没激活,或库没装进去。
跑起来之前,先看懂三件事:
messages是一段对话,不是一个问题。 它是个列表,有角色之分。 你以后写的每一段提示词,本质上都在设计这个列表。system是给他立的规矩,user是你现在说的话。 规矩立得好不好,直接决定他回答的稳定性——这是第 6 章 语言:从预测到对话的全部内容。resp.choices[0].message.content是一层层剥出来的。 不必背,但要意识到:返回的不是一句字符串,而是一个结构。 形如[0]这样的东西叫下标,表示"取第一个"。
试一试:把
user那句话换掉,问点你真的好奇的东西。改一行,跑一次。 这一步确认程序实际使用了预期配置—— 你刚刚第一次让程序按你的意思说话了。
5.6 保护 API Key
这一节不是例行提醒,是最容易出真实损失的地方,所以单列一节。
API Key 有两个特点:
- 它等于钱。 泄露出去,别人调用消耗的是你的额度。
- GitHub 上有人在扫。 自动化脚本全天候扫描公开仓库里的密钥, 从提交到被利用,常常只要几分钟。
所以,三项检查:
| 检查 | 做法 | 反例 |
|---|---|---|
| 不进代码 | 从环境变量读(如 5.5 的写法) | api_key="sk-abc123..." 直接写在 .py 里 |
| 不进仓库 | .env 写进 .gitignore |
把 .env 一起 commit 上去 |
| 泄露立刻换 | 一旦发现传上去了,先去后台吊销,再清理仓库 | 只删文件,以为没事了(历史记录里还在) |
⚠️ 警告:删掉文件不等于删掉记录。 Git 保存的是历史, 你删了文件,之前的版本里密钥依然在。正确的第一动作是去服务商后台吊销那个 Key, 再处理仓库历史。顺序反了,损失已经发生了。
我们会把这一条放进 L0 的关卡否决项:密钥硬编码并提交到公开仓库的,本关卡不通过。 这不是苛刻——这是最低的职业底线,而且是在你还只损失几块钱的时候学会。
现在就把第二条做完(一分钟,别留到下一节——留到下一节就迟了):
echo ".env" >> .gitignore # macOS / Linux
Add-Content .gitignore ".env" # Windows PowerShell
不习惯敲命令也行:用编辑器打开
.gitignore,加一行.env,存盘。 怎么确认做对了:在仓库目录里敲git status——看不到.env就对了; 如果它出现在"未跟踪文件"列表里,说明还没加进去,回去再补一次。
5.7 从脚本到一个能用的东西
到这里你有了一个"跑一次就结束"的脚本。关卡要的不是脚本,是一个你能拿给别人用的东西。
差在哪里?差在循环和上下文——让它能连续对话、记得上一句。
第一步:把它变成能连续对话的
上一节的代码跑一次就退出。 要连续对话,你需要: ① 把历史对话存起来,② 用一个循环反复读你的输入。
下面是完整能跑的版本(把它存成 chat.py,放在 my-first-ai 目录里):
import os
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv() # 读 .env(漏了这句会 KeyError)
client = OpenAI(
api_key=os.environ["AI_API_KEY"],
base_url=os.environ["AI_BASE_URL"], # 漏填直接报 KeyError,好过拿假地址撞服务器
)
# ① 历史:整个对话就存在这个列表里,每轮都带上
messages = [
{"role": "system", "content": "你是一个简洁的助手,回答不超过两句话。"}
]
print("开始对话(输入 exit 退出)")
# ② 循环:反复读输入 → 发给模型 → 把回答也存进历史
while True:
user_input = input("\n你:")
if user_input.strip() in ("exit", "quit", ""):
break
messages.append({"role": "user", "content": user_input})
resp = client.chat.completions.create(
model=os.environ.get("AI_MODEL", "你的模型名"),
messages=messages, # ← 关键:带上全部历史
)
answer = resp.choices[0].message.content
print("它:" + answer)
messages.append({"role": "assistant", "content": answer}) # ← 别忘了把它的回答也存进去
三个地方最容易忘(也是这一节最值得记的):
| 忘掉会怎样 | 为什么 |
|---|---|
忘了 messages.append 存它的回答 |
它不记得自己刚说过什么,对话会前后矛盾 |
忘了每轮把 messages 传进去 |
每轮都是全新对话,"记得上一句"失效 |
忘了 load_dotenv() |
密钥读不到,报 KeyError(见第 13 章 工具箱 13.2.3 环境变量与 .env) |
跑起来(确认虚拟环境已激活):
python chat.py
能来回聊三轮以上,L0 关卡的核心产出物就到手了。
【进阶】:跑几轮之后你会发现,历史越长它越慢、也越贵—— 因为每一轮都把全部历史重新发一遍。这正是第 6 章 语言:从预测到对话 6.5 上下文:留下当前需要的信息「上下文管理」要解决的问题。 先别急,先用起来。
第二步:写一份别人能照着跑的 README
检查 README 是否充分的一个办法,是"换一台电脑、换一个人,能不能跑起来"。 你自己机器上能跑,不算数——因为你机器上恰好装了什么,你是不知道的。
直接抄下面这份,把括号里的换成你的:
# 我的第一个 AI 应用
一个能连续对话的命令行小程序。
## 环境要求
- Python 3.10 或更高(`python --version` 检查)
## 怎么装
1. 建虚拟环境:`python3 -m venv .venv`(Windows:`python -m venv .venv`)
2. 激活:`source .venv/bin/activate`(Windows PowerShell:`.venv\Scripts\Activate.ps1`;
CMD:`.venv\Scripts\activate.bat`)
3. 装依赖:`python -m pip install openai python-dotenv`
## 怎么配
1. 新建一个文件,名字就叫 `.env`(照 `.env.example` 里的键名写)
2. 把这几项填上你自己的值:
- `AI_API_KEY=`(你的密钥)
- `AI_BASE_URL=`(按服务商官方文档填写,是否含 /v1 由服务商规定)
- `AI_MODEL=`(模型名,去服务商文档看)
## 怎么跑
`python chat.py`,然后按提示输入。输入 `exit` 退出。
## 已知问题
- 历史越长响应越慢(还没做上下文裁剪)
为什么每一项都不能省——逐条对着看:
| 这一项 | 省了会怎样 |
|---|---|
| 环境要求 | 别人用 Python 3.8,装到一半失败,不知道是版本问题 |
| 激活命令 | 别人忘了激活,库装到全局,报"找不到模块" |
.env.example 那一步 |
别人不知道要配什么,或者直接把密钥写进代码 |
| 模型名去哪看 | 别人卡在"我的模型名是什么" |
| 已知问题 | 别人以为是自己机器的问题 |
注意最后一条:把"已知问题"写出来不是自曝其短,是帮别人省时间, 也是第 9 章 Agent:让行动可控、结果可验说明已知限制的做法在你第一个作品上的应用。
第三步:把仓库交出去
L0 关卡的验收人要拿到你的仓库。前面已经初始化 Git 并练习过一次本地提交,现在把对话程序保存为一次提交,再上传。以下命令都在 my-first-ai 目录执行。
先运行 git status。再运行 git check-ignore .env .venv/,核对已有的密钥文件与环境目录被忽略;不要显示 .env 内容。若 .env 已被跟踪,先按工具箱处理,确认不再进入提交后再继续。
先核对当前仓库的署名,避免换了仓库或电脑后遗漏配置:
git config --get user.name
git config --get user.email
两项都应是自己希望写入提交记录的值;为空、仍是占位文字或属于他人时,先按首次提交前:设置并核对 Git 署名设置再继续。
只暂存这次需要交付的文件。暂存是选出下一次提交要保存的内容,还没有上传:
git add .gitignore chat.py .env.example README.md
git diff --cached --name-only
git diff --cached
核对文件清单与内容:没有 .env、密钥、私人材料或 .venv。确认后提交并检查:
git commit -m "完成第一个对话程序"
git log -1 --oneline
git status
成功时会看到一条带短编号的提交记录;git status 可能仍列出未提交的练习文件,不影响这四个交付文件已经保存。以后修改文件要再次暂存、提交。步骤对应 Git 的暂存说明(核对:2026-10-13)。
用 GitHub 托管仓库:
- 去
github.com注册账号; - 新建一个空仓库(New repository),先选 Private(私有),不要勾选自动添加 README、许可或
.gitignore,避免与本地已有文件产生两段独立历史;确认内容可公开后再决定是否改变可见性。私有仓库需要另给验收人读取权限; - 按它页面上给的命令,把本地仓库连上去并推送:
尖括号
<...>里的是占位符——要换成你自己的东西再敲,别原样粘。 (下面这行的<你的仓库地址>换成第 2 步页面上给你的那串地址。)
git remote add origin <你的仓库地址>
git branch -M main
git push -u origin main
第一次推送可能需要登录。GitHub 的 HTTPS Git 操作不接受账号密码;按官方流程使用凭据管理器或适当权限的访问令牌。令牌同样是密钥,不写进仓库地址、不发给别人。认证入口与权限设置见 GitHub 官方认证说明(核对:2026-10-13)。无法完成认证时,把报错脱敏后请助教协助;不要反复尝试账号密码。
推送后打开仓库页面,确认四个交付文件及最新提交能看到;确认
.env不在文件清单中。请验收人确认其账号有权读取。⚠️ 推之前再做一次第 5.6 节那个检查: 仓库里有没有
.env?.gitignore里写了它吗? 公开仓库里的密钥可能被他人获取;提交前检查,发现泄露后立即吊销或轮换。提示:不懂"私有/公开"就先用私有——它不影响验收人看你(你把他加为协作者就行), 同时继续检查协作者权限与提交内容;私有仓库也不应用来保存真实密钥。
这一节的三个最小要求(自查)
- [ ] 能连续对话(不是跑一次就结束)——
python chat.py能来回聊三轮以上 - [ ] 有 README,写清怎么装、怎么配、怎么跑
- [ ] 别人照着 README 能跑起来——最省事的验证:发给一个同学,什么都不说
5.8 练习与提交
概念复习题
- 虚拟环境解决什么问题?它与保存代码的 Git 仓库有什么不同?
- 为什么
.env.example应保留变量名,却不能包含真实密钥? - 程序能在自己电脑运行,README 还需要写什么,别人才能复跑?
参考答案与思路见附录 E 练习参考思路。
参加部门培养:本章正式关卡(L0)
动手关卡(L0 · 对应 Gate)
这一关没有标准答案,但有明确的验收标准。判定依据见附录 A 关卡与验收标准, 你需要交出的东西是:
| 交什么 | 要求 |
|---|---|
| 一个对话应用 | 能连续对话(不是跑一次就结束) |
| 一个 Git 仓库 | 含 .gitignore,且历史记录里没有密钥 |
| 一份 README | 写清楚:怎么装、怎么配、怎么跑 |
| 一次口头解释 | 能说清你的代码在做什么,为什么这么写 |
验收方式:验收人会只凭你的 README,在他自己的机器上跑一遍,全程不问你。 跑通之后,他会问你几个"为什么"。
不通过的情形(一票否决):
- 只有截图,拿不出能跑的东西;
- 密钥硬编码并提交到了公开仓库;
- 代码能跑,但你无法解释它在做什么。
提示:过不了很正常,不通过会明确告诉你差在哪一项,改完可以重新交,次数不限。 这个体系里没有"过期作废"这回事,只有"还没做到"。
第 6 章 语言:从预测到对话
语言的入口:一句话怎样接着一句话
读下面这段虚构通知:
读书会原定周五14点在A教室举行,现改为周六,地点不变。报名仍在周四截止。
活动是哪天?报名是哪天?你大概不难回答。但请看自己刚才做了什么:“现改为”覆盖了前面的日期,“地点不变”把后一句接回前一句,“仍”又说明报名日期没有跟着改变。读懂一句话,需要让分散的词彼此发生关系。
若只数“周五”“周六”“周四”各出现多少次,三个数相同,关系却完全不同。第一章把图像表示成像素;到了语言,我们同样要问:怎样把文字变成计算能使用的表示,同时保留顺序与联系?
先把文字变成可以预测的片段
语言模型通常用分词器将文本变成一串编号,每个编号对应一个token。它可能是一个字、一个词的一部分或其他片段,具体怎样切由分词器决定,不能简单地把“一个token”等同于“一个汉字”。
编号再变成向量。最初的向量与后续各层计算,让每个位置能够带上来自上下文的信息。常见的自回归语言模型训练任务是:给定前面的token,提高训练文本中下一个token的预测概率。
用第二章的眼光看,这仍是一个带参数的函数:输入是编码后的上下文,输出是一组候选token的分数或概率。改变上下文是在改变输入;训练更新权重是在改变函数。随后怎样从概率中选择一个token,是生成过程的另一环。
例如“下雨了,出门记得带……”后面有许多可能。“伞”在这样的语境中合理,“望远镜”也并非语法上绝对不可能,却需要不同背景。模型输出一组候选的分数,经转换得到概率分布,再按生成规则选择后续token;把新token接到前面,继续下一步,直到结束或达到限制。
这是一种训练目标与生成过程的说明,不是“模型只记着一张接龙表”。为了在广泛文本中做好预测,模型可以学到关于词语、语法和其他规律的复杂表示。它究竟学到了哪些可迁移能力,需要拿新任务检验;训练目标本身不会替我们回答“它是否像人一样理解”。
让一句话中的位置相互参照
注意力机制提供了一种计算联系的办法:当前某个位置根据自身与其他位置的表示,算出一组权重,再按权重汇合其他位置带来的信息。不同层、不同注意力头可以形成不同的关系;模型同时还需要位置等信息,才能区分顺序。
可以用上面的通知练一个阅读问题:理解“地点不变”时,人需要回到“A教室”;理解活动时间时,需要注意修改关系。这个例子帮助我们感受“参照上下文”为什么必要,但不能据此断言某个注意力头就负责“理解不变”。注意力权重也不是模型全部推理过程的说明书。
2017年的Transformer论文在机器翻译任务中组织了这类计算。后来,以Transformer为基础的语言模型经过大规模预训练,能够由同一个模型处理多种文本任务;指令训练等后续训练又帮助它适应请求与对话。原论文中的模型和你今天调用的聊天系统并不完全相同。Transformer原论文、GPT-3研究、指令与人类反馈训练研究提供了这几步的资料入口(核对:2026-10-13)。
为什么它会把没有的事说得很顺
继续问那份通知:“活动由谁主持?”原文没有提供主持人。一个合适的回答应当指出缺失;生成过程中却也可能出现一个看起来符合场景的姓名。
原因不能一概归为某一个参数,但我们已经能辨认其中的张力:生成在上下文中合适的文字,与确认一个事实有可靠依据,不是同一道检验。预训练、后续训练、提供资料和输出验证都可能影响结果。
当文字成为接口,我们能够用一句请求调动已有模型,不必为每种表达另写一套程序。这扩大了人能够尝试的事情,也使“请求究竟是什么、依据从哪里来”变得格外重要。接下来,我们就用同样的眼光写提示词。
6.1 提示词是任务说明
把刚才那份通知交给模型,说“总结一下”。它可能留下活动时间,也可能着重说明地点;还可能省去你最在意的报名截止日期。
这个回答未必违反了原请求,因为“总结”容许许多取舍。若我们要为同学生成一张参加活动的便签,就应明确要保留活动时间、地点和报名截止日期,并说明缺失信息怎样处理。
给模型的任务说明与材料,通常统称为提示词。它改变模型这次可以利用的信息与任务条件;在这里的接口调用中,修改提示词并不重新训练模型参数。
可以先把它当成一份可检查的任务约定:输入是什么,输出用于什么,哪些内容必须有依据,哪些格式供后续程序读取。说清楚这些,也让我们更容易判断一次回答究竟错在哪里。
6.2 写清任务、输入与约束
下面换成一条虚构的物品遗失描述。我们想提取时间、地点、事件类型和涉及人数。若只说“帮我总结”,模型无法知道我们需要这些字段,也不知道原文没提到的内容应当留空。
本章用四个位置组织要求:
| 要素 | 需要说明什么 | 本例 |
|---|---|---|
| 角色 | 处理材料的职责 | 虚构警情文本结构化助手 |
| 任务 | 要从输入中得到什么 | 提取四个约定字段 |
| 约束 | 什么信息允许使用 | 只用原文;缺失值不猜测 |
| 输出格式 | 后续怎样读取 | 输出JSON对象 |
这是一种便于练习的写法,不代表所有请求都必须包含相同的四段。角色名称也不会使模型自动获得专业资格。将这些要求放在本例的system消息中,用户描述放在user消息中;接口和模型怎样支持不同消息角色,以所用服务的说明为准。
一个完整的例子
先认识 JSON:它是一种让程序读取数据的文本格式。下面是合法的 JSON 对象,双引号里是键名;值可以是字符串、数字或 null(未提供)。本例约定 persons 表示描述中提到的人数。
{"time": null, "location": "教学楼一层", "type": "物品遗失", "persons": 1}
字段不能省略,不确定的信息填 null。中文标点、尾部多余逗号以及写成 Python 的 None 都不符合这个 JSON 例子。下面是放进 Python 文件的提示词变量,其中 string | null 和 number | null 是字段类型说明,不是让模型原样输出的值。
SYSTEM_PROMPT = """你是一个警情文本结构化助手。
## 任务
把用户给出的一段警情描述,抽取成结构化字段。
## 约束
1. 只抽取原文出现过的信息,不要推测、不要补全
2. 原文没提到的字段,填 null
3. 涉及人员数量写阿拉伯数字
4. 不要输出任何解释、说明或客套话
## 输出格式
只输出一个 JSON 对象。不要用 markdown 代码块包裹。
{
"time": "string | null",
"location": "string | null",
"type": "string | null",
"persons": number | null
}
"""
这段约定补充了字段与缺失值的处理方式;仍然可能有歧义,例如“人数”究竟统计报告人、失主,还是描述中所有人。先明确本任务统计描述中涉及的人数,再用边界例子检查。
试一试:取一项你最近想让模型完成的任务,补上原先没有说明的输入范围和输出用途。再问自己:另一位同学只看这段说明,能否判断一份结果合不合要求?
把提示词接到一个独立的输入
为了避免上一条咨询影响下一条,从第五章的 chat.py 复制一份,命名为 extract.py,保留导入、load_dotenv() 和 client 的配置。把上面的 SYSTEM_PROMPT 放在 client 定义之后;把 client 定义之后的原聊天代码全部替换为 SYSTEM_PROMPT 和下面这段,不保留旧的欢迎语、messages 或 while 循环。这里是接入片段,不是一个独立文件。
text = input("输入一条虚构描述:")
resp = client.chat.completions.create(
model=os.environ["AI_MODEL"],
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": text},
],
)
print(resp.choices[0].message.content)
运行 python extract.py,粘贴一条描述并回车。程序只请求一次,输出后退出;测试下一条时重新运行,因此不携带上一条会话。每次请求可能产生费用,先用下面的三条虚构资料练习,别把真实案件资料发给模型。第6.3节的解析片段可以接在这段最后一行之后,它读取的正是这里的 resp。
顺便说清一个参数:温度
模型生成时通常会为候选token给出分布。温度是部分服务提供的采样参数:较低温度通常使分布更集中,较高温度通常增加多样性。它不补充资料,也不核实事实。
抽取字段时,可以比较低温度与原配置的稳定性;产生候选名称时,可以比较更丰富的候选是否有用。温度低不保证答案正确或完全重复,温度高也不直接等于错误。是否支持这一参数、可用范围与默认值,按具体模型文档核对。
改过参数后,用同一组开发用例重测,保存配置与原始输出。否则,你很难知道变化来自要求变清楚了,还是这次恰好采到了另一个回答。
6.3 输出格式与程序校验
人看到“时间未知”,可以理解它在说什么;程序则需要明确的字段、类型和缺失值约定。上一节要求JSON,是为了让下一步能够读取结果。
先把检查分成三层:文本能否解析为JSON,字段是否满足约定,字段里的事实是否有原文支持。第一层成功,不会自动证明后两层成功。一个格式完美的虚构姓名,仍然是错误。
当返回格式偏离约定
即便你明确要求"只输出 JSON,不要用代码块包裹",模型有时还是会给你:
```json
{"time": "2026-03-05 21:30", "location": "中心广场", ...}
```
多了三行围栏,json.loads() 直接报错。解析器按照格式工作,所以需要显式处理这种返回。
工程上的处理是两手都做:
这是接在第6.2节
extract.py调用之后的片段:resp就是client.chat.completions.create(...)返回的对象, 单独把这段复制出去会NameError。这里要看的是处理思路。
import json
import re # 第二层兜底要用
raw = resp.choices[0].message.content
try:
data = json.loads(raw)
except json.JSONDecodeError:
# 常见原因一:模型仍用 ```json ... ``` 包裹
cleaned = raw.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
try:
data = json.loads(cleaned)
except json.JSONDecodeError:
# 常见原因二:前面还带一句客套话("好的,以下是……")
# 兜底:把它输出里的第一对花括号整个抠出来
m = re.search(r"\{.*\}", cleaned, re.S)
if not m:
raise
data = json.loads(m.group(0))
- 提示词与服务支持的格式约束用于表达要求。
- 程序检查实际返回值,处理仍可能出现的偏差。
上面那段兜底也只挡得住两种最常见的情况(围栏、客套话)——它只尝试恢复这两种格式;仍有可能解析失败,不能保证程序不报错。 提示词约定不能替代程序校验;解析失败时应返回明确错误,或在次数受限的条件下重新请求。
还有更进一步的做法:很多服务支持「结构化输出」或「函数调用」, 按接口支持的约束生成格式;仍需检查拒绝、截断和业务字段。这是第 9 章 Agent:让行动可控、结果可验的内容,现在知道有这么回事就行。
6.4 两种值得比较的信息组织方式
要求写清以后,还可以改变提供信息的方式:让结果包含能够核对的中间字段,或给出一些输入与输出的例子。是否有帮助,用同一任务上的比较判断。
6.4.1 把复杂任务拆成可核对的步骤
对需要多条证据的任务,可以要求模型先提取有关字段,再给结论与简短依据。例如从一段虚构通知中提取活动日期、报名截止日期和各自原句,再判断询问的是哪一种日期。
这让我们能够检查中间产物,而不是只看最后一句话。模型生成的解释仍可能不准确,不能把它当作内部计算过程的忠实记录。是否改善结果,需要和直接回答在同一评测集上比较;一些推理模型有自己的推理机制,不必一律要求输出长篇思考。
对于简单格式转换,额外步骤可能只增加长度和延迟。保留那些能帮助验证的字段即可。
6.4.2 Few-shot:给例子,而不是给描述
有些要求很难用语言描述清楚。比如"什么叫正式但不生硬的语气"—— 你说一百句形容词,不如给两个例子。
【例 1】
输入:这个事儿我们搞不定,你找别人吧。
输出:该事项超出我方职责范围,建议转由相关部门处理。
【例 2】
输入:赶紧的,明天之前必须交。
输出:请于明日之前提交,感谢配合。
现在按同样风格处理下面的输入:
输入:{用户输入}
这叫 Few-shot(少样本):给几个输入-输出样例,让它照着做。
适用条件:你的要求难以描述、但容易举例。典型场景:语气风格、格式转换、 分类判断的边界。
代价:例子会占用上下文(下一节讲),例子多了成本和延迟都上升。 可以从少量有代表性的例子开始,比较加入或移除某个例子的影响。数量本身不保证效果。
6.5 上下文:留下当前需要的信息
回到第五章的连续对话程序:第二轮请求把前面的消息也发出,所以模型能利用先前的内容。若程序没有发送那段历史,就不能仅凭界面上还显示着它,假定模型已经看见。
模型一次能够处理的token数量有限,这个范围叫上下文窗口,输入与输出的限制依模型而异。更长的材料还会带来信息选择、延迟和费用问题。
| 遇到的情况 | 先观察什么 | 可以比较的办法 |
|---|---|---|
| 请求超过长度限制 | 实际发送了哪些消息 | 分段处理、摘要或选择有关片段 |
| 关键条件被忽略 | 条件是否完整到达,是否与别处冲突 | 调整组织和位置,用同一用例重测 |
| 多轮成本不断增加 | 历史内容与实际token用量 | 保留必要状态,压缩已完成部分 |
压缩也会丢信息。摘要若删去了“报名仍在周四截止”,后面再追问也无法靠强调语气补回来。请保留支持判断的原文,并检查摘要是否改变条件。
组织这些信息有时称为“上下文工程”。第八章将处理材料很多、会更新的情形:先检索与问题有关的内容,再把它连同出处交给模型。
6.6 比较两个提示词
假如加上“缺失字段填null”以后,模型不再猜日期,这是一条有用的观察。下一步换一条缺地点的描述,或一条同时提到两个时间的描述,检查改善是否仍然存在。
三条成功之后,还要看什么
少量例子能帮我们确认程序走通、发现明显错误。若想声称方案适用于某类输入,还需说明这些例子从哪里来、覆盖哪些情况,以及有没有参与提示词的修改。
保存A、B两版提示词,让它们处理同一批输入,逐条记录字段是否有依据、是否遗漏、是否符合格式。复杂的提示词也可能遗漏更多条件;比较的意义,就是允许结果改变我们的预期。
参加正式L1验收时,还需至少十条符合独立性要求的测试用例与A/B记录,详见章末及附录A。下面先练如何形成这样的证据。
独立测试与开发用例
根据需求编写用例,能够检查某条规则是否执行。例如专门测试“日期缺失时返回 null”,是有用的功能检查。但这些题目如果始终围绕已经知道的写法,不能单独证明对新输入有效。
最终评价需要未参与提示词选择、具有独立来源的输入。教学中可以请另一位同学按任务约定编写并保留测试资料,也可以使用获准的公开资料;不要为了“真实”而发送未公开材料。记录来源、改写关系及哪些用例已经看过。
现行 L1 将“用例与 prompt 同源”列为否决项,提交时应给出来源与独立性证据。开发时的功能测试仍然保留,与最终验收测试分开报告。
先练三条,再组织正式评测
先用以下开发用例熟悉记录方法。它们与本书提示词一起编写,不能充当正式 L1 的独立测试证据。对每条输入,分别用 A(例如“抽取时间、地点、类型、人数,输出 JSON”)和 B(第6.2节完整提示词)各运行一次;保存两版提示词、模型名和原始输出。
| 虚构输入 | 先写下的检查点 | A 原始输出与判定 | B 原始输出与判定 |
|---|---|---|---|
| 一名同学在教学楼一层报告水杯遗失,时间未说明。 | time 为 null;地点有依据;人数为1 | 运行后填写 | 运行后填写 |
| 有人报告物品遗失,时间、地点及涉及人数均未说明。 | time、location、persons 均为 null,不补信息 | 运行后填写 | 运行后填写 |
| 2026年10月1日14时,两名同学在操场报告背包遗失。 | 时间地点均来自原文;人数为2 | 运行后填写 | 运行后填写 |
先检查能否解析为 JSON,再查四个字段是否齐全、类型是否符合约定、值是否由原文支持。某字段不对就记录具体错误,不用“感觉差不多”代替判定。本轮可以约定所有检查均满足才记该条通过,分别报告通过数/总数及错误类型。
三条都能记录后,再按任务收集或请他人保留独立测试资料,组织至少十条正式用例。开发时看过并据此修改提示词的资料,继续作为开发用例,不改名冒充未见测试。
A/B 对比怎么做(第 3 章方法的直接应用)
第 1 步 建 10 条以上用例(含难例,来自真实输入)
第 2 步 写两个版本的提示词(A:朴素版 B:结构化版)
第 3 步 两个版本跑同一套用例
第 4 步 逐条记录:对 / 错 / 半对
第 5 步 算出得分,写下结论:哪版更好、好在哪一类输入上
第 5 步是重点:不只是"B 比 A 好",而是"B 在字段缺失类输入上明显更好, 但在口语化输入上没差别"。这种结论才是能指导下一步的信息。
6.7 当对面是个智能体:任务级提示词
前面主要讨论一次调用怎样得到一个结果。如果助手还能读取文件、运行程序,就需要把任务说明扩展到行动范围:它可以接触哪些资料,哪些修改已获允许,怎样验证完成。
这类系统常称为智能体,第九章会拆开它的循环。现在先从使用者的角度,写一份能让行动有依据的请求。普通对话工具未必具有这些能力,先看实际提供的功能。
把模糊请求改成可检查的任务
“帮我看看这个文件”可以指阅读、检查,也可以被理解成修改。若对象能行动,这些理解的差异会落实到文件变化上。
例如,你希望分类脚本遇到空文件时给出明确提示。请求里可以写清目标、可修改的文件、需要保留的接口,以及用什么输入检查。这样既留下解决问题的空间,也为复核行动提供依据。
用约束减少歧义
“帮我看看这个文件”没有说明要检查什么。“检查这个脚本遇到空输入时是否报错;保留接口和目录结构;先给复现步骤,再提出修改”则明确了目标、边界与交付物。
这里讨论的是任务描述的可执行性,不是在计算香农信息量。没有必要用字数衡量提示词好坏;有用的细节应帮助助手确定下一步行动,并让你能够判断结果是否合格。
一句话里该装进四样东西
第 5 章(上) 准备实验环境给过最小可用版本,这里说清为什么是这四样:
| 装什么 | 不装会怎样 |
|---|---|
| 目标——要什么结果(不是要什么动作) | 它去执行你字面说的动作,但那不是你真正要的 |
| 约束——不许做什么、边界在哪 | 它顺手改了你没让它动的东西 |
| 完成标准——怎么算做完 | 它做一半就停,并宣称完成了 |
| 可用资源——能访问什么、什么环境 | 它去猜、或者卡在权限上 |
注意第一行:说"帮我改一下这个函数"是动作,说"我要让这个函数在输入为空时不崩" 才是结果。说明结果能帮助它选择办法;若某些步骤本来就有必须遵循的规程,也应一并写清。
语义混淆:三种来源
这是信息密度之外的另一类问题,更隐蔽:
| 来源 | 例子 | 它会怎么处理 |
|---|---|---|
| 模糊指代 | "那个文件"、"上次说的那个" | 可能猜错所指对象;需要明确名称或先澄清 |
| 一词多义 | "优化一下" = 更快?更准?更便宜? | 可能按与任务不同的目标行动 |
| 情态混淆 | "你可以考虑…"、"建议…" | 可能无法确定是否必须执行 |
第三条最值得记:写给模型时"建议"是软要求;写给智能体时,它可能真的当没看见。 请将必须满足的约束与可参考的建议分开,方便检查执行结果。
要它做,就用要它做的句式:"必须"、"不要"、"做完之后要…"。
任务粒度:拆还是不拆
这是最容易走极端的两个方向:
| 做法 | 后果 |
|---|---|
| 你替它把每一步都规定好 | 它就只是个执行器——流程稳定时这可能正合适;遇到未知情况时则需要另设分支 |
| 只说一句"帮我搞定" | 它自由发挥,走向你没预料的地方 |
建议的判断标准:
结果你能说清、路径你说不清 → 让它自己规划(把目标、约束、完成标准写足); 路径你很清楚、而且必须那样走 → 你就明说步骤(比如有硬性规程的时候)。
它跑偏了,是纠偏,不是重开
用模型时,答错了你通常改 prompt 重来。 用智能体时,重开的代价大得多(它可能已经做了十步)。
更好的做法是在过程中把它拉回来:
"停一下。你现在在改第 3 个文件,但我只让你改第 1 个。
请先说明你为什么动它,然后回到原来的范围。"
要点:说清"它做了什么、你要什么",而不是只说"不对"。 "不对"对它没有信息量,它会继续猜。
多轮之后,它开始忘事
对话长了、做了很多步之后,它可能不再遵循早先的约束—— 这可能与上下文过长、截断或关键信息未被使用有关,需要检查实际请求(6.5 上下文:留下当前需要的信息)。
实用做法:在关键节点重新交代一次约束,别指望它一直记得。 说得直白点:重要的约束,值得多说几遍。
这一节和前后章的关系
| 章 | 讲什么 |
|---|---|
| 本章(ch06) | 给模型写规格 + 给智能体下任务 |
| 第 9 章 Agent:让行动可控、结果可验(L4) | 它内部到底怎么转(智能体循环)、怎么自己造一个 |
这一节只是让你用得更好。至于它为什么这样工作、以及怎么造一个, 相关实现见第 9 章 Agent:让行动可控、结果可验,可以按当前问题往返查阅。
6.8 练习与提交
概念复习题
- 为什么格式正确的 JSON 仍可能不符合任务要求?
- 提示词开发用例与最终测试用例为什么要分开?
- 给一个含目标、材料、边界和验收方式的任务请求。
参考答案与思路见附录 E 练习参考思路。
参加部门培养:本章正式关卡(L1)
动手关卡(L1 · 对应 Gate)
选一个能说明用途、输入范围和判定方式的任务;可以来自校园生活,也可以从教学练习中提出自己的问题。涉及敏感业务时用虚构材料替代,资料来源与授权另记。然后交出四样东西:
| 交什么 | 要求 |
|---|---|
| 一个结构化提示词 | 含角色 / 任务 / 约束 / 输出格式四要素 |
| ≥10 条测试用例 | 来自真实输入,至少 3 条是"你知道它可能出错"的难例 |
| 至少一次 A/B 对比 | 朴素版 vs 结构化版,跑同一套用例,逐条记录 |
| 结论 | 不只是"A 比 B 好",要说清好在哪一类输入上 |
验收方式:验收人会拿你没见过的输入去测你的提示词, 结果要和你评测记录里写的一致(详见附录 A 关卡与验收标准)。
不通过的情形:
- 只有提示词,没有评测;
- 把与提示词共同编写的开发用例,当作独立测试证据;
- 改了提示词却没有重测。
提示:如果你的任务是"要被程序接的",请同时交上容错代码。 那不是 L1 的硬性要求,但它是你走向 L3/L4 时一定会用到的习惯。
第 7 章 让一个实验成为自己的作品
第一章里,你认出了纸上的字,机器只拿到一组数。现在我们准备训练一个真的分类器。随后把输入换成文字,看看同一类学习方法是否仍然占优。
本章有两个普通CPU实验,以及一个调用已有语言模型的应用。先选一项开始即可:前两项在工具安装后不调用远端模型,第三项需要第五章的服务配置。完整代码可以展开复制,运行以后请先看具体错例,再回头读报告程序。
7.1 把数字交给模型
数字0到9是十个类别。模型从训练图像与标签中学习,再处理保留的新图。如果训练集中某个数字最多,始终猜它也会答对一部分;这个简单办法构成一条基线。我们要看学习之后增加了什么。
一张实验记录要回答什么
第3章已经区分训练、开发、测试与压力集。这里把它们放进同一轮实验:
| 动作 | 本次怎样做 | 为什么这样做 |
|---|---|---|
| 写清任务 | 输入数字图像或短文本,输出一个类别 | 避免一边实验一边改变“算对”的定义 |
| 固定拆分 | 训练集学参数,开发集选方案,测试集最后评价;压力例另列 | 不让最终答案参与方案选择 |
| 留简单基线 | 图像总猜训练中最多的一类;文本按关键词判断 | 有可解释的比较对象 |
| 只改一个因素 | 两个候选只改变同一参数C | 结果变化才有较清楚的归因 |
| 记录分母与错例 | 保存每条预测、类别、样本数和错误 | 分数相同可能对应不同失败方式 |
| 让别人复跑 | 保留代码、数据、拆分、工具版本与选择理由 | 别人能核对结论从哪里来 |
先用最直观的准确率:正确预测数除以总数。数字实验若360张对了348张,就是348/360,约96.7%。再看每个类别的漏报与误报:精确率(precision)问“被预测成这一类的样本中,有多少是真的”;召回率(recall)问“实际属于这一类的样本中,找回了多少”。把两者合成的分数叫F1分数(F1 score);逐类计算F1后不加权平均,得到宏平均F1(macro-F1)。本例对未能计算的类别F1记0。它让小类别也有一份权重,但仍不能代替错误代价分析。
这些程序保存两种候选的开发集结果,选好后分别让基线和选中的模型各预测一次测试集。不要把“测试只在最后做”理解成一生只能运行一次程序;按原样复跑可以核对结果。问题在于:看到测试结果后据此改方案,再把同一个测试集的高分说成独立验证。如果测试已经影响了下一版选择,就要披露,并准备新的独立测试资料。
图像实验:64个数怎样变成一个数字
先看一张8×8灰度图。每个像素的亮度是一个数,把64个数排成一行,就得到模型能读取的特征向量(feature vector)。这是一个很小的数字识别任务;它不负责定位文字,也不处理真实照片里的光线、旋转与遮挡。
准备步骤见CPU模型实验的工具包。将下面完整代码保存成 cv_digits_lab.py,在激活的实验环境中运行:
python cv_digits_lab.py
展开完整图像实验代码(cv_digits_lab.py)
"""L2 可选准备练习:CPU 手写数字分类;不用模型 Key。
运行:python cv_digits_lab.py [输出目录]
先比较开发集,固定方案后才进入一次最终测试阶段;不据测试结果再调参。
数据:Alpaydin, E. & Kaynak, C. (1998), UCI, DOI 10.24432/C50P49,
CC BY 4.0;sklearn 的副本是原数据的测试部分。本练习重新划分,非官方榜单。
"""
import hashlib
import html
import json
import platform
import sys
import time
from pathlib import Path
import numpy as np
import scipy
import sklearn
from sklearn.datasets import load_digits
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, f1_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
SEED = 42
CS = (0.25, 2.0) # 只改变 C;其他条件相同。相同分数时保留先列的较小 C。
def digest(value):
raw = json.dumps(value, ensure_ascii=False, sort_keys=True).encode("utf-8")
return hashlib.sha256(raw).hexdigest()
def evaluate(model, indices, X, y):
predictions = model.predict(X[indices])
rows = [{"index": int(i), "gold": int(gold), "predicted": int(pred)}
for i, gold, pred in zip(indices, y[indices], predictions)]
return {"n": len(rows), "accuracy": float(accuracy_score(y[indices], predictions)),
"macro_f1": float(f1_score(y[indices], predictions, labels=list(range(10)),
average="macro", zero_division=0)),
"confusion_labels": list(range(10)),
"confusion_matrix": confusion_matrix(y[indices], predictions,
labels=list(range(10))).tolist(),
"predictions": rows, "errors": [r for r in rows if r["gold"] != r["predicted"]]}
def write_errors(path, errors, images):
# SVG 直接写像素方块,不需要绘图库;最多显示 12 张,JSON 保留全部错例。
shown = errors[:12]
height = 90 + max(1, (len(shown) + 3) // 4) * 125
parts = [f'<svg xmlns="http://www.w3.org/2000/svg" width="720" height="{height}" '
f'viewBox="0 0 720 {height}" role="img" aria-labelledby="title">',
'<title id="title">最终测试错误图册(最多十二张)</title>',
'<rect width="100%" height="100%" fill="#f5f1e8"/>',
'<text x="20" y="28" font-size="18">Final test errors: '
f'{len(errors)} (showing {len(shown)})</text>']
if not shown:
parts.append('<text x="20" y="60">No errors in this small split; no general guarantee.</text>')
for n, row in enumerate(shown):
x0, y0 = 20 + (n % 4) * 175, 60 + (n // 4) * 125
for r in range(8):
for c in range(8):
shade = round(float(images[row["index"], r, c]) * 255 / 16)
parts.append(f'<rect x="{x0+c*10}" y="{y0+r*10}" width="10" '
f'height="10" fill="rgb({shade},{shade},{shade})"/>')
label = f'#{row["index"]} true={row["gold"]} pred={row["predicted"]}'
parts.append(f'<text x="{x0}" y="{y0+100}" font-size="12">{html.escape(label)}</text>')
path.write_text("\n".join(parts + ["</svg>"]), encoding="utf-8")
def main():
started = time.perf_counter()
output = Path(sys.argv[1] if len(sys.argv) > 1 else "cv-output")
output.mkdir(parents=True, exist_ok=True)
data = load_digits()
X, y = data.data, data.target
indices = np.arange(len(y))
remaining, test = train_test_split(indices, test_size=0.2, stratify=y, random_state=SEED)
train, dev = train_test_split(remaining, test_size=0.25, stratify=y[remaining], random_state=SEED)
splits = {name: ids.tolist() for name, ids in (("train", train), ("dev", dev), ("test", test))}
assert not (set(train) & set(dev) or set(train) & set(test) or set(dev) & set(test))
assert len(train) + len(dev) + len(test) == len(y)
baseline = DummyClassifier(strategy="most_frequent").fit(X[train], y[train])
trials, models = [], []
for C in CS:
# StandardScaler 在 pipeline.fit 中只见训练集;dev/test 只 transform。
model = make_pipeline(StandardScaler(), LogisticRegression(C=C, max_iter=1000,
random_state=SEED))
model.fit(X[train], y[train])
trials.append({"C": C, "dev": evaluate(model, dev, X, y)})
models.append(model)
best = max(range(len(trials)), key=lambda n: trials[n]["dev"]["macro_f1"])
selected_C = trials[best]["C"]
# 方案在此锁定。两种最终方案各预测 test 一次;没有反复挑测试最高的模型。
final = {"baseline": evaluate(baseline, test, X, y),
"selected": evaluate(models[best], test, X, y)}
report = {"seed": SEED, "source": {
"credit": "Alpaydin, E. & Kaynak, C. (1998), Optical Recognition of Handwritten Digits",
"doi": "https://doi.org/10.24432/C50P49", "license": "CC BY 4.0",
"url": "https://archive.ics.uci.edu/dataset/80/optical%2Brecognition%2Bof%2Bhandwritten%2Bdigits",
"note": "sklearn copy of original test subset; our new split is a teaching split"},
"versions": {"python": platform.python_version(), "numpy": np.__version__,
"scipy": scipy.__version__, "sklearn": sklearn.__version__},
"data_sha256": digest({"pixels": X.astype(int).tolist(), "labels": y.tolist()}),
"code_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"split_indices": splits, "split_sha256": digest(splits),
"baseline_dev": evaluate(baseline, dev, X, y), "candidates": trials,
"selection": {"criterion": "dev macro-F1; tie -> first C", "C": selected_C,
"refit_train_plus_dev": False},
"final_test": final, "elapsed_seconds": time.perf_counter() - started,
"limits": ["No writer ID: this does not establish cross-writer generalization.",
"8x8 digits do not establish OCR or real scene performance.",
"Public teaching holdout; do not tune after viewing test outcomes."]}
(output / "report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
write_errors(output / "errors.svg", final["selected"]["errors"], data.images)
print(f"train/dev/test = {len(train)}/{len(dev)}/{len(test)}; selected C = {selected_C}")
for name, result in final.items():
print(f"{name}: test n={result['n']}, accuracy={result['accuracy']:.3f}, macro-F1={result['macro_f1']:.3f}")
print(f"输出:{output / 'report.json'};{output / 'errors.svg'}")
if __name__ == "__main__":
main()
先运行,再找五处:load_digits() 取数据;train_test_split() 分集;model.fit() 从训练资料学参数;开发集结果决定 best;final 才做最终测试。报告和绘图函数可以稍后逐行读。
训练前,StandardScaler 按训练集学到的均值和尺度调整特征;开发、测试只使用这些已固定的变换。把预处理和分类器接成一条流水线(pipeline),可以减少误把开发或测试资料用于拟合的机会。这里的逻辑回归(logistic regression)虽然名字带“回归”,用于分类。参数C调节正则化强度:较小的C对应较强的限制,不代表“模型更小”;本例只在开发集比较两个C,不需要现在推导它的数学公式。
本次复跑的输出如下。工具环境记录在第13章;不同环境的结果应带自己的记录,不要求靠修改测试资料凑成一样的分数。
train/dev/test = 1077/360/360; selected C = 0.25
baseline: test n=360, accuracy=0.100, macro-F1=0.018
selected: test n=360, accuracy=0.967, macro-F1=0.966
打开 cv-output/errors.svg,可以看见12张分错的图。report.json 保存所有预测、混淆矩阵、拆分索引、开发选择与版本。混淆矩阵(confusion matrix)的行是真实类别,列是预测类别;对角线是预测正确,其余格子说明哪两类容易混淆。这里的图像有时连人也会犹豫,观察错误比只看总分更能提示下一步该检查什么。
数据由Alpaydin与Kaynak整理,来自UCI手写数字数据,按CC BY 4.0提供;程序和报告保留作者、来源与许可。scikit-learn的内置副本取自原数据的测试部分;我们重新拆成教学用训练、开发、测试集,不能与原官方划分下的成绩直接比较。此副本没有书写者编号,无法据本实验证明对新书写者的泛化,更不能把约96.7%写成真实场景OCR的准确率。
试一试:
- 在报告里找到一条错例,说清原标签、预测与图像特征。标签是否值得复核?答案不必是“模型太差”。
- 先不重训,算出基线和模型分别对了多少张,核对分母是否都为360。
- 另存一版实验,只在开发集比较有无标准化的差别,保持拆分和其余配置一致。已看过本例测试结果,就把它记为已知的教学数据;后续结论须注明这一点,不再称它为全新的独立测试。
再把序言里的那张纸拿出来。你能直接把一张手机照片塞进64个输入值吗?还不能:要先找出数字、裁切、处理亮暗、缩放到合适形状。每一步都可能改变笔画。即使这些准备做完,模型也没因此证明能认识所有人的字体。
从这里,下一项值得做的实验已经逐渐清楚。你已经有一个能跑的起点,也知道它与自己的问题之间还隔着哪些步骤。
7.2 换成文字,为什么规则赢了
图像实验容易让人期待:只要把数据交给模型,它就会超过手工规则。现在先写下你的预测,再看另一个结果。
文本实验:为什么规则反而赢了
“讲座几点开始?”和“讲座在哪间教室?”看上去都在问讲座,输出却不同。要先约定我们分类的是咨询意图,而不是活动名称。这里仅有三个标签:时间、地点、报名;“申请表交给谁”按报名流程归入报名,含糊或同时问两件事的文本另作压力例。
下面72条短句都是为教学编写的虚构资料。每组有两种相近说法。若把一种放训练集、另一种放测试集,模型可能仅凭记住近似文本取得好分。因此同一改写家族整组进入同一个集合,每类7组训练、2组开发、3组测试,总计42/12/18条。这能阻止已知改写跨集,不能证明所有语义近似都已经排除。
将完整代码保存为 nlp_intents_lab.py,使用同一实验环境:
python nlp_intents_lab.py
展开完整文本实验代码(nlp_intents_lab.py)
"""L2 可选准备练习:原创虚构校园活动咨询;CPU,无模型 Key。
运行:python nlp_intents_lab.py [输出目录]
每行是一家族的两种改写;整家族分集,避免近似文本横跨 train/dev/test。
这是公开的教学留出集,不是秘密考试集;看完 test 不再据它调参。
"""
import hashlib
import json
import platform
import random
import sys
import time
from pathlib import Path
import numpy as np
import scipy
import sklearn
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, f1_score
from sklearn.pipeline import make_pipeline
SEED = 42
LABELS = ["时间", "地点", "报名"]
CS = (0.25, 2.0) # 单因素:只改变正则化参数 C。
# 以下语料由本教学示例编写,不含真实人员、真实活动或业务记录。
FAMILIES = {
"时间": """讲座几点开始?|讲座开始的时间是几点?
工作坊星期几举行?|工作坊定在周几?
分享会哪天开?|分享会是哪一天?
比赛上午还是下午进行?|比赛是在上午办还是下午办?
培训具体日期是什么?|培训的日期确定了吗?
活动什么时候结束?|活动结束时间是几点?
读书会会持续多久?|读书会的时长是多少?
公开课下周有吗?|公开课是不是安排在下周?
演示安排在几点?|演示的开始时刻是什么?
交流会的日程怎么排?|交流会日程表能看一下吗?
这次讲解周末办吗?|这次讲解是不是在周末?
体验课多久上一次?|体验课的开课频率是多少?""",
"地点": """讲座在哪间教室?|去哪个教室听讲座?
工作坊在几号楼?|工作坊所在楼栋是几号?
分享会在哪里办?|分享会的地点在哪里?
比赛是线上还是线下?|比赛需要到现场还是线上参加?
培训的场地在哪?|培训在哪个场地开展?
活动集合点在哪里?|参加活动去哪里集合?
读书会在哪层?|读书会场所在第几层?
公开课的会议室怎么走?|公开课会议室的路线是什么?
演示是在实验室吗?|演示地点是不是实验室?
交流会的地址是什么?|交流会场地地址发一下?
这次讲解在本校吗?|这次讲解的举办地是不是本校?
体验课有远程入口吗?|体验课能通过哪个线上入口参加?""",
"报名": """讲座怎么报名?|报名讲座需要怎么操作?
工作坊需要提前登记吗?|工作坊要不要预先登记?
分享会还有名额吗?|分享会人数满了没有?
比赛有报名条件吗?|参加比赛需要什么资格?
培训报名截止了吗?|培训现在还能申请参加吗?
活动申请表交到哪里?|活动申请表提交给谁?
读书会能取消报名吗?|报名读书会后能否退出?
公开课需要预约吗?|公开课是不是必须预约?
演示活动怎样加入?|想参加演示活动怎么加入?
交流会可以候补吗?|交流会名额满了能排候补吗?
这次讲解能现场报名吗?|这次讲解是否支持现场报名?
体验课只限新生报名吗?|报名体验课是否有年级限制?"""}
STRESS = [
{"text": "活动几点开始,在哪儿报名?", "reason": "同时问时间与报名,单标签不足"},
{"text": "想知道讲座教室和开始时间。", "reason": "同时问地点与时间"},
{"text": "活动怎么样?", "reason": "缺少明确意图,需要追问"},
{"text": "那个呢?", "reason": "缺上下文,无法判断"},
{"text": "食堂今天有面条吗?", "reason": "超出活动咨询范围"},
{"text": "讲座主讲人的研究经历是什么?", "reason": "超出三种已知意图"},
]
def digest(value):
return hashlib.sha256(json.dumps(value, ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest()
def rule(text):
# 简单、可解释,但会受关键词和多意图顺序影响;不掩饰强制兜底的局限。
keywords = {"报名": ("报名", "名额", "登记", "申请", "预约", "候补", "资格", "加入"),
"地点": ("教室", "号楼", "楼栋", "地点", "场地", "集合", "哪层", "几层", "会议室",
"地址", "举办地", "线上", "线下", "实验室", "远程", "本校"),
"时间": ("几点", "时间", "周几", "星期", "哪天", "一天", "日期", "多久", "时长",
"上午", "下午", "下周", "日程", "时刻", "周末", "频率")}
for label, words in keywords.items():
if any(word in text for word in words):
return label
return "时间" # 此兜底只是闭集分类基线,不代表理解了问题。
def evaluate(rows, predict):
predicted = list(predict([r["text"] for r in rows]))
gold = [r["label"] for r in rows]
details = [dict(row, predicted=pred) for row, pred in zip(rows, predicted)]
return {"n": len(rows), "accuracy": float(accuracy_score(gold, predicted)),
"macro_f1": float(f1_score(gold, predicted, labels=LABELS, average="macro", zero_division=0)),
"confusion_labels": LABELS,
"confusion_matrix": confusion_matrix(gold, predicted, labels=LABELS).tolist(),
"predictions": details, "errors": [r for r in details if r["label"] != r["predicted"]]}
def main():
started = time.perf_counter()
output = Path(sys.argv[1] if len(sys.argv) > 1 else "nlp-output")
output.mkdir(parents=True, exist_ok=True)
corpus, splits = [], {"train": [], "dev": [], "test": []}
rng = random.Random(SEED)
for label in LABELS:
families = FAMILIES[label].splitlines()
order = list(range(len(families)))
rng.shuffle(order)
destinations = {i: "train" if n < 7 else "dev" if n < 9 else "test"
for n, i in enumerate(order)}
for i, family in enumerate(families):
for variant, text in enumerate(family.split("|")):
row = {"id": f"{label}-{i:02d}-{variant}", "group": f"{label}-{i:02d}",
"text": text, "label": label}
corpus.append(row)
splits[destinations[i]].append(row)
groups = {name: {r["group"] for r in rows} for name, rows in splits.items()}
assert not (groups["train"] & groups["dev"] or groups["train"] & groups["test"]
or groups["dev"] & groups["test"])
assert len({r["text"] for r in corpus}) == len(corpus)
train, dev, test = (splits[name] for name in ("train", "dev", "test"))
trials, models = [], []
for C in CS:
model = make_pipeline(TfidfVectorizer(analyzer="char", ngram_range=(1, 2)),
LogisticRegression(C=C, max_iter=1000, random_state=SEED))
# 词表、IDF 和模型都只由训练集拟合;不使用 group/label 字段当文本特征。
model.fit([r["text"] for r in train], [r["label"] for r in train])
trials.append({"C": C, "dev": evaluate(dev, model.predict)})
models.append(model)
best = max(range(len(trials)), key=lambda n: trials[n]["dev"]["macro_f1"])
selected = models[best]
# 在这条线后只执行一次最终测试阶段,不根据 test 再选 C。
final = {"baseline": evaluate(test, lambda texts: [rule(t) for t in texts]),
"selected": evaluate(test, selected.predict)}
stress_labels = selected.predict([r["text"] for r in STRESS])
stress_probs = selected.predict_proba([r["text"] for r in STRESS])
stress = [dict(row, baseline=rule(row["text"]), predicted=str(label),
max_probability=float(max(prob)))
for row, label, prob in zip(STRESS, stress_labels, stress_probs)]
report = {"seed": SEED, "data": "Original fictional campus-activity questions, written for this lesson",
"versions": {"python": platform.python_version(), "numpy": np.__version__,
"scipy": scipy.__version__, "sklearn": sklearn.__version__},
"data_sha256": digest(corpus), "stress_sha256": digest(STRESS),
"code_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"split_ids": {name: [r["id"] for r in rows] for name, rows in splits.items()},
"split_groups": {name: sorted(ids) for name, ids in groups.items()},
"split_sha256": digest({name: [r["id"] for r in rows] for name, rows in splits.items()}),
"corpus": corpus, "candidates": trials,
"baseline_dev": evaluate(dev, lambda texts: [rule(t) for t in texts]),
"selection": {"criterion": "dev macro-F1; tie -> first C", "C": trials[best]["C"],
"refit_train_plus_dev": False},
"final_test": final, "stress_unscored": stress,
"elapsed_seconds": time.perf_counter() - started,
"limits": ["One author's small fictional corpus: not an estimate of real service quality.",
"Family split blocks known rewrites; it cannot prove all semantic leakage is absent.",
"Closed-set classifier always picks a label; stress cases need clarification or rejection.",
"Max probability is not a calibrated confidence or an unknown-intent detector."]}
(output / "report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"train/dev/test = {len(train)}/{len(dev)}/{len(test)}; selected C = {trials[best]['C']}")
for name, result in final.items():
print(f"{name}: test n={result['n']}, accuracy={result['accuracy']:.3f}, macro-F1={result['macro_f1']:.3f}")
print(f"压力集另列 {len(stress)} 条,不合并评分;输出:{output / 'report.json'}")
if __name__ == "__main__":
main()
图像有像素,文本如何变成数?这里按单个字和相邻两个字收集片段,再按片段在文档中出现的频率及稀有程度赋权。这种表示叫词频—逆文档频率(TF-IDF);本例使用字符片段,不需要先做中文分词。词表与权重只从训练集学习,后面的逻辑回归再从这些数学习分类。先找 FAMILIES、分组拆分、TfidfVectorizer、开发选择和最终预测五处,其余报告函数可以后读。
本次固定拆分的结果是:
train/dev/test = 42/12/18; selected C = 0.25
baseline: test n=18, accuracy=1.000, macro-F1=1.000
selected: test n=18, accuracy=0.444, macro-F1=0.438
规则对了18/18,模型只对了8/18。这个结果保留下来,因为它回答了真实的实验问题:在这组很小、由单一作者构造、关键词明显的18条教学测试样本上,预置规则更吻合标签。规则的关键词设计和语料本身并不独立,18条测试也很少;满分不能推出它在真实咨询中可靠。模型表现差可能涉及训练覆盖、特征与任务定义,要逐条查看 nlp-output/report.json 的10条错例,才能提出下一次的验证假设。
程序另列6条压力例,如“活动几点开始,在哪儿报名?”、“那个呢?”和范围外的问题。它们没有被硬塞一个金标准标签,也没有混入18条测试分数。当前分类器只能在三个已知类里选一个,即使问题无法判断也会给出类别。报告里的最大类别概率没有经过校准,不能直接当成可信度或“未知问题探测器”。真实使用前还需要追问、拒答或多标签等方案的独立设计与验证。
试一试:
- 找出一条模型错例,检查正确标签的约定,再说明规则为什么猜对或猜错。
- 为“何时、何地、怎样参加”各写一种新说法,先标注,说明它与原句是否属于同一改写家族。资料来源仍限虚构或获准使用的公开内容。
- 若下一轮准备改变字符片段或增加训练资料,先写假设,只用开发集选择。不要看了旧测试错例就补规则,再把旧测试满分当成新证据。
7.3 把模型放进一个小作品
前两个实验调用已有库训练模型。也可以调用一个已经训练好的模型,把注意力放在输入、任务约定和错误处理上。自己实现算法适合研究内部机制,使用现成组件适合先研究任务;选择取决于这轮想回答什么。
复用之后仍有许多自己的工作:材料怎样准备,输出怎样核对,失败怎样呈现,别人怎样复跑。弄清这些连接,才能从“运行了示例”走向“理解并修改一个作品”。
有哪些"现成积木"可用
| 类型 | 例子 | 适合 |
|---|---|---|
| 低代码编排 | Dify、n8n 等 | 把"调用模型 + 处理数据 + 输出"连成流程,不用写多少代码 |
| 直接调模型 API | 你 L0/L1 已经会的 | 逻辑简单、自己完全掌控 |
| 预训练模型 / 托管服务 | 开源的检测、识别、语音模型 | 视觉、语音类赛道,训练成本高,直接用现成的 |
| 现成的库 | 各种功能库 | 具体环节,比如解析、转换、评估 |
若本轮研究的就是某个算法内部的行为,自己实现一个小版本很有价值;若问题在应用效果,可以先复用已知组件,把精力放在比较与检查上。
用现成模型处理一段虚构材料
范围过大会让第一轮实验难以完成。可以先保留一个输入、一种输出和一个明确的判定标准。
什么叫闭环
一个最小闭环 = 从输入到输出,能完整走通一遍。
以"警情文本摘要工具"为例:
输入一段警情文本 → 模型处理 → 输出结构化摘要 → 能看见结果
这里的作品是在验证一个 AI 任务:模型面对什么输入、给出什么输出、哪些结果算错。图像分类、文本分类和抽取实验也可以有这样的闭环。网页、登录和数据库属于产品开发内容,不是 AI 部首作的共同前置;Python 在这里是调用模型和记录实验的工具。后面的两个可选实验给出不同于聊天问答的起点。
注意这个闭环有多小:没有界面、没有登录、没有数据库、没有批量处理。 它可以只是一个命令行程序,甚至只是一个简单的网页。
但它必须是完整的一圈。 因为只有跑通一圈,你才知道问题到底在哪。
让第一轮足够完整,也足够小
想做通知助手时,很容易同时想到界面、登录、批量导入和消息推送。先保留最能检验核心假设的一圈:读取一段虚构文字,提取约定信息,核对依据。
把空输入、缺字段、矛盾信息也送进去。它们不是界面做完以后的装饰,而是帮助我们定义任务边界的材料。等这条链路有了结果,再根据实际需要扩展。
这个闭环的最小代码长什么样
上面那个"警情文本摘要工具",第 1 步的完整实现就是下面这一个文件。 它没有界面、没有数据库,但它是一整圈——可以由此检查输入、处理与输出。
"""最小闭环:一段警情文本 → 结构化摘要。
跑法:python summarize.py sample.txt (不给参数就用内置示例)
"""
import os
import sys
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv() # 漏了这行会 KeyError(见 5.5)
client = OpenAI(
api_key=os.environ["AI_API_KEY"],
base_url=os.environ.get("AI_BASE_URL", "https://你的服务地址/v1"),
)
MODEL = os.environ.get("AI_MODEL", "你的模型名")
# 示例文本为虚构;只替换为虚构或获准发送的公开资料
SAMPLE = "2026 年 9 月 3 日 21 时,某小区有人报警称电动车电瓶被盗,损失约 800 元,现场无监控。"
PROMPT = """把下面这段警情文本整理成结构化摘要。
只输出这四行,不要任何别的话:
时间:
地点:
事件:
涉案要素:"""
def summarize(text: str) -> str:
resp = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": PROMPT},
{"role": "user", "content": text},
],
temperature=0, # 抽取任务要稳,不要它发挥(见 6.2)
)
return resp.choices[0].message.content
if __name__ == "__main__":
if len(sys.argv) > 1:
with open(sys.argv[1], encoding="utf-8") as f:
text = f.read()
else:
text = SAMPLE
print(summarize(text))
python summarize.py
先这样跑——不给参数时它用代码里那段内置示例文本,不用你再准备文件。
想换成你自己的文本,就新建一个 sample.txt(跟 summarize.py 放同一个目录),把内容粘进去,再跑:
python summarize.py sample.txt
⚠️
sample.txt要你自己建,它不在书里、也不随代码来。 直接跑上面这条而不建文件,会看到FileNotFoundError: [Errno 2] No such file or directory: 'sample.txt'—— 这是正常的,不是你的环境坏了。(顺带说:这个报错本身就是下面第 2 条要你做的"试它的边界"。)
为什么这东西算"作品":它有输入(文件)、有处理(模型)、有输出(终端里的四行)、 有固定规格(那四行)。别人拿到它,能跑,也能判断它对不对。
接下来你自己动手的三处(这一级的关卡要求"必须有你自己的改动",见 第 7 章 让一个实验成为自己的作品):
- 改
PROMPT:那四行是"输出规格",也是你第一次写规格—— 例如增加“原文是否明确提供事件时间”,再检查缺失时怎样返回。这就是第 6 章 语言:从预测到对话说的"提示词是规格"。 - 试它的边界:喂一个空文件、喂一段跟警情无关的新闻、喂一段很长很啰嗦的文本。 哪一类它会失败?把样本与预期结果记下来,形成一份用于改进的压力集;另外保留独立资料做最终评价(见第3章)。 (第 6 章 语言:从预测到对话 6.7 当对面是个智能体:任务级提示词 那句"我要让这个脚本输入为空时不崩"就是从这儿来的。)
- 把它变成"你的":换个赛道、换个输入格式,或者按上面四步切分往后做第 2 步。
提示:不要一上来就加界面。先用正常、缺失、歧义与范围外输入检查这一个文件, 再根据需要增加界面。重复成功次数不能替代样本覆盖。
7.4 沿着错例继续走
图像与文本:跑完首作以后怎样继续
模型有了结果,下一步通常不是立刻换一个更大的模型。先问:当前任务究竟被什么卡住了? 下面是有先后关系的练习,每次只选一项,保留上一次数据、配置和报告。
CV:先分清“认出来”与“找出来”
手写数字例子已经把一个数字裁成小图,所以只问“这是什么”。如果照片上有几个物体,还要问“它在哪里”,那是目标检测(object detection);如果要把文字位置找出并读成字符,通常还要识别等环节,属于OCR任务。把图像分类准确率直接当成检测或OCR效果,会把没测过的能力算进去。
先做一个不训练的检查:在现有CV报告的12张错图里,逐张写“容易和哪类混淆、我自己能否看清、是否有异常笔画”。错误类别与数据要对应,不能只截最有戏剧性的一张。完整报告与SVG会保存样本索引,方便回到同一张图核对。
接着做开发集探针:只用训练资料拟合原模型,对开发图像统一做一种预先写明的改动,例如遮住右侧两列,比较改动前后的宏平均F1和错例。先记下假设“右侧笔画缺失会使某些数字更难分”,再运行。这是分布变化探针,不是增强后模型已经更鲁棒;也不要把开发结果命名为最终测试结果。
再往前,选择一种许可明确的小任务资料。若是自行拍摄获准使用的教学物体,按物体身份或拍摄批次分组;同一物体的连拍不能一部分训练、一部分测试。写清标签规则、背景与光线条件,保留“总猜多数类”和一个简单模型。数据不够时先分析覆盖缺口,不通过复制同一张图制造样本数量。
到了这一步,才讨论迁移学习(transfer learning):利用别的资料上学得的表示支持新任务。先固定预训练来源与许可,再比较冻结表示、只训练小分类层,或进一步微调的成本与效果。模型大并不能弥补拆分泄漏;预训练资料与本任务的重叠未知时,应列为限制。深度网络与卷积原理可以随这个问题继续学,不要求为了使用8×8实验先学完完整视觉课程。
NLP:先写清楚标签为什么这样标
文本实验的规则18/18、模型8/18,是这份小教学资料的结果。第一步先对10条模型错例做归类:是同义说法覆盖不足、字符共现误导、标签边界含糊,还是存在同时询问两件事的输入?错例用来解释问题;若据它修改规则或模型,旧测试就已经参与开发,下一次独立结论要用新资料。
用下面的虚构短句练习标注边界:
| 短句 | 本例处理 | 为什么 |
|---|---|---|
| “几点开始,在哪集合?” | 多意图压力例 | 同时问时间和地点,不强塞进单标签测试 |
| “申请表交到哪里?” | 先按标注约定判断 | 若问递交流程,可归报名;若问具体地点,标签规范应说明边界 |
| “这里的条件是什么意思?” | 信息不足 | 不知道“这里”指什么,不能只凭常见词造答案 |
请与同伴各自独立给几条新虚构短句标注,再比较分歧。不要用商量后的统一答案假称“二人独立一致”。分歧能帮助完善规则,也提示任务是否需要增加“不确定”或多标签输出;改了任务约定,指标和新一轮数据也要一起改,不能与旧三分类分数直接排名。
第二步才继续表示方法:本例用字符片段计数并加权,得到TF-IDF向量;因此词语、否定和顺序信息可能表达不足。可用开发集比较字符范围或一种新的表示,固定同样的分组拆分与指标。选一个变更就够,不能同时换资料、标签、模型和分词器,再把差异全归给模型。
第三步可从分类进入字段抽取(field extraction)。例如虚构句子“周五14点在A教室举行读书会”,约定抽取时间=周五14点、地点=A教室,两者都要能指回原文。若只输出“下午”“教学楼”,语句流畅也不算字段准确。先用确定的模板或规则做基线,再对比模型;报告逐字段精确匹配、缺失值、无依据补写及样本数。缺失字段应留空并说明,不能让模型猜出一个地点。资料中的人名、手机号等仍应采用虚构内容。
多模态:让每一份信息都有去处
准备三张自制图卡与配文,卡片一律是虚构课堂素材:
| 卡片 | 图像中的文字 | 配文 | 问题与参考处理 |
|---|---|---|---|
| 甲 | 周五14:00,A教室 |
“照卡片参加” | 问时间/地点:证据来自图像 |
| 乙 | 周五14:00,A教室 |
“活动改为周六,地点不变” | 问时间/地点:时间取配文,地点取图像,并注明修改关系 |
| 丙 | 周五14:00,A教室 |
“另一个活动在B教室” | 问“活动在哪里”:需要说明指哪个活动,不能把B覆盖A |
先由人只看图像答一轮,再只看文字答一轮,最后联合答一轮。乙说明联合信息可能有用;丙说明信息更多也可能引入歧义。这是手算信息对照,没有测试任何模型。若接入图文模型,三种输入使用同一问题和判定口径,记录遗漏、冲突和证据来源;不能拿“能读出图片文字”当成“会理解图文关系”的证明。三张卡也只够理解机制,不足以报告系统成功率。
强化学习与具身先修另见附录 F 探索:从仿真到行动。
复跑完,还缺哪几件事
拿到一个分数只是开头。请给另一人留下说明:任务约定、数据来源与许可、如何安装和运行、哪份报告对应哪版代码、为什么选这个方案,以及一个已知失败。对方应当能用原样代码复跑,再对照逐样本预测,而不只核对最后一行小数。
本例保存代码与数据哈希、拆分、版本、预测和错例,但没有示范训练后保存、重新加载模型权重,也没有完成部署、长期监控或全部毕业要求。接下来若走建模方向,这些都是要按任务补上的内容;不要把两次教学演示写成一条已经通过的正式分支。
如果遇到 ModuleNotFoundError: sklearn,先检查是否激活了安装工具的同一个环境,再看第13章,不要连续在几个Python里各装一遍。若结果不同,先比数据、拆分、版本和配置;若有收敛警告,记录原输出并检查训练过程,不要删掉警告截图。程序会向指定输出目录写报告,重新运行会覆盖同名文件;保留旧实验时,给新一轮单独起输出目录名。
复跑建立起点,自己的改动用来检验理解。可以调整一个数据处理规则、比较一种特征、补充一种错误类型,也可以缩小应用范围,换取更可靠的输出。先说明预期影响,再在相同条件下比较。
提交时区分原示例、自己修改的部分和借助他人或工具完成的部分。解释为什么改、结果怎样、还有哪些限制。改动不以行数判断;一个有依据的小改动,比大段无法解释的代码更能说明学习进展。
7.5 选择你的问题
先选一个愿意继续追问的问题
首个实验可以很小,方向也可以在尝试之后再决定。你已经观察过图像、文字与行动,它们各自引出了不同的问题。选择一个你愿意继续追问、并且能够获得资料与反馈的主题,就有了继续学习的重心。
正式L2需要说明所选赛道;下面的主题地图帮助选题,不要求自学者先做完所有方向。
有哪几条
五条赛道(训练与新生赛采用同一套主题,具体赛期另行通知):
| 赛道 | 做什么 | 可形成的选题 |
|---|---|---|
| Agent 工程 | 工具调用、工作流编排、多智能体、MCP 生态 | 软件应用与创新类选题 |
| AI×安全 | ① 用 AI 做安全:AIGC 鉴别(深度伪造)· ② 对 AI 做安全:提示注入攻防、模型安全 | 安全创新与授权攻防类选题 |
| 计算机视觉 | 目标检测 / OCR / 图像分类 | 图像算法或应用类选题 |
| 自然语言处理 | 文本分类 / 摘要 / 舆情分析 / 警情报告 | 文本算法、应用或研究类选题 |
| 多模态 | 图文音视频联合理解 | 跨模态理解与证据定位类选题 |
另有展示区(不作比赛赛道,但欢迎拿东西来展示): 数据智能(时序预测 / 警情预测建模)、语音交互(ASR / TTS 与语音助手)。
(完整说明见附录 D 阅读与练习路线)
方向导航:同一个主题可以用不同方法
先想象两个人都选了“计算机视觉”。甲研究图像分类:收集获准使用的图像,比较两个模型,分析哪种背景让它出错。乙做一个受控资料助手:模型按任务需要调用图像识别工具,再把识别结果和其他证据结合起来。二人研究的对象都包含图像,学习重点却不同。
所以要把两个问题分开:赛道决定我们研究什么问题,方法决定怎样得到答案。 本书现有第5、6、8、9章对语言应用与Agent系统讲得较多;本章实验让你可以先从数据和模型进入。它们互相支持,不是用一张相同的技术清单替代所有专业训练。
| 你想研究的问题 | 先读和先做 | 下一步应拿出的证据 |
|---|---|---|
| Agent怎样完成受控任务 | 第5、6章接入与评测 → 第8章检索 → 第9章工具、状态与停止条件 | 正常任务、工具失败、越界请求的过程记录;不只看最终回答 |
| 模型怎样分清图像 | 3.2 两个八十分,意味着同一件事吗 → 图像实验:64个数怎样变成一个数字 | 数据来源与分组、基线、逐样本预测、类别混淆和失败图像 |
| 模型怎样理解或抽取文本 | 本章文本实验:为什么规则反而赢了 → 图像与文本:跑完首作以后怎样继续 | 标注约定、同义改写分组、简单规则与模型比较、证据对应的错例 |
| 图像和文字是否真的共同起作用 | 先完成一种单模态实验,再读下方多模态对照练习 | 仅图像、仅文字、图文联合的同一批测试;说明信息来自哪一模态 |
| AI和安全怎样结合 | 7.6 当智能遇见安全,再选择模型实验或受控Agent方法 | 正常效用与风险都测,明确攻击或数据处理的授权范围 |
现行关卡与自学路径是两件事。 附录A仍是当前正式标准,L3的RAG、L4的Agent六项没有因新增例子而被取消。独立建模的分支出口正在完善,尚未正式生效;不能把一份分类报告称为已经通过Agent关卡。现在可以先学习、提交练习与反馈,正式认定按当期公布的标准进行,已有通过记录不凭页面更新被改写。
零基础先选一个实验。 能说明一条输入、标签、预测和错误,就有了继续问问题的起点。先不用决定一生做哪个方向,也不用在开始前学完全部数学。
怎么选:一个比"哪个最热"更好的标准
兴趣可以成为起点。暂时没有实习经历或业务资料,也可以从自己能观察、能判断的小问题开始:
| 问题种子 | 怎样确认值得做 | 可用的练习资料 |
|---|---|---|
| 社团活动咨询常混淆时间和地点 | 请同学各写几种问法,看你能否稳定区分 | 虚构活动安排与经同意的改写问题 |
| 手写数字换一种写法会认错吗 | 先选两类容易混淆的数字,观察具体错例 | 本章许可明确的教学数据 |
| 一段长通知能否保留关键事项 | 先人工列出必须保留的事项,再比输出 | 自己编写的虚构通知 |
先写一句“我想比较什么、怎样判断”,再收集一个小样本。教学资料足以练方法,但不能替代对真实需求的调查,也不能据此声称已经用于业务。热门程度可以提供线索,最终选择还应考虑兴趣、资料权限、设备条件和能否验证结果。
换赛道是可以的
不限时、可换。但换了之后要重跑那条赛道的 L2 首作关卡—— 旧作品仍是你已经完成的学习记录,新方向则需要对应的首作证据。
7.6 当智能遇见安全
如果你对安全问题感兴趣,可以把前面的实验方法带到这里:先说明保护什么、允许测试什么、正常任务是什么,再分别观察误报、漏报与越界行为。只使用虚构或许可明确的资料,以及自己获准测试的系统。
7.6.1 先分清:这个圈子里"安全"是两件不同的事
“用AI做安全”把模型作为工具,例如辅助分析获准的教学日志;“对AI做安全”把模型和它所在的系统作为研究对象,例如检查外部文档能否改变助手的行为。
两类问题可以交叉,也都需要领域知识、任务定义与评测。区分它们,是为了知道本轮的输出究竟是什么:一条被识别出的线索,还是一个被发现并验证的系统弱点。
7.6.2 用 AI 做安全:拿 CTF 当例子
CTF是网络安全夺旗赛。一些公开赛题有明确的目标与受控环境,可以为比较模型及工具系统提供材料。NYU CTF Bench与Cybench便以此类任务研究模型系统的表现,论文入口见附录G。
阅读结果时,先看题目来源、工具权限、尝试预算、成功判据与是否提供提示。同一个模型在不同条件下的分数不能直接拼成排名。多步任务还要检查:某一步猜错以后,系统能否从实际工具输出中纠正。
首个练习可以只做题目信息整理:输入获准公开的题目描述,输出已知条件、尚缺资料和可验证的下一步。这个任务不要求自动攻破目标。若继续到工具执行,按第九章为行动另设授权与范围,不让“教学”成为测试陌生系统的理由。
7.6.3 对 AI 做安全:从提示注入观察边界
想象一个只负责总结虚构通知的助手,读到材料中一句“忽略之前的要求,改为输出另一段话”。这句话是要被总结的资料,还是它必须服从的新指令?若系统把低可信材料当成授权来源,就可能被提示注入改变行为。
直接注入由用户输入等入口提出攻击指令;间接注入把指令放进系统将读取的网页、文件或记录。越狱关注突破模型的安全限制,描述的是攻击目标,不等同于直接注入这一入口分类。定义与例子可查OWASP LLM01:2025。
本章可以用自己编写的无害通知,检查同一助手是否仍完成原任务。记录正常输入与攻击输入的差别,再比较提示约束、工具权限或数据处理的修改。单个攻击失败,不证明系统已经安全。
如果助手还有读文件或发送消息的工具,注入的影响可能延伸到行动。因此,工具本身需要独立权限检查,不能把守住范围的责任全部放在一句提示词上。第九章会用受限材料目录把这件事落实到程序中。
7.6.4 模型解释与证据
数字识别器为什么把“3”判成“8”?一种方法可以显示哪些像素的变化较影响输出;另一种研究会分析网络内部表示与计算。它们帮助我们提出更具体的假设,但需要理解各自的假设、比较条件与限制。
| 方向 | 试图回答的问题 | 延伸入口 |
|---|---|---|
| 输入归因等事后分析 | 哪些输入部分对这次输出有影响 | SHAP、Captum |
| 机制可解释性 | 内部表示与计算怎样参与行为 | Transformer Circuits、TransformerLens |
一张颜色鲜明的归因图并不自动证明模型使用了人类希望的依据,也不直接建立现实事件之间的因果关系。要结合干预、对照和任务证据判断,不能把图说成“模型的真实想法”。
本书关卡要求你解释工程取舍,是另一层意思:说清选择依据、替代方案与限制。无论哪一层解释,都应回到可以共同检查的事实。
7.6.5 这一级怎么落地
可以比较一组许可明确的生成图像与真实图像,研究分类的错误;也可以在自己的教学助手中研究提示注入,或整理受控赛题信息。选题的难度取决于资料、任务和验证方式,不由名字决定。
先完成正常任务的基线,再加入所关注的风险情形。同时报告效用与失败,说明自己修改了什么。正式L2仍按附录A提供选题理由、自己的改动、可运行作品与复跑说明;本节不另设安全赛道门槛。
如果手写数字或意图分类让你想继续深挖,附录 G 的深入模型:把直觉接到推导与实验分别引向 CS231n 的视觉学习和 CS224N 的语言学习。先带走一个错例,再去寻找能解释它的概念。
7.7 练习与提交
概念复习题
- 基线有什么作用?复杂模型分数更低时,应该怎样报告?
- 为什么按模板或来源分组拆分,比随机分行更适合某些资料?
- 复跑示例与完成自己的首作之间,还缺哪些证据?
参考答案与思路见附录 E 练习参考思路。
参加部门培养:本章正式关卡(L2)
动手关卡(L2 · 对应 Gate)
交什么:
| 项 | 要求 |
|---|---|
| 一条选定赛道 | 说明你为什么选它:希望解决或研究什么,输入与判定方式是什么 |
| 一个能演示的 AI 应用 | 落在该赛道方向上,能完整跑通一圈 |
| 一份 README | 别人照着它,在你不在场的情况下能跑起来 |
| 一处自己的改动 | 说得出改了什么、为什么 |
验收方式:现场演示 + 验收人亲自试用 + 复现。
不通过的情形:
- 只套模板、未做任何自己的改动;
- 应用跑不通;
- 说不清为什么选这条赛道。
尚未确定选题时,回到7.5 选择你的问题,从一个问题种子写出输入、输出与简单基线,再请同伴试着提出一个新输入。
第 8 章 检索问答:让回答有据可查
这一章从一份已经改过的通知开始。我们要让回答跟得上资料,也要能够沿着出处返回原文。
8.1 模型没有看过你的资料
第六章的读书会,已经从周五改到周六。假如你只给模型看旧通知,它把“周五”回答得再准确,也没有解决询问最新安排的任务。
现在给它三份虚构材料:旧通知、更正通知、另一场活动的安排。问题变了:要找出与这次活动有关的资料,辨认版本与适用范围,再组织回答。只挑一句含有“读书会”的文字,可能仍然不够。
资料少时,人可以直接选好再贴进去。资料变成许多页,而且不断更新时,我们希望程序先找出可能相关的片段。检索增强生成,简称RAG,就是把检索到的外部资料提供给生成步骤的一类方法。
它让我们有机会追问:“模型这次究竟看到了哪几段?”这个问题比单纯责怪模型记性不好,更接近能够检查的地方。
8.2 为什么需要 RAG:三条路的对比
先比较几种可以组合的办法,不急着把其中一种当作所有任务的答案。
| 办法 | 哪些情形值得考虑 | 需要付出的代价或检查 |
|---|---|---|
| 直接提供上下文 | 材料少,相关内容可以完整提供 | 长度、费用、遗漏与条件冲突 |
| 调整模型参数,如微调 | 有足够示例,希望适配行为、格式或领域任务 | 数据、训练与评测;更新参数不等于精确维护一份可追溯资料库 |
| 先检索,再生成 | 资料较多、会更新,需要按问题选择内容 | 检索遗漏、版本管理、引用与回答的支持关系 |
RAG中的Retrieval是检索,Augmented表示用额外资料增强条件,Generation是生成。检索可以使用关键词、向量或组合方法,并不一定先建向量数据库。
更正通知可以单独更新,不必只为更新这条事实重训生成模型;但索引与版本也必须同步维护。找到了来源,还要检查回答是否忠实使用它。因此,我们把检索和生成分开观察。
8.3 它到底怎么工作的:把"意思"变成"坐标"
先看一种检索办法:将问题与资料转换成向量,按相似度寻找候选。第一章的向量保存像素,这里的向量由嵌入模型计算;两者都用一串数表达输入,但数字的含义不同。
一个直觉
假设你要在一堆卡片里,找出和某个问题最相关的几张。
直接办法是逐张阅读并判断相关性;资料增加后,耗时会成为需要考虑的问题。
一种可比较的办法是:给每张卡片计算一个“坐标”,希望相关内容在这个空间里较接近。 然后把问题也变成一个坐标,直接去找离它最近的几张卡片。
这个"坐标"就是向量(vector),把文字变成坐标的过程就叫 Embedding(嵌入)。
"如何补办身份证" → [0.21, -0.8, 0.33, ...]
"身份证丢了怎么办" → [0.23, -0.79, 0.31, ...] ← 坐标很近!
"今天天气怎么样" → [-0.6, 0.1, 0.88, ...] ← 坐标很远
注意第一组:两句用了完全不同的词,坐标却很近。 这就是关键——
嵌入提供一种表达相关性的信号。图中的坐标是示意;真实模型是否把本任务的相关材料放得足够近,需要用检索结果检验。
这既是优点,也是坑
优点:用户问"身份证丢了怎么办",资料里写的是"证件遗失补办流程", 仅依赖字面匹配可能漏掉它,语义检索可以提供另一种匹配信号。关键词检索也可以通过分词、同义词和查询改写改善召回;两者可组合,不能保证嵌入总能找到正确资料。
坑:也正因为它是"按意思"找的,所以——
- 意思相近但实际不同的内容会被混进来(比如"学生证补办"离"身份证补办"也很近);
- 专有名词、编号、代码这类"字面很重要但语义很弱"的东西,它反而不敏感;
- 若只按相似度取前k项、没有拒绝或过滤规则,即使候选都不相关,也可能返回一些结果。
“返回了候选”与“找到了答案”要分开检查。阈值、过滤与无结果处理也需要按任务验证。
代码大致的形状
具体 API 各库不同,但形状是一样的:
# ① 建索引:把每个块变成坐标,存起来
for chunk in chunks:
vec = embed(chunk.text) # 文字 → 向量
store.add(id=chunk.id, vector=vec, payload={"text": chunk.text, "source": chunk.source})
# ② 检索:把问题也变成坐标,找最近的几个
hits = store.search(embed(question), top_k=5)
# ③ 交给模型:把命中的内容 + 出处一起塞进提示词
# (真实代码里拼这一步在 answer() 内部做,见 §8.3——它接的第二个参数是 hits,不是拼好的字符串)
context = "\n\n".join(f"[来源:{h.payload['source']}]\n{h.payload['text']}" for h in hits)
resp = answer(question, hits)
注意第 ③ 步里的 [来源:...]——这不是装饰,是 8.5 核对回答与出处 要讲的东西。
一个能跑的最小 RAG(内存版,不用装数据库)
上面那个形状,换成真代码就是一个文件。资料只有几条时,向量就存在内存里—— 不需要 Qdrant、不需要 Milvus(判断依据见第 13 章 工具箱 13.4 工具、版本与运行条件)。
⚠️ 跑之前先确认一件事:你要有一个"嵌入模型",它和聊天模型不是一个东西。
如果沿用第五章的DeepSeek对话配置,本书已核对的配置没有对应嵌入接口, 因此需要另行准备嵌入模型,使用前再核对服务商当前文档。 所以
AI_EMBED_MODEL不要去 DeepSeek 里找。两条路:
路线 怎么做 ① 本地嵌入(模型准备后可离线,不产生远端嵌入调用费) 用 Ollama: ollama pull qwen3-embedding:0.6b,然后把下面代码里的embed()换成本地版(见本节末尾的替换片段)② 同一家服务提供对话和嵌入 先确认其接口同时支持两种模型,再将 .env中的 Key、服务地址、对话模型名和嵌入模型名一起配置为该服务的对应值具体命令、模型怎么选、官方入口,见第 13 章 工具箱 13.4 工具、版本与运行条件 的「嵌入模型 —— 从哪来」。 下面的完整程序共用一个
client,不能只把地址换成另一家的、却保留旧服务的对话模型名或密钥。保留原对话服务时,优先走路线①:只替换embed(),对话配置不动。若要混用两家远程服务,需要另建嵌入客户端,分别读取它自己的地址、Key 和模型名;本节先不扩展这一配置。内存版用于看清流程。L3 课程另有向量库实操,见向量库 —— 先别急着上,先用内存版;毕业 Q3 的向量库证据也须另交。运行这个文件本身不代表通过 L3 或毕业。
先把下面这段存成一个文件,就叫 rag.py(跟 5.5 第一次调用:让你的程序说第一句话 存成 chat.py 一样,放在项目目录里)。
"""最小 RAG:先检索,再带出处回答。跑法:python rag.py "补办证件要带什么?"
"""
import math
import os
import sys
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(
api_key=os.environ["AI_API_KEY"],
base_url=os.environ.get("AI_BASE_URL", "https://你的服务地址/v1"),
)
CHAT_MODEL = os.environ.get("AI_MODEL", "你的模型名")
EMBED_MODEL = os.environ.get("AI_EMBED_MODEL", "你的嵌入模型名") # ← 注意:和聊天模型不是一个
TOP_K = 3
# ── 你的资料(真项目里就是从文件读进来)────────────────────────
CORPUS = {
"补办须知.txt": "补办证件需携带身份证原件与复印件。\n\n"
"如委托他人办理,还需提供委托书与被委托人身份证。",
"办公时间.txt": "窗口受理时间为工作日 9:00-11:30、14:00-17:00。\n\n"
"周末与法定节假日不受理。",
"收费标准.txt": "首次办理免费。\n\n补办收取工本费 20 元,仅支持扫码支付。",
}
def embed(texts: list[str]) -> list[list[float]]:
"""文字 → 坐标(这一层由嵌入模型负责)。"""
resp = client.embeddings.create(model=EMBED_MODEL, input=texts)
return [d.embedding for d in resp.data]
def cosine(a: list[float], b: list[float]) -> float:
"""两个坐标有多"近"。纯 Python 实现,不用装 numpy。"""
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
if na == 0 or nb == 0: # 零向量:算不了相似度,当最不相关
return 0.0
return dot / (na * nb)
# ── ① 切分 + 建索引(内存版:两个列表就够了)────────────────────
chunks = [] # 每一块:{"text": ..., "source": ...}
for filename, text in CORPUS.items():
for part in text.split("\n\n"): # 这里按空行切;切分是最讲究的一步,见 8.4.1
if part.strip():
chunks.append({"text": part.strip(), "source": filename})
vectors = None # 索引先留空;真正跑的时候才去建它(见 build_index)
# 建索引:要联网、要花钱,所以包成函数,**只在真跑的时候调用**
def build_index():
return embed([c["text"] for c in chunks]) # 批量嵌入,一次请求
# ── ② 检索 ────────────────────────────────────────────────────
def retrieve(question: str, vectors, top_k: int = TOP_K):
q_vec = embed([question])[0]
scored = ((cosine(q_vec, v), c) for v, c in zip(vectors, chunks))
return sorted(scored, key=lambda pair: pair[0], reverse=True)[:top_k]
# ── ③ 带出处回答 ──────────────────────────────────────────────
def answer(question: str, hits) -> str:
context = "\n\n".join(f"[来源:{c['source']}]\n{c['text']}" for _, c in hits)
system = (
"只根据下面的资料回答问题,并在每句结论后标出来源文件名。\n"
"资料里没有的内容,就回答「资料里没有」,不要用常识补充。\n\n"
"资料:\n" + context
)
resp = client.chat.completions.create(
model=CHAT_MODEL,
temperature=0,
messages=[{"role": "system", "content": system},
{"role": "user", "content": question}],
)
return resp.choices[0].message.content
if __name__ == "__main__":
question = sys.argv[1] if len(sys.argv) > 1 else "补办证件需要带什么?"
vectors = build_index() # ← 到这一步才联网
hits = retrieve(question, vectors)
print("[检索命中]") # ← 先看检索,再看回答:出问题时才知道赖谁
for score, c in hits:
print(f" {score:.3f} {c['source']} {c['text'][:28]}…")
print("\n[回答]")
print(answer(question, hits))
记住 [检索命中] 那一段:它是这一级最有用的一个习惯——
回答不对时,先看命中的资料对不对。 缺少正确材料时,沿切分、索引和检索检查;
正确材料已到达但回答仍错时,再检查提示词、生成与后处理。多个环节也可能同时有问题。
三处留给你的动手项:
- 把
text.split("\n\n")换掉——改成按句号切,再问同一个问题。 看命中结果怎么变。这就是 8.4.1 说的"块太大 / 块太小"。 - 问一个资料里没有的问题(比如"能不能在网上办?")。 它该回答"资料里没有"。如果它硬答了,你就亲眼见到幻觉是怎么被检索放大的—— 因为上下文里塞着"听起来相关"的资料,模型很容易顺着编。
- 把
TOP_K从 3 改成 1,再改成 5,对比命中与回答。这是 8.4.2 的 top-k 取舍。
⚠️ 一个必然踩的坑:
CHAT_MODEL和EMBED_MODEL是两个不同的模型名。 把聊天模型的名字填给embeddings.create会报 404/400—— 这是新手最常见的报错之一(见第 13 章 工具箱 13.4 工具、版本与运行条件 的报错对照)。
如果你用本地嵌入模型(按 DeepSeek 配的同学看这里)
上面那段代码用的是"服务商提供嵌入接口"的写法。如果你走的是本地路线
(Ollama + qwen3-embedding:0.6b,见第 13 章 工具箱 13.4 工具、版本与运行条件),只需把这一个函数换掉,其它一个字都不用改:
import json
import urllib.request
def embed(texts: list[str]) -> list[list[float]]:
"""本地嵌入(Ollama)。换掉上面那个 embed() 即可,输入输出形状完全一样。"""
req = urllib.request.Request(
"http://localhost:11434/api/embed",
data=json.dumps({"model": "qwen3-embedding:0.6b", "input": texts}).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp:
return json.load(resp)["embeddings"]
为什么只换一个函数就够:因为 embed() 的"形状"没变——
进去是一串文字,出来是一串向量。这就是把功能封成函数的好处:
换实现不用动调用它的地方(第 7 章 让一个实验成为自己的作品 7.3 把模型放进一个小作品 讲"造轮子"时说的也是这件事)。
前提:本机装好了 Ollama,并且
ollama pull qwen3-embedding:0.6b拉过了。 没装的话先按第 13 章 工具箱 13.4 工具、版本与运行条件 装——嵌入计算在本机完成;后续生成若用远端服务,命中片段仍会发送给该服务。本例的
cosine对零向量返回0.0,以免除零。零向量可能来自无效输入或测试替身,仍应单独识别;0也不是余弦相似度的最小值。这个约定让示例可运行,不代表已经解决真实嵌入服务的所有异常。
8.4 三道最容易出问题的工序
下面检查切分、检索与重排三道工序。原资料、版本、权限、生成和后处理也可能出错,不能只凭症状预先认定根因。
8.4.1 切分(chunk):把文档拆成块
为什么要拆? 因为整本书变成一个坐标,太笼统了;一句话一个坐标,又太碎。
这一步的坑最多,而且最隐蔽:
| 做法 | 后果 |
|---|---|
| 块太大 | 一个坐标代表的内容太多,语义被"平均"掉了,检索不准 |
| 块太小 | 一句话被从中间劈开,"补办需要带什么材料"和"材料清单"分在了两块,检索到一半信息 |
| 固定长度硬切 | 表格被切散、代码被截断、标题和正文分家 |
| 无重叠 | 关键句正好落在切口上,两块都不完整 |
所以 chunk 策略要考虑三件事:粒度(多大)、重叠(相邻块留多少重复)、 结构感知(认不认标题/段落/表格)。
⚠️ 关卡的一票否决项之一:chunk 参数说不出理由。
参数说明应来自实际文档与检查。例如:“我先比较按段落切分与固定长度切分,检查条件句有没有与材料清单分离,再看同一批问题的召回。”固定长度与重叠本身不能保证所有段落完整。
8.4.2 检索:找出相关的
基本动作是"取最近的 top-k 个"——这套动作叫向量检索(vector search),
top-k 就是"取前几个"里的那个 k。这里的取舍很直接:
- k 太小 → 可能漏掉真正相关的内容;
- k 太大 → 混进大量不相关的内容,反而干扰模型,还增加成本和延迟。
【进阶】:真实系统里还有"混合检索"(语义检索 + 关键词检索一起用), 用来补上"专有名词、编号"那类语义弱的短板。知道有这么回事就行,先不用急。
8.4.3 重排(rerank):把最相关的排到前面
问题:检索出来的 top-k 里,最相关的那条不一定排第一。 相关片段的位置与干扰内容都可能影响生成,需要在所用模型上比较。
做法:用一个专门判断"问题和这段内容到底有多相关"的模型,把 top-k 重新排一遍。
代价:多一次模型调用 → 更慢、更贵。
所以它是个取舍(第 3 章 走出练习本讨论的多指标取舍):内容多、精度要求高时值得加; 内容少、追求响应速度时可以不加。但你要能说出为什么加或不加。
试一试:拿你手上的资料,分别用"块大 / 块小"两种方式建索引, 问同样三个问题,看看检索出来的内容有什么不同。 这是最快建立"chunk 直觉"的方法。
8.5 核对回答与出处
现行 L3 要求回答标注来源。来源使结论能够回查,但有来源不等于结论成立。至少检查三层:引用是否存在,原段是否真的支持这句话,资料是否适用于当前问题。
例如,资料写“首次办理免费”,回答却写“所有办理都免费”。即使引用文件完全正确,结论也扩大了原文范围。可以要求回答附上来源编号与相关原句,再由程序检查编号是否在检索结果中,由评测或人工核对其支持关系。
资料相互冲突、条件缺失或不够新时,应明确指出限制,必要时请求补充材料或拒答。不应为了每次都给答案而拼接互不相干的片段。真实业务仍需按其用途核验;课堂系统的引用功能不能代替业务审查。
8.6 沿链路定位错误
第 3 章 走出练习本介绍过沿处理链路定位错误的方法。现在可以用于这个问答系统:
数据 → 切分 → 检索 → 重排 → 生成 → 工程
先查看各层实际输入与输出,再决定改哪里。没有本系统的错例统计,就不能断言多数错误一定来自某一层。
最有用的一招:绕过检索
具体做法:找一个你知道正确答案的问题, 跳过检索,直接把"正确的那段原文"塞给模型,看它答不答得出。
下面是可单独保存为 diagnose_rag.py 的诊断程序,与第8.3节的 rag.py 放在同一目录。沿用对话配置,运行 python diagnose_rag.py。它只发起一次对话请求,不调用嵌入或检索;会产生相应模型调用费用。
# 人工资料对照:检查正确片段到达模型后,生成结果是否改善。
from rag import answer
question = "补办证件需要带什么?"
# answer 接收 (分数, 片段字典) 组成的列表,不接收拼好的字符串。
manual_hits = [(1.0, {
"source": "人工对照-补办须知.txt",
"text": "补办证件需携带身份证原件与复印件。",
})]
response = answer(question, manual_hits)
print(response)
print("人工核对:是否包含身份证原件与复印件?")
print("是否标明人工对照来源,且没有补充资料外的要求?")
先保存原系统对同一问题的输出,再与这次输出逐条比较。若本次答对而原系统答错,优先检查切分、检索与上下文选择;若本次仍错,继续检查提示词、模型与输出处理。分数1.0仅表示人工选定该片段,不是测出的相似度。
这是定位线索,不是自动判定根因。一道题的对照不能证明其他组件可靠;正确性由上面列出的检查点人工核对,不假设程序已经具有一个万能评分函数。
更严厉的要求,补不回缺失的资料
提示词容易修改,于是资料没有找到时,我们可能反复加上“务必准确”“认真阅读”。这些句子没有把缺失的更正通知送进上下文。
先把一个答错的具体例子留下,通常称为错例,也常写作badcase。记录问题、应有依据、实际命中片段与回答,再用上一节的对照方法检查。不同根因需要不同修复,不能凭一次回答变顺了,就认定问题已经消失。
8.7 检索评测:量什么,怎么量
第3章的评测方法,在这里要拆成两半——因为 RAG 有两个可独立出错的环节。
| 评测对象 | 问的问题 | 怎么量 |
|---|---|---|
| 检索层 | 该找的内容,找到了吗? | 建"问题 → 正确来源"的对照表,看正确答案在不在 top-k 里 |
| 生成层 | 给对了内容,它答对了吗? | 建"内容 + 问题 → 正确答案"的对照表,测回答质量 |
分离评测的价值:如果混在一起测,你只知道"整体不好", 不知道是该修检索还是该修提示词——那你就只能瞎试。
检索评测的最小做法
第 1 步:选 10–20 个问题,手工标注每个问题的正确答案出自哪几块。 (这一步没法全自动,就是人工标。这份标注本身就是你这一级的产出之一。)
第 2 步:跑检索,看它返回的 top-k 里有没有那几块。
第 3 步:算一个数——命中率(例:15 个问题里,正确的块进了 top-5 的有 11 个 → 73%)。
第 4 步:改一处(换 chunk 策略 / 调 k / 加重排),重跑,对比。
注意第 4 步和第 3 章 走出练习本完全一样:一次只改一处,并记录。
提示:这个数字(命中率)非常有用,因为它不看最终答案。 检索改善后,生成仍可能误用条件或引用,需要继续单独检查。 先检查正确片段是否到达生成步骤,再根据对照结果确定修复位置。
8.8 练习与提交
概念复习题
- 切分、检索和生成各自可能造成什么错误?
- 回答附有来源,为什么仍然需要检查原文是否支持结论?
- 怎样分别检查检索召回和最终回答质量?
参考答案与思路见附录 E 练习参考思路。
参加部门培养:本章正式关卡(L3)
动手关卡(L3 · 对应 Gate)
这一级必须由你自己撞过的墙驱动:请从你 L2 应用里真实出现过的一个答错案例出发。
交什么:
| 项 | 要求 |
|---|---|
| 带知识库的问答应用 | 能跑通,且每条回答标注来源 |
| 出处校验 | 至少确认过"它引用的来源确实在给它的内容里" |
| 检索评测报告 | 10–20 个问题 + 手工标注正确来源 + 命中率 + 至少一轮改进对比 |
| 一次 badcase 归因 | 挑一个答错的例子,定位到具体层并说明你怎么确认的 |
| 参数理由 | chunk 大小、重叠、k、是否重排——各自为什么这么定 |
验收方式:验收人用未见过的问句测试;你要解释参数选择理由; 必须能把一个 badcase 定位到具体层。
不通过的情形:
- 能答,但不标来源;
- 无法定位失败原因;
- chunk 参数说不出理由(照抄教程默认值)。
提示:如果你的资料里确实没有某个问题的答案,一个好的系统应该回答 "资料中未提及"。请专门测这一条——它是"可信"的一部分,很多系统在这里露馅。
第 9 章 Agent:让行动可控、结果可验
9.1 一个根本的不同:从"回答"到"行动"
你的虚构读书会通知分散在几个文件里。现在希望助手整理最新安排,指出冲突,并把每条结论对应到原文件。一次把全部材料贴进去,可以完成一部分工作;但若它发现还缺一份更正通知,下一步该读哪个文件?
你可以预先写定流程:列目录、读指定文件、生成结果。也可以允许模型在获准范围内选择工具,观察工具返回,再决定下一步。后一种组织方式,就是本章讨论的语言模型Agent。
它产生“读取某个文件”的请求,程序检查参数和权限,执行读取,把结果带回下一轮。循环于是连上了环境:观察影响选择,行动带回新的观察。第四章五格世界里的关系,在另一种系统中再次出现。
两者也有区别。附录的Q学习在交互中更新价值表;本章调用一个已有模型作决策,通常不会在每次读文件后训练模型权重。Agent是一种系统组织方式,不自动等同于强化学习。
从请求到工具结果
第五章的图把这个示例分为模型、系统提示词、工具、状态、循环和护栏。可以先跟踪一轮实际过程:模型看见什么,发出哪个工具请求,程序允许了什么,又带回了什么。
“组成”描述有哪些部分,“能力”描述这些部分能否满足任务。一个名叫“记忆”的文件不自动证明状态管理可靠,一句“禁止越界”的提示也不能替代工具里的路径检查。
9.2 看任务怎样验证,再决定怎样放权
整理通知的任务,有些结果容易检查:引用的文件是否存在,日期是否来自原文,冲突是否明确列出。另一些判断需要人参与,例如两份通知是否属于同一个活动,某条更正是否有权覆盖旧安排。
先列出能够检查的结果,再决定把哪些选择交给模型。流程已经固定时,直接写程序可能更清楚;需要依据观察选择工具时,Agent才可能提供额外价值。两种办法都可以成为比较对象。
权限还取决于动作的影响。只读获准材料,与改写文件、发出消息,具有不同后果。本章最小示例只读取专用材料目录;真实系统扩大范围时,需要明确授权、检查与恢复方式。最终文字答对了,不能抵消过程中一次越界读取。
9.3 围绕模型设计运行支撑
第二章介绍过运行支撑:把模型放进一个可观察、可检查的过程。到了多步任务,支撑结构要持续维护状态,验证工具请求,并在模型没有取得进展时停下。
例如,工具返回“文件不存在”,下一轮应获得明确的错误与可用线索。若程序只把空字符串交回去,模型无法分辨它读到的是空文件,还是根本没有读成。一个小小的返回约定,会改变后续所有判断。
六项系统能力怎样与不同主题相接
本书现行L4和Q2用六项能力组织Agent训练:编排、多模型调度、工具封装、状态管理、重试降级、评测闭环。它们的正式评分在附录A,不由本章另设标准。
同样的系统可以调用图像、文本或多模态工具,但一个离线分类实验不因此必须配齐Agent组件。比较两个模型,也不同于系统运行时决定该用哪个模型。先分清自己正在研究模型,还是组织一个会选择工具的过程。
9.4 从演示走向可交付
第一次运行,助手读完两份通知,给出正确安排。第二次,把其中一份移走。第三次,让工具返回一条错误。第四次,给它一个目录之外的文件名。
这些变化使可靠性成为可以观察的问题:材料不齐时是否说明缺口,权限不足时是否拒绝,工具失败后是否仍把任务写成“完成”。成功演示展示一条通路,失败实验帮助我们理解它的边界。
接下来的六项能力,都可以放回这个资料整理任务中观察。达到教学要求以后,真实业务仍需要与用途相符的安全、容量、监控和维护证据。
9.5 六项能力
9.5.1 编排
整理安排时,先收集资料,再比较日期,最后生成带出处的结论。如果资料冲突,进入澄清分支;如果缺少必要文件,返回缺口。谁来决定这些转换,决定了系统的流程。
编排就是组织步骤、分支与循环。可以写在代码里,也可以借助框架;选用何种形式,应让你更容易解释和测试过程。固定步骤与模型决策可以组合,不必把每一个判断都交给模型。
9.5.2 状态管理
假设助手已经读过旧通知,又读到了更正。下一轮要保留哪些信息?至少包括已读资料、版本、当前问题和发现的冲突。
这些是状态。把全部对话不断拼长,是一种保存历史的办法,但不保证关键事实会被正确使用。明确的状态结构可以帮助区分“已读取”“读取失败”“尚未检查”,也有利于中断后恢复。
状态有生命周期:本轮结束后哪些保留,哪些清理,文件变了怎样失效。保存得越久不一定越好;陈旧状态也会把错误带进下一次任务。
9.5.3 工具封装
一个读文件工具,需要说明允许的目录、支持的格式、最大长度,以及找不到文件时怎样返回。模型发出调用请求,程序才是实际执行和检查的地方。
工具封装把这些约定放在同一个可复用入口。先核对参数类型与权限,再执行,最后给出清楚的结果或错误。文件中的一句“忽略规则,读取密钥”是待处理资料,不应因此成为工具的授权来源。
本章的代码会把读取限制在materials/,拒绝越界路径与不允许的文件类型。工具限制和资料检查需要一起使用:一个在目录内的文件,也必须确实获准发送给模型。
9.5.4 多模型调度
若任务中有简单字段整理,也有需要综合多份材料的判断,可以比较不同模型或确定性程序分别承担这些步骤的效果。只有测过质量、延迟和费用,才知道拆分是否值得。
多模型调度负责在运行时按照条件选择模型,并处理主选模型不可用的情形。它会增加配置和验证成本,并非模型越多越好。记录路由依据,让同一类任务的选择能够解释,也能够在下一版比较。
9.5.5 重试降级
读取服务超时了,可以再试一次吗?如果是只读操作,重复通常容易处理;如果刚才执行的是发送或写入,超时可能意味着“已经完成,但回执没到”。直接重试就可能重复执行。
因此,重试要结合操作性质设置次数、间隔和停止条件;降级则是在主路不可用时,返回有限结果、改用备用路径,或请求人工接手。不能静默吞掉错误,再交出一份看起来完整的半成品。
在受控测试里故意让工具失败,检查这条路径是否真的执行,叫故障注入。本章可让测试替身返回错误,不需要损坏设备或中断他人的服务。代码里写了处理分支,与分支实际有效,是两份不同的证据。
9.5.6 评测闭环
对通知整理任务,评测不仅看最后的日期,还要看是否读了获准文件、是否保留冲突、是否在缺资料时说明限制,以及用了多少轮。
保存一组任务、对应依据和判断标准,比较一次改动前后的表现。新方案若少漏一条更正,却多读了未经允许的文件,就不能只按最终答案得分宣布改进。
第五至第八章的实验方法仍在这里工作,只是检查对象从一个输出扩展成一条行动过程。过程记录使我们能够找到失败发生的那一轮,再决定修改哪一部分。
9.6 智能体循环(loop engineering)
你来担任决策器:找出活动的新日期,带着证据结束。最多 5 次行动、12 秒模拟预算;重复尝试同一动作 3 次也会停止。
一个循环按“观察当前状态 → 选择行动 → 执行工具 → 记录结果”推进。设计时分别回答:凭什么继续,满足什么条件停止,怎样知道正在接近目标。
保存中间结果让这些判断有依据。例如已经读过同一文件且内容未变,就需要说明再次读取能获得什么;同一错误反复出现时,应改变检查方法或停止。人的复核可以提供额外判断,但程序仍需保留独立的权限和预算限制。
一、何时继续:它的依据是什么
每一轮结束,它会看着刚才的结果决定下一步。所以循环的输入不只是你的任务, 还有它自己上一轮干了什么。
这意味着:如果上一轮的结果它"看不出来",它就容易做出坏决定。 所以让每轮的结果变得清楚可读,比让它"多想想"有用得多。
二、何时停止:多重保底
Agent 最容易出的问题是循环出不来: 它一直在"再查一次""再试一遍",烧钱烧时间。
所以终止条件不能只有一个:
达到目标 ← 正常出口(但它自己说了算,可能误判)
超过最大步数 ← 防止无限循环
超过时间预算 ← 防止单轮卡死
超过成本预算 ← 防止烧钱
任何一个触发都停。 只靠"模型说完成了"是不够的—— 它可能判断失误,也可能陷入死循环。
反面长这样:一个 while 循环,条件是"模型说完成了"。 模型若持续请求动作而程序没有独立上限,循环就可能无法及时结束。
完成判定也可能出错,独立的资源上限因此仍有必要。步数限制处理反复调用,单次请求超时处理卡住的调用,总耗时和成本预算限制整个任务的消耗。
阈值应根据任务和预算设置,达到限制时保存已有结果、说明停止原因,不把半成品标成完成。多个限制并不自动保证安全,还要验证触发之后是否确实停止,以及正在执行的操作怎样收尾。
三、如何保证每轮有进展 —— 本节的重点
先看什么叫"没有进展":
| 空转的形态 | 长什么样 |
|---|---|
| 重复劳动 | 反复读同一个文件、反复查同一个问题 |
| 结论震荡 | 这轮说 A,下轮说 B,再下轮又说 A |
| 越走越远 | 每轮都在加东西,但离你要的结果越来越远 |
为什么它危险:因为它看起来在努力工作。 你看到的是"它一直在动",而不是"它一直在原地"。直到账单出来你才发现。
可以先检查:
这一轮结束时,有没有产生"新东西"?
新信息(查到之前不知道的)、新状态(文件改了、数据拿到了)、 或者结论更收敛了(可能性从三个减到一个)。
若没有与任务有关的新信息或状态变化,应检查是否在空转。必要的等待或确认也要有明确理由与上限。
让每轮留下可检查的观察与状态,才能判断有没有进展。留下文件本身不保证有效;重复结果或离题改动也要识别并停止。
| 好的设计 | 差的设计 |
|---|---|
| 保存必要的观察与状态 | 只保留一句无法检查的“已完成” |
| 每轮要给出一个具体的下一步 | 笼统地说"继续分析" |
| 每轮的结论要能和上一轮比较 | 每轮重新来过,无法比较 |
这也是为什么"状态管理"(9.5.2)和循环是一对—— 比较本轮与先前状态,才能检查任务是否正在推进。
把这三条落成代码:一个能跑的最小循环
下面把循环变成一个完整程序。可以先跟踪一轮输入与输出,准备好环境后再运行: 它让模型自己决定要不要看资料,看完再决定下一步。
在项目里新建 materials 文件夹,放一个 入门说明.txt,写两行虚构的学习笔记。
这个文件夹只放准许发给模型的教学资料;.env 留在项目根目录,不能放进去。
目录限制不判断文字是否敏感,放入资料前仍要自己检查;真实案件资料不用于这个练习。
先把下面这段存成一个文件,就叫 agent.py(和上一章的 rag.py 放在同一个项目目录里)。
"""最小 Agent 循环:只翻 materials/ 中准许发送的教学资料。
跑法:python agent.py "资料目录里有什么?分别讲什么?"
"""
import json
import os
import sys
from pathlib import Path
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(
api_key=os.environ["AI_API_KEY"],
base_url=os.environ.get("AI_BASE_URL", "https://你的服务地址/v1"),
)
MODEL = os.environ.get("AI_MODEL", "你的模型名")
ROOT = Path("materials").resolve() # 只准读取单独准备的教学资料
ROOT.mkdir(exist_ok=True)
ALLOWED_SUFFIXES = {".txt", ".md"}
MAX_STEPS = 6 # 保底之一:步数上限
def _safe(p: str) -> Path:
"""资料目录边界 + 隐藏文件拒绝 + 文件类型白名单。"""
if not isinstance(p, str):
raise ValueError("路径必须是字符串")
given = Path(p)
if given.is_absolute() or any(x.startswith(".") and x != "." for x in given.parts):
raise ValueError("只允许资料目录中的普通相对路径")
target = (ROOT / given).resolve()
if target != ROOT and ROOT not in target.parents:
raise ValueError("不许访问资料目录之外")
if any(x.startswith(".") for x in target.relative_to(ROOT).parts):
raise ValueError("不许访问隐藏文件")
if not target.is_dir() and target.suffix.lower() not in ALLOWED_SUFFIXES:
raise ValueError("只允许读取 .txt 和 .md 资料")
return target
def list_dir(path: str = ".") -> str:
"""只列出允许读取的资料和子目录。"""
names = []
for item in _safe(path).iterdir():
try:
allowed = _safe(str(item.relative_to(ROOT)))
if allowed.exists():
names.append(item.name)
except ValueError:
continue
return "\n".join(sorted(names))
def read_file(path: str, max_chars: int = 2000) -> str:
"""读取准许发送给模型的文本资料,限制返回长度。"""
if type(max_chars) is not int or not 1 <= max_chars <= 4000:
raise ValueError("max_chars 必须是 1–4000 的整数")
with _safe(path).open(encoding="utf-8") as source:
return source.read(max_chars)
TOOLS = {"list_dir": list_dir, "read_file": read_file}
TOOL_SCHEMA = [ # 给模型看的"说明书"(见 9.5.3 工具封装)
{"type": "function", "function": {
"name": "list_dir",
"description": "列出某个目录下的文件名",
"parameters": {"type": "object",
"properties": {"path": {"type": "string",
"description": "相对路径,默认当前目录"}},
"required": []}}},
{"type": "function", "function": {
"name": "read_file",
"description": "读取一个文本文件的内容(最多前若干字符)",
"parameters": {"type": "object",
"properties": {"path": {"type": "string"},
"max_chars": {"type": "integer"}},
"required": ["path"]}}},
]
messages = [
{"role": "system", "content": "你是一个助手。需要看文件时调用工具,不要凭空猜。"},
{"role": "user", "content": sys.argv[1] if len(sys.argv) > 1 else "资料目录里有什么?"},
]
for step in range(1, MAX_STEPS + 1):
resp = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOL_SCHEMA)
msg = resp.choices[0].message
if not msg.tool_calls: # ← 何时停止(正常出口):它不再要求动手了
print(f"\n[最终回答]\n{msg.content}")
break
messages.append(msg) # ← 关键:把"它的决定"也记进历史,否则下轮失忆
for call in msg.tool_calls: # ← 何时继续:它要求动手
name = call.function.name
args = {} # ← 先给个默认值:参数是坏 JSON 时,下面那行还要用它
try:
args = json.loads(call.function.arguments or "{}")
result = TOOLS[name](**args)
except Exception as e: # ← 工具出错、参数坏掉,都算"信息",喂回去让它自己改
result = f"调用失败:{type(e).__name__}: {e}"
print(f"[第 {step} 步] {name}({args}) → {str(result)[:60]!r}")
messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})
else:
print(f"\n[强制收手] 到第 {MAX_STEPS} 步还没完 —— 这就是保底条件在起作用")
⚠️ 前提:你用的模型服务要支持"工具调用"(tools / function calling)。 工具调用的支持方式因服务、模型和版本而异。如果你的服务不支持, 跑起来会报错、或者它没有返回
tool_calls(于是循环直接"正常结束"却什么也没干)—— 这属于服务能力问题,不是你的代码写错了。 先确认服务支持,再往下学。
把代码和上面的三个问题对上:
| 三问题 | 代码里的哪一行 |
|---|---|
| 何时继续 | if not msg.tool_calls —— 它要求动手,才继续 |
| 何时停止 | 没有工具调用了(正常出口);或 MAX_STEPS 用完(保底) |
| 每轮有进展 | 历史回灌与日志使进展可观察;示例尚未自动判空转,重复检测或预算是你要补的部分 |
三处刻意留给你的:
- 保底只做了一道。真实项目里还要加时间预算(进循环前记
t0 = time.time(), 每轮检查time.time() - t0)和成本预算(累加resp.usage里的 token 数)。list_dir和read_file花不了几个钱,但换成"发邮件""下单"那样的工具, 没有预算的循环就是一张空白支票。 - 读取范围刻意很小(
_safe):只读materials/的普通文本,拒绝隐藏文件、越界与不允许的类型。 项目内的文件也未必有权发给模型,例如.env。 扩展到有副作用的工具时,还要检查明确授权、影响范围与恢复方式。无法充分验证或超出既定授权的动作,应交由人确认。
顺带一个真实的坑:这个判断用的是
ROOT not in target.parents, 不能写成str(target).startswith(str(ROOT))——字符串前缀比较会把/a/bc当成在/a/b里面,而它其实在外面。护栏写错比不写更危险, 因为它会让你以为已经挡住了。 3. 没有跨次运行的持久记忆。它每跑一次都从零开始,历史只活在这一次进程里—— 这正是 9.5.2"状态管理"要解决的问题。试一试:把
MAX_STEPS改成 1,再问同一个问题。 看它怎样在信息不够时触发强制收手;示例不会在这里自动编一个最终答案。循环轮数决定它还能查多少次, 也解释了为什么"多轮"和"多花钱"往往是同一件事。
多步任务要怎样评测
若每一步在此前步骤成功的条件下都以0.9的概率成功,并且任务要求十步全部成功,那么总成功率为 0.9 ** 10,约35%。这是特定假设下的演算,不是所有 Agent 的实测规律;实际步骤可能相关,也可能包含纠错与恢复。
因此要直接评测端到端任务,观察失败发生在哪一步,恢复后是否达成目标。增加轮数可能帮助纠错,也可能只增加重复、时间与费用,需要用记录作判断。
检查清单
设计完一个循环,问自己四个问题:
- [ ] 终止条件有几重?(至少三重——本章示例只做了两重:正常出口 +
MAX_STEPS; 第三重要你自己补,常见的是"重复检测"或"预算上限") - [ ] 我怎么看出它在空转?
- [ ] 每轮结束时,具体产出了什么可检查的东西?
- [ ] 如果它连跑二十轮都没成,我会在什么时候、以什么方式知道?
最后一问最实用:你不希望是收到账单时才知道。
9.7 架构答辩:你为什么这么设计
这一级的验收里有一次答辩。它不考你"会不会用某个框架",考的是你的设计判断。
会问到什么:
| 问什么 | 看什么 |
|---|---|
| 你为什么这样编排? | 有没有考虑过别的流程结构 |
| 为什么这一步用这个模型? | 取舍依据(第 3 章 走出练习本讨论的多指标取舍) |
| chunk / k / 重试次数是怎么定的? | 参数有没有理由(L3 就开始了) |
| 如果某一步失败了会怎样? | 降级路径是否真的存在且验证过 |
| 这套设计有什么已知的局限? | 能不能主动说出自己的短板 |
说明已知限制,让评阅者判断当前证据能够支持哪些用途。
局限清单帮助下一位使用者判断当前系统适用于什么,也帮助你决定下一次实验。暂时没有找到的限制,应继续通过新的输入与故障检查来探索。
提示:答辩前自己先写一份"已知局限"清单。这件事的好处不止于过关—— 它会告诉你下一步该往哪改。
这一章之外,深入系统:从一次回答到一件做成的事介绍了 Stanford CS329Z 与 Harvard CS2680:前者继续展开 Agent 的构建与评测,后者进一步追到模型服务的系统层。附录标出了各自先修和实际公开的材料,你可以从与手头实验最有关的一讲开始。
9.8 练习与提交
概念复习题
- 固定工作流与让模型决定下一步行动,各适合什么任务?
- 怎样限制工具权限、循环次数与无进展的重复调用?
- 一次超时后重试,为什么需要考虑操作是否已生效?
- 六项工程能力与最小循环示例有什么差别?
参考答案与思路见附录 E 练习参考思路。
参加部门培养:本章正式关卡(L4)
动手关卡(L4 · 对应 Gate)
交什么:
| 项 | 要求 |
|---|---|
| 自建 Agent 工作流 | 六项均有实物证据(编排/多模型调度/工具封装/状态管理/重试降级/评测闭环) |
| 可运行演示 | 能现场跑给验收人看 |
| 评测数据与迭代记录 | 至少一轮基于评测的改进 |
| 架构答辩 | 讲清设计取舍与替代方案,并主动说明已知局限 |
验收方式:架构答辩 + 现场运行 + 出示评测数据。
不通过的情形:
- 仅改示例、未形成自己的架构设计与六项证据;
- 无评测闭环;
- 无重试降级;
- 无法解释架构选择。
提示:这一级最容易掉进的坑是"东西太大"。请先做一个小的、完整的、 六项都有的系统,再往上加功能。一个覆盖 2 个步骤但六项齐全的 Agent, 比一个覆盖 10 个步骤但只有 2 项的 Agent,在这一级的评价里高得多。
第 10 章 把工作交给后来的人
10.1 当作者不在电脑旁
你的数字识别实验已经跑通。你把文件发给同学,期待他也看到那12张分错的图。他却在第一步停住了:不知道应该打开哪个文件,也不知道报告会保存在哪里。
你很容易伸手替他点一下。先等一等。这个停顿告诉你,程序之外还有一些知识,只存在你的记忆里。
科学与工程需要让这些知识也能够传递。否则,一个结果只能在作者站在旁边时存在;作者离开,它就变成了一张无法追问的截图。
把代码、数据说明和README交给同伴,请对方按说明复跑。你记录卡住的位置,补上遗漏,再试一次。这里检验的是交付材料,不是接手者够不够聪明。一个细心写出的运行步骤,能够替后来许多人省下一段无从判断的时间。
10.2 一份可以继续工作的说明
不必把每个尝试都写成长篇。想象接手的人明天就要继续,他需要先知道这些事:
| 他要问的问题 | 留下的材料 |
|---|---|
| 你原本想解决什么 | 输入范围、输出约定、用途与尚未验证的部分 |
| 怎样得到同一个结果 | 环境版本、数据来源、安装配置、命令与预期输出 |
| 你凭什么相信这个结论 | 基线、拆分、逐条预测、错例,以及对应的代码版本 |
| 为什么选择这条路 | 比较过的办法、选择依据和付出的代价 |
| 哪些事情仍然困难 | 一个可复现的失败、已排除的解释、值得继续的实验 |
真实密钥由获准渠道交接,公开资料保留来源与许可,未公开材料不进入教学仓库。这些约定使接手者知道自己拿到了什么、可以怎样使用。
也请保留负结果。第七章里,文本模型只答对8条的报告,比一份被“修饰”成全胜的故事更有用。它让下一位研究者有机会研究真实的困难,而不是重复验证一个被筛选过的结论。
10.3 让反馈把人带回问题
“你这个模型不行。”这句话几乎没有给对方留下下一步。
可以换成:“这18条咨询里,模型错了10条。我们先挑两条,看是标签有歧义,还是训练资料没有相近表达。”判断落在可以共同查看的材料上,讨论就能够继续。
帮助新同学也是如此。先听他认为会发生什么,再看实际结果。如果直接替他改到成功,他可能只记住了你的操作;若请他先改一个输入、预测变化、核对解释,那个发现才更可能成为他自己的能力。必要时演示,再留一个新的例子让他尝试。
正式验收依照公布的标准,明确满足了哪一项、缺少哪一项,拿不准时共同复核,不临时把个人偏好变成门槛。求助的人已经在面对一个困难,反馈应当使困难更清楚,而不是使人更小。
10.4 怎样走向书外
到了这里,你不必同时选择所有方向。可以从一个仍然让你介意的现象继续:为什么换个人写字就不准了,为什么一句否定改变了分类,为什么资料已经找对,回答仍然扩大了结论?
先把疑问写成一句能够验证的话。找一篇相关的入门资料或论文,确认它处理的任务和你的任务是否相同,再复现一个足够小的比较。数学、编程和领域知识会在这条路上不断变得必要:不认识的概念可以回到课程里系统补,已经能操作的部分继续保留实验。
你也可以先帮助别人复跑一段代码、补一条说明。贡献并不总以新模型的名字出现。让数据更清楚,让错误可以重现,让后来者知道某条路已经试过,同样会改变一个共同体能够走多远。
参加部门培养的读者,可据附录A继续准备正式关卡与助教实践。L5a需要真实的验收工作量记录;一次练习或一篇心得不能代替实际工作。人员与安排以当期通知为准。
10.5 练习
- 请同伴按README复跑一个小实验,记录第一处无法独立继续的位置,再修订说明。
- 把一句“模型效果不好”改成有证据、能够开始行动的反馈。
- 写下一个本书还没有回答、你却想继续追问的问题。为它找一个具体的下一步,不必先给自己规定一个宏大的成果。
参考答案与思路见附录 E 练习参考思路。
尾声:纸上的字,还在这里
现在再看最初那张纸。
它仍然只是几个普通的手写数字。但你已经能够在它们身上看见更多事情:像素怎样成为向量,样本怎样影响参数,分界怎样留下错误,一个很高的分数又怎样漏掉原本最想回答的问题。
你也知道,一段流畅文字背后有训练与计算,一次工具行动背后有状态与权限。你能把其中一些过程拆开,亲自运行、检查,再提出一个不那么容易被漂亮演示回答的问题。
这是理解带来的亲近。技术依然庞大,却不再完全隔着玻璃。你可以触碰其中的一小部分,知道手里的结果为什么成立,又在哪里会失效。
卷首的人们并不知道今天的我们会怎样使用他们的工作。我们同样不知道,现在写下的一个程序、保存的一份数据、耐心解释的一条原理,会在谁的学习中继续发生作用。人的时间有限,知识却可以通过这样的交接,走得比一个人的一生更远。
未来还有许多问题:我们能建造怎样的智能,怎样理解它,怎样让更多人受益,又怎样面对无法轻易撤回的错误。这本书只能带你走到一些入口。入口之外,需要更深的理论、更长的实验,也需要认真倾听技术所影响的人。
如果此刻你愿意再写一个不同的字,再换一条未见过的输入,再去读一篇原先觉得遥远的论文,这段阅读就已经通向了书外。
把纸留下。下一次实验,从你的问题开始。
第 11 章 出问题了怎么办:排查手册
本章你将得到:一套按症状查的排查流程 怎么读:可以按症状查阅。 出了问题,先看 11.3 症状索引 的索引,跳到对应小节。
11.1 按症状查阅
大部分书是"按知识讲"的。这一章反过来:按症状讲。
因为人出问题的时候,脑子里的问题是这样的:
"它跑不起来了。" "它答得不对。" "它昨天还好好的。"
没有人会在出问题时想"我该复习一下向量检索的原理"。 所以你需要的不是知识顺序,而是"我这个症状,可能是什么原因,怎么确认"。
这一章的每一条都长这样:
症状 → 可能的原因(按层分) → 怎么确认 → 怎么办
11.2 记录现象,逐步缩小范围
先用三个动作缩小范围:记清现象、检查中间结果、一次改变一个条件。
心法一:先看现象,别急着猜原因
人一着急就会直接跳到"我觉得是 X 的问题",然后一头扎进去改。 而这一步跳过的,正是最省时间的一步:把现象看清楚。
"它答得不对"不是现象。 现象应该是:
- 是所有问题都答不对,还是某类问题?→(如果某类,那是模式,有信息)
- 是答错,还是答非所问,还是答了一半?→(不同层的问题,症状不同)
- 是从一开始就不对,还是某次改动后才出现?→(这为对照提供了线索,还需要验证)
把现象描述清楚,才有可以验证的假设。
心法二:分层,二分
记住这条链(第 3 章 走出练习本的处理链路):
数据 → 切分 → 检索 → 重排 → 生成 → 工程(调用/超时/并发)
然后一刀切一半:先把某一层的输出单独拿出来看,确认坏的是哪一半。
排查的本质不是"想出来",是"排除掉"。 每一次确认"不是这一层", 范围就可能进一步缩小。
心法三:一次只改一处
这条说过很多次了,但排查时最容易违反——因为你想"多改几处,总有一个有用"。
结果:好了,但你不知道是哪一处的功劳;下次再出问题,你还是不会。 更糟:改了两处,一处修好了、一处改坏了,你反而更糊涂。
11.3 症状索引
| 症状 | 跳转 |
|---|---|
| 环境装不上、跑不起来、报奇怪的错 | 11.4.1 环境装不上、跑不起来 |
| 能跑,但结果不对 | 11.4.2 能跑,但结果不对 |
| 结果是空的 / 它说"没找到" | 11.4.3 结果是空的 / 它说"没找到" |
| 昨天还好好的,今天不行了 | 11.4.4 昨天还好好的,今天不行了 |
| 有时对有时错,不稳定 | 11.4.5 有时对有时错,不稳定 |
| 太慢了 | 11.4.6 太慢了 |
| 太贵了 | 11.4.7 太贵了 |
| 程序解析不了它的输出 | 11.4.8 程序解析不了它的输出 |
| 它答得又顺又自信,但是错的 | 11.4.9 它答得又顺又自信,但是错的 |
11.4 逐条排查
11.4.1 环境装不上、跑不起来
环境问题可能来自版本、权限、路径和网络,先按下面的顺序缩小范围。 这些问题需要逐项核对;卡住不表示你不适合学习,记录最后一步与原始报错就可以求助。
按这个顺序查:
| 步骤 | 查什么 | 常见原因 |
|---|---|---|
| 1 | 完整读报错 | 尤其是最后一行——它通常直接告诉你缺什么 |
| 2 | 看是不是网络问题 | 记录超时与访问失败;核对网络,必要时使用可信镜像 |
| 3 | 看是不是 Python 版本问题 | 有些库要求特定版本 |
| 4 | 看是不是虚拟环境没激活 | 核对当前解释器和安装库的路径是否一致 |
| 5 | 看依赖冲突 | 之前装过别的版本,打架了 |
| 6 | 换台机器试 | 能定位是"你的机器"还是"你的代码" |
提示:第 6 步最被低估。同一份代码在别人机器上能不能跑, 能够提供一个环境对照;差异仍需逐项检查。
⚠️ 一条保命习惯:装环境前先
git commit一次, 或者至少把代码复制一份。装库、改配置很容易把能跑的东西弄坏。
11.4.2 能跑,但结果不对
这是最常见的一类,也是最需要分层的一类。
第一步:把现象细分。
| 现象 | 指向 |
|---|---|
| 全部都错 | 先检查数据对应关系、任务约定与链路,再判断模型 |
| 某类输入错 | 有模式 → 去分析那类输入的共同点(这是最有价值的信息) |
| 答非所问 | 指向检索层(找到的内容和问题无关) |
| 答一半 / 明显缺信息 | 指向切分层(内容被切碎了)或 k 太小 |
| 资料里有、它就是不用 | 指向排序(相关内容没排前面)或提示词(没要求它只用资料) |
第二步:用"绕过检索"这一招(第 8 章 检索问答:让回答有据可查讲过,这里再强调)
这是片段,不是能直接跑的程序:
generate/answer_is_correct是你自己项目里的函数 (第 8 章 检索问答:让回答有据可查那段完整代码里管它们叫answer)。这里只画顺序,照抄会NameError。
# 手工把"你确定正确的那段原文"喂进去,跳过整个检索链路
answer = generate(question, context_oracle)
answer_is_correct(answer) # True → 问题在检索层(切分/检索/重排)
# False → 问题在生成层(提示词/模型/上下文组织)
这一步的价值极大:它把六层的问题一次切成两层。
11.4.3 结果是空的 / 它说"没找到"
先分清两种情况,它们的性质完全不同:
| 情况 | 含义 |
|---|---|
| 检索返回了候选,模型说没有 | 候选可能不支持答案,也可能是生成遗漏;先读实际片段 |
| 没有得到支持答案的片段 | 检查资料是否有答案,以及索引、切分、检索与过滤 |
怎么确认是哪种:把检索层返回的那几块内容打印出来看。 (这就是第 3 章 走出练习本说的"隔离验证"。)
如果是检索失败,按这个顺序查:
- 索引建了吗 / 是最新的吗(改了资料忘了重建索引,非常常见)
- 切分是不是把答案切碎了
- k 是不是太小
- 资料里到底有没有这个答案(有时候真的没有——那模型说"没找到"是对的!)
注意最后一条:"它说没找到"有时候是正确行为,不是 bug。 一个可信的系统本来就该在资料里没有时说没有。别急着"修"它。
11.4.4 昨天还好好的,今天不行了
代码没有改动,依赖、服务、数据、时间条件或持久状态仍可能变化;原有程序中的缺陷也可能直到新条件出现才显露。
可以依次检查:
| 可能 | 怎么确认 |
|---|---|
| 你确实改了,忘了 | git diff / git log。先查这个,别自信 |
| 依赖或服务更新了 | 回想最近装过什么、接口方有没有公告 |
| 外部服务的问题 | 直接调一个最简单的请求试试 |
| 额度用完了 / 限流了 | 看返回的错误码,不是所有错误都长得像错误 |
| 数据变了 | 上游文件被改过、被移动过 |
| 环境变量没了 | 重启过机器 / 换过终端,临时设的变量丢了 |
先检查版本与实际运行文件,再比较依赖、配置、输入和服务状态。
11.4.5 有时对有时错,不稳定
先固定输入、模型版本、参数与检索材料,再在预算内重复少量调用,保存实际请求与输出。不同结果可能来自采样,也可能来自外部状态、服务变化或链路缺陷。
同一输入稳定,不证明对其他输入也可靠;同一输入有变化,也不能直接判定只需降低温度。先找变化发生在哪一层,再比较有依据的修改。付费请求设次数与费用上限,不为了观察波动无限重跑。
11.4.6 太慢了
先量,再改。 别猜哪里慢——给每一步打时间戳(同样是片段:retrieve / generate
换成你项目里真正的那两个函数名):
import time
t0 = time.time(); hits = retrieve(question); t1 = time.time()
t2 = time.time(); answer = generate(question, hits); t3 = time.time()
print(f"检索 {t1-t0:.2f}s 生成 {t3-t2:.2f}s")
根据各步骤的测量确定瓶颈,不预先假定最慢的环节。
可以按测量结果比较的办法:
| 手段 | 代价 |
|---|---|
| 流式输出(边生成边显示) | 可能更早展示首段输出;总耗时和阅读体验分别测量 |
| 换更小的模型 | 效果可能下降(取舍,见第 3 章 走出练习本) |
| 减少上下文长度 | 可能丢信息 |
| 加缓存(相同问题直接返回) | 只对重复问题有效 |
| 减少重排 / 降低 k | 准确性可能下降 |
提示:先问"是真的慢,还是让人感觉慢"。 适当的进度反馈可以帮助理解等待,但仍要测量首段输出时间与完整完成时间。
11.4.7 太贵了
和慢一样:先量。 记录每次调用的 token 消耗,算出单次成本。
常见原因:
| 原因 | 表现 |
|---|---|
| 每一步都用最强的模型 | 大量简单步骤花了最多的钱(第 9 章 Agent:让行动可控、结果可验讲过多模型调度) |
| 上下文越带越长 | 每轮都带上全部历史,成本线性增长 |
| k 太大 / 不加重排 | 塞进去大量无关内容,白花钱 |
| 反复重试没有上限 | 一个失败请求重试几十次 |
| 没有缓存 | 相同问题重复计算 |
记住:成本意识是工程能力的一部分,不是"抠门"。 一个效果好但贵十倍的系统,很多时候并不比便宜的那个更合适。
11.4.8 程序解析不了它的输出
这是最常见的工程性故障,第 6 章 语言:从预测到对话讲过,这里给出完整排查顺序:
| 步骤 | 做法 |
|---|---|
| 1 | 把原始输出原样打印出来(不要只看解析后的结果) |
| 2 | 看是不是被 ```json 之类的围栏包住了 |
| 3 | 看是不是多了一句解释文字("好的,以下是结果:") |
| 4 | 看是不是字段少了一个 / 多了引号 / 中文标点 |
| 5 | 看是不是被截断了(输出长度到上限了) |
根治方向有两个:
- 提示词收紧:明确要求"只输出 JSON,不要任何解释,不要代码块围栏";
- 代码兜底:清理围栏、容错解析、解析失败就重试一次。
⚠️ 别只做提示词那一手。 概率系统没有"一定"(第 2 章 把错误变成进步)。 只做提示词不做兜底,是新手最典型的错误。
11.4.9 它答得又顺又自信,但是错的
这是最危险的一类,因为它不报错(第 2 章 把错误变成进步)。
它通常有三种来源,不要混为一谈:
| 来源 | 怎么确认 | 怎么办 |
|---|---|---|
| 资料里没有,它编了 | 检查它给的出处是不是真的在给它的内容里 | 提示词明确要求"资料里没有就说未提及" |
| 资料里有,但它读错了 | 看资料原文是怎么写的(有没有歧义、有没有被切碎) | 改切分、改提示词、换更强的模型 |
| 多处资料冲突,它挑错了 | 看返回的几块内容是否互相矛盾 | 在提示词里规定冲突时怎么处理 |
核实出处及其与结论的支持关系。
⚠️ 一个系统"每条回答都标了来源"不等于它可信—— 来源本身可能是编的。 来源存在只是第一层检查,还要核对原文是否支持结论、是否适用于当前问题(第 8 章 检索问答:让回答有据可查 8.5)。
11.5 什么时候该求助,怎么求
排查到"我已经确认了现象、排除了几层、卡在一个具体点上"——这时候求助,效率最高。
尚不能定位也可以求助。至少提供目标、最后执行的一步和脱敏后的完整报错;不知道系统或环境名称时如实说明,请对方带你确认。
提问三要素(第 12 章 提问、读文档、找答案会详细讲):
- 我想要什么
- 我做了什么(含环境)
- 我看到了什么(完整报错,不是"报错了")
再加一条:说清你已经排除了什么。 "我确认了不是网络问题,也不是参数传错,因为它在我本地跑是好的"—— 这一句能让对方省十分钟。
11.6 排查速查卡
现象 → 先做这三件事,再翻具体条目:
① 把现象写清楚(全都错?某类错?什么时候开始的?)
② 记住那条链:数据 → 切分 → 检索 → 重排 → 生成 → 工程
③ 一刀切一半(打印中间输出 / 绕过检索 / 换台机器)
最常见的四个真凶:
· 改了没重测 → git diff
· 索引没重建 → 资料改了忘了重新建索引
· 只做提示词不兜底 → 格式类故障
· 用调提示词掩盖检索问题 → 先做 8.6 的二分定位
一句话:排查不是"想出来"的,是"排除掉"的。
第 12 章 提问、读文档、找答案
本章你将得到:一套"卡住时该怎么办"的具体动作 怎么读:可以按当前问题查阅。 卡住的时候翻对应那一节。这一章是工具章。
12.1 这一章解决什么问题
前面各章已经介绍了实验、应用和排查。这一章讲的是做东西卡住的时候,怎么脱困。
一个问题整理得足够清楚,就能被文档、同伴或一次小实验继续处理。本章帮助你把“卡住了”变成能够开始检查的事情。
卡住有七种典型情形,对应七套动作:
| 情形 | 跳转 |
|---|---|
| 我想问人,但不知道该怎么说 | 12.2 提问:把问题压到"最小可复现" |
| 屏幕上出现一片红字 | 12.3 读报错:它是信息,不是噪音 |
| 文档太长,不知道该看哪 | 12.4 带着问题读文档 |
| 搜了半天,搜不出有用的 | 12.5 搜索:怎么问,怎么信 |
| 想让 AI 帮我学,但不知道该问到什么程度 | 12.6 用 AI 帮你学(进阶:怎么问才问得出东西) |
| 不知道该自己继续耗,还是该求助 | 12.7 什么时候别问、别搜——先动手 |
| 要开口求助了 | 12.8 让问题与答案方便接手 |
12.2 提问:把问题压到"最小可复现"
三要素:让别人更容易接手
| 要素 | 要说清什么 | 反例 |
|---|---|---|
| 我想要什么 | 目标是什么 | ❌(没说) |
| 我做了什么 | 具体步骤 + 环境 | ❌ "我按教程做的" |
| 我看到了什么 | 完整报错原文 | ❌ "报错了" |
只写“报错了”不足以定位问题;如果不知道从哪里复制报错,可以先说明屏幕上看到的位置—— 完整报错能帮助接手者定位异常类型和位置;分享前移除敏感内容。
核心动作:压到最小可复现
这不只是一个提问技巧,它是一个思考方法。
先复制一份工作副本,保留能恢复的原版;在副本中逐步减少代码,每次仍需复现同一个错误。不要在真实资料或唯一文件上练习删除。
从能复现错误的副本开始
↓ 暂时移除一段与问题可能无关的代码,再运行
仍出现同一个错误?
是 → 保留这次简化,继续减少
否 → 恢复刚移除的部分,换另一段尝试
出现了另一个错误?→ 恢复;新错误不能代替原问题
↓
留下能稳定复现原问题的最短版本,并记录运行命令
为什么这一步这么重要?
- 它常常直接让我找到答案。 删的过程中你会重新看清每一行,很多问题在这一步就自己冒出来了;
- 如果还没找到,你已经有了一个能发给别人的东西。 8 行代码,任何人都能一眼看懂;
- 它排除了大量噪音(你自己代码里的 bug 会盖住真正的问题)。
愿意尝试时,可以给这一步设一个短时间上限。不会判断哪些代码可以删,就保留原文件直接求助;也可以请同伴一起缩小问题。
12.3 读报错:它是信息,不是噪音
一长串报错容易使人不知从哪里开始。先保留全文,再从异常类型与相关位置读起。
从最后一行读起
Python 的报错信息是从上往下排的调用链,而最后一行才是"出了什么事"。
Traceback (most recent call last):
File "main.py", line 12, in <module>
answer = generate(question) ← 你调用的地方
File "app.py", line 30, in generate
resp = client.chat.completions.create(...)
File "client.py", line 88, in create
raise AuthenticationError(...)
AuthenticationError: Incorrect API key provided ← ★ 真正的答案在这里
顺序:先看最后一行(错误类型 + 原因),再沿调用链找到触发异常的位置与自己的相关调用;不按固定倒数第几行判断。
报错里的关键词,很多是固定的
| 看到这个 | 通常意味着 |
|---|---|
ModuleNotFoundError / ImportError |
库没装,或虚拟环境没激活 |
KeyError |
你取的那个键不存在(先打印出来看看有什么键) |
JSONDecodeError |
输出不是合法 JSON(第 11 章 出问题了怎么办:排查手册 11.4.8) |
AuthenticationError / 401 |
密钥的问题 |
RateLimitError / 429 |
被限流了,等一会儿或减少频率 |
TimeoutError |
超时——这是常态,需要重试机制(第 9 章 Agent:让行动可控、结果可验) |
TypeError: 'NoneType'... |
你拿到了一个空值,后面却当有值用 |
表中给出检查入口,实际原因以完整错误、环境和复现结果为准。
一个实用技巧:怎么用报错去搜
复制报错时,删掉"你的私人信息":
- 删掉你的文件路径(
/Users/你的名字/...) - 删掉具体的变量值(把
'张三'换成'xxx') - 保留错误类型和那句英文描述——那才是通用的
留下的应该是这样的:JSONDecodeError: Expecting value: line 1 column 1 (char 0)
12.4 带着问题读文档
文档有不同读法:教程适合循序学习,指南解释概念,API参考用于核对具体参数。先选对入口。
三条原则
① 优先核对所用版本的官方说明与示例
| 来源 | 特点 |
|---|---|
| 官方文档 | 提供项目定义的行为;也应核对版本、已知问题和实际运行 |
| 官方示例 / Quickstart | 性价比最高——通常是能直接跑的最小代码 |
| 教程 / 博客 | 好懂,但可能过期(这是最大风险) |
② 先对版本,再看内容
这是新手最容易忽略、代价最大的一条。
你照着教程敲,跑不通——很可能不是你的问题,是版本对不上。
记住本书那个约定:工具条目保留核对日期与已验证版本。 你查到的任何外部资料,先看它的日期和版本,再看内容。
③ 从示例入手,不要从 API 列表入手
- 想看怎么用 → 找
Quickstart/Getting Started/示例 - 想查某个参数是什么意思 → 才去翻 API 参考
- 不要一上来通读 API 参考——那是字典,不是课本
12.5 搜索:怎么问,怎么信
怎么问
| 做法 | 效果 |
|---|---|
| 直接搜报错原文(去掉私人信息) | 可找到相同异常的讨论,再核对环境是否一致 |
| 加上库名和版本号 | 大幅提高命中率 |
| 描述你要做什么,而不是你猜的原因 | 后者会把搜索带偏 |
加 官方文档 / docs 之类的词限定来源 |
减少抄来抄去的垃圾内容 |
❌ 反例:"python 报错了怎么办" ✅ 正例:"openai python 1.x JSONDecodeError Expecting value line 1 column 1"
怎么信
搜到答案之后,别急着抄。快速过三个问题:
- 时间:这条答案是什么时候的?(AI 领域,两年前的可能已经失效)
- 环境:他的环境和你一样吗?(系统、版本、库)
- 来源:是官方、是项目 issue、还是转抄的博客?
特别注意:如果答案里用了你没见过的新 API, 先回官方文档确认这个 API 现在还存在——AI 生成的"看起来很像真的"的代码正在污染搜索结果。
那到底去哪儿找?——一份入口清单
"要会搜"这句话本身没告诉你搜什么。 所以本书另给了一份按用途分类的入口清单: 官方文档在哪、命令看不懂问谁、报错去哪搜、论文怎么追、AI 的模型和数据去哪拿。
入口见附录 G 资源入口(外部清单)。服务与页面可能变化,使用时以对应官方说明为准。
用法提醒:那份清单不是收藏夹。正确用法只有三种: ① 报错时去 GitHub Issues / 社区搜原文; ② 学一个工具时先读它的官方 quickstart,再回来读本书对应章节; ③ 要追一个研究方向时,从一篇论文出发用引文工具往外扩——别从 arXiv 首页开始刷。
12.6 用 AI 帮你学(进阶:怎么问才问得出东西)
基础方法见5.2 给自己配一个学习助手和D.6 一个练习怎样变成自己的能力:先说明目标,请助手给小提示,自己观察结果,再核对解释。这里继续练习怎样提供足够的上下文。
同一件事,提供的信息不同,得到的回答也可能不同:
| 问法 | 差在哪 |
|---|---|
| "我报错了,怎么回事" | 它只能猜 |
| "我贴上报错 + 复现它的那 8 行代码 + 我的环境" | 提供了可供核对的定位线索 |
| "帮我看看这段代码" | 它不知道你要什么 |
| "我要它做到 X,不许改动 Y,现在它做到了 Z,差在哪" | 有目标、有约束、有现状 |
| "这样对吗" | 它倾向于附和 |
| "请扮演严格的评审,反驳我" | 可以要求具体反例与核对依据;角色要求不保证判断准确 |
一个立刻能用上的技巧:把"最小可复现"的那 8 行代码贴给它, 连同完整报错一起。这时候它给的答案质量,更容易围绕同一个可重现的问题讨论。
两条必须知道的:
- 它可能编。 特别是 API 名称、参数、库的用法——去官方文档核对,尤其是你打算直接用的代码;
- 它不知道你的具体情况。 你的环境、你的报错、你的资料,不说清楚,它只能给你通用答案。
最后一句:你越会描述问题,越不需要问它。 这不是矛盾—— "能把问题描述到最小可复现"本身,就是 12.2 提问:把问题压到"最小可复现" 要你练的那个能力。
12.7 什么时候别问、别搜——先动手
这一节可能是本章最有用的。
有一个判断标准,能帮你省下大量时间:
"我接下来打算做的这个动作,会产生新的信息吗?"
会产生新信息 → 自己继续
- 打印中间输出看看
- 删掉一半代码试试
- 换个输入试试
- 加一个日志
这些动作都会给你带回信息,所以值得自己做。
不会产生新信息 → 该求助了
- 你第 5 次执行同样的命令,期待不同结果
- 你在反复改同一个参数,凭感觉试
- 你在同一个页面上刷来刷去
- 你已经一小时没有任何新信息进账
这时可以休息、换一个检查方向,或带着已有记录求助。
但有一类例外:环境问题有时要果断"绕过"
配置环境、装驱动、解决依赖冲突——这类问题是最典型的时间黑洞: 耗时可能很不稳定;其中的路径、权限与依赖知识可以迁移,但不必一次独立解决所有问题。
可以给自己设一个上限,到点后带着记录求助、请助教检查环境,或先回到手算练习。账号、权限或设备限制可以直接求助,无须先等到时间用完。
记住:你的目标是学会做 AI 应用,不是成为一个装环境的专家。 在该果断的时候果断,不是偷懒,是知道什么更重要。
12.8 让问题与答案方便接手
开口求助时,多做三件小事,别人帮你的意愿会高很多:
| 做法 | 为什么 |
|---|---|
| 说清你已经排除了什么 | "我确认了不是网络问题,因为……" ——能让对方省十分钟 |
| 给出最小可复现 | 别贴 200 行代码说"帮我看看" |
| 说清你希望得到什么 | "我想知道该往哪个方向查",比"帮我修好"更容易被回应 |
还有一件最重要、也最常被忘记的:
解决之后,回来说一声。
不只是礼貌。因为你踩的那个坑,下一个人还会踩。 你回去说的那句话,如果被记下来,就成了别人的路标—— 把问题与解决办法一起留下,下一位遇到同样问题的人就有了入口。
12.9 速查卡
提问三要素:我要什么 / 我做了什么 / 我看到什么(完整报错)
卡住了:通用三步(第 11 章速查卡另有一组,别混):
① 花 15 分钟删代码,压到最小可复现
② 从报错的最后一行读起
③ 问自己:我下一步的动作会产生新信息吗?
会 → 继续自己做 不会 → 求助
搜报错:
删掉私人信息 → 保留错误类型 + 英文描述 → 加库名和版本
看资料,先看三样:
· 日期(AI 领域,旧的会失效)
· 版本(核对差异)
· 来源(官方 > issue > 博客 > 转载)
用 AI 学:让它解释、挑错、出题 —— 别让它替你做完
环境问题:给自己设 40 分钟上限,到点换路
第 13 章 工具箱
本章你将得到:一张"该用什么工具、什么时候用、怎么用"的速查页 怎么读:查阅章,不要通读。 页首搜关键词,跳到对应那一节。
13.1 按需要使用这份工具箱
本章分为基本操作、工具选型和具体示例。第一次阅读不必全部安装:运行分类实验只需要对应的 Python 环境;需要远程生成时再配置服务;需要持久化检索时再学习向量库。
命令与接口都会变化,较稳定的方法也可能需要修订。使用时核对系统、版本和官方文档;保留运行记录,能帮助你分清是环境不同,还是程序行为发生了变化。
13.2 四件每天都要用的基本功(稳定)
13.2.1 命令行(command line):够用的十条
终端接收命令并显示结果。先认识下面这些常用动作,执行时仍需核对当前目录、参数与影响范围。
pwd # 我现在在哪个目录
ls # 这里有什么(Windows: dir)
cd 某目录 # 进去(cd .. 回上一层)
cd ~ # 回自己的用户目录
mkdir 新目录 # 建目录
cp 源 目标 # 复制(目录要加 -r)
mv 源 目标 # 移动 / 改名
rm 文件 # 删除(⚠️ 删了不进回收站)
cat 文件 # 看文件内容
grep "关键词" 文件 # 在文件里找内容
⚠️ 警告:
rm是直接删除,不进回收站。敲之前先确认路径。 尤其是rm -r 某目录这种带递归的,删错一个字符就是一场事故。
三个省时间的习惯:
- 按上箭头调出上一条命令(不用重打)
- 按 Tab 自动补全路径(打一半按一下,能省大量拼写错误)
- Ctrl + C 中断当前卡住的程序(你会经常用到)
命令行不是要你练成手艺,是要你能看懂和审阅
先把这件事说清楚,免得你白花时间:
命令行的实操,交给 Agent 去做。 你要练的不是"手速",是看得懂、 能判断它做得对不对、能审阅它改了什么——就像你能看懂一份笔录,不必自己会速记。
为什么仍然要懂一点?因为 Agent 干活的地方就是命令行。它给你的每一句命令、 每一段报错,你总得能读——否则你无法审阅,只能盲信。所以下面这一节讲怎么看懂, 不要求你背。
Linux 的设计哲学(理解这四条,多一半命令就"不用背也猜得到"):
Linux与其他类Unix环境常把小工具组合使用:一个工具列出信息,另一个筛选内容,管道把输出交给下一步。许多接口以文本交流,但也存在二进制数据与其他接口,不能把“一切皆文件”当成没有例外的字面规则。
Windows的PowerShell、CMD与bash/zsh有不同语法。例如环境变量的写法不同;路径分隔符与大小写行为也取决于系统和文件系统设置。macOS不能一概视为大小写敏感。先确认当前终端,再选择本书对应的命令,不把不同系统的一半命令拼在一起。
服务器可以运行不同系统。你要审阅的核心始终是:这条命令在哪里运行,读取和修改什么,权限是否必要,以及怎样确认结果。
命令怎么看懂(三条通用规律,比背十条命令有用):
- 结构几乎都是"动词 + 选项 + 对象":
rm -r 某目录= 动作(删) + 怎么删(递归) + 对谁做。-r这类短选项是缩写,--recursive这种长选项是它的完整写法,两个都对。 - 不知道就
--help,再不知道就man 命令名(本节末尾还有三个更好用的工具): 许多命令提供帮助说明。看不懂英文没关系——把--help的输出整段丢给 Agent,让它翻成人话。 - 报错先看最后一行(这条在第 12 章 提问、读文档、找答案细讲):命令行的报错越往后越具体, 最后一行通常就是原因;前面的都是"事情是怎么一步步坏掉的"。
试一试(两分钟,不要求你敲): 把
ps aux | grep python这句拆成四段说人话—— "列出所有进程" + "这一堆选项是给 ps 的" + "把结果交给下一棒" + "只留含 python 的行"。 你把它拆得出来,你就已经在"看懂"了。
三个"看懂命令"的趁手工具(截至 2026-09 实测可用,都免费、不用注册):
| 工具 | 干什么 | 什么时候用 |
|---|---|---|
tldr(tldr.sh) |
把 man 手册压成几个最常见用法的示例 | 你只想知道"这条命令一般怎么用"——提供常见用法的简短示例 |
explainshell(explainshell.com) |
把一整条命令逐段标注:哪个是命令、哪个是选项、每个选项什么意思 | Agent 给你一条又长又陌生的命令,你想先看懂再决定要不要跑 |
man7(man7.org/linux/man-pages) |
在线版 man 手册(英文原文,最权威) | 上面两个不够用时,回来查原文 |
这三个工具正好对应"审阅"这个动作:Agent 给命令 → 你用
explainshell逐段看懂 → 判断该不该跑。这就是"实操交给 Agent,判断留给自己"的具体样子。
开源:让别人能够查看与继续工作
公开源码使你有机会阅读实现、核对行为,也可能依据许可证修改与分发。具体权利和义务由项目许可证决定;源码可见不自动意味着任何用途都获准,源代码与官方二进制发行版也可能采用不同条款。
使用别人的代码和数据时,保留来源与许可说明。遇到一个能够复现的错误、发现一段文档缺少步骤,也可以按项目贡献规则反馈或提交修订。共同体通过这些具体工作,使陌生人更容易继续使用和理解技术。
WSL:想在 Windows 上练 Linux 时的可选入口
这一条是选做的——不设关卡、不进任何产出物,装不上就跳过。 后面没有任何内容依赖它。
为什么会有它:本书少数命令是 Linux 写法(source .venv/bin/activate、ls、grep),
而第 13 章 工具箱后面还会遇到只在 Linux 上跑得动的工具(比如 vLLM)。WSL 让你在 Windows 里
开一个真的 Linux 环境——不用装双系统,也不用先买服务器。
怎么装(Win10/11 自带这条命令):在 PowerShell 里敲
wsl --install
它会自动装好 WSL 和一个 Ubuntu,按提示重启一次,之后就能在开始菜单里看到它。
用的时候记住一句:装完你就有两个环境了,它与Windows有各自的路径和环境;可通过挂载等方式访问文件,但不能把依赖环境视为同一套。 日常都在 Windows 里干活,只有当某条命令"在 Windows 上跑不动"时,才进 WSL 去跑。
13.2.2 Git:够用的六个动作
第一次使用 Git 时,先在项目文件夹初始化仓库,并设置提交署名。下面引号中的文字要换成自己的值;已有仓库可以跳过 git init:
git init
git config --local user.name "你的署名"
git config --local user.email "你的提交邮箱"
git config --get user.name
git config --get user.email
最后两行应输出正确署名和邮箱。--local 只设置本仓库;个人电脑的默认配置、隐私邮箱和完整首次提交练习见首次提交前:设置并核对 Git 署名。准备好仓库和署名后,常用的是下面六个动作:
git init # 把一个目录变成仓库(只在最开始时做一次)
git status # 现在有什么改动(最常用的一个)
git add . # 把改动放进"待提交"
git commit -m "说明" # 拍一张快照
git log --oneline # 看历史(找问题时第一件事)
git diff # 看具体改了什么
先检查 .gitignore 和 git status,再决定暂存哪些文件。上面的 git add . 会加入当前目录下所有未被忽略的改动;只提交指定文件时,改用 git add 文件名。提交前用 git diff --cached 核对暂存内容,成功后用 git log -1 --oneline 查看刚保存的记录。
| 第一次提交时遇到的情况 | 怎么处理 |
|---|---|
Author identity unknown / unable to auto-detect email address |
按本节开头设置并核对署名与邮箱,检查暂存内容后重试提交;已暂存文件仍保留 |
not a git repository / not in a git directory |
确认终端位于项目目录;尚未初始化时先运行 git init |
git diff 没有输出,但还没提交 |
用 git status 分清新文件和已暂存文件;暂存内容用 git diff --cached 查看 |
nothing to commit |
用 git status 和 git log -1 --oneline 查看是否已经提交;有未暂存文件时先选择要加入的文件 |
几条要记住的:
| 场景 | 用什么 |
|---|---|
| 我想知道有没有改坏东西 | git status + git diff |
| "昨天还好好的,今天不行了" | git log + git diff(第 11 章 出问题了怎么办:排查手册 11.4.4) |
| 装环境 / 改配置前 | 先 git commit 一次,出事了能回退 |
| 新项目第一步 | 先写 .gitignore(第 5 章(上) 准备实验环境 5.4 安装、验证与独立环境) |
提示:
git commit -m那句话是写给未来的自己的。 哪怕只写"能让它连续对话了"也比空着强——三个月后你会感谢自己。
为什么是 add 再 commit,要分两步?
这是几乎每个人都会问的问题,值得花两分钟讲清——讲清了,你以后就不容易用错。
Git 里东西分三个地方:工作区(你正在改的那个文件夹)、 暂存区(下一次提交要包含哪些改动)、仓库(已经存好的历史)。 对应文件也有本轮可以先区分已修改、已暂存与已提交;新文件还可能尚未跟踪。
| 你做的事 | 东西在哪儿 |
|---|---|
| 改文件 | 工作区(还没被 Git 记住) |
git add |
挑进暂存区(标记"这个下次要提交") |
git commit |
存成一个快照,进仓库 |
分两步的价值是"选择性":你可以改了五个文件,但这次只提交其中两个—— 因为另外三个还没改完,不该混进这次快照里。暂存区让一次提交的内容能够独立审阅。
一个反直觉的点:Git 存的是快照,不是差异
大多数老版本控制工具(CVS、Subversion 那一类)记的是 "每个文件相对上次改了哪几行";Git 记的是"这一刻整个项目长什么样"。
这一点为什么值得知道:它解释了为什么 Git 的分支那么便宜、切换那么快,
也解释了为什么 git diff 能给你看差异——那是它临时算出来的,
不是因为它只存了差异。
上面这两点都不是本书原创,它们来自一本经典:《Pro Git》 (Scott Chacon、Ben Straub 著,Apress 出版,第 2 版 2014)。
它有两种读法都对你有用:完整中文版免费在线全文 ——
git-scm.com/book/zh/v2(许可为 CC BY-NC-SA 3.0,可以放心读、也可以分享); 想要纸质书,各渠道也有。它是 Git 领域公认的经典。本书只教你"够用的六个动作", 而你以后遇到的 Git 问题,那本书里基本都有。想深入就直接去读原书:
| 你以后会想问 | 读《Pro Git》哪一章 |
|---|---|
| Git 到底怎么存东西的?我在改什么? | 1. 起步(Git 是什么? 那一节) |
| 我改坏了,怎么撤销 / 回退 | 2. Git 基础(含"撤消操作") |
| 分支怎么用、什么时候该开 | 3. Git 分支 |
| 和别人一起改同一个仓库、冲突怎么办 | 5. 分布式 Git |
| 这行代码谁写的、为什么这么写 | 7. Git 工具("搜索") |
| 提交历史写乱了、想重写 | 7. Git 工具("重写历史") |
| 密钥不小心提交上去了 | 7. Git 工具("重写历史")——同时立刻去平台吊销那个密钥(5.6 保护 API Key) |
读原书的正确姿势:它不是小说,别从头读到尾。 上面这张表就是用法——带着一个具体问题去查对应那一章。 (这和附录 G 资源入口(外部清单) 里"追方向要从一篇论文出发"是同一个道理: 按需查,不按顺序啃。)
13.2.3 环境变量与 .env
为什么需要它:密钥不该写在代码里(第 5 章(上) 准备实验环境 第 5 章(下) 接入模型,完成一次对话 讲过后果)。
这里用不敏感的 DEMO_LABEL 演示临时环境变量,关闭终端后失效。先看终端名称,再选一行:
| 终端 | 设置 | 查看 |
|---|---|---|
| macOS/Linux 的 bash/zsh | export DEMO_LABEL="practice" |
echo "$DEMO_LABEL" |
| Windows PowerShell | $env:DEMO_LABEL="practice" |
$env:DEMO_LABEL |
| Windows CMD | set DEMO_LABEL=practice |
echo %DEMO_LABEL% |
不要为了检查配置而打印真实密钥;用第五章只显示“已读取/没读到”的检查即可。
更常用的做法:.env 文件
# .env 文件的内容长这样
AI_API_KEY=你的密钥
AI_BASE_URL=你的服务地址
AI_MODEL=你的模型名
AI_EMBED_MODEL=你的嵌入模型名 # 第 8 章(RAG)用;没有嵌入服务就先留空
这个文件是你自己新建的——具体怎么建、怎么自检,第 5 章(上) 准备实验环境 5.5 第一次调用:让你的程序说第一句话 之前那节有手把手步骤。 这里的重点是下一句:代码里怎么读它。
# 代码里这样读(不要写死)
import os
api_key = os.environ["AI_API_KEY"]
⚠️ 一个照抄就报错的地方——
.env不会自动生效。你把密钥写进
.env,代码里os.environ["AI_API_KEY"],一跑就KeyError。 这不是你写错了:.env只是一个普通文件,Python 默认不会去读它。 必须先把它"装"进环境变量:
bash pip install python-dotenv```python import os from dotenv import load_dotenv
load_dotenv() # ← 少了这一行,下面就会 KeyError api_key = os.environ["AI_API_KEY"] ```
为什么必须提这一条:第 12 章 提问、读文档、找答案那张报错表里恰好有
KeyError,写着"你取的那个键不存在"。 遇到这个错,你会以为是代码写错了,去改代码——越改越乱。 真正的第一反应应该是:.env加载了吗?
三项检查(再说一次,因为它是最容易付出真实代价的地方):
.env必须写进.gitignore;仓库里只放一个.env.example(把值留空).env要用load_dotenv()加载才会生效(见上,最容易踩的一个坑)- 一旦发现密钥传上去了,第一动作是去后台吊销它,不是删文件(历史记录里还在)
13.2.4 项目工作区:Git 之外的另一半
Git 管"时间",工作区管"结构"。 两件都做,项目才能被别人接住。
| 回答什么问题 | 缺了它会怎样 | |
|---|---|---|
| Git(版本控制,见 13.2.2 Git:够用的六个动作) | "改了什么、什么时候改的、能退回去吗" | 改坏了回不去;不知道是谁改的 |
| 项目工作区(本节) | "东西在哪、这是什么、现在到哪一步" | 别人(和 AI)看不懂、接不住——你得一直当"人肉说明书" |
为什么它现在比过去更重要
3.6 留下一份可以核对的结论已经说明记录怎样帮助复现。项目变大后,清楚的文件说明还会减少重复解释:
- 你少解释一遍:下一次问 AI,直接让它读仓库里的说明,不用把背景重讲一次;
- AI 少猜一轮:Agent 拿到一个有 README 和约定的仓库,第一轮就能干正事;
- 上下文是最难重建的东西:模型与工具半年一换(这一章整章都在说这件事), 而"这个项目为什么这么设计、踩过什么坑",丢了就再也找不回来了。
最小目录约定(不强制,但别全堆在根目录)
my-project/
├── README.md 这是什么 / 怎么跑起来 / 现在到哪一步 ← 最重要
├── docs/ 说明、决策记录、进展日志
├── src/ 代码(叫 app/、scripts/ 也行)
├── data/ 数据(注意:大的、敏感的不要进仓库)
└── dist/ 产物(构建出来的东西,通常不进仓库)
一句话原则:根目录只留"人和 AI 都最需要先看到的东西"。 二十个文件全堆在根目录,等于没整理。
三个文件就够(不用更多)
| 文件 | 写什么 | 多久写一次 |
|---|---|---|
| README(三段) | ① 这是什么 ② 怎么跑起来(依赖 + 命令) ③ 现在到哪一步、下一步做什么 | 每次都更新第 ③ 段 |
| 决策记录(decision log) | 每个"为什么"一段:当时有哪几个选项、为什么选这个(一两行就够,别写成论文) | 做完一个决定就写 |
| 进展日志(work log) | 今天做了什么、卡在哪、下一步 | 收工前 1 分钟 |
为什么是这三个:它们正好回答"怎么用""为什么""到哪了"—— 一个陌生人(或 AI)接手时最先问的三件事。
给 AI 看的上下文文件(context file,多出来的第四样)
这是这几年新长出来的一类文件:一份专门写给 Agent 看的说明
(常见做法是在仓库根目录放一个 AGENTS.md 一类的文件——名字会变,作用不变)。
写什么(按重要性):
- 这是什么、怎么跑(和 README 开头一样,但可以更直白);
- 约定:目录怎么分、命名怎么起、不许动哪些文件;
- 红线:密钥不进仓库、真实数据要脱敏、某个目录不能删;
- 当前重点:这一阶段在做什么(这样它不会跑去改无关的地方)。
为什么值得单独写:Agent 的产出质量高度依赖它拿到的上下文。 持久说明可以供后续读取;仍要核对具体工具是否加载了它,以及执行是否遵守。 (顺带说一句:这本手册的源仓库就是这么组织的——根目录有一份写给 AI 看的章程。)
判据:10 分钟上手测试
别用"文档写没写"来检验,用这个:
找一个没参与过的人(或者一个新开的 AI 会话),只给他仓库地址、不给任何口头解释。 看他在 10 分钟内能不能说出:这是什么、怎么跑起来、现在到哪一步、下一步做什么。
- 能 → 这个项目真的能被接住;
- 不能 → 缺的就是上面那几样里的某一样。补它,不要补字数。
试一试(20 分钟,今天就能做):挑一个你已有的项目(哪怕只有两个文件)—— ① 补一份三段式 README;② 写一条决策记录("为什么用了 X 而不是 Y"); ③ 建一个
docs/进展日志.md,写下今天这一行。 然后找个同学跑一遍那个 10 分钟测试。
13.3 选型:每类工具解决什么问题(稳定)
从当前任务出发选择工具。下面列出采用它的理由和需要付出的代价,不用样本数量或流程步数划一条通用门槛。
| 工具类别 | 适合解决的问题 | 选择时核对 |
|---|---|---|
| LLM API | 在程序中调用远端模型 | 能力、费用、延迟与材料发送权限 |
| 本地推理 | 在自有设备运行模型 | 内存、速度、模型许可及其他组件是否联网 |
| 向量库 | 持久保存与检索向量,支持过滤或并发 | 数据规模、更新方式、恢复要求;小实验可先用内存列表 |
| 编排工具 | 组织分支、状态和执行记录 | 是否比直接代码更易理解、测试和维护 |
| MCP | 通过共同协议接入工具或资源 | 客户端兼容性、权限与传输方式 |
| 实验跟踪 | 管理配置、指标和结果对应关系 | 手工表格能否满足复现需要,协作是否需要更完整工具 |
| 容器 | 帮助重建运行环境 | 系统架构、驱动、外部服务与数据卷仍需管理 |
| 云服务器 | 持续提供可访问的服务 | 是否确有托管需求,费用、权限与维护由谁负责 |
正式工具关有独立要求:七项按0/1/2分评分,总分至少9分,Git、Docker/Linux、向量库三项分别至少1分。七项都只达到1分时仍不够毕业。具体量表见附录 A 关卡与验收标准;日常实验的选型则先满足任务需要。
13.4 工具、版本与运行条件
13.4.1 先读运行条件
下列条目各自说明用途、操作入口和限制。先确认它是否适合当前任务,再核对官方文档中的版本、模型名称与参数。远程服务可能产生费用;本地服务也需要设备与存储。不要把某次可运行的配置当作长期不变的承诺。
13.4.2 工具与最小示例
LLM API 服务 —— 怎么确定 base_url 和 model
解决什么问题:让程序用上模型。 什么时候用:L0 一上手就需要。 截至 2026-09 有效:
- 三个参数:
api_key(你的密钥)、base_url(服务地址)、model(用哪个模型)。 base_url:兼容 OpenAI 的服务,地址通常以/v1结尾—— 但不是所有服务都带(下面 DeepSeek 的例子就不带)。 服务商给你什么就填什么,别自己加也别删。model:去服务商的控制台或文档里看,名字是由他们定的。- 怎么验证填对了:跑一次最简单的调用,能返回内容就对了。
报
401/AuthenticationError→ key 的问题;报404→base_url或model的问题。
本书的 DeepSeek 配置见动手之前:配置模型服务。官方入口为 API 文档和 控制台。核对日期:2026-10-11。
也可以使用学校提供或其他获准服务。以 阿里云百炼官方文档为例,使用前按自己的业务空间、地域与模型核对地址和权限;不要把不同地域的配置拼在一起。额度、活动与费用以账号当期规则为准。
计费与运行预算
批量调用前,先用少量虚构输入观察用量与耗时,再设置次数、并发和预算上限。输入、输出、缓存以及推理相关用量的计费方式由服务商定义;检查返回的用量字段和官方账单,不只估计提示词的字符数。
缓存是否命中、缓存多久和怎样收费,需要按具体服务核对。缓存命中也不等于回答正确或结果可复现;它与任务质量是不同的问题。
本地推理 —— Ollama 起步,vLLM 进阶
解决什么问题:让模型推理在自己的设备上执行。安装、下载及其他工具仍可能联网,需单独检查。 什么时候用:练习量大要省钱、或处理不宜外传的数据时。 截至 2026-09 有效:
先用 Ollama(入门最简单):
# 装好之后,一条命令就能跑
ollama run <模型名>
它会在本机起一个服务(默认 127.0.0.1:11434),并且提供 OpenAI 兼容接口——
接入时需同时核对服务地址、本机已有的模型名、客户端密钥占位约定与接口能力,不能只改地址却保留远端模型名。
vLLM 是进阶(要给多人提供服务时才需要):
pip install vllm
vllm serve <模型名> # ← 官方现在的写法;
# 老的 python -m vllm.entrypoints.openai.api_server 已被废弃
⚠️ vLLM 对系统有要求(官方安装文档会写清支持哪些系统)。 在 Windows 上装不上是常见情况,不是你的错,而且不影响 L0–L3 的任何关卡—— 日常自己用,本节的 Ollama 就够了。 真要上 vLLM,通常是在 Linux 服务器上;想在 Windows 上练,用上面 13.2.1 命令行(command line):够用的十条 末尾讲的 WSL(可选)。
三个必须知道的词(附录 A 关卡与验收标准 工具关会问):
| 词 | 一句话 |
|---|---|
| 显存 | 显卡的内存。模型越大、并发越高,要得越多。可考虑更小模型、量化或所用工具支持的卸载方案,分别测试代价 |
| 量化 | 把模型压小一点好装下,效果和速度可能变化,须按任务测试。是个取舍(第 3 章 走出练习本讨论的多指标取舍) |
| 并发 | 同时处理几个请求。并发会影响内存、吞吐与单请求延迟,关系需在具体配置下测量 |
常见坑:以为"装得上就能跑"——显存不够会直接失败,而且报错常常不明显。
官方入口:Ollama 与 vLLM 各自的官方文档。
嵌入模型(embedding model)—— 从哪来(聊天与嵌入分别配置)
解决什么问题:把文字变成向量——第 8 章 检索问答:让回答有据可查(RAG)的第一步,本章向量检索示例需要它;关键词检索方案可以不使用嵌入。 什么时候用:L3 第一次做 RAG 就要。 截至 2026-09 有效:
本书已核对的DeepSeek对话配置没有对应嵌入接口;后续服务能力可能变化,使用前核对官方文档。
所以第 8 章 检索问答:让回答有据可查的 AI_EMBED_MODEL 不要去 DeepSeek 里找。两条路,推荐第一条:
| 路线 | 怎么做 | 代价 |
|---|---|---|
| ① 本地跑一个嵌入模型(推荐) | 用你已经装的 Ollama 拉一个嵌入模型,免费、离线、不用再注册任何东西 | 本例 0.6B 标签约 639MB;下载量不等于运行内存 |
| ② 使用获准的远程嵌入服务 | 按模型、地域和权限配置客户端 | 费用、长度和额度按所用服务当前文档核对 |
路线②按百炼嵌入文档核对当前模型ID、地域、维度、单批限制、费用与可用额度。不同地区或模型的配置不能混用。若聊天与嵌入来自两家服务,需要各自的客户端、地址和密钥;本书最小RAG完整程序只配置了一个远程客户端。
路线 ① 的具体做法(Ollama,官方推荐这几个模型:embeddinggemma、qwen3-embedding:0.6b、all-minilm;
中文入门示例使用 qwen3-embedding:0.6b;官方标签页,2026-10-10 核对约 639MB;
不带标签可能下载更大的默认模型。建索引和查询必须用同一个模型与维度):
ollama pull qwen3-embedding:0.6b # 本例固定 0.6B,约 639MB;首次下载
先确认服务在跑:
ollama装完不一定会自己起服务,端口不通时下面这条 curl 会连不上。 用ollama list检查连接;服务未启动时,按安装方式启动Ollama应用或在另一终端运行ollama serve。嵌入模型通过嵌入接口使用,不用聊天命令验证。
# 试一下它能不能出向量(返回一个数字数组就对了)
curl -X POST http://localhost:11434/api/embed \
-H "Content-Type: application/json" \
-d '{"model": "qwen3-embedding:0.6b", "input": "补办证件需要带什么"}'
这条路的好处(顺手记两个知识点):
- 嵌入计算在本机进行,不产生远端嵌入调用费;下载模型与后续生成步骤是否联网,要分别检查;
- 它返回的向量是"单位长度"的(官方文档写明 L2 归一化), 所以算相似度时点积就等于余弦——第 8 章 检索问答:让回答有据可查那段代码照样能用,不用改。
常见坑:
- 把对话模型的名字填给嵌入接口(DeepSeek 用户最容易犯,必报 404);
- 语料是中文却选了只擅长英文的嵌入模型(检索会"看起来能跑但总找错");
- 资料与嵌入配置都没有变化,却在每次提问时重新嵌入整个语料库(此时可复用索引;资料或嵌入模型变化时再更新。第 8 章 检索问答:让回答有据可查 8.3 它到底怎么工作的:把"意思"变成"坐标" 的 build_index() 就是干这个的)。
官方入口:docs.ollama.com(嵌入能力页 + /api/embed 接口文档)。
向量库 —— 先别急着上,先用内存版
解决什么问题:存向量、快速找相近的。 什么时候用:需要持久保存、过滤、并发检索或管理更新时;先核对当前实验是否需要这些能力。
先用内存中的列表看清相似度检索怎样工作,再按13.3 选型:每类工具解决什么问题(稳定)判断是否需要专门的库。下面的示例展示前一种做法:
这个小示例将向量保存在内存中,退出后不保留索引:
pip install numpy # ← 这一版用 numpy,所以要装它
import numpy as np
def cosine(a, b):
na, nb = np.linalg.norm(a), np.linalg.norm(b)
if na == 0 or nb == 0: # 零向量:算不了相似度(与第 8 章那段保持一致)
return 0.0
return float(a @ b / (na * nb))
# vectors: 你的语料向量(numpy 数组),texts: 对应原文
def search(q_vec, vectors, texts, top_k=5):
sims = [cosine(q_vec, v) for v in vectors]
order = np.argsort(sims)[::-1][:top_k] # 从大到小取前 k 个
return [(texts[i], sims[i]) for i in order]
不想装 numpy 也可以:第 8 章 检索问答:让回答有据可查 8.3 它到底怎么工作的:把"意思"变成"坐标" 给了一份纯 Python 的同功能实现 (不用装任何东西,几百条数据够用)。两版都在,按你手上的环境挑一个。
几十行就够了,不需要装任何数据库。这也是"造轮子"的反面教材: 先把问题解决,等真的装不下了再换向量库。
但“项目暂时用不上”不等于“毕业时可以完全不会”:附录 A 把建集合与检索列为必达。 下面用 Qdrant 的本地模式练一次,不买云服务、不装 Docker、不调用模型。 参考 Qdrant 官方客户端的本地模式说明,本段于 2026-10-10 用客户端 1.19.1 验证。
先在你自己的项目虚拟环境中安装:
python -m pip install qdrant-client==1.19.1
把下面存成 vector_demo.py,运行 python vector_demo.py。它会在当前目录新建 qdrant_demo 存储文件夹;
不要把这个文件夹提交到代码仓库。这里三维向量是人工构造的演示数据,不代表模型真的理解了文本。
"""最小向量库练习:建集合、写入、检索、关闭后重新打开。"""
from qdrant_client import QdrantClient, models
COLLECTION = "learning_demo"
client = QdrantClient(path="qdrant_demo")
try:
if not client.collection_exists(COLLECTION):
client.create_collection(
COLLECTION,
vectors_config=models.VectorParams(size=3, distance=models.Distance.COSINE),
)
client.upsert(COLLECTION, points=[
models.PointStruct(id=1, vector=[1.0, 0.0, 0.0], payload={"text": "补办证件"}),
models.PointStruct(id=2, vector=[0.0, 1.0, 0.0], payload={"text": "开放时间"}),
])
hits = client.query_points(COLLECTION, query=[1.0, 0.0, 0.0], limit=1).points
print("首次检索:", hits[0].payload["text"])
assert hits[0].id == 1
finally:
client.close() # 释放本地存储锁,才能重新打开
client = QdrantClient(path="qdrant_demo")
try:
hits = client.query_points(COLLECTION, query=[1.0, 0.0, 0.0], limit=1).points
print("重新打开:", hits[0].payload["text"])
assert hits[0].id == 1
finally:
client.close()
两次都应输出“补办证件”。连续运行两次也应成功,因为集合只建一次、同 ID 的写入会更新。
给自己加三个检查:把查询改成 [0.0, 1.0, 0.0] 会怎样?把查询改成两维会怎样?关掉程序再打开,数据为什么还在?
接回第 8 章时,把人工向量替换成 embed() 的结果,集合维度改成实际向量长度;
语料和查询必须用相同模型与维度。模型或维度变了,先重建索引;不要把不同空间的向量混进一个集合。
本地练习证明集合、写入、检索和持久化的基本操作;不证明你已经会生产环境的索引调优、并发或部署。 需要独立服务时再读 Qdrant 本地服务 Quickstart,按自己的环境审阅部署方案。
云服务器(VPS)—— 什么时候需要持续运行
运行本书的本地模型实验,无须先购买云服务器。只有确实需要持续对外提供服务、远程运行或共享资源时,才进一步比较托管方式、权限、费用与维护责任。
云服务器不限定某一种操作系统。购买或领取试用前,核对所需系统、实例配置、地域、到期处理、自动续费、存储与网络费用。停止实例也不一定停止全部关联资源的计费。
若使用学生计划,可从阿里云高校计划等官方入口查看当前资格与可用产品。活动额度、有效期和适用范围会变化,本书不把它们写成可长期领取的承诺。也可以向学校确认已有资源;未获确认的资源,不应写进实验的必要前提。
本地推理则仍需要设备、存储与维护,其他组件也可能联网。选择依据是本轮任务的实际需要,而不是“免费”两个字。
编排 —— 什么时候真的需要它
解决什么问题:把多个步骤连成流程。 什么时候用:需要管理分支、状态、恢复与执行记录时;先比较直接代码能否满足需要。
| 类型 | 典型工具 | 特点 |
|---|---|---|
| 低代码 | Dify / n8n | 拖拽连线,上手快,适合先跑通 |
| 代码级 | LangGraph 等 | 控制力强,适合复杂分支与状态 |
截至 2026-09 有效:这两类的 API 与界面变化都很快,本书不给具体调用方式—— 先按官方 Quickstart 跑通一个最小例子,再回到第 9 章 Agent:让行动可控、结果可验对照那六项。
先确认框架解决了什么具体困难,例如中断恢复、分支管理或执行记录,再比较引入它的学习与维护成本。流程步数不能单独决定是否需要框架。
官方入口:各工具官网的 Quickstart。
MCP —— 它到底解决什么
解决什么问题:让一个工具能被多个系统重复使用。 什么时候用:L4,且同一个工具要在多个流程里被用到时。
一句话理解:没有共同协议时,不同宿主可能需要不同适配;已有函数或接口仍可复用。 MCP 定义了一套统一的接法——客户端和服务端按约定交换信息;接入时仍需核对协议版本、传输方式与权限。
注意:不要为了用 MCP 而用 MCP。 只给自己一个脚本用的工具, 直接写个函数就行。MCP 的价值在"复用",不在"高级"。
官方入口:MCP 官方文档(搜 "Model Context Protocol")。
动手:让一个客户端真正调用工具
截至 2026-10-10,验证版本:mcp==1.28.1(Python SDK v1 维护线)。SDK v2 的 API 不同,先锁定本练习版本;升级时按官方 v1 文档迁移。
这次只跑本机子进程,不开端口、不读文件、不调用收费模型。
在新的练习目录里创建虚拟环境,激活后安装:
python -m pip install "mcp==1.28.1"
新建 line_server.py,填入完整代码:
# 最小 MCP server:只处理调用者提供的虚构文本
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("handbook-lines")
@mcp.tool()
def count_lines(text: str) -> int:
"""统计非空行;不访问磁盘或网络。"""
return sum(bool(line.strip()) for line in text.splitlines())
if __name__ == "__main__":
mcp.run(transport="stdio")
在同一目录新建 line_client.py:
# 最小 MCP client:发现工具、调用成功、验证错误返回
import asyncio
from pathlib import Path
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main() -> None:
server = StdioServerParameters(
command=sys.executable,
args=[str(Path(__file__).with_name("line_server.py"))],
)
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("工具:", [tool.name for tool in tools.tools])
ok = await session.call_tool("count_lines", {"text": "第一行\n\n第二行"})
print("结果:", [part.text for part in ok.content if part.type == "text"])
bad = await session.call_tool("count_lines", {})
print("缺参数被拒绝:", bool(bad.isError))
if __name__ == "__main__":
asyncio.run(main())
运行 python line_client.py,应看到工具名 count_lines、结果 2、缺参数被拒绝 True。
客户端会启动 server,退出时清理子进程;不需要另开终端手动启动 server。
出问题先看:ModuleNotFoundError → 检查当前 Python 与安装位置;找不到 server → 两文件放同一目录;
协议解析失败 → server 不要把调试信息 print() 到 stdout,日志写 stderr。
提交物:两份文件、版本号、成功与失败输出,以及一句解释“客户端怎样发现工具”。 这证明协议链路能跑;要达到附录 A 的 MCP 高分档,还须把工具接入自己的 Agent/宿主并演示使用。
实验跟踪 —— 从"一个表格"开始
解决什么问题:记录并比对多次实验。 什么时候用:从第一次需要比较结果时开始,规模小时一张表格也可以。
先做一份不需要账号、也不联网的自动记录练习。保存为 track_runs.py:
# 最小实验记录:固定测试集,记录配置、预测和得分
import csv
import json
from pathlib import Path
CASES = [("遗失证件", "证件"), ("银行卡被盗刷", "其他"), ("补办证件", "证件")]
RULES = {"baseline": ["遗失"], "candidate": ["遗失", "补办"]}
rows = []
for name, keywords in RULES.items():
predictions = ["证件" if any(k in text for k in keywords) else "其他" for text, _ in CASES]
accuracy = sum(pred == expected for pred, (_, expected) in zip(predictions, CASES)) / len(CASES)
rows.append({"run": name, "cases": len(CASES), "accuracy": accuracy,
"config": json.dumps(keywords, ensure_ascii=False),
"predictions": json.dumps(predictions, ensure_ascii=False)})
with Path("runs.csv").open("w", newline="", encoding="utf-8-sig") as output:
writer = csv.DictWriter(output, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
for row in sorted(rows, key=lambda item: item["accuracy"], reverse=True):
print(f'{row["run"]}: {row["accuracy"]:.0%} ({row["cases"]} cases)')
print("记录已写入 runs.csv;再次运行会覆盖本次练习记录。")
运行 python track_runs.py,应得到 candidate: 100%、baseline: 67% 和一份 runs.csv。
这里是用规则模拟两组实验的管道练习,不是模型能力测试。换成真正的模型实验时,保持测试集不变,
同时保存数据版本、模型名、提示词/参数、逐条预测、耗时与成本,才能解释两轮差异。
再做一次:加一条不含“遗失/补办”的证件测试,观察两组得分怎样变化; 提交代码、两轮 CSV 和“哪条样本导致退步”的说明。先写结论再找证据,会把实验变成自我证明。
实验多起来后,可以把同样的配置与指标交给 W&B 等工具管理,按W&B 官方 Quickstart执行。 开启在线跟踪前先检查上传范围,真实数据、密钥与敏感文本不能顺手传上去。 本练习证明记录与横向比较的流程,不自动证明你已熟练使用 W&B。
容器与部署 —— 让"在别人机器上也能跑"
解决什么问题:把程序连同运行环境一起打包,帮助重建环境;硬件、驱动、外部服务和数据仍会造成差异。 什么时候用:要交给别人跑、或要部署时。
怎么装:Windows / macOS 装 Docker Desktop(docs.docker.com/get-docker/,Windows 上它依赖 WSL2);
Linux 用官方文档里的安装步骤。docker --version 只能确认客户端;再运行 docker info,能连接 Docker 服务才可继续。
截至 2026-09 有效:
docker build -t my-app . # 按 Dockerfile 打包成镜像(名字只能用小写字母、数字、中划线)
docker run my-app # 运行
docker compose up # 多个容器一起起(需要 docker-compose.yml)
Dockerfile 的形状是固定的三步:基于什么基础镜像 → 装依赖 → 怎么启动。
为什么 L2 关卡要你"别人能跑起来"(第 7 章 让一个实验成为自己的作品): 容器化就是这件事的工程化答案——但它比你手写 README 重, 所以先用 README 解决,等真的要交付了再上容器。
官方入口:Docker 官方文档。
动手:运行、检查并停止一个本地服务
先在独立的空目录新建 index.html,内容写 <h1>Hello, CyberSWAT</h1>。
创建 compose.yaml,不要把 .env、密钥或整个工作目录挂进容器:
services:
preview:
image: nginx:1.27-alpine
ports:
- "127.0.0.1:8088:80"
volumes:
- ./index.html:/usr/share/nginx/html/index.html:ro
在这个目录运行以下命令,每次一条;首次需要下载镜像:
docker compose up -d
docker compose ps
docker compose logs --tail 20
浏览器访问 http://127.0.0.1:8088,应看到 Hello, CyberSWAT。
改一行 HTML,刷新应看到变化;结束后运行 docker compose down,再次访问应无法连接。
这个版本用于复现练习;正式部署须另核镜像维护状态、更新策略与服务边界。
先审阅,再执行:127.0.0.1 说明只在本机访问;8088:80 是宿主与容器的端口映射;
:ro 是只读挂载。若 Agent 把它改成整个磁盘挂载、privileged: true 或公开监听,先要求解释理由与影响。
常见失败:Docker 服务未启动 → 回到 docker info;端口占用 → 换一个本机端口;
挂载变成目录 → 检查 index.html 是否已创建、当前目录是否正确;下载失败 → 查官方镜像与学校认可的来源。
提交物:compose 文件、启动/日志/停止的结果,以及对端口和只读挂载的解释。 它对应“会跑现成镜像”;写自己的 Dockerfile 与排障属于下一步。Linux 部分仍考理解与审阅,不考手工运维。
CPU模型实验的工具包
第7章的数字和文本实验使用同一套工具。以下环境于2026-10-11在Linux、Python 3.13.13上完成安装和实跑,其他系统应核对安装包兼容性,并在自己的设备保存运行记录。请为实验建立独立环境。
按5.4 安装、验证与独立环境的方法新建并激活一个独立实验环境。在实验目录新建 lab-requirements.txt,粘贴以下完整内容:
cloudpickle==3.1.2
joblib==1.6.0
narwhals==2.27.1
numpy==2.5.4
scikit-learn==1.9.1
scipy==1.18.1
threadpoolctl==3.7.0
python -m pip install -r lab-requirements.txt
首次安装需要获取工具包;安装后两个实验的数据已在工具包或代码内,运行不请求外部模型。该版本组合的安装、拆分和预测已有复跑记录,不代表其他版本一定得到完全相同结果。下载受限时按附录G的国内镜像、学校渠道与助教帮助处理,不以关闭校验或绕过网络管理解决。没有适配安装包、空间不足或权限受限时,保留报错,由助教安排兼容环境,不要求学员自行改系统权限。
可核对的原理资料:数据泄漏与流水线、逻辑回归与C参数、TF-IDF字符特征、F1与宏平均。链接与描述核验于2026-10-11;版本更新后复核。
13.4.3 核对版本与运行条件
工具更新后,先核对官方文档中的版本、接口和运行条件。安装成功不等于示例已验证;保存一次最小运行结果,再扩大任务。模型名称、价格、额度与硬件要求以服务商当期说明为准,本文不承诺免费使用。
更多文档和基础课程见附录 G 资源入口(外部清单)。访问受限时使用官方国内入口、可信镜像或学校渠道,也可以请助教提供适配环境。
13.5 速查卡
每天用的四样:
cd / ls / pwd 进目录、看内容、我在哪
git status / diff 我改了什么
git commit -m "..." 拍快照(改环境前先做一次!)
README / 决策记录 / 日志 留上下文——别人(和 AI)才接得住
保命三条:
rm 不进回收站 → 敲之前看路径
.env 必须进 .gitignore
密钥泄露 → 第一动作是吊销,不是删文件
选型先问:我"真正"需要它了吗?
资料几百条 → 不用向量库
流程两三步 → 不用编排框架
只试了两三次 → 不用实验跟踪
→ 工具解决"已有的问题",不是"想象中的问题"
本章会过期,正文不会。
看到「截至 YYYY-MM」,用之前先核实。
附录 A 关卡与验收标准
本附录整理现行正式标准,修订日期为2026-10-11。正文提供练习和解释,验收按本附录及当期正式公布的同一版本执行;发现冲突时请维护者核对,不自行猜测要求。
CPU 模型实验和附录 F 的探索可提前学习,不自动替代 L0–L5。表中的“生产级”“生产可用”是现行量表用语;教学作品通过验收,不代表已获准接入真实业务。
A.1 七条通用规则(所有级别都适用)
| 规则 | 内容 |
|---|---|
| 不可跳级 | 正式升级须逐级过 Gate;可提前阅读、旁听或探索,不据此免除前级产出物。允许跳内容(已会的不必重学),不允许跳产出物 |
| 可运行优先 | 产出物必须能当场跑给验收人看。截图、PPT、文档不能替代实际运行 |
| 可复现 | 验收人只凭你的仓库和 README,在不问你的前提下能跑出结果 |
| 可解释 | 你要能回答"为什么这么做",否则视为未通过 |
| 不限时 | 关卡异步,没有截止日期。快慢自由,但没过关不升级 |
| 可换赛道 | 从 L2 起可以换赛道,换后须重跑该赛道的 L2 首作关卡 |
| 署名归属 | 作品署名归你个人与团队 |
关于"已经会了的人":你可以直接提交关卡产出物申请免修该级课程, 但产出物的标准不降。这是给"大一就有基础"的同学的通道。
A.2 六级关卡速查表
| 级 | 你要交什么 | 怎么验 | 一票否决 |
|---|---|---|---|
| L0 | 跑通第一个对话应用 + 个人 Git 仓库 + README | 验收人凭 README 独立跑通 | 只有截图;密钥硬编码进公开仓库;无法解释代码 |
| L1 | 生产级 prompt + ≥10 条测试用例 + A/B 对比记录 | 用未见过的输入测试,结论须一致 | 无评测;用例与 prompt 同源;改后不重测 |
| L2 | 赛道首作:落在所选赛道的可演示 AI 应用;含可交付说明三段(这是什么 / 怎么跑 / 到哪一步) | 现场演示 + 验收人亲自试用 + 复现(别人照说明能 10 分钟上手) | 只套模板不改动;跑不通;说不清为何选此赛道 |
| L3 | 带知识库的可信问答(须标来源)+ 检索评测报告 | 未见问句测试 + 把 badcase 定位到具体层 | 不标来源;无法定位失败;chunk 参数说不出理由 |
| L4 | 自建 Agent 工作流(六项均有实物)+ 架构答辩 | 架构答辩 + 现场运行 + 出示评测数据 | 仅改示例、未形成自己的架构设计;无评测闭环;无重试降级;无法解释架构 |
| L5a | 实际的关卡验收工作量记录 | 部长 + 验收组认定 | 无实际验收记录(挂名不算) |
L4 的“六项”见 A.5.2。可以按许可借鉴代码并注明来源,但只改名称、提示词或局部参数不足以成为自建架构;能够解释示例也还不够。需要自己的任务与设计取舍,以及六项能力的实物、故障处理和评测迭代证据。
两处措辞的说明(要求本身未变,只是把原文的简写讲明白): - L1 的「结论须一致」指:验收人用你没见过的输入去测,结果要和你评测记录里写的一致。 独立复测应能支持所报告的结论;若差异很大,要检查样本量、来源、判分方式与随机波动,不仅比较两个百分数。 - L3 的「badcase 分层归因」指:能把一个答错的例子,定位到具体是哪一层出的问题 (切分 / 检索 / 重排 / 生成)。对应3.5 结果变了,怎样找原因的对照方法,并在第8章逐层检查。
本附录凡与训练营原文措辞不同处,均为把简写讲清楚,未增加或减少任何要求。 如有出入,仍以训练营公布的为准。
A.3 验收流程(五步)
① 提交 ──▶ ② 复现 ──▶ ③ 质询 ──▶ ④ 判定 ──▶ ⑤ 留档
仓库+README 验收人独立跑 原理关问答 通过/有条件/ 标准产出物
+评测记录 不询问作者 10–15 分钟 不通过+差在哪 +常见失分点
| 步 | 要点 | 常见问题 |
|---|---|---|
| ① 提交 | 代码仓库 + README(安装/运行/配置)+ 评测记录 | README 只写"运行 main.py",缺环境说明 |
| ② 复现 | 验收人不问你,独立跑通 | 你在旁边"帮忙点一下" = 复现失败 |
| ③ 质询 | 问你"为什么"(对应原理关) | 跳过质询直接看结果 |
| ④ 判定 | 必须写明差在哪一项 | 含糊说"再改改" |
| ⑤ 留档 | 留下标准产出物与常见失分点 | 不存档 → 下届判定尺度漂移 |
A.4 判定三档
| 档 | 含义 | 后续 |
|---|---|---|
| 通过 | 达到该级出口标准 | 升级 |
| 有条件通过 | 主体达标,个别项缺失 | 限期补件,补完升级 |
| 不通过 | 未达标准 | 明确告诉你差在哪 → 改完重交,次数不限 |
A.5 毕业标准:三个问题
过了各级关卡 ≠ 毕业。毕业要另过这三问。
A.5.1 原理关 ——「为什么选这个方案?」
| 观测点 | 看什么 |
|---|---|
| 选型论证 | 你能不能举出 ≥2 个替代方案并说明为什么不选 |
| 参数归因 | 关键参数(chunk 大小、模型选择、温度、rerank 阈值)能否说出取值理由 |
| 失败归因 | 遇到 badcase 能否定位到具体层(切分 / 检索 / 重排 / 生成) |
| 档 | 分 | 什么表现 |
|---|---|---|
| 未达 | 0 | 只能复述教程;被问"为什么"答"教程这么写的";分不清"跑不通"和"效果差" |
| 达标 | 1 | 能说明自己方案的理由,但举不出替代方案或对比依据;能定位明显失败 |
| 优秀 | 2 | 能举 ≥2 个替代方案并给出取舍依据;能对 badcase 分层归因;能主动说出方案的已知局限 |
A.5.2 作品关 ——「能不能搭出自己的 Agent 工作流?」
六项,每项 0/1/2 分,满分 12。
| # | 项 | 0 分 | 1 分 | 2 分 |
|---|---|---|---|---|
| 1 | 编排 | 单次调用,无流程 | 用现成框架/低代码搭出流程 | 自定义编排含条件分支与循环,能说明编排理由 |
| 2 | 多模型调度 | 单一模型硬编码 | 多模型但按固定用途分开 | 按任务/成本/能力动态路由,且有降级模型 |
| 3 | 工具封装 | 仅用内置或示例工具 | 自己写了工具函数并接入 | 封装为可复用工具/MCP server,有入参校验与错误返回 |
| 4 | 状态管理 | 无状态,每次独立 | 有会话历史 | 有明确状态结构(记忆/中间态/可恢复),能说明状态生命周期 |
| 5 | 重试降级 | 无错误处理 | 有 try/catch 与重试 | 有超时、重试策略、fallback 模型或路径,且经故障注入验证 |
| 6 | 评测闭环 | 无评测,凭手感 | 有测试用例,人工看过 | 有评测集 + 指标 + 至少一轮基于评测的迭代记录 |
A.5.3 工具关 ——「现代化工具栈熟不熟?」
七项,每项 0/1/2 分,满分 14。
分档口径:0 = 不会 · 1 = 会用(能完成基本操作) · 2 = 熟练(能排障、能配到生产可用、能教别人)
| # | 工具 | 1 分(会用) | 2 分(熟练) |
|---|---|---|---|
| 1 | Git / GitHub | 会 commit / push | 会分支、PR、冲突解决、Code Review |
| 2 | Docker / Linux | Docker:会跑现成镜像 · Linux:看得懂一条命令在干什么、读得懂报错,知道 Linux 与 Windows 的设计差异(不考命令实操——执行侧交给 Agent,见 13.2.1 命令行(command line):够用的十条) | Docker:会写 Dockerfile/compose、能排障 · Linux:懂设计哲学(一切皆文件 / 小工具组合 / 约定优于配置)、开源精神与许可常识,能审阅 Agent 给的命令与部署方案 |
| 3 | LLM API 生态 | 会调 OpenAI 兼容接口 | 懂流式、函数调用、结构化输出、限流与重试 |
| 4 | 本地推理(Ollama/vLLM) | Ollama 跑过模型 | 会用 vLLM 部署,懂显存/量化/并发 |
| 5 | 向量库(Qdrant/Milvus) | 会建集合与检索 | 懂索引类型、过滤、调优、持久化 |
| 6 | MCP | 用过现成 MCP server | 自己写过 MCP server 并接入 Agent |
| 7 | 实验跟踪(W&B 等) | 手工记录实验 | 用工具跟踪并可横向比对 |
A.5.4 综合判定
| 判定 | 条件 |
|---|---|
| 毕业 | 原理关 ≥ 1 且 作品关 ≥ 8 且 工具关 ≥ 9,且满足下表的必达项 |
| 未毕业 | 任一项未达 → 按差距补,重交不限次数 |
必达项(不论总分,都不得为 0):工具关中的 Git 协作 / Docker·Linux / 向量库 三项。
理由:这是"能交付一个系统"的最低门槛。三项都只要求"会用"档(≥1),不要求熟练,但不许是 0。 不会 Git 无法协作;看不懂命令行与报错就无法审阅部署过程;L3 课程包含向量库实操,毕业还须提交向量库至少1分的证据。内存 RAG 示例只帮助理解流程,不能替代向量库实操;L3 Gate 仍看可信问答、检索评测及失败定位,不能以“装好了数据库”代替这些验证。
⚠️ Linux 这一项考什么(口径修订,2026-09-15)
执行交给 Agent,人负责看懂与判断。 所以 Linux 部分不考命令实操,考三件事: ① 看得懂——一条命令在做什么、报错在说什么; ② 系统观——Linux 的设计哲学(一切皆文件、小工具组合、约定优于配置)、 开源精神与许可证常识、与 Windows 的底层设计差异; ③ 能审阅——能判断 Agent 给的命令与部署方案合不合理、有没有风险。
"能自己从零装一台服务器并运维"不再是要求。 对应正文见第 13 章 工具箱 13.2.1 命令行(command line):够用的十条。
三个毕业门槛分别计算,不能用某一项高分抵消另一项未达。工具关三项必达至少各1分,同时总分仍须达到9分;七项都只得1分时,总分7分,尚未达标。
A.5.5 一票否决(不论分数,直接不毕业)
| # | 情形 | 理由 |
|---|---|---|
| 1 | 作品无法运行 | 与"能交付一个系统"根本矛盾 |
| 2 | 代码不是本人所写,且无法解释原理 | 学术诚信;且原理关必然为零 |
| 3 | 评测闭环为 0 | 无评测 = 无工程,就是"感觉对了" |
| 4 | 密钥或敏感数据泄露到公开仓库 | 安全红线。AI 部做 AI×安全赛道,此条尤不可犯 |
A.6 申诉与重交
- 不通过时,验收人必须书面写明差在哪一项,不得只给结论
- 你对判定有异议,可以申请换人复验(由部长另指定一名验收人)
- 重交次数不限、时间不限
- 已通过的关卡,不会因后续课程调整而追溯失效
A.7 提交前核对
确认提交材料对应同一版本,README 能独立使用,评测包含来源与判定口径。明确标注原示例、外部贡献和自己的工作。正式验收人员与安排以当期通知为准。
不会的内容可以继续练习,已会的内容可以申请免修课程;对应产出和解释要求仍需满足。对结果有疑问时,按上节申请复验。
附录 B 术语表
怎么用:读正文遇到不认识的词,回来查。每个词给简短解释,并标明进一步阅读的位置。 想知道精确含义,去看正文对应章节——术语表只负责让你读得下去。
定义用于本书的具体语境;更完整的假设与边界请回到正文。
术语按任务分组。先找到需要的解释,再回到正文里的例子;不必逐项背诵。
一、最基础的几个词
| 术语 | 白话解释 | 在哪遇到 |
|---|---|---|
| 模型 | 根据输入产生预测或其他输出的规则与参数组合;可以做分类、回归或生成,不只指聊天模型 | 第 2 章 把错误变成进步、图像实验:64个数怎样变成一个数字 |
| 像素 / pixel | 图像网格中的一个位置及其灰度或颜色数值 | 第1章 |
| 向量 / vector | 本书入门例子中是一组按固定顺序排列的数,如64个像素值 | 第1、2章 |
| 矩阵 / matrix | 有行列结构的数表;例如每行一个样本、每列一个特征 | 第1、2章 |
| 监督学习 | 利用输入及相应目标答案训练模型 | 第1、2章 |
| 自监督学习 | 从数据自身构造预测目标来训练,如用前文预测后续token | 第2、6章 |
| 注意力机制 | 根据位置之间的表示关系计算权重并汇合信息的一类机制 | 第6章 |
| 大语言模型 / LLM | LLM = Large Language Model,处理语言的大规模模型;本书对话案例主要使用按上下文预测后续token的生成模型。“大模型”还可指其他模态的模型 | 第5–9章 |
| 参数 | 模型内部的一堆数字,训练就是把这些数字调好。你说"模型多大"通常指参数多少 | 第 2 章 把错误变成进步 |
| 提示词 / prompt | 你给模型的输入。不是"提问",是"给它立规矩 + 给信息 + 派任务" | 第 6 章 语言:从预测到对话 |
| 上下文 | 模型这次能"看到"的全部内容(你的提示词 + 历史对话)。它有长度上限 | 第 6 章 语言:从预测到对话 |
| 温度 / temperature | 影响生成采样分布的参数;较低通常更集中,较高通常更多样,均不保证事实正确 | 第 6 章 语言:从预测到对话 |
| 幻觉 | 模型输出无依据、不符合事实或不忠实于所给材料的内容;成因与处理需要结合具体任务检查 | 第 2 章 把错误变成进步 |
| token | 分词器处理文本的单位,可能是词、词的一部分或其他片段;不同语言和分词器的对应关系不同,长度上限与计费常按token计算 | 第 5 章(下) 接入模型,完成一次对话/ 13.4.2 工具与最小示例 |
| 神经网络(neural network) | 由带参数的计算单元和连接组成的模型;前馈网络将各层函数组合起来,训练通过数据调整参数 | 2.5 多层计算怎样一起改 |
| 激活函数 / activation function | 加在计算单元中的变换;ReLU把负数变为0,正数保持,提供非线性 | 2.5 多层计算怎样一起改 |
| 隐藏层 / hidden layer | 输入与输出之间的中间计算层;通常不直接给每个单元指定训练答案 | 2.5 多层计算怎样一起改 |
| 深度学习(deep learning) | 利用多层可学习表示的方法;具体结构与训练有长期发展,不能只用某一年划出起点 | 卷首历史与第2、4章 |
| 强化学习(reinforcement learning) | 系统通过行动与奖励信号学习策略,关注长期累积回报;奖励可以逐步或稀疏出现,不限于最后输赢。自对弈是获得交互经验的一种方法 | 卷首历史与第2、4章 |
二、怎么用上模型
| 术语 | 白话解释 | 在哪遇到 |
|---|---|---|
| API | 程序之间说话的接口。你不用把模型搬回家,通过 API 向它发请求 | 第 5 章(上) 准备实验环境 |
| 密钥 / API Key | 调用 API 的凭证。它等于钱,泄露就会被人拿去用 | 第 5 章(上) 准备实验环境(重点) |
| OpenAI 兼容 | 一种接口格式约定。很多家服务都做成"和 OpenAI 一样",所以代码能通用 | 第 5 章(上) 准备实验环境 |
| 本地推理 | 在自己的设备执行模型预测;所需CPU、内存或显卡取决于模型与任务。模型和依赖准备齐全后可以离线运行,不按远端调用计费,仍有设备与维护成本 | 13.4.2 工具与最小示例、CPU模型实验的工具包 |
| Ollama | 一个跑本地模型的工具,装完一条命令就能用。适合入门 | 13.4.2 工具与最小示例 |
| vLLM | 一个更适合"给多人提供服务"的本地推理框架。要配显存、并发这些 | 13.4.2 工具与最小示例 |
| 显存 | 显卡的内存。模型越大、并发越高,需要越多 | 13.4.2 工具与最小示例 |
| 量化 | 用较低精度表示权重或计算,可能减少存储与内存需求;质量和速度变化需实测 | 13.4.2 工具与最小示例 |
| 环境变量 | 存在操作系统里、不写在代码里的配置。密钥就该放这里 | 第 5 章(上) 准备实验环境 |
三、让它知道你的资料(RAG 一族)
| 术语 | 白话解释 | 在哪遇到 |
|---|---|---|
| RAG | Retrieval-Augmented Generation。先查资料,再让模型根据资料回答。解决"它不知道你的数据" | 第 8 章 检索问答:让回答有据可查 |
| 知识库 | 你喂给系统的资料集合。RAG 的原料 | 第 8 章 检索问答:让回答有据可查 |
| Embedding / 嵌入 | 把一段文字变成一串数字(向量),使得意思相近的文字,数字也相近 | 第 8 章 检索问答:让回答有据可查 |
| 向量 | 有顺序的一串数,可表示图像特征或文本等;作为嵌入时可把它看作某种表示空间里的坐标 | 2.3 用一个权重看见学习、第 8 章 检索问答:让回答有据可查 |
| 向量库 | 专门存向量、并能快速找出"最相近的几个"的数据库。如 Qdrant、Milvus | 第 13 章 工具箱 13.3 选型:每类工具解决什么问题(稳定) |
| 切分 / chunk | 把长文档拆成小块再入库。块太大检索不准,太小会丢上下文。这是最常出问题的一步 | 第 8 章 检索问答:让回答有据可查 |
| 检索 | 从知识库里找出和问题最相关的几块内容 | 第 8 章 检索问答:让回答有据可查 |
| 重排 / rerank | 检索出来的顺序往往不够准,用另一个模型重新排一遍,把最相关的提到前面 | 第 8 章 检索问答:让回答有据可查 |
| badcase | 表现不好的具体例子。做 RAG 的工作,大半是在收集和分析 badcase | 第 3、8 章 |
四、让它动手(Agent 一族)
| 术语 | 白话解释 | 在哪遇到 |
|---|---|---|
| Agent / 智能体 | 不只是回答,而是能自己决定下一步、并调用工具去完成的系统 | 第 9 章 Agent:让行动可控、结果可验 |
| 编排 | 把多个步骤串成一个流程:先做什么、什么条件下走哪条路 | 第 9 章 Agent:让行动可控、结果可验 |
| 工作流 | 上面那个流程本身 | 第 9 章 Agent:让行动可控、结果可验 |
| 工具调用 / 函数调用 | 模型提出结构化的工具调用请求,程序校验后执行相应动作,再把结果交回模型 | 第 9 章 Agent:让行动可控、结果可验 |
| MCP | 一种让模型接入外部工具的统一协议。写得好的 MCP server 可以被反复复用 | 第 9 章 Agent:让行动可控、结果可验 |
| 多模型调度 | 一个系统里用多个模型,按任务难度或成本动态选择用哪个 | 第 9 章 Agent:让行动可控、结果可验 |
| 状态管理 | 记住"刚才说到哪了、做过什么"。不控状态的 Agent 只是单轮问答 | 第 9 章 Agent:让行动可控、结果可验 |
| 降级 / fallback | 主方案失败时的备用方案。比如主模型超时了,换个小模型顶上 | 第 9 章 Agent:让行动可控、结果可验 |
五、工程基本功
| 术语 | 白话解释 | 在哪遇到 |
|---|---|---|
| 仓库 / repo | 存放代码的地方,带完整的历史记录 | 第 5 章(上) 准备实验环境 |
| Git | 给代码拍快照的工具。改坏了能回到上一版 | 第 5 章(上) 准备实验环境 |
| README | 仓库的说明书:怎么装、怎么配、怎么跑。别人能不能用你的东西,全看它 | 第 5 章(上) 准备实验环境 |
| 虚拟环境 | 一个独立的装库盒子,避免不同项目的依赖打架 | 第 5 章(上) 准备实验环境 |
| 依赖 | 你的代码要用到的别人写的库 | 第 5 章(上) 准备实验环境 |
| Docker / 容器 | 把一个程序连同它的运行环境一起打包,帮助重建一致环境,仍需核对架构、驱动、权限和外部依赖 | 第 13 章 工具箱 13.3 选型:每类工具解决什么问题(稳定)、13.4.2 工具与最小示例 |
| 镜像 | 容器打包好的那个"包",可以被下载和运行 | 13.3 选型:每类工具解决什么问题(稳定) |
| 部署 | 把你在本地跑通的东西,变成别人也能访问的服务 | 第 13 章 工具箱 13.4 工具、版本与运行条件 |
| 命令行 / 终端 | 用打字代替点鼠标的操作界面 | 第 5 章(上) 准备实验环境 |
六、怎么知道它行不行
| 术语 | 白话解释 | 在哪遇到 |
|---|---|---|
| 评测集 | 用于按约定检验任务表现的资料。现行L1要求至少10条并覆盖难例;这是入门门槛,不是统计充分保证。常规与压力例分开报告 | 第2、3章 |
| 基线 / baseline | 用来比较的简单方法或已有方案及其结果;改前版本、常数预测和关键词规则都可以是基线 | 第 3 章 走出练习本、7.1 把数字交给模型 |
| 标签 / label | 已知或约定的目标答案;标签也可能有歧义或错误,需要说明来源与规则 | 7.1 把数字交给模型 |
| 特征向量 / feature vector | 把一个输入表示成一串可用于计算的数,例如8×8图片的64个亮度值 | 图像实验:64个数怎样变成一个数字 |
| 精确率 / precision | 预测为某类的样本中,实际属于这一类的比例 | 7.1 把数字交给模型 |
| 召回率 / recall | 实际属于某类的样本中,被找回的比例 | 7.1 把数字交给模型 |
| F1 / 宏平均F1 | F1合成精确率与召回率;宏平均对每类F1不加权平均。不是准确率的别名 | 7.1 把数字交给模型 |
| 混淆矩阵 / confusion matrix | 按真实类与预测类交叉计数;本例行是真实类、列是预测类 | 图像实验:64个数怎样变成一个数字 |
| 流水线 / pipeline | 把预处理和模型接成固定流程;拟合时仍须确保只使用训练资料 | 图像实验:64个数怎样变成一个数字 |
| 逻辑回归 / logistic regression | 用于分类的一种模型;名字中的“回归”不表示本例在预测连续数值 | 图像实验:64个数怎样变成一个数字 |
| TF-IDF | 根据片段在文档中的频率与稀有程度表示文本;本例使用字符片段,词表和权重只从训练集学习 | 文本实验:为什么规则反而赢了 |
| 分类 / classification | 为输入选择离散类别,如数字0–9或咨询意图;区别于预测连续数值的回归 | 7.1 把数字交给模型 |
| 损失 / loss | 训练时用于衡量差异并驱动参数更新的目标,未必等同于最终任务指标 | 2.3 用一个权重看见学习 |
| 梯度 / gradient | 函数随参数变化的局部方向与速度;梯度下降沿反方向更新参数 | 2.3 用一个权重看见学习 |
| 超参数 / hyperparameter | 由我们设置或在开发集选择的训练、模型配置,如学习率和C;不是训练学出的权重 | 2.3 用一个权重看见学习 |
| 迁移学习 / transfer learning | 将其他任务或资料上学得的表示用于新任务,仍需核对资料、许可与独立评价 | 图像与文本:跑完首作以后怎样继续 |
| 指标 | 把"好不好"变成一个能算的数 | 第 3 章 走出练习本 |
| A/B 对比 | 两个方案跑同一套评测集,比较结果 | 第 3、6 章 |
| 迭代记录 | 记录每次"改了什么、分数怎么变"。让后续选择有据可查 | 第 3 章 走出练习本 |
| 故障注入 | 故意制造错误(断网、超时),看你的降级机制有没有真的生效 | 第 9 章 Agent:让行动可控、结果可验 |
| 回归 | 机器学习中的回归预测连续数值;工程里的回归测试检查改动是否破坏已有功能,二者不同 | 2.3 用一个权重看见学习、第 3 章 走出练习本 |
七、你可能听到但暂时不用深究的
| 术语 | 白话解释 |
|---|---|
| 训练 / 微调 | 训练从数据调整模型参数;微调在已有训练基础上继续适配。Agent应用起步可以不训练模型,建模方向则需要理解和验证训练过程 |
| Q学习 / Q-learning | 从交互奖励更新状态与动作的价值估计,再据此选择行动的一种强化学习方法;不保证比简单策略更好 |
| 终止 / 截断 | 终止是任务真正结束;截断是外部采样预算等使回合停止。有限时域本身的截止还需纳入任务状态 |
| 具身智能 | 带有身体的智能体通过感知与行动同环境形成闭环;可采用控制、规划、模仿学习或RL等方法 |
| 模仿学习 / imitation learning | 从示范学习行动策略;示范中拟合得好,仍需在闭环中检验偏离与恢复 |
| 多模态 | 同时处理文字、图片、音频。是一条赛道方向 |
| ASR / TTS | 语音转文字 / 文字转语音 |
| Agentic | 形容"像 Agent 那样能自主行动"的用法,是个流行词,别被它唬住 |
遇到新词,先问它在当前例子里解决什么问题,输入与输出是什么,以及暂时不使用它会有什么影响。对应机制可查5.3 一张地图:AI 工程的常用机制;不要为了理解一个词被迫预先学完整个领域。
本附录的维护:术语与圈内叫法都在变。 按本培养体系的分工,附录 B 术语表 由助教团随训练营推进逐步补充, 每学期复核一次(与第 13 章 工具箱工具箱同步)。
补术语时请遵守四条规矩: 白话先行、只用一种写法(中文术语 + 英文原名)、首次出现处只标一次、登记入本表并更新状态。
附录 C 速查表
怎么用:这一份是浓缩版,供你打印或收藏。想看解释,翻对应章节。
与第 13 章 工具箱的关系:本附录的内容取自正文与第 13 章 工具箱里稳定的部分(命令、判断标准)。 会过期的具体工具用法不在这里——那种内容集中在第 13 章 工具箱 13.4 工具、版本与运行条件,并标注日期。
⚠️ 本附录若与正文冲突,以正文为准。
C.1 命令行(够用的十条)
pwd # 我在哪个目录
ls # 这里有什么 (Windows: dir)
cd 某目录 # 进去 (cd .. 回上一层)
cd ~ # 回自己的用户目录
mkdir 新目录 # 建目录
cp 源 目标 # 复制 (目录加 -r)
mv 源 目标 # 移动 / 改名
rm 文件 # 删除 ⚠️ 不进回收站
cat 文件 # 看文件内容
grep "关键词" 文件 # 在文件里找内容
三个省时习惯:↑ 调上一条命令 · Tab 补全路径 · Ctrl+C 中断卡住的程序
⚠️
rm直接删除,不进回收站。 敲之前看路径,尤其是rm -r 目录。这一页要你"认识",不是要你背。 为什么要懂一点命令行、Linux 的设计哲学是什么、 和 Windows 差在哪、怎么看懂一条命令与一句报错——见 13.2.1 命令行(command line):够用的十条。 实操交给 Agent,你负责看懂与审阅。
C.2 Git(够用的六个)
git init # 把一个目录变成仓库(只在最开始时做一次)
git status # 现在有什么改动(最常用)
git add . # 放进"待提交"
git commit -m "说明" # 拍快照
git log --oneline # 看历史
git diff # 看具体改了什么
| 什么时候 | 用什么 |
|---|---|
| 想知道有没有改坏东西 | git status + git diff |
| "昨天还好好的,今天不行了" | git log + git diff ← 先查这个,别自信 |
| 装环境 / 改配置之前 | 先 git commit 一次 |
| 新项目第一步 | 先写 .gitignore |
commit -m那句话是写给未来的自己。 哪怕只写"能让它连续对话了"也比空着强。
工作区三件(Git 管"时间",这三件管"结构"——详见 13.2.4 项目工作区:Git 之外的另一半):
README.md 这是什么 / 怎么跑起来 / 现在到哪一步 ← 三段,最重要
docs/决策记录.md 为什么选 A 不选 B(一两行就够)
docs/进展日志.md 今天做了什么 / 卡在哪 / 下一步
判据:别人(或一个 AI)只拿仓库地址,10 分钟能不能上手
C.3 Python 环境与密钥
python3 -m venv .venv # 建虚拟环境(Windows: python -m venv .venv)
source .venv/bin/activate # 激活(Windows PowerShell: .venv\Scripts\Activate.ps1)
python -m pip install 包名 # 给当前解释器装库
Windows PowerShell 用 python 或已经验证可用的 py 创建环境;激活见第五章。文件操作在 PowerShell 用 Copy-Item、Move-Item、Remove-Item、Get-Content;CMD 才用 copy、move、del、type。不要混用终端语法,删除操作先核对路径。
密钥的最短正确链:
.env 文件 → AI_API_KEY=自己的密钥
程序先执行 → from dotenv import load_dotenv; load_dotenv()
再读取 → import os; os.environ["AI_API_KEY"]
.gitignore → .env
提交到仓库 → .env.example(只留占位符,不放真实密钥)
load_dotenv() 来自 python-dotenv,须装在运行程序的同一环境。这里沿用第五章文件与程序在同一项目目录的约定。
⚠️ 泄露后第一动作是去后台「吊销」密钥,不是删文件——Git 历史里还在。
C.4 排查:卡住了先做这三件事
① 把现象写清楚 全都错?某类错?什么时候开始的?
② 记住那条链 数据 → 切分 → 检索 → 重排 → 生成 → 工程
③ 一刀切一半 打印中间输出 / 绕过检索 / 换台机器
最常见的四个真凶:
| 真凶 | 怎么确认 |
|---|---|
| 改了没重测 | git diff |
| 索引没重建(资料改了忘了重建) | 检查索引时间 |
| 只做提示词不兜底 | 格式类故障 |
| 用调提示词掩盖检索问题 | 先做"绕过检索"的二分定位 |
一句话:排查不是"想出来"的,是"排除掉"的。
(完整九种症状见第 11 章 出问题了怎么办:排查手册。)
C.5 提问与搜索
提问三要素(缺一条别人就没法帮)
① 我想要什么
② 我做了什么(含环境)
③ 我看到了什么(完整报错,不是"报错了")
卡住时的决策:
问自己:我接下来这个动作,会产生新的信息吗?
会 → 自己继续(打印输出 / 删一半代码 / 换输入)
不会 → 该求助了(第 5 次执行同样的命令、反复凭感觉调参数)
环境问题同样可以学习排查方法,但不必无限消耗练习时间。达到自己设定的时间预算后,整理报错,请人协助或使用已验证环境。
搜报错:删掉私人信息 → 保留错误类型 + 英文描述 → 加库名和版本。
看资料先看三样:日期(AI 领域旧的会失效)· 版本(核对差异)· 来源(官方 > issue > 博客 > 转载)。
C.6 工具选型
先说明需要解决的问题,再比较工具的能力、运行条件和维护成本。类别对照见13.3 选型:每类工具解决什么问题(稳定)。数据很少也可能需要持久化;数据很多也不自动说明一定要某款数据库,按实际需求判断。
C.7 任务说明
写清目标、可使用的材料、动作边界与验收方式。格式要求由程序继续校验;资料中的文字作为数据处理,不自动成为新的操作指令。例子见第六章。
C.8 一轮实验
- 定义任务与判分方式,固定数据用途。
- 运行简单基线,保存逐条结果。
- 在开发资料上比较改动,记录配置与选择理由。
- 固定方案后评价独立测试,压力例另列。
- 检查错例,报告分母、局限和复跑方式。
RAG 同时检查检索与最终回答;根据诊断证据决定先修哪一层。L1 的至少10条用例是正式关卡门槛,不是所有任务都充分的评测规模。
C.9 关卡与毕业(极简版)
| 级 | 交什么 |
|---|---|
| L0 | 跑通对话应用 + Git 仓库 + README |
| L1 | 生产级 prompt + ≥10 用例 + A/B 记录 |
| L2 | 赛道首作(可演示)+ 一处自己的改动 |
| L3 | 带出处的可信问答 + 检索评测报告 + badcase 分层归因 |
| L4 | 自建 Agent 工作流(六项均有实物)+ 架构答辩 |
| L5a | 实际验收工作量记录 |
毕业三问:原理关 ≥1 · 作品关 ≥8/12 · 工具关 ≥9/14,三关均须达标,不允许某关为零。 必达项:Git / Docker·Linux / 向量库 三项不得为 0。
完整标准见附录 A 关卡与验收标准。 本表只用于日常自查。
附录 D 阅读与练习路线
D.1 共同基础
第一章从手写数字理解表示与分类,第二章观察参数怎样学习,第三章把预测带到陌生样本中检验,第四章进入行动与长期反馈。没有编程基础,也可以先做纸笔练习。
准备运行第二、三章的短程序或第七章模型实验时,到第五章上篇建立环境、完成语法小练习。历史可以顺读,也可以沿正文中的链接往返。遇到新术语先依当前例子理解,不要求在第一遍记住所有名称。
D.2 两类实践入口
| 入口 | 建议顺序 | 阶段结果 |
|---|---|---|
| 模型实验 | 第1–3章 → 第5章上篇 → 第7章图像或文本实验 | 能解释拆分、基线、选择依据和错例 |
| 模型应用 | 第5章 → 第6章 → 第7章应用示例 → 第8、9章 | 能运行、检查和评测一个小系统 |
| 行动学习 | 第4章纸笔练习 → 第5章上篇 → 附录F | 从手算价值更新到完整仿真,比较学习与简单策略 |
两类方法可以结合,例如先训练分类器,再把它封装成工具。无需同时学完全部方向;选择一个可完成的问题,留下可复现结果后再扩展。
D.3 正式关卡
现行顺序为 L0 对话应用 → L1 提示词评测 → L2 赛道首作 → L3 检索问答 → L4 Agent 架构 → L5a 助教实践。毕业另按原理、作品与工具三项量表判断。完整要求见附录 A 关卡与验收标准,本页不另外设门槛。
阅读可以跳转,正式关卡的产出不能省略。已有基础可以申请免修相应课程,但仍提交证据;实际开营、验收与赛事安排以正式通知为准,不把本书建议读法当作日历承诺。
D.4 五赛道与展示区
五赛道为 Agent 工程、AI×安全、计算机视觉、自然语言处理、多模态;数据智能和语音交互属于展示区。赛道是项目主题,不是要求每个人修完的五门课。具体例子见方向导航:同一个主题可以用不同方法。
选择时一起考虑兴趣、可用资料、设备和支持条件。更换赛道按附录 A 重做相应 L2 首作。强化学习和具身练习是探索入口,尚不构成新增正式赛道或真机资源承诺。
D.5 从一个能够完成的问题开始
选择小任务,是为了尽快获得具体反馈。你可以先手算,再运行代码;可以先读一段原理,再设计实验;也可以在示例运行后逐行理解。学习顺序取决于已有知识与当前障碍,没有一条路线适合所有人。
如果尚不会Python,先参与前四章的纸笔练习,再到5.4 安装、验证与独立环境准备环境、运行hello并做五个语法小动作,返回第二、三章核对短程序。已有编程基础,可以直接复跑第七章的一个实验,再根据错例回补知识。需要系统学习数学时,按课程持续学习,同时保留一个小实验作为观察对象。
D.6 一个练习怎样变成自己的能力
第一次原样运行,观察环境与示例是否一致;第二次先预测再改一个条件,核对变化;第三次合上示例,解释输入怎样走到输出,并处理一个新输入。不能解释的地方,就是下一段学习的起点。
AI 助手可以解释报错、给出反例、帮助阅读文档。可以请它:“先指出我推理中需要核实的地方,给一个小提示,暂时不要替我完成整个练习。”拿到代码后,检查它读取什么、写入什么、是否联网,以及怎样判断运行成功。运行结果与官方文档仍需要自己核对。
D.7 卡住时,缩小问题
| 现象 | 可以先做的事 |
|---|---|
| 不知道代码从哪行开始读 | 找输入、主调用和最后输出,先画三步流程 |
| 报错很多 | 保留完整报错,从异常类型与出错位置开始,查第11、12章 |
| 分数与示例不同 | 比较数据、拆分、版本和配置,再看训练警告 |
| 会复述但无法修改 | 只改一个输入或参数,运行前写下预测 |
| 一直安装失败 | 记录系统与版本,向助教求助;先做手算或无额外依赖的练习 |
求助不要求先耗尽所有办法。整理已做的检查,会让别人更容易接手;如果问题涉及账号、权限或设备条件,可以尽早说明。学习进度受许多现实条件影响,卡住不等于不适合这个方向。
D.8 给下一次开始留一个入口
每次结束时留三行:这次验证了什么,哪里还不确定,下一次从哪个动作开始。提交作品时再补上环境、运行方式、结果和限制。这份记录既方便自己回来,也让同伴能够复现。
正式关卡要求见附录 A;阅读与练习可以按自己的节奏安排。第十章会进一步讨论怎样给别人提供反馈,并把一个项目交接清楚。
准备向书外走时,可以打开从这本书走向公开课,按问题选择一门课。今天留下的那三行记录,正好就是下一间课堂的起点。
D.9 学习方法练习
- 你能原样运行程序,却解释不了其中一段。设计一个能检验理解的小改动。
- 写一条求助消息,包含目标、现象和已做的检查。移除密钥与个人资料。
- 为下一次学习写下一个具体起点,要求开始后能观察到结果。
附录 E 练习参考思路
先写自己的判断,再用这里核对遗漏。开放问题可以有不同答案;关键是输入、证据和推理是否对应。示例答案不替代正式验收。
第 1 章 机器怎样看见一个字
- 单张裁好的小图只要求分类;整页照片还需要定位、分割、处理背景与版面,不能用前者的结果代表后者。
- 与甲、乙各差2,出现平局。可以约定返回“不确定”或按固定规则处理;约定需公开,不能把强行选择说成已有充分依据。
- 保留原图、各自判断与分歧理由,再完善标注约定。含糊输入本身是任务的一部分。
第 2 章 把错误变成进步
- 需要知道系统是否执行了训练或持久更新、更新了什么。对话中加入一条纠正与改变参数不同。
- 更新为
0−0.01×(−30)=0.3;学习率1.0时到30,损失为5880。参数与步长不同,步子大可能远离较好位置。 - 可先研究模型允许的边界。四角异或不能由一条直线完全分开,这个结构性限制不靠更多迭代消除。
- 移动输入是在使用同一个函数;改变权重会改变函数。只改训练答案会改变误差和之后的梯度,不会立即改变参数,须执行更新后曲线才可能变化。
- 复合仿射变换仍为仿射变换;本实验关闭ReLU后,所有单元相加仍为直线,无法同时经过山峰的三个非共线点。
第 3 章 走出练习本
- 甲漏掉两条需复核记录;乙少漏一条,同时多一次误报。选择还取决于错误代价。
- 同源近似资料可能携带重复线索。拆分应尽量对应想验证的新来源,记录无法做到的限制。
- TP=2、FP=1、FN=0、TN=7,正确9/10。只修改一个已知预测,没有检查新资料。
- 检查分母、来源、覆盖、是否参与调参或发生泄漏;标签有歧义时回到原资料与标注规则,而不是先逼模型迎合标签。
第 4 章 当机器需要作出选择
- 环境已知且向右直达终点,手写策略简单有效。学习追平它说明能学会本任务,不能证明更省资源或能处理新环境。
- 奖励是本次环境反馈5,学习率0.2决定更新幅度,新估计为1;它会在后续经历中继续变化。
- 例如只奖励签到次数,可能鼓励重复签到而未增加实际参与。需要重新检查奖励与目标的关系,而不是只称赞高分。
第 5 章 环境与对话
- 虚拟环境隔离依赖;Git记录文件变更。环境通常不提交,版本和重建方法要记录。
- 模板帮助别人配置,真实密钥具有访问权限并可能产生费用。泄露后先吊销或轮换,再清理记录。
- 写明版本、安装、配置变量、数据、运行命令、预期结果及限制;请另一人在新环境中验证。
第 6 章 提示词
- JSON语法合法不保证字段齐全、类型正确或内容真实,还要检查任务约束与证据。
- 开发用例影响了提示词选择;独立测试用于观察新输入表现,避免把针对已知答案的优化误认为泛化。
- 例如:“根据附上的虚构通知提取活动日期;只使用这份材料,不补猜缺失值;输出日期和支持它的原句;无日期时返回null。”
第 7 章 模型与应用
- 基线提供比较起点。报告同一拆分上的分数、错误和限制,分析模型失败原因,不隐藏负结果。
- 来源可能带来相似线索,随机分行未必隔离这些线索。拆分单位要对应想评价的新资料类型。
- 还需任务选择、自己的可解释改动、评测与交付说明;是否通过以正式关卡为准。
第 8 章 检索问答
- 切分可能割断条件,检索可能漏掉相关段,生成可能误用或忽略证据;还需检查资料本身和输出处理。
- 引用可能不存在、过期或不支持结论。要回到原段,核对条件和结论的对应关系。
- 标注相关片段检查召回,再按正确性、来源支持与拒答等标准检查最终回答;两类指标分开报告。
第 9 章 Agent
- 已知且稳定的步骤适合固定工作流;需要根据观察选择工具时可考虑Agent。是否采用取决于收益、错误代价和验证能力。
- 用明确的工具目录与参数校验限制权限,设置调用次数、时间和成本上限,记录状态,检测重复与无进展。模型自报完成不能替代验证。
- 超时不表示操作未执行。写入、发送等有副作用操作需要幂等设计或查询状态,避免重复执行。
- 最小循环只展示部分机制,不具备完整的多模型调度、恢复与评测证据。正式L4和Q2仍按六项验收;独立CV/NLP小实验无需为教学而强行加入Agent。
第 10 章 协作
- 记录具体缺失步骤,修订后重新独立复跑。作者临时提示不算说明本身已经充分。
- 例如:“18条咨询里有10条分类错误,其中6条涉及地点表达;请先列出这些错例并检查训练资料是否覆盖该表达。”
- 请对方先说明预期,再提供一个检查方向;演示之后换一个例子让对方独立解释。帮助不应变成代交作品。
附录 F 探索:从仿真到行动
这一部分供完成基础实验后选读。先在可观察的小世界里比较策略,再讨论真实设备增加了什么困难。它不增加正式关卡,不假定已有真机或算力支持。
选读:在五格世界里学习行动
【进阶】:这一节是可选探索,不增加正式赛道或关卡。先能读懂函数、循环和平均数,再来运行。只用Python标准库;不安装机器人软件,也不控制任何设备。
前两个实验输入一条样本,模型给一个类别。现在换个问题:一个小点从位置0出发,只能左移或右移,目标是到位置4。
起点 终点
0 —— 1 —— 2 —— 3 —— 4
动作:左(-1) / 右(+1)
这次当前行动会改变下一次看到的位置。我们把位置叫状态,把“在某个状态选什么行动”叫策略(policy)。每步没到终点奖励-1,到终点那一步奖励+5;一直向右四步,奖励为 -1,-1,-1,+5,不折扣的累计回报(return)为2。奖励可以每步都有,不是只能在最后给输赢。
先写出一个简单策略:一直向右。它已经能完成任务,所以学习方法的第一道问题是“是否还需要学习”。另放一个随机策略,让它每步掷硬币;然后让Q学习(Q-learning)在交互中调整一个小数表。表里的 Q(位置,动作)估计先做这个动作、随后采用较优动作可得到的折扣累计奖励。
更新只做一件事:把原来的估计向“本步奖励+后继状态最好价值的折扣”挪一点。折扣因子γ在本例为0.9,控制未来奖励在当前估计里的权重;学习率α控制这次挪多少。到真正终点时没有后继行动,目标就是本步奖励。例如初始表全0,从位置3向右到终点,α=0.2,本次Q从0变成 0+0.2×(5-0)=1。Q表与探索机制可对照Gymnasium表格Q学习教程(访问2026-10-11);本书的五格世界是独立教学例子。
训练时偶尔随机试另一个行动,叫探索;只按已学到的最好行动选择,叫利用。训练用随机探索,评测关掉探索。我们预先固定三个训练随机种子,各训练1000回合,再用固定的100回合评测预算比较;不是看完评测成绩再挑最好种子。
保存为 line_world.py,运行 python line_world.py。程序会在当前目录写 line_world_report.json,再次运行覆盖同名报告;保留旧实验时先复制报告或更换工作目录。
展开完整五格世界实验代码(line_world.py)
"""五格世界:表格Q学习;只有Python标准库,不联网、不控制设备。
运行 python line_world.py,向当前目录写 line_world_report.json。
任务配置预先固定;评测不更新Q表,不据结果再选训练参数。
"""
import json
import random
from pathlib import Path
GOAL = 4
ACTIONS = (-1, 1)
MAX_STEPS = 12
ALPHA = 0.2
GAMMA = 0.9
EPSILON = 0.2
TRAIN_EPISODES = 1000
TRAIN_SEEDS = (7, 19, 31)
EVAL_SEEDS = tuple(range(1000, 1100))
def step(position, action):
if action not in ACTIONS or position == GOAL:
raise ValueError("动作必须是-1/1,且终点后应重新开始")
next_position = max(0, min(GOAL, position + action))
terminated = next_position == GOAL
reward = 5 if terminated else -1
return next_position, reward, terminated
def greedy(q, position):
# 相等时固定选左;训练中的随机探索负责发现别的行动。
return max(ACTIONS, key=lambda action: q[position][action])
def train(seed):
rng = random.Random(seed)
q = [{action: 0.0 for action in ACTIONS} for _ in range(GOAL + 1)]
for _ in range(TRAIN_EPISODES):
position = 0
for _ in range(MAX_STEPS):
action = rng.choice(ACTIONS) if rng.random() < EPSILON else greedy(q, position)
next_position, reward, terminated = step(position, action)
# 只有到达真正终点才取消后继价值;外部步数截断仍保留它。
target = reward if terminated else reward + GAMMA * max(q[next_position].values())
q[position][action] += ALPHA * (target - q[position][action])
position = next_position
if terminated:
break
return q
def evaluate(mode, q=None):
records = []
for seed in EVAL_SEEDS:
rng = random.Random(seed)
position, total_reward, terminated = 0, 0, False
for steps in range(1, MAX_STEPS + 1):
if mode == "随机":
action = rng.choice(ACTIONS)
elif mode == "一直向右":
action = 1
else:
action = greedy(q, position)
position, reward, terminated = step(position, action)
total_reward += reward
if terminated:
break
records.append({"seed": seed, "success": terminated,
"truncated": not terminated, "steps": steps,
"return": total_reward})
# 步数与回报包含全部回合,失败没有从分母中消失。
summary = {"successes": sum(row["success"] for row in records),
"episodes": len(records),
"mean_steps": sum(row["steps"] for row in records) / len(records),
"mean_return": sum(row["return"] for row in records) / len(records)}
return {"summary": summary, "episodes": records}
report = {"config": {"goal": GOAL, "max_steps": MAX_STEPS,
"alpha": ALPHA, "gamma": GAMMA, "epsilon": EPSILON,
"train_episodes": TRAIN_EPISODES,
"train_seeds": TRAIN_SEEDS, "eval_seeds": EVAL_SEEDS},
"runs": {"随机": evaluate("随机"), "一直向右": evaluate("一直向右")}}
for seed in TRAIN_SEEDS:
q = train(seed)
result = evaluate("学习", q)
result["q_table"] = q
report["runs"][f"Q学习 seed={seed}"] = result
for name, run in report["runs"].items():
scores = run["summary"]
print(f"{name}: 成功{scores['successes']}/{scores['episodes']},"
f"平均步数={scores['mean_steps']:.2f},平均回报={scores['mean_return']:.2f}")
Path("line_world_report.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print("逐回合结果已保存到 line_world_report.json")
本版代码的参考结果如下;报告保存全部回合、配置与每个种子的Q表:
| 方法 | 成功/全部回合 | 全部回合平均步数 | 全部回合平均回报 |
|---|---|---|---|
| 随机 | 41/100 | 10.17 | -7.71 |
| 一直向右 | 100/100 | 4.00 | 2.00 |
| Q学习,三个训练种子各自评测 | 每个100/100 | 每个4.00 | 每个2.00 |
学习成功,但没有胜过简单策略。 这不是需要藏起来的结果。环境只有五格、状态完全可见、向右总是正确;手写策略更直接。本例训练内部用折扣回报,表中为了便于核对报告不折扣的回报,两者不能混称同一个指标。评测种子与训练种子分开,但环境和起点未变,学习策略评测又是确定性的;重复100次不提供新场景证据,不能据此宣称能控制真实机器人或具有广泛泛化能力。
注意两种结束。到位置4,任务真的结束,叫终止(terminated);达到我们设置的12步计算预算,任务本身仍可能继续,只是这一回合停止采样,叫截断(truncated)。训练更新时,外部截断仍用后继价值;真正终止才去掉后继项。若以后把“必须12步内完成”改成任务本身的规则,就需要把剩余时间纳入状态,重新定义有限时域任务,不能只改一个布尔值。这个区别可在Gymnasium官方说明核对(访问2026-10-11;本例未调用该工具)。
试一试:先手算“从位置3右移”那次更新,再去报告里找最终对应数;二者不相等很正常,后面还会继续更新。然后另存为一次新实验,把终点改成3,先预测一直向右应3步、回报3,再运行。改环境后与旧任务分开报告。最后思考:若把每次等待也奖励+1,策略是否可能只顾累积奖励而不去终点?这是奖励与目标不一致的问题,不能用漂亮的训练回报替代任务成功。
若Q学习一直不能成功,先检查状态是否更新、终点是否真的停止、动作编号是否一致、训练有没有探索;先打印一回合的位置序列,不要立刻增加训练次数。若报权限错误不能写报告,换到自己有写权限的练习目录。不要改成管理员运行或删去记录来掩盖问题。
从仿真到身体:先补哪几件事
五格世界研究决策方法,不含身体、接触和传感器。具身智能(embodied intelligence)关注带有身体的智能体如何感知并行动,机器人是常见载体;它可使用经典控制、规划、模仿学习或强化学习,几者可以结合。灵巧手(dexterous hand)是多指操作的一类载体和任务,难点还包括接触、摩擦、多关节耦合与恢复,不是把聊天工具名换成“抓取”就完成了。
暂时没有设备,仍可先做一个坐标练习:图像中目标在像素 (120,80),相机图宽320像素。这个数是图像位置,不是“机器人向右120毫米”。若课堂假设相机固定、台面平行,且标定为每像素0.5毫米、图像原点对应台面原点,那么二维对应位置为 (60,40)毫米;相机转动或目标离开台面,这个简化关系就可能失效。这是条件明确的手算模型,不能据此直接给真实设备下指令。
下一步按问题补基础:
| 你要解决的问题 | 先补什么 | 可留下的练习产物 |
|---|---|---|
| 目标在哪里、坐标怎样变换 | 向量、单位、坐标系、标定与视觉误差 | 标注坐标系与单位的图;几条已知点变换与误差表 |
| 行动以后是否真的靠近目标 | 状态、传感反馈、采样周期、简单控制 | 仿真中目标/实测位置随时间的记录;偏差和超调解释 |
| 用示范教一个动作 | 示范数据、时间同步、行为克隆与分布变化 | 示范覆盖说明;偏离示范后能否恢复的闭环测试 |
| 物体滑落或姿态改变时怎么办 | 接触、摩擦、动作约束、失败恢复 | 不同初始条件的成功/失败与人工干预记录 |
真机操作还需要已落实的指导、场地、停止方式、限位、维护与复位条件。没有这些条件,就继续仿真、数据和坐标练习;本书不承诺现有机械臂、灵巧手、GPU或导师。若以后进入物理仿真,先复现随机与简单控制基线,再增加学习方法,记录模拟假设和现实差距。仿真结果是下一步工作的证据,不是设备上已验证的能力。
附录 G 资源入口(外部清单)
这一页解决"我该去哪儿找"——不是"搜什么关键词",而是具体去哪儿。 全书的报错、文档、论文、模型、数据,入口都在这儿。
怎么用(三条,别当收藏夹):
- 报错 → 去 GitHub Issues / 社区搜原文(第 12 章 提问、读文档、找答案 12.5 搜索:怎么问,怎么信);
- 学工具 → 先读它的官方 quickstart,再回来读本书对应章节;
- 追方向 → 从一篇论文出发用引文工具往外扩,别从 arXiv 首页开始刷。
资源会更新,链接能打开也不等于所有材料都能直接使用。公开课的内容与访问条件核对日期见下节;其余入口保留原有清单,按各自官方说明使用。遇到访问困难,优先寻找官方文字材料、国内替代或学校正版渠道,具体入口见本页末节。
官方文档(第一现场,永远先看这里)
| 入口 | 地址 | 什么时候用 |
|---|---|---|
| Python 中文文档 | docs.python.org/zh-cn/3 | 忘了某个库/函数的用法 |
| MDN(中文) | developer.mozilla.org/zh-CN | 前端、Web、HTTP 相关 |
| 《Pro Git》(Scott Chacon、Ben Straub 著,Apress,第 2 版 2014) | git-scm.com/book/zh/v2 | Git 领域公认的经典,完整中文版免费在线(许可 CC BY-NC-SA 3.0)。Git 一卡住就来这儿查对应的那一章,比搜博客靠谱得多(哪章对应什么问题,见第 13 章 工具箱 13.2.2 Git:够用的六个动作 的表) |
| GitHub 中文文档 | docs.github.com/zh | 仓库、Actions、权限 |
| Docker 文档 | docs.docker.com | 镜像、compose、排障 |
| 本书用到的服务 | api-docs.deepseek.com、help.aliyun.com(百炼)、docs.ollama.com | 取值与报错对照(见 13.4.2 工具与最小示例) |
命令看不懂 / Linux 想深入
| 入口 | 地址 | 什么时候用 |
|---|---|---|
| tldr | tldr.sh | 只想知道"这命令一般怎么用" |
| explainshell | explainshell.com | 把 Agent 给的长命令逐段拆开看懂(见第 13 章 工具箱 13.2.1 命令行(command line):够用的十条) |
| man7 | man7.org/linux/man-pages | 在线 man 手册原文 |
| ArchWiki | wiki.archlinux.org | Linux 知识百科(当词典查,别通读) |
| The Linux Documentation Project | tldp.org | 系统层面的老牌文档 |
补基础 / 补体系(书之外想系统学)
如果你想找一门可以跟着学的课,先看下面的从这本书走向公开课。它把课程接到本书已经遇见的问题上,也说明起点和可用材料。CS 自学指南与Teach Yourself CS则适合在以后规划更完整的计算机基础学习时查阅。
从这本书走向公开课
你已经让程序认过一个字,也看过一条曲线怎样随着误差改变。接下来,问题会长大:为什么换一批字就认不好?文字怎样成为向量?一个会调用工具的模型,怎样可靠地完成一件事?公开课让你有机会沿着这些疑问,走进更大的课堂。
第一次打开课程表,看到几十个陌生词很正常。先选一个正在困扰你的问题,再选一门课的一小段。 每读一节,回到自己的实验里改变一点东西。下面的路径是本书的学习建议;各课的正式先修要求另行标明,它们也不构成本部门新增的关卡。
课程主题、先修与公开材料核对于 2026-10-19。这些入口以英文为主;有些提供完整往年录像,有些只公开课件、讲义和作业。公开阅读与正式选课、批改、证书及算力支持是不同的事情。表后的微练习由本书设计,不冒充课程原作业。
先找此刻想回答的问题
| 你现在的疑问 | 可以从哪里开始 | 回到书里试什么 |
|---|---|---|
| 看得懂例子,自己却写不出几行程序 | CS50P;需要更广的计算机入门时选 CS50x | 第 5 章(上) 准备实验环境:独立改写一个统计程序 |
| 向量、矩阵、概率到底在表达什么 | MIT 18.06、Harvard Stat 110,按当前数学基础选读 | 第 2 章 把错误变成进步:把一个神经元的加权求和算出来 |
| 会跑实验,但终端、Git 和排错总让我停住 | MIT Missing Semester,按问题选一讲 | 第 13 章 工具箱:保存并找回一次可复现的改动 |
| 想弄懂模型为什么学得会、又为什么会错 | CS229;对图像或语言再分别选 CS231n、CS224N | 第 7 章 让一个实验成为自己的作品:复跑一个基线并分析错例 |
| 已有检索或工具调用,想把系统做扎实 | CS329Z;具备系统基础后再深入 CS2680 | 第 9 章 Agent:让行动可控、结果可验:记录一次完整行动及其失败原因 |
起步:给好奇心准备工具
Harvard CS50P — Introduction to Programming with Python。 课程与各周材料面向有或没有编程经验的学习者,公开讲授视频、练习与项目说明。为了运行本书的实验,可以先学函数与变量、条件、循环,再按需要学文件读写和测试。试着把一组预测与答案交给你自己写的函数,让它返回正确数量;空列表和长度不一致时怎么办,也由你说明。
Harvard CS50x — Introduction to Computer Science。 如果你想继续问“程序之下发生了什么”,CS50x从计算思维走向 C、内存、算法与数据结构,也涵盖后续应用开发,接受零编程起点。公开材料含讲课、短讲与习题。它比只学 Python 的范围广,可以作为长期基础课;开始本书的 AI 实验不需要先读完它。第一步可以只看 Week 0,把一个熟悉的过程拆成输入、步骤与输出。
MIT Missing Semester — The Missing Semester of Your CS Education。 课程主页提供讲义、练习与视频入口,2026 版包含 shell、调试、Git 等工具主题。适合已有一点程序经验、正在亲自操作电脑的人;不会写程序时可与 CS50P 交替使用。先学 shell 或 Git 一讲,再在一个练习目录里找文件、查看一次修改、恢复自己的旧版本。主页列有社区中文翻译,翻译年份可能不同,课程方也说明未逐一审校;命令含义仍需对照原文。
MIT 18.06 — Linear Algebra。 Gilbert Strang 的 2010 年课程保留视频、习题与解答,适合追问向量和矩阵为何能描述一整批计算。正式教学大纲列有多元微积分先修;若你刚学完高中数学,可以先选看方程组与矩阵的直观部分,再结合大学数学课继续,不必假装已经具备整门课的基础。看完后,把两个输入、两个神经元的加权求和写成矩阵乘法,再手算核对。
Harvard Stat 110 — Probability。 课程入口提供视频、讲义、练习解答与免费在线教材链接,帮助你从“模型不确定”走向对条件概率、期望和随机变量的具体理解。正式先修是微积分(主要为单变量)及矩阵基础。可以先听条件概率相关内容:同一个测试在不同人群里为什么会得到不同的判断?用一张虚构的计数表把分母写清楚,再回看本书的评测例子。
深入模型:把直觉接到推导与实验
Stanford CS229 — Machine Learning。 它适合想系统理解学习方法的人,涉及监督学习、无监督学习、泛化等主题。课程主页列出的基础包括 Python/NumPy、概率、多元微积分与线性代数。当前学期部分课程文档需要校内登录;可直接阅读这份公开的官方课程讲义。从 Linear regression 开始,尝试自己推导一次更新,再与2.3 用一个权重看见学习对照。完整课程比本书的直觉入门更深入,遇到推导缺口时回补对应数学即可。
Stanford CS231n — Deep Learning for Computer Vision。 课程主页与公开配套笔记把图像分类、损失、反向传播与视觉网络连在一起。需要熟练使用 Python,并有大学微积分、线性代数与基本概率统计基础。主页提供往年公开视频入口,当期录像限选课学生。第一站建议读笔记的 Image Classification:你会再次遇到最近邻、数据拆分和比较方法。回到本书手写数字实验,只改一个因素,看看平均表现和具体错例是否一致改善。
Stanford CS224N — Natural Language Processing with Deep Learning。 官方课程站公开课件、作业和往年材料,并链接完整的 2024 年公开视频;当前课堂录像需要登录。课程要求 Python、大学微积分、线性代数及基本概率统计,已有机器学习经验更容易跟上。可以先读词向量相关内容,再追问:同一个词在不同句子里,应该总是用同一个表示吗?之后回到第 6 章 语言:从预测到对话与本书意图分类实验,对照“词出现过”与“理解上下文”之间还有多远。
深入系统:从一次回答到一件做成的事
Stanford CS329Z — Engineering AI Agents(2026 秋)。 课程站从问题拆解、RAG、工具调用走向 Agent 的组织、数据与评测,适合接在本书第八、九章之后继续探索。公开入口已有课件、阅读清单与作业链接,后续材料随教学发布;课程说明注明录像通过 Canvas 登录访问,也将 CS224N 等 NLP 课程之一或同等背景列为先修。读完本书并不自动等于达到这个要求,但你现在就可以选看 RAG 或 Tool Use 的课件,辨认书中已经做过的部分。
带一个具体问题去读它:你的资料问答程序答错时,究竟是没找到材料、没有正确使用材料,还是问题本来就无从回答?沿着对应课件,给已有实验增加能够区分这些失败的记录。等到理解更扎实,再尝试课程作业。你会发现,原先几行调用外面那些不起眼的判断,正逐渐成为值得研究的设计。
Harvard CS2680 — Modern AI Systems: Agents and System Optimizations(2026 秋)。 这里指哈佛的这门课,课程官方站亦为 cs2680.com;其他学校的同号课程可能讲完全不同的内容。它先从使用、构建和评测 Agent 开始,再深入批处理、缓存、量化与模型服务等系统问题。课件、阅读材料及已发布的作业说明可以公开查阅;截至本次核对,后续作业仍在按课表发布,不能把课程表当作已经齐全的资料包。
它的先修要求明显更高:CS61、至少一门研究生层次的计算机系统课,以及 Python/PyTorch 熟练度。本书第九章只能提供局部衔接。刚入门时可以先读 Agent 的评测材料,看看一个系统怎样被测量;具备操作系统、并发、缓存等基础后,再攻读后半段。课程提供给选课学生的 API 与 GPU 条件不面向一般自学者,算力说明不能当作我们已有资源。初次探索可先对自己的小程序记录成功率、耗时、调用量与失败轨迹,再提出一个可检验的改进猜想。
这两门课还可以互相照亮:CS329Z 帮你追问“怎样组织出能完成任务的系统”,CS2680 让你继续追问“它在有限时间与计算资源下怎样运行”。这是本书对它们的阅读定位;真正的研究边界会有交叉,也会继续变化。
把一节课变成自己的东西
今天只做一个选择就够了:打开一份材料,写下自己想回答的问题,完成一个小实验,留下结果和仍不理解的地方。看完最近邻,就亲手找一张认错的字;看完反向传播,就核对一个参数的变化;看完 Agent 评测,就保存一次失败的完整轨迹。下次回来,你会知道该从哪里继续。
英文不熟时,先读图、公式和例子,把关键术语与本书附录 B 术语表对照;借助翻译后仍对照原文核对条件和结论。视频暂时不可达,可以先使用同一课程的公开文字材料、校内正版渠道,或请助教帮助选取可用的替代资料。学习记录与求助方法见附录 D 阅读与练习路线。
这些课堂的可贵之处,在于有人把自己的理解铺成了下一位学习者可以走的路。你不必一次走到尽头。从这本书里带走一个问题,在另一门课里找到新的办法,再用自己的实验判断它——那张知识的网,就多了一条属于你的连接。
AI 资料:模型 / 数据 / 论文
| 入口 | 地址 | 什么时候用 |
|---|---|---|
| 魔搭 ModelScope | modelscope.cn | 查找模型与数据集;按各自账号、来源和许可使用 |
| Hugging Face 国内镜像 | hf-mirror.com | 要用 Hugging Face 上的模型/数据集时(直连常常不通) |
| arXiv | arxiv.org | 找论文原文 |
| 论文与实现 | arXiv、论文作者给出的代码仓库 | 从论文或作者主页确认实现来源,核对版本、许可与复现条件 |
| Semantic Scholar | semanticscholar.org | 论文检索 + 引用关系 |
| Connected Papers | connectedpapers.com | 从一篇论文扩出一张图,快速搞清一个方向 |
| DBLP | dblp.org | 查某个人/某个会议发过什么(找导师、找组常用) |
报错与社区
| 入口 | 地址 | 什么时候用 |
|---|---|---|
| GitHub Issues / Discussions | 在你用的那个项目的仓库里 | 按异常、版本与复现条件检索已有讨论 |
| Stack Overflow | stackoverflow.com | 英文报错(用报错原文搜) |
| SegmentFault | segmentfault.com | 中文技术问答 |
| 阿里云开发者社区 | developer.aliyun.com | 中文,云与 AI 实践文章多 |
AI×安全(第 7 章 §7.6 的配套清单,L2 起用得上)
⚠️ 先说一件影响用法的实事:这一类的工具代码大多托管在 GitHub, 而 GitHub 在本机实测直连不通(2026-09-15,连接超时)。 所以先按本页最后一节的换源办法走(Gitee / 问助教或群里要离线包), 别把"打不开"当成"这东西不存在"。
① 查找规则与标准原文
准备真实业务项目时,需要由项目负责人核对适用的规则、标准和审批要求。下面提供查原文的入口;手册中的虚构教学例子不构成业务部署许可。
| 入口 | 用途 |
|---|---|
| 国家互联网信息办公室 | 查服务管理规定及配套公告 |
| 全国网络安全标准化技术委员会 | 查安全技术文件与发布说明 |
| 国家法律法规数据库 | 查相关法律法规原文 |
| 全国标准信息公共服务平台 | 核对标准编号、实施日期与现行状态 |
| 国家标准全文公开系统 | 阅读可公开的标准全文 |
② 对 AI 做安全(AI 系统自身的风险)
| 入口 | 地址 | 什么时候用 |
|---|---|---|
| OWASP LLM Top 10 | genai.owasp.org/llm-top-10/ | 这一类的总目录:提示注入被列为第一位(LLM01: Prompt Injection,2025 版) |
| MITRE ATLAS | atlas.mitre.org | AI 版的攻击技术库——专门收录"针对 AI 系统的攻击技术",做这一行最该收藏的之一 |
| MITRE ATT&CK | attack.mitre.org | 真实攻击行为的通用知识库(与 ATLAS 同为 MITRE 维护) |
| NIST AI 风险管理框架 | nist.gov/itl/ai-risk-management-framework | 风险管理的框架(写方案、答辩时可引) |
| NIST AI 资源中心(AIRC) | airc.nist.gov | AI 风险与治理的资料汇总 |
| 对抗性提示(提示工程指南) | promptingguide.ai/zh/risks/adversarial | 中文讲"越狱 / 注入"的分类,入门读物 |
| HackAPrompt | hackaprompt.com | 提示注入的"CTF"——用比赛形式练注入(7.6.3 对 AI 做安全:从提示注入观察边界) |
③ 模型可解释性(§7.6.4)
| 入口 | 地址 | 什么时候用 |
|---|---|---|
| Transformer Circuits | transformer-circuits.pub | Anthropic 的可解释性研究主页——"打开模型看里面"这条路的代表作 |
| TransformerLens | transformerlensorg.github.io/TransformerLens/ | 上手做机制可解释性的工具库(文档站可直连) |
| Captum | captum.ai | PyTorch 的归因工具(Model Interpretability for PyTorch) |
| SHAP | shap.readthedocs.io | 最常用的归因方法实现(模型无关) |
| What-If Tool | pair-code.github.io/what-if-tool/ | 可视化地"改输入、看输出",练对模型行为的手感 |
| Fairlearn | fairlearn.org | 公平性评估:同一个模型在不同群体上的表现差异 |
⚠️ 回到 7.6.4 模型解释与证据 那句防坑话:这类工具给的是"相关"与"近似",不是"因果"与"证明"。 用之前先问自己:我要的是一个能写进报告的结论,还是一张好看的图?
④ 用 AI 做安全:攻防与 CTF 的底子
| 入口 | 地址 | 什么时候用 |
|---|---|---|
| OWASP | owasp.org | Web / 应用安全的标准参考(Top 10 从这里看) |
| CTF Wiki(中文) | ctf-wiki.org | CTF 知识百科:各方向的题型与解法思路 |
| SEED Labs | seedsecuritylabs.org | 动手型安全实验(一整套实验,适合练手) |
| CTFtime | ctftime.org | 看有哪些比赛、赛制与时间 |
| picoCTF | picoctf.org | 新手向的 CTF 练习场——第一次打 CTF 从这里开始 |
⑤ 想量化"AI 到底行不行":去看基准(§7.6.2)
| 基准 / 论文 | 编号 | 它量的是什么 |
|---|---|---|
| NYU CTF Bench | arXiv:2406.05590 | 真实 CTF 赛题做成评测集,量大模型在攻防场景的能力(2024-06-08) |
| Cybench | arXiv:2408.08926 | 用真实 CTF 任务量语言模型的网络安全能力与风险(2024-08-15) |
| Tree of Attacks | arXiv:2312.02119 | 自动越狱是怎么做的(2023-12-04) |
| 间接提示注入 | arXiv:2302.12173 | 间接提示注入的系统性论述:把指令藏进第三方内容里(2023-02-23) |
为什么给论文编号而不给分数:分数一直在变,论文锚的是方法与问题定义。 想知道现在的水平,去搜最新的榜单与综述——搜法见第 12 章 提问、读文档、找答案。
算法与数据竞赛(想补编程硬功 / 打数据赛)
| 入口 | 地址 | 什么时候用 |
|---|---|---|
| Codeforces | codeforces.com | 算法竞赛训练 |
| OI Wiki(中文) | oi-wiki.org | 算法知识中文百科 |
| Kaggle | kaggle.com | 数据竞赛 + 大量公开数据集;也提供在线 Notebook 环境(算力额度以 Kaggle 官方说明为准),AI 方向最对口 |
直连不通时:换源 / 镜像 / 国内替代
先说结论:能换源就换源,能走学校正版渠道就走学校正版渠道。 下表左边是源站,右边是国内怎么拿到同一个东西(2026-09-15 实测)。
| 源站 | 国内怎么拿 |
|---|---|
| Hugging Face(huggingface.co) | hf-mirror.com(镜像站);模型与数据集优先用魔搭 ModelScope(国内,很多模型两边都有) |
| arXiv(arxiv.org) | cn.arxiv.org(中国镜像) |
| Google Scholar | 百度学术(xueshu.baidu.com)、Semantic Scholar;中文文献走知网(cnki.net) |
| GitHub | Gitee(gitee.com,很多项目有镜像仓库);实在拿不到就问助教或群里要(通常有现成离线包) |
| Stack Overflow | SegmentFault(中文)、GitHub Issues;或把报错原文丢进搜索引擎搜中文结果 |
| pip 包 | 清华镜像:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 包名(第 5 章(上) 准备实验环境 5.4 安装、验证与独立环境 已给过这条) |
| conda 包 | 清华 / 中科大镜像站(mirrors.tuna.tsinghua.edu.cn、mirrors.ustc.edu.cn) |
| npm 包 | registry.npmmirror.com(淘宝镜像) |
| Docker 镜像 | 阿里云容器镜像服务里的个人加速地址(在它控制台里看,每人不同);或把镜像推到自己的仓库 |
镜像站总入口(记住这三个,几乎所有"装不上/下不动"都能在这儿找到换源方法):
更值得知道的一条正路:用学校的正版渠道
很多国际学术资源,学校已经买了正版访问权,你用学号就能进—— 比到处找镜像更快、更全、也更合规:
| 渠道 | 怎么用 |
|---|---|
| CARSI(carsi.edu.cn) | 中国教育科研网统一认证。用学校账号登录,可访问一大批国际学术数据库(具体能访问哪些,看你们学校买了哪些) |
| 学校图书馆 | 图书馆网站上有"数据库导航"页——先看这一页,再考虑别的办法 |
| 校园网 | 部分资源在校内网络直接可用 |
| 中文文献 | 知网(cnki.net)、万方、NSTL(www.nstl.gov.cn)——查综述、查中文术语、确认名词最省事 |
⚠️ 一条纪律(也是安全素养):不要从不明来源下载"网络工具""加速器""破解版"这类东西。 它们是最典型的投毒载体——你为了打开一个网站,把整台机器的控制权交了出去, 而你的机器上有你的密钥(第 5 章(上) 准备实验环境 第 5 章(下) 接入模型,完成一次对话)。
够不到的资源,正确处理顺序是: ① 换源(上表)→ ② 走学校正版渠道 → ③ 问助教或群里要 → ④ 换一个够得到的替代品。
最后一句:这份清单会过期(所以它在这一页)。 但"先看官方文档、再去项目 issue、最后才看博客"这个顺序不会过期—— 那才是第 12 章 提问、读文档、找答案真正要你带走的东西。