第 13 章 · 停止原因、中止与错误处理
本章目标:理解 AssistantMessage 的全部 stopReason,掌握 AbortController 主动中止、中止后继续会话,以及调试 Provider payload 的方法。
13.1 Stop Reasons 全景
每次生成结束时,AssistantMessage.stopReason 会告诉你为什么结束:
| stopReason | 含义 |
|---|---|
"pending" | 仅出现在部分消息中,尚不知道最终停止原因 |
"stop" | 模型本轮的最终消息,自然结束 |
"length" | 输出达到最大 token 上限被截断 |
"toolUse" | 模型正在调用工具,等待工具结果 |
"error" | 生成过程中发生错误 |
"aborted" | 请求被 abort signal 取消 |
// 根据停止原因分支处理
if (message.stopReason === 'length') {
// 被截断了:提示用户或用 continue 续写
console.warn('输出被 maxTokens 截断');
} else if (message.stopReason === 'toolUse') {
// 需要执行工具并把结果回填
console.log('模型等待工具结果');
}responseId 不保证存在
AssistantMessage 可能携带 responseId(Provider 上游标识),但不要假设它在所有 Provider 上都存在。
13.2 错误不会抛出流函数
pi-ai 的一个重要设计:请求失败永远不会从 stream 函数抛出异常。包括 abort 和工具校验错误在内的所有失败,都通过 error 事件 + 最终消息的 stopReason 传达:
// 错误处理:监听 error 事件 + 检查最终消息
for await (const event of models.stream(model, context)) {
if (event.type === 'error') {
// event.reason 是 "error" 或 "aborted"
console.log(`${event.reason === 'aborted' ? 'Aborted' : 'Error'}:`, event.error.errorMessage);
}
}
// 流结束后检查最终消息
if (message.stopReason === 'error' || message.stopReason === 'aborted') {
console.log('失败原因:', message.errorMessage);
}这种"错误即数据"的设计让重试逻辑可以完全同步地写在流消费之后,不需要 try/catch 包裹异步循环。
13.3 AbortController 主动中止
用标准的 AbortController 取消进行中的请求——比如用户点了"停止生成"按钮:
const controller = new AbortController();
// 2 秒后自动中止(模拟超时)
setTimeout(() => controller.abort(), 2000);
const response = await models.complete(model, context, {
signal: controller.signal, // 传入 abort 信号
});
// 中止后 stopReason 为 aborted,可能携带部分内容
if (response.stopReason === 'aborted') {
console.log('Request was aborted:', response.errorMessage);
// response.content 中可能已有部分生成内容,可选择性保留
}在 Agent 类上更简单——直接调用 agent.abort() 取消当前操作,然后可用 waitForIdle() 等待完全停止:
agent.abort(); // 取消当前生成/工具执行
await agent.waitForIdle(); // 等待运行完全落定13.4 中止后继续会话
中止不会破坏会话状态。continue() 从当前上下文恢复运行,不添加新消息,适合错误后重试:
// 中止或出错后,从当前状态重试
// 注意:context 最后一条消息必须是 user 或 toolResult,不能是 assistant
await agent.continue();continue() 的前置条件
continue() 要求上下文最后一条消息是 user 或 toolResult。如果最后是 assistant 消息(比如正常结束后想续写),应改用 prompt() 追加新的用户消息。
13.5 调试 Provider Payload
当请求莫名失败时,第一步是看实际发出去的请求体。pi-ai 提供了调试钩子把 Provider payload 打印出来:
// 调试:观察转换后发给 Provider 的真实 payload
const stream = models.stream(model, context, {
// 开启调试后可在回调中检查请求体
onPayload: (payload) => {
console.log(JSON.stringify(payload, null, 2));
},
});常见排查清单:
- 工具 schema 不兼容——某些 OpenAI 兼容端点不支持 strict 模式或 grammar 工具;
- 消息角色顺序——部分兼容端点要求 toolResult 后必须跟 assistant 消息;
- 思考参数不支持——
reasoning_effort字段被网关拒绝; - 字段名差异——
max_tokensvsmax_completion_tokens。
这些兼容性差异大多可以通过模型的 compat 标志位修正(下一章详解)。
本章小结
- 六种 stopReason 覆盖自然结束、截断、工具等待、错误与中止;
- 错误永不抛出流函数,统一通过 error 事件与 stopReason 传达——"错误即数据";
AbortController传入 signal 或agent.abort()都能中止,部分内容仍可保留;continue()从当前状态恢复,前提是最后一条消息为 user/toolResult;- 调试时打印 Provider payload,重点检查工具 schema、角色顺序与思考参数。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. stopReason 为 "toolUse" 意味着什么?
2. pi-ai 中请求失败(如网络错误)时会发生什么?
3. 调用 agent.continue() 的前置条件是?
4. 中止一个进行中的 Agent 操作,最直接的方法是?
🛠️ 动手实践
- 实现一个带"停止生成"按钮的 CLI:按 Ctrl+C 触发 abort,保留已生成的部分内容。
- 写一个自动重试包装器:遇到
stopReason === 'error'时指数退避重试,最多 3 次。 - 故意向一个 OpenAI 兼容端点发送不兼容的工具 schema,用 payload 调试找出被拒绝的字段。
解决了错误处理,下一章学习如何接入任意 OpenAI 兼容端点——第 14 章。