Appearance
课 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 这些能力,也能继续沿用这套接口。
这一课先用到的是 generateText、streamText 和 messages。后面做到 tool call 和 Agent 时,你会更明显感受到这层抽象的价值。
环境准备
课程使用阿里云百炼(Qwen)演示,新用户有免费额度。通过 Vercel AI SDK 可随时切换到其他模型。
安装依赖
课程代码在 basic/examples/ 目录下。进入项目后安装依赖:
bash
cd agents
pnpm install获取 API Key
- 打开 阿里云百炼,登录或注册
- 点击首页「常用功能」→「API Key」,进入 API Key 管理页
- 点击「创建 API Key」
- 点击列表上的复制按钮,拿到你的 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.tsgenerateText 会等模型生成完整回答后一次性返回。简单直接,但用户需要等待全部生成完才能看到内容。
为什么这课先用 Vercel AI SDK
不同模型供应商的 API 格式各不相同——OpenAI 用 Chat Completions,Anthropic 用 Messages API,消息结构、流式格式、tool call 写法都有差异。如果直接调用各家 SDK,切换模型就意味着改业务代码。
Vercel AI SDK 抹平了这些差异。你只需要换一行 model,generateText、streamText 这些调用代码完全不用动。后续课程中切换模型、做对比测试时会反复体会到这个好处。
流式输出:边生成边显示
真实的聊天应用不会让用户干等。流式输出(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) 实现:
- 客户端发起一个 HTTP 请求
- 服务端保持连接打开,模型每生成一段就通过 SSE 推送一次
- 客户端收到后立即渲染
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 没有记忆。每次调用都是独立的——模型不知道你之前聊过什么。
要实现多轮对话,你需要每次都把完整的对话历史传给模型:
先看图更容易理解:所谓“多轮对话”,本质上是应用层在每一轮都把已有历史重新带回去。
多轮对话为什么越来越重
第 1 轮请求只带当前 user message
→
第 2 轮请求带上第 1 轮 user + assistant
→
第 3 轮请求带上前 2 轮完整历史
→
第 10 轮请求历史继续累积,Token 更高
应用层不断重传历史,换来“记得上下文”的效果,同时也带来延迟、成本和质量压力。
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()每轮对话做了两件事:
- 把用户输入加入
messages - 把模型回复也加入
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/project: basic/project 的目标是先把一个能正常对话的基础聊天应用跑通。流式输出、多轮对话和基础 System Prompt,正是主线最早期的可用闭环。
这一课给主线新增了什么能力:
- 基础消息收发
- 流式输出
- 多轮对话
- 基础 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- 第一轮说「我叫大齐,是前端工程师」,连续聊几轮后问「你还记得我叫什么吗?」
- 不退出程序,持续对话,观察它能否跨多轮记住内容
- 退出后重新启动,再问「你记得我是谁吗?」——确认哪些状态被持久化、哪些被丢失
面试追问
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)等开箱即用的能力。