项目目录
项目简介
Pi 是面向开发者的轻量编程助手,提供代码读写、命令执行和树形会话,通过 Extension、Skill 与提示模板适配开发流程,也可通过 SDK 或 RPC 接入其他应用。
如何写进简历
迁跃 · SDK 升级与兼容验证助手 | 场景化示例
项目描述:面向内部 SDK 及接入项目的研发人员,根据目标版本、旧接口调用和兼容性错误辅助升级,保留不同迁移方案的补丁、兼容性说明与验证记录。
技术栈:TypeScript、Node.js、Pi、JSONL
主要工作:
- 树形会话与迁移方案分支:利用带
id/parentId的追加式JSONL会话树记录兼容层与新接口迁移的不同尝试,切换叶节点时重建活动路径,并按需携带分支摘要,让升级方案可回查、可继续。 - 模型上下文投影与压缩:基于最新压缩摘要和近期保留消息构建模型输入,用
context_edit追加省略或替换记录而保留原始历史,使长升级任务能够控制上下文规模,同时追溯此前的接口判断和工具结果。 - 升级阶段 Extension 扩展:通过
Extension注册升级检查命令,利用setActiveTools()切换工具集合,并在context钩子注入目标版本和兼容约束,使检查与修改阶段使用各自的工具和输入。
面试题与回答
Pi Coding Agent 如何设计?
展开收起
核心回答
Pi Coding Agent是基于TypeScript、运行于Node.js的可组合编程助手。它把模型适配、Agent 运行循环和编程会话分开:pi-ai统一模型消息与流式协议,pi-agent-core组织模型请求、工具执行和结果回填,pi-coding-agent在上面提供仓库工具、会话持久化、分支、压缩及扩展能力。一次任务中,模型根据当前上下文提出工具调用,运行时执行工具并回填结果,再请求模型决定下一步。长任务的历史保存为会话树,模型只读取活动分支经过投影和压缩后的内容;团队需要的工具、提示词和运行规则可通过
Extension接入。这使核心循环可以复用,终端、RPC和SDK则承担不同的交互与集成方式。各层怎样分工?
- 模型适配层
pi-ai: 提供模型描述、结构化消息、工具声明、流式事件和最终响应,再由供应商适配器处理实际请求与响应协议。上层不用逐家解析流,但不同模型的能力、错误和调用质量仍可能不同。 - 运行循环
pi-agent-core: 管理当前消息、可用工具与运行状态;调用模型、执行工具、追加结果,并处理用户插入的新指令、后续消息和中断。它不等于会话文件管理器。 - 编程会话层
pi-coding-agent: 组合项目上下文、读写与命令工具、模型设置和资源加载,把消息写入 Session,提供树导航、压缩和 Extension。 - 交互层:
pi-tui提供终端组件,交互模式展示运行事件;RPC通过标准输入输出交换JSONL,SDK则让 Node.js 程序直接创建和控制会话。
模型接口、循环和会话可分别复用,产品入口也共用会话与运行机制。
一次请求怎样完成工具闭环?
以“读取 README 并概括项目”为例,第一次模型请求携带用户消息和
read工具定义。模型返回带调用 ID 的工具调用,循环按工具名找到实现、处理参数并执行,生成带相同 ID 的toolResult。下一次请求加入这个结果,模型才能根据真正读到的文件回答。这段闭环有两个关键边界:工具调用表示模型想做什么,工具结果才表示执行观察到了什么;模型停止生成也不等于开发任务已经验收。 文件修改是否正确、测试是否覆盖需求,仍要根据真实文件和命令结果判断。
运行循环把工具结果追加回上下文,再判断是否需要下一次请求。模型错误或中断会结束当前循环;输出长度达到限制时,可能不完整的工具调用参数会被转成失败结果,避免直接执行半截调用。
流式文本、工具开始和结束等事件用于驱动界面和宿主程序;Session 中的消息、调用和结果用于后续重建。不能只保存屏幕上的文本增量,却丢掉工具调用 ID、错误和完整结果。
为什么会话历史和模型输入要分开?
Pi 的持久 Session 是追加式
JSONL文件。消息、模型切换、摘要等条目都有id和parentId,形成会话树;当前leaf表示选中的位置。返回旧节点后继续追加,就能保留方案 A,再尝试方案 B,而不用覆盖原来的记录。模型输入从当前叶子沿父指针回到根,只采用这条活动路径。切换分支时,可以选择将离开路径到共同祖先之间的内容生成分支摘要,并挂到目标位置;没有选择摘要时,其他分支的发现不会自动全部带过来。会话导航改变的是对话路径,不会把工作目录中的文件一并回滚。
上下文投影进一步处理压缩和上下文编辑:原始记录保留,模型看到的是当前路径中实际有效的消息。
context_edit可以追加一条编辑记录,替换或省略旧消息对模型输入的贡献,而不改写原始消息。这便于保留历史依据,同时减少过期或无用内容对后续判断的影响。长任务怎样压缩?
Pi 结合最近一次模型用量和后续新增消息估算上下文,当接近模型窗口预留边界时触发压缩,也支持手动压缩。它将摘要及保留起点写成新的
compaction条目,后续请求采用摘要、近期保留消息和新增消息,旧记录仍留在会话文件中。切分时要维护工具调用与结果的配对关系,不能为了达到一个 Token 数就从工具结果中间截断。较大的交互可能需要单独总结前半段,同时保留后半段继续执行。压缩准备基于模型可见的投影选择这些范围;上下文编辑或压缩使旧用量失效后,也需要重新估算。
压缩摘要和分支摘要解决的问题不同:前者减少当前路径的输入,后者把离开分支的有用发现带到新位置。两者都有信息损失风险;精确错误、关键约束和文件状态应能回到原记录或实际环境核对。
Extension 能怎样接入运行过程?
Extension是运行在 Pi 进程内的 TypeScript 模块,可以注册工具、命令及事件处理器。例如,在运行开始前调整提示词,在工具调用前返回阻止结果,在工具返回后整理内容,或定制分支摘要和压缩行为。请求前的上下文钩子有两层:普通
context处理对话消息,Pi 会恢复系统提示与工具声明;context_with_system处理包含系统消息的完整输入,扩展要自己维护提示词和工具声明。这些请求时的变换不会自动改写持久历史。扩展状态同样要区分用途:
custom条目用于保存程序状态,默认不进入模型上下文;custom_message才用于注入模型需要知道的内容。只需要提供操作知识时可以使用Skill,不必写成扩展代码;需要运行时执行和拦截时再使用 Extension。这种设计把工作流定制留给扩展,但扩展与工具仍受所在进程的权限影响。钩子拦截、项目信任和操作系统隔离各有职责,不能把“支持扩展”当成默认沙箱或完整权限保证。
- 模型适配层
压缩多轮对话后,如何拼接用户消息、模型消息与摘要?
展开收起
Pi 1.4.0将会话保存为带id、parentId的条目树。模型输入由当前分支投影生成:当前路径上的最新压缩摘要+保留的近期条目+压缩后的新增条目,并应用该路径上的上下文编辑。先选路径,再应用压缩边界
buildSessionPath()从当前叶子沿parentId回到根,再反转为执行顺序;它不会遍历所有兄弟分支。buildContextEntries()找到这条路径上最新的compaction,把该条目放在最前,然后接上:- 从
firstKeptEntryId开始、位于压缩条目之前的保留内容;这段中的旧system消息会被排除。 - 压缩条目之后、本分支新增的内容。
压缩条目保存了
systemMessage时,投影先恢复这份提示与工具状态快照,再放摘要。即使较旧的压缩条目仍落在保留范围里,也不会再次注入旧摘要,因此不会出现多份历史摘要层层堆叠。保留原角色,摘要明确标记为历史
普通消息保留
user、assistant、toolResult等角色。内部的compactionSummary经过convertToLlm()转成user角色的文本消息,并带有明确的“此前历史已压缩为以下摘要”前缀与摘要标签。这里使用user作为兼容接口的载体,不表示摘要是用户刚发出的新要求。默认压缩准备会避开工具结果作为切分起点,保留它前面的模型工具调用;若一个用户请求本身过长而需要从中间切开,则另做该请求前半段的摘要,接上后半段消息,而不是硬保留整个超长请求。
上下文编辑不改写原始会话树
context_edit通过目标条目 ID 替换其上下文content,或用replacement: null使它不进入模型输入;同一目标采用当前有效范围中最后一次编辑。原始条目、角色与其他元数据继续留在记录里,编辑只影响投影。因此,界面和历史工具能看到的完整过程,不等于模型这一轮收到的内容。扩展省略工具调用或结果时仍需维护对应关系;投影机制本身不意味着任何任意删改都符合提供商消息要求。切换会话分支也只改变对话路径,磁盘文件或外部动作需要另行核对,不能视为已经回滚。
- 从
展示 Pi 的自定义 Extension 配置。
展开收起
核心回答
可以用一个
/inspect扩展展示完整配置:默认导出函数接收ExtensionAPI,注册命令切换检查工具集,再注册context事件处理器,为当轮模型输入补一条阶段说明。下面示例采用@earendil-works/pi-coding-agent 1.4.0的接口,/inspect off会恢复进入前的工具集合。扩展入口与加载方式。最小扩展文件
将文件保存为项目中的
.pi/extensions/inspect.ts:import type { UserMessage } from "@earendil-works/pi-ai"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; export default function inspectExtension(pi: ExtensionAPI) { let previousTools: string[] | undefined; const upgradeTask = { targetVersion: "3.0(示例)", compatibility: "保留旧接口入口;列出必须修改的调用点" }; pi.registerCommand("inspect", { description: "切换检查阶段;/inspect off 恢复原工具集", handler: async (args, ctx) => { if (args.trim() === "off") { if (previousTools) pi.setActiveTools(previousTools); previousTools = undefined; ctx.ui.notify("已退出检查阶段", "info"); return; } previousTools ??= pi.getActiveTools(); const available = new Set(pi.getAllTools().map((tool) => tool.name)); pi.setActiveTools(["read", "grep", "find", "ls"].filter((name) => available.has(name))); ctx.ui.notify(`检查工具:${pi.getActiveTools().join(", ")}`, "info"); } }); pi.on("context", (event) => { if (!previousTools) return; const notice: UserMessage = { role: "user", content: `[inspect 阶段说明] 目标 SDK 版本:${upgradeTask.targetVersion};兼容约束:${upgradeTask.compatibility}。当前只检查代码并提出建议,不修改文件或执行命令。`, timestamp: Date.now() }; const messages = [...event.messages]; const at = messages.at(-1)?.role === "user" ? messages.length - 1 : messages.length; messages.splice(at, 0, notice); return { messages }; }); }这里用了三个具体入口:
registerCommand()注册/inspect,命令回调保存原工具集合,再选取已注册的read、grep、find、ls;重复开启不会覆盖最初保存的集合。setActiveTools()改变向模型声明的活动工具;getAllTools()用于排除当前环境中不存在的工具名。它切换的是工具集合,不是重新注册工具。on("context", ...)在模型请求前返回新的消息列表,把明确标识的阶段说明放在最后一条用户问题之前。原历史数组没有被改写,当前问题也仍在末尾。API 与事件类型、context 事件执行。
context处理的是不含系统消息的对话列表,运行器会恢复系统提示和工具状态;示例没有调用持久化消息 API,只为当轮输入补充说明。previousTools是扩展实例内状态,若要支持重新加载和会话分支切换,需要另行保存并恢复这份状态。upgradeTask用示例值展示目标版本和兼容约束怎样进入当轮消息;业务接入时应从当前升级任务装载这些字段,切换任务或迁移分支后重新绑定,避免沿用上个任务的约束。阶段消息只补充模型输入,代码是否兼容仍由实际调用点和测试结果判断。怎样加载和触发
项目扩展在授予项目信任后加载;开发时也可以显式指定文件:
pi --extension ./.pi/extensions/inspect.ts不需要为这个本地
TypeScript文件单独打包。修改文件后,可在交互会话中执行/reload重新加载。项目扩展目录。验证时可按三个观察点检查:
- 输入
/inspect,界面应显示本环境可用的检查工具名;命令本身不需要发起模型请求。 - 触发一次
context事件时,返回列表增加一条阶段说明,原问题保持末尾,传入的历史列表保持原样。可以先用模拟事件检查,不必调用模型。 - 再次输入
/inspect后执行/inspect off,应恢复第一次进入前的工具集合;退出后context钩子不再增加说明。
单文件转译只能检查可转译性,模拟 API 能检查命令与事件逻辑;完整依赖类型检查、真实加载和模型行为需要分别验证。
import type在转译后会被移除,不会成为运行时模块导入。工具集合与执行权限的区别
这个示例定制了模型看到的工具和当前任务阶段,并不提供操作系统沙箱。该版本的
codemode、deferred工具即使不在活动集合中,仍可能由 codemode 脚本调用;需要约束真实动作时,还应按工具调用入口和执行环境设置相应策略。工具暴露与活动集合。参考资料
- 国内:JavaGuide:Harness Engineering:参考工具和钩子由运行系统提供的分工,将阶段说明与真正改变工具集合的代码分开。
- 国内:zero2Agent:工具管理:参考按任务阶段选择本轮工具定义,解释注册工具与激活工具的区别。
- 国内:Spring AI Alibaba:Hooks 和 Interceptors:参考在模型调用前通过钩子调整上下文的组织;示例接口采用
Pi自身的事件 API。 - 国内:WeThinkIn:Agent Harness 工程:参考任务动作、运行环境和预期结果分别定义,为工具集合、消息变化与退出恢复设置独立观察点。
- 国外:TypeScript:Modules Reference:依据类型导入被擦除的规则,说明示例的接口类型与运行时模块加载不同。
- 国外:TypeScript:isolatedModules:依据单文件转译的限制,区分转译检查、完整类型检查与实际运行验证。