Skip to content

课 2 · 第一个 LLM 应用

本课目标

用 Vercel AI SDK 接入 LLM,实现流式输出和多轮对话。课后你会拿到一个可在终端运行的聊天程序。

上一课理解了 LLM 的基本原理,这一课直接上手写代码。

先认识 Vercel 和 Vercel AI SDK

  • Vercel 是一家做前端云平台的公司,最出名的是部署和托管 Web 应用,Next.js 也是它维护的。
  • Vercel AI SDK 是它开源的一套 AI 开发 SDK,核心包名就是 ai

在课程里,可以把它理解成一层“AI 应用基础设施”:

  • 统一不同模型的调用方式,可以接 OpenAI、Anthropic、Qwen 等模型。
  • 提供一套通用接口,文本生成、流式输出、多轮对话都能用同样的写法。
  • 结构化输出、tool call、Agent 这些能力,也能继续沿用这套接口。

这一课先用到的是 generateTextstreamTextmessages。后面做到 tool call 和 Agent 时,你会更明显感受到这层抽象的价值。

环境准备

课程使用阿里云百炼(Qwen)演示,新用户有免费额度。通过 Vercel AI SDK 可随时切换到其他模型。

安装依赖

课程代码在 basic/examples/ 目录下。进入项目后安装依赖:

bash
cd agents
pnpm install

获取 API Key

  1. 打开 阿里云百炼,登录或注册
  2. 点击首页「常用功能」→「API Key」,进入 API Key 管理页
  3. 点击「创建 API Key」
  4. 点击列表上的复制按钮,拿到你的 Key

然后在项目根目录复制 .env.example.env,将 CHAT_API_KEY=sk-xxx 替换为你的

bash
cp .env.example .env

安全提示

.env 文件已在 .gitignore 中,不会被提交。永远不要把 API Key 硬编码在代码里。

最简单的调用:一次性生成

先从最简单的开始——发一条消息,拿一个完整的回答:

typescript
import 'dotenv/config'
import { generateText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'

const model = createOpenAI({
  apiKey: process.env.CHAT_API_KEY,
  baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
}).chat('qwen3.6-plus')

const { text } = await generateText({
  model,
  prompt: '用一句话解释什么是 TypeScript',
})

console.log(text)

运行:

bash
pnpm exec tsx basic/examples/01-llm-chat/02-first-app/index.ts

generateText 会等模型生成完整回答后一次性返回。简单直接,但用户需要等待全部生成完才能看到内容。

为什么这课先用 Vercel AI SDK

不同模型供应商的 API 格式各不相同——OpenAI 用 Chat Completions,Anthropic 用 Messages API,消息结构、流式格式、tool call 写法都有差异。如果直接调用各家 SDK,切换模型就意味着改业务代码。

Vercel AI SDK 抹平了这些差异。你只需要换一行 modelgenerateTextstreamText 这些调用代码完全不用动。后续课程中切换模型、做对比测试时会反复体会到这个好处。

流式输出:边生成边显示

真实的聊天应用不会让用户干等。流式输出(Streaming) 让模型生成一个 Token 就立刻发送给前端,用户可以实时看到文字逐步出现。

typescript
// model 初始化同上

const result = streamText({
  model,
  prompt: '解释一下 React 的虚拟 DOM 机制',
})

for await (const chunk of result.textStream) {
  process.stdout.write(chunk)
}
console.log()

streamText 返回一个可迭代的流。每个 chunk 是模型新生成的一小段文本,通常是一个或几个 Token。

流式输出的底层机制

浏览器环境中,流式输出通过 Server-Sent Events(SSE) 实现:

  1. 客户端发起一个 HTTP 请求
  2. 服务端保持连接打开,模型每生成一段就通过 SSE 推送一次
  3. 客户端收到后立即渲染

Vercel AI SDK 封装了这些细节。在 Next.js 中,你只需要用 useChat hook,流式就自动处理了。但在纯 Node.js 环境(比如我们现在的终端程序),直接遍历 textStream 就行。

消息格式:system / user / assistant

LLM 的对话由一组消息(Messages) 组成,每条消息有一个角色:

角色作用谁写的
system设定 AI 的行为规则和人设开发者
user用户输入用户
assistant模型的回复模型
typescript
// model 初始化同上

const result = streamText({
  model,
  system: '你是一个前端技术顾问,回答简洁专业,用中文。',
  messages: [
    { role: 'user', content: '什么是 Server Components?' },
  ],
})

for await (const chunk of result.textStream) {
  process.stdout.write(chunk)
}
console.log()

system 和 messages 的区别

  • system 对应 system 消息,设定全局行为规则
  • messages 是对话历史,包含 user 和 assistant 的交替消息

Vercel AI SDK 中,system 可以作为独立参数传入(如上),也可以放在 messages 数组的第一条。推荐用独立参数——语义更清晰。

多轮对话:传入完整历史

LLM 没有记忆。每次调用都是独立的——模型不知道你之前聊过什么。

要实现多轮对话,你需要每次都把完整的对话历史传给模型

先看图更容易理解:所谓“多轮对话”,本质上是应用层在每一轮都把已有历史重新带回去。

typescript
// basic/examples/01-llm-chat/02-first-app/index.ts(完整代码)
import 'dotenv/config'
import { streamText, type ModelMessage } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import * as readline from 'node:readline'

const model = createOpenAI({
  apiKey: process.env.CHAT_API_KEY,
  baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
}).chat('qwen3.6-plus')

const messages: ModelMessage[] = []

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
})

function ask(query: string): Promise<string> {
  return new Promise((resolve) => rl.question(query, resolve))
}

console.log('开始对话(输入 /exit 退出)\n')

while (true) {
  const input = await ask('你: ')
  if (input.toLowerCase() === '/exit') break

  messages.push({ role: 'user', content: input })

  const result = streamText({
    model,
    system: '你是一个前端技术顾问,回答简洁专业,用中文。',
    messages,
  })

  process.stdout.write('AI: ')
  let fullResponse = ''
  for await (const chunk of result.textStream) {
    process.stdout.write(chunk)
    fullResponse += chunk
  }
  console.log('\n')

  messages.push({ role: 'assistant', content: fullResponse })
}

rl.close()

每轮对话做了两件事:

  1. 把用户输入加入 messages
  2. 把模型回复也加入 messages

下一次调用时,模型能看到之前所有的对话内容,就实现了"记忆"的效果。

代价

传入的消息越多,每次调用消耗的 Token 越多。 第 1 轮传 1 条消息,第 10 轮传 19 条消息(10 条 user + 9 条 assistant),第 100 轮传 199 条消息。

Token 数量直接决定:

  • 延时:输入越长,首 Token 等待越久
  • 成本:每一轮都要为之前所有内容付费
  • 质量:接近 context window 上限时,模型注意力分散,回答质量下降

这就是上一课说的"Context Window 是有限的,你的代码需要管理它"。具体怎么管理,下一课讲。

换一种接口协议

前面说过 Vercel AI SDK 切换模型只需要改 model,这里验证一下:

typescript
import { createAnthropic } from '@ai-sdk/anthropic'

const model = createAnthropic({
  apiKey: process.env.CHAT_API_KEY,
  baseURL: 'https://dashscope.aliyuncs.com/apps/anthropic/v1', // 改成 anthropic 兼容 API
}).messages('qwen3.6-plus') // 改成 messages 方法

// 业务代码完全不需要改
const result = streamText({
  model,
  prompt: '你好',
})

阿里云百炼同时兼容 OpenAI 和 Anthropic 接口

前面用的是 createOpenAI + OpenAI 兼容端点,这里换成了 createAnthropic + Anthropic 兼容端点。底层是同一个服务,但走的协议不同——而 streamText 那行代码完全没变。这就是 Vercel AI SDK 统一抽象的价值。

同样的道理,如果你想切换到顶级模型,也只需要改 model 一行:

typescript
// Claude Opus 4.6
import { anthropic } from '@ai-sdk/anthropic'
const model = anthropic('claude-opus-4-6')

// GPT-5.4
import { openai } from '@ai-sdk/openai'
const model = openai('gpt-5.4')

本课产物

一个在终端运行的多轮聊天程序,支持:

  • ✅ 流式输出(边生成边显示)
  • ✅ 多轮对话(自动维护消息历史)
  • ✅ System Prompt(可设定 AI 行为)
  • ✅ 可切换接口协议(Vercel AI SDK 统一抽象)

完整代码在 basic/examples/01-llm-chat/02-first-app/index.ts

并入主线项目

本模块能力会在基础项目 basic/project/ 中收束,当前课内 Demo 见下方运行方式。

为什么这一步属于 basic/projectbasic/project 的目标是先把一个能正常对话的基础聊天应用跑通。流式输出、多轮对话和基础 System Prompt,正是主线最早期的可用闭环。

这一课给主线新增了什么能力:

  1. 基础消息收发
  2. 流式输出
  3. 多轮对话
  4. 基础 System Prompt

下一课会继续留在 basic/project,补上上下文窗口控制和基础输入校验,让这个聊天闭环更稳。

试试看

bash
cd daqi-ai-agent

# 默认使用 OpenAI 兼容协议
pnpm exec tsx basic/examples/01-llm-chat/02-first-app/index.ts

# 加 --anthropic 切换到 Anthropic 兼容协议
pnpm exec tsx basic/examples/01-llm-chat/02-first-app/index.ts --anthropic
  1. 第一轮说「我叫大齐,是前端工程师」,连续聊几轮后问「你还记得我叫什么吗?」
  2. 不退出程序,持续对话,观察它能否跨多轮记住内容
  3. 退出后重新启动,再问「你记得我是谁吗?」——确认哪些状态被持久化、哪些被丢失

面试追问

Q:流式输出用的是什么协议?

Server-Sent Events(SSE)。客户端发起一个普通 HTTP 请求,服务端保持连接不断开,通过 SSE 推送增量数据。和 WebSocket 不同,SSE 是单向的(只有服务端推客户端),但对 LLM 的逐 Token 输出场景来说够用了。

Q:为什么每次调用都要传完整的消息历史?

因为 LLM 是无状态的。每次 API 调用都是独立的推理过程,模型本身不保存任何对话记录。要让模型"记住"之前聊的内容,只能把历史消息作为输入传进去。这也是为什么对话越长成本越高——每一轮都要重新传入所有历史。

Q:Vercel AI SDK 和直接调用 OpenAI SDK 有什么区别?

Vercel AI SDK 是统一抽象层,抹平了各家模型在接口协议、流式格式、tool call 等方面的差异。此外还提供了结构化输出(generateText + Output.object())和前端集成(useChat)等开箱即用的能力。

面向前端工程师和独立开发者的 AI 应用工程课程