找回密码
 立即注册
首页 业界区 业界 使用fetchEventSource构建高效AI智能助手:文件搜索场景 ...

使用fetchEventSource构建高效AI智能助手:文件搜索场景的完整实现与深度解析

胁冉右 6 小时前
使用fetchEventSource构建高效AI智能助手:文件搜索场景的完整实现与深度解析

在当今AI技术飞速发展的浪潮中,如何将大模型的能力无缝集成到企业应用中,成为每个开发者面临的挑战。今天,我将分享一个基于fetchEventSource技术构建的AI智能文件搜索助手的完整实现,该组件已在企业级文档管理系统中成功应用,显著提升了用户检索效率。我们将深入剖析其技术实现,包括流式响应、SSE通信、Markdown渲染等核心功能。
一、技术背景:为什么选择SSE与fetchEventSource

在实现AI对话类应用时,我们通常面临两种技术路线选择:

  • WebSocket:双向全双工通信,适合实时交互场景
  • Server-Sent Events (SSE):单向服务器推送,轻量级实现
对于AI助手这类服务器持续生成内容,客户端只需接收的场景,SSE具有明显优势:

  • 基于HTTP协议,无需特殊协议支持
  • 自动重连机制
  • 简单的消息格式
  • 浏览器原生支持
而@microsoft/fetch-event-source库则为我们提供了现代化、Promise风格的SSE API封装,相比原生EventSource具有更灵活的配置和控制能力。
二、功能需求与产品设计

我们的AI智能文件搜索助手需要实现以下核心功能:

  • 自然语言搜索:用户通过对话式提问搜索文件
  • 流式响应:AI生成内容时逐步展示,提升用户体验
  • 相关文件展示:搜索结果附带相关文件推荐
  • 历史记录管理:记录并展示用户最近的搜索记录
  • 富文本支持:支持Markdown、数学公式等复杂内容渲染
  • 文件预览集成:点击文件可直接查看内容
三、核心代码实现:从设计到落地

1. 组件结构与状态管理

首先,我们定义组件的核心数据结构:
  1. export default class AiIntelligentSearch extends Vue {
  2.   private searchQuery = ''; // 用户输入的搜索内容
  3.   private loading = false; // 加载状态
  4.   private recentSearches = [] as ISearchInfo[]; // 最近搜索记录
  5.   private searchFileList: Array<{ // 搜索结果列表
  6.     question: string;
  7.     content: string;
  8.     count: number;
  9.     fileList: IFile[] | Array;
  10.   }> = [];
  11.   private selectedTag = -1; // 选中的历史记录标签
  12.   private fileListStatus: { [key: number]: boolean } = {}; // 文件列表展开/收起状态
  13.   // ...其他状态
  14. }
复制代码
2. 流式响应实现:fetchEventSource深度应用

核心在于aiStreamSearchFileList方法,它使用fetchEventSource实现服务器推送:
  1. async aiStreamSearchFileList() {
  2.   const ctrl = new AbortController(); // 用于中断请求
  3.   
  4.   try {
  5.     await fetchEventSource(process.env.VUE_APP_BASE_URL + '/api/aiStreamSearchFileList', {
  6.       headers: {
  7.         'Content-Type': 'application/json',
  8.         Accept: 'text/event-stream',
  9.       },
  10.       body: JSON.stringify({
  11.         limit: 10,
  12.         offset: 1,
  13.         st: this.searchQuery.trim() + '',
  14.         sort: 0,
  15.         sortType: 1,
  16.         fileAppCode: 'file_management',
  17.       }),
  18.       method: 'POST',
  19.       signal: ctrl.signal, // 传递信号以支持中断请求
  20.       openWhenHidden: true, // 页面退至后台时保持连接
  21.       
  22.       // 连接建立回调
  23.       onopen: async (response) => {
  24.         if (!response.ok) {
  25.           console.error('连接失败:', response.statusText);
  26.           ctrl.abort();
  27.         }
  28.       },
  29.       
  30.       // 收到消息回调
  31.       onmessage: (event) => {
  32.         try {
  33.           const data = JSON.parse(event.data);
  34.           if (data) {
  35.             if (data.status !== 'completed') {
  36.               // 处理流式数据
  37.               if (data.choices && data.choices[0].delta) {
  38.                 const content = data.choices[0].delta.content;
  39.                 if (content) {
  40.                   // 将内容追加到最新的搜索结果
  41.                   if (this.searchFileList.length > 0) {
  42.                     this.searchFileList[this.searchFileList.length - 1].content += content;
  43.                   }
  44.                 }
  45.               }
  46.               // 强制更新视图,显示最新内容
  47.               this.$forceUpdate();
  48.             } else {
  49.               // 处理完成状态
  50.               const lastItem = this.searchFileList[this.searchFileList.length - 1];
  51.               if (!lastItem.content) {
  52.                 lastItem.content = '未找到符合相似度要求的文档。';
  53.               }
  54.               ctrl.abort(); // 中断请求
  55.               this.loading = false;
  56.               this.$forceUpdate();
  57.             }
  58.           }
  59.         } catch (parseError) {
  60.           console.error('解析消息时出错:', parseError);
  61.           ctrl.abort();
  62.           this.loading = false;
  63.         }
  64.       },
  65.       
  66.       // 错误处理
  67.       onerror: (error) => {
  68.         console.error('发生错误:', error);
  69.         ctrl.abort();
  70.         this.loading = false;
  71.         this.$message.error('服务器连接中断,请稍后重试');
  72.       },
  73.       
  74.       // 连接关闭
  75.       onclose: () => {
  76.         ctrl.abort();
  77.         this.loading = false;
  78.         console.log('连接已关闭');
  79.       },
  80.     });
  81.   } catch (error) {
  82.     console.error('启动流式响应时出错:', error);
  83.     ctrl.abort();
  84.     this.loading = false;
  85.     this.$message.error('无法启动流式响应,请检查网络或服务器状态');
  86.   }
  87. }
复制代码
关键点解析

  • 使用AbortController实现请求可取消
  • onmessage中处理流式数据,追加到最新搜索结果
  • this.$forceUpdate()确保Vue及时渲染新增内容
  • 错误处理完善,保证用户体验
3. 搜索执行流程整合

将普通搜索API与流式响应API结合使用:
  1. async searchAIFileList() {
  2.   this.loading = true;
  3.   const param = {
  4.     limit: 10,
  5.     offset: 1,
  6.     st: this.searchQuery.trim(),
  7.     sort: 0,
  8.     sortType: 1,
  9.   };
  10.   // 1. 先调用普通搜索API获取文件列表
  11.   const [err, res] = await to(EMSearchFileList(param));
  12.   
  13.   if (err) {
  14.     this.$message.error('搜索失败,请稍后重试');
  15.     this.loading = false;
  16.     return;
  17.   }
  18.   // 2. 构建初始搜索结果
  19.   const fileList = Array.isArray(res) ? res : [];
  20.   const searchResult = {
  21.     fileList,
  22.     content: '',
  23.     count: fileList.length || 0,
  24.     question: this.searchQuery.trim(),
  25.   };
  26.   // 3. 无结果时设置默认提示
  27.   if (!res || fileList.length === 0) {
  28.     searchResult.content = '未找到符合相似度要求的文档。';
  29.   }
  30.   
  31.   // 4. 添加到结果列表
  32.   this.searchFileList.push(searchResult);
  33.   // 5. 等待DOM更新后滚动到底部
  34.   this.$nextTick(() => {
  35.     this.scrollToBottom();
  36.   });
  37.   // 6. 执行流式响应获取AI生成内容
  38.   await this.aiStreamSearchFileList();
  39.   
  40.   // 7. 更新最近搜索记录
  41.   await this.getRecentSearchList();
  42.   
  43.   // 8. 清空输入框
  44.   setTimeout(() => {
  45.     this.searchQuery = '';
  46.     this.loading = false;
  47.   }, 1000);
  48. }
复制代码
这种两阶段设计确保了:

  • 先快速返回文件列表,提供即时反馈
  • 再通过流式响应逐步生成详细内容
  • 最后更新搜索历史,完善用户体验
4. Markdown与数学公式渲染

为了支持复杂内容展示,我们集成了markdown-it和markdown-it-katex:
  1. renderedMarkdown(content: string) {
  2.   const md = new MarkdownIt({
  3.     html: true, // 启用HTML标签
  4.     linkify: true, // 自动识别URL
  5.     typographer: true, // 启用智能引号等
  6.   });
  7.   
  8.   // 添加数学公式支持
  9.   md.use(markdownItKatex, {
  10.     blockClass: 'katex-block',
  11.     errorColor: '#cc0000',
  12.     throwOnError: false,
  13.     macros: {
  14.       "\\RR": "\\mathbb{R}",
  15.       "\\p": "\\frac{\\partial #1}{\\partial #2}",
  16.     }
  17.   });
  18.   
  19.   // 自定义渲染规则
  20.   md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
  21.     const token = tokens[idx];
  22.     token.attrPush(['target', '_blank']); // 增加target="_blank"
  23.     token.attrPush(['rel', 'noopener noreferrer']); // 增加安全属性
  24.     return self.renderToken(tokens, idx, options);
  25.   };
  26.   return md.render(content || '');
  27. }
复制代码
在模板中使用:
  1.   
复制代码
5. UI设计与交互优化

组件采用精心设计的UI布局,确保良好的用户体验:
  1.   
  2.    
  3.    
  4.       
  5.         {{ item.question }}
  6.       
  7.    
  8.    
  9.    
  10.       
  11.         
  12.         
  13.          
  14.         
  15.         
  16.         
  17.          0">
  18.          
  19.             <img src="https://www.cnblogs.com/@/assets/file-ai-list-search.svg" alt="" />
  20.             共找到{{ item?.fileList.length }}个相关文件
  21.          
  22.          
  23.             
  24.               {{ fileListStatus[index] ? '展开' : '收起' }}
  25.             
  26.          
  27.         
  28.         
  29.         
  30.         
  31.          
  32.             
  33.               <img :src="iconSrc(file.fiType || '')" alt="" />
  34.               {{ file.oldFileName }}
  35.             
  36.          
  37.         
  38.       
  39.    
  40.   
复制代码
关键交互细节:

  • 问题内容可点击复制
  • 文件列表可展开/收起
  • 支持文件图标根据类型动态显示
  • 搜索历史标签支持点击重搜
6. 实用工具方法
  1. // 滚动到最新消息
  2. scrollToBottom() {
  3.   const sectionList = this.$refs.sectionList as HTMLElement;
  4.   if (sectionList) {
  5.     const lastQaList = sectionList.querySelectorAll('.qa-list')[this.searchFileList.length - 1];
  6.     if (lastQaList) {
  7.       lastQaList.scrollIntoView({
  8.         behavior: 'smooth',
  9.         block: 'start',
  10.       });
  11.     }
  12.   }
  13. }
  14. // 复制问题内容
  15. copyQuestion(item: any) {
  16.   const textToCopy = item.question || '';
  17.   navigator.clipboard.writeText(textToCopy).then(() => {
  18.     message.success('复制成功');
  19.   }).catch(() => {
  20.     message.warning('复制失败');
  21.   });
  22. }
  23. // 获取文件图标
  24. get iconSrc() {
  25.   return (suffix: string) => {
  26.     return requireIcon(suffix);
  27.   };
  28. }
复制代码
四、性能优化与错误处理

1. 请求中断机制

使用AbortController实现请求可取消:
  1. const ctrl = new AbortController();
  2. // 传递信号
  3. await fetchEventSource(url, { signal: ctrl.signal });
  4. // 需要中断时
  5. ctrl.abort();
复制代码
2. 错误处理策略
  1. onerror: (error) => {
  2.   console.error('发生错误:', error);
  3.   ctrl.abort(); // 确保请求终止
  4.   this.loading = false;
  5.   // 显示用户友好错误
  6.   this.$message.error('服务器连接中断,请稍后重试');
  7. },
复制代码
3. 空状态处理
  1. // 处理完成状态
  2. if (data.status === 'completed') {
  3.   const lastItem = this.searchFileList[this.searchFileList.length - 1];
  4.   // 确保空内容时有提示
  5.   if (!lastItem.content) {
  6.     lastItem.content = '未找到符合相似度要求的文档。';
  7.   }
  8.   // 确保fileList为数组
  9.   if (!Array.isArray(lastItem.fileList)) {
  10.     lastItem.fileList = [];
  11.   }
  12.   lastItem.count = lastItem.fileList.length || 0;
  13. }
复制代码
五、部署与集成

组件完整集成到企业文档管理系统中,需要处理的集成点:

  • 认证与授权:在请求头中添加token
  • 文件预览:集成现有文件预览组件
  1. viewFile(item: IFile) {
  2.   this.detailFileModal.open(item.fileID);
  3.   // 添加文件访问记录
  4.   ManageModule.addFileRecord(item.fileID);
  5. }
复制代码

  • 环境变量配置:使用process.env.VUE_APP_BASE_URL配置API地址
六、总结与展望

通过fetchEventSource实现的AI智能文件搜索助手,完美解决了传统搜索体验的不足:

  • 实时反馈:流式响应让等待感消失
  • 自然交互:对话式搜索降低使用门槛
  • 精准结果:AI理解用户真实意图
  • 无缝集成:与现有文件系统完美融合
未来优化方向

  • 增加上下文感知,支持多轮对话
  • 优化提示工程,提高搜索准确率
  • 添加个性化推荐,基于用户历史行为
  • 支持多语言处理,满足国际化需求
七、完整代码示例

以下是组件的核心部分,完整代码请参考文末GitHub链接:
  1. <template>
  2.   
  3.     <main-header-component
  4.       :is-show-store-btn="false"
  5.       :is-show-search="false"
  6.       :is-show-upload-btn="false"
  7.     ></main-header-component>
  8.    
  9.       
  10.         
  11.          
  12.             
  13.               
  14.                 <img src="https://www.cnblogs.com/@/assets/file-ai-search-logo.svg" alt=""  />
  15.               
  16.               
  17.                
  18.                   文件搜索助手
  19.                   
  20.                     {{ data.isShowSearch ? '收起' : '展开' }}
  21.                   
  22.                
  23.               
  24.             
  25.             
  26.               AI搜索以自然语言处理和语义理解为核心,通过智能分类、个性化推荐及跨平台实时检索技术,精准捕捉用户意图并快速定位目标内容,同时支持多模态数据的关联分析,实现"对话式"高效搜索体验。
  27.             
  28.              0">
  29.               最近搜索
  30.               
  31.                
  32.                   <img v-if="selectedTag === tag.siId" src="https://www.cnblogs.com/@/assets/file-ai-question-search.svg" alt="" />
  33.                   {{ tag.content?.length > 10 ? tag.content.slice(0, 10) + '...' : tag.content }}
  34.                
  35.               
  36.             
  37.          
  38.          
  39.          
  40.          
  41.             
  42.          
  43.         
  44.         
  45.         
  46.         
  47.          
  48.             
  49.             
  50.               <img src="https://www.cnblogs.com/@/assets/file-ai-send.svg" alt="发送" />
  51.             
  52.          
  53.          
  54.             内容由各平台大模型生成,不能完全保证准确性和完整性,不代表我们的态度或观点
  55.          
  56.         
  57.       
  58.     </a-spin>
  59.     <file-detail-component ref="detailFileModal"></file-detail-component>
  60.   
  61. </template>
复制代码
这个实现不仅展示了fetchEventSource在AI应用中的强大能力,更提供了一套完整的企业级解决方案。通过精心设计的UI/UX和稳定的错误处理机制,为用户提供了流畅、可靠的智能搜索体验。希望这篇文章能为你的AI应用开发提供有价值的参考!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
1.jpg
2.jpg
3.jpg
4.jpg
5.jpg

相关推荐

您需要登录后才可以回帖 登录 | 立即注册