前端每日一题:JavaScript、CSS、网络与框架高频面试题
# 第401题 前端如何实现大模型的流式输出?
30 秒速记
- 流式输出的目标不是让总生成时间变短,而是降低首字等待,让用户持续看到进展
- 浏览器通常用
fetch读取ReadableStream,用TextDecoder按增量解码,不能假设一个数据块就是一条完整消息 - 服务端常用
SSE帧或按行分隔的NDJSON;协议必须能表示文本、状态、错误和结束 - 前端要处理半包、粘包、
UTF-8跨块字符、取消、重连及未完成回答 - 每到一个
token就全量重渲染会卡页面,应合并刷新,并对Markdown做增量或节流渲染
前端做大模型流式输出,核心是持续读取响应流,再按应用层协议拼出完整事件。 我会用 fetch 获取 response.body,用 TextDecoder 保留跨数据块的字节状态,然后在缓冲区中按换行符拆帧。页面上不会每收到一个字就立刻重渲染,而是按帧或很短的时间窗合并更新。同时要把用户取消、网络中断和服务端错误分开处理,否则界面只会留下一段不知道是否完成的半截文字。
假设服务端返回按行分隔的 NDJSON,每行都是一个独立的 JSON 事件。一次网络读取可能只拿到半行,也可能一次拿到三行,所以必须保留未完整的尾部:
async function* readNdjson(response, signal) {
if (!response.ok || !response.body) {
throw new Error(`HTTP ${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
try {
while (true) {
if (signal?.aborted) throw signal.reason
const { value, done } = await reader.read()
buffer += decoder.decode(value, { stream: !done })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
if (line.trim()) yield JSON.parse(line)
}
if (done) break
}
if (buffer.trim()) yield JSON.parse(buffer)
} finally {
reader.releaseLock()
}
}
const controller = new AbortController()
const response = await fetch('/api/chat', {
method: 'POST',
signal: controller.signal,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ prompt: '解释事件循环' })
})
let answer = ''
for await (const event of readNdjson(response, controller.signal)) {
if (event.type === 'delta') answer += event.text
if (event.type === 'error') throw new Error(event.message)
if (event.type === 'done') break
}
TextDecoder.decode(value, { stream: true }) 会保留未组成完整字符的字节,避免中文在数据块边界处变成乱码。buffer 解决的则是应用层半包问题。生产中还应给每次对话分配请求标识,只允许当前请求更新页面;用户重新提问时,先调用 AbortController.abort() 终止旧流,防止旧回答串到新消息里。
渲染层的常见瓶颈是更新太细。模型一秒可能送出几十个增量,如果每次都重新解析全部 Markdown,回答越长开销越大。可以先把文本收进变量,通过 requestAnimationFrame 或 30–50ms 的刷新窗口更新视图。流结束后再做一次完整校验和最终渲染,并保留错误态、重试入口与已接收的部分内容。
面试官追问
追问 1线上偶尔有中文变成 �,但后端打印的字符串正常,你会先查哪里?
先查前端是否对每个数据块单独新建 TextDecoder 或忘了传 { stream: true }。UTF-8 的一个中文字可能跨两个块,需要让同一个解码器保留中间状态,最后再完成尾部解码。
追问 2后端说每次 write 的都是完整 JSON,前端能不能直接对每次 reader.read() 的结果做 JSON.parse?
不能。应用层的一次 write 不会与传输层的一次 read 保持边界对齐,代理、压缩和网络分段都会改变块大小。前端必须按明确的帧协议分隔,不能把数据块当消息。
追问 3用户点了停止,页面不再动,但服务端仍在调模型并计费,这算做完取消了吗?
没有。前端的 AbortController 只是起点,服务端还要监听连接关闭,并把取消信号向下游模型请求传播。否则只停了界面接收,计算和费用仍在发生。
追问 4回答越长页面越卡,性能面板显示大量 Markdown 解析,你会怎么改?
先合并增量,把视图更新控制在每帧一次或几十毫秒一次,避免一个 token 触发一次全树渲染。再检查能否只解析未完成的尾部,或在流结束前降低语法高亮等重操作的频率。
# 第400题 RAG 是什么,一次检索增强生成经历了哪些步骤?
30 秒速记
RAG先从可信知识库检索相关片段,再把片段和问题一起交给模型;它不会改变模型参数- 离线链路是文档清洗、切块、生成
embedding并建索引;在线链路是理解问题、召回、重排、组装上下文与生成 - 只做向量相似度容易丢掉精确词,实际常把关键词检索与向量检索混合,再用重排器精排
- 切块不是字数越小越好:太小丢上下文,太大则噪声多、占用更多
token - 没有权限过滤、来源引用和检索评估的
RAG,只是一个看起来会查资料的聊天框
RAG 是在生成答案前,先从外部知识库找到与问题最相关的证据。 它适合企业文档、产品手册和持续变化的业务知识,因为更新索引比重新微调模型更快。完整链路不只是向量搜索,还包括切块、权限过滤、混合召回、重排、引用和评估。如果召回的材料不对,生成模型说得再流畅也只是根据错证据作答。
下面的示例用一个可替换的检索器表达在线链路。真实项目可以把 search 换成向量数据库与全文搜索的混合召回,但权限过滤必须发生在内容进入模型之前:
type Chunk = {
id: string
text: string
score: number
allowedUserIds: string[]
}
async function answerWithRag(question: string, userId: string) {
const candidates = await search(question, { limit: 20 })
const visible = candidates.filter((chunk: Chunk) =>
chunk.allowedUserIds.includes(userId)
)
const evidence = (await rerank(question, visible))
.slice(0, 5)
.map((chunk, index) => `[${index + 1}] ${chunk.text}`)
.join('\n\n')
return generate({
system: '只根据证据作答;证据不足时直说不确定,并标注引用编号。',
prompt: `问题:${question}\n\n证据:\n${evidence}`
})
}
这段代码的顺序很重要:先召回,再用当前用户身份过滤,然后重排和裁剪。如果先把候选文本送给外部重排服务,再做权限过滤,未授权文档已经泄露给了下游。上下文也不应无限堆叠,应优先保留能直接支撑答案的片段,同时附上文档标识、版本和原文位置,便于用户核对。
评估时要把“检索对不对”和“模型答得好不好”分开。可以准备一组带标准证据的问题,检查正确片段是否进入前 K 名,再单独检查答案是否被证据支持、引用是否可回溯。否则一旦答错,团队只会笼统地调整提示词,却没发现真正问题是切块或召回。
面试官追问
追问 1公司手册明明有答案,系统却总召回另一篇用词很像的旧文档,你怎么排查?
先看标准答案所在片段有没有进入召回候选,再分别检查切块边界、索引更新、关键词与向量召回得分。若新旧文档都命中,应把版本、生效时间等元数据加入过滤或重排,不能只靠语义相似度。
追问 2为什么不把整本手册都放进提示词,反而要做切块和检索?
整本塞入会增加延迟与成本,相关证据也容易被大量无关内容淹没,而且文档可能超出上下文窗口。检索的价值是用更小、更相关且可追溯的证据支持回答。
追问 3两个部门共用同一知识库,模型答案里出现了另一部门的内部制度,问题在哪一层?
这是检索链路的授权失败,不是靠提示词说“不要泄密”就能解决。应在召回或至少在内容出开可信边界前,根据用户和文档权限做服务端过滤,并增加跨租户的自动化测试。
追问 4上线后只看用户点赞率,能不能判断 RAG 检索效果?
不能。点赞会同时受文案、交互和模型表达影响,无法直接定位召回问题。需要用带标准证据的测试集评估召回与重排,再用引用正确性、事实一致性和用户反馈补充端到端评估。
# 第399题 Function Calling 与 MCP 有什么区别?
30 秒速记
Function Calling是模型输出结构化工具调用意图的能力;模型不会自己执行函数- 应用层负责声明工具、验证参数、执行真实操作,再把结果传回模型
MCP是连接AI应用与外部工具、资源、提示的协议,解决“如何发现和调用能力”的接入一致性- 两者可以配合:
MCP服务端暴露工具,宿主把工具描述交给模型,模型再通过Function Calling选择调用 - 无论用哪个,权限、参数校验、幂等、超时与人工确认都必须由代码守住
Function Calling 是模型和应用之间的结构化调用机制,MCP 则是 AI 应用连接外部能力的通用协议。 模型只会返回“想调哪个工具、参数是什么”,真正的查库、发邮件或下单由应用执行。MCP 可以让这些工具以统一方式被发现和调用,但不会替你做业务授权。所以两者不是互相替代,而是处在不同层次。
