编者按:这篇文章由 Dexter Storey、Sarim Malik和 Ted Spare撰写,他们来自 Rubric Labs 团队。
重要链接
目标
本指南旨在解释用于在生产环境中部署调度 agent Cal.ai 的底层技术和逻辑,该 agent 使用了 LangChain。
背景
最近,我们在 Rubric Labs 的团队有机会构建 Cal.com 的 AI 驱动的电子邮件助手。作为背景介绍,我们是一个由构建者组成的团队,他们喜欢解决具有挑战性的工程问题。
该项目的愿景是简化复杂的日历管理世界。我们想到的部分核心功能包括
- 将非正式电子邮件转换为预订: “想明天下午 2 点见面吗?”
- 列出和重新安排预订: “取消我的下一次会议”
- 回答基本问题: “我的星期二看起来怎么样?”
等等。总而言之,我们希望构建一个个性化的、AI 驱动的调度助手,它可以为最终用户提供一套完整的 CRUD 操作,所有操作都在他们的电子邮件客户端中使用自然语言完成。

架构
我们决定使用 AI agent 来实现这一目标,特别是 OpenAI functions agent,考虑到 Cal.com 的 API 公开了一组具有清晰输入的预订操作,这种 agent 更擅长处理结构化数据。底层架构如下所述。

输入
Cal.com 用户可以向 username@cal.ai
(例如 ted@cal.ai)发送电子邮件,其中包含诸如“你能明天某个时候与 sarim@rubriclabs.com 预订一个会议吗?”之类的请求。
传入的电子邮件在 接收路由中使用 MailParser 进行清理和路由,并且地址通过 DKIM 记录进行验证,使其难以欺骗。
在这里,我们还进行了额外的检查,例如确保电子邮件来自 Cal.com 用户,以防止滥用。在电子邮件经过验证和解析后,它将被传递给 agent 循环。
Agent
该 agent 是 LangChain OpenAI functions agent,它使用 GPT 模型的微调版本。此 agent 被传递预定义的功能或工具,并且能够检测何时应调用功能。这使我们能够指定所需的输入和期望的输出。
该 agent 在 agent 循环中进行了文档记录。
OpenAI functions agent 有两个必需的输入
- 工具 — 工具只是一个 Javascript 函数,具有名称、描述和输入模式
- 聊天模型 — 聊天模型是 LLM 模型的一种变体,它使用“聊天消息”作为输入和输出的接口
除了工具和聊天模型之外,我们还传递一个前缀提示,为模型添加上下文。让我们看一下每个输入。
提示
提示是一种通过提供上下文来编程模型的方法。用户的个人信息(userId
、user.username
、user.timezone
等)用于构建传递给 agent 的提示。
const prompt = `You are Cal.ai - a bleeding edge scheduling assistant that interfaces via email.
Make sure your final answers are definitive, complete and well formatted.
Sometimes, tools return errors. In this case, try to handle the error
intelligently or ask the user for more information.
Tools will always handle times in UTC, but times sent to users should be
formatted per that user's timezone.
The primary user's id is: ${userId}
The primary user's username is: ${user.username}
The current time in the primary user's timezone is: ${now(user.timeZone)}
The primary user's time zone is: ${user.timeZone}
The primary user's event types are: ${user.eventTypes}
The primary user's working hours are: ${user.workingHours}
...
`
在此处查找完整提示。
聊天模型
聊天模型是语言模型的一种变体,它将聊天消息公开为输入和输出。对于此项目,我们使用 OpenAI 的 GPT-4
模型,可以使用 OPENAI_API_KEY
访问它(在此处阅读以生成 API 密钥)。
为了更好地采用,我们知道用户体验必须快如闪电。我们尝试了 gpt-3.5-turbo
,但有趣的是,它通常会以更迂回的方式完成任务,从而花费更长的时间。
温度设置为 0 以生成更一致的输出。较高的温度会导致更具创造性的、不一致的输出。
const model = new ChatOpenAI({
modelName: "gpt-4",
openAIApiKey: env.OPENAI_API_KEY,
temperature: 0,
});
工具
工具由 Javascript 函数、名称、输入模式(使用 Zod)和描述组成,旨在处理结构化数据。
此项目使用的工具类型为 DynamicStructuredTool,它旨在处理结构化数据。动态结构化工具是 LangChain 的 StructuredTool
类的扩展,它基本上使用提供的函数覆盖了 _call
方法。请参阅下面的 DynamicStructuredTool
的模式。
DynamicStructuredTool({
description: "Creates a booking on the primary user's calendar.", // Description
func: createBooking(), // Function
name: "createBooking", // Name
schema: z.object({ // Schema
from: z.string().describe("ISO 8601 datetime string"),
...
}),
});
DynamicStructuredTool 的输入,完整示例可在此处查看
通过创建一套工具,我们为 agent 提供了执行预定义操作的额外能力。此项目使用的所有工具都记录在此处,并在下面列出。每个工具都具有复杂的逻辑,使其能够理解用户的请求,通过执行 CRUD 任务与 Cal.com API 交互,并处理响应。
const tools = [
createBooking(apiKey, userId, users),
deleteBooking(apiKey),
getAvailability(apiKey),
getBookings(apiKey, userId),
sendBookingLink(apiKey, user, users, agentEmail),
updateBooking(apiKey, userId),
];
上面工具的输入是我们不希望 AI agent 访问的变量,因此,它们被注入到工具中,绕过 agent 循环。
使用动态提示和聊天模型,工具解析器能够选择最合适的工具来处理用户的请求,然后在循环中运行,直到它能够满足期望的行为。这遵循迭代学习,甚至允许通过在出现错误时选择备用工具来进行回退。

执行器
最后,工具、聊天模型和提示用于 初始化 agent 并运行执行器以生成响应。
const executor = await initializeAgentExecutorWithOptions(tools, model, {
agentArgs: { prefix: prompt },
agentType: "openai-functions",
verbose: true,
});
响应
在 agent 运行循环,利用不同的工具、与 LLM 模型聊天并与 Cal.com API 交互后,它会结束并得出最终响应。然后,使用电子邮件服务(例如 Sendgrid 或 Resend)将此响应通过电子邮件发送给用户。电子邮件将是确认或错误。
技术栈
结论
很明显,具有限定范围工具的 AI agent 是一种非常强大的组合。将 LLM 模型的知识与结构化工具相结合,有助于解决具有结构化数据的自然语言问题,从而提高这些 agent 的可靠性。我们对 AI agent 的未来发展感到非常兴奋,并希望我们与 Cal.com 和 LangChain 的合作,在生产环境中创建一个开源 agent,将成为未来项目的模板。