news 2026/9/28 18:25:46

【pi-mono】Pi-Mono 系统级架构深入分析:从 Monorepo 到 Agent 的 TypeScript 工程化落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【pi-mono】Pi-Mono 系统级架构深入分析:从 Monorepo 到 Agent 的 TypeScript 工程化落地

1. 为什么我要拆 pi-mono 的 Monorepo 架构

第一次看到 pi-mono 的仓库结构时,我盯着packages/目录看了很久。一个 AI 编程助手项目,居然拆出了 7 个核心子包,从底层 LLM API 抽象一路铺到终端 TUI、Web 界面、Slack 机器人,甚至还有 GPU Pod 管理 CLI。这种"系统级"的工程化组织方式,和大多数把代码堆在src/里的项目完全不是一个思路。

pi-mono 是一个用 TypeScript 编写的 AI 编程助手 Monorepo,采用 npm workspaces 管理多包协作,构建工具是 tsgo(TypeScript 编译器)+ vite,包管理用 npm,版本号 0.67.68,总源文件约 600 个.ts/.tsx。它的核心价值在于:把"LLM 调用 → Agent 循环 → 工具执行 → 多端渲染"这条链路拆成了可独立演进、可单独测试、可被不同前端复用的分层结构。

这篇文章适合谁?如果你正在做以下任何一件事,pi-mono 的架构都值得参考:想把一个 AI 应用从单文件脚本演进成多端产品;想用 npm workspaces 管理一个包含 Agent 核心、UI 层、CLI 工具的中大型 TypeScript 项目;或者你单纯想搞清楚"一个 Agent 系统到底该怎么分层"。我会给出可复制的 workspaces 配置骨架、包依赖拓扑,以及构建验证命令,让你能在自己的项目里复现同类架构。

2. 前置准备:TaoToken 与本地环境

在动手复现架构之前,得先解决"Agent 到底调用谁"的问题。pi-mono 的@pi-ai层抽象了 22+ 个 Provider,但如果你只是想跑通自己的 Agent 循环,没必要一开始就接那么多。我建议先用一个兼容 OpenAI 协议的统一入口把链路打通,TaoToken 就是这样一个选择——它提供统一的 API 端点,模型对话、Coding Plan、API Keys 管理都有对应的控制台入口。

具体来说,你需要准备三样东西:

第一,一个可用的 API Key。登录 TaoToken 控制台后,在 API Keys 页面创建一个密钥,格式通常是sk-开头的一串字符。这个 Key 会作为环境变量注入到你的 Agent 配置里。

第二,确认你的 Node.js 版本。pi-mono 用的是 ESM 模块体系,建议 Node 18 以上,npm 9 以上,因为 npm workspaces 的--workspace参数在旧版本上行为不一致。

第三,一个空目录作为 Monorepo 根。不要在一个已有package.json的项目里直接套 workspaces,依赖提升(hoisting)会和你原有的node_modules打架。

注意:TaoToken 的 API 端点是https://taotoken.net/api,不要在后面拼/v1之类的路径,具体路径由你调用的 SDK 决定。模型对话入口在控制台里可以直接测试,接入文档里有各语言的示例。

环境变量建议这样组织,放在根目录的.env.local(记得加进.gitignore):

# .env.local TAOTOKEN_API_KEY=sk-your-key-here TAOTOKEN_BASE_URL=https://taotoken.net/api

然后在代码里通过process.env.TAOTOKEN_API_KEY读取。pi-mono 的AuthStorage模块做的就是类似的事——把密钥从环境变量或文件里读出来,统一注入到 Provider 层,业务代码不直接碰密钥。

3. 可复制的 npm workspaces 配置骨架

pi-mono 的包依赖拓扑是这样的(自底向上):

@pi-tui ─────────────┐ @pi-web-ui ──────────┤ @pi-mom ─────────────┼──→ @pi-coding-agent ──→ @pi-agent-core ──→ @pi-ai @pi-pods ────────────┘

@pi-ai是最底层,不依赖任何内部包;@pi-agent-core只依赖@pi-ai;@pi-coding-agent依赖前三者;UI 层(tui/web-ui/mom/pods)各自依赖@pi-coding-agent或@pi-agent-core。这个拓扑的关键约束是:依赖只能向上,不能向下,也不能横向。@pi-ai绝对不能 import@pi-agent-core的任何东西,否则整个分层就塌了。

根目录package.json的 workspaces 配置骨架:

{ "name": "my-agent-monorepo", "version": "0.1.0", "private": true, "type": "module", "workspaces": [ "packages/ai", "packages/agent-core", "packages/coding-agent", "packages/tui", "packages/web-ui" ], "scripts": { "build": "npm run build --workspaces --if-present", "build:ai": "npm run build --workspace=packages/ai", "typecheck": "tsc --build --dry", "clean": "rm -rf packages/*/dist packages/*/node_modules node_modules" }, "devDependencies": { "typescript": "^5.6.0", "tsgo": "^0.1.0" } }

每个子包的package.json要显式声明内部依赖,用workspace:*协议(npm 9+ 支持):

{ "name": "@my/agent-core", "version": "0.1.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "dependencies": { "@my/ai": "workspace:*" }, "scripts": { "build": "tsgo -p tsconfig.json" } }

这里有几个我踩过的坑。第一,exports字段必须同时给types和import,否则 TypeScript 在 ESM 下解析不到类型声明,编辑器里全是红波浪线。第二,main指向dist而不是src,因为 workspace 之间是通过构建产物互相引用的,不是直接读源码——这一点和某些用 tsconfig paths 直接映射源码的方案不同,pi-mono 走的是"先构建、再引用"的路线,好处是每个包可以独立发布,坏处是改了下层包必须重新 build 才能被上层看到。

根tsconfig.json用 project references 组织:

{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "declaration": true, "composite": true, "skipLibCheck": true }, "references": [ { "path": "packages/ai" }, { "path": "packages/agent-core" }, { "path": "packages/coding-agent" } ] }

composite: true是 project references 的硬性要求,它会让 tsc 生成.tsbuildinfo文件,增量构建时只重编改动的包。moduleResolution: "Bundler"是为了配合 ESM 的exports字段解析。

4. Agent 模块的组织方式与核心循环

pi-mono 把 Agent 拆成两层:@pi-agent-core提供纯粹的 Agent 循环,不绑定任何具体工具;@pi-coding-agent在它之上实现编码场景的会话管理、工具集和扩展机制。这个拆法的好处是,如果你要做一个非编码类的 Agent(比如客服机器人),可以直接复用@pi-agent-core,不用拖上整个 coding-agent。

@pi-agent-core的核心是agentLoop(),它的执行流程是这样的:

// packages/agent-core/src/agent-loop.ts(简化示意) export async function agentLoop( agent: Agent, prompts: AgentMessage[], streamFn: StreamFn ): Promise<AgentMessage[]> { const messages = agent.convertToLlm(prompts); let stopReason: string = "toolUse"; while (stopReason === "toolUse") { const stream = await streamFn(agent.model, { messages }); const assistantMsg = await collectStream(stream); messages.push(assistantMsg); const toolCalls = extractToolCalls(assistantMsg); if (toolCalls.length === 0) { stopReason = "stop"; break; } for (const call of toolCalls) { await agent.hooks.beforeToolCall?.(call); const result = await agent.executeTool(call); await agent.hooks.afterToolCall?.(call, result); messages.push(toToolResultMessage(call, result)); } } return messages; }

这个循环的关键设计点有三个。第一,streamFn是注入的,不是硬编码的,所以你可以换成任何实现了stream(model, context, options)签名的函数——pi-mono 的@pi-ai提供的就是这个签名。第二,工具执行有beforeToolCall和afterToolCall钩子,扩展系统就是挂在这两个点上。第三,循环终止条件是stopReason !== "toolUse",而不是简单的"没有工具调用",这样能兼容某些 Provider 返回的中间状态。

工具执行模式支持sequential和parallel两种。parallel模式下,多个工具调用并发执行,但结果按原始顺序收集回上下文——这个细节很重要,因为 LLM 期望 toolResult 的顺序和 toolCall 的顺序一致,乱序会导致后续推理出错。

@pi-coding-agent在 Agent 之上包了一层AgentSession,负责会话持久化和上下文压缩。会话用 JSONL 格式存储,每个会话一个目录,包含context.jsonl(结构化 API 消息)和log.jsonl(人类可读日志)。当对话超过模型上下文窗口时,shouldCompact()检测触发,findCutPoint()找到合适的截断点,compact()把旧消息替换成摘要。这套机制让长对话不会因为 token 溢出而崩掉。

5. 验证请求与构建结果

配置写完后,先验证 workspace 链接是否正确:

npm install npm ls --workspaces --depth=0

你应该看到所有子包被列出来,且内部依赖显示为-> ./packages/xxx的符号链接形式。如果某个包显示missing,说明 workspaces 数组里的路径写错了。

接着构建底层包,验证 TypeScript 编译链路:

npm run build:ai ls packages/ai/dist/

正常输出应该包含index.js、index.d.ts和若干.tsbuildinfo。如果报Cannot find module '@my/ai',八成是上层包的package.json里没写workspace:*依赖,或者exports字段的路径和实际产物对不上。

然后写一个最小验证脚本,确认 Agent 循环能跑通:

// packages/coding-agent/scripts/smoke.ts import { createAgentSession } from "@my/coding-agent"; const session = await createAgentSession({ apiKey: process.env.TAOTOKEN_API_KEY!, baseUrl: process.env.TAOTOKEN_BASE_URL!, model: "gpt-4o-mini", }); const result = await session.prompt("用一句话解释什么是 Monorepo"); console.log(result.content);

用tsx或node --loader ts-node/esm跑这个脚本。如果返回了模型输出,说明从@pi-ai的 Provider 层到@pi-agent-core的循环层再到@pi-coding-agent的会话层,整条链路是通的。如果卡在streamFn报 401,检查 API Key 是否从环境变量正确读取;如果报fetch failed,检查baseUrl是否写成了https://taotoken.net/api而不是带/v1的路径。

构建全部包并做类型检查:

npm run build npm run typecheck

typecheck用的是tsc --build --dry,它不会真正输出文件,只检查 project references 的依赖顺序和类型一致性。如果某个包的类型声明没生成,这个命令会直接报错,比等到运行时才发现问题要早得多。

6. 本篇常见错误排查

错误一:npm ERR! workspace not found

原因通常是workspaces数组里的 glob 路径和实际目录不匹配。pi-mono 用的是显式列出每个包路径的写法,而不是packages/*通配。显式列出的好处是构建顺序可控,坏处是新增包容易忘。如果你用通配符,确保packages/下没有非包的目录(比如docs/),否则 npm 会尝试把它当 workspace 处理。

错误二:ESM 下Cannot use import statement outside a module

这是package.json里漏了"type": "module"。每个子包都要加,不只是根目录。另外,如果你的构建产物是.js但源码是.ts,确保tsconfig.json的module设为ESNext或NodeNext,不要用CommonJS。

错误三:类型声明找不到,编辑器报Could not find a declaration file

检查子包的exports字段。TypeScript 在moduleResolution: "Bundler"下会优先读exports.types,如果只写了main和types而没写exports,某些版本的 TS 会解析失败。最稳妥的写法是exports里同时给types和import两个条件。

错误四:改了底层包,上层包没更新

这是"先构建再引用"模式的固有代价。解决方案有两个:一是用npm run build --workspaces全量重建;二是开发期用tsc --watch在每个包上跑增量编译。pi-mono 的tsgo本身就支持 watch 模式,比原生 tsc 快不少。如果你实在受不了这个延迟,可以临时在根 tsconfig 里加paths映射到源码,但发布前一定要切回产物引用。

错误五:Agent 循环卡死不退出

检查stopReason的判断逻辑。有些 Provider 在工具调用后会返回stopReason: "toolUse"但实际没有 toolCall,导致循环空转。在extractToolCalls后加一个判断:如果toolCalls.length === 0,强制把stopReason设为"stop"。另外给循环加一个最大迭代次数(比如 20 次),防止无限循环烧 token。

7. 从架构复现到实际接入

把 Monorepo 骨架搭起来只是第一步,真正让 Agent 跑起来还需要一个稳定的模型入口。我建议的接入顺序是:先用模型对话页面验证你的 API Key 和 baseUrl 能正常返回,再把同样的配置注入到@pi-ai的 Provider 层。如果你打算长期在这个架构上做编码类 Agent,Coding Plan 提供了更适合高频调用的配额方案,比按次计费更划算。

接入文档里有各语言的完整示例,包括流式和非流式两种调用方式。对于 pi-mono 这种基于EventStream的架构,你需要的是流式接口——stream(model, context, options)返回一个异步迭代器,逐块吐出AssistantMessage的增量内容。把@pi-ai的streamSimple()替换成你自己的实现,只要签名一致,上层agentLoop()完全不用改。

最后提醒一点:Monorepo 的包边界一旦定下来,就不要轻易让上层包反向依赖下层包。我见过太多项目一开始分层清晰,后来为了"图方便"在@pi-ai里 import 了@pi-agent-core的类型,结果整个依赖图变成一团乱麻,构建顺序再也理不清。pi-mono 的拓扑之所以能保持干净,就是因为每个包的职责边界卡得很死——@pi-ai只管 Provider 抽象,@pi-agent-core只管循环,@pi-coding-agent只管编码场景。你的项目也应该这样。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 18:25:17

STM32 上跑 MQTT 怎么选?嵌入式 MQTT 客户端 C 语言实现对比

STM32 上跑 MQTT 怎么选&#xff1f;嵌入式 MQTT 客户端 C 语言实现对比做嵌入式设备联网&#xff0c;STM32 上跑 MQTT 是绕不开的话题。LwIP 开源 MQTT 客户端自己写&#xff0c;还是直接用现成 SDK&#xff1f;本文对比主流选择&#xff0c;给出 STM32F4 实测结论。建议 CSD…

作者头像 李华
网站建设 2026/9/28 18:24:45

智谱 Z Code 配置 TaoToken:Claude Code、Codex、Gemini 统一 Key 接入指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 18:24:25

UVa 930 Polynomial Roots

题目描述 给定一个 nnn 次多项式 P(x)anxnan−1xn−1…a1xa0P(x) a_{n}x^{n} a_{n - 1}x^{n - 1} \ldots a_{1}x a_{0}P(x)an​xnan−1​xn−1…a1​xa0​ 的全部 n1n 1n1 个系数&#xff0c;以及该多项式的 n−2n - 2n−2 个实根&#xff0c;要求计算出剩下的两个实根。…

作者头像 李华