《60天AI学习计划启动 | Day 20–30 总结(前端 + AI 方向)》

一、Day 20–30 总结(前端 + AI 方向)

  • Day 20 项目总结

    • 把前面所有能力整合成一个完整 AI 应用:聊天 + RAG + Function Calling + Agent
    • 建立自己的知识体系图:基础(LLM/Prompt)→ RAG → Agent → 工程化(性能/监控/部署)
  • Day 21–24:RAG & LangChain 进阶

    • Day 21:LCEL(RunnableSequence/Parallel)+ 结构化输出,链路声明式组合
    • Day 22:RAG 检索优化:chunk 策略、上下文压缩、多向量/Hybrid Search
    • Day 23:约束式回答 + 引用标注 + 自检链,减少幻觉
    • Day 24:RAG 评估与自动化测试:基准集 + 脚本 + LLM 评分器,防回归
  • Day 25–27:前端 UX + 安全 + 工具设计

    • Day 25:AI 聊天 UX:流式滚动、错误/中断态、历史会话、快速问题引导
    • Day 26:安全 & 权限:Prompt 注入防护、多租户过滤、日志脱敏、输出 XSS 防护
    • Day 27:Function Calling 最佳实践:函数 Schema 设计、两阶段调用(LLM 决策 + 后端执行)
  • Day 28–30:数据/多模态 + 前端基础设施

    • Day 28:文档接入流水线:解析 → 清洗 → 切片 → 打标签 → 入向量库
    • Day 29:结构化数据/报表问答(用 Demo 电商):语义层(指标/维度/过滤),自然语言 → 查询 JSON
    • Day 30:前端 AI 基础设施:useChat hook、通用消息模型、ChatWindow 组件、多后端可插拔

二、前端 + AI 通用 Demo(React + TS,涵盖 20–30 天的关键点)

说明:

  • 假设已有后端接口 POST /api/chat,支持流式返回 text/event-stream,只要按 payload 约定即可;
  • 代码重点在 通用前端封装:消息模型、useChat、流式处理、多模态入口位、错误/中断。
// 简化版 Demo:React + TS,前端通用 AI Chat 封装

import React, { useState, useRef, useCallback, useEffect } from 'react'

type Role = 'user' | 'assistant' | 'system'
type MessageStatus = 'pending' | 'streaming' | 'done' | 'error'

interface Message {
  id: string
  role: Role
  content: string
  createdAt: number
  status?: MessageStatus
  // 结构化元信息:可用于 RAG 引用 / 报表解释等
  meta?: {
    citations?: Array<{ index: number; snippet: string }>
    toolCalled?: string
  }
}

interface ChatOptions {
  apiUrl: string
}

interface ChatError {
  type: 'network' | 'timeout' | 'server' | 'biz'
  message: string
}

function useChat({ apiUrl }: ChatOptions) {
  const [messages, setMessages] = useState<Message[]>([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<ChatError | null>(null)
  const controllerRef = useRef<AbortController | null>(null)

  const sendMessage = useCallback(
    async (text: string, files?: File[]) => {
      if (!text.trim() || loading) return

      setError(null)

      const userMsg: Message = {
        id: crypto.randomUUID(),
        role: 'user',
        content: text.trim(),
        createdAt: Date.now()
      }
      const aiMsgId = crypto.randomUUID()
      const aiMsg: Message = {
        id: aiMsgId,
        role: 'assistant',
        content: '',
        createdAt: Date.now(),
        status: 'streaming'
      }

      setMessages((prev) => [...prev, userMsg, aiMsg])
      setLoading(true)

      const ctrl = new AbortController()
      controllerRef.current = ctrl

      try {
        // 构造 payload:支持多模态 / 结构化上下文
        const payload = {
          messages: [...messages, userMsg].map((m) => ({
            role: m.role,
            content: m.content
          })),
          files: files?.map((f) => ({ name: f.name, type: f.type })) ?? []
        }

        const res = await fetch(apiUrl, {
          method: 'POST',
          body: JSON.stringify(payload),
          headers: { 'Content-Type': 'application/json' },
          signal: ctrl.signal
        })

        if (!res.ok || !res.body) {
          throw new Error(`Server error: ${res.status}`)
        }

        const reader = res.body.getReader()
        const decoder = new TextDecoder()
        let done = false
        let buffer = ''

        while (!done) {
          const chunk = await reader.read()
          done = chunk.done
          if (chunk.value) {
            buffer += decoder.decode(chunk.value, { stream: true })
            // 假设服务端用 SSE:以 \n\n 分隔 data: JSON
            const parts = buffer.split('\n\n')
            buffer = parts.pop() || ''

            for (const part of parts) {
              const line = part.trim()
              if (!line.startsWith('data:')) continue
              const jsonStr = line.slice(5).trim()
              if (!jsonStr || jsonStr === '[DONE]') continue
              const data = JSON.parse(jsonStr) as {
                type: 'delta' | 'final' | 'error'
                content?: string
                citations?: Message['meta']['citations']
                error?: string
              }

              setMessages((prev) =>
                prev.map((m) =>
                  m.id === aiMsgId
                    ? {
                        ...m,
                        content: data.content ? m.content + data.content : m.content,
                        status:
                          data.type === 'final'
                            ? 'done'
                            : data.type === 'error'
                            ? 'error'
                            : 'streaming',
                        meta: data.citations ? { citations: data.citations } : m.meta
                      }
                    : m
                )
              )

              if (data.type === 'error' && data.error) {
                setError({ type: 'server', message: data.error })
              }
            }
          }
        }
      } catch (e: any) {
        if (e.name === 'AbortError') {
          // 用户主动中断,不当成错误
        } else if (e.message?.includes('Network')) {
          setError({ type: 'network', message: '网络异常,请稍后重试' })
        } else {
          setError({ type: 'server', message: e.message ?? '服务异常' })
        }
        setMessages((prev) =>
          prev.map((m) =>
            m.id === aiMsgId ? { ...m, status: 'error' } : m
          )
        )
      } finally {
        setLoading(false)
        controllerRef.current = null
      }
    },
    [apiUrl, loading, messages]
  )

  const abort = useCallback(() => {
    controllerRef.current?.abort()
  }, [])

  const retryLast = useCallback(() => {
    const lastUser = [...messages].reverse().find((m) => m.role === 'user')
    if (lastUser) sendMessage(lastUser.content)
  }, [messages, sendMessage])

  return { messages, loading, error, sendMessage, abort, retryLast }
}

// 非常简化的 Chat 组件 Demo
export function ChatWindow() {
  const { messages, loading, error, sendMessage, abort, retryLast } = useChat({
    apiUrl: '/api/chat'
  })
  const [input, setInput] = useState('')
  const fileRef = useRef<HTMLInputElement | null>(null)
  const filesRef = useRef<File[]>([])

  const handleSend = () => {
    const files = filesRef.current
    filesRef.current = []
    setInput('')
    sendMessage(input, files)
    if (fileRef.current) fileRef.current.value = ''
  }

  return (
    <div className="chat-root">
      <div className="messages">
        {messages.map((m) => (
          <div key={m.id} className={`msg ${m.role}`}>
            <div className="content">{m.content}</div>
            {m.meta?.citations && (
              <div className="citations">
                {m.meta.citations.map((c) => `[${c.index}]`).join(' ')}
              </div>
            )}
          </div>
        ))}
        {loading && <div className="msg assistant">AI 正在思考中...</div>}
      </div>

      {error && (
        <div className="error-bar">
          {error.message}
          <button onClick={retryLast}>重试</button>
        </div>
      )}

      <div className="input-bar">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
              e.preventDefault()
              handleSend()
            }
          }}
          placeholder="请输入问题,Enter 发送,Shift+Enter 换行"
        />
        <input
          type="file"
          multiple
          ref={fileRef}
          onChange={(e) => {
            filesRef.current = Array.from(e.target.files || [])
          }}
        />
        <button onClick={handleSend} disabled={loading || !input.trim()}>
          发送
        </button>
        {loading && <button onClick={abort}>中断</button>}
      </div>
    </div>
  )
}

上面这个 Demo 把 20–30 天的几个关键点都串起来了:

  • 通用 Message 模型 + useChat 抽象(Day 20/30)
  • 流式处理、错误/中断(Day 21/25)
  • 预留 meta.citations 承载 RAG 引用(Day 22/23)
  • 支持多模态文件入口(Day 28/31)
  • 通过统一 error 结构做 UX & 日志(Day 24/26)

你可以在任何项目里直接接一个自己的 /api/chat,用这套前端壳子开始快速迭代。

posted @ 2025-12-17 09:47  XiaoZhengTou  阅读(1)  评论(0)    收藏  举报