函数调用
- 函数调用 =「大语言模型(支持函数调用)+预定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 调用
-
实现自定义函数的两种方法
-
- tool 直接包装现有函数
-
- DynamicTool 写清楚函数名称,描述
-
- 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' });
使用
- 给模型绑定一个 tools,会在调用后生成的结果中,多一个 tool_calls (注意模型不会自动执行函数,他只会返回函数名和传递参数!!!)
js
/**
* "tool_calls": [
* {
* "name": "date-difference-calculator",
* "args": {
* "date1": "2024-03-16",
* "date2": "today"
* },
* "type": "tool_call",
* "id": "call_RNiMIvbyYseWDjt3GObJwbIj"
* }
* ],
*/
- 需要手动判断返回结果,并且自己执行函数,并且一般需要组装 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 结构化输出的策略有:
- Prompt:通过 prompt 让LLM 输出特定结构的内容,兼容所有 LLM,但是输出不稳定。(不常用)
- 函数/工具调用:让LLM绑定函数,并设置选择模式为强制,让LLM 强制调用函数,从而获取结构化输出数据。
- 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个步骤:
- 输入理解:Agent 首先解析用户输入,理解其意图和需求。
- 计划定制:基于对输入的理解,Agent会定制一个执行计划,决定使用哪些工具和执行的顺序。
- 工具调用:Agent 按照计划调用相应的工具,执行必要的操作。
- 结果整合:收集所有工具返回的结果,进行整合和解析,形成最终的输出。
- 反馈循环:如果任务没有完成或者需要进一步的消息,Agent 可以迭代上述过程直到满足条件为止。
-
一个Agent来说,组成模块有3个部分
- Tools: Agent 可以访问的工具集
- Executor: 执行 Agent 计划的逻辑
- 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的使用步骤
- 拉取对应的rpompt(一定是要对应的prompt ,智能体强依赖对应的 prompt 的输出),填充所使用的工具tool
如 createReactAgegt => hwchase17/react 的prompt createToolCallingAgent => hwchase17/openai-tools-agent 的prompt
- 创建智能体,本质上还是一个 Runnable 链,createToolCallingAgent
- 创建智能体执行器,AgentExecutor。本质上弥补了 Runnable 不能循环执行,就去一直执行,直到最后的解析出 stop 出循环的地方
- 得到答案,输出(不同的智能体的输出,也是内置了不同的 智能体结果解析器 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还存在不少缺陷,如下:
- 只有循环步骤并没有条件步骤,一个 Agent 应用只能一条路走到黑,不能执行不同的路由;
- 没法亦或者很难将多个 Agent 融合起来相互协作;
- 因对 Prompt与输出解析器的过度封装,导致要修改 Agent 内部的方案变得异常困难;
- 无论是 Agent还是 AgentExecutor,因其黑盒机制,无法在执行的过程中进行额外的干预;
- 想对 Agent 进行扩展或者动态切换LLM 难度非常大,例如添加记忆、切换LLM等;
ps:如果大家有疑惑的地方,可以私信咨询我哦~旨在帮助前端er入门生产级别的AI编程