请求发出前,Context 还会经历什么
从 AgentContext 投影、SimpleStreamOptions 归一化追到 provider payload,解释通用请求字段在哪些阶段被变换、收紧、覆盖或忽略。
完成认证不等于 payload 已经完成。第 6 章得到的 Context 只有 systemPrompt、标准 Message[] 和可选 tools;Agent 的内部消息、队列策略和事件 sink 都不会越过这条线。与此同时,Agent 把解析后的 API key、当前 AbortSignal 以及自己的 stream 配置交给流函数。这里形成的是 provider 无关输入,还不是任何一家 API 的 JSON。
Context 先从 Agent 状态里投影出来
StreamOptions 是一份静态能力上限:温度、输出 token、signal、自定义 fetch、transport、缓存、session id、headers、超时、重试、metadata、provider-scoped env 等都能在类型里出现。注释同时保留了边界,例如不支持自定义 fetch 的适配器可以拒绝,某些 provider 会忽略 transport 或 metadata。字段存在不代表所有后端语义相同。
通用 options 还要按模型收紧
这里还有两套入口。stream() 面向已经知道具体 API 的调用方,已知 API 会在 TypeScript 中映射到自己的 options 类型;streamSimple() 才接受带通用 reasoning 与 thinkingBudgets 的 SimpleStreamOptions,替调用方做能力 clamp。两者返回相同事件流,却不承诺相同参数抽象。若扩展需要某家 API 的 toolChoice 或 service tier,应选具体入口,不能指望 simple 层替它无损猜测。
flowchart LR
accTitle: Context 与请求选项的收紧过程
accDescr: Agent 上下文先投影成通用 Context,通用选项按模型窗口归一化,再由具体适配器转换消息和构建 payload
A["AgentContext"] --> C["pi-ai Context"]
O["SimpleStreamOptions"] --> B["buildBaseOptions"]
C --> B
B --> P["provider-specific options"]
C --> X["消息与工具转换"]
P --> J["API payload"]
X --> J
J --> H["onPayload / HTTP request / onResponse"]
streamSimple() 是最常见的收紧入口。buildBaseOptions() 会估算当前 Context token,用模型窗口减去输入和 4096 个安全 token,再限制 maxTokens,但至少保留 1;其余通用字段按请求继续传递。推理等级和 thinking budget 则由适配器按模型能力处理,不能仅凭调用方写了 xhigh 就推断最终请求仍是 xhigh。
这项估算是保护带,不是精确 tokenizer 或溢出证明。结果至少为 1,意味着输入已经逼近窗口时,Pi 仍会让 provider 得到一个合法的正输出上限;真实服务若采用不同计数方式,仍可能返回 overflow,之后才由第 12 章的会话策略恢复。反过来,调用方显式设置很大的 maxTokens 也不会原样透传,而会被压到这套估算公式给出的结果。
以 OpenAI Completions 适配器为例,streamSimple 先构建 base options、按模型 clamp reasoning,再进入 provider-specific stream。真正的 client 会合并模型默认 headers、session affinity 与请求覆盖;buildParams() 才转换消息,选择 max_tokens 或 max_completion_tokens 字段,并决定缓存与工具参数。
最终 payload 属于具体适配器
还有两个运行时观察点。onPayload 在发送前可以检查或替换 provider payload,onResponse 在 HTTP 响应到达、body 流被消费前收到状态与 headers。它们不是对 AgentContext 的静态修改;若回调替换了 payload,替换结果才是实际请求材料。
因此排查一次“参数为什么没生效”,需要先问它停在哪层:Agent config 是否传入,buildBaseOptions 是否保留,模型能力是否 clamp,适配器是否认识,最后 onPayload 是否替换。只打印最初的 Context 或 options,只能证明调用意图,不能证明线路上的最终 payload。
sessionId、cache retention 和 transport 尤其如此:它们只有被目标适配器映射成 header、payload 字段或连接选择后才产生线上效果。未知 provider 静默忽略某项通用偏好,也不能反推 Agent 没有传值。
可以用下面的只读切片确认三个阶段没有混在一起:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
git -C "$repo" show v0.83.0:packages/agent/src/agent-loop.ts |
nl -ba | sed -n '277,312p'
git -C "$repo" show v0.83.0:packages/ai/src/api/simple-options.ts |
nl -ba | sed -n '12,77p'
git -C "$repo" show v0.83.0:packages/ai/src/api/openai-completions.ts |
nl -ba | sed -n '609,717p'
依赖已安装时,可在 packages/ai 下运行 node ../../node_modules/vitest/dist/cli.js --run test/context-estimate.test.ts 检查 Context 估算。请求发出后,适配器还需把各自的流式协议还原成 Agent 能稳定消费的事件。