面试表达
面试时怎么讲
一句话:我围绕「AI 前端交互与流式体验(SSE / Vercel AI SDK / Tool UI / 引用)」做过从问题拆解、方案设计、工程落地到效果验证的闭环。
追问重点:为什么这么设计、指标怎么验证、失败案例如何定位、上线后怎么观测和回滚。
证据建议:准备 README、架构图、关键代码片段、评测表和 2 个坏例复盘。
把"后端 AI 能力"变成"用户愿意付钱的产品":Streaming UI、Tool 调用过程可视化、引用与追溯、ECharts 报告页、取消/重试/错误。
本地学习进度
只保存在当前浏览器,不上传服务器。
学习产出
实战交付,P1。阅读时尽量把知识点转成可演示项目、面试回答和简历证据。
能回答
课程目标(14 天) / 技术选型总览 / 推送方式四象限
能交付
整理一页项目笔记,记录问题、方案、指标和取舍
下一步
把本篇总结成 90 秒面试讲述
面试表达
一句话:我围绕「AI 前端交互与流式体验(SSE / Vercel AI SDK / Tool UI / 引用)」做过从问题拆解、方案设计、工程落地到效果验证的闭环。
追问重点:为什么这么设计、指标怎么验证、失败案例如何定位、上线后怎么观测和回滚。
证据建议:准备 README、架构图、关键代码片段、评测表和 2 个坏例复盘。
简历表达
围绕 AI 前端交互与流式体验(SSE / Vercel AI SDK / Tool UI / 引用) 场景,完成「把"后端 AI 能力"变成"用户愿意付钱的产品":Streaming UI、Tool 调用过程可视化、引用与追溯、ECharts 报告页、取消/重试/错误。」相关能力建设,沉淀可复用工程方案、测试样本和面试表达材料。
读完本课你能:
| 能力 | 推荐 | 备注 |
|---|---|---|
| 框架 | Next.js 15 App Router + React 19 | RSC + Server Actions 原生支持流 |
| AI SDK | Vercel AI SDK v4 | streamText、useChat、useObject |
| 传输 | SSE(首选) | 单向、HTTP 友好、断线重连好做 |
| 可选 | WebSocket | 双向实时(语音/白板) |
| 渲染 | react-markdown + rehype | 流式 Markdown + 代码高亮 |
| 图表 | ECharts 6(或 Recharts) | 配合后端 JSON spec |
| 状态 | Zustand / React Query | 会话列表 + 断线恢复 |
服务端推 双向实时
┌──────────────────┬──────────────────┐
HTTP 友好 │ SSE (text/event) │ WebSocket │
│ 首选,兼容 CDN │ 语音、协同白板 │
├──────────────────┼──────────────────┤
特殊 │ Chunked HTTP │ WebTransport │
│ (fetch + reader) │ (前沿) │
└──────────────────┴──────────────────┘后端(App Router Route Handler):
// app/api/chat/route.ts
export async function POST(req: Request) {
const { prompt } = await req.json();
const stream = new ReadableStream({
async start(ctrl) {
for await (const token of callLLM(prompt)) {
ctrl.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ t: token })}\n\n`));
}
ctrl.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
ctrl.close();
},
});
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
}前端:
"use client";
const [text, setText] = useState("");
const res = await fetch("/api/chat", { method: "POST", body });
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解析 SSE event line
decoder.decode(value, { stream: true }).split("\n\n").forEach(parseEvent);
}// 前端
const { messages, input, handleSubmit, isLoading, stop } = useChat({ api: "/api/chat" });
// 后端
return streamText({ model: openai("gpt-4o"), messages }).toDataStreamResponse();useChat 状态机、stop() 取消、错误边界。ReactMarkdown 每 token 重解析造成卡顿。react-markdown + remark-gfm,用 memo。react-syntax-highlighter 的 light build,懒加载语言。scrollIntoView({block:"end"}) 节流 50ms。后端 emit 多种事件:
{"type":"tool_call","name":"search_docs","args":{"q":"..."}}
{"type":"tool_result","name":"search_docs","latency":820,"hits":5}
{"type":"token","delta":"根据检索..."}
{"type":"citation","docs":[{"id":"d12","title":"..."}]}前端用一个 Timeline 组件分段展示:
⏳ search_docs(q="向量检索") ... 820ms · 5 条结果
🧠 生成中:根据检索,向量检索通常……
📎 引用:[向量数据库入门](/docs/d12)[[d12:para3]] 占位,前端解析成脚注。AbortController,同时服务端收到 req.signal.aborted 停 tool。id: <checkpoint>,重连时带 Last-Event-ID 让后端从 checkpoint 继续(需要 agent-state-persistence-hitl 支持)。loading.tsx 骨架屏。场景:自然语言查数 → 后端返回 JSON spec → 前端渲染图表 + 解读。
const option = useMemo(() => buildECharts(resp.chart), [resp]);
<ReactECharts option={option} style={{ height: 320 }} />
<p className="mt-3 text-sm text-zinc-600">{resp.explanation}</p>next/dynamic({ ssr: false })。streamObject + zod schema 直接流式产出 JSON(图表参数、表单填充)。useObject({ schema }) 增量渲染局部字段。threads, activeThreadId, messagesByThread。serverMessageId,用于后端 checkpoint 对齐。persist 中间件 + localStorage。<ErrorBoundary> 包住每个工具卡与渲染器。DONE_ERR → 保留已输出 + 小字提示。trace_id 暴露给用户,方便客服查询。aria-live="polite" 流式区域,屏幕阅读器不连续打断。prefers-reduced-motion 关动画。react-virtuoso 虚拟滚动,避免 DOM 膨胀。interrupt,前端收到 type: "interrupt" 事件 → 渲染审批卡:动作、参数、影响面、批准/拒绝按钮。POST /approve → 后端 resume(thread_id, decision)。type AgentEvent =
| { type: "token"; delta: string }
| { type: "tool_call"; id: string; name: string; args: unknown }
| { type: "tool_result"; id: string; ok: boolean; summary: string; latencyMs: number }
| { type: "citation"; refs: { docId: string; para: number; title: string }[] }
| { type: "chart"; spec: EChartsOption; explanation: string }
| { type: "interrupt"; payload: unknown }
| { type: "error"; code: string; message: string; traceId: string }
| { type: "done"; totalTokens: number; cost: number };type 分发到不同渲染器,天然可扩展。tool_result.ok=false,红色 + "重试"按钮;不要让错误静默吞掉。req.signal.aborted 级联传给 LLM provider 和 tool;LangGraph 用 RunnableConfig.cancel。application/json 导致中间件缓冲——必须 text/event-stream 并禁用 buffering。proxy_buffering on,流被缓;需 proxy_buffering off; proxy_read_timeout 600s;。useChat 把 messages 直接对象比较,漏掉增量更新。tool_result 再渲染。aria-live,a11y 审核不过。app/(chat)/...(Next.js 15 聊天外壳)components/AgentTimeline.tsxcomponents/CitationPopover.tsxcomponents/ReportCard.tsx(ECharts)components/ApprovalCard.tsxlib/agent-event.ts(协议类型 + 解析器)| 级别 | 资源 |
|---|---|
| A | Vercel AI SDK 文档 |
| A | Next.js 15 Streaming & Suspense |
| A | React 19 use() & Suspense |
| A | MDN SSE(EventSource) |
| A | ECharts 官方 |
| B | LangGraph Streaming Guide(Python 侧) |
| B | Virtuoso / TanStack Virtual 虚拟滚动 |
与 ai-data-analysis-system、agent-state-persistence-hitl、model-calling-and-prompt-engineering 联动。