切图仔实现AI接口联网手册

124 阅读9分钟

前言

最近拿AI接口来搭建QQ机器人玩,遇到了模型信息不实时的问题,如今市面大部分AI模型都是自带联网搜索的,遂好奇是怎么实现的,于是把学习的过程记录下来分享一下~

描述

阅读需知:

  • 这里不进行模型原理的深入研究,只针对如何使用模型提供的 API 来实现功能
  • 使用或了解过一些请求模型 API 的概念
  • 本文所用模型为 deepseek 官方接口,使用其他的 API,如 硅基流动 等第三方渠道不会有太大区别,基本都符合 OpenAI 规范

模型上下文

这个应该算是基本内容了,不过为了后续展开这里还是简单提一下,即:

模型本身是没有记忆的,对模型而言,当调用模型接口时,每一次都是完全新的对话,如果想让模型记住之前的内容,需要把所有的上下文(包括每一次用户提问,以及模型的回答等),都包含在本次对话传递给模型,所以上下文的 token 会不断膨胀

大概会是以下效果:

import OpenAI from "openai"

const openai = new OpenAI({ 
  baseURL: 'https://api.deepseek.com', 
  apiKey: '$yourKey', 
  defaultHeaders: { 'Content-Type': 'application/json' }, 
})

// 第一次问答
openai.chat.completions.create({
  messages: [
    { role: 'user', content: '你好' }
  ]
})

// 第二次问答
openai.chat.completions.create({
  messages: [
    { role: 'user', content: '你好' },
    { role: 'assistant', content: '你好,有什么可以帮你的吗' },
    { role: 'user', content: '为什么仿生人会梦到电子羊' },
  ]
})

// 第三次回答...
  

Function Call

已知模型没有记忆,需要通过我们传递的上下文的内容来理解要求并输出,如果我们想让模型来进行联网搜索,首先我们要做的,就是要让模型理解 什么时候该去执行联网搜索 这个行为,此时就需要引入 function call 的概念:

它允许语言模型与外部工具和 API 集成,简单理解的话主要功能就两个:

  • 识别应该调用函数的时机
  • 生成函数调用的参数

因此,现在需要做的是,定义一个 tool,它的类型大概如下所示:

type FunctionTool = { 
  // 指定工具类型为"function",区别于其他可能的工具类型 
  type: "function" 
  // function对象包含了函数的定义和参数规范 
  function: { 
    // 函数的名称,用于调用识别 
    name: string, 
    
    // 可选的函数描述,说明函数的用途和功能 
    // 这对AI理解何时应该调用此函数非常重要 
    description?: string, 
    
    // 函数的参数定义,使用JSON Schema格式规范 
    parameters: { 
      // 指定参数集合为对象类型 
      type: "object" 
      
      // 定义各个参数的属性和验证规则 
      properties: 
      { 
        // 参数名称作为key 
        [key: string]: { 
          // 参数的数据类型
          type: "string" | "number" | "boolean" | "object" | "array" 
          
          // 可选的参数描述,说明参数的用途、格式或约束 
          // 这帮助AI理解应提供什么样的值 
          description?: string 
          
          // 可选的枚举列表,限制参数值必须是指定选项之一 
          // 例如:["light", "dark", "auto"]表示这三个选项中只能选一个 
          enum?: string[] 
          
          // 其他可能的JSON Schema字段: 
          // minimum/maximum: 数值类型的最小/最大值 
          // minLength/maxLength: 字符串类型的最小/最大长度 
          // pattern: 字符串必须匹配的正则表达式模式 
          // format: 特定格式如"email"、"date"、"uri"等 
          // items: 定义数组元素的模式(当type为"array"时) 
          // properties: 定义嵌套对象的属性(当type为"object"时) 
          // required: 嵌套对象中的必需属性(当type为"object"时) 
        } 
      }, 
      // 可选的必需参数列表,指定哪些参数是调用函数时必须提供的 
      // 例如:["query", "count"]表示调用时必须提供这两个参数 
      required?: string[] 
    } 
  }
}

对话检查

当定义好了一个 tool 后,附在请求字段中发送给对话模型

const getWeatherTool = {
  type: 'function',
  function: {
    name: 'get_weather',
    description: '严格遵守仅当用户问题中询问天气相关问题时触发',
    parameters: {
      type: 'object',
      properties: {},
      required: []
    }
  }
}

const checkChat = await openai.chat.completions.create({
  model: 'deepseek-chat',
  messages: [{ role: 'user', content: '你好,今天广州天气如何' }],
  tools: [getWeatherTool],
  tool_choice: 'auto'
})

上面即是一个简单的触发 function call 请求方式,需要了解以下几点概念:

  • 此次的对话请求并不是最终要发给用户的消息内容,这相当于一个检查请求,检查此次用户对话是否需要触发
  • 为了避免混淆,此次检查对话一般不加入 prompt 内容,让最原始的模型进行判断即可

调用策略

tool_choice 字段决定了模型采用策略,一般让它自动识别即可,如果需要强制执行某个 tool,则定义 tool_choice: { type: 'function', function: { name: '$functionName' } }

一次检查函数调用对话,有以下决策过程:

  • 分析用户消息
  • 评估可用的函数定义
  • 决定是否调用函数
  • 如果决定调用,则生成适当的参数

即模型是否决定调用函数,是由多个因素共同作用产生的结果

  1. function.description
    • 重要程度:★★★★★
    • 作用:提供函数的总体用途和调用时机
    • 最佳实践:在这里明确说明何时应该何时不应该调用此函数
description: '仅当用户明确询问天气信息时调用此函数'  
  1. parameters
    • 重要程度:★★★★☆
    • 作用:通过参数结构暗示调用条件
    • 影响:
      • 参数类型和必需性暗示了输入条件
      • 参数描述进一步细化调用场景
      • 模型会评估能否从用户输入中提取满足参数要求的值
{  
  type: "function",  
  function: {  
    name: "get_weather",  
    // 编写严格明确的description
    description: "严格限制:仅当用户明确询问天气信息且指定具体地点时才调用。用户必须明确表达查询天气的意图(如'北京天气怎么样'),且必须包含具体地点。对于任何其他类型的问题、评论或无地点指定的天气问询,均不应调用此函数。",  
    // 设计明智的参数结构
    parameters: {  
      type: "object",  
      properties: {  
        location: {  
          type: "string",  
          description: "用户明确指定的城市或地区名称"  
        },  
        query_intent: {  
          type: "string",  
          // 增加枚举参数限定场景
          enum: ["weather_query", "not_weather_query"],  
          description: "判断用户意图是否为天气查询。仅当为'weather_query'时才应调用函数。"  
        },  
        has_location: {  
          // 使用逻辑参数控制调用条件
          type: "boolean",  
          description: "确认用户是否提供了具体地点。必须为true才能调用函数。"  
        }  
      },  
      required: ["location", "query_intent", "has_location"]  
    }  
  }  
}  
  1. 上下文历史
    • 重要程度:★★★★☆
    • 作用:提供语义线索
    • 影响方式:模型会考虑整个对话流程和用户意图
  2. 函数名称
    • 重要程度:★★★☆☆
    • 作用:提供情境理解
    • 影响方式:描述性的函数名能帮助模型理解函数用途
name: "get_weather"

上面例子中,假设用户问:"北京今天天气怎么样?",模型大致决策过程如下:

  1. 分析用户消息:识别出"天气"主题和"北京"地点
  2. 检查可用函数:发现 get_weather 函数
  3. 阅读函数描述:"仅当用户明确询问天气信息时调用此函数"
  4. 评估参数结构:需要一个location参数
  5. 判断能否提供参数:能从用户消息中提取"北京"作为location
  6. 做出决策:条件满足,决定调用函数

重新对话

一般模型决定调用 function call,那么此次对话的消息有以下特点:

  • 当决定调用工具而不是直接回答时,content 通常为 null

  • 返回会带上 tool_calls 字段,里面包含了本次采用 tool 的信息

    • id:唯一标识符,用于后续跟踪此调用
    • type:固定为 function
    • function.name:匹配您定义的函数名称
    • function.arguments: 包含JSON格式的参数字符串,这是一个经过转义的JSON字符串
const checkChat = await openai.chat.completions.create({
  model: 'deepseek-chat',
  messages: [{ role: 'user', content: '你好,今天广州天气如何' }],
  tools: [getWeatherTool],
  tool_choice: 'auto'
})

const data = checkChat.choices[0].message

console.log(data)
// 结构如下所示:
{  
  "role": "assistant",  
  "content": null,  
  "tool_calls": [  
    {  
      "id": "call_abc123def456",  
      "type": "function",  
      "function": {  
        "name": "get_weather",  
        // 如果在parameters声明了参数,此次模型会把对应的信息结果塞在这里,如有必要可辅助后续操作
        "arguments": "{"location":"北京","query_intent":"weather_query","has_location":true}"  
      }  
    }  
  ]  
}

此时已经可以通过模型判断出用户需要进行额外操作的意图,那么只需要根据这个意图,去执行本地的特定方法就行,其流程需要注意以下几点:

  • 新的对话中,不需要再带上用于触发 function call 的字段
  • 新的对话中,需要在消息上下文中加入完整的触发 function call 时消息结构,同时也需要带上本地函数的执行结果数据,并以对应的 toolId 进行绑定,这样相互作用模型才能了解具体的流程
const checkChat = await openai.chat.completions.create({
  model: 'deepseek-chat',
  messages: [{ role: 'user', content: '你好,今天广州天气如何' }],
  tools: [getWeatherTool],
  tool_choice: 'auto'
})

const data = checkChat.choices[0].message


if(Array.isArray(data)) {
  const toolCall = message.tool_calls[0]
  
  if (toolCall.function.name === 'get_weather') {
    // 假设本地有一个自定义的getWeatherForLocation方法
    const args = JSON.parse(toolCall.function.arguments)
    const location = args.location
    const weatherData = await getWeatherForLocation(location)
    
    // 重新开始一个新的对话,带上上下文的同时,带上tool信息
    const finalChat = await openai.chat.completions.create({  
        model: 'deepseek-chat',  
        messages: [  
          // 原始用户消息  
          { role: 'user', content: '你好,今天广州天气如何' },  
          // 包含工具调用的AI响应  
          checkChat.choices[0].message,  
          // 工具执行结果  
          {   
            role: 'tool',   
            tool_call_id: toolCall.id,   
            content: JSON.stringify(weatherData)  
          }  
        ]  
      }) 
  }
}

执行函数

由上面例子可知,最终需要把执行函数的返回内容加入到对话上下文中,虽然返回的格式没有严格的规定,但是有一些最佳实践可以遵循:

  • 结构化数据:便于模型理解和处理
  • 完整信息:包含回答用户问题所需的所有数据
  • 可序列化:可以转换为 JSON 字符串
  • 合理深度:避免过度嵌套

例子中的获取天气返回格式可以如下所示

// 简单版本
{  
  "location": "北京",  
  "temperature": "12°C",  
  "condition": "晴天",  
  "date": "2023-11-24"  
}  
// 复杂版本
{  
  "location": "北京",  
  "current": {  
    "temperature": 12,  
    "temperature_unit": "celsius",  
    "condition": "晴",  
    "humidity": 45,  
    "wind": {  
      "speed": 10,  
      "direction": "东北",  
      "unit": "km/h"  
    }  
  },  
  "forecast": [  
    {  
      "date": "2023-11-25",  
      "day_of_week": "星期六",  
      "condition": "多云",  
      "temperature": {  
        "max": 14,  
        "min": 5,  
        "unit": "celsius"  
      },  
      "chance_of_rain": "20%"  
    }  
    // 可以包含更多天的预报  
  ],  
  "metadata": {  
    "last_updated": "2023-11-24T14:30:00+08:00",  
    "data_source": "OpenWeather API"  
  }  
}

metadata 字段是一种包含 关于数据的数据 的常见模式,它提供了上下文信息,而不是核心数据本身,其字段名称不是一个严格规定的固定标准,而是一种广泛采用的习惯性约定,其可能功能如下:

  1. 对话连贯性

    • AI可以利用最后更新时间生成更自然的回复:

      • "北京的天气数据是30分钟前更新的,现在温度12°C"
  2. 可能的后续问题处理

    • 用户:"这个数据可靠吗?"
    • AI可以引用元数据:"数据来自OpenWeather API,这是一个知名的天气数据提供商"
  3. 限制澄清

    • 当数据较旧时自动提供说明:

      • "请注意,这是6小时前的数据,实际天气状况可能已有变化"

同时,还应考虑到接口报错的情况:

// 接口错误时传递的格式
{  
  "error": true,  
  "message": "无法获取北京的天气信息",  
  "reason": "API服务不可用"  
}  

联网功能

看到这里,大伙应该已经知道该怎么让 AI 联网搜索了,总得思路就一条:

用户输入提问用提示词规定模型判断是否需要联网搜索需要联网时触发对应的 function call执行本地自定义处理联网数据方法把处理好的数据重新发给模型用户得到最终答案

这里面的过程可以很简单,也可以很复杂

想要准确让模型识别哪些问题需要联网,可以用很复杂的提示词来严格规范

而如何拿到联网搜索的数据,更是很有讲究,可以结合搜索引擎 API + 页面爬虫 + 检查是否满足答案,判断是否进入新的一轮重新搜索,这些完全可以根据具体场景拓展开来

因为本文只是一篇很简单的介绍向博客,因此这里尽可能选择简单的方式进行讲解,具体复杂的做法就看大伙想做到什么程度啦~

下面主要用现成的搜索引擎 API 来搭建这个功能,如果不想借助第三方可以手动用 puppeteer 去手动搜索再爬结果也不是不行

目前市面上可供选择的搜索引擎 API 有:

  • Google Custom Search API
  • Bing Web Search API
  • 百度云智能提供
  • 阿里云提供

等等,不过这些官方的要么就是注册麻烦要钱,要么就是只提供给公司不供个人,所以目前我选用的是 serper.dev,它拿的还是谷歌的搜索结果,且注册就送一个月的 2500 条搜索额度,适合快速拿去测功能,如果还有好用的搜索引擎 API 接口也麻烦分享一下哦

下面贴一下我简单测试用的 tool 定义,实际根据个人场景完全自定义即可,此处仅做参考

export const webSearchTool = {
  type: 'function',
  function: {  
    name: "search_web",  
    description: "当需要实时或最新信息时搜索网络。适用于:1)模型知识截止日期后的事件/数据 2)需要最新信息的查询 3)模型不确定的事实性问题",  
    parameters: {  
      type: "object",  
      properties: {  
        query: {  
          type: "string",  
          description: "搜索查询词,应简洁包含关键信息,语言为用户发言语言"  
        },  
        current_date: {  
          type: "string",  
          description: "当前日期时间,格式为ISO 8601 (YYYY-MM-DD)"  
        },  
        time_sensitivity: {  
          type: "string",  
          enum: ["high", "medium", "low"],  
          description: "查询的时效性敏感度:high(需要最新数据),medium(近期数据可接受),low(历史数据可接受)"  
        },  
        reason_for_search: {  
          type: "string",  
        description: "解释为什么需要搜索而不是使用模型知识"  
        }  
      },  
      required: ["query", "current_date", "reason_for_search"]  
    }  
  } 
}

流程大致代码

const checkChat = await openai.chat.completions.create({
    model: models.deepseek,
    messages: [{ role: 'user', content: $content }],
    tools: [webSearchTool],
    tool_choice: 'auto'
  })
  
const checkChatData = checkChat.choices[0].message

const useFunctionCall = Array.isArray(checkChatData.tool_calls)

const messages = [...$historyContent]  // 这里包含之前的想要加入的所有上下文信息

if(useFunctionCall) {
  const callId = checkChatData.tool_calls[0].id
  const functionName = checkChatData.tool_calls[0].function.name
  const functionArguments = JSON.parse(checkChatData.tool_calls[0].function.arguments)

  // 联网搜索
  if(functionName === 'search_web') {
    const searchWord = functionArguments.query
    const searchContent = await webSearchFunction(searchWord)
    
    // 把联网结果添加进上下文
    messages.push(checkChatData)
    messages.push({
      role: 'tool',
      tool_call_id: callId,
      content: JSON.stringify(searchContent)
    })
  }
}

// 开启新的对话
const compeletion = await openai.chat.completions.create({
  model: models.deepseek,
  messages: messages,
})


结语

莫得了,各位闲的没事干可以研究玩玩~

image.png