《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 21:LCEL(
-
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 基础设施:
useChathook、通用消息模型、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,用这套前端壳子开始快速迭代。

浙公网安备 33010602011771号