跳转到内容

Function Tool

刀刀
0字
0分钟
2026/8/18

机制概述

大模型操作局限

LLM 可以回答问题,但无法直接执行具体操作(如订票)。大模型只会回复订票方法说明,不会真正调用订票接口。

上下文方案

上下文方案实现

通过上下文方案解决:在提示词中约定,若用户要求订票,则返回 JSON 数据。代码层检测到该数据后,调用预先编写的订票方法。

md
# 角色

你是一个个人助手

# 逻辑

1. 用户如果要订票,则返回如下json,json中的target属性,就是用户本次订票的目的地。当你返回json数据时,不要代码md格式,直接返回json数据。

```json
{
  "type": "ticket",
  "arguments": {
    "target": "目的地"
  }
}
```
js
app.get('/simple', async (req, res) => {
  const { keyword } = req.query
  const system = fs.readFileSync('./context2.md')
  const systemString = systemString.toString()
  const llmres = await openai.chat.completions.create({
    model: 'qwen-plus',
    messages: [
      {
        role: 'system',
        content: systemString,
      },
      {
        role: 'user',
        content: keyword,
      },
    ],
  })
  const message = llmres.choices[0].message
  const content = JSON.parse(message.content)
  if (content.type === 'ticket') {
    // 调用订票方法。真正做订票这个事情,是通过提前写好的代码来实现的。
    const ticket = await ticket(content.arguments.target)
    res.json(ticket)
  }
  res.json(message)
})

上下文方案缺陷

此方案存在以下问题:

  1. 当用户只需普通聊天时,content 返回的是普通文本而非 JSON 字符串,JSON.parse 会报错。核心矛盾:无法区分 content 究竟是要调用内置方法的 JSON,还是纯文本回复。
  2. 并非所有大模型都能稳定理解上下文并返回标准 JSON。部分模型可能在 JSON 前后附加额外文字,或包裹 Markdown 代码块语法。

需要一种机制,将"大模型调用方法"与"纯文本回答"明确区分,且所有模型统一遵循。

Function Tool 机制

意义:LLM 只能做语言回答与逻辑推理,无法执行具体操作。通过 Function Tool 机制,提前声明可用工具,大模型在需要时返回工具调用指令,由代码层执行。

规范:所有大模型接口遵循统一的 Function Tool 定义与输出规范。告知大模型可用工具时使用同一种格式输入,大模型声明调用哪个工具也使用同一种格式输出。

区分:大模型调用 Function Tool 时使用独立字段,不与 content 混合;将执行结果反馈给大模型时,使用专门的 role: tool

定义 Function Tool

js
const response = await openai.chat.completions.create({
  ...message, // model 等属性
  tools: [
    {
      type: 'function', // 固定值
      function: {
        name: 'ticket', // 工具名称
        description: '订票工具', // 工具描述
        // 工具参数,用 JSON Schema 定义
        parameters: {
          type: 'object', // 参数类型
          properties: {
            city: {
              type: 'string', // 参数类型
              description: '出发城市', // 参数描述
            },
          },
          required: ['city'], // 必填参数
        },
      },
    },
  ],
})

Function Tool 实现

js
const tools = [
  {
    type: 'function', 
    function: {
      name: 'ticket', 
      description: '当用户需要订票的时候调用此订票工具', 
      parameters: {
        type: 'object', 
        properties: {
          target: {
            type: 'string', 
            description: '用户要去的城市目的地', 
          }, 
        }, 
        required: ['target'], 
      }, 
    }, 
  }, 
]

const toolMap = {
  ticket() {},
}

module.exports = {
  tools,
  toolMap,
}
js
const { tools, toolMap } = require('./tools.js') 

app.get('/simple', async (req, res) => {
  const { keyword } = req.query
  const system = fs.readFileSync('./context2.md')
  const systemString = systemString.toString()
  const llmres = await openai.chat.completions.create({
    model: 'qwen-plus',
    messages: [
      {
        role: 'system',
        content: systemString,
      },
      {
        role: 'user',
        content: keyword,
      },
    ],
    tools: tools, 
  })
  const message = llmres.choices[0].message
  res.json(message)
})

流式传输

生产环境的接口通常使用 SSE 流式传输。大模型调用 Function Tool 时也会分片段返回,但必须等全部返回后再处理,不可像文本那样边返回边处理

server.js
js
const { tools, toolMap } = require('./tools.js')

app.get('/simple', async (req, res) => {
  const { keyword } = req.query
  const system = fs.readFileSync('./context2.md')
  const systemString = systemString.toString()
  const llmres = await openai.chat.completions.create({
    model: 'qwen-plus',
    messages: [
      {
        role: 'system',
        content: systemString,
      },
      {
        role: 'user',
        content: keyword,
      },
    ],
    tools: tools,
    stream: true, 
  })
  const chunkList = [] // 单纯用来查看数据用,无实际用处
  for await (const chunk of llmres) {
    chunkList.push(chunk)
  }
  fs.writeFileSync('./chunk.json', JSON.stringify(chunkList, null, 2)) // 单纯用来查看数据用,无实际用处
  const message = llmres.choices[0].message
  res.json(message)
})

查看数据格式,choicesdelta 多了 tool_calls 字段,即 Function Tool 的返回结果。返回逻辑如下:

  1. 首次返回完整数据对象,包含 idindextypefunction 对象,function 对象包含 namearguments 两个属性,name 是函数名,arguments 是函数参数。
  2. 后续 arguments 拆成片段逐次返回。属性可能缺失,如 nameid
json
[
  {
    // ...
    "choices": [
      {
        // ...
        "delta": {
          "content": "",
          "role": "assistant",
          "tool_calls": [
            {
              "index": 0,
              "id": "tool_0",
              "type": "function",
              "function": {
                "name": "ticket",
                "arguments": ""
              }
            }
          ]
        }
      }
    ]
  },
  {
    // ...
    "choices": [
      {
        "delta": {
          "tool_calls": [
            {
              "function": {
                "arguments": "beijing"
              },
              "index": 0,
              "id": "",
              "type": "function"
            }
          ]
        }
      }
    ]
  }
]

掌握数据格式后,实现 tool_calls 的处理逻辑。

server.js
js
const { tools, toolMap } = require('./tools.js')

app.get('/simple', async (req, res) => {
  const { keyword } = req.query
  const system = fs.readFileSync('./context2.md')
  const systemString = systemString.toString()
  const llmres = await openai.chat.completions.create({
    model: 'qwen-plus',
    messages: [
      {
        role: 'system',
        content: systemString,
      },
      {
        role: 'user',
        content: keyword,
      },
    ],
    tools: tools,
    stream: true,
  })
  let resObj = {
    role: 'assistant', 
    id: '', 
    content: '', 
  } 
  for await (const chunk of llmres) {
    const delta = chunk.choices[0].delta 
    resObj.id = chunk.id 
    resObj.content += delta.content 
    // 判断是否有方法调用
    if (delta.tool_calls && delta.tool_calls.length > 0) {
      // 拼接 tool_calls 部分
      if (resObj.tool_calls) {
        // 已经是第一个以后的 chunk,直接走拼接
        delta.tool_calls.forEach((toolCall) => {
          const toolIndex = toolCall.index 
          // 根据index找到resObj,要拼接的对象
          const targetTool = resObj.tool_calls[toolIndex] 
          if (chunkTool.function?.name) {
            targetTool.function.name += chunkTool.function.name 
          } 
          if (chunkTool.function?.arguments) {
            targetTool.function.arguments += chunkTool.function.arguments 
          } 
        }) 
      } 
      else {
        // 是第一个 chunk,走赋值
        resObj.tool_calls = delta.tool_calls
      } 
    } 
  } 
  res.end() 
})

执行 Function Tool

执行流程

alt text

执行代码实现

server.js
js
const { tools, toolMap } = require('./tools.js')

app.get('/simple', async (req, res) => {
  const { keyword } = req.query
  const system = fs.readFileSync('./context2.md')
  const systemString = systemString.toString()
  const llmres = await openai.chat.completions.create({
    model: 'qwen-plus',
    messages: [
      {
        role: 'system',
        content: systemString,
      },
      {
        role: 'user',
        content: keyword,
      },
    ],
    tools: tools,
    stream: true,
  })

  let resObj = {
    role: 'assistant',
    id: '',
    content: '',
  }

  for await (const chunk of llmres) {
    const delta = chunk.choices[0].delta
    resObj.id = chunk.id
    resObj.content += delta.content
    // 判断是否有方法调用

    if (delta.tool_calls && delta.tool_calls.length > 0) {
      // 拼接 tool_calls 部分

      if (resObj.tool_calls) {
        // 已经是第一个以后的 chunk,直接走拼接

        delta.tool_calls.forEach((toolCall) => {
          const toolIndex = toolCall.index
          // 根据index找到resObj,要拼接的对象
          const targetTool = resObj.tool_calls[toolIndex]

          if (chunkTool.function?.name) {
            targetTool.function.name += chunkTool.function.name
          }

          if (chunkTool.function?.arguments) {
            targetTool.function.arguments += chunkTool.function.arguments
          }
        })
      } else {
        // 是第一个 chunk,走赋值
        resObj.tool_calls = delta.tool_calls
      }
    }
  }
  if (resObj.tool_calls && resObj.tool_calls.length > 0) {
    const toolCalls = resObj.tool_calls
    for (let toolIndex = 0; toolIndex < toolCalls.length; toolIndex++) {
      const singleToolCall = toolCalls[toolIndex] 
      const toolName = singleToolCall.function.name // 工具名称
      const toolArguments = JSON.parse(singleToolCall.function.arguments) // 工具参数
      const result = await toolMap[name](arguments) // 方法执行可能是异步的
      const toolQueryObj = {
        role: 'tool',
        content: result,
        id: singleToolCall.id,
      }
      const llmres = await openai.chat.completions.create({
        model: 'qwen-plus',
        messages: [
          {
            role: 'system',
            content: systemString,
          },
          ...singleConvertList,
          toolQueryObj,
        ],
        tools: toolList,
        stream: true,
      })
    }
  }
  res.end()
})

前端消息过滤

调用 Function Tool 时前端不应展示中间过程。以订票为例,用户发出订票指令后,应直接收到"订票成功"的答复,工具调用与执行结果均不展示。

  1. 前端判断 content 为空字符串时不展示
  2. roletool 的消息也不展示

贡献者

The avatar of contributor named as 刀刀 刀刀

页面历史

刀刀博客累计访客 人;文档累计访问量共