当前位置: 首页 > 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.zskr.cn/news/115003.html

相关文章:

  • AI对比:传统刷题与智能生成Flutter面试准备
  • 固件升级时fd一直增加,升级十几次后crash
  • 5分钟用MySQL存储过程搭建业务逻辑原型
  • 基于CentOS 9的快速开发环境搭建指南
  • MySQL 中 COUNT (*) 与 COUNT (col) 区别
  • 企业级Spring Boot项目中的AutoConfiguration.imports实战
  • JookDB在电商平台中的实战应用案例
  • 《60天AI学习计划启动 | Day 38: 多会话 多 Tab 同步(前端层)》
  • 传统网页存档vs互联网档案馆:效率对比分析
  • 企业级Python环境部署实战 - 官方源的正确使用
  • Promise.js在电商网站支付流程中的实战应用
  • 2025年年终伺服压机推荐:从技术参数到服务生态的全方位横评,附5款高适配性型号清单 - 品牌推荐
  • torch.where vs numpy.where:性能对比全解析
  • 效率翻倍:一键切换工作/娱乐分辨率方案
  • 2025年无人机探测设备制造企业权威推荐榜单:无人机反制模块/无人机侦测反制设备/无人机管制设备源头厂家精选 - 品牌推荐官
  • 1小时搞定:用await快速开发天气查询CLI工具
  • LobeChat节日营销专题页内容策划
  • 2025年沥青搅拌设备源头厂家权威推荐榜单:沥青搅拌站/温拌泡沫沥青设备/沥青混凝土搅拌站源头厂家精选 - 品牌推荐官
  • 低成本打造专属声优!EmotiVoice声音克隆实测分享
  • 2025年12月电线/防火/控制电缆厂家推荐指南:五家企业实力铸就品质之选 - 深度智识库
  • 2025年高精度大理石量具品牌推荐:大理石量具角尺靠谱厂商有 - mypinpai
  • 50、Perl函数详解:MRO、多调用、数值及兼容性函数
  • 企业级Maven项目部署问题实战解析
  • GB/T 40363-2021 硬质聚氨酯泡沫塑料检测
  • 解决mapper.xml中SQL语句不提示的问题
  • 打破语音合成单调性:EmotiVoice带来情绪多样性
  • 效率对比:传统排查vsAI解决Yarn问题耗时实验
  • 语音合成个性化设置:保存常用音色模板功能
  • ESP32 FreeRTOS任务管理大全:概念、实现、优化与调试的一站式学习手册
  • 2025AAAI-DivShift: Exploring Domain-Specific Distribution Shift in Large-Scale, Volunteer-Collected