Skip to content

第 13 章 · 停止原因、中止与错误处理

本章目标:理解 AssistantMessage 的全部 stopReason,掌握 AbortController 主动中止、中止后继续会话,以及调试 Provider payload 的方法。

13.1 Stop Reasons 全景

每次生成结束时,AssistantMessage.stopReason 会告诉你为什么结束

stopReason含义
"pending"仅出现在部分消息中,尚不知道最终停止原因
"stop"模型本轮的最终消息,自然结束
"length"输出达到最大 token 上限被截断
"toolUse"模型正在调用工具,等待工具结果
"error"生成过程中发生错误
"aborted"请求被 abort signal 取消
typescript
// 根据停止原因分支处理
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 传达:

typescript
// 错误处理:监听 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 取消进行中的请求——比如用户点了"停止生成"按钮:

typescript
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() 等待完全停止:

typescript
agent.abort();             // 取消当前生成/工具执行
await agent.waitForIdle(); // 等待运行完全落定

13.4 中止后继续会话

中止不会破坏会话状态。continue() 从当前上下文恢复运行,不添加新消息,适合错误后重试:

typescript
// 中止或出错后,从当前状态重试
// 注意:context 最后一条消息必须是 user 或 toolResult,不能是 assistant
await agent.continue();

continue() 的前置条件

continue() 要求上下文最后一条消息是 usertoolResult。如果最后是 assistant 消息(比如正常结束后想续写),应改用 prompt() 追加新的用户消息。

13.5 调试 Provider Payload

当请求莫名失败时,第一步是看实际发出去的请求体。pi-ai 提供了调试钩子把 Provider payload 打印出来:

typescript
// 调试:观察转换后发给 Provider 的真实 payload
const stream = models.stream(model, context, {
  // 开启调试后可在回调中检查请求体
  onPayload: (payload) => {
    console.log(JSON.stringify(payload, null, 2));
  },
});

常见排查清单:

  1. 工具 schema 不兼容——某些 OpenAI 兼容端点不支持 strict 模式或 grammar 工具;
  2. 消息角色顺序——部分兼容端点要求 toolResult 后必须跟 assistant 消息;
  3. 思考参数不支持——reasoning_effort 字段被网关拒绝;
  4. 字段名差异——max_tokens vs max_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 操作,最直接的方法是?

🛠️ 动手实践

  1. 实现一个带"停止生成"按钮的 CLI:按 Ctrl+C 触发 abort,保留已生成的部分内容。
  2. 写一个自动重试包装器:遇到 stopReason === 'error' 时指数退避重试,最多 3 次。
  3. 故意向一个 OpenAI 兼容端点发送不兼容的工具 schema,用 payload 调试找出被拒绝的字段。

解决了错误处理,下一章学习如何接入任意 OpenAI 兼容端点——第 14 章