
Pi Agent 系列第三篇这次不聊跑通了聊点架构上的硬骨头。前两篇我们把环境搭起来、把第一个 Agent 跑通但后台收到最多的私信是同一个问题Pi 的代码仓库这么大到底从哪儿看起我的答案很直接——先看它的 monorepo 骨架。Pi 把命令行入口、核心编排、工具链、协议层全部收进同一个仓库根目录的 workspace 配置、每个包之间的依赖关系、构建缓存策略就是整个项目的骨架和血管。这篇我以 monorepo 为索引把 Pi 的架构一层层剥开讲清楚每个目录为什么存在、模块之间怎么协作以及你二次开发时最容易踩的坑。1. 先理解 monorepoPi 面对的是一道选择题1.1 monorepo 与多仓库的本质差别在看 Pi 的代码之前得先搞清楚一个前提像 Pi 这种体量的项目代码组织方式基本只有两条路——多仓库multirepo和单仓库monorepo。多仓库的意思是每个模块独立建一个 git 仓库比如pi-core、pi-cli、pi-tools各管各的monorepo 则反过来所有模块都放在同一个仓库里通过 workspace 机制来区分边界。很多人以为 monorepo 就是把所有代码堆在一起这是误解。monorepo 的核心不是物理上放一起而是用统一的依赖图谱管理多个包的工程化关系。每个包依然有自己的package.json、自己的测试和构建但它们的版本、依赖、发布节奏可以在同一个仓库内被统一编排。我在看 Pi 仓库之前专门对比过这两种模式的取舍维度多仓库monorepo代码复用依赖包版本发布使用方自行升级直接 workspace 引用改完即生效跨模块重构要同时改多个仓库、多次提交一次提交原子变更新人上手成本需要 clone 多个仓库、理解包间关系clone 一个仓库即可全局浏览构建与测试各仓独立 CI联调成本高统一流水线 增量缓存版本一致性容易漂移需要手动对齐root 统一编排天然一致这个对比不是绝对的但对于 Pi 这种核心逻辑、CLI、工具链高度耦合的 Agent 项目monorepo 是明显更有优势的那条路。1.2 Pi 选 monorepo 的三个核心理由第一个理由是原子提交。Pi 的代码库里pi/core改一个决策逻辑往往需要同步调整pi/tools里的工具注册方式和pi/cli里的参数透传。如果拆成多个仓库一个功能改动可能要跨三四个仓库开 PR等所有仓库都合并才能验证完整效果极其痛苦。monorepo 里这就是一个提交的事CI 跑完整个链路改动完整可验证。第二个理由是依赖调试成本。多仓库模式下你想在本地调试pi/core被 CLI 调用的效果就得在pi-cli里npm link指向本地包。npm link在复杂依赖树下的坑我后面会专门讲但简单说就是——它会把你的 node_modules 搞成一场灾难。Pi 用 pnpm workspace 后pi/core和pi/cli之间是软链接直连本地改动即时生效不需要任何 link 操作。第三个理由是基础设施复用。Pi 的 monorepo 根目录有一套统一的 ESLint、Prettier、TypeScript 配置和 CI 流水线。一个 PR 进来全仓库的检查、测试、构建都走同一套规则不会出现CLI 用的 TS 版本和 Core 不一致这种低级问题。模块多了以后这种一致性省下的排查时间非常可观。2. 仓库顶层从根目录读起骨架就藏在文件名里2.1 根目录配置文件是阅读地图拿到 Pi 的仓库第一件事不是打开src而是看根目录下到底有哪些文件。一个成熟的 monorepo 项目根目录通常通过文件名的约定把自己的一切交代清楚。Pi 的仓库顶层结构大致长这样pi/ ├── apps/ │ ├── cli/ │ └── docs/ ├── packages/ │ ├── core/ │ ├── tools/ │ ├── mcp/ │ └── sdk/ ├── .npmrc ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json ├── turbo.json └── .github/这里面每一个文件都不是摆设。pnpm-workspace.yaml声明了哪些目录是 workspace 包turbo.json定义了构建任务之间的依赖关系和缓存策略pnpm-lock.yaml锁住整个依赖树版本根目录package.json则统一管理所有子包的脚本入口。我最先看的一定是pnpm-workspace.yaml因为它直接告诉你这个仓库的边界packages: - apps/* - packages/*这段配置的意思是apps和packages下所有直接子目录都算作 workspace 里的独立包。它们之间可以互相引用并且共享同一个锁文件。看到这个文件你就知道 Pi 的仓库被划分成了两个大区应用层和包层。2.2 packages 与 apps 的边界按交付形态划分很多人第一次看 monorepo 会困惑apps和packages到底什么区别标准其实很简单——看这个包的最终交付形态是什么。apps目录下放的是可运行的产品。apps/cli构建出来的东西是一个用户可以直接执行的命令行程序它把packages里的逻辑组装成一个完整的交付物。apps/docs是文档站本质也是一个独立部署的产品。它们的特点是有自己的启动入口、有独立的环境变量、面向最终用户。packages目录下放的是可复用的库。pi/core是 Agent 编排的核心逻辑别人可以在自己的程序里调用pi/tools是工具集注册表可以被 core 消费也可以被 sdk 暴露给外部pi/mcp是 Agent 和外界通信的协议层pi/sdk是给二次开发者的编程接口。这些包都不直接运行它们是被组装进apps的零件。判断一个包应该放哪里的标准我一直用这句话如果你这个目录里有bin字段、有启动脚本、有生产环境配置文件它大概率是 app如果只有exports导出和 API 定义它是 package。Pi 的划分非常干净cli 里几乎没有核心逻辑core 里没有一处直接读取用户配置的逻辑这种边界感是 monorepo 架构最值钱的资产。3. 核心包拆解Pi 的三层骨架零件3.1 编排层Agent 的循环不是玄学pi/core是整个 Pi 的心脏也是我建议你第二个打开看的包。它做的事情抽象出来只有一件维护一个 Agent 循环。这个循环可以拆成四个阶段感知Perceive接收用户输入、系统提示词、工具返回结果组装成模型可以理解的上下文。决策Decide把上下文交给大模型推理得到下一个动作。这个动作可能是调用某个工具继续生成内容或者直接给出最终回答。行动Act如果模型决定调用工具core 负责找到对应工具、传参、执行、拿到结果。反思Reflect把工具结果拼回上下文判断任务是否结束没结束就带着新信息回到感知阶段。pi/core的核心抽象就是这个循环状态机。它不关心具体调用了什么模型也不关心工具是代码执行器还是浏览器它只负责把感知-决策-行动-反思这个循环稳定地转起来。这也是为什么 core 是所有包里最稳定的部分模型可以换、工具可以加循环不需要动。3.2 工具层能力不是写死的是注册进来的pi/tools有意思的地方在于它的设计哲学Pi 的能力是插件化的不是内置的。每个工具模块暴露统一的接口任何满足这个接口的代码都能被注册进 Agent 的工具列表。一个工具通常声明这些信息名字、描述、入参 schema、执行函数。名字和描述是给大模型看的模型通过它们决定要不要调用这个工具、什么时候调用入参 schema 是给参数校验用的防止模型生成非法参数执行函数才是真正干活的逻辑。这种设计的直接后果是每加一个新工具不需要改动 core 的代码。tools 包本身维护一个注册表core 启动时把注册表里的工具全部加载进上下文。所以看 Pi 的仓库时你会发现 core 的依赖列表非常短真正重的依赖都沉淀在 tools 包里。这里有个很关键的细节工具的注册顺序会影响模型的调用倾向。Pi 在启动时会对工具列表做排序把高频、轻量、安全的工具排在前面因为模型在长上下文里更容易注意到排在前面的工具描述。这个细节如果你自己写 Agent一定要记住。3.3 协议层Agent 与世界的对话契约pi/mcp解决的是 Agent 与外部系统通信的标准化问题。这里的 mcp 指的就是 Model Context Protocol一种让 AI 应用和外部数据源、工具之间进行标准化交互的开放协议。在设计上Pi 没有让 core 直接去请求文件系统、数据库或 HTTP 服务而是全部通过 mcp 这一层转发。这么做的收益有两个第一是隔离。core 不需要知道外部服务是 REST 接口还是本地命令行只要拿到 mcp 标准的响应就能继续决策。协议成了挡在核心逻辑和外部世界之间的缓冲层。第二是生态兼容。任何实现了 mcp 协议的外部服务都可以无缝接入 Pi。这相当于给 Pi 开了一条对接全行业通用工具的高速公路不用每个服务写一套私有 adapter。在 monorepo 里看这个包你会发现它的依赖最少——它存在的意义就是定义标准和实现协议不掺杂业务逻辑。这也是分层清晰的标志。4. 一次闭环任务在仓库里怎么跑完4.1 入口触发从敲下命令到上下文初始化现在我们把三个核心包串起来看一次真实任务在 Pi 仓库里是怎么流转的。这是理解 monorepo 包间协作关系最快的方式。用户执行pi run 帮我写一个计算器的测试用例入口在apps/cli。cli 做的事情非常少解析参数、初始化日志、加载配置文件然后调用pi/sdk的start方法。sdk 负责组装核心执行环境——它会读取配置文件里配的模型信息初始化pi/core的循环实例再从pi/tools加载启用的工具列表最后把这次会话的初始上下文构建出来。这一步里最容易理解 monorepo 价值的就是依赖传递cli 依赖 sdksdk 依赖 core 和 tools它们的版本在 workspace 里直接联动不存在CLI 用的 core 是老版本这种问题。4.2 Agent 循环的决策路径上下文是唯一事实来源进入循环后core 先把用户问题和工具描述组装成一个完整的 prompt发给模型。模型返回的第一个决策可能是调用 CodeTool 执行静态分析。此时 core 做三件事校验参数格式、把这次决策记录进会话历史、调用 tools 包里注册的对应执行函数。执行结果不会直接粗暴地扔回给模型而是先经过一层结果裁剪——太长的话截断、格式不对的清理、敏感信息脱敏——然后写回会话历史带着新状态进入下一轮循环。这个上下文是唯一事实来源的设计是 Pi 架构最核心的原则。所有中间状态都通过会话历史传递不另搞一套内存状态。好处是断点续跑、日志回放、多轮一致性都变得非常简单——只要把历史恢复Agent 就能从任意断点继续工作。4.3 结果产出与状态回写尾声也是起点循环的退出条件有两个模型输出了最终答案或者超过最大轮数限制。最终答案会经 sdk 封装成结构化输出交还给 cli 展示给用户。但还有一个容易被忽略的设计每次运行的完整轨迹都会被写回本地存储。这意味着你可以回顾上一次 Agent 执行了哪些工具调用、每步耗时多少、上下文最终消耗了多少 token。这些数据是优化提示词和工具描述的第一手素材也是二次开发拓展功能时的重要参考。从这条链路回头看 monorepo 的意义很清晰cli、sdk、core、tools 各自只负责一段通过 workspace 内部依赖无缝衔接。任何一段要替换都只需要改动对应包重新构建整体即可。5. 工程化实践依赖、构建和发布的骨架细节5.1 pnpm workspace 与幽灵依赖问题Pi 的依赖管理用的是 pnpm 的 workspace 模式。pnpm 和 npm/yarn 最大的区别是它不会把所有依赖平铺在 node_modules 顶层而是采用内容寻址存储 符号链接的方式每个包只能访问自己声明过的依赖。这个特性在 monorepo 里尤其重要。npm 的依赖提升机制常常把幽灵依赖偷偷暴露给没有声明的包——你在 A 包里能importB 的依赖但不报错因为它在顶层 node_modules 里。等发布上线后环境变了就崩。pnpm 的严格链式解析从依赖机制上杜绝了这个问题。当然 pnpm 也有代价。Pi 的仓库里如果出现两个包依赖同一个库但版本要求不同的情况会提示你可能要配置public-hoist-pattern或调整版本设计。这在 AI Agent 项目里很常见比如 core 用 lodash 4tools 里某个工具可能用 lodash 3。pi 的做法是尽量统一版本把这种冲突扼杀在设计期。实操提示在 Pi 仓库里新增包时先看清楚它依赖的库版本是否和 core 保持一致。不要图省事引入新版本统一版本族能规避大量隐性问题。5.2 Turborepo 增量构建的逻辑Pi 的构建任务由 Turborepo 编排配置文件是根目录的turbo.json。它的核心机制是缓存——根据输入文件内容、依赖关系、环境变量计算一个哈希值如果哈希命中就直接拿之前的构建产物跳过整个构建过程。这套机制在 monorepo 里的放大效应非常夸张。你改pi/tools下某个工具Turborepo 会只重构建 tools 包及其下游引用sdk 和 cli core 如果没变就不会重跑。配合远程缓存CI 上的构建速度可以提升一个数量级。但缓存引擎带来的坑也很典型。我就遇到过 Pi 的构建缓存命中错误版本的场景——配置文件改了但没有触发重新构建因为 Turbo 没把那个配置文件纳入输入依赖。解决方法是显式地在任务的inputs里声明所有可能影响产物的文件路径。后面踩坑小结我会细说。5.3 版本策略同步发布还是独立发布最后一个工程化问题是包的版本号怎么管理。Pi 用的是一种混合模式核心包版本严格同步发布core、sdk、mcp的版本号保持一致因为它们内部的接口契约高度耦合工具包则允许独立版本因为工具迭代速度快、发布频率高。这种策略在执行上有个小技巧每次发版前先跑一遍全量构建和测试然后按依赖顺序从底层包往上发最后更新顶层 cli。Pi 的 scripts 目录里有一个专门处理发布顺序的脚本避免人为漏发、错发。monorepo 里最怕的无非是上面改了、下面没发一运行就崩——有了自动化顺序控制这个问题基本被消灭。6. monorepo 实战中的坑我替你们都踩过6.1 幽灵依赖与依赖提升最经典的问题是幽灵依赖。在早期用 npm workspace 跑 Pi 时我曾经发现pi/mcp里没有声明zod但代码里直接import了zod——因为 npm 把 zod 提升到了顶层 node_modules所以本地跑不报错。直到发布到干净环境运行时才爆出Cannot find module zod。排查这类问题没有捷径就是把node_modules彻底删掉用 pnpm 重新安装再测试。pi 的仓库转向 pnpm 后这种问题几乎绝迹。如果你在自己的项目里仍然用 npm/yarn建议在 CI 里加一条检查禁止未声明依赖被引用。6.2 循环依赖改一个包全盘崩溃monorepo 发展到中期最常见的坑是循环依赖。七个包互相依赖依赖图上形成了一个环。这种架构下每次构建都是碰运气改一个包可能把另外几个全部拖垮。Pi 的解法很朴素但有效依赖方向必须单向流动。core可以依赖tools的接口定义但tools永远不能反过来依赖core的实现。严格执行后依赖图变成清晰的有向无环图构建顺序、发布顺序全都自动确定。我判断一个 monorepo 是否健康的唯一标准就是依赖图有没有环。6.3 构建缓存失效与 CI 超时还有一个让我挠头的坑是 Turborepo 远程缓存失效。你的代码没变但远程缓存因为环境变量哈希变化导致每次都 missCI 上全量构建每次跑二三十分钟。解决姿势是把构建用到的所有环境变量显式放到turbo.json的env列表里同时确认 CI 的环境变量集合保持稳定。一旦配置好了注意观察本地构建日志里的cache hit命中率。Pi 仓库里日常命中率一般在 90% 以上如果你发现这个数字大幅下降先检查是不是有隐式输入文件漏掉了。最后一个我在实战中体会到的小建议把根目录的README.md里维护一张包依赖图纯文字版就够每个新来的同事第一件事就是读这张图。架构这种东西代码敲多了人就容易陷到细节里看不清全貌一张清晰的依赖关系图比任何文档都管用。看懂 Pi 的骨架本质上就是看懂这张图的每一层为什么那样画——然后你就能顺着骨架长出新的肌肉。