跳到主要内容

创建日期:2026-09-07 | 最近更新:2026-09-07 事实核对基于 @earendil-works/pi-ai / @earendil-works/pi-agent-core 0.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(本文)
1pi-ai:统一几十家 LLM 的模型连接层:provider / models / 鉴权 / 工具 / 流式事件 / thinking / 跨厂换手 / 序列化
2pi-agent-core:Agent harness 的多轮循环与事件流Agent 类 / 工具生命周期 / 会话与记忆 / 低层 loop
3拆 InkOS:确认闸门、原子落盘、记忆检索怎么在 pi 上长出来待写

InkOS 系列 是上下游:InkOS 的 packages/core 就是 @mariozechner/pi-ai + pi-agent-core 0.67.1(老 scope、改名前的版本)上叠出来的生产 harness。读懂 pi 再看 InkOS,等于先拿到地基再看房子。


1. pi-agent 到底是什么

先说它不做什么:pi 不是要帮你编排任务图的框架(不是 LangChain 那种「链/图/记忆抽象一堆」的东西),也不是聊天 UI(那是下游 pi-web-ui 的事)。pi 做的是 agent 最吃紧的那段脏活:

  1. 连模型:几十家厂商、各自的鉴权、各自的流式格式、各自的工具调用细节、各家 thinking/reasoning 参数不一样……
  2. 跑循环:模型「提议调工具」→ 宿主执行 → 把 toolResult 回填 → 再让模型继续,直到它说停。以及这个过程中该发出的每一条事件,好让 UI / 日志 / 会话持久化都有抓手。

这两件事被拆成两个包,各管一层:

为什么值得单独研究它(相对直接用各家 SDK):

  • 模型是「纯数据」:一个 Model 对象就是一堆可 JSON 序列化的字段(id/api/provider/baseUrl/reasoning/input/output/cost/contextWindow…),没有挂任何实现。厂商目录是生成的静态数据,同步查询、可序列化、可随意替换。
  • 换模型 / 换厂商是「配置」不是「重写」:同一个 Context(含系统提示、多轮消息、工具调用与结果)可以在任意支持它的模型间搬来搬去,库会自动做兼容转换。
  • 无 Key 也能开发和测试:内置 fauxProvider() 用脚本化响应模拟模型,本文所有示例就是它跑的。
  • 树摇友好:每个厂商一个子路径入口(providers/openaiproviders/anthropic…),按需引入。
  • 链路透明:流式事件(text_delta / thinking_delta / toolcall_delta)一路打通到 UI,工具参数是边流边解析的 partial JSON

血统与改名史(认个脸,免得搜错仓库)

新(当前)
GitHub 仓库badlogic/pi-monogithub.com/earendil-works/pi(monorepo,包在 packages/aipackages/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-coreAgent:会调工具的带状态多轮循环 + 事件流本系列主角,写 agent 的主入口
pi-coding-agent开箱即用的终端编程 agent(read/bash/edit/write + 会话管理)想先用成品感受「pi 系 agent」手感,或给它写 Skill
pi-session-backend-sqlite-node会话持久化的 SQLite 后端(Node node:sqliteagent 要重启不丢记忆时
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. 两个心智模型(动手前先刻进脑子)

  1. 模型提议,宿主执行。 模型绝不直接碰文件/网络,它只是提议一个结构化工具调用(get_weather({location:"上海"}));真正执行的是你的代码,执行结果以 toolResult 回填给模型。pi 把这条循环的骨架给你了,执行权始终在你手里——这是它跟「auto-GPT 式乱跑」的根本区别,也是安全闸门能落下去的前提。
  2. 分两层看问题。 遇到报错先想是「连模型这层」还是「agent 循环这层」:pi-ai 管的是把 Context(系统提示+消息+工具)发给某个模型并拿回结构化的回复;pi-agent-core 管的是「要不要继续调下一个工具、上下文该不该压缩、会话往哪存、事件往哪发」。调 Key / 模型名 / 参数 → 查 pi-ai;调循环行为 / 事件 / 会话 → 查 agent-core。
  3. 消息里可以混进「只有人/UI 看得懂」的东西。 AgentMessage 允许自定义角色(比如一条 notification),LLM 看不懂没关系,convertToLlm 会在每次发请求前把「给人看的」过滤掉、把「给模型的」翻译成标准 user/assistant/toolResult。这让 UI 状态和模型上下文解耦。
  4. 一切都可序列化。 ContextModel、会话都能 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 -gnpx 跑一下 pi-coding-agent,用聊天式终端体验「有工具、有会话的 agent」到底长什么样(它本身就是 pi-ai/agent-core 的集大成应用)。
  • 在自己项目里加一个会调工具的助手:照 4.1 抄,把 fauxProvider 换成真实厂商即可。
  • 读懂 InkOS:InkOS 的确认闸门、原子落盘、Zod 状态机,全部是长在 pi 之上的。读完 篇 2 的事件流 再回头看它的「模型提议 → 宿主确认 → 确定性工具执行 → 以落盘为准」,处处对得上。

参考