第二章 读工程前,先认识这些基本概念
2.1 终端、命令和当前目录
终端是一个让你输入文字指令、查看文字结果的窗口。你在其中输入的指令,通常先由一个叫“Shell”的程序解释。macOS 常见的 Shell 是 zsh,很多 Linux 环境使用 bash。终端、Shell 和 Pi 各有职责:终端提供窗口,Shell 解释系统命令,Pi 是可以从 Shell 启动的一个程序。
例如输入 pi,通常表示让 Shell 查找并启动一个叫 pi 的命令。Pi 启动后,你在它的输入框里输入自然语言,接收内容的程序就变成了 Pi。以后遇到 /login、/model 这样的命令,要记住它们属于 Pi 的交互界面。把 /login 直接敲在系统 Shell 中,系统不会因此自动完成 Pi 登录。
程序启动时有一个“当前工作目录”。许多相对路径都从这里解释。若当前目录是 /home/lin/demo,路径 src/main.ts 通常指向 /home/lin/demo/src/main.ts。以 / 开始的 Unix 路径通常是绝对路径;../ 表示上一级目录。Windows 的绝对路径形式不同,核心问题仍是“从哪里开始找”。
在 macOS 或 Linux 的 Shell 中,可以用以下命令认识目录:
pwd
ls
pwd 显示当前目录,ls 列出目录内容。用 cd 切换目录时,要把示例路径换成你电脑上的真实路径。文档中的 /path/to/project 是占位符,不是一个保证存在的目录。本书不会让你为了理解一个例子而猜测某个文件必定已经存在。
对 Pi 来说,目录尤其重要:项目配置、上下文文件、工具读取路径和会话归属都与工作范围有关。遇到“它读错了项目”的情况,第一步先确认从哪个目录启动,而不是立即怀疑模型不能理解要求。
2.2 Node.js、npm 和软件包
JavaScript 是一种编程语言。Node.js 是运行 JavaScript 的环境,使程序可以在浏览器之外使用文件、网络和进程等能力。Pi 的主要代码围绕 Node.js 运行,本快照要求 Node.js 至少为 22.19.0。版本号是运行环境的版本,不是 Pi 的版本。
npm 是一个包管理工具。“包”可以理解为一组可以安装和复用的代码及其说明。package.json 是包的说明文件,它会记录名称、版本、依赖和脚本。“依赖”指这个包工作时还需要使用的其他包。安装一个软件时,包管理器往往还会安装它的依赖,因此你看见的实际文件比项目自己编写的源文件多得多。
在 Pi 的根目录中,package.json 使用 workspaces 把多个包组织在同一个仓库里。这种组织方式叫 monorepo,即多包共用一个仓库。packages/ai 和 packages/agent 各有自己的包说明,但可以在同一次开发中协调修改。
package-lock.json 记录依赖解析的精确结果,帮助不同机器安装一致的版本。node_modules 是安装后放置依赖的目录。初次读源码时,通常先看项目自己的 packages/*/src;需要核对外部库接口时,再查 node_modules 中的类型和实现。
两个看起来接近的命令,目的可能不同:
npm install --ignore-scripts
npm run check
前者安装依赖,并跳过安装生命周期脚本;后者运行本项目在 scripts.check 中定义的检查。npm run 后面的名称不是语言内置指令,必须回到当前项目的 package.json 看它具体执行什么。在本工程里,check 包含格式写入步骤,开发章节会进一步解释。
2.3 TypeScript、模块与异步
TypeScript 在 JavaScript 基础上增加了类型描述。类型让工具在运行前检查一些明显不匹配的用法。例如,一个函数声明输入应为数字,你却传入一份任意对象,类型检查就可能提前报错。
下面是只用来说明语言的例子,与 Pi 的公开 API 无关:
function add(left: number, right: number): number {
return left + right;
}
const result = add(2, 3);
function 定义函数,可以把它理解成一个有名字、可重复调用的步骤。left 和 right 是参数;冒号后的 number 表示数字类型。return 给出结果。const 创建一个不能重新赋值的变量名。你现在不必记住全部语法,能找到输入和输出就已经可以读懂不少工程代码。
文件之间通过“模块”协作。一个模块可以用 export 导出功能,另一个模块用 import 导入。本仓库使用 ESM,也就是 JavaScript 的标准模块系统。读到一串导入时,可以先看来自哪个包:它通常透露了当前文件负责哪一层,以及依赖哪一层。
很多操作不会立即完成,例如等待网络响应、等待工具执行结束。“异步”指程序可以等待这些操作,而不把它们假定为瞬间返回。在源码里,async 和 await 常常围绕这种等待出现。Promise 表示一个未来才会得到结果或错误的操作。
流式回答进一步把“一次操作”拆成一系列事件。你可能先收到一小段文本,接着收到更多文本,最后才收到结束事件。因此屏幕上已有内容,并不等于整个请求已经成功完成。模型请求、工具执行和界面更新会在不同时间发生,这也是 Agent 需要明确状态和事件的原因。
2.4 JSON、JSONL 和状态
JSON 是一种保存结构化数据的文本格式。它用对象、数组、字符串、数字等来表达信息。下面这份数据只解释 JSON 的样子,不是 Pi 的设置文件,也不能直接拿去配置 Pi:
{
"title": "练习项目",
"enabled": true,
"files": ["calculator.js", "README.md"]
}
花括号表示对象;title 这样的字符串是字段名;方括号表示数组。JSON 中的字段名和字符串使用双引号。很多 JavaScript 代码里可以写的内容,例如函数和普通注释,并不属于 JSON 的标准语法。
JSONL 表示每一行都是一个完整 JSON 值。它适合逐条追加记录。Pi 的 CLI 会话就使用 JSONL,并通过条目之间的父子关系表达分支。要注意,“文件是一行一条记录”只说明存储形式;每条记录有哪些字段、怎样关联,仍由项目自己的数据结构决定。不要自行编造一行 JSON 就当作合法会话文件。
“状态”是程序在某个时刻记住的信息。例如当前选择的模型、已经收到的消息、某个工具是否正在运行,都可以是状态。“事件”则描述发生了什么,例如开始生成、文本增加、工具结束。你可以把事件看成推动状态变化的通知。理解这个区别后,终端为何能实时更新、会话为何需要持久化,都会更容易解释。
本章小结
终端是操作入口,当前目录决定许多路径的含义。Node.js 提供运行环境,npm 管理依赖,TypeScript 帮助描述和检查代码。模块连接文件,异步处理等待,JSON 保存数据。先理解这些词的职责,不需要一次学完一门语言。
练习与参考答案
练习一: 在系统终端输入 /model 没有打开模型选择,应该先检查什么?
参考答案: 检查是否已经进入 Pi 的交互界面。斜杠命令属于 Pi;系统 Shell 并不负责解释它。
练习二: 想知道 npm run check 会做什么,应该看哪个文件?
参考答案: 看当前项目的 package.json,找到 scripts 下的 check。名字相同的脚本,在不同项目中可能做不同事情。
练习三: 为什么“已显示第一行回答”不能证明模型请求已成功结束?
参考答案: 流式请求会陆续发送事件。后续仍可能取消或失败,必须结合结束状态判断。