前言
最近拿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' } }
一次检查函数调用对话,有以下决策过程:
- 分析用户消息
- 评估可用的函数定义
- 决定是否调用函数
- 如果决定调用,则生成适当的参数
即模型是否决定调用函数,是由多个因素共同作用产生的结果
function.description
:- 重要程度:★★★★★
- 作用:提供函数的总体用途和调用时机
- 最佳实践:在这里明确说明何时应该和何时不应该调用此函数
description: '仅当用户明确询问天气信息时调用此函数'
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"]
}
}
}
- 上下文历史
- 重要程度:★★★★☆
- 作用:提供语义线索
- 影响方式:模型会考虑整个对话流程和用户意图
- 函数名称
- 重要程度:★★★☆☆
- 作用:提供情境理解
- 影响方式:描述性的函数名能帮助模型理解函数用途
name: "get_weather"
上面例子中,假设用户问:"北京今天天气怎么样?",模型大致决策过程如下:
- 分析用户消息:识别出"天气"主题和"北京"地点
- 检查可用函数:发现
get_weather
函数 - 阅读函数描述:"仅当用户明确询问天气信息时调用此函数"
- 评估参数结构:需要一个location参数
- 判断能否提供参数:能从用户消息中提取"北京"作为location
- 做出决策:条件满足,决定调用函数
重新对话
一般模型决定调用 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
字段是一种包含关于数据的数据
的常见模式,它提供了上下文信息,而不是核心数据本身,其字段名称不是一个严格规定的固定标准,而是一种广泛采用的习惯性约定,其可能功能如下:
-
对话连贯性:
-
AI可以利用最后更新时间生成更自然的回复:
- "北京的天气数据是30分钟前更新的,现在温度12°C"
-
-
可能的后续问题处理:
- 用户:"这个数据可靠吗?"
- AI可以引用元数据:"数据来自OpenWeather API,这是一个知名的天气数据提供商"
-
限制澄清:
-
当数据较旧时自动提供说明:
- "请注意,这是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,
})
结语
莫得了,各位闲的没事干可以研究玩玩~