第六章:Pi 如何把循环留给可组合的宿主
6.1 底层库和完整应用分开看
Pi 仓库包含模型访问、代理循环和 coding-agent 应用等层次。底层 Agent 保存活动运行、消息和队列,agent-loop.ts 推进循环;应用层再加入会话文件、资源发现、交互界面和扩展。把 coding-agent 的所有行为都归给底层 Agent,会错过真正的策略边界。Pi · agent-loop.ts L161–175
Agent.prompt() 在已有活动运行时拒绝启动另一次 prompt。它提示使用 steer 或 followUp 等机制。这种显式拒绝让状态拥有单一活动推进者,而不是让调用方随意重入循环。Pi · agent.ts L371–385
6.2 模型能力从函数注入
循环接收 StreamFn,其职责是根据模型、上下文和选项提供事件流。宿主可以注入真实提供方,也可以注入测试实现。循环因此不必知道 API 密钥从哪里保存,或者某个供应商连接如何建立。Pi · types.ts L31–45
这种解耦的价值不仅是支持更多模型,更在于测试。你可以稳定地产生工具调用、截断响应或取消事件,再检查宿主是否正确处理,而不用依赖模型随机地复现某种异常。本书实验正采用这一思路,但定义了自己的简化协议,没有声称兼容 Pi 的完整接口。
6.3 应用历史到模型历史有两道转换
在生成请求前,transformContext 可以修改应用消息视图,然后 convertToLlm 把它转成模型认识的消息。两者解决不同问题:前者选择或重组信息,后者处理表示与协议兼容。Pi · agent-loop.ts L388–402
假设应用保存了一张“用户打开了文件树”的通知卡片,它可以留在界面历史里,却不必转成模型消息。相反,工具读取到的源码虽然可以只在界面展示预览,其必要内容仍应进入模型上下文。转换边界让这两种需求能够明确表达。
6.4 校验、钩子、执行和结果排序
循环找到工具,准备并校验参数,再进入工具钩子与执行。钩子是宿主可以接入策略的位置,不是底层库自动具备的全套权限设施。绕开这条执行路径直接调用函数时,相应策略不会凭空生效。
并行工具可以按真实完成顺序发出完成事件,但整理给模型的结果保持原调用顺序。源码通过保存对应槽位并等待结果,随后依次产生模型消息。这个区别让 UI 及时更新,同时保持模型记录稳定。Pi · agent-loop.ts L644–658
还有一个很有教学意义的分支:当助手响应以长度限制结束时,消息中的工具调用会生成失败结果,而不直接执行潜在不完整的参数。即使其中某段 JSON 碰巧可以解析,也不意味着整个响应已通过完整性判断。Pi · agent-loop.ts L266–280
6.5 会话树属于上层应用
coding-agent 的 SessionManager 保存带 id 与 parentId 的条目,再通过 buildSessionContext() 解析活动路径、压缩等信息。底层 Agent 的消息列表与应用层会话树有联系,却不能画等号。Pi · session-manager.ts L574–588
这对扩展开发尤其重要。你可能需要在界面保留某项条目,在模型视图忽略它,在恢复时又用它重建配置。先确定条目的生命周期,再决定放进哪一层,通常比增加一个通用“metadata”字段更可靠。
练习:把模型流替换成一个固定响应函数,哪些部分仍应照常工作?答案包括参数校验、工具策略、结果记录与宿主验收。若替换模型后这些机制一并消失,说明模型接口与运行时职责耦合过深。