通过`ai.js`与`@ai-sdk`实现前后端tool注入与交互

示例代码摘取自开源项目:next-ai-draw-io

1. 总述

ai.js 中,工具(Tools)通过 streamText 配置的 tools 参数注入,分为两类:

  • 服务端工具 :配置包含 execute 异步函数,可在服务端自动执行并返回结果;
  • 客户端工具 :仅配置元信息(description/inputSchema),无 execute 函数,需前端执行后手动回传结果。

2. 工具注入(streamText 核心配置)

javascript 复制代码
// 服务端 streamText 调用(核心代码)
const result = await streamText({
  model: openai('gpt-4o'),
  messages: allMessages, // 对话历史
  // 注入工具
  tools: {
    // 1. 服务端工具:带 execute 函数
    get_shape_library: {
      description: "获取draw.io形状库文档", // 模型识别工具的描述
      inputSchema: z.object({ // 入参校验Schema(zod)
        library: z.string().describe("形状库名称,如aws4")
      }),
      // 服务端自动执行的函数,返回值直接传给模型
      execute: async ({ library }) => {
        // 服务端业务逻辑(文件读取/API调用等)
        const content = await fs.readFile(`./docs/${library}.md`, 'utf-8');
        return content; // 结果自动返回给模型,无需手动处理
      }
    },
    // 2. 客户端工具:仅元信息,无 execute
    display_diagram: {
      description: "在draw.io渲染图形(客户端执行)",
      inputSchema: z.object({
        xml: z.string().describe("draw.io的mxCell XML字符串")
      })
      // 无 execute 函数,需前端处理
    }
  },
  experimental_repairToolCall: async ({ toolCall, error }) => {
    // 修复模型返回的工具入参格式错误(如截断的JSON)
    if (error.name === 'InvalidToolInputError') {
      return { ...toolCall, input: jsonrepair(toolCall.input) };
    }
    return null;
  },
  onFinish: ({ totalUsage }) => {
    // 对话结束回调(记录token/日志)
  }
});

// 转换为前端可识别的流式响应
return result.toUIMessageStreamResponse({
  sendReasoning: true, // 返回模型推理过程
});

注解

  • tools 对象中,每个键为工具名称,值为工具配置;
  • inputSchema(zod 格式)用于校验模型传入的工具参数;
  • toUIMessageStreamResponse 是服务端与前端 useChat 通信的关键,格式化流式响应为前端可解析的结构(包含 toolCall/text 等字段)。

3. 前端工具交互(useChat 核心配置)

tsx 复制代码
// 前端 useChat 调用
const { messages, sendMessage, addToolOutput } = useChat({
  // 配置传输层,对接服务端 /api/chat 接口
  transport: new DefaultChatTransport({
    api: '/api/chat',
  }),
  // 模型触发工具调用时的回调
  onToolCall: async ({ toolCall }) => {
    const { id: toolCallId, toolName, input: toolInput } = toolCall;
    
    try {
      // 区分工具类型处理
      switch (toolName) {
        // 客户端工具:前端执行逻辑 + 手动回传结果
        case 'display_diagram': {
          const { xml } = toolInput as { xml: string };
          // 前端业务逻辑(如调用draw.io API渲染图形)
          await renderDiagram(xml);
          
          // 手动回传结果给模型
          addToolOutput({
            toolCallId: toolCallId, // 必须匹配模型返回的ID(关键)
            toolName: toolName,
            output: JSON.stringify({ // 结果需序列化为字符串
              success: true,
              message: '图形渲染成功'
            })
          });
          break;
        }
        // 服务端工具:前端无需处理,等待服务端自动执行
        case 'get_shape_library': {
          console.log('等待服务端返回形状库结果');
          break;
        }
        default:
          // 未知工具返回错误
          addToolOutput({
            toolCallId,
            toolName,
            output: '',
            error: `未知工具:${toolName}`
          });
      }
    } catch (error) {
      // 工具执行失败,回传错误信息
      addToolOutput({
        toolCallId,
        toolName,
        output: '',
        error: (error as Error).message
      });
    }
  },
  // 工具结果回传后自动触发模型继续回复
  sendAutomaticallyWhen: ({ messages }) => true,
  onError: (error) => console.error('聊天错误:', error)
});

注解

  • DefaultChatTransport:封装前端与服务端的通信逻辑,自动解析服务端返回的流式响应;
  • onToolCall:前端捕获模型工具调用指令的唯一入口;
  • addToolOutput:客户端工具结果回传的核心方法,toolCallId 必须与模型返回的 ID 一致(否则模型无法关联结果);
  • sendAutomaticallyWhen: () => true:确保工具结果回传后,模型自动生成最终回复,完成闭环。

二、服务端工具 vs 客户端工具:交互核心区别

维度 服务端工具(带 execute) 客户端工具(无 execute)
执行主体 服务端(Node.js) 前端(浏览器 / 客户端)
核心触发逻辑 AI.js 自动触发 execute 函数 前端 onToolCall 回调手动触发
结果返回方式 execute 返回值由 AI.js 自动封装并传给模型 前端调用 addToolOutput 手动回传结果
关键依赖 服务端环境(文件 / 数据库 / 后端 API) 前端环境(DOM / 客户端 SDK / 本地存储)
通信成本 无额外网络请求(服务端内部完成) 需 2 次网络请求(指令下发 + 结果回传)
核心约束 execute 需自行捕获异常,返回友好提示 toolCallId 必须与模型返回的 ID 严格匹配
典型场景 数据查询、文件读取、后端 API 调用 前端 UI 交互、本地操作、客户端 SDK 调用

三、核心流程梳理

1. 服务端工具流程

用户指令 → 服务端 streamText → 模型调用服务端工具 → AI.js 自动执行 execute → 结果自动传给模型 → 模型生成回复 → 前端展示。

2. 客户端工具流程

用户指令 → 服务端 streamText → 模型调用客户端工具 → 服务端通过流式响应下发 toolCall 指令 → 前端 onToolCall 触发 → 执行客户端逻辑 → addToolOutput 回传结果 → 服务端封装结果喂给模型 → 模型生成回复 → 前端展示。

相关推荐
小高0071 天前
Elips:领域模型与 DSL 设计实践:从配置到站点的优雅映射
前端·javascript·后端
远瞻。1 天前
【博客】前端新手如何创建自己的个人网站相册
前端·docker·博客·反向代理
青莲8431 天前
Java并发编程基础与进阶(线程·锁·原子类·通信)
android·前端·面试
祎直向前1 天前
linuxshell测试题
前端·chrome
Honmaple1 天前
SpringBoot + Seata + Nacos:分布式事务落地实战,订单-库存一致性全解析
spring boot·分布式·后端
irises1 天前
开源项目next-ai-draw-io核心能力拆解
前端·后端·llm
德育处主任1 天前
『NAS』部署轻量级开源图片水印工具-ImageWatermarkTool
前端·javascript·docker
掘金者阿豪1 天前
JVM由简入深学习提升(类加载过程+父子类加载过程+类加载器+双亲委派机制)
后端
pas1361 天前
28-mini-vue customRender
前端·javascript·vue.js