LLMOps开发(四) Tool + Agent

函数调用

  • 函数调用 =「大语言模型(支持函数调用)+预定Prompt +函数/工具参数列表 +本地调用代码。

大语言模型本身的函数调用并不会调用我们预定义的参数,而是仅仅生成我们需要调用的函数的调用参数而已,具体的调用函数的动作,需要我们再应用中代码实现

  • convert_to_openai_tool
  • 如果需要将工具转换成 openai工具形式参数,可以使用 convert_to_openai_tool() 辅助函数,
js 复制代码
from langchain_community.tools import DuckDuckGoSearchRun

from langchain_core.utils.function_calling import convert_to_openai_tool

search = DuckDuckGoSearchRun()

print(convert_to_openai_tool(search))

自定义工具函数

  • 工具函数也是 Runnable 协议,也可以直接 invoke 调用

  • 实现自定义函数的两种方法

      1. tool 直接包装现有函数
      1. DynamicTool 写清楚函数名称,描述
      1. DynamicStructuredTool 通过 zod 包,生成对应的函数参数约束
  • tool 直接包装现有函数

js 复制代码
import { StructuredTool, tool } from '@langchain/core/tools';

 const stringReverseTool = tool(
      async (input: string) => input.split('').reverse().join(''),
      {
        name: 'string-reverser',
        description:
          'reverses a string. input should be the string you want to reverse.',
        schema: z.string().describe('The string you want to reverser'),
      },
    );
js 复制代码
 async dynamicTool() {
    const stringReverseTool = new DynamicTool({
      name: 'string-reverser',
      description:
        'reverses a string. input should be the string you want to reverse.',
      func: async (input: string) => input.split('').reverse().join(''),
    });

    const res = await stringReverseTool.invoke('hello world');
    console.log(
      '=>(study.tools.service.ts 33) stringReverseTool',
      stringReverseTool,
    );
    console.log('=>(study.tools.service.ts 33) res', res);

    // const tools = [stringReverseTool];
  }
  • 通过 zod 包,生成对应的函数参数约束
js 复制代码
import { z } from 'zod';

const dateDiffTool = new DynamicStructuredTool({
      name: 'date-difference-calculator',
      description: '计算两个日期之间的天数差',
      schema: z.object({
        date1: z.string().describe('第一个日期,以YYYY-MM-DD格式表示'),
        date2: z.string().describe('第二个日期,以YYYY-MM-DD格式表示'),
      }),
      func: async ({ date1, date2 }) => {
        const d1 = new Date(date1);
        const d2 = new Date(date2);
        const difference = Math.abs(d2.getTime() - d1.getTime());
        const days = Math.ceil(difference / (1000 * 60 * 60 * 24));
        return days.toString();
      },
    });

在 ChatModel 使用函数调用

  • 不是每一个模型都支持函数调用,使用前需要注意。目前支持最好的即使 OpenAI,所以一般函数调用的规范都向 OpenAI 模型靠齐
python 复制代码
// 这是 openAi 调用的方法
completion = Client.chat.completions.create( mode1="gpt-3.5-turbo-16k"
    messages=messages,
    tools=tools,
    tool_choice="auto"
)
  • 在 LangChain 中,可以使用 convertToOpenAiTool 方法将自定义工具转化成符合 GPT 模型的参数格式,使用 bind 函数来传递对应的tools和tool_choice
  • 不过 LangChain 添加了 bindTools 可以直接绑定工具函数,内部自动转化了
js 复制代码
 const llmWithTools = new ChatOpenAI({
      modelName: 'gpt-3.5-turbo-16k',
      configuration: {
        baseURL: this.configService.get('OPENAI_API_BASE_URL'),
      },
    }).bindTools([tool], { tool_choice: 'auto' });

使用

  1. 给模型绑定一个 tools,会在调用后生成的结果中,多一个 tool_calls (注意模型不会自动执行函数,他只会返回函数名和传递参数!!!)
js 复制代码
 /**
     *  "tool_calls": [
     *     {
     *       "name": "date-difference-calculator",
     *       "args": {
     *         "date1": "2024-03-16",
     *         "date2": "today"
     *       },
     *       "type": "tool_call",
     *       "id": "call_RNiMIvbyYseWDjt3GObJwbIj"
     *     }
     *   ],
     */
  1. 需要手动判断返回结果,并且自己执行函数,并且一般需要组装 message 再把完整的包括工具返回的函数也返回给大模型执行
js 复制代码
async chainWithTool(query = 'Today is how many days from 2024-03-16') {
    const prompt = await ChatPromptTemplate.fromMessages([
      {
        role: 'system',
        content: `你是OpenAI开发的聊天机器人,请回答用户的问题,如果需要可以调用工具函数`,
      },
      { role: 'user', content: '{question}' },
    ]);

    const tool = this._getDateDiffTool();
    const llm = new ChatOpenAI({
      modelName: 'gpt-3.5-turbo-16k',
      configuration: {
        baseURL: this.configService.get('OPENAI_API_BASE_URL'),
      },
    });
    const llmWithTools = new ChatOpenAI({
      modelName: 'gpt-3.5-turbo-16k',
      configuration: {
        baseURL: this.configService.get('OPENAI_API_BASE_URL'),
      },
    }).bindTools([tool], { tool_choice: 'auto' });

    // const parser = new StringOutputParser();

    const chain = RunnableSequence.from([prompt, llmWithTools]);

    const res = await chain.invoke({
      question: query,
    });

    /**
     *  "tool_calls": [
     *     {
     *       "name": "date-difference-calculator",
     *       "args": {
     *         "date1": "2024-03-16",
     *         "date2": "today"
     *       },
     *       "type": "tool_call",
     *       "id": "call_RNiMIvbyYseWDjt3GObJwbIj"
     *     }
     *   ],
     */
    // 判断是工具调用还是正常输出结果
    const tool_calls = res.tool_calls;
    if (tool_calls.length <= 0) {
      // 没有调用工具
      console.log(res.content);
    } else {
      const message = (await prompt.invoke(query)).toChatMessages();
      for (const tool_call of tool_calls) {
        const tool = tool_call.name;
        const args = tool_call.args;
        const tool_id = tool_call.id;
        console.log('=>(study.tools.service.ts 141) tool', tool);
        console.log('=>(study.tools.service.ts 142) args', args);

        // todo 调用工具 tool 只取到了名字,这个函数又是在示例上的其他方法,无法直接调用
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        const res = await tool.invoke(args);
        message.push(new FunctionMessage(res));
        console.log('=>tool_id', tool_id);
        console.log('=>tool', tool);
        console.log('=>args', args);
        console.log('=>res', res);
      }

      // 最后再把工具的结果返回回去,调用LLM
      return await llm.invoke(message);
    }
    console.log('=>res', res);
  }

不支持函数调用的模型如何调用工具

  • 之前提到并不是所有的模型,都支持函数调用,那么不支持的,应该怎么办
  • 在LangChain 中,除了封装了 convert_to_openai_tool()工具快速将工具转换成 GPT 模型的函数参数,还可以使用 render_text_description_and_args()或者 render_text_description()快速将工具转换成描述文本,使用技巧一模一样,其中前者转换的描述带参数结构信息,后者仅为函数基础介绍。
  • 在 Prompt 中加入该函数的描述
    • 缺点。不能批量调用函数
    • 传入 prompt 耗费大量 token
  • renderTextDescriptionAndArgs 方法
    • 在对应的 prompt 中,预留函数调用描述的位置
js 复制代码
system_prompt = ""您是一个由OpenAI开发的聊天机器人,可以访问以下一组工具。

以下是每个工具的名称和描述:

{rendered_tools}

根据用户输入,返回要使用的工具的名称和输入。

将您的响应作为具有^name 和'arguments 键的JSON块返回。

arguments 应该是一个字典,其中键对应于参数名称,值对应与请求的值。"""

函数调用快速提取结构化数据

目前常见的几种让LLM 结构化输出的策略有:

  1. Prompt:通过 prompt 让LLM 输出特定结构的内容,兼容所有 LLM,但是输出不稳定。(不常用)
  2. 函数/工具调用:让LLM绑定函数,并设置选择模式为强制,让LLM 强制调用函数,从而获取结构化输出数据。
  3. JSON模式:对于支持JSON 模式输出的LLM,还可以通过设置输出结构为 JSON模式,从而获取结构化数据。
  • withStructuredOutput 用 zod 定义参数,强制 gpt 生成结构化的数据
  • withStructuredOutput 内部会生成一个虚拟函数 extract,绑定bindTools ,强制模型调用函数;如果模型支持 json_mode,还会使用模型的json模式(支持的少,忽略)
js 复制代码
async chainWithStructuredOutput() {
    const prompt = await ChatPromptTemplate.fromMessages([
      {
        role: 'system',
        content: `你是OpenAI开发的聊天机器人,请从用户的描述中提取假设性问题和答案`,
      },
      { role: 'user', content: '{question}' },
    ]);

    const outputSchema = z.object({
      question: z.string().describe('假设性问题'),
      answer: z.string().describe('假设性答案'),
    });

    const llm = new ChatOpenAI({
      modelName: 'gpt-3.5-turbo-16k',
      configuration: {
        baseURL: this.configService.get('OPENAI_API_BASE_URL'),
      },
    }).withStructuredOutput(outputSchema, { strict: true });

    const chain = RunnableSequence.from([prompt, llm]);

    const res = await chain.invoke({
      question: '我叫晓晓宝,我今年3岁了',
    });

    console.log('=>res', res);
  }

函数调用出错捕获,回退重试等

  • 思路
    • 出错:调用的函数中包裹一个 try catch,如果出错返回 catch 内容;或者是返回错内容后用另外的一个模型再走一遍
    • 回退:建立两个llmChain,另外一个用更好的模型,如 gpt-4o,用 withFallBack 来让另外的链条重试
    • 重试:携带错误信息,重试策略

Agent

  • 无论一个 Agent设计得多么复杂,使用什么架构,最基础的工作流程其实都非常简单,只有5个步骤:
  1. 输入理解:Agent 首先解析用户输入,理解其意图和需求。
  2. 计划定制:基于对输入的理解,Agent会定制一个执行计划,决定使用哪些工具和执行的顺序。
  3. 工具调用:Agent 按照计划调用相应的工具,执行必要的操作。
  4. 结果整合:收集所有工具返回的结果,进行整合和解析,形成最终的输出。
  5. 反馈循环:如果任务没有完成或者需要进一步的消息,Agent 可以迭代上述过程直到满足条件为止。
  • 一个Agent来说,组成模块有3个部分

    1. Tools: Agent 可以访问的工具集
    2. Executor: 执行 Agent 计划的逻辑
    3. Prompt Templates:指导 Agent 如何理解和处理输入的模板,可以定制化以适应不同的任务
  • ReACT智能体的缺陷

    • 这样 ReACT智能体底层检测到 Final Answer 这个关键词,就可以提取出最终答案,但是LLM 的输出是及其不稳定的,在实际测试中,哪怕是 GPT-40模型,它会输出如下的数据:
    • reactAgent 严格依赖 Prompt 输入的结果,判断输出是否包含 Final Answer ,就结束
    • 一定需要配合 hwchase17/react 这个 prompt 模板
js 复制代码
Answer the following questions as best you can. You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}

内置Agent的使用步骤

  1. 拉取对应的rpompt(一定是要对应的prompt ,智能体强依赖对应的 prompt 的输出),填充所使用的工具tool

如 createReactAgegt => hwchase17/react 的prompt createToolCallingAgent => hwchase17/openai-tools-agent 的prompt

  1. 创建智能体,本质上还是一个 Runnable 链,createToolCallingAgent
  2. 创建智能体执行器,AgentExecutor。本质上弥补了 Runnable 不能循环执行,就去一直执行,直到最后的解析出 stop 出循环的地方
  3. 得到答案,输出(不同的智能体的输出,也是内置了不同的 智能体结果解析器 outputParser)
js 复制代码
 const tools = [this.serpAPI, this.calculator];

    const prompt = await pull<ChatPromptTemplate>(
      'hwchase17/openai-tools-agent',
    );

    //     const prompt = `
    //      System: You are a helpful assistant
    // Human: {input}
    // Human: {agent_scratchpad}`;

    const llm = new ChatOpenAI({
      configuration: {
        baseURL: this.configService.get('OPENAI_API_BASE_URL'),
      },
    });

    const agent = await createToolCallingAgent({
      llm,
      tools,
      prompt,
    });

    const agentExecutor = new AgentExecutor({
      agent,
      tools,
    });

    const res = await agentExecutor.invoke({
      input: 'what is LangChain?',
    });

传统 Agent的缺陷

  • 在LangChain 中,无论是什么类型的 Agent(内置封装),都必须通过 AgentExecutor 来创建执行者才可以运行具有循环+工具执行的智能体,在智能体执行者的底层,实际操作是调用 Agent智能体, 执行它选择的操作/工具,将操作输出传递回 Agent,然后重复,伪代码如下:
  • 传统 Agent还存在不少缺陷,如下:
    1. 只有循环步骤并没有条件步骤,一个 Agent 应用只能一条路走到黑,不能执行不同的路由;
    2. 没法亦或者很难将多个 Agent 融合起来相互协作;
    3. 因对 Prompt与输出解析器的过度封装,导致要修改 Agent 内部的方案变得异常困难;
    4. 无论是 Agent还是 AgentExecutor,因其黑盒机制,无法在执行的过程中进行额外的干预;
    5. 想对 Agent 进行扩展或者动态切换LLM 难度非常大,例如添加记忆、切换LLM等;

ps:如果大家有疑惑的地方,可以私信咨询我哦~旨在帮助前端er入门生产级别的AI编程

相关推荐
混血哲谈1 小时前
全新电脑如何快速安装nvm,npm,pnpm
前端·npm·node.js
win4r4 小时前
🚀多维度测评OpenAI最新GPT-4.1模型!百万token上下文窗口!编程能力和指令遵循能力大幅提升!Cline+GPT-4.1十分钟零代码开发macOS
chatgpt·openai·ai编程
魔云连洲5 小时前
用Webpack 基础配置快速搭建项目开发环境
前端·webpack·node.js
石页玩逆向5 小时前
某企查-某度旋转验证码V2算法分析
node.js
·薯条大王6 小时前
Node.js 模块包的管理和使用是
node.js
十分钟空间6 小时前
MCP(Model Context Protocol)技术与项目集成指南
ai编程·mcp
vil du7 小时前
c# AI编程助手 — Fitten Code
开发语言·c#·ai编程
熊猫片沃子7 小时前
使用nvm解决nodejs多版本问题,难道不香吗?
前端·npm·node.js
Mintopia8 小时前
Node.js 与前端现代工具链进阶指南
前端·javascript·node.js
孟陬10 小时前
Node.js 如何检测 script 脚本是在项目本身运行
node.js·bun