Skip to content

第 6 章 · 生成设置与生命周期回调

本章目标:

  • 掌握 maxOutputTokenstemperaturetopP 等语言模型调用选项的含义与取舍
  • 区分影响生成行为的 Call Options 与影响传输行为的 Request Options
  • 学会用 timeout 对象对总时长、单步、首块、工具执行分别设置超时
  • 掌握 onStart / onStepStart / onLanguageModelCallEnd / onToolExecutionEnd 等生命周期回调的执行顺序与应用场景

6.1 常见设置总览

💡 本章示例中的 model 变量按第 3 章的方式构造:createGateway()(Vercel AI Gateway)或 createOpenAICompatible()(自定义 OpenAI 兼容 Provider)均可,设置项与 provider 无关。

大语言模型(LLM)通常提供一些设置来调整输出。除 model、prompt 以及 provider 特定设置外,所有 AI SDK 函数都支持以下常见设置:

ts
const result = await generateText({
  model,
  maxOutputTokens: 512,
  temperature: 0.3,
  maxRetries: 5,
  timeout: 10000,
  prompt: 'Invent a new holiday and describe its traditions.',
});

⚠️ 部分 provider 不支持所有通用设置。使用不支持的设置时会产生警告,可通过结果对象的 warnings 属性查看。

这些设置分为两大类:

  • Language Model Call Options:影响模型如何生成响应(token 上限、采样行为、惩罚、停止序列、seed、reasoning)
  • Request Options:影响传输层——重试、取消、超时,不影响生成行为本身

6.2 Language Model Call Options

maxOutputTokens

生成的最大 token 数。

temperature

温度设置。值会透传给 provider,范围取决于 provider 和模型。对多数 provider 而言,0 意味着结果几乎确定,值越高随机性越强。建议只设置 temperaturetopP 其中之一,不要同时设置。

💡 自 AI SDK 5.0 起,temperature 不再默认设为 0

topP

Nucleus sampling(核采样)。对多数 provider 来说是 0 到 1 之间的数值,例如 0.1 表示只考虑概率质量前 10% 的 token。同样建议与 temperature 二选一。

topK

每个后续 token 只从概率最高的 K 个选项中采样,用于剔除「长尾」低概率响应。仅推荐高级场景,通常只需要 temperature

presencePenalty 与 frequencyPenalty

  • presencePenalty:影响模型重复 prompt 中已有信息的倾向
  • frequencyPenalty:影响模型重复使用相同词语或短语的倾向

对多数 provider,0 表示无惩罚,范围取决于具体 provider 和模型。

stopSequences

停止序列。设置后,模型生成其中任一序列时就会停止生成文本。provider 可能限制停止序列的数量。

seed

用于随机采样的种子(整数)。如果设置且模型支持,调用将产生确定性结果。

reasoning

控制模型在回答前进行多少推理:

取值行为
'provider-default'使用 provider 默认推理行为(省略时的默认值)
'none'关闭推理
'minimal'最少推理
'low'快速简洁的推理
'medium'平衡的推理
'high'彻底的推理
'xhigh'最大程度推理

如果你同时在 providerOptions 中设置了 reasoning 相关选项(如 openai.reasoningEffortanthropic.thinking),provider 特定选项优先,顶层 reasoning 参数会被忽略。

6.3 Request Options

maxRetries

最大重试次数。设为 0 可禁用重试。默认值为 2

abortSignal

可选的 abort signal,用于取消调用。可以从用户界面转发以取消调用,或用 AbortSignal.timeout 定义超时:

ts
const result = await generateText({
  model,
  prompt: 'Invent a new holiday and describe its traditions.',
  abortSignal: AbortSignal.timeout(5000), // 5 秒
});

timeout

以毫秒为单位的可选超时。调用超过指定时长会被中止。这是便捷参数,内部会创建 abort signal;可与 abortSignal 同时使用——两者任一满足即中止。

可以传数字,也可以传功能更丰富的对象:

  • totalMs:整个调用(含所有步骤)的总超时
  • stepMs:每一步(LLM 调用)的超时,适合多步生成中独立限制每步时间
  • firstChunkMs:(仅流式)每步第一个内容输出的超时。文本增量、reasoning 增量、tool-input 增量、生成文件和 tool call 都算内容输出;响应元数据、流开始、空增量和传输活动不算
  • chunkMs:(仅流式)开始输出后内容块之间的超时,用于检测「生成中途卡死」的流
  • toolMs:所有工具执行的默认超时,超时会中止并以 tool-error 返回,让模型可以响应或重试
  • tools:按工具名覆盖超时(如 weatherMsslowApiMs),优先级高于 toolMs
ts
// 数字格式:5 秒总超时
await generateText({
  model,
  prompt: 'Invent a new holiday and describe its traditions.',
  timeout: 5000,
});

// 组合:总共 60 秒 + 每步 10 秒
await generateText({
  model,
  prompt: 'Invent a new holiday and describe its traditions.',
  timeout: { totalMs: 60000, stepMs: 10000 },
});

// 流式:内容卡住 5 秒即中止
streamText({
  model,
  prompt: 'Invent a new holiday and describe its traditions.',
  timeout: { chunkMs: 5000 },
});

// 工具执行超时与按工具覆盖
await generateText({
  model,
  tools: { weather: weatherTool, slowApi: slowApiTool },
  timeout: {
    toolMs: 5000, // 所有工具默认 5 秒
    tools: {
      weatherMs: 3000, // weather 工具 3 秒
      slowApiMs: 10000, // slowApi 工具 10 秒
    },
  },
  prompt: 'What is the weather in San Francisco?',
});

headers

随请求发送的额外 HTTP 头,仅适用于基于 HTTP 的 provider。例如部分可观测性 provider 支持 Prompt-Id 这样的头:

ts
import { generateText } from 'ai';

const result = await generateText({
  model,
  prompt: 'Invent a new holiday and describe its traditions.',
  headers: {
    'Prompt-Id': 'my-prompt-id',
  },
});

💡 headers 设置只针对单个请求。也可以在 provider 配置中设置 headers,那会随该 provider 的每个请求发送。

6.4 生命周期回调基础

事件回调让你能在 AI SDK 调用的重要节点运行自己的代码。可以直接附加到 generateTextstreamTextembedembedManyrerank 调用上,用于观察发生了什么、记录用量、调试多步生成和监控工具执行。

典型用途:

  • 记录请求使用了哪个模型、prompt 形态和设置
  • 为分析或计费记录 token 用量、延迟、finish reason 和警告
  • 理解多步工具调用如何从模型响应走到工具执行再到最终答案
  • 通过 runtimeContexttoolsContext 附加自己的请求/用户/租户标识

需要跨应用自动 OpenTelemetry 埋点时用 Telemetry(第 13 章);只想针对特定调用运行自定义代码时用事件回调。

tsx
import { generateText } from 'ai';

const result = await generateText({
  model,
  prompt: 'What is the weather in San Francisco?',

  onStart({ callId, modelId }) {
    console.log('Generation started', { callId, modelId });
  },

  onEnd({ callId, usage, finishReason }) {
    console.log('Generation finished', {
      callId,
      finishReason,
      totalTokens: usage.totalTokens,
    });
  },
});

回调可以是同步或异步的。回调抛错时会被内部捕获,AI SDK 调用继续进行。由于回调属于生命周期的一部分,请保持它们轻量,或将耗时工作排入后台系统。

6.5 典型应用场景

请求日志

onStartonEnd 分别记录一次调用的开始与结束。callId 在所有生命周期事件中都可用,可用于关联同一请求的日志:

tsx
import { generateText } from 'ai';

const result = await generateText({
  model,
  prompt: 'Write a short product description for a camping mug.',

  onStart({ callId, provider, modelId }) {
    logger.info('ai.request.started', {
      callId,
      provider,
      modelId,
    });
  },

  onEnd({ callId, finishReason, usage, warnings }) {
    logger.info('ai.request.finished', {
      callId,
      finishReason,
      usage,
      warningCount: warnings?.length ?? 0,
    });
  },
});

这个模式非常适合审计日志、内部看板和特定功能的用量追踪。

测量模型性能

onLanguageModelCallEnd 会在 provider 响应被规范化并解析完成后触发。对 streamText,事件还包含流式专属的计时数据,如首个输出耗时和输出块间隔:

tsx
import { streamText } from 'ai';

const result = streamText({
  model,
  prompt: 'Explain partial prerendering in two paragraphs.',

  onLanguageModelCallEnd({ callId, modelId, usage, performance, providerMetadata }) {
    metrics.histogram('ai.model.response_time_ms', performance.responseTimeMs, {
      callId,
      modelId,
    });

    metrics.gauge('ai.model.tokens_per_second', {
      output: performance.outputTokensPerSecond,
      total: performance.effectiveTotalTokensPerSecond,
      tokens: usage.totalTokens,
    });

    logger.info('ai.model.provider_metadata', { callId, providerMetadata });
  },
});

for await (const textPart of result.textStream) {
  process.stdout.write(textPart);
}

要单独衡量 provider 的工作量就用 model-call 事件;要包含 SDK 管理的工作(如本地工具执行)的计时就用 step 事件。

调试多步工具调用

使用工具时,一个用户请求可能涉及多次模型调用。每次模型调用是一个 step:模型可能在某步调用工具、拿到工具结果后在下一步产出最终回答:

tsx
import { generateText, isStepCount, tool } from 'ai';
import { z } from 'zod';

const result = await generateText({
  model,
  stopWhen: isStepCount(5),
  prompt: 'What is the weather in San Francisco?',
  tools: {
    weather: tool({
      description: 'Get the weather in a location',
      inputSchema: z.object({ location: z.string() }),
      execute: async ({ location }) => getWeather(location),
    }),
  },

  onStepStart({ stepNumber, messages, steps }) {
    console.log(`Step ${stepNumber} started`, {
      messageCount: messages.length,
      previousSteps: steps.length,
    });
  },

  onStepEnd({ stepNumber, finishReason, toolCalls, usage, performance }) {
    console.log(`Step ${stepNumber} finished`, {
      finishReason,
      toolCalls: toolCalls.map(toolCall => toolCall.toolName),
      totalTokens: usage.totalTokens,
      stepTimeMs: performance.stepTimeMs,
    });
  },
});

这能回答:模型是调用了工具还是直接回答?请求用了几步?哪一步消耗最多 token?时间花在模型响应还是本地工具执行?

监控工具执行

工具执行回调围绕工具的 execute 函数运行,用于记录工具用量、延迟、成功结果与错误:

tsx
import { generateText, tool } from 'ai';
import { z } from 'zod';

const result = await generateText({
  model,
  prompt: 'Find flights from SFO to JFK tomorrow morning.',
  tools: {
    searchFlights: tool({
      description: 'Search available flights',
      inputSchema: z.object({
        origin: z.string(),
        destination: z.string(),
      }),
      execute: async input => searchFlights(input),
    }),
  },

  onToolExecutionStart({ callId, toolCall }) {
    logger.info('ai.tool.started', {
      callId,
      toolCallId: toolCall.toolCallId,
      toolName: toolCall.toolName,
      input: toolCall.input,
    });
  },

  onToolExecutionEnd({ callId, toolCall, toolExecutionMs, toolOutput }) {
    logger.info('ai.tool.finished', {
      callId,
      toolCallId: toolCall.toolCallId,
      toolName: toolCall.toolName,
      durationMs: toolExecutionMs,
      success: toolOutput.type === 'tool-result',
    });
  },
});

toolOutput 是一个 discriminated union:当 toolOutput.type === 'tool-result' 时输出在 toolOutput.output;为 'tool-error' 时错误在 toolOutput.error

6.6 生成生命周期顺序

generateTextstreamText 的生命周期最丰富,因为它们可能涉及 prompt、模型调用、工具调用和多步骤。

典型的单步生成按此顺序执行回调:

  1. onStart
  2. onStepStart
  3. onLanguageModelCallStart
  4. onLanguageModelCallEnd
  5. onStepEnd
  6. onEnd

带本地工具执行的多步生成通常如下:

  1. onStart
  2. onStepStart
  3. onLanguageModelCallStart
  4. onLanguageModelCallEnd
  5. onToolExecutionStart
  6. onToolExecutionEnd
  7. onStepEnd
  8. 重复 step 回调直到满足停止条件
  9. onEnd

onStepStart / onStepEnd 描述完整的一步;onLanguageModelCallStart / onLanguageModelCallEnd 只描述这一步内部的模型调用。当一步包含本地工具执行时,这个区别很重要——step 时长可能大于模型响应时长。

⚠️ 旧的 onStepFinish 已弃用,新代码请使用 onStepEnd

6.7 Runtime Context 与 Tool Context

生命周期回调会收到贯穿整个调用的 runtimeContexttoolsContext,这让回调可以在不改 prompt 或工具输入的情况下附加应用上下文:

tsx
import { generateText, tool } from 'ai';
import { z } from 'zod';

const result = await generateText({
  model,
  prompt: 'Check the order status.',
  runtimeContext: {
    requestId: 'req_123',
    tenantId: 'tenant_abc',
  },
  tools: {
    getOrderStatus: tool({
      inputSchema: z.object({ orderId: z.string() }),
      contextSchema: z.object({ region: z.string() }),
      execute: async ({ orderId }, { context }) =>
        getOrderStatus(orderId, context.region),
    }),
  },
  toolsContext: {
    getOrderStatus: {
      region: 'us-east-1',
    },
  },

  onStart({ callId, runtimeContext }) {
    logger.info('ai.request.started', {
      callId,
      requestId: runtimeContext.requestId,
      tenantId: runtimeContext.tenantId,
    });
  },

  onToolExecutionStart({ toolCall, toolContext }) {
    logger.info('ai.tool.started', {
      toolName: toolCall.toolName,
      region: toolContext.region,
    });
  },
});

⚠️ 遥测集成可以在导出前过滤 runtimeContexttoolsContext,但生命周期回调收到的是完整的上下文对象——小心不要从回调里记录密钥或敏感用户数据。

嵌入与重排序也有简化版回调:embedMany / rerank 提供 onStart / onEnd,可用于统计检索管道中嵌入了多少值、重排序如何改变结果集等。

本章小结

  • 设置分两类:Language Model Call Options(temperature/topP/maxOutputTokens/reasoning 等)影响生成;Request Options(maxRetries/abortSignal/timeout/headers)影响传输
  • temperaturetopP 建议二选一;AI SDK 5.0 起 temperature 不再默认 0
  • reasoning 统一了各家的推理力度控制,provider 特定的 providerOptions 优先于顶层 reasoning
  • timeout 支持数字与对象格式,可细粒度控制 totalMs/stepMs/firstChunkMs/chunkMs/toolMs 及按工具覆盖
  • 生命周期回调顺序:onStart → onStepStart → onLanguageModelCallStart → onLanguageModelCallEnd → (onToolExecutionStart → onToolExecutionEnd) → onStepEnd → onEnd
  • 回调抛错被内部捕获不中断流程;callId 可关联同一请求的全部事件;注意不要在回调里记录敏感数据

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 关于 temperature 和 topP,官方建议是?

2. timeout 对象格式中,chunkMs 的作用是什么?

3. 带本地工具执行的多步生成中,正确的回调顺序是?

4. 生命周期回调内部抛出异常会怎样?

🛠️ 动手实践

  1. 写一个对比实验:同一 prompt 分别用 temperature: 0temperature: 1 各生成 3 次,观察差异;再用 seed: 42 验证确定性。
  2. 给一个带慢速工具(内部 sleep 3 秒)的多步生成配置 timeout: { totalMs: 30000, stepMs: 8000, toolMs: 2000 },观察 tool-error 如何产生并被模型处理。
  3. onStart/onStepStart/onLanguageModelCallEnd/onToolExecutionEnd/onStepEnd/onEnd 六个回调各打印一行日志并带上 callId,跑一次两步的工具调用,验证第 6.6 节的顺序图。