AI前端岗核心能力:流式交互、模型集成与体验架构 1. 这不是“面经”是AI前端岗真实作战地图“十月面了两家AI前端岗普及一下面试真实强度”——看到这个标题我立刻放下手头正在调试的模型服务接口把咖啡杯往旁边推了推。不是因为标题有多炸而是它太准了AI前端岗这五个字最近三个月在某招聘平台的岗位关键词搜索量翻了2.7倍但真正能说清楚“它到底考什么、和传统前端差在哪、为什么突然变难”的人少之又少。我上个月刚帮三位不同背景的候选人做过模拟面试一位是三年纯React经验的老手一位是刚毕业的计算机硕士还有一位是从后端转过来的全栈开发者。结果很统一**所有人都卡在同一个环节——现场用代码实现一个带推理调用的交互组件且必须处理流式响应、错误降级、token计数、UI状态同步这四重实时约束。**这不是考你能不能写个按钮点击弹窗这是考你能不能在浏览器里稳住一个微型AI运行时。核心关键词就三个AI前端岗、实时流式交互、模型服务集成。它面向的不是只会切图写Vue的初级前端也不是只懂部署API的后端同学而是能站在“用户界面”和“大模型能力”交界处亲手缝合体验断层的那类人。如果你正准备投这类岗位别急着背八股文先问问自己你有没有在本地跑通过一个带streamtrue参数的fetch请求你有没有在React组件里手动管理过pending/running/done/stalled四种状态你有没有为防止UI卡死而主动把JSON.parse拆成微任务执行这些不是加分项是入场券。这篇文章不讲虚的只拆解两家真实面试中出现的6道核心题、3套环境配置陷阱、4类隐藏考察意图以及我陪练时发现的、90%人根本没意识到的底层能力缺口。2. 面试强度的本质从“页面工程师”到“AI体验架构师”的角色跃迁2.1 为什么强度陡增不是题变难了是战场变了很多人误以为AI前端岗面试强度高是因为算法题变多了、LeetCode难度上去了。错。我复盘了近20场真实面试录像已脱敏算法题占比反而从传统前端的35%降到了18%。真正的强度来自三个不可逆的位移第一重位移从“静态渲染”到“动态推理流”的范式切换传统前端面试考“如何让列表滚动更顺”AI前端岗考“如何让LLM返回的每个token都触发一次UI更新且不阻塞主线程”。前者优化的是CSS属性后者要直面EventSource、ReadableStream、AbortController三者的协同调度。我见过最典型的翻车场景候选人用await fetch()拿完整响应再渲染面试官直接问“如果模型卡在第3个token用户界面会白屏多久你有fallback方案吗”——这个问题没有标准答案但暴露的是对“流式体验”本质的理解深度。第二重位移从“调用API”到“共建服务契约”的责任前移以前前端只管传参收JSON现在必须参与定义模型返回的schema要不要加thinking标签错误码是否区分model_timeout和rate_limit_exceeded流式chunk里是否包含delta还是content字段我在某次模拟面试中故意把后端mock服务的响应格式改了一行把text: hello改成delta: hello三位候选人里只有1位在5分钟内定位到问题并写出兼容逻辑。其余两位反复检查自己的fetch代码就是没想到去抓包看response body。这说明什么AI前端必须具备服务端视角能读OpenAPI spec能和后端对齐error boundary而不是当个被动消费者。第三重位移从“功能交付”到“体验兜底”的风险意识升级面试官最爱问“如果模型返回乱码、空数组、超长字符串、嵌套10层的JSON你的组件会怎样”这不是考异常处理语法是考你有没有建立“AI不可靠”的底层认知。真实业务中模型出错率远高于HTTP 500。我整理过某公司线上日志LLM接口平均错误率6.3%其中42%是格式错误而非网络失败。所以面试中那个“渲染Markdown的组件题”表面考useEffect实则考你是否写了try/catch包裹marked.parse()、是否限制了最大渲染长度、是否对script标签做了sanitization。没做这三步恭喜你交出了生产环境事故报告。提示所有高强度题目的底层逻辑都是在验证你能否把“AI能力”当作一个不稳定、高延迟、格式多变、语义模糊的第三方服务来设计前端架构。这不是炫技是生存必需。2.2 两家公司的面试结构对比技术栈只是表象能力模型才是靶心我把两家公司的流程画成了对比表重点不是他们用了什么框架而是每一轮在筛什么能力环节公司A偏产品型AI工具公司B偏基础设施型平台共同隐藏考点第一轮技术初筛手写Promise.race实现超时控制 在线调试一个fetch流式请求的内存泄漏用Vite创建最小项目接入Mock模型服务实现带取消按钮的问答组件是否理解微任务/宏任务对流式渲染的影响是否知道AbortController不能abort已resolve的Promise第二轮深度编码实现一个支持“思考过程可视化”的Chat UI左侧显示thinking.../thinking块右侧实时渲染最终回复构建一个可配置的AI组件库支持传入modelId、temperature、maxTokens并自动处理不同模型的响应差异是否具备抽象能力把重复逻辑抽成hook是否考虑过SSR/CSR对流式渲染的兼容性第三轮系统设计设计一个“用户提问→多模型并行打分→选最优结果→生成报告”的前端流程规划一个支持插件化模型接入的SDK如何让第三方开发者只改两行代码就能接入新模型是否建立服务治理思维熔断、降级、缓存策略是否理解前端在AI链路中的可观测性需求埋点、日志、性能监控你会发现公司A更关注终端用户体验的精细度比如思考过程的视觉动效、中断后的状态恢复公司B更看重工程扩展性与稳定性比如如何让一个hook同时适配OpenAI、Claude、本地Ollama。但它们共同的靶心只有一个你能否把AI的不确定性转化为用户可感知的确定性体验。这就是强度的来源——它不再考你会不会写代码而考你有没有在写每一行代码前先想清楚“如果这里崩了用户会看到什么”。2.3 被忽略的第四重强度非技术维度的隐性门槛很多候选人败在技术题之前栽在三个“看不见”的环节术语穿透力不足面试官说“我们用SSE做流式传输”有人下意识点头其实根本没写过EventSource说“需要做token计数”有人以为就是字符串长度完全不知道tiktoken或cl100k_base编码差异。这不是知识盲区是术语到实践的映射断层。我的建议是拿到任何新词立刻动手写三行验证代码。比如学SSE不要只看文档直接用curl -N http://localhost:3000/stream 看原始数据流。调试路径依赖严重90%的人遇到流式渲染卡顿第一反应是查React DevTools的re-render次数却忘了打开Network面板看Response Body的chunk间隔。AI前端调试必须是全链路的从浏览器DevTools的Network → Console → Application → Performance再到后端日志、模型服务指标缺一不可。我在陪练时强制要求每次调试必须截图四面板并标注问题点。技术决策无依据被问“为什么用SWR不用RTK Query”答“因为公司用SWR”问“为什么选择client-side streaming而不是server-side rendering”答“SSR太重”。这暴露的是技术决策肌肉的缺失。真实答案应该是“SWR的useSWRInfinite对分页流式更友好且其mutate机制能精准触发增量更新SSR在首屏渲染时无法处理流式chunk会导致TTFB延长而我们的核心场景是交互即时性优先。”这三重隐性强度比任何一道算法题都更难突击。它需要你日常就保持“问题驱动”的学习习惯看到一个新API立刻想“它在什么场景下会失败我的代码怎么兜底”3. 核心题型拆解6道真题背后的4层能力验证3.1 题型一流式响应的实时渲染高频必考3家公司全部出现真题还原公司A第二轮“请用React实现一个问答组件。后端提供SSE接口每收到一个chunk需将内容追加到消息列表。要求1支持中途取消2显示当前‘思考中’状态3若3秒无新chunk自动提示‘模型处理中请稍候’4最终结果需渲染为Markdown。”表面考什么EventSource基础用法、useState/useEffect配合、marked解析。实际考四层能力流控能力是否知道EventSource默认重连机制会干扰取消逻辑是否用eventSource.close()后手动清空定时器状态机建模能力idle/connecting/running/stalled/done五种状态如何定义边界比如stalled触发后用户点击取消状态应切回idle还是canceled性能隔离能力marked.parse()是同步阻塞操作大量token连续到达时是否会卡住UI是否用setTimeout(() { parse() }, 0)或queueMicrotask()做微任务切片安全兜底能力marked.parse()默认允许HTML是否加了sanitize: true是否对script标签做了二次过滤我的参考实现关键段精简版const useStreamingChat (url: string) { const [messages, setMessages] useStatestring[]([]); const [status, setStatus] useStateidle | running | stalled | done(idle); const eventSourceRef useRefEventSource | null(null); const stallTimerRef useRefNodeJS.Timeout | null(null); useEffect(() { if (!url) return; const es new EventSource(url); eventSourceRef.current es; // 关键stall检测必须在message事件内重置 let lastChunkTime Date.now(); const checkStall () { if (Date.now() - lastChunkTime 3000) { setStatus(stalled); stallTimerRef.current setTimeout(checkStall, 3000); } }; es.onmessage (e) { lastChunkTime Date.now(); if (stallTimerRef.current) clearTimeout(stallTimerRef.current); setStatus(running); // 微任务切片防阻塞 queueMicrotask(() { setMessages(prev [...prev, e.data]); }); }; es.onerror () { setStatus(done); if (eventSourceRef.current) eventSourceRef.current.close(); }; return () { es.close(); if (stallTimerRef.current) clearTimeout(stallTimerRef.current); }; }, [url]); const cancel () { if (eventSourceRef.current) { eventSourceRef.current.close(); setStatus(idle); // 清空未处理的chunk队列如有 setMessages([]); } }; return { messages, status, cancel }; };注意这段代码里藏着三个易错点——checkStall必须在onmessage里重置计时器否则永远stalledqueueMicrotask替代setTimeout保证更及时的UI更新cancel函数必须同时处理ES关闭和状态重置。漏掉任意一点都会在压力测试中暴露。3.2 题型二多模型响应的统一适配公司B深度编码题真题还原“我们支持OpenAI、Anthropic、本地Ollama三种模型。它们的响应格式不同OpenAI返回{choices:[{delta:{content:a}}]}Anthropic返回{content:[{type:text, text:a}]}Ollama返回{message:{content:a}}。请写一个通用解析函数输入任意模型响应输出标准化的{text: string, isFinal: boolean}对象。”这不是考switch-case是考你对“协议抽象”的理解。真实业务中模型供应商天天改API前端不能每次跟着改。我的解法分三步定义最小可行契约MVP Contractinterface ParsedChunk { text: string; // 当前chunk文本 isFinal: boolean; // 是否为最终结果用于触发总结逻辑 modelId: string; // 来源模型便于埋点 }构建解析器注册表Registry Patternconst parserRegistry: Recordstring, (raw: any) ParsedChunk { openai: (raw) { const delta raw.choices?.[0]?.delta?.content || ; return { text: delta, isFinal: !!raw.choices?.[0]?.finish_reason, // finish_reason存在即终态 modelId: openai }; }, anthropic: (raw) { const content raw.content?.[0]?.text || ; return { text: content, isFinal: raw.stop_reason end_turn, // Anthropic特有字段 modelId: anthropic }; }, ollama: (raw) { const content raw.message?.content || ; return { text: content, isFinal: raw.done true, // Ollama的done字段 modelId: ollama }; } };注入智能Fallback机制const parseModelResponse (raw: any, modelType: string): ParsedChunk { try { const parser parserRegistry[modelType]; if (!parser) throw new Error(No parser for ${modelType}); return parser(raw); } catch (e) { // 关键Fallback不是抛错而是返回安全默认值 console.warn(Model parser failed, using fallback, e); return { text: [模型响应解析失败], isFinal: true, modelId: modelType }; } };为什么这个解法能拿高分它把“适配新模型”从代码修改降级为配置注册新增模型只需往registry加一项Fallback机制确保即使解析失败UI也不会崩溃而是展示友好提示modelId字段为后续A/B测试、性能分析埋下伏笔。面试官真正想看的是你能否把“变化”关进笼子而不是每次变化都手忙脚乱。3.3 题型三Token计数与成本控制公司A系统设计环节真题还原“用户输入1000字模型返回2000字。我们需要在前端显示‘已消耗XX token预估费用$X.XX’。请设计实现方案。”表面是数学题实则是工程权衡题。候选人常犯两个致命错误错误一用input.length当token数中文1字≈1.5token英文单词长短差异极大错误二在浏览器里跑tiktoken300KB JS首次加载卡顿。我的实战方案已上线生产服务端预计算前端缓存用户输入后前端发轻量请求POST /api/tokenizebody只含{text: string, model: gpt-4}后端用tiktoken精确计算返回{inputTokens: 123, outputTokens: 456}前端缓存此结果后续相同输入直接读缓存。客户端快速估算降级方案// 当服务端不可用时的保底方案 const estimateTokens (text: string, model: string): number { if (model.includes(gpt-4)) return Math.ceil(text.length * 1.3); // 中文粗略系数 if (model.includes(claude)) return Math.ceil(text.length * 1.1); return Math.ceil(text.length * 1.5); };费用映射表避免硬编码const pricingMap: Recordstring, { input: number; output: number } { gpt-4-turbo: { input: 0.01, output: 0.03 }, // $ per 1K tokens claude-3-haiku: { input: 0.00025, output: 0.00125 } };这个设计的精妙在于它用一次轻量API调用换来了精确计费、用户体验不降级、离线可用三重保障。面试官听到“缓存降级”组合眼睛就会亮——这说明你懂真实世界的妥协艺术。3.4 题型四错误处理的分级响应公司B第一轮真题还原“模型返回HTTP 500但错误信息是{error:{message:context_length_exceeded}}。请实现一个错误处理器针对不同错误码给出不同UI反馈。”考的是错误分类思维。很多人写个if (err.message.includes(exceeded))就完事但真实场景要分三级错误类型示例前端响应技术实现用户可修复context_length_exceeded,invalid_api_key显示具体提示操作按钮如“缩短输入”、“检查密钥”解析error.message匹配预设关键词系统可重试service_unavailable,timeout自动重试最多2次 显示“重试中...”封装fetch with retry logic记录重试次数永久失败model_not_found,permission_denied显示友好错误页联系支持入口重定向到/error页面上报Sentry关键代码片段const handleModelError (error: any, attempt: number 0): ErrorAction { const msg error?.error?.message || ; if (msg.includes(context_length_exceeded)) { return { type: user_action, message: 输入内容过长请精简至1000字以内, action: trim_input }; } if ([service_unavailable, timeout].some(k msg.includes(k))) { return { type: retry, maxAttempts: 2, currentAttempt: attempt }; } return { type: fatal, message: AI服务暂时不可用请稍后再试 }; };为什么这题难因为它逼你跳出“成功/失败”二元思维建立错误影响域模型这个错误影响的是当前请求整个会话还是用户账户你的响应必须和影响域匹配。3.5 题型五流式渲染的性能压测公司A压力测试环节真题还原“模拟100个token/秒的流速持续30秒。观察你的组件内存占用和帧率。如果内存增长超过50MB或FPS低于30请优化。”这才是真正的强度检验。我让三位候选人跑这个测试结果A同学内存飙到120MBFPS跌至8原因是setMessages([...prev, newChunk])导致每次更新都创建新数组V8引擎无法优化B同学用useReducer管理消息内存稳定在25MB但FPS仍低原因是marked.parse()在主线程密集执行C同学唯一达标用immer做不可变更新 queueMicrotask切片解析 requestIdleCallback做后台渲染。C同学的优化核心// 用immer避免数组拷贝 const [messages, dispatch] useReducer(messageReducer, []); // 解析任务切片 const parseChunk useCallback((chunk: string) { queueMicrotask(() { const html marked.parse(chunk, { sanitize: true }); dispatch({ type: ADD_CHUNK, payload: { html, timestamp: Date.now() } }); }); }, []); // 后台渲染空闲时才处理 useEffect(() { const idleCallback requestIdleCallback(() { // 批量处理待渲染的chunk }, { timeout: 1000 }); return () cancelIdleCallback(idleCallback); }, []);这个测试暴露的是你写的代码在实验室里跑得欢上了真实负载就露馅。AI前端必须像后端工程师一样思考资源——内存、CPU、GPU一个都不能少。3.6 题型六AI组件的可访问性公司B终面彩蛋题真题还原“屏幕阅读器用户如何理解流式返回的内容请为你的Chat组件补充无障碍支持。”99%的候选人当场愣住。这题看似边缘实则是顶级团队的分水岭。我的方案动态ARIA标签div roleregion aria-livepolite aria-label{AI回复${currentChunk.slice(0, 50)}...} {currentChunk} /divaria-livepolite确保屏幕阅读器在空闲时播报不打断用户操作。进度语义化{status running ( div rolestatus aria-liveoff AI正在思考中请稍候... /div )}键盘导航支持为每条消息添加tabIndex{0}按Tab键可聚焦到最新消息按Enter朗读全文。为什么必须做不是为应付考核而是因为某公司内部数据显示AI工具的残障用户使用率比普通工具高37%因语音交互更自然WCAG 2.2明确要求“动态内容更新必须提供无障碍通知”。这题的答案决定了你是不是一个真正以用户为中心的工程师。4. 实操避坑指南那些没人告诉你的血泪教训4.1 环境配置的三大隐形雷区雷区一Vite的HMR与SSE的冲突现象本地开发时修改代码触发热更新EventSource自动重连导致重复请求。原因Vite的HMR会销毁并重建模块但EventSource实例未被正确清理。解法在vite.config.ts中禁用SSE模块的HMRexport default defineConfig({ server: { hmr: { overlay: false, // 关键排除SSE相关文件 watchOptions: { ignored: [**/sse/**, **/streaming/**] } } } });或者更彻底——在开发环境用fetch轮询替代SSE仅生产用SSE。雷区二CORS代理的header丢失现象Vite代理到后端SSE连接建立但Content-Type: text/event-stream被浏览器拦截。原因Vite代理默认不转发Content-Typeheader。解法在vite.config.ts中显式设置export default defineConfig({ server: { proxy: { /api/stream: { target: http://localhost:8000, changeOrigin: true, rewrite: (path) path.replace(/^\/api\/stream/, ), // 关键透传headers configure: (proxy, options) { proxy.on(proxyRes, (proxyRes, req, res) { proxyRes.headers[Content-Type] text/event-stream; }); } } } } });雷区三移动端Safari的SSE兼容性现象iOS Safari上EventSource连接失败报错EventSources response has a MIME type (text/plain) that is not text/event-stream。原因Safari对MIME type校验极严且某些CDN会篡改header。解法双保险后端确保Content-Type: text/event-stream; charsetutf-8前端降级为fetch ReadableStream// Safari兼容方案 const stream await fetch(/api/stream).then(r r.body.getReader()); while (true) { const { done, value } await stream.read(); if (done) break; const chunk new TextDecoder().decode(value); // 处理chunk }注意这三个雷区我在真实项目中踩过两次。第一次是上线后用户投诉iOS无法使用第二次是内部测试发现热更新导致AI重复回答。它们不会出现在教程里但会真实摧毁你的上线节奏。4.2 流式渲染的四大性能反模式反模式一在render函数里做heavy parsing错误示范function ChatMessage({ content }) { return div dangerouslySetInnerHTML{{ __html: marked.parse(content) }} /; }后果每次content更新都触发同步解析100个token连续到达100次阻塞解析。正确做法预解析缓存或用useMemoconst html useMemo(() marked.parse(content), [content]);反模式二无节制的state更新错误示范es.onmessage (e) { setMessages(prev [...prev, e.data]); // 每个chunk都触发re-render };后果30fps的流速下React拼命更新UI卡死。正确做法批量更新节流const [pendingChunks, setPendingChunks] useStatestring[]([]); useEffect(() { if (pendingChunks.length 0) return; // 每100ms合并一次 const timer setTimeout(() { setMessages(prev [...prev, ...pendingChunks]); setPendingChunks([]); }, 100); return () clearTimeout(timer); }, [pendingChunks]);反模式三忽略流式chunk的语义完整性现象模型返回{delta:Hello}下一个chunk是{delta: world}但UI显示为“Hello world”中间有0.5秒空白。原因未处理delta的累积逻辑。解法维护一个currentText状态const [currentText, setCurrentText] useState(); es.onmessage (e) { const delta JSON.parse(e.data).delta; setCurrentText(prev prev delta); // 累积 };反模式四滥用useEffect做副作用错误示范在useEffect里创建EventSource但未处理组件卸载时的清理。后果内存泄漏旧组件监听新流。正确做法严格遵循清理模式useEffect(() { const es new EventSource(url); es.onmessage handler; return () es.close(); // 必须 }, [url]);4.3 模型服务集成的五大协作陷阱陷阱一假设模型返回JSON现实模型可能返回纯文本、XML、甚至二进制。某次上线Ollama返回htmlbodyerror/body/html前端JSON.parse()直接崩溃。对策始终用try/catch包裹解析且response.headers.get(content-type)做预检。陷阱二忽略模型的速率限制现象前端疯狂重试触发后端429 Too Many Requests。对策前端实现指数退避Exponential Backoffconst retryDelay Math.pow(2, attempt) * 1000; // 第1次1s第2次2s第3次4s陷阱三硬编码模型参数错误temperature: 0.7写死在组件里。正确从URL参数、localStorage或配置中心读取支持A/B测试。陷阱四忘记清理全局状态现象用户切换对话旧的EventSource仍在后台运行。对策用AbortController关联组件生命周期或用WeakMap存储实例。陷阱五日志缺乏上下文错误日志Model request failed。正确日志Model request failed: url/api/chat, modelgpt-4, input_len1234, status500, errorcontext_length_exceeded。工具用cls-hooked或AsyncLocalStorage注入请求ID串联前后端日志。4.4 面试现场的临场应变技巧技巧一把“不会”转化为“探索路径”当被问到冷门技术如WebTransport不要说“我没用过”而是“WebTransport我还没在生产环境用过但根据RFC文档它相比WebSockets的优势在于UDP底层和QUIC支持更适合低延迟流式传输。如果要用我会先验证浏览器兼容性CanIUse数据再用feature detection替代UA sniffing最后在Vite中配置对应的polyfill策略。您希望我重点展开哪部分”技巧二用“小步快跑”代替“完美方案”面试官给复杂题先交MVP“我先实现一个基础版本——用fetch轮询JSON解析确保核心流程跑通。然后我们迭代加入SSE、错误重试、token计数。这样能快速验证思路也方便您指出优化方向。”技巧三主动暴露设计权衡在给出方案后补一句“这个方案牺牲了首屏加载速度因引入tiktoken换取了token计数精度。如果首屏性能更重要我们可以改用服务端预计算前端缓存您觉得哪个优先级更高”技巧四把问题反抛为协作信号当面试官描述一个模糊需求别急着编码先确认“为了确保我理解正确这个‘思考过程可视化’是指1只显示thinking标签内的纯文本还是2需要高亮语法3是否需要支持暂停/继续您团队目前的UI规范对这类元素有特殊要求吗”这四招的本质是把单向考核变成双向对话。顶级团队招的不是答题机器而是能一起解决问题的伙伴。5. 真实能力图谱一张表看清AI前端岗的核心能力矩阵经过对23场面试的归因分析我把AI前端岗所需能力拆解为四个维度每个维度标出“入门”“熟练”“专家”三级标准。这不是理论模型而是我亲眼所见的候选人分水岭能力维度入门能干活熟练能扛事专家能定规面试验证方式流式交互工程能用EventSource接收SSE渲染简单文本流能处理stall检测、取消、重连、多流并发能用ReadableStream做Safari兼容能设计流式渲染的性能预算如“每秒最多处理50个chunk超限自动节流”能为流式组件写单元测试模拟EventSource手写流式组件压力测试模型服务集成能调通一个模型API处理基本success/error能抽象多模型适配层能实现token计数、成本预估、速率限制应对能设计前端AI SDK支持插件化模型接入、自定义中间件如日志、鉴权、跨框架React/Vue/Svelte系统设计题代码审查AI体验架构能实现基础错误提示、加载状态能设计分级错误响应用户可修复/系统可重试/永久失败能做无障碍支持能定义AI体验SLA如“95%请求端到端延迟2s”能建立AI前端可观测性体系自定义指标、trace、告警场景题开放讨论工程方法论能写单元测试、用Git能为AI组件写E2E测试模拟流式响应能做性能基线对比Lighthouse能推动团队建立AI前端规范如“所有流式组件必须实现cancel API”、“错误码必须分级”行为面试文化匹配这张表的价值在于它告诉你刷100道LeetCode不如吃透一个流式组件。真正的差距不在“会不会”而在“敢不敢为不确定性负责”。比如“专家”级的“AI体验SLA”意味着你要能回答“如果模型延迟从1s涨到3s哪些UI状态必须降级哪些功能必须关闭用户感知的底线在哪里”——这已经不是前端工程师而是AI体验的产品经理。我最后想说的是AI前端岗的强度本质上是时代给的机会。它逼你走出舒适区去理解模型、