欢迎光临
我们一直在努力

从工具循环到上下文压缩:读懂 Pi 编程 Agent 的内部架构

我是安徽最忧郁程序员无隅

在这里插入图片描述

很多人第一次接触编程 Agent,容易把它理解成“给大模型加几个读文件、执行命令的工具”。但真正决定 Agent 是否可扩展、可观察、可长期运行的,不是工具数量,而是模型、工具、上下文、会话和 UI 之间有没有清晰边界。

Pi 是一个很适合拿来拆解的案例:它把核心运行时做得很小,再把终端交互、会话、扩展、Skills 等能力放到上层。这篇文章不把 Pi 当作一个命令行产品来介绍,而是借它回答一个更通用的问题:一个生产级编程 Agent 到底由哪些机制组成?

文中架构结论基于 Pi 官方开源仓库与文档整理;具体默认值、配置项和目录可能随版本变化,落地时请以官方文档为准。

一、先抓住本质:Pi 不是一个“大 Prompt”,而是一套可分层的运行时

从官方仓库的包划分看,Pi 至少可以被理解成四层能力:

层级代表包负责什么
Provider 适配层 pi-ai 统一不同模型提供商的流式调用接口
Agent 运行时 pi-agent-core 维护状态、协调模型调用与工具调用
终端渲染层 pi-tui 负责终端组件与差量渲染
编程 Agent 应用层 pi-coding-agent 负责会话、提示词、工具、扩展、Skills 和交互模式

最重要的边界是:TUI 不等于 Agent。

终端 UI 只是事件的一个消费者。只要核心运行时把“模型开始流式输出”“工具开始执行”“工具产生进度”“当前 Turn 结束”等过程暴露为事件,同一套 Agent Core 就可以被终端、脚本、JSON 客户端,甚至其他应用复用。

这也是我们自己写 Agent 时值得保留的设计原则:

  • 把 Provider 的差异压在最底层,业务层面对统一的消息和流。
  • 把“推理 + 调用工具”的反馈循环放在核心层。
  • 把会话、界面、项目约定、扩展放到外围,而不是塞进循环。
  • 把每次状态变化做成可观察事件,避免核心逻辑直接操作 UI。

Pi 官方仓库对这些包的职责有明确说明;SDK 文档也展示了 Agent 的状态中包含消息、模型、工具、系统提示词和流式中的消息等信息。Pi Monorepo Pi SDK 文档

二、核心链路:模型不是直接“做事”,而是在反馈循环中决定下一步

工具型 Agent 的核心,不是一次 LLM 调用,而是一个不断接收反馈的循环。它的抽象可以写成下面这样:

while (true) {
const assistantMessage = await streamModel(context)
const toolCalls = findToolCalls(assistantMessage)

if (toolCalls.length === 0) {
return assistantMessage
}

const results = await executeTools(toolCalls)
context.messages.push(results)
}

在这里插入图片描述

这段伪代码只有几行,但里面有四个不能混淆的角色:

  • Context Builder:决定这一次模型能看到什么,例如系统指令、当前问题、历史消息和工具 Schema。
  • Model Stream:接收模型流式返回的 Assistant 消息,并从中解析是否存在工具调用。
  • Tool Scheduler:校验参数、执行 Hook、调度工具,并把工具输出标准化。
  • Result Writer:把工具结果以模型能理解的形式追加进上下文,推动下一轮推理。
  • 这里最容易忽略的是“工具结果”不是给用户看的日志,而是下一次模型推理的输入。比如模型先调用 read 读取文件,拿到内容后才可能决定调用 edit;执行测试获得报错后,才可能形成新的修复假设。工具把外部世界变成模型可消费的证据。

    生产实现还会补充几个工程约束:

    • 相互独立的工具调用可以并行,以降低总等待时间;
    • 即使并行完成,写回消息时仍按模型原始调用顺序排列,避免上下文语义混乱;
    • 模型流、工具执行与会话操作共享取消信号,用户中断时能一致停止;
    • 事件流只描述“发生了什么”,不决定“终端怎么画出来”。

    因此,真正可复用的不是这段 while,而是它的 Contract:模型产出结构化意图,运行时执行意图,再把环境反馈交还给模型。

    三、最关键的状态设计:Session 保存历史,Context 服务下一步决策

    长任务一定会遇到一个矛盾:我们既希望保留完整历史,方便回溯和分支探索;又不能把全部记录无差别塞给模型,否则上下文窗口很快耗尽。

    Pi 的解决思路是把两个概念刻意分开:

    • Session 回答“此前发生过什么”。
    • Model Context 回答“模型下一步需要知道什么”。

    在这里插入图片描述

    会话为什么适合做成树,而不是线性聊天记录

    一次编程任务经常会出现这样的过程:先采用方案 A,后来发现不合适,再回到早期决策点尝试方案 B。如果会话只有线性消息列表,回退往往意味着丢弃后续内容。

    用 id 和 parentId 把记录组织为树后,可以从旧节点继续新增子分支,原分支仍然保留:

    用户:实现登录
    └── Assistant:方案 A
    ├── 用户:继续方案 A
    └── 用户:改用方案 B

    这份树是“完整事实记录”。它适合追加、检查和回放,也能让用户比较不同路径。Pi 的 SDK 文档允许直接替换 Agent 的消息数组,用于恢复或分支类场景;这也说明对话状态是核心运行时里一个独立、可管理的对象。Pi SDK 文档

    压缩为什么不是“删聊天记录”

    压缩解决的是模型上下文有限,而不是历史记录无用。

    当活跃路径过长时,运行时可以把较早消息总结成结构化 Checkpoint,并保留最近、最具体的消息尾部。一个合格的 Checkpoint 至少应包含:

    • 当前目标与约束;
    • 已完成、进行中、阻塞中的工作;
    • 关键技术决策;
    • 重要文件、函数名和报错;
    • 下一步行动。

    之后模型看到的是“Checkpoint + 最近上下文”,而不是从第一条消息开始的全部记录。压缩改变的是模型的视野,不是已经发生过的历史。

    这条经验很适合迁移到自己的 Agent 项目中:持久化层优先保证完整性;上下文层优先保证相关性。把两者混成同一个数组,后面会很难同时做好回放、分支和 Token 控制。

    四、扩展、Skills 与终端 UI:把变化放在稳定边界之外

    如果把所有能力都写进 Agent Core,核心会很快变成一个难以维护的“全能框架”。Pi 更偏向于提供稳定边界,让变化从边界进入。

    Extension:改变 Agent “能做什么”

    扩展更适合放可执行能力和运行时 Hook,例如:

    • 注册新的工具、命令和快捷键;
    • 在工具执行前做审批、审计或路径保护;
    • 注入模型 Provider;
    • 增加消息 Renderer 或终端组件;
    • 改写压缩、会话或工作流策略。

    以自定义工具为例,核心循环并不需要知道“发布预览环境”是什么;它只需要面对统一的工具 Contract:

    pi.registerTool({
    name: "deploy_preview",
    description: "部署当前分支的预览环境",
    parameters: schema,
    async execute(toolCallId, params, signal, onUpdate) {
    // 执行任务,并通过 onUpdate 报告进度
    return { content: [{ type: "text", text: "Preview ready" }] }
    },
    })

    安全策略也应该放在这个边界:例如 Shell 命令执行前审批、敏感路径写入前拦截、工具输出进入模型前脱敏。这样核心循环保持通用,环境策略则可以按项目替换。扩展相关 API 和示例可从官方仓库的 coding-agent 文档与示例目录继续追踪。Pi Monorepo

    Skill:改变 Agent “何时、如何做”

    Skill 更像一份可复用的工作说明书。它通常以 SKILL.md 为入口,告诉 Agent:

    • 这个任务适用于什么场景;
    • 需要先读哪些文件;
    • 应执行哪些命令;
    • 哪些操作需要批准;
    • 最终如何验证和交付。

    它和 Extension 的分工可以一句话记住:

    机制本质典型用途
    Extension 新增能力或拦截生命周期 新工具、Hook、UI、Provider 集成
    Skill 复用操作方法 发布流程、CI 排错、代码审查规范

    Skill 的价值还在于渐进加载:启动时只向模型暴露名称、描述和位置这类“目录信息”;真正匹配任务后,再读取完整说明。这样技能库变大时,不会把所有细节永久占满系统提示词。官方 SDK 示例展示了 Skill 的发现、筛选和自定义注入方式。Skills SDK 示例

    TUI:它展示运行时,但不拥有运行时

    终端 UI 的难点是模型文本、工具进度和用户输入会同时变化。Pi 的 pi-tui 使用差量渲染:比较新旧帧后,尽量只追加或刷新变化的尾部,从而减少整屏重绘和闪烁。

    这个设计再次强调了边界:UI 订阅事件并呈现状态,而不是直接接管工具循环。于是交互式终端、一次性命令、JSON 事件输出或 SDK 嵌入,都能共享同一个 Agent Harness。

    五、从 Pi 可以复用的 Agent 构建顺序

    如果从零实现一个编程 Agent,建议按下面的顺序推进,而不是一开始就堆 Prompt、子 Agent 和复杂 UI:

  • 先统一模型流式接口,明确消息与工具调用 Schema。
  • 实现最小而正确的“模型 → 工具 → 结果 → 模型”循环。
  • 给消息、Turn、工具执行加事件,先让过程可观察。
  • 分离 Session 与 Model Context,支持持久化和上下文投影。
  • 再加入工具权限、扩展 Hook、压缩和 Skills。
  • 最后构建终端或 Web UI,让 UI 成为 Harness 的 Adapter。
  • Pi 最值得学习的并不是某一个命令或某一个默认配置,而是这种架构取舍:每层都足够小,边界足够清楚,复杂能力通过扩展和组合获得。

    对于正在做 Python Agent 应用的同学,这套思路可以直接映射到自己的项目:用状态对象管理对话,用工具协议隔离外部操作,用事件或回调隔离界面,用摘要控制长上下文,用 Skill 把重复流程沉淀成可执行规范。这样做出来的 Agent 才不只是“能调用模型”,而是一个能持续工作、可维护、可扩展的应用运行时。

    参考资料

    • Pi Agent Harness 官方仓库
    • Pi Coding Agent SDK 文档
    • Pi Settings 文档
    • Pi Skills SDK 示例
    赞(0)
    未经允许不得转载:171主机测评 » 从工具循环到上下文压缩:读懂 Pi 编程 Agent 的内部架构
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址