LangChain工具使用:简化AI函数调用
本文介绍了如何在LangChain中使用OpenAI的tools功能,通过Zod简化参数定义流程。主要内容包括:1)使用Zod定义工具函数的JSON Schema,包括基础类型、对象、数组等;2)在LangChain中绑定工具到模型,实现单工具和多工具调用;3)控制模型调用工具的行为;4)应用实例:通过tools实现数据打标签和信息提取任务,展示了LLM在自然语言处理任务中的强大能力。文章通过具体
本章对应源代码:https://github.com/RealKai42/langchainjs-juejin/blob/main/lc-tools.ipynb
上一节中,我们学习了如何直接使用 openAI 的原生 API 去使用 function calling (tools)功能,需要自己维护历史、写参数类型并且自己实现函数的调用,确实比较繁琐。这一节,我们将学习在 langchain 中如何使用该功能,会极大的减缓使用门槛,并且很容易集成到现有 chain 中。
同时,我们会讲解几个使用 tools 对数据进行打标签、信息提取等常见的操作
在 langchain 中使用 tools
在 langchain 中,我们一般会使用 zod 来定义 tool 函数的 JSON schema,我们可以专注在参数的描述上,参数的类型定义和是否 required 都可以有 zod 来生成。 并且在后续定义 Agent tool 时,zod 也能进行辅助的参数类型检测。
zod 是 js 生态中常见的类型定义和验证的工具库,我们这里用一些例子简单带大家快速入门一下:
首先是简单的使用,我们订一个 string 类型的 schema:
import { z } from "zod";
const stringSchema = z.string();
stringSchema.parse("Hello, Zod!");
如果我们传入一个非 string 类型的值:
stringSchema.parse(2323);
就会报错
ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": [],
"message": "Expected string, received number"
}
]
报错信息的可读性是非常高的,而且也很适合把报错信息传递给 llm,让它自己纠正错误。
然后,我们用一系列的示例迅速介绍足够我们定义 tool 参数使用的 zod 知识:
// 基础类型
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
// 数组
const stringArraySchema = z.array(z.string());
stringArraySchema.parse(["apple", "banana", "cherry"]);
// 对象
const personSchema = z.object({
name: z.string(),
age: z.number(),
// 可选类型
isStudent: z.boolean().optional(),
// 默认值
home: z.string().default("no home")
});
// 联合类型
const mixedTypeSchema = z.union([z.string(), z.number()]);
mixedTypeSchema.parse("hello");
mixedTypeSchema.parse(42);
考虑到方便 llm 理解和传递参数,一般不建议定义过于复杂的类型,会让 llm 容易犯错。
然后,我们就可以用 zod 去定义我们函数参数的 schem,例如以上一节课中获取天气的函数为例:
const getCurrentWeatherSchema = z.object({
location: z.string().describe("The city and state, e.g. San Francisco, CA"),
unit: z.enum(["celsius", "fahrenheit"]).describe("The unit of temperature"),
});
这里我们定义了两个参数:
- location 是 string 类型,并且添加描述
- unit 是枚举类型,并添加相应的描述
这里我们没有指定 optional,默认就是 required,我们可以使用 zod-to-json-schema 去将 zod 定义的 schema 转换成 JSON schema:
import { zodToJsonSchema } from "zod-to-json-schema";
const paramSchema = zodToJsonSchema(getCurrentWeatherSchema)
就可以将上面我们定义的 schema 转换成 openAI tools 所需要的 JSON Schema :
{
type: "object",
properties: {
location: {
type: "string",
description: "The city and state, e.g. San Francisco, CA"
},
unit: {
type: "string",
enum: [ "celsius", "fahrenheit" ],
description: "The unit of temperature"
}
},
required: [ "location", "unit" ],
additionalProperties: false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
然后,我们就可以在 model 去使用这个 tool 定义:
const model = new ChatOpenAI({
temperature: 0
})
const modelWithTools = model.bind({
tools: [
{
type: "function",
function: {
name: "getCurrentWeather",
description: "Get the current weather in a given location",
parameters: zodToJsonSchema(getCurrentWeatherSchema),
}
}
]
})
await modelWithTools.invoke("北京的天气怎么样");
这里就会返回一个 AIMessage 信息,并携带着跟 tool call 有关的信息:
AIMessage {
lc_serializable: true,
lc_kwargs: {
content: "",
additional_kwargs: {
function_call: undefined,
tool_calls: [
{
function: [Object],
id: "call_IMLAkWEhmOyh6T9vYMv65uEP",
type: "function"
}
]
},
response_metadata: {}
},
lc_namespace: [ "langchain_core", "messages" ],
content: "",
name: undefined,
additional_kwargs: {
function_call: undefined,
tool_calls: [
{
function: {
arguments: '{\n "location": "北京",\n "unit": "celsius"\n}',
name: "getCurrentWeather"
},
id: "call_IMLAkWEhmOyh6T9vYMv65uEP",
type: "function"
}
]
},
response_metadata: {
tokenUsage: { completionTokens: 23, promptTokens: 88, totalTokens: 111 },
finish_reason: "tool_calls"
}
}
跟我们之前直接使用 openai 的 API 的结果是类似的,增加了更多 langchain 内部使用的信息。
这里的 bind 并不是 model 特有的一个工具,是所有 Runnable 都有的方法,可以将 runnable 需要的参数传入,然后返回一个只需要其他参数的 Runnable 对象。
因为绑定 tools 后的 model 依旧是 Runnable 对象,所以我们可以很方便的把它加入到 LCEL 链中:
import { ChatPromptTemplate } from "@langchain/core/prompts";
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant"],
["human", "{input}"]
])
const chain = prompt.pipe(modelWithTools)
await chain.invoke({
input: "北京的天气怎么样"
});
多 tools model
同样的,我们也可以在 model 中去绑定多个 tools,就像直接使用 openai 的 API 类似:
const getCurrentTimeSchema = z.object({
format: z
.enum(["iso", "locale", "string"])
.optional()
.describe("The format of the time, e.g. iso, locale, string"),
});
zodToJsonSchema(getCurrentTimeSchema)
注意,这里我们对参数使用了 optional 工具函数,就输出的 json scheme 中就不会将这个参数标志为 required
{
type: "object",
properties: {
format: {
type: "string",
enum: [ "iso", "locale", "string" ],
description: "The format of the time, e.g. iso, locale, string"
}
},
additionalProperties: false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
然后,使用多个 tools 的代码也是类似,modelWithMultiTools 就会根据用户的输入和上下文去调用合适的 function:
const model = new ChatOpenAI({
temperature: 0
})
const modelWithMultiTools = model.bind({
tools: [
{
type: "function",
function: {
name: "getCurrentWeather",
description: "Get the current weather in a given location",
parameters: zodToJsonSchema(getCurrentWeatherSchema)
}
},
{
type: "function",
function: {
name: "getCurrentTime",
description: "Get the current time in a given format",
parameters: zodToJsonSchema(getCurrentTimeSchema)
}
}
]
})
控制 model 对 tools 的调用
我们也可以像使用 API 一样通过 tool_choice 去控制 llm 调用函数的行为:
model.bind({
tools: [
...
],
tool_choice: "none"
})
或者强制调用某个函数:
const modelWithForce = model.bind({
tools: [
...
],
tool_choice: {
type: "function",
function: {
name: "getCurrentWeather"
}
}
})
使用 tools 给数据打标签
在数据预处理时,给数据打标签是非常常见的操作。例如之前我们会使用 jieba 这个 python 库对评论进情感打分,找出评论中含有恶意的部分。
而有了大模型后,跟自然语言相关的绝大部分任务都可以使用 llm 来代替,而且得益于 llm 展现出来非常强大的跨语言理解能力,我们的工具可以是针对任何语言,也可以让 llm 去分辨使用的是什么语言。这些任务在 llm 之前都需要非常复杂的实现才能达到的。
我们首先定义提取信息的函数 scheme :
const taggingSchema = z.object({
emotion:z.enum(["pos", "neg", "neutral"]).describe("文本的情感"),
language: z.string().describe("文本的核心语言(应为ISO 639-1代码)"),
});
这里,我们会核心强调是提取文本中的核心语言,来应对部分中英混杂的情况,如果对语言标记的准确性非常看重,可以在这里加入更多的描述,例如占比 50% 以上的主体语言。
然后,我们将 tool bind 给 model,注意在 tagging 任务中,需要设置为强制调用这个函数,来保证对任何输入 llm 都会执行 tagging 的函数:
const model = new ChatOpenAI({
temperature: 0
})
const modelTagging = model.bind({
tools: [
{
type: "function",
function: {
name: "tagging",
description: "为特定的文本片段打上标签",
parameters: zodToJsonSchema(taggingSchema)
}
}
],
tool_choice: {
type: "function",
function: {
name: "tagging"
}
}
})
然后,我们使用这个 model 去组合成 chain:
import { JsonOutputToolsParser } from "@langchain/core/output_parsers/openai_tools";
const prompt = ChatPromptTemplate.fromMessages([
["system", "仔细思考,你有充足的时间进行严谨的思考,然后按照指示对文本进行标记"],
["human", "{input}"]
])
const chain = prompt.pipe(modelTagging).pipe(new JsonOutputToolsParser())
这里我们也用到了 system prompt 常用的技巧,就是 “仔细思考” 、“你有充足的时间进行严谨的思考”,有论文验证过,这些词有点像 magic word 一样,加入后就能明显提升输出的质量,越来越玄学了。
注意这里,我们并没有必要去实现 taggingSchema 所对应的函数,因为我们需要的就是 llm 输出的 json 标签,所以我们使用 JsonOutputToolsParser 直接拿到 tools 的 json 输出即可。
我们可以测试一下:
await chain.invoke({
input: "hello world"
})
// [ { type: "tagging", args: { emotion: "neutral", language: "en" } } ]
await chain.invoke({
input: "写代码太难了,👴 不干了"
})
// [ { type: "tagging", args: { emotion: "neg", language: "zh" } } ]
await chain.invoke({
// 日语,圣诞快乐
input: "メリークリスマス!"
})
// [ { type: "tagging", args: { emotion: "pos", language: "ja" } } ]
await chain.invoke({
input: "我非常喜欢 AI,特别是 LLM,因为它非常 powerful"
})
// [ { type: "tagging", args: { emotion: "pos", language: "zh" } } ]
可以看到,因为我们声明了提取数据中的核心语言,即使是最后一个例子这种混杂的情况,也能提取到正确的信息。
在这里展现的就是 llm zero-shot learning 的能力,即对于新任务只需要 prompt 的描述,甚至不需要给出任务实例 或者使用一部分数据进行训练,即可以完成任务。
使用 tools 进行信息提取
我们再看 tools 另一个常见的应用,信息的提取。信息提取和打标记类似,如果从学术角度可能有一些区别,但在我们实际工程上没必要做太大的区分。感受上就是打标签是给数据打上给定的一些标记,而信息提取是 llm 理解原始文本后提取其中的信息,类似于我们常用的粘贴快递地址,就自动提取姓名、手机和地址一样。
在信息提取时,一般是会提取多个信息,类似于一段文本中涉及到多个对象的内容,一次性都提取出来。
让我们先定描述一个人的信息 scheme:
const personExtractionSchema = z.object({
name: z.string().describe("人的名字"),
age: z.number().optional().describe("人的年龄")
}).describe("提取关于一个人的信息");
这里 age 我们设计成可选的 number,因为年龄可能是没有的,避免 llm 硬编一个。我们通过对整个 object 添加 describe,让 llm 对整个对象有更多理解。
然后,我们基于这个去构造更上层的 scheme,从信息中提取更复杂信息:
const relationExtractSchema = z.object({
people: z.array(personExtractionSchema).describe("提取所有人"),
relation: z.string().describe("人之间的关系, 尽量简洁")
})
这里我们复用 personExtractionSchema 去构建数组的 scheme,去提取信息中多人的信息,并且提取文本中人物之间的关系。
得益于 llm 良好的语言能力,我们只需要有简单的 prompt 就让 llm 在信息提取任务上有很好的表现。我们看一下这个复杂的 scheme 转换后的结果:
const schema = zodToJsonSchema(relationExtractSchema)
{
type: "object",
properties: {
people: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string", description: "人的名字" },
age: { type: "number", description: "人的年龄" }
},
required: [ "name" ],
additionalProperties: false,
description: "提取关于一个人的信息"
},
description: "提取所有人"
},
relation: { type: "string", description: "人之间的关系, 尽量简洁" }
},
required: [ "people", "relation" ],
additionalProperties: false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
然后我们把这个 schema 构建成 chain :
const model = new ChatOpenAI({
temperature: 0
})
const modelExtract = model.bind({
tools: [
{
type: "function",
function: {
name: "relationExtract",
description: "提取数据中人的信息和人的关系",
parameters: zodToJsonSchema(relationExtractSchema)
}
}
],
tool_choice: {
type: "function",
function: {
name: "relationExtract"
}
}
})
const prompt = ChatPromptTemplate.fromMessages([
["system", "仔细思考,你有充足的时间进行严谨的思考,然后提取文中的相关信息,如果没有明确提供,请不要猜测,可以仅提取部分信息"],
["human", "{input}"]
])
const chain = prompt.pipe(modelExtract).pipe(new JsonOutputToolsParser())
这里 prompt 设计,我们使用 仔细思考,你有充足的时间进行严谨的思考 去增强 llm 输出的质量,然后用 如果没有明确提供,请不要猜测,可以仅提取部分信息 来减少 llm 的幻想问题。
然后我们先测试一下简单的任务:
await chain.invoke({
input: "小明现在 18 岁了,她妈妈是小丽"
})
[
{
type: "relationExtract",
args: {
people: [ { name: "小明", age: 18 }, { name: "小丽", age: null } ],
relation: "小丽是小明的妈妈"
}
}
]
这里数据中并没有小丽的年龄,所以 llm 直接留空,并没有强行提取信息。
因为 llm 是根据自己对语言的理解能力,而不是根据传统的匹配规则等,所以在语意中隐含的信息也有良好的提取能力:
await chain.invoke({
input: "我是小明现在 18 岁了,我和小 A、小 B 是好朋友,都一样大"
})
[
{
type: "relationExtract",
args: {
people: [
{ name: "小明", age: 18 },
{ name: "小A", age: 18 },
{ name: "小B", age: 18 }
],
relation: "小明是小A和小B的好朋友"
}
}
]
对于 edge case,也有较好的处理效果:
await chain.invoke({
input: "我是小明"
})
[
{
type: "relationExtract",
args: { people: [ { name: "小明", age: null } ], relation: "" }
}
]
小结
这一节我们学习了如何在 langchain 中使用 openAI tools,通过 zod 减少了我们编写 schema 的繁琐。更重要的,我们学习了如何使用 tools 对数据进行打标签和数据提取,这意味着 llm 并不只是一个 chat bot 的用处,我们可以把他融入在日常的很多数据处理任务中,替代传统很多需要复杂编码才能解决的问题。
更多推荐


所有评论(0)