You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Vercel AI SDK替换OpenAI助手文件引用为连续编号

解决方案:后端处理格式转换(最优选择)

核心结论:直接在后端AssistantResponse的messageDone事件中完成引用替换和参考文献生成,而非前端计算属性或消息追加时修改。原因在于:

  • 后端能直接获取OpenAI返回的完整注解(含文件ID)和文件元数据,无需前端额外请求
  • 一次性处理完格式,前端无需维护跨消息的参考文献状态,避免逻辑混乱
  • 流式返回的最终结果直接是用户需要的格式,前端渲染逻辑极简

具体实现步骤(基于你提供的后端代码修改)

1. 新增文件元数据获取工具函数

在你的API路由中添加一个函数,根据文件ID获取文件名:

async function getFileName(fileId) {
  try {
    const file = await openai.files.retrieve(fileId);
    return file.filename;
  } catch (error) {
    console.error('获取文件信息失败:', error);
    return '未知文件';
  }
}

2. 修改messageDone事件处理逻辑

替换你原有的messageDone代码,完成引用替换和参考文献生成:

.on('messageDone', async (event) => {
  try {
    if (event.content[0].type !== 'text') return;

    const { text } = event.content[0];
    let processedContent = text.value;
    const references = [];
    const citationMap = new Map(); // 存储【X:Y†Source】到脚注编号的映射

    // 1. 收集所有注解并建立映射
    if (text.annotations) {
      let footnoteIndex = 1;
      for (const annotation of text.annotations) {
        if (annotation.type === 'file_citation') {
          // 匹配消息中的引用标记(比如【4:2†Source】)
          const citationTag = `【${annotation.text}†Source】`;
          if (!citationMap.has(citationTag)) {
            const fileName = await getFileName(annotation.file_citation.file_id);
            citationMap.set(citationTag, footnoteIndex);
            references.push(`[${footnoteIndex}] ${fileName}`);
            footnoteIndex++;
          }
        }
      }

      // 2. 替换内容中的引用标记为脚注格式
      processedContent = processedContent.replace(/【(\d+:\d+)†Source】/g, (match) => {
        const footnoteNum = citationMap.get(match);
        return footnoteNum ? `[${footnoteNum}]` : match;
      });

      // 3. 拼接处理后的内容和参考文献
      if (references.length > 0) {
        processedContent += '\n\n### 参考文献\n' + references.join('\n');
      }

      // 4. 返回处理后的内容给前端
      res.write(formatAssistantStreamPart('text', processedContent));
    }
  } catch (error) {
    console.error('处理引用格式失败:', error);
    res.write(formatAssistantStreamPart('text', '处理引用时发生错误,请重试'));
  }
})

3. 前端渲染简化

前端无需额外处理,直接渲染返回的消息内容即可:

<template>
  <div v-for="message in messages" :key="message.id" class="message">
    <div v-html="message.content"></div> <!-- 用v-html渲染换行和标题 -->
  </div>
</template>

为什么不选前端方案?

如果用前端计算属性或append时修改:

  • 需要维护全局的参考文献列表,跨消息的引用编号容易重复或混乱
  • 前端无法直接获取文件元数据,必须额外调用API获取文件名,增加复杂度
  • 流式返回的过程中,引用标记是逐步出现的,前端实时替换会导致编号不稳定

额外优化建议

  • 可以把参考文献单独存储在消息对象的references字段中,前端分开渲染(比如放在消息底部的折叠面板),提升可读性
  • 缓存已获取的文件元数据,避免重复调用OpenAI的文件API

内容的提问来源于stack exchange,提问作者fooman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 08:52:20