一条 Prompt 在调用模型前还要过多少关
按真实执行顺序追踪 extension command、input hook、Skill 与模板展开、流中排队、认证、预压缩和 before_agent_start,解释 prompt 为什么不是一次直接发送。
调用 session.prompt("解释这个仓库") 看起来像把字符串交给模型。AgentSession.prompt() 实际上更像一段带短路的编译管线。同一个输入可能被扩展命令吃掉,可能被 input hook 改写,可能展开成本地 Skill 全文,也可能在已有 run 中只进入 steering 队列。只有走完空闲分支的输入才会创建 user message。
flowchart TD
accTitle: Prompt 进入 Agent 前的分支
accDescr: 斜杠输入可先被扩展命令消费;其余输入经 hook 和本地展开后,按运行状态进入队列或完成模型与压缩预检,最后组装本轮消息
I["原始 text + images"] --> C{"命中扩展命令?"}
C -->|是| CX["执行 handler,结束"]
C -->|否| H["input hook: handled / transform / continue"]
H --> E["展开 Skill 与 Prompt Template"]
E --> B{"Agent 正在 streaming?"}
B -->|是| Q["steer 或 followUp"]
B -->|否| V["模型与认证预检"]
V --> P["必要时先检查 compaction"]
P --> M["user + nextTurn custom messages"]
M --> BA["before_agent_start"]
BA --> R["_runAgentPrompt"]
扩展命令不会消耗模型响应
管线的第一道判断只在默认允许模板展开且文本以 / 开头时发生。_tryExecuteExtensionCommand() 从 ExtensionRunner 查找注册命令,构造 command context 并直接调用 handler;找到命令以后,无论 handler 正常结束还是抛错,这条输入都被视为已处理,prompt() 不会继续创建 user message。
这解释了一个常见误判:终端里输入 /foo 后没有 assistant message,不一定是模型没响应。若 /foo 是扩展命令,模型根本没被调用。相反,未知斜杠命令会继续向后走,可能命中 Skill 或文件模板;仍未命中时,它就是普通文本。
input hook 看见的仍是用户原文
未被命令消费的输入先进入 extension input 事件。handler 可以返回 handled 终止管线,也可以同时替换文本和图片。Skill 与 prompt template 的展开发生在它之后,所以 input hook 能根据用户真正键入的命令做判断,而不是面对一大段已经注入的 Skill 内容。
接着 _expandSkillCommand() 把 /skill:name args 展开成带 name、location 和相对引用基址的 <skill> 块,并去掉 Skill 文件 frontmatter;文件读取失败时记录 extension error,保留原始文本。普通 prompt template 则支持 $1、$@、默认值与参数切片。替换只扫描模板正文,不会递归解释参数里碰巧出现的 $1。
已有 run 时,prompt 变成排队操作
展开完成后,prompt() 检查 isStreaming。此时 streamingBehavior 是必填项:steer 会在当前 assistant turn 的工具调用完成后、下一次 LLM 调用前送达;followUp 要等 Agent 没有更多工具调用和 steering 消息才送达。两条路径都会更新 AgentSession 自己维护的显示队列,再调用底层 Agent 的队列方法。
扩展 command 不能排队,因为它需要一个当前有效的 command context 并立即执行。公共 steer() 与 followUp() 会显式拒绝命中的扩展命令;prompt() 则把命令判断放在 streaming 分支之前,所以即使 Agent 忙碌,命令仍能立即运行。
空闲分支先证明“可以请求”,再构造消息
Agent 空闲时,控制面先冲刷待写的 bash 消息,然后检查当前 model 和 provider 认证。接着它用上一条 assistant message 做一次 pre-prompt compaction 检查。这条路径主要覆盖上次被用户 abort 的响应:新 prompt 已经准备提交,因此只压缩上下文,不在这里调用 agent.continue()。
随后才出现真正的 user message。文本和图片组成 content 数组,nextTurn custom messages 被接在这条 user message 后面。before_agent_start 扩展还能追加 custom messages,或只为本轮替换 system prompt。override 会在 _runAgentPrompt() 的 finally 中清掉,不会自然变成永久配置。
源码与 characterization test 可以分别验证静态顺序和 faux provider 行为:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
git -C "$repo" show v0.83.0:packages/coding-agent/src/core/agent-session.ts |
nl -ba | sed -n '1114,1265p;1267,1325p'
# 仅当该 checkout 已按仓库要求安装依赖;测试使用 faux provider,不需要真实 API key
cd "$repo"
npx vitest --run packages/coding-agent/test/suite/agent-session-prompt.test.ts
本册固定源码目录当前不含 node_modules,所以这里只实际核对了第一组只读命令,没有把第二条测试写成已通过。还有一个术语边界:RPC 的 preflightResult(true) 只表示输入已被接受、命令已处理或消息已成功入队,模型响应仍可能在之后失败。
prompt 管线最终把输入交给 Agent,而第 25 章已经看到完成消息会被 SessionManager 接住。下一章不再看模型调用,直接打开 JSONL:它怎样用 append-only 记录把一段看似线性的对话保存成树。