PI Agent

PI Agent:把工作流选择权交还给使用者

一个 Coding Agent 真正难的地方,不是写出一个能调用模型的 while,而是让模型协议、工具循环、会话历史、终端交互和工作流扩展彼此解耦;PI Agent 的回答是把系统拆成四层,并把核心压缩到 "状态、事件、工具循环和停止条件" 这些稳定职责上;其余能力通过会话层、资源加载器和扩展 API 组合出来

沿着一次请求从 CLI 或 SDK 进入、经过会话与 Agent core、流过模型适配器和工具钩子、最后落到可分支的 JSONL 历史这条路径阅读 PI,可以同时看懂它的包边界、执行控制和产品取舍

建立全局地图:

PI 的代码仓库不是一个把所有能力塞进 CLI 的大包,而是由四层职责组成的 monorepo(单体代码仓库);依赖方向从产品表面向底层协议收敛,底层包不反过来依赖终端 UI

主要职责典型位置
pi-ai模型目录、provider 适配、统一消息与流事件packages/ai/src
pi-agent-core有状态 Agent、turn 生命周期、工具调用与事件packages/agent/src
pi-coding-agent会话、资源、上下文策略、扩展和 Coding 产品规则packages/coding-agent/src/core
pi-tui终端输入、渲染和交互表面packages/tui

这四层形成一条清晰的请求流程:

CLI / SDK
  -> AgentSession
  -> Agent core
  -> pi-ai stream
  -> tool hooks
  -> JSONL session tree

这里最重要的不是箭头本身,而是谁拥有哪种状态;模型协议只负责把厂商差异翻译成统一事件,Agent core 只推进一次运行,AgentSession 才负责队列、资源和持久化,TUI 只把可观察状态投影给用户

PI 的总览可以浓缩成三条设计原则:

  1. 核心只承诺生命周期;它处理 prompt、流事件、tool call 和停止条件,却不替上层决定计划模式、子任务或审批体验
  2. 模型先统一成事件流;Anthropic、OpenAI、Gemini 或本地路由的差异停留在 provider 适配器中
  3. 会话和扩展属于产品层;AgentSession、JSONL tree 与 ExtensionAPI 共同提供可操控、可分支、可重载的使用体验

因此,PI 与其他 Agent 路线的差异不是功能多少,而是复杂度放在哪里:DSH 把边界做成插件树,nanobot 强调直接可读的分层,PI 则把工作流选择权交给外围层和使用者

统一模型协议:pi-ai 把多家模型压成一条流

为什么要有这一层:

不同模型在认证方式、思考块、缓存、工具调用和流式事件上各不相同;如果这些差异直接进入 Agent Loop,循环就会被 provider 分支占满,换模型也会牵动上层逻辑

pi-ai 提供模型目录和统一的 stream 函数,让上层只面对稳定的 MessageToolCallAssistantMessageEvent;常见的事件词汇包括开始、增量文本、工具调用、完成、错误、使用量和停止原因

请求怎样流过适配器:

// 根据服务商与模型ID,获取模型实例(封装apiKey、baseUrl、参数配置)
const model = models.getModel(provider, modelId)

// 发起大模型流式请求,返回异步事件流(文本分片、思考分片、工具调用分片等)
const stream = models.streamSimple(
  model, llmContext, options
)

// 消费流式事件,每收到一个分片就向外派发事件(供上层回调/TUI渲染使用)
for await (const event of stream) emit(event)

这里的 Model Registry 负责选择模型,provider 负责向厂商发请求,streamSimple() 把原始响应转换成统一的助手事件流;Agent 不需要知道当前模型来自哪家服务,只需消费这些事件并更新状态

在源码中验证:

  • packages/ai/src/types.ts:查看 AssistantMessageEvent 等联合类型,确认文本、思考、工具调用和结束原因的公共表示
  • packages/ai/src/providers:对照不同 provider,观察差异如何在适配器内被翻译
  • packages/agent/src/stream-fn.ts:查看 Agent core 接收统一流的入口

这一层解决的是协议边界,不是 Agent 决策;模型目录、事件格式、token 和成本统计统一之后,循环才有可能保持窄而稳定

Agent Loop:状态机如何反复调用工具:

一个 turn 何时结束:

在 PI 中,一个 turn 包含一次模型响应以及由它触发的工具执行;只要工具结果仍然需要交回模型,Agent run 就继续,直到模型不再请求工具、用户取消或出现不可恢复错误

await agent.prompt(userMessage)

agent_start -> turn_start
  assistant stream
  tool_execution*
  tool results
turn_end -> next turn? -> agent_end

agent_startturn_startturn_endagent_end 不是装饰性日志,它们让 UI、会话层和扩展能观察并介入生命周期;取消、错误、并行工具和排队输入也都可以挂在明确的阶段上

读循环时先找什么:

先从 await agent.prompt(userMessage) 找输入,再追踪助手流怎样变成工具调用、工具结果怎样追加回上下文,最后确认什么条件让循环从 turn_end 走向下一轮或 agent_end

源码证据集中在 packages/agent/src/agent.tsAgent.prompt / statepackages/agent/src/agent-loop.tsagentLoop,以及 packages/agent/src/types.ts 的事件和状态类型

最容易犯的误读是把 Agent core 当成完整产品;它只负责一段可观察、可取消、能在工具后续轮的运行,模型选择、资源装配、会话写入和终端体验仍由外围层拥有

双重转换:应用消息不等于模型消息

为什么要分两次转换:

终端执行记录、自定义扩展消息和压缩摘要等内容需要留在应用历史里,却不一定符合 provider 的消息协议;如果把所有存储内容原样发送给模型,模型上下文会失控,扩展消息也可能破坏请求格式

PI 先用 transformContext 做业务级的裁剪、注入和预算治理,再用 convertToLlm 过滤并转换成模型真正能接受的 Message[]

// 转换上下文:做压缩、截断、脱敏、上下文窗口适配等处理
const transformed = await transformContext(messages)
// 将内部消息格式,转换成大模型API可识别的请求消息格式
const llmMessages = await convertToLlm(transformed)

// 仅UI渲染/自定义本地消息:保留在本地会话存储,不会发送给大模型服务商API
// UI-only/custom messages can stay persisted
// without leaking into the provider request

可以把它理解成两道工序:历史像素材库,transformContext 决定本轮取哪些镜头,convertToLlm 再把选中的内容转码成 provider 协议;因此 "保存什么" 和 "模型看到什么" 是两个独立问题

源码证据包括 packages/agent/src/types.tsAgentMessage / transformContextpackages/agent/src/agent-loop.ts 的调用位置,以及 packages/coding-agent/src/core/messages.tsconvertToLlm

这条边界带来三个结果:应用消息可以扩展,上下文治理可以独立演进,模型协议仍保持窄;也正因为如此,压缩摘要、UI 专属消息和产品事件才不会反向污染最小循环

工具执行:并行提速,但结果仍可预测

预检、执行和收口:

多个工具可以并发执行,但权限判断、写操作和结果持久化需要稳定顺序;PI 把工具调用拆成预检、执行和后置钩子,并允许全局策略与单工具 executionMode 同时参与

Tool Calls
  -> Validate
  -> beforeToolCall
  -> Parallel / Sequential
  -> afterToolCall
  -> Ordered Results

核心控制流可以写成:

// 工具调用前置钩子:执行前校验,block阻断工具执行 / allow允许继续
beforeToolCall(call) -> block | allow

// 根据工具配置决定执行模式:sequential串行 / parallel并行(多工具批量调用生效)
executeMode = tool.sequential ? "sequential" : "parallel"

// 异步执行工具,拿到原始执行结果(文件读写、shell、MCP等真实操作)
result = await tool.execute(...)

// 工具调用后置钩子:override覆写返回结果;terminate终止整个Agent任务循环
afterToolCall(call, result) -> override | terminate

预检阶段可以阻断或审计调用;执行阶段按工具的顺序要求选择并行或串行;后置钩子可以覆盖结果或提前终止;即使多个工具完成时间不同,回写给模型和会话的结果仍按模型原始顺序组织

源码证据与边界:

  • packages/agent/src/agent-loop.tsexecuteToolCalls 展示工具数组如何分组,以及 await 如何形成并发屏障
  • packages/agent/src/types.tsbeforeToolCall / afterToolCall 的返回类型明确支持阻断、覆盖和终止
  • packages/coding-agent/src/core/tools:查看 Coding 产品层如何提供具体工具集合

不要把权限、超时和审计逻辑复制到每个工具 handler;工具定义能力,调用管线承载横切策略,这样并发优化不会牺牲可预测性

AgentSession:把最小内核变成可操控产品

运行中输入怎样介入:

用户经常会在 Agent 调工具时补充要求,产品必须回答新消息什么时候生效;PI 把输入分成两种队列:steering 在当前工具批次后介入,follow-up 等 Agent 完成全部工作后再送达

// 向会话提交用户输入消息,由session处理用户交互
session.prompt(message)

// 按下Enter键:将用户消息作为主轮次任务排入队列
Enter     -> queueSteering(message)
// Alt+Enter快捷键:将消息作为后续补充追问排入队列,不重置当前正在运行的任务
Alt+Enter -> queueFollowUp(message)

// 等待Agent执行变为空闲状态:等待工具调用、模型推理全部跑完
await session.agent.waitForIdle()

这不是简单的 "中断或不打断" 开关,而是明确的时序契约;AgentSession 同时协调模型切换、资源加载、压缩、持久化和产品事件,Agent core 则继续专注于运行本身

为什么需要会话层:

如果让终端 UI 直接操纵 Agent core,输入队列、会话文件和资源生命周期会散落在各个入口;Session 把这些产品状态集中起来,CLI、SDK、TUI 和其他表面就能共享同一套控制面

源码证据是 packages/coding-agent/src/core/agent-session.tsAgentSessionpackages/coding-agent/src/core/agent-session-runtime.ts 的运行时装配,以及 packages/coding-agent/src/core/event-bus.ts 的事件桥接

PI 的交互感来自队列语义:用户不必等 Agent 完全停下,也能精确决定新消息是在当前工作后转向,还是在整轮完成后排队

JSONL Tree:一个文件里的分支历史

从线性日志到会话树:

编码任务经常需要回到旧消息尝试另一条路径;复制整份聊天既浪费空间,也会丢失分支关联;PI 的 session JSONL 让每个条目只追加,并用 parentId 指向父节点

{
  "type": "message",
  "id": "b2c3d4e5",
  "parentId": "a1b2c3d4",
  "message": {}
}

切换分支时不需要复制文件,只要改变当前 leaf,再把新消息作为该节点的子节点追加;旧分支仍然留在同一个 JSONL 中,可以回放、导出或做分支摘要

这和 Git commit graph 的思路相似:历史节点不被覆盖,当前可见路径由叶节点决定;存储事实与模型当前视图因此可以同时存在

源码证据:

  • packages/coding-agent/src/core/session-manager.ts:追踪 append / branch / leaf 的更新逻辑
  • packages/coding-agent/docs/session-format.md:查看 JSONL entry schema 与 parentId 关系
  • packages/coding-agent/src/core/compaction/branch-summarization.ts:观察离开分支后如何摘要

PI 没有把会话树藏在不可读的数据库里;一份追加式 JSONL 同时承担恢复、分支和导出,这也是它适合调试与学习的重要原因

ResourceLoaderExtensionAPI:核心之外的产品空间

最小主义到底拒绝了什么:

PI 刻意不把 plan mode、subagent、权限弹窗或后台 Bash 写死在核心里;这不是功能缺失,而是避免用一套默认工作流替所有使用者做决定

DefaultResourceLoader 从全局目录、项目 .pi 和 package 发现提示、技能、主题等资源;ExtensionAPI 则提供统一入口来注册工具、事件、命令、快捷键和 TUI 行为

// 监听工具调用事件,挂载防护钩子(权限校验、拦截高危工具调用)
pi.on("tool_call", guard)
// 向插件/运行时实例注册自定义工具,Agent后续可以调用该工具
pi.registerTool(tool)
// 注册会话内部命令,用户输入 /plan 时触发对应处理函数
pi.registerCommand("plan", handler)
// 注册键盘快捷键,按下 ctrl+t 执行指定动作
pi.registerShortcut("ctrl+t", action)

// /reload 内置命令:原地刷新插件、配置、资源,无需重启整个程序
/reload // refresh resources in place

扩展可以把审批、计划、子任务或自定义界面加到产品层,/reload 允许资源在不重启进程的情况下重新发现和装配;核心因此保持小,工作流却可以持续生长

源码证据包括 packages/coding-agent/src/core/resource-loader.tspackages/coding-agent/src/core/extensionspackages/coding-agent/docs/extensions.md

这里要区分两件事:资源加载器负责 "发现哪些东西",扩展 API 负责 "怎样把能力挂进运行时";前者解决来源和优先级,后者解决注册与生命周期

结语:小内核不是终点,而是选择权的起点

PI Agent 的核心答案是:让 pi-ai 统一模型语言,让 pi-agent-core 只推进可观察的 Agent 生命周期,让 AgentSession 管理交互与产品状态,再用 JSONL tree 保存可恢复、可分支的事实;工作流、审批、计划、工具和界面则通过资源与扩展层按需组装

因此,一个极小的 Agent core 仍然可以长成完整 Coding Agent,并不是因为核心偷偷包含了所有功能,而是因为它把稳定边界留在内层,把变化和选择留给外层;当你要设计自己的 Agent 时,最值得借鉴的不是某个快捷键或某段 CLI 代码,而是先决定哪些状态必须由核心拥有,哪些行为应该交给会话层和扩展生态

内容概要:本文系统研究了基于模型预测控制(MPC)与滚动时域估计(MHE)集成的控制方法,旨在实现动态系统的高精度目标点镇定。通过构建MPC与MHE的协同框架,利用MHE对系统状态进行实时、高效的滚动优化估计,克服传感器测量噪声与初始状态不确定的影响,并将估计结果反馈至MPC控制器,实现对未来控制序列的滚动优化,从而提升系统在复杂干扰和不确定性环境下的镇定性能与鲁棒性。研究涵盖算法原理推导、数学建模、仿真设计与验证全过程,提供了完整的Matlab代码实现,充分展示了该集成策略在状态估计与反馈控制协同优化方面的优越性。; 适合人群:具备自动控制理论基础和Matlab编程能力,从事控制工程、自动化、机器人、航空航天或相关领域研究的研发人员及研究生。; 使用场景及目标:①应用于移动机器人、无人机、自动驾驶等需要高精度状态反馈的自主系统目标点镇定任务;②解决系统状态不可直接测量或受强噪声干扰时的状态估计与反馈控制耦合问题;③为先进预测控制与状态估计算法的联合设计与工程实现提供可复现的技术范例与实践指导; 阅读建议:建议读者结合Matlab代码逐模块分析算法实现细节,重点关注MHE状态估计与MPC控制指令生成之间的数据交互逻辑与时序配合,并尝试在不同非线性系统模型上进行迁移测试,以深入理解MPC-MHE集成机制的核心优势与调参规律。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值