RAG本身:
“你们为什么选择 RAG 方案?它解决了什么问题?有没有考虑过其他方案,比如直接对模型进行微调(Fine-tuning)?”
为什么不微调?
- 为了准确性
- 更多的是做风格的优化。
- 微调的模型需要大量的数据,并且需要大量的计算资源。
RAG的流程?
- 文本解析:通过各种loader和加载器来加载
- 文本切片:
- 不按固定长度切片,而是优先按照段落、句子、单词来切片。最大程度来保证每个Chunk的语义完整性。
- chunk_size设置为512个token,并且有50个token的重叠区。
- 向量化:
- 使用向量模型来向量化
- 在线检索生成流程
- 取出五个最关联的文本块原文,然后构建prompt,然后生成答案。
“你们的 RAG 流程是怎样的?文档是如何被切片(Chunking)和向量化的?用的是什么向量数据库?”
关于前端的技术挑战(展现你的专业深度):
“流式响应(Streaming)你是怎么实现的?”
回答思路:我们后端使用了 Server-Sent Events (SSE) / 或者基于 Fetch API 的 ReadableStream。前端接收到这种流式数据后,实时地将其追加到 UI 上,并实现打字机效果,这样用户几乎在请求发出的瞬间就能看到反馈,极大地优化了等待体验。
长连接下的流式文本渲染 (Streaming Text Rendering)
- 面临的问题与挑战 (The Problems) “在AI项目中,用户最不能忍受的就是点击发送后,面对一个长久的加载菊花(spinner)。为了提供即时反馈,我们必须采用流式渲染。但这引入了几个核心挑战:”
UI 阻塞与性能噩梦:如果后端以非常高的频率(比如每秒 50次)发来一个个 token(单词或字符),而我每次都用 setState 去更新整个文本块,会导致 React 组件以极高的频率进行 re-render。这会迅速阻塞浏览器的主线程,导致页面卡顿、用户输入无响应,体验极差。
不优雅的“打字机”效果:如何实现一个平滑、自然的打字机效果,而不是生硬的文本追加?如何处理 Markdown、代码块等富文本格式在流式输出时的正确渲染?
错误处理与中断:如果长连接在传输中途断开,UI应该如何响应?用户是否可以主动中断一个正在进行的、但看起来跑偏了的 AI 回复?
- 解决方案 (The Solutions)
直接通过useRef直接操作dom,发完之后再统一setState。
“引用来源高亮是怎么做的?”
回答思路:后端在流式数据中会包含一些特殊标记来指明某段文本来自哪个文档的哪部分。前端需要解析这些标记,一方面将文本渲染出来,另一方面将来源信息储存起来。当用户悬停或点击文本时,我们能立刻找到并高亮对应的原始文档片段。
“在AI等待回复时,你是如何处理UI状态的?”
回答思路:我们设计了一套丰富的加载状态。比如会显示“正在搜索知识库...”、“正在生成答案...”等提示,让用户知道 AI 正在“思考”,而不是程序卡死了。同时,输入框会暂时禁用,防止用户重复提交。
其余追问:
关于流式解析:你提到了自己实现了一个带缓冲区的解析器来处理不完整的消息。这很好。那么请问,你是如何处理 Unicode 字符被截断 的情况?比如一个 3 字节的汉字,恰好在第二个字节处被切分到两个数据块(chunk)里,你的 TextDecoder 在处理第一个不完整的数据块时不会报错吗?你是如何保证字符解码的完整性和正确性的?
- TextDecoder开启stream: true模式,始终使用同一个实例。
关于 React 状态管理:在 React 环境中,你拿到一个又一个的 token,然后渲染到界面上。你是如何管理这个正在“生长”的字符串状态的?是每次拿到新 token 都调用 setState(prev => prev + newToken) 吗?如果模型生成速度非常快(比如每秒 50-100 个 token),这种高频的 setState 调用会不会导致性能问题或过度渲染?你有没有做过相应的优化,比如批量更新(batching)?
- 通过useRef设置一个缓冲区,然后在requestAnimationFram中调用来解决这个问题。
关于背压(Backpressure):你考虑了客户端重连对服务器的冲击,那反过来,如果后端 LLM 生成 token 的速度远快于客户端的网络下行速度,或者快于浏览器渲染速度,数据就会在网络管道或客户端内存中堆积。这就是典型的“背压”问题。你在 ReadableStream 的消费端,有没有考虑或实现任何背压处理机制?还是说完全依赖于 TCP 的滑动窗口流控?
- 底层机制:TCP的流量控制,滑动窗口
- 一旦缓冲区满了,TCP 协议栈会自动通知服务端,将发送窗口调小,甚至暂停发送数据。
- ReadableStream内部有个“队列”和“高水位线”,当数据到达的速度快于我们的read()速度的时候,数据会在这个内部队列中缓冲。
- 然后它会发出暂停信号,并且信号会通过tcp传到服务器。
- 这样就实现了一个从应用层到底层的背压机制。
- 底层机制:TCP的流量控制,滑动窗口