Skip to content

全书动手核验清单 ​

小a的同事问他:“这本书你读完了,可你凭什么说你懂了?”

小a愣了一下。他确实读完了所有章节,也跟着源码走查了一遍,但“读过”和“懂了”之间还差着什么。他回去找老z。

“差的就是动手核验。”老z把各章末尾的练习汇总成这本清单,“读完不算数,做出证据才算数。每章末尾的动手核验都要求你自己产出可观察的结果——不是回答‘它做了什么’,而是证明‘我让它做了,而且我看到了’。本附录服务全书:概念篇用于检验边界理解,源码篇用于复现调用证据,实现篇用于验证产物。不要把模型一次输出当作结论。”

“这本清单怎么用?”小a问。

“按顺序做,每一项都要落三个东西:做了什么、看到了什么、说明了什么。”老z说,“‘做了什么’是操作,'看到了什么'是证据,'说明了什么'是结论。三项缺一项,这个核验就不完整——尤其别跳过中间那项直接写结论,那等于拿猜的当验的。做完一项,在这一项旁边打钩并写上证据摘要,这本清单才成为你的记录,而不是别人的文字。”

小a:“那我是不是每一项都要做到,才能算‘懂’?”

“不是每一项都必须,但每一类至少要有一项做扎实。”老z说,“这本清单分三类:概念核验验证边界理解,源码走查验证阅读证据,实现核验验证产物可运行。三类各做一两项,你就同时证明了概念、阅读和动手三个层面的掌握;只挑自己会的做,那是表演,不是核验。三类都碰过,这本书才算真的翻完了。”

概念核验:边界不是定义,是测试 ​

老z把概念篇的核验归为三类,每一类都对应一个容易混淆的边界。

“概念篇的核验有个共同特点,”老z说,“**每一项都在测一条边界,而不是测一个功能。**威胁模型测‘概念边界’,检索测‘忠实性边界’,工具与循环测‘权限和停止边界’。做的时候盯着边界看:哪个测试把边界显式画了出来,哪个测试就把概念变成了可观察的行为。”

威胁模型与安全边界 ​

  1. 为一个“写文件 Agent”列威胁模型:列出资产(要写的文件、工作区外文件)、攻击面(用户输入、工具参数、提示注入)、主体(Agent 进程、用户、模型)与可接受损失。证明威胁模型、授权、审批、隔离是四个不同的概念——它们互补但不可互换。
    • 预期:你能指着清单说出"这一项属于威胁模型,那一项属于授权",而不是把它们混成一团。
    • 失败信号:如果你发现某条同时属于两个类别,说明类别边界没划清。
  2. 设计一个“可外发网络请求”的工具:为它写输入 schema、确认记录与拒绝结果。验证仅靠提示词中的安全规则不能强制执行——模型可以无视提示,但工具执行器拒绝时模型无法绕过。
    • 预期:移除提示词中的安全规则后,工具执行器仍然拒绝危险请求;移除执行器检查后,模型会尝试越权。
    • 失败信号:如果模型"听话"只靠提示词,说明你的安全全在软约束上。
  3. 构造一段提示注入:把恶意指令藏在一个工具返回的文件内容里,观察模型是否被诱导。记录模型的实际行为,而不是假设“模型应该能识别”。
    • 预期:至少能看到一次模型被注入影响的行为(哪怕只是语气变化),以此证明注入是真实风险。
    • 失败信号:如果你"假设模型不会被骗",那这个测试就没做。

“前三项做完,合起来看一遍,”老z说,“它们共同验证的是**‘安全靠层,不靠词’**——第一项把四层概念分开,第二项证明工具层是硬约束而提示词是软约束,第三项证明提示词这条软约束确实会被攻破。**三根探针,探的是同一个地质:安全结构到底有多深。**如果你做完发现‘原来我的安全全在提示词里’,那这三项就给你交了一张准确的地质图。”

检索与引用 ​

  1. 用十篇带日期与权限标签的文档测试关键词检索、向量检索和混合检索。人工核对引用——验证召回的不只是文本相似,元数据(日期、权限)是否被正确使用。
    • 预期:三种检索各有漏召——关键词漏语义近似,向量漏精确术语,混合最好但仍非完美。
    • 失败信号:如果某一种检索"全对",检查你的测试样本是不是太简单。
  2. 设计一个“答案不在文档里”的查询,观察检索系统是承认缺失还是编造引用。这是 RAG 忠实性的核心测试。
    • 预期:系统输出"未找到相关资料"或类似的不确定性表达。
    • 失败信号:如果它编造了一个看似合理的引用,说明忠实性有问题。

“第四、五项是一对,”老z说,“第四项测的是召回能力,第五项测的是承认局限——前者看它找得全不全,后者看它找不到时会不会硬编。一个系统可以两项都做好,也可以一项好一项差:召回好但会硬编,危险在引用造假;召回差但会认怂,安全但没用。做完这两项,你对自己的 RAG 是‘能找对但爱撒谎’还是‘找不全但很诚实’,就心里有数了——这两种都还有得救,最没救的是‘又找不对又爱编’。”小a在第五项旁补了一句:诚实性比召回率更接近信任的底线。

工具与循环 ​

  1. 为一个只读工具写测试,再为一个会修改文件的工具写测试。验证只读工具不需要审批,而修改工具需要——这一差别来自副作用,不来自工具名。
    • 预期:只读工具直接执行;修改工具触发审批或确认点。
    • 失败信号:如果两者执行路径完全相同,说明审批边界没设计。
  2. 让一个 Agent 循环跑到 max_turns:记录它停止前的最后几步,分析它为什么没收敛。这是循环设计最现实的测试——大多数循环问题都在预算耗尽时暴露。
    • 预期:循环在达到预算时停止,并记录明确的停止原因(max_turns 而非 complete)。
    • 失败信号:如果它静默停了或没有停止原因,说明状态机有缺陷。

“第七项值得多说两句。”老z说,“记录‘停止前的最后几步’,不是为了围观,是为了回答一个问题:它是‘差点收敛’还是‘根本没在收敛’? 前者说明预算差一点、调高 maxTurns 可能就好了;后者说明循环设计有问题,加预算只是多烧钱。判断依据是停止前的趋势:每轮都在缩短任务差距,是差点收敛;每轮都在重复同样的动作,是原地打转。这个区别,决定你是调参数还是改设计。”

“工具与循环这一组还有一层关联,”老z说,“第六项证明‘审批边界有设计’,第七项证明‘停止边界有状态’。前者管‘能不能做’,后者管‘什么时候停’——两个边界都成立,Agent 才既不会乱来,也不会空转。只做了第六项,你的 Agent 可能很守规矩但停不下来;只做了第七项,它可能停得干脆但什么都敢做。边界是成对设计的,核验也要成对核。”

“还有一条补充,”老z说,“跑 max_turns 前先记一次 token 基线。 跑完对比‘最后一步时的累积用量’和‘预期用量’——差距越大,说明循环在收敛路径上浪费越多。这个数字不直接改代码,但它能告诉你要不要先去看工具失败信息,而不是急着调预算。”小a在第七项旁边写了“先看趋势,再调预算”。

源码走查:复现调用证据 ​

在 pi-mono@583f153d502aa8e958eefdb9af0fbd3344e68f95 根目录执行以下命令,先确认你确实在正确的基线上:

sh
git rev-parse HEAD
# 应输出 583f153d502aa8e958eefdb9af0fbd3344e68f95(或以 583f153d 开头)

确认基线后,逐段阅读核心文件(命令给出起始行,方便定位):

sh
sed -n '1,260p' packages/ai/src/models.ts        # 模型请求主路径
sed -n '1,260p' packages/agent/src/agent-loop.ts  # 循环核心
sed -n '1,260p' packages/protocol/src/codec.ts    # 协议编解码
sed -n '1,200p' packages/server/src/snapshots.ts  # 远程 snapshot
sed -n '1,200p' packages/evals/src/pi-harness.ts  # 评测 harness

“先做两步前置确认。”老z说,“第一,git rev-parse HEAD 的输出必须是以 583f153d 开头——不是这个基线,下面的行号全都对不上,别硬套;第二,sed 命令只是给了起始行,文件可能比 260 行更长,读到文件末尾为止,别在 260 行就停。行号是路标,不是边界。两步做完,才开始走查——走查的每一项,都要能说出‘我在哪一行看到了什么’。”

小a:“基线对不上会怎样?”

“最常见的两种情况,”老z说,“一是你 clone 的是最新 main,行号整体漂移——这时要么 checkout 到 583f153d,要么接受行号作废、只看符号名;二是你改过本地代码,行号局部偏移——这时只信你手头文件的真实行号。基线的意义是‘结论可复现’:别人在你的记录里看到行号,换台机器也能定位到同一段代码。基线一乱,复现就断。”小a把基线检查写成了走查前的第一动作。

阅读时完成以下走查任务,每项都要写下你观察到的具体行号或符号:

  1. 认证与流交接:从 ModelsImpl.streamSimple 标出认证合并(applyAuth)、Provider 选择(requireProvider)与事件流返回这三个交接点。说明认证错误发生在哪一步,而不是由 TUI 猜测。
    • 预期:三个交接点的行号各自落在一处,你能指出认证错误是在 applyAuth 抛出的,而不是由调用方凭空判断。
    • 失败信号:如果你只能说出"认证会失败",却说不出在哪一步失败,说明你还没把交接点钉在具体代码上。
  2. 循环的停止原因:在 runAgentLoop 中找到 complete、max_turns、aborted、error 四种停止原因分别在哪段代码产生。验证工具 error 不是 transport error,仍作为 tool result 回到模型。
    • 预期:四种停止原因各对应一段可指认的代码;工具 error 走的是 tool result 通道,而不是被当成连接错误抛掉。
    • 失败信号:如果某个停止原因找不到产出点,或工具 error 被混进 transport error,说明你还没分清"工具层失败"和"协议层失败"。
  3. chunk 边界 ≠ 消息边界:分两次向 ClientMessageDecoder.push 送入同一个 frame 的两半,验证第一次返回空数组、第二次才产出消息。这是网络分帧的真实行为,不是教科书抽象。
    • 预期:第一次 push 返回 [],第二次 push 返回完整消息;记录两次返回的数组长度。
    • 失败信号:如果第一次就返回了消息,说明你的实现把 chunk 当成了消息,分帧语义理解错了。
  4. 会话 fork 不创建 worktree:对照 fork.ts 的 readSessionEntriesForFork 和 Git 命令,确认会话 fork 只复制 session entry,没有调用任何 Git 或文件系统隔离 API。
    • 预期:fork.ts 中找不到任何 git、worktree 或目录复制调用;fork 前后的工作区文件没有任何变化。
    • 失败信号:如果 fork 后工作区文件被复制或修改,说明 fork 的边界比你以为的更宽,需要重新界定。
  5. snapshot 与 progress 的不同发送路径:在 snapshots.ts 和 sessions.ts 中标出 session_snapshot(带 revision 自增)和 session_progress(增量)分别在什么条件下发送。不宣称本快照实现自动重连或 exactly-once。
    • 预期:两种消息的发送条件各自明确——snapshot 在状态整体变化时发送并自增 revision,progress 在增量活动时发送。
    • 失败信号:如果你找不到两者的区分条件,或把 progress 当成了权威状态源,说明你对"快照定稿、进度提示"的顺序还没建立。
  6. compaction 的失败语义:在 compaction.ts 中找到 CompactionError 的两个 code——aborted 和 summary_failed,分别由模型的什么 stop reason 触发。验证压缩是可能失败的独立请求,不是静默截断。
    • 预期:两个 code 各能对回一个 stop reason;压缩失败时有显式的错误路径,而不是无声地把历史裁掉。
    • 失败信号:如果找不到 CompactionError,或压缩失败被静默吞掉,说明你看到的是简化版实现,不是 pi 的失败语义。

“走查的产出物,不只是一张写满行号的纸。”老z说,“最好是一份**‘代码-行为对应表’**:左边是你观察到的代码(行号加一两句),右边是它对应的可观察行为。比如‘applyAuth 在 132 行抛出认证错误’对应‘未配置凭据时,请求在模型层就失败,UI 不该猜测’。这张表以后就是你的排障地图——代码变了,表要跟着更新;行为变了,表要第一个被翻出来对照。”

实现核验:产物可运行 ​

实现篇的核验围绕 examples/mini-agent 展开。这个产物的验收标准是:代码可运行、测试可通过、行为可观察。

“实现核验和源码走查有个关键区别,”老z说,“走查是在别人的代码里找证据,实现核验是你自己的产物要扛得住证据。走查证的是‘pi 是这样做的’,实现核验证的是‘我做的也能这样跑’。所以这里每一项的失败信号都更值钱——走查发现‘源码和我想的不一样’是收获,实现发现‘我的产物不达标’才是该修的。”

“前两项测的是‘不越界’,中间两项测的是‘停得干净’,第五项测的是‘错得分明’。”老z把五项串了一遍,“路径边界决定它能碰什么,取消决定它停不停得下来,cleanup 决定它走不留痕迹,错误分离决定它坏得清不清楚。**一个 Agent 产物的成熟度,不看它有多能跑,看它停得干不干净、坏得明不明白。**五项的失败信号里,前四项是‘行为缺陷’,第五项是‘可诊断性缺陷’——后者最容易被拖到最后,但它恰恰是排障的地基。”

  1. 跑通测试:在 examples/mini-agent 下执行测试命令,确认全部通过。记录测试数量与耗时,而不是只看“pass/fail”。
    • 预期:全部测试通过,测试数与本书声称一致。
    • 失败信号:如果有测试失败,先确认是环境问题还是产物问题——不要跳过失败测试。
  2. 只读工具与路径边界:为最小 Agent 写一个只读工具(如 read),再写一个测试,验证它拒绝工作区外路径(用 .. 或绝对路径尝试越界)。确认拒绝来自 workspacePath 的 realpath 检查,而不是来自提示词。
    • 预期:.. 越界、绝对路径越界、符号链接绕过都被拒绝,且拒绝发生在工具层。
    • 失败信号:如果路径检查靠正则前缀匹配,符号链接会绕过它。
  3. 取消的可诊断性:使一次模型流被取消(通过 AbortSignal),确认退出码、日志和会话状态都可诊断——你能从这三处分别看到中止的痕迹,而不是它们静默地“什么都没发生”。
    • 预期:日志有中止记录、会话状态标记为 aborted、退出码反映中断。
    • 失败信号:如果取消后状态显示"完成",说明取消没真正传播。
  4. cleanup 无残留:用临时目录运行集成测试,验证 cleanup 后临时目录被递归删除,且真实模型测试与 fake 测试分开标记——fake 测试不消耗 API 配额,真实测试结果不能伪装成确定性证明。
    • 预期:临时目录不存在;fake 和真实测试在报告里分开列出。
    • 失败信号:如果残留目录存在,说明 cleanup 的 finally 块有缺陷。
  5. 错误类型分离:对工具失败、模型错误、协议错误分别断言不同的错误类型,避免统一吞成空结果。这是排障的基础——如果所有错误都长一样,你就永远定位不到故障层。
    • 预期:三类错误各有不同的错误类型或 code,调用方能区分。
    • 失败信号:如果所有错误都返回同一个类型,排障时无法定位故障层。

“第五项做完,补一个小的自检,”老z说,“把三类错误打印出来看形状:工具失败的错误里有没有带上工具名和调用 ID?模型错误里有没有 Provider 名和 stop reason?协议错误里有没有层信息?形状齐全,排障才有路可走;形状光秃秃,类型分得再开也救不了你。”小a在第五项旁写了“类型 + 上下文,缺一不可”。

核验的纪律 ​

小a做完这些,问老z:“做完一次就够了?”

“不够。”老z说,“核验不是一次性考试,而是持续纪律。每次改了 prompt、换了模型版本、加了工具,都要重新跑相关核验。模型行为会随版本漂移,你的核验清单就是漂移的探针。”

“先解释一下‘漂移的探针’这个词。”老z说,“模型不是稳定的——同一个 prompt,这个版本答对,下个版本可能答错;工具链升级,路径边界可能悄悄变宽。**你的清单不只是在检验‘当初做对了没有’,更是在每次环境变动后检验‘现在还对不对’。**清单上的每一项,都是插在系统里的探针:环境没变,它安静躺着;环境一漂移,它就报错。这才是‘持续纪律’的含义——不是复习,是监测。”

小a:“那这套清单,什么时候算真正‘拥有’了?”

“当你能不看清单,自己说出每一项要测什么、失败信号是什么的时候。”老z说,“清单是训练轮。训练轮的意义是让你学会自己骑——学会之后,你要能根据新的系统、新的风险,自己设计新的核验项。**照单做是做作业,会改单才是掌握了方法。**这本书教你的不是这二十几项,是‘怎么给一个概率性系统设计核验项’这件事本身。”

“给这条纪律配一个节奏,”老z说,“大改跑全套,小改跑相关。 换模型版本、改 system prompt、改核心循环——跑全套,因为影响面横跨所有章节;加一个工具、改一个工具描述——只跑相关项,因为其他核验覆盖不到它的行为。**‘全套’和‘相关’怎么分,是经验活;但无论怎么分,原则只有一个:改动过的链路,必须有对应的核验回执。**没有回执的改动,等于没验证过的改动。”

小a:“那核验记录怎么存,才能让‘重新跑’变得便宜?”

“存成脚本,别存成段落。”老z说,“能命令化的核验(跑测试、跑走查、跑对比)一律写成脚本,放进仓库;不能命令化的(人工判断),存结论模板——‘我改了 X,观察到了 Y,据此判断 Z’。**下次重跑时,脚本一次跑完,人工项照着模板快速过一遍。**核验记录的价值不是‘存在过’,是‘能低成本地重来一次’。”

核验的三条纪律

  1. 证据优先于信心——写下你观察到的行号、输出、耗时,而不是“我觉得它对了”。
  2. 失败路径优先于成功路径——成功路径谁都会测,故障手册里的每个症状都要能复现。
  3. 真实与 fake 分开标记——fake 测试证明逻辑正确,真实测试证明链路可用,两者证据等级不同,不能混报。

小a:“三条纪律里,第二条‘失败路径优先’能不能举个具体的例子?”

“能。比如取消。”老z说,“成功路径是‘请求完成,返回答案’——谁都会测;失败路径是‘请求中途被取消,状态对不对’——测的人少一半。你测一次取消,胜过测十次成功:成功路径只证明‘没坏’,失败路径证明‘坏了也知道怎么坏’。故障手册里每一项症状,都值得在清单里配一个对应的复现测试——这不是额外工作,是把清单变成真正的探针。”

回到全书目标:这本清单不是为了让你“做完”,而是建立一个可重复的验证习惯——读完后你能独立复现证据,而不是依赖书里的结论。

小a把清单折好夹进书里。他知道,这本清单的终点不是最后一页的打钩,而是下一次他改 prompt、换模型、加工具时,能自动想起那句“改动过的链路,必须有对应的核验回执”。动手核验没有毕业,只有一次又一次的重跑。