# 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 使用即可**,无需重复实例化: ```typescript import { llmClient } from '@/app/api/llm-client' ``` 如需在其他 server 端模块中单独创建实例: ```typescript 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. 非流式聊天 ```typescript 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`,逐块处理流式响应: ```typescript 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. 图片生成 ```typescript 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. 视频生成 视频生成分为**创建任务**和**轮询任务状态**两步: ```typescript // 第一步:创建视频生成任务 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 搜索(非流式) ```typescript const result = await llmClient.aiSearch({ query: 'Next.js 15 新特性', freshness: 'week', // 可选:day / week / month }) if (result.success) { const messages = result.data?.messages } ``` ### 6. AI 搜索(流式) ```typescript 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) 对候选文档按照与查询的相关性重新排序: ```typescript 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 中的完整示例 ```typescript // 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` 单独引入