创建日期:2026-09-07 | 最近更新:2026-09-07 事实核对基于
@earendil-works/pi-ai/@earendil-works/pi-agent-core0.85.1(2026-09-05 发布于 npm,作者 Mario Zechner)的 README、源码与本机 Node 24 实测;示例代码均已跑通(用内置的fauxProvider,不需要任何 API Key)。版本演进后以 README 为准。
pi 入门:会调工具、能换模型的 Agent 底座
一句话:pi 是 Mario Zechner(
badlogic,LibGDX 作者)维护的一套 TypeScript agent 运行时,它把「连各种大模型」和「跑 agent 循环」拆成两层:pi-ai统一几十家 LLM 的接入,pi-agent-core提供会说话、会调工具、有状态、有事件流的多轮 Agent。本文写的「pi-agent」就是指这套框架——InkOS 系列里称它为 pi-agent harness,本栏目顺着叫。官方名就一个字:pi。
本文定位
本系列把 pi 当作「写 agent 时的工程底座」来拆,而不是平铺文档。目标是把 它是什么 / 两层怎么分工 / 我 30 分钟能跑出什么 / 坑在哪 讲清楚,给「想自己搭 agent 或看懂 InkOS 这类生产 harness」的人一份可验证的速查。
| 篇 | 主题 | 状态 |
|---|---|---|
| 0 | 入门:pi 是什么 / 家族地图 / 装好 / 第一个会调工具的 Agent(本文) | ✅ |
| 1 | pi-ai:统一几十家 LLM 的模型连接层:provider / models / 鉴权 / 工具 / 流式事件 / thinking / 跨厂换手 / 序列化 | ✅ |
| 2 | pi-agent-core:Agent harness 的多轮循环与事件流:Agent 类 / 工具生命周期 / 会话与记忆 / 低层 loop | ✅ |
| 3 | 拆 InkOS:确认闸门、原子落盘、记忆检索怎么在 pi 上长出来 | 待写 |
和 InkOS 系列 是上下游:InkOS 的
packages/core就是@mariozechner/pi-ai+pi-agent-core0.67.1(老 scope、改名前的版本)上叠出来的生产 harness。读懂 pi 再看 InkOS,等于先拿到地基再看房子。
1. pi-agent 到底是什么
先说它不做什么:pi 不是要帮你编排任务图的框架(不是 LangChain 那种「链/图/记忆抽象一堆」的东西),也不是聊天 UI(那是下游 pi-web-ui 的事)。pi 做的是 agent 最吃紧的那段脏活:
- 连模型:几十家厂商、各自的鉴权、各自的流式格式、各自的工具调用细节、各家 thinking/reasoning 参数不一样……
- 跑循环:模型「提议调工具」→ 宿主执行 → 把
toolResult回填 → 再让模型继续,直到它说停。以及这个过程中该发出的每一条事件,好让 UI / 日志 / 会话持久化都有抓手。
这两件事被拆成两个包,各管一层:
为什么值得单独研究它(相对直接用各家 SDK):
- 模型是「纯数据」:一个
Model对象就是一堆可 JSON 序列化的字段(id/api/provider/baseUrl/reasoning/input/output/cost/contextWindow…),没有挂任何实现。厂商目录是生成的静态数据,同步查询、可序列化、可随意替换。 - 换模型 / 换厂商是「配置」不是「重写」:同一个
Context(含系统提示、多轮消息、工具调用与结果)可以在任意支持它的模型间搬来搬去,库会自动做兼容转换。 - 无 Key 也能开发和测试:内置
fauxProvider()用脚本化响应模拟模型,本文所有示例就是它跑的。 - 树摇友好:每个厂商一个子路径入口(
providers/openai、providers/anthropic…),按需引入。 - 链路透明:流式事件(
text_delta/thinking_delta/toolcall_delta)一路打通到 UI,工具参数是边流边解析的 partial JSON。
血统与改名史(认个脸,免得搜错仓库)
| 旧 | 新(当前) | |
|---|---|---|
| GitHub 仓库 | badlogic/pi-mono | github.com/earendil-works/pi(monorepo,包在 packages/ai、packages/agent 等) |
| npm scope | @mariozechner/pi-* | @earendil-works/pi-* |
| 状态 | 已弃用(npm 上标 deprecated,提示改用新 scope) | 0.85.1(2026-09-05) |
| 作者 | Mario Zechner | 同一人 |
所以搜代码时旧教程里的
@mariozechner/pi-ai等于现在的@earendil-works/pi-ai。InkOS 1.8.0 锁的 0.67.1 还是老 scope——那是改名前的版本,不是笔误。
pi 家族(monorepo 地图)
github.com/earendil-works/pi (monorepo,@earendil-works scope)
├─ pi-ai # 统一 LLM API + 厂商/模型目录 + 鉴权 + 工具
├─ pi-agent-core # ★ Agent harness:会话 + 工具循环 + 事件流
├─ pi-coding-agent # 类 Claude Code 的终端编程 agent CLI(pi 的旗舰下游)
├─ pi-session-backend-sqlite-node # agent-core 会话的 Node sqlite 后端
├─ pi-telemetry # 厂商无关 telemetry 契约与 schema
├─ pi-tui # 差分渲染的终端 UI 库
├─ pi-web-ui # AI 聊天界面的 web components
└─ chord(独立仓库/scope) # 服务/RPC/插件运行时,agent-core 的 facet 原语
包(@earendil-works/*,0.85.1) | 一句话 | 你会什么时候用到 |
|---|---|---|
| pi-ai | 统一 LLM 接入:模型目录、鉴权、流式、工具、thinking | 任何「要跟模型说话」的代码 |
| pi-agent-core | Agent:会调工具的带状态多轮循环 + 事件流 | 本系列主角,写 agent 的主入口 |
| pi-coding-agent | 开箱即用的终端编程 agent(read/bash/edit/write + 会话管理) | 想先用成品感受「pi 系 agent」手感,或给它写 Skill |
| pi-session-backend-sqlite-node | 会话持久化的 SQLite 后端(Node node:sqlite) | agent 要重启不丢记忆时 |
| pi-telemetry | 埋点契约与 schema | 想给 agent 加可观测性 |
| pi-tui / pi-web-ui | 终端 / 网页聊天 UI | 想给 agent 包一层界面 |
| chord | 服务、RPC、状态同步、插件组合运行时 | 多进程/插件化 agent(较进阶) |
pi-ai / agent-core 要求 Node ≥ 22.19。本文用 Node 24 实测。
2. 安装
npm i @earendil-works/pi-ai @earendil-works/pi-agent-core
按需再加:会话持久化 npm i @earendil-works/pi-session-backend-sqlite-node。
顺便:pi-ai 自带一个 CLI(
npx @earendil-works/pi-ai login/list),用来给各厂商做 OAuth / 存 Key,后面篇 1 讲鉴权时会提到。
3. 两个心智模型(动手前先刻进脑子)
- 模型提议,宿主执行。 模型绝不直接碰文件/网络,它只是提议一个结构化工具调用(
get_weather({location:"上海"}));真正执行的是你的代码,执行结果以toolResult回填给模型。pi 把这条循环的骨架给你了,执行权始终在你手里——这是它跟「auto-GPT 式乱跑」的根本区别,也是安全闸门能落下去的前提。 - 分两层看问题。 遇到报错先想是「连模型这层」还是「agent 循环这层」:
pi-ai管的是把Context(系统提示+消息+工具)发给某个模型并拿回结构化的回复;pi-agent-core管的是「要不要继续调下一个工具、上下文该不该压缩、会话往哪存、事件往哪发」。调 Key / 模型名 / 参数 → 查 pi-ai;调循环行为 / 事件 / 会话 → 查 agent-core。 - 消息里可以混进「只有人/UI 看得懂」的东西。
AgentMessage允许自定义角色(比如一条notification),LLM 看不懂没关系,convertToLlm会在每次发请求前把「给人看的」过滤掉、把「给模型的」翻译成标准user/assistant/toolResult。这让 UI 状态和模型上下文解耦。 - 一切都可序列化。
Context、Model、会话都能JSON.stringify存起来再恢复。想持久化/断点续聊/跨服务搬上下文,不需要另造格式。
4. 30 分钟上手:写出第一个会调工具的 Agent
下面这段我实际跑过。它不联网、不要 Key——用 pi 内置的 fauxProvider(脚本化响应的假模型)模拟了一个「先查天气、再回答」的两轮对话。真机跑时把假 provider 换成真实厂商即可(差异只在 3 行)。
4.1 用 Agent(agent-core 自动跑工具循环)
import { Type, createModels, fauxProvider,
fauxAssistantMessage, fauxText, fauxToolCall } from '@earendil-works/pi-ai';
import { Agent } from '@earendil-works/pi-agent-core';
// ---- ① 模型层:先造一个 Models 集合,注册 provider ----
const faux = fauxProvider(); // 假模型:响应按队列消费,无网络
const models = createModels();
models.setProvider(faux.provider);
const model = faux.getModel();
// 脚本化两轮:第一轮模型要调 get_weather;拿到 toolResult 后第二轮给最终文本
faux.setResponses([
fauxAssistantMessage([fauxToolCall('get_weather', { location: 'Shanghai' })], { stopReason: 'toolUse' }),
fauxAssistantMessage([fauxText('上海今天 25°C,晴。(由 Agent 自动完成)')]),
]);
// ---- ② Agent 层:声明系统提示 + 工具,剩下的循环交给 Agent ----
const agent = new Agent({
initialState: {
systemPrompt: '你是一个会调用工具的助手。',
model,
tools: [{
name: 'get_weather',
label: '查天气',
description: '查询某城市的天气',
parameters: Type.Object({ location: Type.String({ description: '城市名' }) }),
execute: async (toolCallId, params) => {
console.log(` [tool] get_weather(${params.location}) 执行中…`);
return {
content: [{ type: 'text', text: JSON.stringify({ location: params.location, celsius: 25, condition: '晴' }) }],
};
},
}],
},
streamFn: models.streamSimple.bind(models), // 把「怎么连模型」喂给 Agent
});
// 订阅事件:想接 UI / 日志 / 流式输出就靠它
agent.subscribe((event) => {
const t = event.type;
if (t === 'turn_start') console.log(`== ${t} ==`);
if (t === 'message_end') {
const kinds = (event.message.content ?? []).map((b) => b.type).join(',');
console.log(` [message_end] ${event.message.role} content=[${kinds}]`);
}
if (t === 'tool_execution_start') console.log(` [tool_execution_start] ${event.toolName}`);
if (t === 'turn_end') console.log(`== ${t} == toolResults=${event.toolResults.length}`);
if (t === 'agent_end') console.log(`== ${t} == messages=${event.messages.length}`);
});
await agent.prompt('上海天气怎么样?');
console.log('\n最终上下文条数:', agent.state.messages.length);
运行输出(实测):
== turn_start ==
[message_end] user content=[text]
[message_end] assistant content=[toolCall] ← 模型提议:get_weather
[tool_execution_start] get_weather
[tool] get_weather(Shanghai) 执行中…
[tool_execution_end] get_weather ok=true
[message_end] toolResult content=[text] ← 宿主把结果回填
== turn_end == toolResults=1
== turn_start == ← 自动开第二轮
[message_end] assistant content=[text] ← 模型读完 toolResult 后回答
== turn_end == toolResults=0
== agent_end == messages=4
最终上下文条数: 4
注意第 1 行的两个关键字:模型那条消息的 content 是 toolCall(不是嘴上的承诺),随后 tool_execution_* 是你的代码在执行。「模型提议 → 宿主执行 → 回填 → 再来一轮」这个循环,由 Agent 自动闭环了。
4.2 不用 Agent,手写同一条循环(pi-ai 层,让你看清 harness 替你做了什么)
import { Type, createModels, fauxProvider,
fauxAssistantMessage, fauxText, fauxToolCall } from '@earendil-works/pi-ai';
const faux = fauxProvider();
const models = createModels();
models.setProvider(faux.provider);
const model = faux.getModel();
const context = {
systemPrompt: '你是一个会调用工具的助手。',
messages: [{ role: 'user', content: '上海天气怎么样?', timestamp: Date.now() }],
tools: [{
name: 'get_weather',
description: '查询某城市的天气',
parameters: Type.Object({ location: Type.String({ description: '城市名' }) }),
}],
};
// 第一轮:模型决定调 get_weather
faux.setResponses([
fauxAssistantMessage([fauxToolCall('get_weather', { location: 'Shanghai' })], { stopReason: 'toolUse' }),
]);
const first = await models.complete(model, context);
context.messages.push(first);
// ↑ stopReason === 'toolUse' → 说明它想要工具结果,循环该继续
// 宿主执行工具,把结果作为 toolResult 追加进上下文
const call = first.content.find((b) => b.type === 'toolCall');
context.messages.push({
role: 'toolResult',
toolCallId: call.id,
toolName: call.name,
content: [{ type: 'text', text: JSON.stringify({ location: 'Shanghai', celsius: 25, condition: '晴' }) }],
isError: false,
timestamp: Date.now(),
});
// 第二轮:模型读 toolResult 后给出最终回答
faux.setResponses([fauxAssistantMessage([fauxText('上海今天 25°C,晴。')])]);
const second = await models.complete(model, context);
context.messages.push(second);
console.log('最终回答:', second.content.filter((b) => b.type === 'text').map((b) => b.text).join(''));
这两段对照着读,就能看出 Agent 帮你省掉的正是那段 while stopReason==='toolUse' 的机械循环(外加会话、事件、失败兜底)。这也呼应了本文开头:pi-ai 给你「跟模型打交道的原语」,pi-agent-core 给你「把循环跑稳的骨架」。
5. 换个真实模型跑(只需改 3 行)
把 4.1 里的模型层换成真实厂商(下面以 Anthropic 为例,代码来自官方 README,未实测——需要 ANTHROPIC_API_KEY):
import { createModels } from '@earendil-works/pi-ai';
import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic';
const models = createModels();
models.setProvider(anthropicProvider()); // env 里读 ANTHROPIC_API_KEY
const model = models.getModel('anthropic', 'claude-sonnet-4-6');
其余(new Agent({...})、streamFn: models.streamSimple.bind(models)、工具定义)一字不改。换厂商不换业务代码——这是「模型即数据 + provider 集合」的直接红利。
内置 provider 一长串(pi-ai 只收录支持工具调用的模型):OpenAI、Anthropic、Google、Mistral、Azure OpenAI、OpenAI Codex、DeepSeek、NVIDIA NIM、Groq、Cerebras、Cloudflare AI Gateway/Workers AI、xAI、OpenRouter、Vercel AI Gateway、Together、Baseten、Hugging Face、Moonshot、MiniMax、Qwen Token Plan、小米 MiMo、Kimi For Coding、GitHub Copilot、Amazon Bedrock、ZAI Coding Plan、Ant Ling 等三十余家,外加任意 OpenAI 兼容端点(Ollama / vLLM / LM Studio)——自己造一个 createProvider 就接入。详细清单与鉴权见 篇 1。
6. 现在你能做什么
- 想快速判断手感:
npm i -g或npx跑一下 pi-coding-agent,用聊天式终端体验「有工具、有会话的 agent」到底长什么样(它本身就是 pi-ai/agent-core 的集大成应用)。 - 想在自己项目里加一个会调工具的助手:照 4.1 抄,把
fauxProvider换成真实厂商即可。 - 想读懂 InkOS:InkOS 的确认闸门、原子落盘、Zod 状态机,全部是长在 pi 之上的。读完 篇 2 的事件流 再回头看它的「模型提议 → 宿主确认 → 确定性工具执行 → 以落盘为准」,处处对得上。
参考
- 仓库(monorepo):github.com/earendil-works/pi(旧:
github.com/badlogic/pi-mono) @earendil-works/pi-aiREADME / npm:npmjs.com/package/@earendil-works/pi-ai@earendil-works/pi-agent-coreREADME / npm:npmjs.com/package/@earendil-works/pi-agent-core- 上游消费方:本仓库 InkOS 入门(InkOS 依赖
@mariozechner/pi-*0.67.1) - 许可:MIT
- 本文事实基于
@earendil-works/pi-ai/pi-agent-core0.85.1(2026-09-05 发布)核对;示例在 Node 24 实测。版本演进后以 README 与pi-ai --help为准。