实战案例

三个由浅入深的例子:流式聊天、多工具助手、无 UI 结构化抽取。模型用 Gateway 字符串 openai/gpt-4.1-miniID 会变,换成你账号里能用的)。依赖见 安装


案例 1:App Router 流式聊天

目标: Next.js 首页对话;密钥只在 Route Handler。

// app/api/chat/route.ts
import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  streamText,
  toUIMessageStream,
  type UIMessage,
} from 'ai';

export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();
  const result = streamText({
    model: 'openai/gpt-4.1-mini',
    instructions: 'Reply concisely in the user language.',
    messages: await convertToModelMessages(messages),
    onError({ error }) {
      console.error(error);
    },
  });
  return createUIMessageStreamResponse({
    stream: toUIMessageStream({ stream: result.stream }),
  });
}
// app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';

export default function Chat() {
  const [input, setInput] = useState('');
  const { messages, sendMessage, status } = useChat();
  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          {m.role}: {m.parts.map((p, i) => (p.type === 'text' ? <span key={i}>{p.text}</span> : null))}
        </div>
      ))}
      <form
        onSubmit={(e) => {
          e.preventDefault();
          sendMessage({ text: input });
          setInput('');
        }}
      >
        <input value={input} onChange={(e) => setInput(e.currentTarget.value)} disabled={status !== 'ready'} />
      </form>
    </div>
  );
}

验证:pnpm run dev,发「用一句话介绍 AI SDK」,应逐字出现回复。刷新后前端状态清空(未做持久化是正常的)。DefaultChatTransportbody 可带 chatId 做服务端存储——那是产品层,不是 SDK 必选项。更多 status / stopuseChat


案例 2:天气 + 单位换算助手

在案例 1 的 streamText 上加工具,并设 stopWhen: isStepCount(5),否则模型调完工具就停、没有自然语言总结。完整 tool() 定义见 工具调用

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

const tools = {
  weather: tool({
    description: 'Get the weather in a location (fahrenheit)',
    inputSchema: z.object({ location: z.string().describe('City name') }),
    execute: async ({ location }) => ({ location, temperature: 72 }),
  }),
  convertFahrenheitToCelsius: tool({
    description: 'Convert fahrenheit to celsius',
    inputSchema: z.object({ temperature: z.number() }),
    execute: async ({ temperature }) => ({
      celsius: Math.round((temperature - 32) * (5 / 9)),
    }),
  }),
};

const result = streamText({
  model: 'openai/gpt-4.1-mini',
  messages, // 已经 convertToModelMessages
  stopWhen: isStepCount(5),
  tools,
});

前端对 tool-weather / tool-convertFahrenheitToCelsiusJSON.stringify 或卡片。问「上海现在多少摄氏度?」应看到两次工具 part,再跟一句回答。真实天气 API 的 Key 留在服务端;校验 location,不要把原始字符串拼进 URL。


案例 3:无 UI 结构化抽取

适合脚本、Webhook、队列 worker——不经过 useChat

import { generateText, NoObjectGeneratedError, Output } from 'ai';
import { z } from 'zod';

try {
  const { output } = await generateText({
    model: 'openai/gpt-4.1-mini',
    output: Output.object({
      schema: z.object({
        title: z.string().describe('Short title'),
        tags: z.array(z.string()).describe('Up to 5 tags'),
        summary: z.string().describe('Two sentences'),
      }),
    }),
    prompt: `Extract metadata from:\n${articleText}`,
  });
  console.log(output.title, output.tags);
} catch (error) {
  if (NoObjectGeneratedError.isInstance(error)) {
    console.error(error.cause, error.text);
  } else {
    throw error;
  }
}

边生成边显示:改 streamText + partialOutputStream,或前端 useObject。本地模型把 model 换成 Ollama 兼容实例(见 Provider);小模型 JSON 差时换更大云模型。


反模式

反模式改进
浏览器里调 generateText只在 Route Handler / 脚本里调
默认 stopWhen 却要最终回答工具场景设 isStepCount(5)
渲染 message.content渲染 message.parts
实验性 streamUI 当默认聊天生产用 useChat
NEXT_PUBLIC_ 前缀加在密钥上去掉该前缀

下一步

评论