1. AI笔记应用概览与初始设置 [0]

  1. 教程目标:
    1. 使用Vercel AI SDK、Convex和OpenAI API构建一个带AI聊天机器人的笔记应用。 [0]
  2. 应用功能演示:
    1. 登录界面:应用具备登录/注册功能。 [2]
    2. 笔记管理:用户可以创建和删除笔记。 [4]
    3. AI聊天机器人:
      1. 能够查找笔记信息并回答问题。 [4]
      2. 例如,查找Wi-Fi密码。 [5]
      3. 提供富文本格式和相关笔记链接。 [10]
      4. 能回答后续问题,如提供购物清单。 [12]
      5. 若无需搜索,AI会直接回答。 [15]
      6. 无论笔记数量多少,AI总能精准定位信息。 [17]
  3. 后端技术选型:
    1. 选择Convex作为后端服务。 [19]
    2. Convex提供数据库、认证和向量存储功能。 [19]
    3. Convex是免费的。 [20]
  4. 项目起始代码:
    1. 教程已准备好起始代码,以便专注于核心部分。 [24]
    2. 代码仓库链接将在视频描述中提供。 [25]
    3. 默认分支名为starting code。 [26]
  5. 克隆和运行代码:
    1. 克隆仓库:使用git clone [URL]克隆项目。 [27]
    2. 安装依赖:在命令行运行npm install。 [37]
    3. 启动开发服务器:运行npm run dev启动应用。 [39]
    4. 可在localhost:3000访问。 [39]
  6. 起始代码状态:
    1. 当前没有登录界面。 [42]
    2. 无法创建笔记。 [43]
    3. AI聊天机器人不工作。 [44]
    4. 已具备深色模式。 [46]

2. Convex后端设置 [47]

  1. Convex作为后端:
    1. Convex将作为应用的后端。 [47]
    2. 包含笔记数据库、认证、文件存储等。 [48]
    3. 所有功能集中,无需注册多个服务。 [50]
  2. Convex设置步骤:
    1. 进入VS Code项目。 [53]
    2. 在命令行停止当前执行(Ctrl+Z)。 [54]
    3. 运行npx convex dev启动Convex开发服务器。 [55]
    4. 登录/创建账户:
      1. 选择“Login or create account”。 [59]
      2. 确认默认设备名,按Y在浏览器中打开Convex。 [59]
      3. 在浏览器中创建或登录Convex账户。 [60]
      4. 在VS Code中同意服务条款并确认项目名。 [65]
    5. Convex文件生成:
      1. 项目将创建convex文件夹及生成文件。 [68]
      2. 这些文件会在代码更改时自动更新。 [70]
      3. npx convex dev必须持续运行。 [71]
      4. 若停止,需重新运行。 [73]
      5. 需开第二个终端运行npm run dev。 [74]
    6. 环境变量:
      1. npx convex dev创建.env.local文件。 [76]
      2. 包含CONVEX_DEPLOYMENT和NEXT_PUBLIC_CONVEX_URL。 [76]
      3. 手动添加NEXT_PUBLIC_BASE_URL设为localhost:3000。 [77]
    7. 前端设置Convex Provider:
      1. 在app文件夹创建convex-client-provider.tsx文件。 [80]
      2. 定义convex常量,初始化ConvexReactClient。 [85]
      3. 使用process.env.NEXT_PUBLIC_CONVEX_URL。 [87]
      4. 导出ConvexClientProvider函数,接收children。 [88]
      5. 返回ConvexProvider并传入client。 [90]
      6. 此Provider将Convex客户端暴露给整个应用。 [92]
    8. 集成Provider:
      1. 在app/layout.tsx文件中。 [96]
      2. 将children包裹在ConvexClientProvider中。 [101]
      3. 至此,Convex功能可在React应用中使用。 [103]

3. Convex身份验证设置 [104]

  1. Convex身份验证:
    1. Convex提供身份验证功能。 [104]
    2. 用户数据与笔记存储在同一数据库中。 [105]
  2. 身份验证设置步骤:
    1. 打开第三个终端。 [109]
    2. 运行npx @convex-dev/auth命令。 [110]
    3. 输入本地Web服务器URL(localhost:3000)。 [114]
    4. 自动配置密钥并设置文件。 [115]
  3. 生成的文件:
    1. auth.config.ts:配置Convex认证。 [118]
      1. Convex site URL环境变量存储在Convex仪表板中。 [121]
      2. Next.js项目只包含前端环境变量。 [125]
    2. http.ts:添加了认证所需的auth HTTP routes。 [127]
    3. convex/auth.ts:
      1. 在providers数组中添加password以启用密码认证。 [129]
      2. 需要手动导入password。 [131]
      3. 教程仅使用邮件和密码认证。 [133]
    4. convex/schema.ts:
      1. 创建schema.ts文件。 [135]
      2. 定义schema常量并赋值defineSchema。 [137]
      3. 使用...authTables包含认证数据库表(用户和会话)。 [138]
      4. 将schema默认导出以激活。 [140]
    5. convex-client-provider.tsx更新:
      1. 将ConvexProvider替换为ConvexAuthNextjsProvider。 [142]
      2. 需要手动导入ConvexAuthNextjsProvider。 [144]
      3. 此更改确保Convex认证与Next.js兼容。 [144]
    6. layout.tsx更新:
      1. 将返回内容包裹在ConvexAuthNextjsServerProvider中。 [147]
      2. 此Provider确保Convex认证在服务器端工作。 [150]
      3. ConvexClientProvider仍保留。 [153]
  4. Middleware设置:
    1. 在src文件夹创建middleware.ts文件。 [155]
    2. 用于根据认证状态保护特定页面。 [157]
    3. 复制粘贴预设代码(视频描述提供链接)。 [158]
    4. 功能:
      1. 检查页面是否受保护。 [160]
      2. 未登录访问保护页面,自动重定向到登录页。 [161]
      3. /notes下的所有路由均受保护。 [162]
      4. 已登录访问登录页,自动重定向到/notes。 [163]
  5. 测试Middleware:
    1. 重启Convex和应用。 [169]
    2. 访问/notes,应重定向到登录页。 [174]
    3. 访问首页(非/notes),无需登录。 [177]
  6. 登录表单实现 (SignInForm.tsx):
    1. 在onSubmit函数中处理登录/注册逻辑。 [181]
    2. step状态:区分登录和注册。 [183]
    3. 表单验证:使用Shadcn和React Hook Form实现实时验证。 [186]
      1. 验证规则(如邮件格式、密码长度)在auth文件夹的schema.ts中定义。 [187]
      2. Convex默认要求密码至少8个字符。 [190]
    4. Hooks:
      1. useAuthActions:获取signIn和signOut函数。 [194]
      2. useState:管理isLoading状态。 [198]
      3. useRouter:从next/navigation导入,用于重定向。 [201]
    5. onSubmit逻辑:
      1. 设置isLoading为true。 [207]
      2. try块:
        1. 调用signIn函数,传入password提供者、邮件、密码和flow(step)。 [209]
        2. 显示成功toast消息(根据step显示“signed in successfully”或“account created successfully”)。 [216]
        3. 重定向到/notes。 [220]
      3. catch块:
        1. 记录错误到控制台。 [222]
        2. 区分预期错误:
          1. 若错误消息包含“invalid account ID”或“invalid secret”,则显示“invalid credentials”错误。 [231]
          2. 通过form.setError在表单根部设置错误。 [240]
          3. 此错误将在UI中渲染。 [246]
        3. 处理非预期错误:显示通用错误toast消息“Something went wrong, please try again.”。 [250]
      4. finally块:重置isLoading为false。 [255]
    6. 提交按钮:在isLoading为true时禁用。 [257]
  7. 测试登录/注册:
    1. 确保npx convex dev运行。 [259]
    2. 尝试使用不存在的账户登录,显示“invalid credentials”。 [265]
    3. 创建新账户,显示“account created successfully”并登录。 [267]
  8. 登出功能实现 (SignOutButton.tsx):
    1. 在SignOutButton.tsx的onClick处理器中。 [272]
    2. 调用useAuthActions钩子中的signOut函数。 [275]
    3. 点击按钮后,用户将登出并返回登录界面。 [277]
  9. Convex仪表板:
    1. 通过npx convex dashboard或dashboard.convex.dev访问。 [287]
    2. 在data选项卡中,可以看到authTables。 [294]
    3. users表中包含已注册的用户信息。 [298]

4. 笔记数据表与操作 [300]

  1. 创建笔记数据表:
    1. 在convex/schema.ts文件中。 [302]
    2. 添加notes表。 [304]
    3. 字段定义:
      1. title:v.string类型。 [306]
      2. body:v.string类型。 [308]
      3. userId:v.id('users')类型,关联用户表。 [309]
    4. 索引:
      1. 为userId创建by_userId索引。 [313]
      2. 允许按userId搜索笔记。 [314]
    5. 保存更改,确保npx convex dev运行以同步更新。 [317]
  2. 创建笔记功能 (convex/nodes.ts):
    1. 在convex文件夹中创建nodes.ts文件。 [319]
    2. createNote突变:
      1. 导出createNote常量,赋值为mutation(从generated文件夹导入)。 [321]
      2. args:接收title和body(v.string)。 [324]
      3. returns:返回新创建笔记的_id。 [330]
      4. handler(异步函数):
        1. 认证用户:获取userId。 [337]
        2. 权限检查:若userId为null,抛出认证错误。 [339]
        3. 插入笔记:使用context.db.insert将title、body和userId插入notes表。 [343]
    3. 前端调用 (CreateNoteButton.tsx):
      1. 在CreateNoteButton.tsx的onSubmit函数中。 [351]
      2. useMutation钩子:获取createNote函数(从API.nodes.createNote导入)。 [354]
      3. try块:
        1. 调用await createNote,传入title和body。 [360]
        2. 显示成功toast消息。 [363]
        3. 重置表单并关闭对话框。 [364]
      4. catch块:显示错误消息和toast。 [368]
      5. 提交按钮:在isSubmitting为true时禁用并改变文本。 [371]
    4. 测试创建笔记:
      1. 在应用中创建笔记。 [378]
      2. 笔记创建成功,但界面尚未显示。 [383]
      3. Convex仪表板的nodes表会显示新笔记。 [384]
  3. 获取笔记功能 (convex/nodes.ts):
    1. 在nodes.ts中添加getUserNodes函数。 [385]
    2. query类型:导出getUserNodes常量,赋值为query。 [388]
    3. args:为空,不接收参数。 [390]
    4. handler(异步函数):
      1. 认证用户:获取userId。 [395]
      2. 权限检查:若userId为null,返回空数组。 [397]
      3. 查询笔记:
        1. 使用context.db.query('notes')。 [399]
        2. 通过withIndex('by_userId')按用户ID过滤。 [400]
        3. 使用order('desc')按时间戳降序排序。 [407]
        4. 调用collect()返回结果。 [409]
    5. 前端显示 (NotesPage.tsx):
      1. 在NotesPage.tsx中。 [411]
      2. 使用useQuery钩子调用API.nodes.getUserNodes。 [412]
      3. 使用notes.map渲染NoteItem组件。 [415]
      4. NoteItem.tsx:
        1. 定义NoteItemProps接口,包含note(类型为doc<'nodes'>)。 [417]
        2. 渲染note.title和note.body。 [427]
      5. use client指令:在NotesPage.tsx顶部添加"use client"。 [432]
    6. 测试获取笔记:
      1. 应用现在显示已创建的笔记。 [436]
      2. 创建新笔记,界面会即时更新(Convex特性)。 [438]

5. 笔记详情与删除功能 [444]

  1. 打开笔记详情对话框:
    1. 在NoteItem.tsx中。 [445]
    2. handleOpenNote函数:
      1. 点击笔记时调用。 [451]
      2. 使用window.history.pushState将noteId作为搜索参数添加到URL。 [452]
      3. 此方法比Router.push更快,不重新加载页面。 [460]
    3. NotePreviewDialog.tsx:
      1. 在NoteItem组件下渲染。 [464]
      2. 接收note作为prop。 [466]
      3. 渲染note.title和note.body。 [468]
      4. useSearchParams钩子:从next/navigation获取URL搜索参数。 [470]
      5. isOpen状态:根据URL中的noteId与当前笔记ID是否匹配来判断对话框是否打开。 [471]
      6. handleClose函数:
        1. 通过window.history.pushState移除URL中的noteId搜索参数。 [475]
        2. 关闭对话框。 [477]
      7. 将isOpen和handleClose绑定到Dialog组件的open和onOpenChange属性。 [478]
    4. 测试打开笔记:
      1. 点击笔记,URL显示noteId,对话框打开。 [458]
      2. 刷新页面,对话框仍显示。 [480]
      3. 点击X或外部关闭对话框。 [481]
  2. 删除笔记功能:
    1. 在convex/nodes.ts中添加deleteNote突变。 [482]
    2. mutation类型:导出deleteNote常量,赋值为mutation。 [484]
    3. args:接收noteId(类型为v.id('notes'))。 [485]
    4. handler(异步函数):
      1. 认证用户:复制createNote中的认证逻辑。 [488]
      2. 获取笔记:使用context.db.get获取待删除笔记。 [490]
      3. 笔记存在性检查:若笔记不存在,抛出“note not found”错误。 [492]
      4. 权限检查:若笔记不属于当前用户,抛出“User is not authorized...”错误。 [494]
      5. 删除笔记:使用await context.db.delete删除笔记。 [497]
      6. 更新错误消息为“User must be authenticated to delete a node”。 [495]
    5. 前端调用 (NotePreviewDialog.tsx):
      1. 在NotePreviewDialog.tsx中。 [496]
      2. useMutation钩子:获取deleteNote函数(从API.nodes.deleteNote导入)。 [498]
      3. useState钩子:管理deletePending状态。 [499]
      4. handleDelete函数(异步):
        1. 设置deletePending为true。 [501]
        2. try块:
          1. 调用await deleteNote,传入noteId。 [503]
          2. 显示成功toast消息“Note deleted.”。 [505]
          3. 调用handleClose关闭对话框。 [505]
        3. catch块:记录错误并显示错误toast。 [506]
        4. finally块:重置deletePending为false。 [508]
      5. 将handleDelete绑定到删除按钮的onClick。 [510]
      6. 在deletePending为true时禁用删除按钮并改变文本。 [511]
      7. 在handleClose开始处,若deletePending为true则阻止关闭。 [514]
    6. 测试删除笔记:
      1. 在应用中删除笔记。 [516]
      2. 笔记删除快速,前端自动更新。 [517]
      3. 基本笔记应用设置完成,接下来实现AI聊天机器人。 [518]

6. AI聊天机器人核心功能设置 [519]

  1. OpenAI API设置:
    1. 使用OpenAI ChatGPT API。 [519]
    2. 需要支付详情(API费用极低)。 [520]
    3. Vercel AI SDK支持多种LLM提供商。 [523]
    4. 在OpenAI账户中设置计费并创建API Key。 [526]
    5. API Key存储:
      1. 将API Key存储在Convex仪表板的环境变量中。 [530]
      2. 变量名必须为OPENAI_API_KEY,AI SDK会自动识别。 [534]
      3. 粘贴API Key并保存。 [537]
  2. 创建AI服务器端点 (http.ts):
    1. 在http.ts文件中创建公开服务器端点。 [539]
    2. 这些是HTTP actions,不同于Convex查询或突变。 [541]
    3. http.route:
      1. 设置path为/api/chat。 [546]
      2. method为POST,发送聊天消息。 [551]
      3. handler(httpAction):
        1. 认证用户:获取userId。 [556]
        2. 权限检查:若userId为null,返回401 Unauthorized响应。 [558]
        3. 读取消息:从request.json()中解构messages。 [564]
          1. 消息类型为UIMessage[](从ai包导入)。 [566]
        4. 限制消息数量:取最后10条消息(messages.slice(-10)),减少请求成本。 [571]
        5. 流式文本生成:
          1. 调用AI SDK的streamText函数(从ai包导入)。 [578]
          2. model:使用OpenAI.chat('...')定义模型(例如3.5-Turbo或4.0-Turbo)。 [582]
          3. systemPrompt:定义AI角色和指令。 [592]
          4. messages:将lastMessages转换为model messages。 [595]
          5. onResponse:处理错误。 [600]
        6. 返回流式响应:返回result.toUIMessageStreamResponse()。 [603]
          1. 将模型消息转回UI消息。 [605]
          2. 实现文本流式传输效果。 [606]
    4. CORS配置:
      1. 在http.ts中添加另一个http.route块,配置CORS。 [611]
      2. 在toUIMessageStreamResponse函数中添加headers,允许所有来源。 [618]
  3. 前端AI聊天按钮 (AIChatButton.tsx):
    1. Convex Site URL:
      1. 从.env获取NEXT_PUBLIC_CONVEX_URL。 [628]
      2. 使用正则表达式将URL中的cloud替换为site,以适应HTTP Actions。 [631]
    2. input状态:管理聊天输入框的内容。 [640]
    3. 认证Token:
      1. 使用useAuthToken(从Convex导入)获取认证Token。 [643]
      2. HTTP Actions需要手动发送认证Token。 [643]
    4. useChat钩子:
      1. 从AI SDK的React包导入。 [648]
      2. transport配置:
        1. new DefaultChatTransport()。 [652]
        2. api:设置为/api/chat的完整URL(使用Convex Site URL)。 [654]
        3. headers:发送Authorization: Bearer [token]。 [658]
    5. onSubmit函数:
      1. 阻止表单默认提交行为(刷新页面)。 [670]
      2. 检查输入是否为空(input.trim())。 [673]
      3. 调用sendMessage函数(来自useChat钩子),传入input。 [675]
      4. 提交后清空输入框。 [677]
    6. 表单绑定:
      1. 将onSubmit绑定到聊天表单。 [684]
      2. 将input状态绑定到文本区域的value。 [694]
      3. 将e.target.value绑定到onChange更新input状态。 [696]
    7. 渲染消息:
      1. 使用messages.map渲染messages数组中的每条消息。 [688]
      2. 暂时使用JSON.stringify显示完整消息内容。 [691]
    8. 测试聊天:
      1. 发送消息给AI。 [698]
      2. AI响应会流式显示,但格式和滚动功能待完善。 [700]
  4. 聊天消息组件 (ChatMessage):
    1. 在AIChatButton.tsx中定义ChatMessage函数。 [706]
    2. 接收message(UIMessage类型)作为props。 [709]
    3. 渲染最新步骤:
      1. currentStep = message.parts[message.parts.length - 1]。 [719]
      2. 使用预设UI代码(视频描述提供链接)。 [722]
      3. 导入Markdown组件,用于渲染Markdown格式文本。 [728]
      4. 根据message.role(user或assistant)调整样式和对齐。 [731]
    4. 在消息渲染处使用ChatMessage组件。 [734]
    5. 测试格式化:聊天消息现在显示格式化后的Markdown文本。 [741]
  5. 聊天体验改进:
    1. 初始欢迎消息:
      1. 创建initialMessages常量(UIMessage[]类型)。 [757]
      2. 设置id、role(assistant)和parts(文本内容)。 [761]
      3. 将initialMessages传递给useChat钩子的messages参数。 [770]
      4. 聊天框初始显示欢迎消息。 [772]
      5. initialMessages也会随后续消息一起发送到后端。 [773]
    2. 加载状态与禁用按钮:
      1. 从useChat钩子获取setMessages和status。 [776]
      2. 创建isProcessing常量:若status为submitted或streaming则为true。 [779]
      3. 在onSubmit中,若isProcessing为true则阻止发送消息。 [783]
      4. 清空聊天按钮:onClick调用setMessages(initialMessages)。 [786]
      5. clear chat按钮和发送按钮在isProcessing为true时禁用。 [790]
    3. 自动滚动:
      1. 使用useEffect钩子。 [799]
      2. 当open或messages改变时触发。 [807]
      3. 调用messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })。 [803]
      4. 聊天内容自动平滑滚动到底部。 [809]
    4. 回车发送消息:
      1. 创建handleKeyDown函数。 [813]
      2. 若按下Enter键且未按Shift键,则调用onSubmit发送消息。 [816]
      3. 若同时按Shift和Enter,则创建换行。 [819]
      4. 将handleKeyDown绑定到文本区域的onKeyDown事件。 [825]
      5. 修复TypeScript类型错误。 [828]
      6. 测试回车发送和Shift+回车换行。 [832]
    5. 显示加载和错误状态:
      1. 创建lastMessageIsUser常量,判断最后一条消息是否由用户发送。 [836]
      2. 在聊天消息渲染区域下方,根据status显示:
        1. 若status为submitted且lastMessageIsUser为true,显示Loader组件。 [842]
        2. 若status为error,显示ErrorMessage组件。 [848]
      3. ErrorMessage组件显示“Something went wrong, please try again.”。 [850]
      4. 测试加载指示器和错误状态。 [854]

7. 向量嵌入与RAG(检索增强生成) [858]

  1. 向量嵌入概念:
    1. 通过向量嵌入,比较文本的语义。 [858]
    2. 文本被OpenAI的嵌入模型转换为数字数组(向量)。 [860]
    3. 语义相似的文本会产生彼此接近的向量。 [863]
    4. 通过比较向量距离,找到相关文本。 [869]
  2. 存储向量嵌入:
    1. Convex原生支持向量存储,方便将嵌入存储在同一数据库中。 [871]
    2. convex/schema.ts更新:
      1. 添加nodeEmbeddings表。 [874]
      2. 字段定义:
        1. content:原始文本,v.string。 [877]
        2. embedding:向量数组,v.array(v.float64())。 [880]
        3. nodeId:关联笔记ID,v.id('nodes')。 [883]
        4. userId:关联用户ID,v.id('users')。 [885]
      3. 索引:
        1. 创建by_noteId索引。 [887]
        2. 添加vectorIndex,命名为by_embedding。 [890]
        3. vectorField设为embedding。 [892]
        4. dimensions设为1536(OpenAI默认)。 [894]
        5. filterFields包含userId,允许按用户过滤。 [896]
      4. 保存更改,确保npx convex dev运行。 [898]
  3. 生成嵌入 (src/lib/embeddings.ts):
    1. 在src/lib文件夹中创建embeddings.ts文件。 [899]
    2. embeddingModel:定义嵌入模型(OpenAI.embedding('text-embedding-3-small'))。 [901]
    3. 文本分块:
      1. generateChunks函数:将输入文本按双换行符(\n\n)分割成块。 [906]
      2. 去除空块。 [917]
      3. 分块是为了更精确地捕获文本含义。 [906]
    4. generateEmbeddings函数(复数):
      1. 异步函数,接收input字符串。 [922]
      2. 返回包含文本和嵌入的数组。 [925]
      3. chunks:调用generateChunks分块。 [930]
      4. embedMany:调用AI SDK的embedMany函数批量生成嵌入。 [934]
      5. 将嵌入与原始文本(chunks)一起返回。 [939]
  4. 创建笔记与嵌入 (convex/nodesActions.ts):
    1. Convex mutations不能直接调用第三方API。 [943]
    2. 使用Convex actions调用外部API。 [949]
    3. 在convex文件夹中创建nodesActions.ts文件。 [952]
    4. 顶部添加"use node"以使用Node.js API。 [953]
    5. createNote action:
      1. 导出createNote常量,赋值为action。 [956]
      2. 复制旧createNote mutation的args和returns。 [958]
      3. handler(异步函数):
        1. 认证用户:获取userId。 [963]
        2. 组合文本:将笔记title和body组合成一个字符串。 [966]
        3. 生成嵌入:调用await generateEmbeddings,传入组合文本。 [971]
        4. 内部突变:
          1. 将原createNote mutation重命名为createNoteWithEmbeddings。 [976]
          2. 更改为internalMutation,只能由Convex函数调用。 [976]
          3. 移除认证逻辑,通过参数传入userId和embeddings。 [980]
          4. 在createNoteWithEmbeddings中,除了插入笔记,还循环插入每个嵌入到nodeEmbeddings表。 [991]
          5. Convex mutation在单个事务中执行所有数据库操作,确保原子性。 [1003]
        5. 调用内部突变:在action中调用await context.runMutation(internal.nodes.createNoteWithEmbeddings, ...)。 [1009]
        6. 返回新笔记的_id。 [1015]
    6. 更新前端调用:
      1. 在CreateNoteButton.tsx中,将useMutation改为useAction。 [1036]
      2. 调用路径更新为API.nodesActions.createNote。 [1037]
    7. 测试创建笔记与嵌入:
      1. 删除现有笔记,创建新笔记。 [1040]
      2. 在Convex仪表板的nodeEmbeddings表中,可以看到分块后的嵌入数据。 [1044]
  5. 删除笔记与嵌入 (convex/nodes.ts):
    1. 在deleteNote mutation中。 [1020]
    2. 在删除笔记后,查询所有与该笔记ID相关的嵌入。 [1023]
    3. 循环遍历并使用context.db.delete删除每个嵌入。 [1028]
    4. 所有删除操作在单个事务中执行。 [1028]
  6. 生成单次嵌入 (src/lib/embeddings.ts):
    1. 在embeddings.ts中添加generateEmbedding函数(单数)。 [1053]
    2. 接收text,返回单个向量数组。 [1055]
    3. 调用AI SDK的embed函数。 [1057]
  7. 查找相关笔记 (convex/nodesActions.ts):
    1. 在nodesActions.ts中导出findRelevantNodes函数。 [1060]
    2. internalAction类型:只能由其他Convex函数调用。 [1062]
    3. args:接收query字符串和userId。 [1068]
    4. handler(异步函数):
      1. 生成查询嵌入:调用generateEmbedding,传入args.query。 [1073]
      2. 向量搜索:
        1. 使用context.vectorSearch在nodeEmbeddings表进行搜索。 [1076]
        2. 指定by_embedding索引。 [1079]
        3. vector为生成的查询嵌入。 [1080]
        4. limit为16。 [1082]
        5. filter根据userId过滤。 [1084]
        6. 结果按相关性自动排序。 [1087]
      3. 过滤结果:
        1. resultsAboveThreshold:过滤score > 0.3的嵌入。 [1090]
        2. embeddingIds:提取过滤后嵌入的ID。 [1098]
      4. 获取对应笔记:
        1. 在convex/nodes.ts中添加fetchNodesByEmbeddingIds函数。 [1101]
          1. internalQuery类型,接收embeddingIds数组。 [1104]
          2. 获取每个嵌入,提取nodeId。 [1111]
          3. 使用Set获取唯一的nodeId。 [1116]
          4. 循环获取这些唯一的笔记并返回。 [1128]
        2. 在findRelevantNodes中调用await context.runQuery(internal.nodes.fetchNodesByEmbeddingIds, { embeddingIds })。 [1134]
      5. 返回结果:返回处理后的相关笔记(仅包含ID、title、body、creationTime)。 [1138]
  8. 集成RAG到AI聊天端点 (http.ts):
    1. 更新系统提示:
      1. 修改systemPrompt,指示AI可以搜索笔记。 [1148]
      2. 要求AI使用Markdown格式。 [1153]
      3. 提供笔记链接格式/notes/note ID。 [1154]
      4. 保持回答简洁。 [1158]
    2. 工具调用 (Tools):
      1. 在streamText函数中添加tools对象。 [1163]
      2. 工具定义:
        1. name: findRelevantNodes。 [1165]
        2. description: 帮助AI决定何时使用该工具(“Retrieve relevant notes from the database based on the user's query”)。 [1167]
        3. parameters: 使用Zod定义工具所需的参数。 [1171]
          1. 需要query参数,类型为Z.string。 [1174]
        4. execute函数:实际执行工具逻辑。 [1179]
          1. 记录AI生成的query。 [1182]
          2. 调用await context.runAction(internal.nodesActions.findRelevantNodes, { query, userId })。 [1186]
          3. 将结果映射为LLM所需的数据(ID、title、body、creationTime)。 [1194]
    3. maxSteps配置:
      1. 在useChat钩子中设置maxSteps为3。 [1203]
      2. 允许工具调用和文本生成两个步骤。 [1205]
  9. 测试AI搜索笔记:
    1. 创建包含有用信息的笔记。 [1208]
    2. 提问“What's my Wi-Fi password?”。 [1210]
      1. AI会进行工具调用,然后提供密码和笔记链接。 [1212]
      2. Convex控制台显示AI生成的查询和相关向量嵌入。 [1214]
    3. 提问“Give me my list of movies to watch”或“my grocery list”。 [1221]
      1. AI会自动生成查询,执行向量搜索并找到相关信息。 [1226]
    4. 创建新笔记(如“My favorite color is blue”),AI可立即搜索。 [1228]
    5. 无需搜索的问题(如“Thank you”),AI会直接回复。 [1235]
  10. 工具调用时的文本显示:
    1. 在ChatMessage组件中。 [1238]
    2. 若currentStep.type为tool_invocation,则显示“searching nodes...”动画。 [1241]
    3. 测试显示“searching notes...”然后流式传输答案。 [1248]

8. 生产部署 [1253]

  1. Convex生产部署:
    1. Convex自动提供开发和生产环境。 [1256]
    2. 在Convex仪表板中,进入项目设置,点击URL and Deploy Key。 [1258]
    3. 点击Create a production deployment。 [1259]
    4. 生成生产部署密钥:
      1. 点击Generate production deploy key。 [1262]
      2. 输入名称(例如AI SDK node app)。 [1263]
      3. 复制生成的密钥。 [1265]
    5. 生产环境变量:
      1. 在settings -> environment variables中,添加OPENAI_API_KEY。 [1266]
      2. 从开发部署复制值并粘贴。 [1268]
  2. Vercel部署Next.js应用:
    1. 将代码推送到GitHub仓库。 [1270]
    2. 在Vercel仪表板中创建新项目。 [1273]
    3. 导入GitHub仓库。 [1275]
    4. 构建设置:
      1. 覆盖Build Command为npx convex deploy --cmd "npm run build"。 [1278]
      2. 此命令用于将必要代码推送到Convex生产部署。 [1281]
    5. 环境变量:
      1. 添加CONVEX_DEPLOY_KEY,值为之前复制的生产部署密钥。 [1282]
      2. 添加CONVEX_URL,值为Convex仪表板中的Deployment URL。 [1285]
    6. 点击Deploy。 [1289]
  3. Convex生产认证:
    1. 在项目命令行中执行npx @convex-dev/auth --production。 [1290]
    2. 等待Vercel部署完成,获取生产URL。 [1293]
    3. 将Vercel提供的生产URL输入到命令提示符中。 [1296]
    4. 确认后,Convex会在后端配置新密钥,启用生产认证。 [1299]
  4. 测试生产应用:
    1. 访问部署后的Vercel URL。 [1302]
    2. 由于是独立数据库,需创建新账户。 [1303]
    3. 创建笔记并测试AI聊天功能。 [1306]
    4. AI成功搜索笔记并回答问题。 [1311]
  5. 总结:
    1. 教程提供了Vercel AI SDK文档链接。 [1316]
    2. 鼓励用户提出更多高级聊天机器人教程需求。 [1317]