当前位置: 首页 > news >正文

《60天AI学习计划启动 | Day 40: 前端 AI SDK 抽象(aiClient + hooks)》

Day 40:前端 AI SDK 抽象(aiClient + hooks)

学习目标

  • 抽象 一套通用的 aiClient 接口(不用关心具体后端实现细节)
  • 封装 常用 hooks:useChat(非流式)、useStreamingChat(流式)
  • 为后面 在任何项目中快速接 AI 打基础

核心知识点

  • aiClient 抽象

    • 核心思想:前端只依赖一个统一客户端,不直接散落 fetch('/api/xxx')
    • 可以定义接口:
      interface AIClient {chat(payload: ChatRequest): Promise<ChatResponse>streamChat(payload: ChatRequest): AsyncIterable<ChatChunk>
      }
      
    • 具体实现可以对接不同后端(自家服务 / OpenAI / 其他网关)
  • hooks 层

    • useChat(client):一次性请求,适合短回答、非流式场景
    • useStreamingChat(client):基于 AsyncIterable 或 SSE 读流,适合聊天页面

实战作业(附完整代码)

作业 1:定义 AIClient 接口 + 一个 HTTP 实现

// aiClient.ts
export interface ChatMessage {role: 'system' | 'user' | 'assistant'content: string
}export interface ChatRequest {messages: ChatMessage[]meta?: Record<string, any>
}export interface ChatResponse {answer: stringusage?: {promptTokens?: numbercompletionTokens?: numbertotalTokens?: number}
}export interface ChatChunk {type: 'delta' | 'final' | 'error'content?: stringerror?: string
}export interface AIClient {chat(req: ChatRequest): Promise<ChatResponse>streamChat(req: ChatRequest): AsyncIterable<ChatChunk>
}// 一个基于 HTTP 的简单实现(假设后端有 /api/chat 和 /api/chat/stream)
export class HttpAIClient implements AIClient {constructor(private baseUrl = '') {}async chat(req: ChatRequest): Promise<ChatResponse> {const res = await fetch(this.baseUrl + '/api/chat', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify(req)})if (!res.ok) throw new Error(`HTTP ${res.status}`)const data = await res.json()return {answer: data.answer ?? '',usage: data.usage}}async *streamChat(req: ChatRequest): AsyncIterable<ChatChunk> {const res = await fetch(this.baseUrl + '/api/chat/stream', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify(req)})if (!res.body) throw new Error('No response body')const reader = res.body.getReader()const decoder = new TextDecoder()let done = falselet buffer = ''while (!done) {const chunk = await reader.read()done = chunk.doneif (chunk.value) {buffer += decoder.decode(chunk.value, { stream: true })const parts = buffer.split('\n\n')buffer = parts.pop() || ''for (const part of parts) {const line = part.trim()if (!line.startsWith('data:')) continueconst jsonStr = line.slice(5).trim()if (!jsonStr || jsonStr === '[DONE]') continueconst data = JSON.parse(jsonStr) as ChatChunkyield data}}}}
}

作业 2:useChat(非流式)hook

// useChat.ts
import { useState, useCallback } from 'react'
import type { AIClient, ChatMessage } from './aiClient'interface UseChatOptions {client: AIClientinitialMessages?: ChatMessage[]
}export function useChat({ client, initialMessages = [] }: UseChatOptions) {const [messages, setMessages] = useState<ChatMessage[]>(initialMessages)const [loading, setLoading] = useState(false)const [error, setError] = useState<string | null>(null)const send = useCallback(async (content: string) => {const text = content.trim()if (!text || loading) returnsetError(null)const userMsg: ChatMessage = { role: 'user', content: text }const newMessages = [...messages, userMsg]setMessages(newMessages)setLoading(true)try {const res = await client.chat({ messages: newMessages })const aiMsg: ChatMessage = {role: 'assistant',content: res.answer}setMessages((prev) => [...prev, aiMsg])} catch (e: any) {setError(e?.message || '请求失败')} finally {setLoading(false)}},[client, messages, loading])return { messages, loading, error, send }
}

作业 3:useStreamingChat hook(基于 streamChat

// useStreamingChat.ts
import { useState, useCallback, useRef } from 'react'
import type { AIClient, ChatMessage, ChatChunk } from './aiClient'interface UseStreamingChatOptions {client: AIClientinitialMessages?: ChatMessage[]
}export function useStreamingChat({client,initialMessages = []
}: UseStreamingChatOptions) {const [messages, setMessages] = useState<ChatMessage[]>(initialMessages)const [loading, setLoading] = useState(false)const [error, setError] = useState<string | null>(null)const abortRef = useRef<AbortController | null>(null)const send = useCallback(async (content: string) => {const text = content.trim()if (!text || loading) returnsetError(null)const userMsg: ChatMessage = { role: 'user', content: text }const baseMessages = [...messages, userMsg]setMessages(baseMessages)const aiMsgId = Symbol('aiMsg') // 本地标记let currentAI: ChatMessage = { role: 'assistant', content: '' }setMessages((prev) => [...prev, currentAI])setLoading(true)try {// 不使用 AbortController 控制 fetch 本身,因为 AsyncIterable 内部已封装for await (const chunk of client.streamChat({ messages: baseMessages })) {if (chunk.type === 'delta' && chunk.content) {currentAI = {...currentAI,content: currentAI.content + chunk.content}setMessages((prev) => {const next = [...prev]next[next.length - 1] = currentAIreturn next})} else if (chunk.type === 'error') {throw new Error(chunk.error || '流式错误')}}} catch (e: any) {setError(e?.message || '流式请求失败')} finally {setLoading(false)abortRef.current = null}},[client, messages, loading])return { messages, loading, error, send }
}

明日学习计划预告(Day 41)

  • 主题:LangChain 复杂 Chain(Router / Parallel / Map-Reduce)
  • 方向
    • 用 Router Chain 按“问答/代码/报表”路由到不同链路
    • 前端只需要感知一个统一的 /smart-chat 接口,由后端 Chain 决定内部流程
http://www.jsqmd.com/news/104179/

相关文章:

  • AI对比:传统刷题与智能生成Flutter面试准备
  • Spring新手必看:5步搞定Bean初始化失败的简单教程
  • 固件升级时fd一直增加,升级十几次后crash
  • AI如何自动清理Windows系统垃圾文件
  • 2025长沙美甲美睫培训学校TOP5权威推荐:速成班费用与优 - myqiye
  • 基于CentOS 9的快速开发环境搭建指南
  • CentOS 9在企业级Web服务中的实战部署
  • 2025年铝箔翅片定制厂家权威推荐榜单:铝防爆箔/亲水铝箔/铝翅片源头厂家精选 - 品牌推荐官
  • 2025年内衬不锈钢复合管品牌推荐:新澎内衬不锈钢复合管质量 - mypinpai
  • 事倍功半是蠢蛋69 TODO
  • 用Packet Tracer快速验证网络架构设计的3种方法
  • 企业级Spring Boot项目中的AutoConfiguration.imports实战
  • JookDB在电商平台中的实战应用案例
  • 《60天AI学习计划启动 | Day 38: 多会话 多 Tab 同步(前端层)》
  • 传统网页存档vs互联网档案馆:效率对比分析
  • 如何用AI一键生成Jupyter Notebook环境配置脚本
  • 企业级Python环境部署实战 - 官方源的正确使用
  • CMHHP实战:构建智能慢性病管理系统
  • 3步快速解决Windows安装配置错误
  • Promise.js在电商网站支付流程中的实战应用
  • 2025年年终伺服压机推荐:从技术参数到服务生态的全方位横评,附5款高适配性型号清单 - 品牌推荐
  • 工程优化- Tenacity库用于重试机制及使用示例
  • AI如何解决PDF预览难题?快马平台一键生成解决方案
  • torch.where vs numpy.where:性能对比全解析
  • 传统VS现代:EasyPlayer开发效率提升300%的秘密
  • EmotiVoice与语音唤醒技术结合的智能家居场景设想
  • 效率翻倍:一键切换工作/娱乐分辨率方案
  • 2025年无人机探测设备制造企业权威推荐榜单:无人机反制模块/无人机侦测反制设备/无人机管制设备源头厂家精选 - 品牌推荐官
  • 1小时搞定:用await快速开发天气查询CLI工具
  • LobeChat节日营销专题页内容策划