拆开 Pi Monorepo:改模型、循环、产品和 UI 时,代码应该放在哪一层

拆开 Pi Monorepo:改模型、循环、产品和 UI 时,代码应该放在哪一层

团队准备给 Coding Agent 增加一个新 Provider。最省事的做法,是在主应用里直接写认证、请求体转换和流式解析。过两周 Tool Call 格式不兼容,又在同一个目录加一层修补。再过一个月 Web、CLI 和评测都需要这个 Provider,三套实现开始各自演化。

根因是模型协议变化没有唯一归属。只要一种变化可以同时穿透 Runtime、产品和 UI,系统就会慢慢长成一团"哪里都能改、哪里都不敢改"的胶水。

Pi 的 Monorepo 提供了一个值得研究的反例。在固定版本 v0.82.1 / b4f2936 中,四个核心公开包分别承接模型 Provider、通用 Agent Runtime、编码产品组合和终端 UI 四类变化原因。

这篇文章不做源码目录导游,只回答一个工程问题:当 Harness 发生变化时,代码首先应该落在哪一层,怎样证明它没有污染其他层。

一、先确认真实依赖结构

Pi 的四个核心公开包是:

text 复制代码
@earendil-works/pi-ai
@earendil-works/pi-agent-core
@earendil-works/pi-coding-agent
@earendil-works/pi-tui

但真实依赖并不是 TUI → Coding Agent → Agent Core → AI 这种单链。固定版本的 Package Manifest 显示:

text 复制代码
pi-agent-core ───────→ pi-ai

pi-coding-agent ────→ pi-agent-core
        ├───────────→ pi-ai
        └───────────→ pi-tui

pi-tui               可独立作为终端 UI 库
pi-ai                 可独立作为统一 LLM API

pi-coding-agent 是组合层,所以它会直接使用模型、Runtime 和 TUI,而不是为了追求"严格分层"强迫所有调用绕过 Agent Core。pi-agent-core 则只声明依赖 pi-ai,不需要知道终端怎么渲染,也不需要知道 AGENTS.md、 Coding Session 或产品命令。

这张图比"四层金字塔"更有用,因为它揭示了两个判断:

  1. 上层可以组合多个下层能力,但下层不能反向知道产品策略。
  2. 包边界的目标不是减少所有直接依赖,而是阻止一种变化无理由扩散。

package.json 的固定构建脚本依次准备 TUI、AI、Agent、SQLite Storage、Coding Agent 和 Server。这能证明产物准备顺序与 Monorepo 协同关系,却不能单独当作运行时调用栈。真正的职责边界仍要回到各包 Manifest、README 和上层依赖来判断。

先用一张变化路由表确定首要所有者,后文再解释每个选择的原因:

变化原因 首要落点 不应拥有的事实
Provider 协议、认证、流式格式 pi-ai 工具副作用与产品 UI
Tool 生命周期、事件、取消、消息转换 pi-agent-core Provider Wire Format 与编码产品策略
默认 Tool、Session、资源、Extension pi-coding-agent 通用模型协议与终端渲染
终端组件、输入、Overlay pi-tui Agent 状态与权限事实
SQLite、浏览器或远程持久化 独立 Storage / Adapter 通用 Core 的默认依赖

二、pi-ai:把模型差异收敛成协议,不接管工作流

pi-ai 的 README 将它定义为统一 LLM API。它处理 Provider 集合、模型目录、认证解析、流式响应、Thinking、 Tool Call 消息、Token 与成本、取消和跨模型 Context Handoff。换言之,它负责把不同模型服务转换为 Agent 能消费的共同语义。

这一层最容易出现的误区,是看到 ToolTool Call 类型,就把工具执行也塞进模型层。实际上应分成两件事:

text 复制代码
pi-ai
  知道模型怎样表达"我要调用工具"
  知道 Tool Schema 怎样进入请求
  知道流式 Tool Call 怎样被解析

pi-agent-core
  决定 Tool Call 何时进入执行
  验证并运行工具
  产生 Tool Result
  决定是否继续下一轮

"模型能提出动作"与"系统执行动作"必须分开。否则 Provider Adapter 会开始知道文件系统、Shell、权限和重试,新增模型时就可能改变 Tool Runtime 的安全语义。

因此,下列变化首先属于 pi-ai

  • 新增或修改 Provider 协议。
  • 认证、OAuth、Header 和 Endpoint 解析。
  • 模型目录与能力元数据。
  • Provider Wire Format 与统一 Message 的转换。
  • 流式文本、Reasoning、Tool Call 与用量事件。
  • Context 在不同模型之间的可移植表示。

它不应拥有文件读写、Coding Session、项目规则、终端组件或任务恢复策略。

三、pi-agent-core:拥有生命周期,而不是拥有某种产品

只有一次模型请求,还不是 Agent Runtime。pi-agent-corepi-ai 之上加入状态、消息转换、Agent Loop、Tool 执行、事件流、Abort、Context Transform、Steering 与 Follow-up 等生命周期机制。

固定 README 给出的最小链路是:应用创建 Agent,注入模型流函数,订阅事件,再调用 prompt()。有 Tool Call 时, Runtime 负责工具开始、更新、结束和 Tool Result 的顺序,然后决定是否继续模型回合。

这一层应该拥有的是"任何工具型 Agent 都会遇到的状态问题":

  • 一轮何时开始、何时结束。
  • Assistant Message 何时成为稳定状态。
  • Tool Call 怎样校验、阻断、执行和落回上下文。
  • Abort、Terminate 与停止条件分别影响什么。
  • 自定义应用消息怎样转换为 LLM Message。
  • 上下文压缩或注入从哪里接入。
  • 事件订阅者看到的顺序是什么。

它不应该知道工具是不是 readeditbash,也不应该默认所有 Agent 都有项目文件、Git、终端主题或 AGENTS.md

这条边界给出了一个实用判断:如果把 Coding Agent 换成写作、研究或业务自动化 Agent,某段机制仍然成立,它更可能属于 Agent Core。如果它只在编码产品中成立,就不应为了"复用"硬塞进通用 Runtime。

四、pi-coding-agent:编码产品的策略汇合点

pi-coding-agent 的 Manifest 直接依赖 AI、Agent Core 与 TUI。它还组合 readwriteeditbash、Session、资源加载、系统提示词、模型与凭证、Extension、Skill、Package、Project Trust,以及 Interactive、Print/JSON、RPC 和 SDK 等运行模式。

这些职责说明它是产品策略汇合层。

产品层的职责不是重新实现下层机制,而是做带有明确使用场景的选择:

text 复制代码
通用机制                         编码产品策略
────────────────────────────────────────────
Tool 生命周期            →       默认提供 read/write/edit/bash
Message / Context        →       加载 AGENTS.md、Skills、项目资源
Agent State              →       Coding Session、分支与持久化
事件流                   →       Interactive、JSON、RPC、SDK 输出
模型注册接口             →       登录、模型选择与用户配置
扩展入口                 →       Extension、Package 与产品命令

因此,新增一种项目资源、改变 Session 行为、调整默认编码 Tool、增加产品命令或接入 Project Trust,首要落点通常是 Coding Agent。只有当修改揭示出所有 Agent 都缺少的通用生命周期能力时,才应该下沉 Agent Core。

这里需要警惕"方便下沉":因为 Core 被所有上层复用,把产品特例放进去看似少写一层适配,实际是让所有未来应用承担编码产品的概念。真正的复用不是所有代码都放到底层,而是底层提供足够小、足够稳定的接口。

五、pi-tui:可以观察和操作状态,但不能创造业务事实

pi-tui 的 Manifest 将它描述为带差分渲染的终端 UI 库。README 展示组件、编辑器、Markdown、Overlay、 Autocomplete 与终端图片等能力。它可以独立用于非 Agent 应用。

这层负责呈现与交互,但不拥有业务事实:

  • 把事件和状态渲染成可理解的界面。
  • 接收用户输入并把意图交给上层。
  • 处理焦点、编辑、补全、Overlay 与终端能力。
  • 优化差分刷新和低闪烁输出。

它可以显示"工具正在运行",但不能自己决定 Tool 已开始。可以显示"任务已取消",但不能因为用户关闭一个 Overlay 就擅自把 Runtime 标成已取消。可以提供确认组件,但真正的权限判定与审计记录必须由产品或安全策略拥有。

这是 UI 分层最容易被忽略的地方:界面状态不是业务状态。只在 TUI 中隐藏按钮、清空进度或显示成功,不会改变 Runtime 和副作用的真实状态。

六、辅助包揭示了第五条规则:平台依赖应外置

固定版本的 Agent Core README 明确说明,SQLite Session Backend 和 node:sqlite Adapter 位于独立的 pi-storage-sqlite-node 包,因此 Core 默认不会拉入 Runtime Builtin 或原生 SQLite 依赖。

这个细节比"还有 Storage 包"更有架构价值。它说明:

text 复制代码
Agent Core 需要持久化能力
不等于
Agent Core 必须绑定 Node.js SQLite

同样的判断适用于浏览器存储、远程数据库、企业凭证库和系统 Keychain。上层需要的是稳定契约,平台相关实现应放在可替换 Adapter 或独立 Package 中。

根目录还有 Server、Evals 等辅助包,但不能仅凭目录名推导其稳定 API。本文只把它们作为一个边界信号:完整 Harness 除了主运行链,还需要部署、评估和存储。这些能力未必都应该进入四个核心公开包。

七、把变化路由落成验收证据

前面的路由表还需要最低验收证据:Provider 变化使用固定请求/响应夹具。Runtime 变化检查状态与事件序列。产品变化运行场景测试。TUI 变化验证渲染与输入。Storage 变化执行可替换实现的契约测试。一个需求可以跨包协作,但每项事实仍只有一个首要所有者。

每次跨层修改都应能回答:

  1. 哪一层拥有这项事实?
  2. 哪一层只消费它?
  3. 更换 Provider、产品或 UI 后,这段代码是否仍应存在?
  4. 失败与取消由谁记录,界面是否只是展示?
  5. 哪个测试能证明变化没有穿透不相关层?

如果回答不了,说明边界合同仍然缺失。

八、分层不是免费午餐

Pi 这种拆分带来复用和替换空间,也引入真实成本。

第一,类型与消息会跨层转换。应用消息、AgentMessage、统一 LLM Message 和 Provider Wire Format 之间,每一次转换都可能丢字段、改变顺序或弱化错误信息。

第二,多包版本需要协同。固定版本中 Coding Agent 依赖同版本范围的 AI、Agent Core 与 TUI。一层 API 演进可能要求上层同步发布和迁移。

第三,调试链更长。一次"工具没有结果"可能来自 Provider 流、消息转换、Loop、Tool、Session 或 UI 展示。没有统一的 Run ID、事件记录和层级日志,分层会把故障变成跨包猜谜。

第四,边界需要持续维护。某项能力从 Core 拆成 Storage Adapter,或从产品层下沉为通用事件,并不是一次性目录设计,而是随着第二个真实复用场景出现不断校准。

因此,不能因为 Pi 使用 Monorepo,就推导"多包一定更先进"。如果团队只有一个应用、没有独立复用和变化隔离需求,过早分包可能只增加发布与调试成本。更简单的替代方案,是先在单包中用模块边界和依赖检查保持分层,等出现第二个真实消费者再拆出 Package。

九、落到自己的 Harness:先签一份最小包边界合同

自研 Harness 不需要复制 Pi 的目录名,可以先为每层写一份极小合同:

yaml 复制代码
layer: agent-runtime
owns:
  - agent_state
  - tool_lifecycle
  - event_order
  - abort_and_stop
depends_on:
  - model_protocol
must_not_know:
  - coding_project_files
  - terminal_components
  - provider_wire_format
consumers:
  - coding_application
  - research_application
acceptance:
  - deterministic_event_sequence
  - blocked_tool_never_executes
  - abort_boundary_is_recorded

ownsmust_not_know 同样重要。很多架构文档只写每层"负责什么",却不写它禁止依赖什么。最后所有层都以"需要联动"为由互相穿透。

一个稳妥的实施顺序是:

  1. 先记录现有依赖与跨层调用,不急着拆包。
  2. 按变化原因给模块指定唯一首要所有者。
  3. 为每层写 owns / depends_on / must_not_know
  4. 用依赖检查、契约测试和事件夹具守住边界。
  5. 只有出现独立消费者或平台依赖时,再拆成可发布 Package。
  6. 用同一个端到端场景验证拆分前后语义不变。

Pi Monorepo 展示了怎样把模型协议、Agent 生命周期、产品策略和呈现状态分配给不同的变化轴。目录只是结果,变化路由才是架构。

当团队能明确"这项事实由谁拥有、哪些层只能消费、哪些依赖绝不能出现、用什么证据验收",Monorepo 才会缩小变化半径。否则只是把耦合拆进多个仍需一起修改和发布的包。

参考资料与证据边界

  1. Pi 根 package.json,固定 Tag v0.82.1github.com/earendil-wo...
  2. Pi AI Manifest 与 README:github.com/earendil-wo...github.com/earendil-wo...
  3. Pi Agent Core Manifest 与 README:github.com/earendil-wo...github.com/earendil-wo...
  4. Pi Coding Agent Manifest 与 README:github.com/earendil-wo...github.com/earendil-wo...
  5. Pi TUI Manifest 与 README:github.com/earendil-wo...github.com/earendil-wo...

固定 Tag 用于证明包名、公开职责、声明依赖和当时构建顺序。"变化路由表""跨层污染检查"和 YAML 边界合同是作者综合,不是 Pi 官方协议。本文依据固定 Tag 的静态清单、README 和构建顺序完成分析,未克隆、构建或运行 Pi Monorepo。真实性能、故障隔离、升级和独立复用成本保持未知。

相关推荐
hh95015 分钟前
Agent Plan × DeepSeek Harness:角色 Prompt 驱动的 Agent 分工优化与协作质量实验
java·前端·人工智能·prompt·adg·agent plan·adg成都社区
YHL15 分钟前
🐉 天龙八部 RAG 知识库实战:从零构建你的武侠 AI 助手
数据库·人工智能
阿基拉de_Akir15 分钟前
② 跨层禁止:机器如何拦截非法语义绑定
人工智能
科技小E18 分钟前
把人从百米高空拉下来:自动化AI算法训练服务器DLTM+无人机巡检让风机光伏缺陷无所遁形
人工智能·自动化·无人机
武子康19 分钟前
一次 Agent 失败后,到底该改模型、Prompt 还是 Router?
人工智能·llm·agent
小K讲AI营销19 分钟前
固态电池战局拆解:机器人为何先于汽车吃到红利
大数据·人工智能·区块链
beiju19 分钟前
别急着埋 SaaS:Agent 时代真正被压缩的是人工胶水层
人工智能
让学习成为一种生活方式19 分钟前
黄花蒿LHC基因家族的串联重复驱动扩张及其UV-B胁迫适应性--BMC Plant Biology
人工智能·算法·机器学习
小小测试开发20 分钟前
RAG应用评测:从指标体系到LLM-as-a-Judge的自动化落地
android·运维·人工智能·自动化