TypeScript研发与代码执行

项目目录

项目简介

Pi 是面向开发者的轻量编程助手,提供代码读写、命令执行和树形会话,通过 Extension、Skill 与提示模板适配开发流程,也可通过 SDK 或 RPC 接入其他应用。

如何写进简历

迁跃 · SDK 升级与兼容验证助手 | 场景化示例

项目描述:面向内部 SDK 及接入项目的研发人员,根据目标版本、旧接口调用和兼容性错误辅助升级,保留不同迁移方案的补丁、兼容性说明与验证记录。

技术栈:TypeScript、Node.js、Pi、JSONL

主要工作:

  • 树形会话与迁移方案分支:利用带 id / parentId 的追加式 JSONL 会话树记录兼容层与新接口迁移的不同尝试,切换叶节点时重建活动路径,并按需携带分支摘要,让升级方案可回查、可继续。
  • 模型上下文投影与压缩:基于最新压缩摘要和近期保留消息构建模型输入,用 context_edit 追加省略或替换记录而保留原始历史,使长升级任务能够控制上下文规模,同时追溯此前的接口判断和工具结果。
  • 升级阶段 Extension 扩展:通过 Extension 注册升级检查命令,利用 setActiveTools() 切换工具集合,并在 context 钩子注入目标版本和兼容约束,使检查与修改阶段使用各自的工具和输入。

面试题与回答

  1. 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。

    这种设计把工作流定制留给扩展,但扩展与工具仍受所在进程的权限影响。钩子拦截、项目信任和操作系统隔离各有职责,不能把“支持扩展”当成默认沙箱或完整权限保证。

  2. 压缩多轮对话后,如何拼接用户消息、模型消息与摘要?

    展开收起

    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 使它不进入模型输入;同一目标采用当前有效范围中最后一次编辑。原始条目、角色与其他元数据继续留在记录里,编辑只影响投影。

    因此,界面和历史工具能看到的完整过程,不等于模型这一轮收到的内容。扩展省略工具调用或结果时仍需维护对应关系;投影机制本身不意味着任何任意删改都符合提供商消息要求。切换会话分支也只改变对话路径,磁盘文件或外部动作需要另行核对,不能视为已经回滚。

  3. 展示 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 重新加载。项目扩展目录。

    验证时可按三个观察点检查:

    1. 输入 /inspect,界面应显示本环境可用的检查工具名;命令本身不需要发起模型请求。
    2. 触发一次 context 事件时,返回列表增加一条阶段说明,原问题保持末尾,传入的历史列表保持原样。可以先用模拟事件检查,不必调用模型。
    3. 再次输入 /inspect 后执行 /inspect off,应恢复第一次进入前的工具集合;退出后 context 钩子不再增加说明。

    单文件转译只能检查可转译性,模拟 API 能检查命令与事件逻辑;完整依赖类型检查、真实加载和模型行为需要分别验证。import type 在转译后会被移除,不会成为运行时模块导入。

    工具集合与执行权限的区别

    这个示例定制了模型看到的工具和当前任务阶段,并不提供操作系统沙箱。该版本的 codemode、deferred 工具即使不在活动集合中,仍可能由 codemode 脚本调用;需要约束真实动作时,还应按工具调用入口和执行环境设置相应策略。工具暴露与活动集合。

    参考资料