Pi 的任务规划与推理模式

本文发布于公众号:stringwu的工程笔记 Pi 的任务规划与推理模式

系列第三篇,源码:github.com/earendil-works/pi

前面的文章:

1 总览

在真实工程场景中,Agent 面临的环境是高度动态且信息不对称的。试图在任务开始的第一秒就生成“完整且绝对正确”的计划,往往会导致系统僵化。

Pi 把任务规划拆成三层各司其职的稳定底座:

  • agent-loop 负责逐轮推进任务;(ReAct循环)

  • thinkingLevel 统一推理强度档位,

  • extension 负责实现具体规划工作流。

Planner 不写死在内核里,而是作为可插拔的脚手架叠在循环之上

这种设计带来了三大关键优势:

  • 自适应演进:通过“观察-行动-看结果-再判断”的逐轮推进(Turn),天然适应不确定性任务。
  • 高可扩展性:计划格式、审批机制与进度看板均交由 Extension 自定义,Core 无需关注具体工作流。
  • 运行时解耦:模型与推理强度可在 Turn 边界动态切换,适配复杂任务的不同阶段。

2 问题层:为什么 Agent 不能靠"一次性计划"

2.1 真实任务的信息是不完整的,计划必须在执行中生长

很多任务从外面看很简单——"分析这个模块""修复这个 bug"。但 agent 真正开始时,并不知道:

  • 哪个测试失败、堆栈是什么;
  • 相关代码在哪里;
  • 是业务逻辑错了,还是测试预期错了;
  • 改完会不会影响别的模块。

所以它不可能在第一秒就写出绝对正确的完整计划。合理的推进方式是:

先看错误 → 再找代码 → 再判断原因 → 再小范围修改 → 再验证

2.2 推论:内置强 Planner 会把 workflow 写死

如果 core 内置一个 Planner:

Planner 决定计划 → Executor 按计划走 → 中途发现新情况还要改计划状态机 → 换模型还要迁移 Planner 内部状态

demo 里看起来规整,真实工程里反而僵硬。因为不同用户对"计划"的要求完全不同:

用户想要的"计划"形态
回答里列几步即可
写进 TODO.md
必须先审批、不能直接改文件
变成任务看板
多 subagent 分工:探索、规划、实现、复查

core 内置一种 Planner,其他所有模式都变成绕路。

2.3 core 应该只管"循环是否可靠",而不是"计划长什么样"

Pi 的判断是:只要 模型输出 → 工具调用 → 工具结果 → 下一轮模型输出 这个循环稳定,上层就能用 extension 做出任意 planning 风格。


3 机制层:core 靠什么把复杂任务跑起来

core 不替你规划路线,它保证每一轮都能做到"收到任务 → 派人执行 → 收集结果 → 决定下一轮"。调度台不管施工流程,只管调度永远可靠。

core 的支撑点有四个:

  • agent-loop(循环)
  • turn/run(任务组织)
  • 上下文转换(模型无关)
  • thinkingLevel + adapter(能力抽象与翻译)。

它们共同把"观察、行动、修正"做扎实。

3.1 agent-loop:循环本身就是 ReAct

Pi 的核心推进在 packages/agent/src/agent-loop.ts。每一轮只做六件事:

prompt / continue
  -> streamAssistantResponse()   // 1. 准备上下文  2. 调模型
  -> assistant message           // 3. 收集输出
  -> extract tool calls          // 4. 执行工具
  -> append tool results         // 5. 结果写回上下文
  -> prepareNextTurn() / shouldStopAfterTurn()  // 6. 判断是否下一轮

ReAct 听起来像论文术语,大白话就是"想一下 → 做一下 → 看结果 → 再想一下"。在 Pi 里它有直接的对应物:

ReAct 概念 Pi 中的对应物
Reasoning assistant 文本或 thinking block
Action toolCall
Observation toolResult
Next reasoning 下一轮模型请求

所以 Pi 不需要一个叫 ReActPlanner 的类——loop 本身就把这个模式跑起来了。

3.2 turn 与 run:把长任务拆成多轮

模型不用在第一轮假装什么都知道,它可以在每次工具返回后基于新证据重新判断。

概念 含义
turn 一次模型响应,以及它触发的一批工具调用
run 用户发起的一次整体运行,可含多个 turn

用户说"分析这个 bug 并给出修复方案",一次 run 可能是:

turn 1  读错误日志      → 拿到日志
turn 2  搜索相关代码    → 拿到文件位置
turn 3  阅读实现        → 输出修复方案
turn 4(继续修复时)调用 edit 改代码
turn 5  运行检查命令    → 输出总结

3.3 上下文转换:让模型看到该看的,且与厂商无关

Pi 保存中立的对话历史,而不是某个厂商专用的请求 JSONL——这是"运行中切换模型"的前提。

请求前经过两步:

AgentMessage[](Pi 自己的工作记录本)
  -> transformContext()   // 像整理会议材料:保留、裁剪、补充
  -> convertToLlm()       // 像翻译员:转成当前 provider 的格式
  -> provider payload

session 里保存的是"用户说了什么、助手回答了什么、工具调用了什么",而不是 OpenAI / Anthropic / Google 各自的原始 payload。切换模型时,只需在下一次请求前重新走一遍 convertToLlm()——这就是"无缝切换"的关键。

3.4 thinkingLevel:先抽象稳定语义,再适配外部差异

上层只表达意图,底层负责翻译,能力差异由降级机制诚实处理。

厂商对"推理"的叫法和参数各不相同:reasoning effort、thinking、budget tokens、自定义字段……Pi 定义统一档位:

type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";

可以把它想成汽车档位:off 是空挡,low/medium 是日常行驶,high/xhigh/max 是爬坡。驾驶员只需换挡,不必知道变速箱里齿轮怎么咬合。

但统一抽象不等于抹平现实:packages/ai/src/models.ts 会根据模型能力判断可用档位,模型不支持就降级。这比"所有模型都假装支持 high reasoning"更诚实,也更适合生产系统。

3.5 provider adapter:把统一档位翻译成厂商语言

Provider / API 大致映射
OpenAI Responses reasoningEffort
Anthropic Messages 新模型 effort,旧模型 thinking token budget
Google Generative AI thinking level 或 budget
OpenAI-compatible 按兼容配置转 reasoning/thinking 字段

就像寄快递只说"加急"或"普通",至于顺丰、FedEx 内部怎么调度,不该让寄件人管。


4 扩展层:plan mode 如何叠在循环之上

planning 不是一条单独管线,而是叠在基础循环上的一组可组合扩展点。

Pi 的 plan mode(packages/coding-agent/examples/extensions/plan-mode/)不是内核功能,而是一个 extension,像"施工前审批":

计划阶段:只读工具 + 注入计划提示 + 模型输出 Plan
  -> extension 解析计划 -> 用户确认
执行阶段:恢复写工具 -> 注入计划步骤 -> 模型执行 -> [DONE:n] 标记进度

它用到的扩展点,每一个都是通用能力:

扩展点 在 plan mode 里的作用
command 注册 /plan、/todos
active tools 计划阶段切到只读工具集
tool hook 拦截不安全 bash
before agent start 注入隐藏提示,要求输出计划
agent end 从输出中解析计划
turn end 根据 [DONE:n] 更新进度
custom state 持久化计划与进度
widget 界面展示计划状态

这意味着:今天可以用编号列表做计划,明天可以换成 JSON、YAML、看板、审批流、多 agent 分工、外部项目管理系统——core 不需要知道这些形态。


5 贯穿机制:为什么所有切换都发生在 turn 边界

模型、推理档位、工具集、system prompt 的变化都放在 turn 边界刷新,而不是在 stream 中途硬改——这是整套设计能稳定运行的地基。

流程(以 thinking level 为例,模型切换同构):

用户选择新档位
  -> AgentSession.setThinkingLevel()
  -> 按当前模型能力 clamp
  -> 写入 agent.state.thinkingLevel
  -> 记录 thinking_level_change,通知 UI 和 extension
  -> 下一轮请求使用新配置

为什么"下一轮生效"而不是立刻打断当前 stream?因为流式请求发出后,provider 已按旧参数开始生成,中途强改会让状态无法解释。如同换挡发生在下一次动力输出,而不是齿轮咬住时硬掰。

这个不起眼的边界机制支撑了所有高级能力:中途切模型、调档位、extension 临时禁用工具、plan mode 从只读切到执行、用户追加 follow-up、插入 steering message。


6 Trade-off:放弃了什么,换来了什么

Pi 放弃了内置强 Planner"看起来完整"的确定性,换来了组合灵活性。

取舍 收益 代价
planning 外置 core 稳定,支持多种计划形态 core 不保证计划被严格执行
turn loop 推进 工具结果实时修正下一步 长任务需多轮上下文管理
thinkingLevel 抽象 UI 和上层逻辑统一 不同模型真实效果不一致
下一轮刷新配置 模型/工具切换稳定 当前 stream 不中途变更
provider-neutral transcript 切模型自然 每次请求前需做上下文转换

这些代价符合 coding agent 的真实处境:agent 不是批处理脚本,它在不断变化的信息环境里工作。稳定的循环边界,比一个看起来完美但难以调整的 Planner 更重要。


7 复杂性被放在了正确的位置

回看全文,这套设计最值得借鉴的不是某个类写得多巧,而是分层:

复杂性 所在层
工具循环 agent-loop
模型能力差异 Models + provider adapter
推理强度选择 thinkingLevel
计划工作流 extension
会话状态 AgentSession + transcript
上下文裁剪/转换 transformContext() / convertToLlm()

给架构设计者的四条启发:

  1. 先抽象稳定语义,再适配外部差异——thinkingLevel 不暴露厂商参数,先定义自己的档位。
  2. 策略做成 extension,而不是塞进 core——plan mode 可以很强,但不该绑死所有任务。
  3. 可变配置放到清晰边界更新——切换都发生在 turn 边界,系统易理解、易恢复。
  4. 让工具结果参与下一轮判断——代码环境是动态的,agent 必须根据现场信息更新判断。

8 总结

agent-loop    负责推进任务
thinkingLevel 负责统一推理档位
provider adapter 负责翻译模型差异
extension     负责定义计划工作流

如果说传统 Planner 像开工前就定死的施工图,Pi 更像一个调度系统:每一步都看现场反馈,再决定下一步怎么走。

coding agent 更现实的组织方式,不是幻想一开始就知道所有答案,而是把"观察、行动、修正"这件事做扎实。


9附:核心组件索引

组件 位置 职责
Agent packages/agent/src/agent.ts 保存模型、thinking level、工具、消息和运行状态
agent-loop packages/agent/src/agent-loop.ts 执行模型响应、工具调用、写回结果、进入下一轮
AgentLoopConfig / ThinkingLevel packages/agent/src/types.ts loop 配置与 hook;统一推理档位
Models packages/ai/src/models.ts 判断模型支持的 thinking level 并降级
provider adapter packages/ai/src/api/ 统一档位翻译成厂商参数
AgentSession packages/coding-agent/src/core/agent-session.ts 会话、模型/档位切换、extension 事件
plan-mode extension packages/coding-agent/examples/extensions/plan-mode/ "先规划、再执行"的受控流程示例
posted @ 2026-10-10 09:37  woodWu  阅读(21)  评论(0)    收藏  举报