8.7 KiB
8.7 KiB
LLM 客户端使用指南
llm/ 目录封装了一个统一的 AI 能力客户端 SkillsClient,支持聊天补全(非流式/流式)、图片生成、视频生成、AI 搜索和语义重排。底层通过标准 fetch 调用远端 Skills API,无需额外依赖。
目录结构
llm/
index.ts # 统一导出入口(SkillsClient + 所有类型)
client.ts # SkillsClient 实现(chat / image / video / aiSearch / rerank)
models.ts # 所有请求/响应类型定义
app/api/
llm-client.ts # 项目内共享单例(读取环境变量,server-only)
环境变量
在 .env.local 中添加以下配置:
| 变量名 | 说明 |
|---|---|
LLM_API_KEY |
Skills API 鉴权密钥(Bearer Token) |
LLM_BASE_URL |
Skills API 服务地址(如 https://your-api.example.com) |
快速开始
项目已在 app/api/llm-client.ts 中创建好了共享单例,在 API Route 中直接 import 使用即可,无需重复实例化:
import { llmClient } from '@/app/api/llm-client'
如需在其他 server 端模块中单独创建实例:
import { SkillsClient } from '@/llm'
const client = new SkillsClient({
apiKey: process.env.LLM_API_KEY!,
baseUrl: process.env.LLM_BASE_URL!,
})
llm-client.ts仅可在 server 端使用(Next.js API Route、Server Action、Server Component),不能在客户端组件中 import。
功能详解
1. 非流式聊天
import { llmClient } from '@/app/api/llm-client'
const response = await llmClient.chat({
model: 'gpt-5.1', // 可选,默认 'gpt-5.1'
messages: [
{ role: 'system', content: '你是一个助手。' },
{ role: 'user', content: '你好!' },
],
temperature: 0.7, // 可选
maxTokens: 1024, // 可选
})
const text = response.choices[0]?.message?.content
2. 流式聊天(SSE)
使用 chatStream() 返回一个 AsyncGenerator,逐块处理流式响应:
import { llmClient } from '@/app/api/llm-client'
// 在 Next.js API Route 中返回流
export async function POST(req: Request) {
const { messages } = await req.json()
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of llmClient.chatStream({ messages })) {
const delta = chunk.choices[0]?.delta?.content ?? ''
if (delta) {
controller.enqueue(encoder.encode(`data: ${delta}\n\n`))
}
}
controller.close()
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' },
})
}
3. 图片生成
const result = await llmClient.imageGenerate({
prompt: '一只在星空下奔跑的狐狸,写实风格',
model: 'Nano Banana Pro', // 可选,默认 'Nano Banana Pro'
quantityGenerated: 1, // 可选,生成张数
})
if (result.success) {
const urls = result.data?.imageList // string[]
}
4. 视频生成
视频生成分为创建任务和轮询任务状态两步:
// 第一步:创建视频生成任务
const task = await llmClient.videoCreateTask({
prompt: '海浪拍打礁石,慢动作,4K',
model: 'Doubao-Seedance-1.5-pro', // 可选
parameters: {
resolution: '1080p',
duration: 5,
generateAudio: true,
},
})
const taskId = task.data?.taskId
// 第二步:轮询任务状态(pending / running / succeeded / failed)
const result = await llmClient.videoGetTask({ taskId })
if (result.data?.status === 'succeeded') {
const videoUrl = result.data.videos?.[0]?.videoUrl
}
5. AI 搜索(非流式)
const result = await llmClient.aiSearch({
query: 'Next.js 15 新特性',
freshness: 'week', // 可选:day / week / month
})
if (result.success) {
const messages = result.data?.messages
}
6. AI 搜索(流式)
for await (const chunk of llmClient.aiSearchStream({ query: 'TypeScript 5.5 新特性' })) {
if (chunk.isFinish) break
const content = chunk.message?.content
process.stdout.write(content ?? '')
}
7. 语义重排(Rerank)
对候选文档按照与查询的相关性重新排序:
const result = await llmClient.rerank({
query: '如何提升 React 渲染性能',
documents: [
'使用 React.memo 避免不必要重渲染',
'今天天气很好',
'useMemo 和 useCallback 可以缓存计算结果',
],
topN: 2, // 可选,返回前 N 条
returnDocument: true, // 可选,是否在结果中附带原文
model: 'qwen3-vl-rerank', // 可选
})
const ranked = result.data // RerankItem[],按 relevanceScore 降序
API 参考
ChatParams
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
messages |
Message[] |
✅ | 对话历史,每条含 role 和 content |
model |
string |
— | 模型名称,默认 'gpt-5.1' |
stream |
boolean |
— | 是否流式,由 chat/chatStream 自动控制 |
maxTokens |
number |
— | 最大生成 token 数 |
temperature |
number |
— | 采样温度(0–2) |
topP |
number |
— | 核采样概率 |
responseFormat |
object |
— | 返回格式(如 { type: 'json_object' }) |
tools |
unknown[] |
— | Function Calling 工具列表 |
toolChoice |
unknown |
— | 工具调用策略 |
ChatResponse
| 字段 | 类型 | 说明 |
|---|---|---|
choices |
Choice[] |
生成结果列表 |
choices[].message |
ChoiceMessage |
非流式时的完整消息 |
choices[].delta |
ChoiceMessage |
流式时的增量内容 |
choices[].finishReason |
string |
结束原因(stop / tool_calls 等) |
usage |
Usage |
Token 用量统计 |
success |
boolean |
是否成功 |
errorMessage |
string |
失败时的错误信息 |
ImageParams
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
prompt |
string |
✅ | 图片描述 |
model |
string |
— | 默认 'Nano Banana Pro' |
quantityGenerated |
number |
— | 生成张数 |
imageUrlList |
string[] |
— | 参考图 URL 列表 |
watermark |
boolean |
— | 是否添加水印 |
VideoParams
| 字段 | 类型 | 说明 |
|---|---|---|
prompt |
string |
视频描述文本 |
model |
string |
默认 'Doubao-Seedance-1.5-pro' |
taskId |
string |
查询任务时传入 |
firstFrame |
string |
首帧图片 URL |
lastFrame |
string |
尾帧图片 URL |
parameters |
VideoParameters |
分辨率、时长、音频等参数 |
AiSearchParams
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
query |
string |
✅ | 搜索问题 |
freshness |
string |
— | 时效过滤:day / week / month |
stream |
boolean |
— | 由 aiSearch/aiSearchStream 自动控制 |
RerankParams
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
query |
string |
✅ | 查询文本 |
documents |
string[] |
✅ | 候选文档列表 |
topN |
number |
— | 返回前 N 条结果 |
returnDocument |
boolean |
— | 结果中是否附带原始文档 |
model |
string |
— | 默认 'qwen3-vl-rerank' |
在 Next.js API Route 中的完整示例
// app/api/chat/route.ts
import { llmClient } from '@/app/api/llm-client'
import type { Message } from '@/llm'
export async function POST(req: Request) {
const { messages }: { messages: Message[] } = await req.json()
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
try {
for await (const chunk of llmClient.chatStream({
model: 'gpt-5.1',
messages,
temperature: 0.7,
})) {
const delta = chunk.choices[0]?.delta?.content
if (delta) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ content: delta })}\n\n`))
}
if (chunk.choices[0]?.finishReason === 'stop') break
}
} catch (err) {
controller.error(err)
} finally {
controller.close()
}
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
})
}
注意事项
SkillsClient所有方法均为async,需在server端调用(API Route、Server Action)- 流式方法(
chatStream、aiSearchStream)返回AsyncGenerator,需用for await...of消费 - 视频生成为异步任务模式,需轮询
videoGetTask()直到status === 'succeeded' - API Key 通过环境变量注入,不要在客户端代码中引用
llm-client.ts - 所有类型均从
@/llm统一导出,无需从@/llm/models单独引入